diff --git a/.changeset/config.json b/.changeset/config.json index 742b881..988dfe9 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,17 +1,17 @@ -{ - "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", - "changelog": [ - "@svitejs/changesets-changelog-github-compact", - { "repo": "TanStack/pacer" } - ], - "commit": false, - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "fixed": [], - "linked": [], - "ignore": [], - "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { - "onlyUpdatePeerDependentsWhenOutOfRange": true - } -} +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", + "changelog": [ + "@svitejs/changesets-changelog-github-compact", +{ "repo": "TanStack/playbooks" } + ], + "commit": false, + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "fixed": [], + "linked": [], + "ignore": [], + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + } +} diff --git a/.github/workflows/process-feedback.yml b/.github/workflows/process-feedback.yml new file mode 100644 index 0000000..096986e --- /dev/null +++ b/.github/workflows/process-feedback.yml @@ -0,0 +1,71 @@ +name: Process Feedback + +on: + discussion: + types: [created] + +jobs: + label: + name: Auto-label feedback + if: github.event.discussion.category.name == 'Feedback' + runs-on: ubuntu-latest + permissions: + discussions: write + steps: + - name: Label by library + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.discussion.body || '' + const libraryPatterns = [ + { pattern: /\brouter\//, label: 'feedback:router' }, + { pattern: /\bstart\//, label: 'feedback:start' }, + { pattern: /\bquery\//, label: 'feedback:query' }, + { pattern: /\btable\//, label: 'feedback:table' }, + { pattern: /\bform\//, label: 'feedback:form' }, + { pattern: /\bvirtual\//, label: 'feedback:virtual' }, + { pattern: /\bstore\//, label: 'feedback:store' }, + { pattern: /\bdb\//, label: 'feedback:db' }, + { pattern: /\bcompositions\//, label: 'feedback:compositions' }, + ] + + const labels = libraryPatterns + .filter(({ pattern }) => pattern.test(body)) + .map(({ label }) => label) + + if (labels.length === 0) return + + // Ensure labels exist + for (const label of labels) { + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + }) + } catch { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + color: '7057ff', + }) + } + } + + // Discussions use the GraphQL API for labels + const { data: discussion } = await github.graphql(` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `, { + owner: context.repo.owner, + repo: context.repo.repo, + number: context.payload.discussion.number, + }) + + core.info(`Matched labels: ${labels.join(', ')}`) diff --git a/.github/workflows/skill-staleness-check.yml b/.github/workflows/skill-staleness-check.yml new file mode 100644 index 0000000..83d18bd --- /dev/null +++ b/.github/workflows/skill-staleness-check.yml @@ -0,0 +1,86 @@ +name: Skill Staleness Check + +# Triggered by repository_dispatch from upstream TanStack package repos. +# Each package repo has a notify-playbook.yml workflow that fires this +# event on merge to main with the list of changed files. + +on: + repository_dispatch: + types: [skill-check] + + # Manual trigger for testing or ad-hoc checks + workflow_dispatch: + inputs: + package: + description: 'Package name (e.g. @tanstack/query)' + required: true + type: string + library: + description: 'Library directory in skills/ (e.g. query, db, router)' + required: true + type: string + +jobs: + check-staleness: + name: Check skill staleness + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm install yaml + + - name: Build prompt from payload + id: prompt + run: | + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + PACKAGE="${{ github.event.client_payload.package }}" + SHA="${{ github.event.client_payload.sha }}" + CHANGED_FILES='${{ toJson(github.event.client_payload.changed_files) }}' + else + PACKAGE="${{ inputs.package }}" + SHA="manual" + CHANGED_FILES="[]" + fi + + LIBRARY="${{ inputs.library || '' }}" + if [ -z "$LIBRARY" ]; then + # Derive library from package name: @tanstack/query -> query + LIBRARY=$(echo "$PACKAGE" | sed 's/@tanstack\///' | sed 's/react-//' | sed 's/vue-//' | sed 's/solid-//' | sed 's/svelte-//' | sed 's/angular-//') + fi + + cat < /tmp/oz-prompt.txt + Read the meta skill at meta/skill-staleness-check/SKILL.md and follow + its instructions. + + Webhook payload: + - package: ${PACKAGE} + - sha: ${SHA} + - changed_files: ${CHANGED_FILES} + - library: ${LIBRARY} + + Run the staleness check for the ${LIBRARY} library. Use + scripts/sync-skills.mjs for detection, then evaluate whether any + skills need updating. If they do, update them and open a PR. + If nothing needs updating, exit silently. + EOF + + echo "library=$LIBRARY" >> "$GITHUB_OUTPUT" + + - name: Run Oz agent + uses: warpdotdev/oz-agent-action@main + id: oz + with: + prompt: | + $(cat /tmp/oz-prompt.txt) + warp_api_key: ${{ secrets.WARP_API_KEY }} + profile: ${{ vars.WARP_AGENT_PROFILE || '' }} diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 0000000..be8b0f2 --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,27 @@ +name: Validate Skills + +on: + pull_request: + paths: + - 'packages/playbooks/skills/**' + - 'packages/playbooks/package_map.yaml' + - 'scripts/validate-skills.ts' + +jobs: + validate: + name: Validate skill files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm install yaml + + - name: Run validation + run: npx tsx scripts/validate-skills.ts diff --git a/.warp/automations/skill-check.yml b/.warp/automations/skill-check.yml new file mode 100644 index 0000000..76e2aa1 --- /dev/null +++ b/.warp/automations/skill-check.yml @@ -0,0 +1,42 @@ +# Warp Oz Automation — Skill Staleness Check +# +# Triggered by repository_dispatch from upstream TanStack package repos. +# Oz evaluates skill staleness, rewrites stale skills, and opens PRs. +# +# Trigger: .github/workflows/skill-staleness-check.yml +# Skill: meta/skill-staleness-check/SKILL.md +# Scripts: scripts/sync-skills.mjs + +name: Skill Staleness Check +trigger: webhook + +# The meta skill that drives the Oz agent's behavior +skill: meta/skill-staleness-check/SKILL.md + +# Supporting meta skills the agent may load during execution +supporting_skills: + - meta/generate-skill/SKILL.md + - meta/tree-generator/SKILL.md + +# Repos the agent needs access to (read for source, write for playbooks) +environments: + - repo: tanstack/playbooks + access: write + - repo: TanStack/query + access: read + - repo: TanStack/router + access: read + - repo: TanStack/db + access: read + - repo: TanStack/form + access: read + - repo: TanStack/table + access: read + +# Payload schema (received from package repo webhook) +# +# { +# "package": "@tanstack/query", +# "sha": "abc123def", +# "changed_files": ["docs/guides/queries.md", "src/query.ts"] +# } diff --git a/README.md b/README.md index 7cc3382..f877c7e 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# TanStack Agents +# @tanstack/playbook diff --git a/skills/domain-discovery/SKILL.md b/meta/domain-discovery/SKILL.md similarity index 100% rename from skills/domain-discovery/SKILL.md rename to meta/domain-discovery/SKILL.md diff --git a/meta/generate-skill/SKILL.md b/meta/generate-skill/SKILL.md new file mode 100644 index 0000000..4cf5fcf --- /dev/null +++ b/meta/generate-skill/SKILL.md @@ -0,0 +1,358 @@ +--- +name: skill-generate +description: > + Generate a complete SKILL.md file for the @tanstack/playbooks repo from + source documentation and domain map artifacts. Activate when bootstrapping + skills for a new library, regenerating a stale skill after source changes, + or producing a skill from a domain_map.yaml entry. Takes a skill name, + description, and source docs as inputs; outputs a validated SKILL.md that + conforms to the tree-generator spec. +metadata: + version: "1.0" + category: meta-tooling + input_artifacts: + - domain_map.yaml + - skill_spec.md + - source documentation + output_artifacts: + - SKILL.md + skills: + - skill-tree-generator + - skill-domain-discovery +--- + +# Skill Generation + +You are generating a SKILL.md file for the `@tanstack/playbooks` agent skills +repo. Skills in this repo are written for coding agents (Claude Code, Cursor, +Copilot, Warp Oz, Codex), not for human readers. Your output will be loaded +into an agent's context window and used to guide code generation. + +There are two modes. Detect which applies. + +**Mode A — Generate from domain map:** A `domain_map.yaml` and `skill_spec.md` +exist. Generate the skill specified by name from these artifacts plus the +source documentation they reference. + +**Mode B — Generate from raw docs:** No domain map exists. Generate directly +from source documentation provided as input. + +--- + +## Inputs + +You will receive: + +1. **Skill name** — format `library-group/skill-name` (e.g. `tanstack-query/core`, + `tanstack-router/loaders`, `db/core/live-queries`) +2. **Skill description** — what the skill covers and when an agent should load it +3. **Source documentation** — the docs, guides, API references, and/or source + files to distill from +4. **Domain map entry** (Mode A only) — the skill's entry from `domain_map.yaml` + including failure modes, subsystems, compositions, and source references + +--- + +## Step 1 — Determine skill type + +Read the inputs and classify the skill type: + +| Type | When to use | +|------|-------------| +| `core` | Framework-agnostic concepts, configuration, patterns | +| `sub-skill` | A focused sub-topic within a core or framework skill | +| `framework` | Framework-specific bindings, hooks, components | +| `lifecycle` | Cross-cutting developer journey (getting started, go-live) | +| `composition` | Integration between two or more libraries | +| `security` | Audit checklist or security validation | + +The skill type determines the frontmatter and body structure. See +skill-tree-generator for the full spec of each type. + +--- + +## Step 2 — Extract content from sources + +Read through the source documentation. Extract only what a coding agent +cannot already know: + +### What to extract + +- **API shapes** — function signatures, hook parameters, option objects, + return types. Use the actual TypeScript types from source. +- **Setup patterns** — minimum viable initialization code +- **Primary patterns** — the 2–4 most important usage patterns +- **Configuration** — defaults that matter, options that change behavior +- **Failure modes** — patterns that look correct but break. Prioritize: + - Migration-boundary mistakes (old API that agents trained on older data produce) + - Silent failures (no crash, wrong behavior) + - Framework-specific gotchas (hydration, hook rules, provider ordering) +- **Constraints and invariants** — ordering requirements, lifecycle rules, + things enforced by runtime assertions + +### What NOT to extract + +- TypeScript basics, React hooks concepts, general web dev knowledge +- Marketing copy, motivational prose, "why this library is great" +- Exhaustive API tables (move these to `references/` if needed) +- Content that duplicates another skill (reference it instead) + +--- + +## Step 3 — Write the frontmatter + +### Core skill frontmatter + +```yaml +--- +name: [library]/[skill-name] +description: > + [1–3 sentences. What this skill covers and exactly when an agent should + load it. Written for the agent — include the keywords an agent would + encounter when it needs this skill. Dense routing key.] +type: core +library: [library] +library_version: "[version this targets]" +sources: + - "[Owner/repo]:docs/[path].md" + - "[Owner/repo]:src/[path].ts" +--- +``` + +### Sub-skill frontmatter + +```yaml +--- +name: [library]/[parent]/[skill-name] +description: > + [1–3 sentences. What this sub-topic covers and when to load it.] +type: sub-skill +library: [library] +library_version: "[version]" +sources: + - "[Owner/repo]:docs/[path].md" +--- +``` + +### Framework skill frontmatter + +```yaml +--- +name: [library]/[framework] +description: > + [1–3 sentences. Framework-specific bindings. Name the hooks, components, + providers.] +type: framework +library: [library] +framework: [react | vue | solid | svelte | angular] +library_version: "[version]" +requires: + - [library]/core +sources: + - "[Owner/repo]:docs/framework/[framework]/[path].md" +--- +``` + +### Frontmatter rules + +- `description` must be written so the agent loads this skill at the right + time — not too broad (triggers on everything) and not too narrow (never + triggers). Pack with function names, option names, concept keywords. +- `sources` uses the format `Owner/repo:relative-path`. Glob patterns are + supported (e.g. `TanStack/query:docs/framework/react/guides/*.md`). +- `library_version` is the version of the source library this skill targets. +- `requires` lists skills that must be loaded before this one. + +--- + +## Step 4 — Write the body + +### Standard body (core, sub-skill, framework) + +Follow this section order exactly: + +**1. Dependency note** (framework and sub-skills only) + +```markdown +This skill builds on [parent-skill]. Read it first for foundational concepts. +``` + +**2. Setup** + +A complete, copy-pasteable code block showing minimum viable usage. +- Real package imports with exact names (`@tanstack/react-query`, not `react-query`) +- No `// ...` or `[your code here]` — complete and runnable +- No unnecessary boilerplate — include exactly the context needed +- For framework skills: framework-specific setup (provider, hook wiring) +- For core skills: framework-agnostic setup (no hooks, no components) + +**3. Core Patterns** (or "Hooks and Components" for framework skills) + +2–4 patterns. For each: +- One-line heading: what it accomplishes +- Complete code block +- One sentence of explanation only if not self-explanatory + +**4. Common Mistakes** + +Minimum 3 entries. Complex skills target 5–6. Format: + +```markdown +### [PRIORITY] [What goes wrong — 5–8 word phrase] + +Wrong: +```[lang] +// code that looks correct but isn't +``` + +Correct: +```[lang] +// code that works +``` + +[One sentence: the specific mechanism by which the wrong version fails.] + +Source: [doc page or source file:line] +``` + +Priority levels: +- **CRITICAL** — Breaks in production. Security risk or data loss. +- **HIGH** — Incorrect behavior under common conditions. +- **MEDIUM** — Incorrect under specific conditions or edge cases. + +Every mistake must be: +- **Plausible** — an agent would generate it +- **Silent** — no immediate crash +- **Grounded** — traceable to a doc page, source file, or issue + +If the domain map includes failure modes with a `skills` list naming +multiple skills, include those failure modes in every SKILL file listed. + +**5. References** (only when needed) + +```markdown +## References + +- [Full option reference](references/options.md) +``` + +Create reference files when the skill would exceed 500 lines, when the +domain covers 3+ independent adapters/backends, or when a topic has >10 +distinct API patterns. + +### Checklist body (security, go-live, audit) + +Use when the primary action is "check these things" not "learn patterns": + +```markdown +# [Library Name] — [Security | Go-Live] Checklist + +Run through each section before [deploying | releasing]. + +## [Category] Checks + +### Check: [what to verify] + +Expected: +```[lang] +// correct configuration +``` + +Fail condition: [what indicates this check failed] +Fix: [one-line remediation] + +## Common Security Mistakes + +[Wrong/correct pairs, same format as standard Common Mistakes] + +## Pre-Deploy Summary + +- [ ] [Verification 1] +- [ ] [Verification 2] +``` + +--- + +## Step 5 — Validate + +Run every check before outputting. Fix any failures. + +| Check | Rule | +|-------|------| +| Under 500 lines | Move excess to references/ | +| Real imports in every code block | Exact package name, correct adapter | +| No external concept explanations | No "TypeScript is...", no "React hooks are..." | +| No marketing prose | No "powerful", "elegant", "best-in-class" | +| Every code block is complete | Works without modification when pasted | +| Common Mistakes are silent | Not obvious compile errors | +| Common Mistakes are library-specific | Not generic TS/React mistakes | +| Common Mistakes are sourced | Traceable to doc or source | +| `name` matches expected directory path | `db/core/live-queries` → `db/core/live-queries/SKILL.md` | +| `sources` filled for sub-skills | At least one Owner/repo:path | +| Framework skills have `requires` | Lists core dependency | +| Framework skills open with dependency note | First prose line references core | +| Description is a dense routing key | Not a human summary — agent-facing | + +--- + +## Step 6 — Output + +Output the complete SKILL.md file content. If reference files are needed, +output those as well with their relative paths. + +If generating multiple skills in a batch (e.g. all skills for a library), +output in this order: + +1. Core overview SKILL.md +2. Core sub-skills in domain order +3. Framework overview SKILL.md for each framework +4. Framework sub-skills +5. Composition skills +6. Security/checklist skills +7. Reference files + +--- + +## Regeneration mode + +When regenerating a stale skill (triggered by skill-staleness-check): + +1. Read the existing SKILL.md and the source diff that triggered staleness +2. Determine which sections are affected by the change +3. Update only affected sections — preserve all other content +4. If a breaking change occurred, add the old pattern as a new Common + Mistake entry (wrong/correct pair) +5. Bump `library_version` in frontmatter +6. Validate the complete file against Step 5 checks + +Do not rewrite the entire skill for a minor source change. Surgical +updates preserve review effort and reduce diff noise. + +--- + +## Constraints + +| Rule | Detail | +|------|--------| +| React adapter only (Phase 1) | No Vue, Solid, Svelte, Angular examples unless generating a framework skill for that adapter | +| All imports use real package names | `@tanstack/react-query`, not `react-query` | +| No placeholder code | No `// ...`, `[your value]`, or `...rest` | +| Agent-first writing | Only write what the agent cannot already know | +| Examples are minimal | No unnecessary boilerplate or wrapper components | +| Failure modes are high-value | Focus on plausible-but-broken, not obvious errors | + +--- + +## Cross-model compatibility + +Output is consumed by all major AI coding agents. To ensure consistency: + +- Markdown with YAML frontmatter — universally parsed +- No XML tags in generated skill content +- Code blocks use triple backticks with language annotation +- Section boundaries use ## headers +- Descriptions are keyword-packed for routing +- Examples show concrete values, never placeholders +- Positive instructions ("Use X") over negative ("Don't use Y") +- Critical info at start or end of sections (not buried in middle) +- Each SKILL.md is self-contained except for declared `requires` diff --git a/meta/skill-staleness-check/SKILL.md b/meta/skill-staleness-check/SKILL.md new file mode 100644 index 0000000..f05c4de --- /dev/null +++ b/meta/skill-staleness-check/SKILL.md @@ -0,0 +1,275 @@ +--- +name: skill-staleness-check +description: > + Evaluate playbook skills for staleness when source files change in upstream + TanStack package repos. Driven by Oz automation on webhook trigger. Matches + changed files against metadata.sources, evaluates whether diffs affect + documented behavior, rewrites stale skills using skill-generate, checks + cross-skill references, and opens PRs. Silent when nothing needs updating. +metadata: + version: "1.0" + category: meta-tooling + input_artifacts: + - webhook payload (package name, commit SHA, changed files) + output_artifacts: + - updated SKILL.md files + - pull requests + skills: + - skill-generate + - skill-tree-generator +--- + +# Skill Staleness Check + +You are an Oz automation agent. Your job is to evaluate whether playbook +skills are stale after upstream source changes, and if so, update them and +open PRs. You act autonomously end-to-end. PRs contain already-updated +skill content, not suggestions. + +If nothing needs updating, exit silently. No PR, no notification. + +--- + +## Inputs + +Webhook payload from an upstream package repo merge to main: + +```json +{ + "package": "@tanstack/query", + "sha": "abc123", + "changed_files": ["docs/framework/react/guides/queries.md", "src/query.ts"] +} +``` + +--- + +## Step 1 — Match changed files to skills + +Read all SKILL.md files under `packages/playbooks/skills/`. For each skill, +extract `sources` from the frontmatter. + +Match `changed_files` from the webhook against `sources` entries across all +skills. Source references use the format `Owner/repo:relative-path` and +support glob patterns. + +A skill is a **candidate** if any of its `sources` entries match a changed +file. + +If no skills match, exit silently. + +### Using sync-skills.mjs + +The repo includes `scripts/sync-skills.mjs` for programmatic staleness +detection. For a given library: + +```bash +node scripts/sync-skills.mjs +``` + +This checks: +- Source file SHA drift (compares stored SHAs in `sync-state.json` against + current remote SHAs via GitHub API) +- Library version drift (frontmatter `library_version` vs current published + version) +- Tree-generator changes (whether the meta skill has been updated since + last sync) + +Use `--report` to write a structured `staleness_report.yaml`: + +```bash +node scripts/sync-skills.mjs --report +``` + +The report classifies skills as needing regeneration (source changed) or +version bump only. + +--- + +## Step 2 — Evaluate each candidate + +For each matched skill: + +1. Read the current SKILL.md content +2. Fetch the file diff from the triggering commit in the source repo +3. Classify the change: + +| Classification | Criteria | Action | +|----------------|----------|--------| +| **No impact** | Diff is typo fix, comment change, test-only, or internal refactor with no API/behavior change | Skip — no update needed | +| **Version bump only** | Diff changes version numbers, dependency ranges, or metadata but no documented behavior | Bump `library_version` in frontmatter | +| **Content update** | Diff changes API shape, behavior, defaults, types, or patterns that the skill documents | Rewrite affected sections | +| **Breaking change** | Diff removes, renames, or fundamentally changes an API the skill documents | Rewrite + add old pattern as Common Mistake | + +### Two-pass classification + +**Pass 1 — Quick scan:** Read the diff summary (files changed, insertions, +deletions). Identify which skill sections could be affected. + +**Pass 2 — Detail evaluation:** For each potentially affected section, read +the full diff hunks and compare against the skill content. Determine if the +change actually affects what the skill documents. + +This prevents over-updating. A 200-line diff to a source file may only +affect one line of one skill, or none at all. + +--- + +## Step 3 — Update stale skills + +For skills classified as needing content updates: + +1. Load the skill-generate meta skill +2. Provide it with: + - The existing SKILL.md content + - The source diff + - The current source documentation (fetch the updated file) +3. Use regeneration mode (surgical update, not full rewrite) +4. Validate the updated skill against all checks + +For version bump only: + +```bash +node scripts/sync-skills.mjs --bump-version +``` + +This updates `library_version` in all frontmatter for the library and +records the new version in `sync-state.json`. + +--- + +## Step 4 — Check cross-skill references + +After updating skills in Step 3, check for cross-skill staleness: + +1. For each skill that was updated, read its `name` +2. Scan all other skills for `requires` entries or `sources` that reference + the updated skill +3. For each skill that references an updated skill, evaluate whether the + update makes the referencing skill stale or inconsistent +4. If stale → update using the same process as Step 3 +5. If not → skip + +This cascade is bounded to **one level**. Skills that reference a +second-order dependency are not automatically re-checked. + +--- + +## Step 5 — Mark skills as synced + +After updating, mark the affected skills as synced so future staleness +checks have a clean baseline: + +```bash +# Mark specific skills +node scripts/sync-skills.mjs --mark-synced + +# Mark all skills for a library +node scripts/sync-skills.mjs --mark-synced --all +``` + +This updates `sync-state.json` with current source file SHAs, the +tree-generator SHA, and the sync timestamp. + +--- + +## Step 6 — Open PRs + +For each skill (or group of skills) that was updated: + +1. Create branch: `skill-update/-` +2. Commit updated SKILL.md file(s) +3. Open PR with structured body + +### PR format + +**Title:** `skill: update (@)` + +**Body:** + +```markdown +### Triggered by +Changes to: + +### What changed in the source + + +### What changed in the skill + + +### Cross-skill impact + + +### Review checklist +- [ ] Skill content is accurate +- [ ] Code examples are complete and copy-pasteable +- [ ] No other skills need corresponding updates +- [ ] Under 500 lines +``` + +### Grouping PRs + +- If multiple skills for the same library are affected by the same commit, + group them in a single PR +- If a cross-skill update is needed (Step 4), open a separate PR for the + downstream skill to keep review scopes clean +- Never mix skills from different libraries in the same PR + +--- + +## No-op behavior + +Exit silently (no PR, no notification, no issue) when ANY of these are true: + +- No changed files match any skill's `sources` +- All matched diffs are classified as "no impact" in Step 2 +- The sync-skills.mjs report shows all skills are current + +--- + +## Operational notes + +### GitHub API usage + +The `sync-skills.mjs` script uses the `gh` CLI for GitHub API access. It +requires: +- `gh` CLI installed and authenticated +- Read access to upstream TanStack package repos (query, router, db, form, + table) +- Write access to the playbooks repo for creating branches and PRs + +### Rate limiting + +When checking multiple libraries or many source files, the script makes +one API call per source file per skill. For large batches, the GitHub API +rate limit (5000 requests/hour for authenticated users) may apply. The +script does not currently batch or cache API responses — if this becomes +an issue, add caching at the `getRemoteFileSha` level. + +### Manual triggering + +Maintainers can run staleness detection manually: + +```bash +# Check a specific library +node scripts/sync-skills.mjs db + +# Check and write a report +node scripts/sync-skills.mjs db --report + +# After reviewing and regenerating, mark as synced +node scripts/sync-skills.mjs db --mark-synced --all +``` + +--- + +## Constraints + +| Rule | Detail | +|------|--------| +| Silent when nothing changes | No noise — exit cleanly if no updates needed | +| Surgical updates over full rewrites | Only change sections affected by the diff | +| One cascade level | Cross-skill checks go one level deep, not recursive | +| PRs scoped to one library | Never mix libraries in a single PR | +| Version bumps are separate from content updates | A version-only bump doesn't require regeneration | +| Commit messages include co-author | `Co-Authored-By: Oz ` | diff --git a/skills/tree-generator/SKILL.md b/meta/tree-generator/SKILL.md similarity index 80% rename from skills/tree-generator/SKILL.md rename to meta/tree-generator/SKILL.md index 568ffef..e398e3f 100644 --- a/skills/tree-generator/SKILL.md +++ b/meta/tree-generator/SKILL.md @@ -1,645 +1,751 @@ ---- -name: skill-tree-generator -description: > - Generate, update, and version a complete skill tree (collection of SKILL.md - files) for any JavaScript or TypeScript library. Produces core skills - (framework-agnostic) and framework skills (React, Solid, Vue bindings) - with dependency linking. Activate when producing skill files from a domain - map, updating existing skills after a library version change, or auditing - skill accuracy. Takes domain_map.yaml and skill_spec.md from - skill-domain-discovery as primary inputs. -metadata: - version: "2.1" - category: meta-tooling - input_artifacts: - - domain_map.yaml - - skill_spec.md - skills: - - skill-domain-discovery ---- - -# Skill Tree Generator - -You produce and maintain a tree of SKILL.md files for a library. Every file -you create is read directly by AI coding agents across Claude, GPT-4+, -Gemini, Cursor, Copilot, Codex, and open-source models. Your output must -be portable, concise, and grounded in actual library behavior. - -Skills are split into two layers: - -- **Core skills** — framework-agnostic concepts, configuration, and patterns -- **Framework skills** — framework-specific bindings, hooks, components - -Agents discover skills via `tanstack playbook list` and read them directly -from `node_modules`. Framework skills declare a `requires` dependency on -their core skill so agents load them in the right order. - -There are two workflows. Detect which applies. - -**Workflow A — Generate:** Build a complete skill tree from a domain map. -**Workflow B — Update:** Diff a library version change and update skills. - ---- - -## Workflow A — Generate skill tree - -### Prerequisites - -You need one of: -- A `domain_map.yaml` and `skill_spec.md` from skill-domain-discovery -- Raw library documentation and source code (run a compressed domain - discovery first) - -If starting from raw docs without a domain map, run a compressed -discovery. This produces lower-fidelity output than the full -skill-domain-discovery skill — prefer running that when time permits. - -1. Build a concept inventory (every export, config key, constraint, warning) -2. Group into 4–7 capability domains using work-oriented names -3. Enumerate 10–20 task-focused skills from the intersection of domains - and developer tasks -4. Extract 3+ failure modes per skill (plausible, silent, grounded) -5. Proceed to Step 1 below - -### Step 1 — Plan the file tree - -From the domain map, each entry in the `skills` list becomes a SKILL.md -file. The `type` field on each skill (`core`, `framework`, `lifecycle`, -`composition`) determines where it goes. Determine the file tree: - -**Core vs framework decision:** - -| Content | Goes in... | -|---------|-----------| -| Mental models, concepts, lifecycle | Core | -| Configuration options and their effects | Core | -| Type system, generics, inference | Core | -| Common mistakes that apply to all frameworks | Core | -| Hooks (`useX`, `createX`) | Framework | -| Components (``, ``) | Framework | -| Provider setup and wiring | Framework | -| SSR/hydration patterns specific to a framework | Framework | -| Framework-specific gotchas | Framework | - -If a library has no framework adapters (e.g. Store, DB), produce only -core skills. - -**Framework-integration domain decomposition:** If the domain map from -skill-domain-discovery contains a single "Framework Integration" domain -and the library has separate framework adapter packages, decompose it -into per-framework skills co-located with each adapter package. Do not -produce a single monolithic framework-integration skill that covers -React, Vue, Solid, etc. in one file. - -**Adapter-heavy domains:** When a domain covers multiple backends or -adapters with distinct config interfaces (e.g. 5 sync adapters, 3 -database drivers), keep one SKILL.md for the shared patterns but -produce one reference file per adapter with its specific config, -setup, and gotchas. The SKILL.md covers what's common; each -`references/[adapter].md` covers what's unique. - -**Skill output structure:** - -``` -skills/ -├── [lib]-core/ # Core skill for the library -│ ├── SKILL.md # Core overview + sub-skill registry -│ ├── [domain-1]/ -│ │ └── SKILL.md # Core sub-skill -│ ├── [domain-2]/ -│ │ └── SKILL.md -│ └── references/ # Optional overflow content -│ └── options.md -├── react-[lib]/ # React framework skill -│ ├── SKILL.md # React overview + sub-skill registry -│ ├── [domain-1]/ -│ │ └── SKILL.md # React-specific sub-skill -│ └── references/ -├── solid-[lib]/ # Solid framework skill (if applicable) -│ └── SKILL.md -├── vue-[lib]/ # Vue framework skill (if applicable) -│ └── SKILL.md -``` - -**Source repository layout for npm distribution:** - -Skills must ship with their respective packages so they're available in -`node_modules` after install. In a monorepo, co-locate skills with the -package they document: - -``` -packages/ -├── [lib]/ # Core package -│ ├── src/ -│ ├── skills/ # Core skills live here -│ │ ├── [lib]-core/ -│ │ │ ├── SKILL.md -│ │ │ └── [domain]/SKILL.md -│ │ └── compositions/ # Composition skills with co-used libs -│ └── package.json # Add "skills" to files array -├── react-[lib]/ # React adapter package -│ ├── src/ -│ ├── skills/ # React framework skills live here -│ │ └── react-[lib]/ -│ │ └── SKILL.md -│ └── package.json # Add "skills" to files array -``` - -Add `"skills"` to each package's `files` array in `package.json` so -skill files are included in the published npm tarball: - -```json -{ - "files": ["dist", "src", "skills"] -} -``` - -### Step 2 — Write the core skill - -The core skill is the foundational overview for the library. It covers -framework-agnostic concepts and contains the sub-skill registry. - -**Frontmatter:** - -```yaml ---- -name: [lib]-core -description: > - [1–3 sentences. What this library does and the framework-agnostic - concepts it provides. Pack with keywords: function names, config - options, concepts. This is a routing key, not a human summary.] -type: core -library: [lib] -library_version: "[version this targets]" ---- -``` - -**Body template:** - -```markdown -# [Library Name] — Core Concepts - -[One paragraph: what this library is, what problem it solves. Factual, -not promotional. Framework-agnostic.] - -## Sub-Skills - -| Need to... | Read | -|------------|------| -| [task 1] | [lib]-core/[domain-1]/SKILL.md | -| [task 2] | [lib]-core/[domain-2]/SKILL.md | - -## Quick Decision Tree - -- Setting up for the first time? → [lib]-core/[setup-domain] -- Working with [concept]? → [lib]-core/[concept-domain] -- Debugging [issue]? → [lib]-core/[domain] § Common Mistakes - -## Version - -Targets [library] v[X.Y.Z]. -``` - -### Step 3 — Write core sub-skills - -One SKILL.md per domain. Follow this structure exactly. - -**Frontmatter:** - -```yaml ---- -name: [lib]-core/[domain-slug] -description: > - [1–3 sentences. What this domain covers AND when to load it. Name - specific functions, options, or APIs. Dense routing key.] -type: sub-skill -library: [lib] -library_version: "[version]" -sources: - - "[repo]:docs/[path].md" - - "[repo]:src/[path].ts" ---- -``` - -**Body sections — in this order:** - -**1. Setup** - -Minimum working example for this domain. -- Use the library's core API, not framework-specific hooks -- Real package imports with exact names -- No `// ...` or `[your code here]` — complete and copy-pasteable -- If a concept is better explained with a framework hook, reference the - framework skill: "For React usage, see `react-[lib]/SKILL.md`" - -**2. Core Patterns** - -2–4 patterns. For each: -- One-line heading: what it accomplishes -- Complete code block using core API -- One sentence of explanation only if not self-explanatory -- No framework-specific code — use core abstractions - -**3. Common Mistakes** - -Each `failure_mode` entry from the domain map becomes a Common Mistake -entry in the SKILL file. Minimum 3 entries. Complex domains target 5–6. - -**Cross-skill failure modes:** The domain map may contain failure modes -with a `skills` list naming multiple skill slugs. Write these into -every SKILL file whose skill is listed. A developer loading the SSR -skill and a developer loading the state management skill both need to -see "stale state during hydration" — the same advice must appear in -both files. Do not deduplicate across skills at the cost of coverage. - -Format: - -```markdown -### [PRIORITY] [What goes wrong — 5–8 word phrase] - -Wrong: -```[lang] -// code that looks correct but isn't -``` - -Correct: -```[lang] -// code that works -``` - -[One sentence: the specific mechanism by which the wrong version fails.] - -Source: [doc page or source file:line] -``` - -Priority levels: -- **CRITICAL** — Breaks in production. Security risk or data loss. -- **HIGH** — Incorrect behavior under common conditions. -- **MEDIUM** — Incorrect under specific conditions or edge cases. - -Every mistake must be plausible (an agent would generate it), silent -(no immediate crash), and grounded (traceable to doc or source). - -**Failure mode status from domain map:** The domain map may include a -`status` field on failure modes. Handle as follows: -- `active` — Include as a normal Common Mistake entry -- `fixed-but-legacy-risk` — Include with a note: "Fixed in v[X] but - agents trained on older code may still generate this pattern" -- `removed` — Do not include. The bug is fixed and the pattern is no - longer relevant. - -**4. References** (only when needed) - -```markdown -## References - -- [Complete option reference](references/options.md) -``` - -Create reference files when any of these apply — not just length overflow: - -- **Length:** The skill would exceed 500 lines without them -- **Multiple subsystems:** The domain covers 3+ independent backends, - adapters, or providers with distinct config interfaces. Create one - reference file per subsystem (e.g. `references/electric-adapter.md`, - `references/query-adapter.md`) -- **Dense API surface:** A topic has >10 distinct API patterns, operators, - or option shapes that agents need for implementation. Move the full - reference to `references/` and keep only the most common 2–3 in the - SKILL.md -- **Deep validation/schema patterns:** If the library has schema - validation, type transforms (TInput/TOutput), or similar deep - configuration surfaces, give them a dedicated reference file even if - they technically fit in the parent skill - -### Step 4 — Write framework skills - -Framework skills build on their core skill. They cover only what is -specific to the framework — hooks, components, providers, and -framework-specific patterns and mistakes. - -**Frontmatter:** - -```yaml ---- -name: react-[lib] -description: > - [1–3 sentences. React-specific bindings for [library]. Name the hooks, - components, and providers. Mention React-specific patterns like SSR - hydration if applicable.] -type: framework -library: [lib] -framework: react -library_version: "[version]" -requires: - - [lib]-core ---- -``` - -**Body template:** - -```markdown -This skill builds on [lib]-core. Read [lib]-core first for foundational -concepts before applying React-specific patterns. - -# [Library Name] — React - -## Setup - -[React-specific setup: provider, hook wiring, app entry point] - -## Hooks and Components - -[React hooks and components with complete examples] - -## React-Specific Patterns - -[Patterns that only apply in React: concurrent features, Suspense -integration, SSR/hydration, etc.] - -## Common Mistakes - -[Only React-specific mistakes. Do not repeat core mistakes. Examples: -calling hooks outside provider, missing Suspense boundary, hydration -mismatch, etc.] -``` - -**Framework sub-skills** follow the same pattern as core sub-skills but -with the framework frontmatter: - -```yaml ---- -name: react-[lib]/[domain-slug] -description: > - [React-specific description for this domain.] -type: sub-skill -library: [lib] -framework: react -library_version: "[version]" -requires: - - [lib]-core - - [lib]-core/[domain-slug] ---- - -This skill builds on [lib]-core/[domain-slug]. Read the core skill first. -``` - -### Step 5 — Write cross-domain tension notes - -The domain map may contain a `tensions` section listing design conflicts -between domains. For each tension, add a brief note to the Common -Mistakes section of every SKILL file whose domain is involved. Format: - -```markdown -### HIGH Tension: [short phrase] - -This domain's patterns conflict with [other domain]. [One sentence -describing the pull.] Agents optimizing for [this domain's goal] -tend to [specific mistake] because they don't account for [other -domain's constraint]. - -See also: [lib]-core/[other-domain]/SKILL.md § Common Mistakes -``` - -The cross-reference ensures agents that load one skill are pointed -toward the related skill where the other side of the tension lives. - -### Step 6 — Write composition skills (if applicable) - -Use the `compositions` entries from `domain_map.yaml` (populated during -skill-domain-discovery Phase 2h) to identify which composition skills -to produce. - -Composition skills cover how two or more libraries work together. These -are framework-specific by default (the integration patterns depend on -framework hooks and providers). - -**Frontmatter:** - -```yaml ---- -name: compositions/[lib-a]-[lib-b] -description: > - [How lib-a and lib-b wire together. Name the specific integration - points: functions, hooks, patterns.] -type: composition -library_version: "[version of primary lib]" -requires: - - [lib-a]-core - - react-[lib-a] - - [lib-b]-core - - react-[lib-b] ---- - -This skill requires familiarity with both [lib-a] and [lib-b]. -Read their core and framework skills first. -``` - -**Body structure:** - -1. **Integration Setup** — How to wire the two libraries together -2. **Core Integration Patterns** — 2–4 patterns showing them working in concert -3. **Common Mistakes** — Mistakes that only occur at the integration boundary - -Do not duplicate content from either library's individual skills. Focus -exclusively on the seam between them. - -### Step 7 — Write security/go-live skills (where applicable) - -For libraries that have security-sensitive surface area (server functions, -auth, data exposure): - -```yaml ---- -name: react-[lib]/security -description: > - Go-live security validation for [library]. Checks [specific concerns]. -type: security -library: [lib] -framework: react -library_version: "[version]" -requires: - - react-[lib] ---- -``` - -Structure as a checklist the agent can run through before deployment: - -1. **Validation checks** — What to verify, with code showing correct config -2. **Common security mistakes** — Wrong/correct pairs specific to this library -3. **Pre-deploy checklist** — Ordered list of verifications - -### Step 8 — Validate the complete tree - -Run every check before outputting. Fix any failures before proceeding. - -| Check | Rule | -|-------|------| -| Every skill from domain_map has a SKILL.md | No orphaned skills | -| Core/framework split is clean | No framework hooks in core skills | -| Every framework skill has `requires` | Links to its core skill | -| Framework skill opens with dependency note | "builds on [core]" prose line | -| Every skill under 500 lines | Move excess to references/ | -| Every code block has real imports | Exact package name, correct adapter | -| No concept explanations | No "TypeScript is...", no "React hooks are..." | -| No marketing prose | First body line is heading or dependency note | -| Every code block is complete | Works without modification when pasted | -| Common Mistakes are silent | Not obvious compile errors | -| Common Mistakes are library-specific | Not generic TS/React mistakes | -| Common Mistakes are sourced | Every mistake traceable to doc or source | -| Core skills reference framework skills | "For React usage, see..." | -| Framework skills don't repeat core content | Only framework-specific | -| Composition skills don't repeat individual skills | Only the seam | -| `name` matches directory path | `router-core/search-params` → `router-core/search-params/SKILL.md` | -| `sources` filled in sub-skills | At least one repo:path per sub-skill | -| Cross-skill failures in all relevant files | Failure modes with multiple `skills` appear in each listed SKILL.md | -| Tensions noted in affected skills | Each tension has notes in all involved domain skills | -| Framework domains decomposed per-package | No single skill covering multiple framework adapters | -| Adapter-heavy domains have references | 3+ adapters/backends → one reference file per adapter | -| Dense API surfaces in references | >10 distinct patterns → reference file, not inline | - ---- - -## Workflow B — Update existing skills - -### Trigger conditions - -Run when: -- The library has released a new version -- A maintainer reports skills produce outdated code -- A changelog or migration guide has been published since skill creation -- Feedback reports indicate skill content is inaccurate - -### Step 1 — Detect staleness - -Compare each skill's `library_version` against the current library version. - -1. Read changelog entries between the two versions -2. Read migration guide (if one exists) -3. For each skill, check if its `sources` files have changed - -Produce a staleness report: - -```yaml -# staleness_report.yaml -library: "[name]" -library_version_in_skills: "[old]" -library_version_current: "[new]" - -stale_skills: - - skill: "[skill name]" - reason: "[what changed]" - severity: "[BREAKING | DEPRECATION | BEHAVIORAL | ADDITIVE]" - changelog_entry: "[relevant entry]" - affected_sections: - - "[Setup | Core Patterns | Common Mistakes]" - -current_skills: - - skill: "[skill name]" - reason: "[no changes affect this domain]" -``` - -### Step 2 — Update stale skills - -**BREAKING changes:** -1. Old pattern becomes a new Common Mistake entry (wrong/correct pair) -2. Update Setup if initialization changed -3. Update Core Patterns if idiomatic approach changed -4. Bump `library_version` in frontmatter -5. Check both core AND framework skills — breaking changes may affect both - -**DEPRECATION changes:** -1. Add Common Mistake: deprecated API as wrong, replacement as correct -2. Update Core Patterns to use non-deprecated API -3. Bump `library_version` - -**BEHAVIORAL changes:** -1. Default value changed → add Common Mistake entry -2. Type signature more restrictive → add Common Mistake entry -3. Update affected code blocks -4. Bump `library_version` - -**ADDITIVE changes:** -1. Evaluate if new feature belongs in existing domain or needs a new skill -2. If existing: add to Core Patterns or references/ -3. If new skill needed: create it and update the parent skill's sub-skill - registry -4. Bump `library_version` - -### Step 3 — Produce a changelog entry - -```markdown -## [date] - -### Updated for [library] v[new version] - -**Breaking changes:** -- [skill name]: [what changed and why] - -**Deprecation updates:** -- [skill name]: [old API] → [new API] - -**New skills:** -- [skill name]: [what it covers] -``` - ---- - -## Constraints — verify for every file - -| Check | Rule | -|-------|------| -| Under 500 lines per SKILL.md | Move excess to references/; also create references for content depth | -| Real imports in every code block | Exact package, correct adapter | -| No external concept explanations | No "TypeScript is...", no "React hooks are..." — library-specific concepts are fine | -| No marketing prose | First body line is heading, code, or dependency note | -| Complete code blocks | Every block works without modification | -| Common Mistakes are silent | Not obvious compile errors | -| Common Mistakes are library-specific | Not generic TS/React mistakes | -| Common Mistakes are sourced | Traceable to doc or source | -| Core skills are framework-agnostic | No hooks, no components, no providers | -| Framework skills have `requires` | Lists core dependency | -| Framework skills open with dependency note | First prose line references core | -| Composition skills require all dependencies | Lists all core + framework skills | -| `name` matches directory | `router-core/search-params` → file at that path | -| `library_version` in every frontmatter | Which version the skill targets | -| Cross-skill failures duplicated | Each listed skill gets the failure mode | -| Tensions cross-referenced | Tension notes in each involved skill point to the other | -| Skills ship with packages | `"skills"` in package.json `files` array | - ---- - -## Cross-model compatibility - -Output is consumed by all major AI coding agents. To ensure consistency: - -- Markdown with YAML frontmatter — universally parsed -- No XML tags in generated skill content -- Code blocks use triple backticks with language annotation -- Section boundaries use ## headers -- Descriptions are keyword-packed for routing -- Examples show concrete values, never placeholders -- Positive instructions ("Use X") over negative ("Don't use Y") -- Critical info at start or end of sections (not buried in middle) -- Each SKILL.md is self-contained except for declared `requires` - ---- - -## Output order - -When generating a complete skill tree: - -1. Core overview SKILL.md — entry point for the library -2. Core sub-skills in domain order -3. Framework overview SKILL.md for each framework -4. Framework sub-skills -5. Composition skills (if applicable) -6. Security skills (if applicable) -7. references/ files for any skill that needs them -8. CHANGELOG.md entry - -When updating: - -1. staleness_report.yaml -2. Updated SKILL.md files (core then framework) -3. CHANGELOG.md entry +--- +name: skill-tree-generator +description: > + Generate, update, and version a complete skill tree (collection of SKILL.md + files) for any JavaScript or TypeScript library. Produces core skills + (framework-agnostic) and framework skills (React, Solid, Vue bindings) + with dependency linking. Activate when producing skill files from a domain + map, updating existing skills after a library version change, or auditing + skill accuracy. Takes domain_map.yaml and skill_spec.md from + skill-domain-discovery as primary inputs. +metadata: + version: "3.0" + category: meta-tooling + input_artifacts: + - domain_map.yaml + - skill_spec.md + skills: + - skill-domain-discovery +--- + +# Skill Tree Generator + +You produce and maintain a tree of SKILL.md files for a library. Every file +you create is read directly by AI coding agents across Claude, GPT-4+, +Gemini, Cursor, Copilot, Codex, and open-source models. Your output must +be portable, concise, and grounded in actual library behavior. + +### Skill types + +Every skill has a `type` field in its frontmatter. Valid types: + +| Type | Purpose | Example | +|------|---------|---------| +| `core` | Framework-agnostic concepts, configuration, patterns | `db-core` | +| `sub-skill` | A focused sub-topic within a core or framework skill | `db-core/live-queries` | +| `framework` | Framework-specific bindings, hooks, components | `react-db` | +| `lifecycle` | Cross-cutting developer journey (getting started, go-live) | `electric-quickstart` | +| `composition` | Integration between two or more libraries | `electric-drizzle` | +| `security` | Audit checklist or security validation | `electric-security-check` | + +Agents discover skills via `tanstack playbook list` and read them directly +from `node_modules`. Framework skills declare a `requires` dependency on +their core skill so agents load them in the right order. + +There are two workflows. Detect which applies. + +**Workflow A — Generate:** Build a complete skill tree from a domain map. +**Workflow B — Update:** Diff a library version change and update skills. + +--- + +## Workflow A — Generate skill tree + +### Prerequisites + +You need one of: +- A `domain_map.yaml` and `skill_spec.md` from skill-domain-discovery +- Raw library documentation and source code (run a compressed domain + discovery first) + +If starting from raw docs without a domain map, run a compressed +discovery. This produces lower-fidelity output than the full +skill-domain-discovery skill — prefer running that when time permits. + +1. Build a concept inventory (every export, config key, constraint, warning) +2. Group into 4–7 capability domains using work-oriented names +3. Enumerate 10–20 task-focused skills from the intersection of domains + and developer tasks +4. Extract 3+ failure modes per skill (plausible, silent, grounded) +5. Proceed to Step 1 below + +### Step 1 — Plan the file tree + +From the domain map, each entry in the `skills` list becomes a SKILL.md +file. The `type` field on each skill (`core`, `framework`, `lifecycle`, +`composition`) determines where it goes. Determine the file tree: + +**Core vs framework decision:** + +| Content | Goes in... | +|---------|-----------| +| Mental models, concepts, lifecycle | Core | +| Configuration options and their effects | Core | +| Type system, generics, inference | Core | +| Common mistakes that apply to all frameworks | Core | +| Hooks (`useX`, `createX`) | Framework | +| Components (``, ``) | Framework | +| Provider setup and wiring | Framework | +| SSR/hydration patterns specific to a framework | Framework | +| Framework-specific gotchas | Framework | + +If a library has no framework adapters (e.g. Store, DB), produce only +core skills. + +**Framework-integration domain decomposition:** If the domain map from +skill-domain-discovery contains a single "Framework Integration" domain +and the library has separate framework adapter packages, decompose it +into per-framework skills co-located with each adapter package. Do not +produce a single monolithic framework-integration skill that covers +React, Vue, Solid, etc. in one file. + +**Adapter-heavy domains:** When a domain covers multiple backends or +adapters with distinct config interfaces (e.g. 5 sync adapters, 3 +database drivers), keep one SKILL.md for the shared patterns but +produce one reference file per adapter with its specific config, +setup, and gotchas. The SKILL.md covers what's common; each +`references/[adapter].md` covers what's unique. + +**Flat vs nested structure:** + +Choose the structure that matches how the domain map's skills are shaped. + +Use **nested** (`[lib]-core/[domain]/SKILL.md`) when: +- Developer tasks cluster cleanly into 3–5 conceptual domains +- The library has a clear core + framework adapter split +- Skills build on each other in a layered way + +Use **flat** (`skills/[skill-name]/SKILL.md`) when: +- Developer tasks are task-focused and don't nest into domains +- The domain discovery process recommended task-focused skills +- Skills map 1:1 to distinct developer intents with minimal overlap + +Both are valid. The domain map's `type` field and structure will signal +which fits. When in doubt, prefer flat — it's simpler and each skill +is independently discoverable. + +**Nested structure:** + +``` +skills/ +├── [lib]-core/ # Core skill for the library +│ ├── SKILL.md # Core overview + sub-skill registry +│ ├── [domain-1]/ +│ │ └── SKILL.md # Core sub-skill +│ ├── [domain-2]/ +│ │ └── SKILL.md +│ └── references/ # Optional overflow content +│ └── options.md +├── react-[lib]/ # React framework skill +│ ├── SKILL.md # React overview + sub-skill registry +│ ├── [domain-1]/ +│ │ └── SKILL.md # React-specific sub-skill +│ └── references/ +├── solid-[lib]/ # Solid framework skill (if applicable) +│ └── SKILL.md +├── vue-[lib]/ # Vue framework skill (if applicable) +│ └── SKILL.md +``` + +**Flat structure:** + +``` +skills/ +├── [lib]-shapes/ # Task-focused skill +│ ├── SKILL.md +│ └── references/ +│ └── shape-options.md +├── [lib]-auth/ # Another task skill +│ └── SKILL.md +├── [lib]-proxy/ +│ └── SKILL.md +├── [lib]-quickstart/ # Lifecycle skill +│ └── SKILL.md +├── [lib]-go-live/ # Lifecycle skill +│ └── SKILL.md +├── [lib]-drizzle/ # Composition skill +│ └── SKILL.md +``` + +**Router skill:** A router skill (lightweight entry point with a decision +table) is optional. If the playbook CLI provides `list` and `show` +commands, agents can discover skills directly without a router. Only +create a router skill if the skill set is large enough (15+) that +browsing the list is insufficient, or if the nested structure needs +an entry point to guide agents to the right sub-skill. + +**Source repository layout for npm distribution:** + +Skills must ship with their respective packages so they're available in +`node_modules` after install. In a monorepo, co-locate skills with the +package they document: + +``` +packages/ +├── [lib]/ # Core package +│ ├── src/ +│ ├── skills/ # Core skills live here +│ │ ├── [lib]-core/ +│ │ │ ├── SKILL.md +│ │ │ └── [domain]/SKILL.md +│ │ └── compositions/ # Composition skills with co-used libs +│ └── package.json # Add "skills" to files array +├── react-[lib]/ # React adapter package +│ ├── src/ +│ ├── skills/ # React framework skills live here +│ │ └── react-[lib]/ +│ │ └── SKILL.md +│ └── package.json # Add "skills" to files array +``` + +Add `"skills"` to each package's `files` array in `package.json` so +skill files are included in the published npm tarball: + +```json +{ + "files": ["dist", "src", "skills"] +} +``` + +### Step 2 — Write the core skill + +The core skill is the foundational overview for the library. It covers +framework-agnostic concepts and contains the sub-skill registry. + +**Frontmatter:** + +```yaml +--- +name: [lib]-core +description: > + [1–3 sentences. What this library does and the framework-agnostic + concepts it provides. Pack with keywords: function names, config + options, concepts. This is a routing key, not a human summary.] +type: core +library: [lib] +library_version: "[version this targets]" +--- +``` + +**Body template:** + +```markdown +# [Library Name] — Core Concepts + +[One paragraph: what this library is, what problem it solves. Factual, +not promotional. Framework-agnostic.] + +## Sub-Skills + +| Need to... | Read | +|------------|------| +| [task 1] | [lib]-core/[domain-1]/SKILL.md | +| [task 2] | [lib]-core/[domain-2]/SKILL.md | + +## Quick Decision Tree + +- Setting up for the first time? → [lib]-core/[setup-domain] +- Working with [concept]? → [lib]-core/[concept-domain] +- Debugging [issue]? → [lib]-core/[domain] § Common Mistakes + +## Version + +Targets [library] v[X.Y.Z]. +``` + +### Step 3 — Write core sub-skills + +One SKILL.md per domain. Follow this structure exactly. + +**Frontmatter:** + +```yaml +--- +name: [lib]-core/[domain-slug] +description: > + [1–3 sentences. What this domain covers AND when to load it. Name + specific functions, options, or APIs. Dense routing key.] +type: sub-skill +library: [lib] +library_version: "[version]" +sources: + - "[repo]:docs/[path].md" + - "[repo]:src/[path].ts" +--- +``` + +**Body sections — in this order:** + +**1. Setup** + +Minimum working example for this domain. +- Use the library's core API, not framework-specific hooks +- Real package imports with exact names +- No `// ...` or `[your code here]` — complete and copy-pasteable +- If a concept is better explained with a framework hook, reference the + framework skill: "For React usage, see `react-[lib]/SKILL.md`" + +**2. Core Patterns** + +2–4 patterns. For each: +- One-line heading: what it accomplishes +- Complete code block using core API +- One sentence of explanation only if not self-explanatory +- No framework-specific code — use core abstractions + +**3. Common Mistakes** + +Each `failure_mode` entry from the domain map becomes a Common Mistake +entry in the SKILL file. Minimum 3 entries. Complex domains target 5–6. + +**Cross-skill failure modes:** The domain map may contain failure modes +with a `skills` list naming multiple skill slugs. Write these into +every SKILL file whose skill is listed. A developer loading the SSR +skill and a developer loading the state management skill both need to +see "stale state during hydration" — the same advice must appear in +both files. Do not deduplicate across skills at the cost of coverage. + +Format: + +```markdown +### [PRIORITY] [What goes wrong — 5–8 word phrase] + +Wrong: +```[lang] +// code that looks correct but isn't +``` + +Correct: +```[lang] +// code that works +``` + +[One sentence: the specific mechanism by which the wrong version fails.] + +Source: [doc page or source file:line] +``` + +Priority levels: +- **CRITICAL** — Breaks in production. Security risk or data loss. +- **HIGH** — Incorrect behavior under common conditions. +- **MEDIUM** — Incorrect under specific conditions or edge cases. + +Every mistake must be plausible (an agent would generate it), silent +(no immediate crash), and grounded (traceable to doc or source). + +**Failure mode status from domain map:** The domain map may include a +`status` field on failure modes. Handle as follows: +- `active` — Include as a normal Common Mistake entry +- `fixed-but-legacy-risk` — Include with a note: "Fixed in v[X] but + agents trained on older code may still generate this pattern" +- `removed` — Do not include. The bug is fixed and the pattern is no + longer relevant. + +**4. References** (only when needed) + +```markdown +## References + +- [Complete option reference](references/options.md) +``` + +Create reference files when any of these apply — not just length overflow: + +- **Length:** The skill would exceed 500 lines without them +- **Multiple subsystems:** The domain covers 3+ independent backends, + adapters, or providers with distinct config interfaces. Create one + reference file per subsystem (e.g. `references/electric-adapter.md`, + `references/query-adapter.md`) +- **Dense API surface:** A topic has >10 distinct API patterns, operators, + or option shapes that agents need for implementation. Move the full + reference to `references/` and keep only the most common 2–3 in the + SKILL.md +- **Deep validation/schema patterns:** If the library has schema + validation, type transforms (TInput/TOutput), or similar deep + configuration surfaces, give them a dedicated reference file even if + they technically fit in the parent skill + +### Step 4 — Write framework skills + +Framework skills build on their core skill. They cover only what is +specific to the framework — hooks, components, providers, and +framework-specific patterns and mistakes. + +**Frontmatter:** + +```yaml +--- +name: react-[lib] +description: > + [1–3 sentences. React-specific bindings for [library]. Name the hooks, + components, and providers. Mention React-specific patterns like SSR + hydration if applicable.] +type: framework +library: [lib] +framework: react +library_version: "[version]" +requires: + - [lib]-core +--- +``` + +**Body template:** + +```markdown +This skill builds on [lib]-core. Read [lib]-core first for foundational +concepts before applying React-specific patterns. + +# [Library Name] — React + +## Setup + +[React-specific setup: provider, hook wiring, app entry point] + +## Hooks and Components + +[React hooks and components with complete examples] + +## React-Specific Patterns + +[Patterns that only apply in React: concurrent features, Suspense +integration, SSR/hydration, etc.] + +## Common Mistakes + +[Only React-specific mistakes. Do not repeat core mistakes. Examples: +calling hooks outside provider, missing Suspense boundary, hydration +mismatch, etc.] +``` + +**Framework sub-skills** follow the same pattern as core sub-skills but +with the framework frontmatter: + +```yaml +--- +name: react-[lib]/[domain-slug] +description: > + [React-specific description for this domain.] +type: sub-skill +library: [lib] +framework: react +library_version: "[version]" +requires: + - [lib]-core + - [lib]-core/[domain-slug] +--- + +This skill builds on [lib]-core/[domain-slug]. Read the core skill first. +``` + +### Step 5 — Write cross-domain tension notes + +The domain map may contain a `tensions` section listing design conflicts +between domains. For each tension, add a brief note to the Common +Mistakes section of every SKILL file whose domain is involved. Format: + +```markdown +### HIGH Tension: [short phrase] + +This domain's patterns conflict with [other domain]. [One sentence +describing the pull.] Agents optimizing for [this domain's goal] +tend to [specific mistake] because they don't account for [other +domain's constraint]. + +See also: [lib]-core/[other-domain]/SKILL.md § Common Mistakes +``` + +The cross-reference ensures agents that load one skill are pointed +toward the related skill where the other side of the tension lives. + +### Step 6 — Write composition skills (if applicable) + +Use the `compositions` entries from `domain_map.yaml` (populated during +skill-domain-discovery Phase 2h) to identify which composition skills +to produce. + +Composition skills cover how two or more libraries work together. These +are framework-specific by default (the integration patterns depend on +framework hooks and providers). + +**Frontmatter:** + +```yaml +--- +name: compositions/[lib-a]-[lib-b] +description: > + [How lib-a and lib-b wire together. Name the specific integration + points: functions, hooks, patterns.] +type: composition +library_version: "[version of primary lib]" +requires: + - [lib-a]-core + - react-[lib-a] + - [lib-b]-core + - react-[lib-b] +--- + +This skill requires familiarity with both [lib-a] and [lib-b]. +Read their core and framework skills first. +``` + +**Body structure:** + +1. **Integration Setup** — How to wire the two libraries together +2. **Core Integration Patterns** — 2–4 patterns showing them working in concert +3. **Common Mistakes** — Mistakes that only occur at the integration boundary + +Do not duplicate content from either library's individual skills. Focus +exclusively on the seam between them. + +### Step 7 — Write checklist/audit skills (where applicable) + +Some skills don't fit the standard body structure (Setup → Core Patterns +→ Common Mistakes). Security, go-live, and some lifecycle skills are +audit-oriented — the agent runs through a checklist to verify correctness +rather than learning patterns. Use the alternative body structure below +for these skill types. + +**When to use the checklist body:** +- `security` type skills — pre-deploy security validation +- `lifecycle` type skills focused on verification (go-live, migration) +- Any skill where the primary action is "check these things" not "learn + these patterns" + +**Frontmatter:** + +```yaml +--- +name: react-[lib]/security +description: > + Go-live security validation for [library]. Checks [specific concerns]. +type: security +library: [lib] +framework: react +library_version: "[version]" +requires: + - react-[lib] +--- +``` + +**Alternative body template (checklist/audit):** + +```markdown +# [Library Name] — [Security | Go-Live | Migration] Checklist + +Run through each section before [deploying | releasing | migrating]. + +## [Category 1] Checks + +### Check: [what to verify] + +Expected: +```[lang] +// correct configuration or code +``` + +Fail condition: [what indicates this check failed] +Fix: [one-line remediation] + +### Check: [what to verify] + +[same structure] + +## [Category 2] Checks + +[same structure] + +## Common Security Mistakes + +[Wrong/correct pairs specific to this library, same format as +Common Mistakes in standard skills] + +## Pre-Deploy Summary + +- [ ] [Verification 1] +- [ ] [Verification 2] +- [ ] [Verification 3] +``` + +The key differences from the standard body: +- No "Setup" section — the agent already has the app running +- Checks replace "Core Patterns" — each check is a verification, not a + teaching pattern +- The summary checklist at the end gives agents a quick pass/fail list +- Common Mistakes section is still present for wrong/correct pairs + +### Step 8 — Validate the complete tree + +Run every check before outputting. Fix any failures before proceeding. + +| Check | Rule | +|-------|------| +| Every skill from domain_map has a SKILL.md | No orphaned skills | +| Core/framework split is clean | No framework hooks in core skills | +| Every framework skill has `requires` | Links to its core skill | +| Framework skill opens with dependency note | "builds on [core]" prose line | +| Every skill under 500 lines | Move excess to references/ | +| Every code block has real imports | Exact package name, correct adapter | +| No concept explanations | No "TypeScript is...", no "React hooks are..." | +| No marketing prose | First body line is heading or dependency note | +| Every code block is complete | Works without modification when pasted | +| Common Mistakes are silent | Not obvious compile errors | +| Common Mistakes are library-specific | Not generic TS/React mistakes | +| Common Mistakes are sourced | Every mistake traceable to doc or source | +| Core skills reference framework skills | "For React usage, see..." | +| Framework skills don't repeat core content | Only framework-specific | +| Composition skills don't repeat individual skills | Only the seam | +| `name` matches directory path | `router-core/search-params` → `router-core/search-params/SKILL.md` | +| `sources` filled in sub-skills | At least one repo:path per sub-skill | +| Cross-skill failures in all relevant files | Failure modes with multiple `skills` appear in each listed SKILL.md | +| Tensions noted in affected skills | Each tension has notes in all involved domain skills | +| Framework domains decomposed per-package | No single skill covering multiple framework adapters | +| Adapter-heavy domains have references | 3+ adapters/backends → one reference file per adapter | +| Dense API surfaces in references | >10 distinct patterns → reference file, not inline | +| Checklist skills use audit body | Security/go-live skills use checklist template, not Setup → Core Patterns → Common Mistakes | + +--- + +## Workflow B — Update existing skills + +### Trigger conditions + +Run when: +- The library has released a new version +- A maintainer reports skills produce outdated code +- A changelog or migration guide has been published since skill creation +- Feedback reports indicate skill content is inaccurate + +### Step 1 — Detect staleness + +Compare each skill's `library_version` against the current library version. + +1. Read changelog entries between the two versions +2. Read migration guide (if one exists) +3. For each skill, check if its `sources` files have changed + +Produce a staleness report: + +```yaml +# staleness_report.yaml +library: "[name]" +library_version_in_skills: "[old]" +library_version_current: "[new]" + +stale_skills: + - skill: "[skill name]" + reason: "[what changed]" + severity: "[BREAKING | DEPRECATION | BEHAVIORAL | ADDITIVE]" + changelog_entry: "[relevant entry]" + affected_sections: + - "[Setup | Core Patterns | Common Mistakes]" + +current_skills: + - skill: "[skill name]" + reason: "[no changes affect this domain]" +``` + +### Step 2 — Update stale skills + +**BREAKING changes:** +1. Old pattern becomes a new Common Mistake entry (wrong/correct pair) +2. Update Setup if initialization changed +3. Update Core Patterns if idiomatic approach changed +4. Bump `library_version` in frontmatter +5. Check both core AND framework skills — breaking changes may affect both + +**DEPRECATION changes:** +1. Add Common Mistake: deprecated API as wrong, replacement as correct +2. Update Core Patterns to use non-deprecated API +3. Bump `library_version` + +**BEHAVIORAL changes:** +1. Default value changed → add Common Mistake entry +2. Type signature more restrictive → add Common Mistake entry +3. Update affected code blocks +4. Bump `library_version` + +**ADDITIVE changes:** +1. Evaluate if new feature belongs in existing domain or needs a new skill +2. If existing: add to Core Patterns or references/ +3. If new skill needed: create it and update the parent skill's sub-skill + registry +4. Bump `library_version` + +### Step 3 — Produce a changelog entry + +```markdown +## [date] + +### Updated for [library] v[new version] + +**Breaking changes:** +- [skill name]: [what changed and why] + +**Deprecation updates:** +- [skill name]: [old API] → [new API] + +**New skills:** +- [skill name]: [what it covers] +``` + +--- + +## Constraints — verify for every file + +| Check | Rule | +|-------|------| +| Under 500 lines per SKILL.md | Move excess to references/; also create references for content depth | +| Real imports in every code block | Exact package, correct adapter | +| No external concept explanations | No "TypeScript is...", no "React hooks are..." — library-specific concepts are fine | +| No marketing prose | First body line is heading, code, or dependency note | +| Complete code blocks | Every block works without modification | +| Common Mistakes are silent | Not obvious compile errors | +| Common Mistakes are library-specific | Not generic TS/React mistakes | +| Common Mistakes are sourced | Traceable to doc or source | +| Core skills are framework-agnostic | No hooks, no components, no providers | +| Framework skills have `requires` | Lists core dependency | +| Framework skills open with dependency note | First prose line references core | +| Composition skills require all dependencies | Lists all core + framework skills | +| `name` matches directory | `router-core/search-params` → file at that path | +| `library_version` in every frontmatter | Which version the skill targets | +| Cross-skill failures duplicated | Each listed skill gets the failure mode | +| Tensions cross-referenced | Tension notes in each involved skill point to the other | +| Skills ship with packages | `"skills"` in package.json `files` array | +| Checklist skills use audit template | Security/go-live skills use checklist body, not standard body | + +--- + +## Cross-model compatibility + +Output is consumed by all major AI coding agents. To ensure consistency: + +- Markdown with YAML frontmatter — universally parsed +- No XML tags in generated skill content +- Code blocks use triple backticks with language annotation +- Section boundaries use ## headers +- Descriptions are keyword-packed for routing +- Examples show concrete values, never placeholders +- Positive instructions ("Use X") over negative ("Don't use Y") +- Critical info at start or end of sections (not buried in middle) +- Each SKILL.md is self-contained except for declared `requires` + +--- + +## Output order + +When generating a complete skill tree: + +1. Core overview SKILL.md — entry point for the library +2. Core sub-skills in domain order +3. Framework overview SKILL.md for each framework +4. Framework sub-skills +5. Composition skills (if applicable) +6. Security skills (if applicable) +7. references/ files for any skill that needs them +8. CHANGELOG.md entry + +When updating: + +1. staleness_report.yaml +2. Updated SKILL.md files (core then framework) +3. CHANGELOG.md entry diff --git a/package.json b/package.json index 84ac339..de62133 100644 --- a/package.json +++ b/package.json @@ -64,11 +64,12 @@ "tinyglobby": "^0.2.15", "tsdown": "^0.19.0", "typescript": "5.9.3", - "vitest": "^4.0.17" + "vitest": "^4.0.17", + "yaml": "^2.7.0" }, - "overrides": { - "@tanstack/agents": "workspace:*", - "@tanstack/router/skills": "workspace:*", - "@tanstack/start/skills": "workspace:*" - } -} + "overrides": { + "@tanstack/agents": "workspace:*", + "@tanstack/router/skills": "workspace:*", + "@tanstack/start/skills": "workspace:*" + } +} diff --git a/packages/agents/README.md b/packages/agents/README.md deleted file mode 100644 index 5e27516..0000000 --- a/packages/agents/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# @tanstack/agents - -TanStack-wide agent infrastructure. - -Contents: - -- `rules/` always-on guidance -- `subagents/` library heuristics (e.g. Router) - -Note: Skill Markdown files are intentionally not added yet. diff --git a/packages/agents/manifest.json b/packages/agents/manifest.json deleted file mode 100644 index 30b810b..0000000 --- a/packages/agents/manifest.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "package": "@tanstack/agents", - "rules": { - "tanstack/safety": "rules/safety.md", - "tanstack/style": "rules/style.md", - "tanstack/conventions": "rules/tanstack-conventions.md", - "tanstack/staging-migration": "rules/staging-migration.md" - }, - "subagents": { - "tanstack-router": "subagents/tanstack-router.md", - "tanstack-query": "subagents/tanstack-query.md", - "tanstack-table": "subagents/tanstack-table.md", - "tanstack-form": "subagents/tanstack-form.md", - "tanstack-virtual": "subagents/tanstack-virtual.md", - "tanstack-start": "subagents/tanstack-start.md", - "tanstack-db": "subagents/tanstack-db.md" - }, - "skills": { - "tanstack": "skills/tanstack/SKILL.md" - } -} diff --git a/packages/agents/package.json b/packages/agents/package.json deleted file mode 100644 index 12d3e2f..0000000 --- a/packages/agents/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@tanstack/agents", - "version": "0.0.0", - "private": true, - "description": "TanStack-wide agent rules, subagents, and entrypoint skills", - "license": "MIT", - "type": "module" -} diff --git a/packages/agents/project.json b/packages/agents/project.json deleted file mode 100644 index f22ec05..0000000 --- a/packages/agents/project.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "agents", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "library", - "sourceRoot": "packages/agents", - "targets": { - "build": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('build:noop\\n')\"" - } - }, - "test:eslint": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:eslint:noop\\n')\"" - } - }, - "test:types": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:types:noop\\n')\"" - } - }, - "test:lib": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:lib:noop\\n')\"" - } - } - } -} diff --git a/packages/agents/rules/safety.md b/packages/agents/rules/safety.md deleted file mode 100644 index ab853e6..0000000 --- a/packages/agents/rules/safety.md +++ /dev/null @@ -1,11 +0,0 @@ -# TanStack Safety - -Purpose: - -- - -Guidelines: - -- -- -- diff --git a/packages/agents/rules/staging-migration.md b/packages/agents/rules/staging-migration.md deleted file mode 100644 index fd5604d..0000000 --- a/packages/agents/rules/staging-migration.md +++ /dev/null @@ -1,11 +0,0 @@ -# Staging + Migration Rules (This Repo) - -Purpose: - -- - -Guidelines: - -- -- -- diff --git a/packages/agents/rules/style.md b/packages/agents/rules/style.md deleted file mode 100644 index ac3a391..0000000 --- a/packages/agents/rules/style.md +++ /dev/null @@ -1,11 +0,0 @@ -# TanStack Style - -Purpose: - -- - -Guidelines: - -- -- -- diff --git a/packages/agents/rules/tanstack-conventions.md b/packages/agents/rules/tanstack-conventions.md deleted file mode 100644 index 0b91757..0000000 --- a/packages/agents/rules/tanstack-conventions.md +++ /dev/null @@ -1,16 +0,0 @@ -# TanStack Conventions - -Purpose: - -- - -Guidelines: - -- -- -- - -IDs: - -- Skill IDs: `` -- Subagent IDs: `` diff --git a/packages/agents/skills/tanstack/SKILL.md b/packages/agents/skills/tanstack/SKILL.md deleted file mode 100644 index 76dd881..0000000 --- a/packages/agents/skills/tanstack/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ -# tanstack (Entrypoint Skill) - -Apply these rules first: - -- `packages/agents/rules/safety.md` -- `packages/agents/rules/style.md` -- `packages/agents/rules/tanstack-conventions.md` -- `packages/agents/rules/staging-migration.md` - -Select a library subagent based on the user task: - -- `packages/agents/subagents/.md` - -If a library skill bundle is installed, open: - -- `./.agents/skills//SKILL.md` diff --git a/packages/agents/subagents/tanstack-db.md b/packages/agents/subagents/tanstack-db.md deleted file mode 100644 index a75eaaa..0000000 --- a/packages/agents/subagents/tanstack-db.md +++ /dev/null @@ -1,20 +0,0 @@ -# tanstack-db (Subagent) - -Scope: - -- - -Optimize for: - -- -- -- - -Avoid: - -- -- - -When stuck: - -- diff --git a/packages/agents/subagents/tanstack-form.md b/packages/agents/subagents/tanstack-form.md deleted file mode 100644 index 27aed58..0000000 --- a/packages/agents/subagents/tanstack-form.md +++ /dev/null @@ -1,20 +0,0 @@ -# tanstack-form (Subagent) - -Scope: - -- - -Optimize for: - -- -- -- - -Avoid: - -- -- - -When stuck: - -- diff --git a/packages/agents/subagents/tanstack-query.md b/packages/agents/subagents/tanstack-query.md deleted file mode 100644 index f463a42..0000000 --- a/packages/agents/subagents/tanstack-query.md +++ /dev/null @@ -1,20 +0,0 @@ -# tanstack-query (Subagent) - -Scope: - -- - -Optimize for: - -- -- -- - -Avoid: - -- -- - -When stuck: - -- diff --git a/packages/agents/subagents/tanstack-router.md b/packages/agents/subagents/tanstack-router.md deleted file mode 100644 index ed1dea4..0000000 --- a/packages/agents/subagents/tanstack-router.md +++ /dev/null @@ -1,28 +0,0 @@ -# tanstack-router (Subagent) - -Scope: - -- Use for TanStack Router routing, data-loading, and error handling decisions. -- Apply when defining route trees, search params, loaders, or SSR constraints. -- Pull in Router skills for focused, API-level guidance. - -Optimize for: - -- End-to-end type safety across routes, params, search, and loader data. -- Loader-first data flows with predictable caching and prefetching. -- Clear nesting with localized error and not-found boundaries. - -Avoid: - -- Untyped search params or ad-hoc parsing outside validation. -- Fetching route-critical data in components instead of loaders. - -When stuck: - -- Which adapter and routing style (file-based vs code-based) are you using? -- What data must be loaded before render vs fetched after mount? -- Is this SSR/Start or client-only? - -Look up skills: - -- Use `@skills/router` to pick the right Router skill. diff --git a/packages/agents/subagents/tanstack-start.md b/packages/agents/subagents/tanstack-start.md deleted file mode 100644 index a5bfc4e..0000000 --- a/packages/agents/subagents/tanstack-start.md +++ /dev/null @@ -1,28 +0,0 @@ -# tanstack-start (Subagent) - -Scope: - -- Use for TanStack Start app architecture, SSR, and server function workflows. -- Apply when wiring Start entry points, adapters, middleware, and deployment targets. -- Reference `tanstack-router` for routing, loaders, and route tree structure. - -Optimize for: - -- Predictable SSR and streaming behavior with clear server/client boundaries. -- Secure server function usage with typed inputs and outputs. -- Cohesive Start setup that matches adapter and hosting constraints. - -Avoid: - -- Mixing server-only code into client bundles or routes. -- Duplicating router-level loader logic in client components. - -When stuck: - -- Which Start adapter and deployment target are you using? -- Is the work about routing/data loading (use `tanstack-router`)? -- What must run on the server vs the client for this flow? - -Look up skills: - -- Use `tanstack-router` guidance when routing is involved. diff --git a/packages/agents/subagents/tanstack-table.md b/packages/agents/subagents/tanstack-table.md deleted file mode 100644 index fa0b164..0000000 --- a/packages/agents/subagents/tanstack-table.md +++ /dev/null @@ -1,20 +0,0 @@ -# tanstack-table (Subagent) - -Scope: - -- - -Optimize for: - -- -- -- - -Avoid: - -- -- - -When stuck: - -- diff --git a/packages/agents/subagents/tanstack-virtual.md b/packages/agents/subagents/tanstack-virtual.md deleted file mode 100644 index 81d91b4..0000000 --- a/packages/agents/subagents/tanstack-virtual.md +++ /dev/null @@ -1,20 +0,0 @@ -# tanstack-virtual (Subagent) - -Scope: - -- - -Optimize for: - -- -- -- - -Avoid: - -- -- - -When stuck: - -- diff --git a/packages/playbooks/package.json b/packages/playbooks/package.json new file mode 100644 index 0000000..3bd0cfd --- /dev/null +++ b/packages/playbooks/package.json @@ -0,0 +1,20 @@ +{ + "name": "@tanstack/playbooks", + "version": "0.1.0-alpha.1", + "description": "AI coding agent skills for the TanStack ecosystem", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tanstack/playbooks" + }, + "files": [ + "skills", + "package_map.yaml" + ], + "devDependencies": { + "yaml": "^2.7.0" + }, + "scripts": { + "test:lib": "vitest run" + } +} diff --git a/packages/playbooks/package_map.yaml b/packages/playbooks/package_map.yaml new file mode 100644 index 0000000..faa4b23 --- /dev/null +++ b/packages/playbooks/package_map.yaml @@ -0,0 +1,27 @@ +schema_version: 1 +playbook: + name: tanstack + version: "0.1.0-alpha.1" + +# Maps npm package names → skill directories +package_map: + "@tanstack/db": [db/core] + "@tanstack/react-db": [db/core, db/react] + "@tanstack/vue-db": [db/core, db/vue] + "@tanstack/solid-db": [db/core, db/solid] + "@tanstack/svelte-db": [db/core, db/svelte] + "@tanstack/angular-db": [db/core, db/angular] + +# Composition skills — surfaced when ALL listed skills are present +# (none yet — add as library skills are created) +compositions: [] + +# Always included regardless of installed packages +always_include: [tanstack] + +# Feedback config +feedback: + enabled: true + target: "github_discussion" + repo: "tanstack/playbooks" + category: "Feedback" diff --git a/packages/playbooks/skills/compositions/.gitkeep b/packages/playbooks/skills/compositions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/playbooks/skills/db/angular/SKILL.md b/packages/playbooks/skills/db/angular/SKILL.md new file mode 100644 index 0000000..b1a87f0 --- /dev/null +++ b/packages/playbooks/skills/db/angular/SKILL.md @@ -0,0 +1,176 @@ +--- +name: db/angular +description: > + Angular bindings for TanStack DB. Covers injectLiveQuery with Angular + signals. Static queries and reactive params with signal-based parameters. + Return shape (data, state, collection, status, isLoading, isReady, + isError) as Angular Signals. Angular 16.0.0+ compatibility. +type: framework +library: db +framework: angular +library_version: '0.5.29' +requires: + - db/core +--- + +This skill builds on db-core. Read db-core first for collection setup, +query builder syntax, operators, mutations, and sync concepts. + +# TanStack DB — Angular + +## Setup + +```bash +npm install @tanstack/db @tanstack/angular-db +``` + +Requires Angular 16.0.0+ (signals). + +Collections are created outside components in a dedicated module: + +```typescript +// collections/todos.ts +import { createCollection, eq } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +export const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (todo) => todo.id, + onUpdate: async ({ transaction }) => { + const { original, changes } = transaction.mutations[0] + await fetch(`/api/todos/${original.id}`, { + method: 'PATCH', + body: JSON.stringify(changes), + }) + }, + }), +) +``` + +## Hooks and Components + +### injectLiveQuery — static query + +```typescript +import { Component } from '@angular/core' +import { injectLiveQuery } from '@tanstack/angular-db' +import { eq } from '@tanstack/db' +import { todosCollection } from '../collections/todos' + +@Component({ + selector: 'app-todo-list', + template: ` + @if (query.isLoading()) { +
Loading...
+ } @else if (query.isError()) { +
Error loading todos
+ } @else { +
    + @for (todo of query.data(); track todo.id) { +
  • {{ todo.text }}
  • + } +
+ } + `, +}) +export class TodoListComponent { + query = injectLiveQuery((q) => + q.from({ t: todosCollection }).where(({ t }) => eq(t.completed, false)), + ) + + toggle(id: string) { + todosCollection.update(id, (draft) => { + draft.completed = !draft.completed + }) + } +} +``` + +All return values (`data`, `state`, `collection`, `status`, `isLoading`, +`isReady`, `isIdle`, `isError`, `isCleanedUp`) are Angular Signals. + +### injectLiveQuery — reactive parameters with signals + +Use the object form with `params` for reactive query parameters: + +```typescript +import { Component, signal } from '@angular/core' +import { injectLiveQuery } from '@tanstack/angular-db' +import { eq, gte, and } from '@tanstack/db' + +@Component({ + selector: 'app-filtered-todos', + template: ` + + +
    + @for (todo of query.data(); track todo.id) { +
  • {{ todo.text }}
  • + } +
+ `, +}) +export class FilteredTodosComponent { + status = signal('active') + minPriority = signal(0) + + query = injectLiveQuery({ + params: () => ({ + status: this.status(), + minPriority: this.minPriority(), + }), + query: ({ params, q }) => + q + .from({ t: todosCollection }) + .where(({ t }) => + and(eq(t.status, params.status), gte(t.priority, params.minPriority)), + ), + }) +} +``` + +The `params` function is a computed signal — the query re-runs whenever +any signal read inside `params` changes. + +## Angular-Specific Patterns + +### Using with Angular services + +Collections can be provided through Angular services for dependency +injection: + +```typescript +import { Injectable } from '@angular/core' +import { createCollection } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +@Injectable({ providedIn: 'root' }) +export class TodoService { + readonly collection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (todo) => todo.id, + }), + ) + + toggle(id: string) { + this.collection.update(id, (draft) => { + draft.completed = !draft.completed + }) + } +} +``` + +## Common Mistakes + +No Angular-specific silent failure modes beyond those covered in db-core. +The Angular adapter's signal-based approach handles reactivity tracking +through the `params` function. See db/core/live-queries and +db/core/mutations-optimistic for common mistakes that apply across all +frameworks. diff --git a/packages/playbooks/skills/db/core/SKILL.md b/packages/playbooks/skills/db/core/SKILL.md new file mode 100644 index 0000000..a986bb2 --- /dev/null +++ b/packages/playbooks/skills/db/core/SKILL.md @@ -0,0 +1,58 @@ +--- +name: db/core +description: > + Reactive client store with normalized collections, sub-millisecond live + queries via differential dataflow, and instant optimistic mutations. + Covers createCollection, query builder (from/where/join/select/groupBy/ + orderBy/limit), operators (eq/gt/lt/like/inArray), aggregates (count/sum/ + avg/min/max), createTransaction, createOptimisticAction, sync adapters. + Framework-agnostic core for @tanstack/db. +type: core +library: db +library_version: '0.5.29' +source_repository: 'https://github.com/TanStack/db' +--- + +# TanStack DB — Core Concepts + +TanStack DB is a reactive client-side data store that normalizes data into +typed collections, provides sub-millisecond live queries powered by +differential dataflow (d2ts), and gives instant optimistic mutations with +automatic rollback. It connects to any backend through sync adapters +(TanStack Query, ElectricSQL, PowerSync, RxDB, TrailBase) or runs purely +local. + +## Sub-Skills + +| Need to... | Read | +| ---------------------------------------------------------- | ------------------------------------- | +| Create and configure collections from any data source | db/core/collection-setup/SKILL.md | +| Build reactive queries with filters, joins, aggregations | db/core/live-queries/SKILL.md | +| Write data with optimistic updates and transactions | db/core/mutations-optimistic/SKILL.md | +| Configure sync, offline support, or build a custom adapter | db/core/sync-connectivity/SKILL.md | + +## Quick Decision Tree + +- Creating a collection or choosing which adapter to use? → db/core/collection-setup +- Querying, filtering, joining, or aggregating collection data? → db/core/live-queries +- Inserting, updating, or deleting items? → db/core/mutations-optimistic +- Configuring sync modes, offline, or building a custom adapter? → db/core/sync-connectivity +- Wiring queries into React components? → db/react/SKILL.md +- Wiring queries into Vue/Svelte/Solid/Angular? → db/[framework]/SKILL.md + +## Architecture + +Data flows in one direction: + +1. **Optimistic state** — mutations apply instantly to the local collection +2. **Persist** — mutation handler sends changes to the backend +3. **Sync** — backend confirms and streams updated state back +4. **Reconcile** — optimistic state is replaced by confirmed server state + +Live queries subscribe to collection changes and incrementally recompute +results via d2ts differential dataflow. A single-row update in a sorted +100k-item collection takes ~0.7ms (M1 Pro). + +## Version + +Targets @tanstack/db v0.5.29. diff --git a/packages/playbooks/skills/db/core/collection-setup/SKILL.md b/packages/playbooks/skills/db/core/collection-setup/SKILL.md new file mode 100644 index 0000000..38fa71b --- /dev/null +++ b/packages/playbooks/skills/db/core/collection-setup/SKILL.md @@ -0,0 +1,366 @@ +--- +name: db/core/collection-setup +description: > + Creating and configuring typed collections. Covers createCollection, + queryCollectionOptions, electricCollectionOptions, powerSyncCollectionOptions, + rxdbCollectionOptions, trailBaseCollectionOptions, localOnlyCollectionOptions, + localStorageCollectionOptions. CollectionConfig (id, getKey, schema, sync, + compare, autoIndex, startSync, gcTime, utils). StandardSchema integration + with Zod, Valibot, ArkType. Schema validation, TInput vs TOutput, + type transformations. Collection lifecycle and status tracking. +type: sub-skill +library: db +library_version: '0.5.29' +sources: + - 'TanStack/db:docs/overview.md' + - 'TanStack/db:docs/collections/query-collection.md' + - 'TanStack/db:docs/collections/electric-collection.md' + - 'TanStack/db:docs/guides/schemas.md' + - 'TanStack/db:packages/db/src/collection/collection.ts' + - 'TanStack/db:packages/db/src/collection/mutations.ts' +--- + +# Collection Setup & Schema + +## Setup + +Every collection needs a data source and a key extractor. The adapter you +choose depends on your backend: + +```typescript +import { createCollection } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: async () => { + const res = await fetch('/api/todos') + return res.json() + }, + getKey: (todo) => todo.id, + onUpdate: async ({ transaction }) => { + const { original, changes } = transaction.mutations[0] + await fetch(`/api/todos/${original.id}`, { + method: 'PATCH', + body: JSON.stringify(changes), + }) + }, + }), +) +``` + +## Core Patterns + +### Choosing the right collection adapter + +| Backend | Package | Options creator | +| -------------------------------- | ----------------------------------- | --------------------------------- | +| REST API / TanStack Query | `@tanstack/query-db-collection` | `queryCollectionOptions()` | +| ElectricSQL (real-time Postgres) | `@tanstack/electric-db-collection` | `electricCollectionOptions()` | +| PowerSync (SQLite sync) | `@tanstack/powersync-db-collection` | `powerSyncCollectionOptions()` | +| RxDB (local-first) | `@tanstack/rxdb-db-collection` | `rxdbCollectionOptions()` | +| TrailBase (self-hosted) | `@tanstack/trailbase-db-collection` | `trailBaseCollectionOptions()` | +| In-memory only | `@tanstack/db` | `localOnlyCollectionOptions()` | +| localStorage (cross-tab) | `@tanstack/db` | `localStorageCollectionOptions()` | + +Each adapter returns a config object spread into `createCollection`. Always +use the adapter matching your backend — it handles sync, handlers, and +utilities correctly. + +### Prototyping with localOnly then swapping to a real backend + +Start with `localOnlyCollectionOptions` and swap to a real adapter later. +The collection API is uniform — queries and components don't change: + +```typescript +import { createCollection, localOnlyCollectionOptions } from '@tanstack/db' + +// Prototype +const todosCollection = createCollection( + localOnlyCollectionOptions({ + getKey: (todo) => todo.id, + initialData: [{ id: '1', text: 'Buy milk', completed: false }], + }), +) + +// Later, swap to real backend — no query/component changes needed: +// const todosCollection = createCollection( +// queryCollectionOptions({ ... }) +// ) +``` + +### Adding schema validation with type transformations + +Use any StandardSchema-compatible library (Zod, Valibot, ArkType, Effect). +Schemas validate client mutations and can transform types: + +```typescript +import { createCollection } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' +import { z } from 'zod' + +const todoSchema = z.object({ + id: z.string(), + text: z.string().min(1), + completed: z.boolean().default(false), + created_at: z + .union([z.string(), z.date()]) + .transform((val) => (typeof val === 'string' ? new Date(val) : val)), +}) + +const todosCollection = createCollection( + queryCollectionOptions({ + schema: todoSchema, + queryKey: ['todos'], + queryFn: async () => fetch('/api/todos').then((r) => r.json()), + getKey: (todo) => todo.id, + }), +) +``` + +Schemas validate only client mutations (insert/update), not server data. + +### Persistent cross-tab state with localStorage + +```typescript +import { createCollection, localStorageCollectionOptions } from '@tanstack/db' + +const settingsCollection = createCollection( + localStorageCollectionOptions({ + id: 'user-settings', + storageKey: 'app-settings', + getKey: (item) => item.key, + }), +) + +// Direct mutations — no handlers needed +settingsCollection.insert({ key: 'theme', value: 'dark' }) +``` + +### Collection lifecycle + +Collections transition through statuses: +`idle` → `loading` → `ready` (or `error`) → `cleaned-up` + +```typescript +collection.status // 'idle' | 'loading' | 'ready' | 'error' | 'cleaned-up' +collection.isReady() // boolean + +// Wait for initial data before proceeding +await collection.preload() +``` + +## Common Mistakes + +### CRITICAL — queryFn returning empty array deletes all collection data + +Wrong: + +```typescript +queryCollectionOptions({ + queryFn: async () => { + const res = await fetch('/api/todos') + if (!res.ok) return [] // "safe" fallback + return res.json() + }, +}) +``` + +Correct: + +```typescript +queryCollectionOptions({ + queryFn: async () => { + const res = await fetch('/api/todos') + if (!res.ok) throw new Error(`Failed: ${res.status}`) + return res.json() + }, +}) +``` + +queryCollectionOptions treats the queryFn result as complete server state. +Returning `[]` means "server has zero items" and deletes everything from +the collection. Throw on errors instead. + +Source: docs/collections/query-collection.md — Full State Sync section + +### CRITICAL — Not knowing which collection type to use for a given backend + +Wrong: + +```typescript +import { createCollection } from '@tanstack/db' + +const todos = createCollection({ + id: 'todos', + getKey: (t) => t.id, + sync: { + /* manually wiring fetch + polling */ + }, +}) +``` + +Correct: + +```typescript +import { createCollection } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +const todos = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (t) => t.id, + }), +) +``` + +Each backend has a dedicated adapter that handles sync, mutation handlers, +and utilities. Use `queryCollectionOptions` for REST, `electricCollectionOptions` +for ElectricSQL, etc. See the adapter table above. + +Source: maintainer interview + +### HIGH — Using async schema validation + +Wrong: + +```typescript +const schema = z.object({ + email: z.string().refine(async (email) => { + const exists = await checkEmail(email) + return !exists + }), +}) +``` + +Correct: + +```typescript +const schema = z.object({ + email: z.string().email(), +}) +``` + +Schema validation must be synchronous. Returning a Promise throws +`SchemaMustBeSynchronousError`, but only at mutation time — not at +collection creation. Async validation belongs in your mutation handler. + +Source: packages/db/src/collection/mutations.ts:101 + +### HIGH — getKey returning undefined for some items + +Wrong: + +```typescript +createCollection( + queryCollectionOptions({ + getKey: (item) => item.metadata.id, + // throws UndefinedKeyError when metadata is null + }), +) +``` + +Correct: + +```typescript +createCollection( + queryCollectionOptions({ + getKey: (item) => item.id, + }), +) +``` + +If `getKey` returns `undefined` for any item, TanStack DB throws +`UndefinedKeyError`. Use a top-level property that exists on every item. + +Source: packages/db/src/collection/mutations.ts:148 + +### HIGH — TInput not a superset of TOutput with schema transforms + +Wrong: + +```typescript +const schema = z.object({ + created_at: z.string().transform((val) => new Date(val)), +}) +// TInput: { created_at: string } +// TOutput: { created_at: Date } +// Updates fail — draft.created_at is a Date, but schema expects string +``` + +Correct: + +```typescript +const schema = z.object({ + created_at: z + .union([z.string(), z.date()]) + .transform((val) => (typeof val === 'string' ? new Date(val) : val)), +}) +// TInput: { created_at: string | Date } ← accepts both +// TOutput: { created_at: Date } +``` + +When a schema transforms types, the input type for mutations must accept +both the pre-transform and post-transform types. Otherwise updates using +the draft proxy fail because the draft contains the transformed type. + +Source: docs/guides/schemas.md + +### HIGH — React Native missing crypto.randomUUID polyfill + +Wrong: + +```typescript +// App.tsx — React Native +import { createCollection } from '@tanstack/db' +// Crashes: crypto.randomUUID is not a function +``` + +Correct: + +```typescript +// App.tsx — React Native entry point +import 'react-native-random-uuid' +import { createCollection } from '@tanstack/db' +``` + +TanStack DB uses `crypto.randomUUID()` internally. React Native doesn't +provide this API — install and import `react-native-random-uuid` at your +entry point. + +Source: docs/overview.md — React Native section + +### MEDIUM — Providing both explicit type parameter and schema + +Wrong: + +```typescript +interface Todo { + id: string + text: string +} +const collection = createCollection( + queryCollectionOptions({ + schema: todoSchema, // also infers types + // conflicting type constraints + }), +) +``` + +Correct: + +```typescript +const collection = createCollection( + queryCollectionOptions({ + schema: todoSchema, // types inferred from schema + }), +) +``` + +When a schema is provided, the collection infers types from it. Also +passing an explicit generic creates conflicting type constraints. Use +one or the other. + +Source: docs/overview.md diff --git a/packages/playbooks/skills/db/core/live-queries/SKILL.md b/packages/playbooks/skills/db/core/live-queries/SKILL.md new file mode 100644 index 0000000..f98f38f --- /dev/null +++ b/packages/playbooks/skills/db/core/live-queries/SKILL.md @@ -0,0 +1,401 @@ +--- +name: db/core/live-queries +description: > + Building reactive queries across collections. Covers Query builder fluent + API (from/where/join/select/groupBy/having/orderBy/limit/offset/distinct/ + findOne). Comparison operators (eq, gt, gte, lt, lte, like, ilike, + inArray, isNull, isUndefined). Logical operators (and, or, not). Aggregate + functions (count, sum, avg, min, max). String functions (upper, lower, + length, concat, coalesce). Math (add, subtract, multiply, divide). Join + types (inner, left, right, full). Derived collections. + createLiveQueryCollection. $selected namespace. Predicate push-down. + Incremental view maintenance via d2ts. +type: sub-skill +library: db +library_version: '0.5.29' +sources: + - 'TanStack/db:docs/guides/live-queries.md' + - 'TanStack/db:packages/db/src/query/builder/index.ts' + - 'TanStack/db:packages/db/src/query/compiler/index.ts' +--- + +# Live Query Construction + +## Setup + +Live queries use a fluent SQL-like builder. Query results are reactive — +they update automatically when underlying collection data changes. + +```typescript +import { createCollection, liveQueryCollectionOptions, eq } from '@tanstack/db' + +const activeUsers = createCollection( + liveQueryCollectionOptions({ + query: (q) => + q + .from({ user: usersCollection }) + .where(({ user }) => eq(user.active, true)) + .select(({ user }) => ({ + id: user.id, + name: user.name, + email: user.email, + })), + }), +) +``` + +For React/Vue/Svelte/Solid/Angular, use the framework hooks instead +(see db/react/SKILL.md or the relevant framework skill). + +## Core Patterns + +### Filtering with WHERE and operators + +All operators return expression objects for the query engine. Import them +from `@tanstack/db`: + +```typescript +import { + eq, + gt, + lt, + gte, + lte, + and, + or, + not, + like, + ilike, + inArray, + isNull, + isUndefined, +} from '@tanstack/db' + +// Single condition +q.from({ t: todos }).where(({ t }) => eq(t.completed, false)) + +// Multiple conditions with AND +q.from({ t: todos }).where(({ t }) => + and(eq(t.completed, false), gt(t.priority, 3)), +) + +// OR conditions +q.from({ t: todos }).where(({ t }) => + or(eq(t.status, 'urgent'), and(gt(t.priority, 4), eq(t.completed, false))), +) + +// Pattern matching +q.from({ u: users }).where(({ u }) => ilike(u.email, '%@example.com')) + +// Null checks +q.from({ t: todos }).where(({ t }) => not(isNull(t.assignee))) + +// Array membership +q.from({ t: todos }).where(({ t }) => + inArray(t.status, ['open', 'in-progress']), +) +``` + +### Projections with SELECT and computed fields + +```typescript +import { + upper, + concat, + coalesce, + add, + count, + sum, + avg, + min, + max, + length, +} from '@tanstack/db' + +// Project specific fields +q.from({ u: users }).select(({ u }) => ({ + id: u.id, + displayName: upper(u.name), + fullName: concat(u.firstName, ' ', u.lastName), + score: add(u.baseScore, u.bonus), + status: coalesce(u.status, 'unknown'), +})) + +// Aggregations with GROUP BY +q.from({ o: orders }) + .groupBy(({ o }) => o.customerId) + .select(({ o }) => ({ + customerId: o.customerId, + totalSpent: sum(o.amount), + orderCount: count(), + avgOrder: avg(o.amount), + lastOrder: max(o.createdAt), + })) + .having(({ $selected }) => gt($selected.totalSpent, 1000)) +``` + +Use `$selected` to reference SELECT fields in HAVING and ORDER BY clauses. + +### Joining collections + +Only equality joins are supported (d2ts differential dataflow constraint): + +```typescript +import { eq } from '@tanstack/db' + +// Inner join +q.from({ o: orders }) + .join({ c: customers }, ({ o, c }) => eq(o.customerId, c.id)) + .select(({ o, c }) => ({ + orderId: o.id, + amount: o.amount, + customerName: c.name, + })) + +// Left join (keeps all orders, customer may be null) +q.from({ o: orders }) + .leftJoin({ c: customers }, ({ o, c }) => eq(o.customerId, c.id)) + .select(({ o, c }) => ({ + orderId: o.id, + customerName: coalesce(c.name, 'Unknown'), + })) +``` + +Join types: `.join()` (inner), `.leftJoin()`, `.rightJoin()`, `.fullJoin()`. + +### Sorting, pagination, and findOne + +```typescript +// Sort and paginate (orderBy is REQUIRED for limit/offset) +q.from({ t: todos }) + .orderBy(({ t }) => t.createdAt, 'desc') + .limit(20) + .offset(40) + +// Get a single item +q.from({ t: todos }) + .where(({ t }) => eq(t.id, selectedId)) + .findOne() +``` + +### Derived collections + +Query results are themselves collections that can be queried further: + +```typescript +const activeUsers = createCollection( + liveQueryCollectionOptions({ + query: (q) => + q.from({ u: usersCollection }).where(({ u }) => eq(u.active, true)), + }), +) + +// Query the derived collection +const topActiveUsers = createCollection( + liveQueryCollectionOptions({ + query: (q) => + q + .from({ u: activeUsers }) + .orderBy(({ u }) => u.score, 'desc') + .limit(10), + }), +) +``` + +## Common Mistakes + +### CRITICAL — Using === instead of eq() in where clauses + +Wrong: + +```typescript +q.from({ t: todos }).where(({ t }) => t.completed === false) +``` + +Correct: + +```typescript +import { eq } from '@tanstack/db' + +q.from({ t: todos }).where(({ t }) => eq(t.completed, false)) +``` + +JavaScript `===` returns a boolean, not an expression object. The query +engine cannot build a filter predicate from a plain boolean. This throws +`InvalidWhereExpressionError`. + +Source: packages/db/src/query/builder/index.ts:375 + +### CRITICAL — Filtering or transforming data in JS instead of using query operators + +Wrong: + +```typescript +const { data } = useLiveQuery((q) => q.from({ t: todos })) +// Then in render: +const filtered = data.filter((t) => t.priority > 3) +const sorted = filtered.sort((a, b) => b.createdAt - a.createdAt) +``` + +Correct: + +```typescript +const { data } = useLiveQuery((q) => + q + .from({ t: todos }) + .where(({ t }) => gt(t.priority, 3)) + .orderBy(({ t }) => t.createdAt, 'desc'), +) +``` + +JS `.filter()`, `.map()`, `.sort()`, `.reduce()` re-run from scratch on +every change. Query builder operators are incrementally maintained via +d2ts — only deltas are recomputed. Always slower to use JS, even for +trivial cases. + +Source: maintainer interview + +### HIGH — Not using the full set of available query operators + +Wrong: + +```typescript +// Using JS for string operations +const { data } = useLiveQuery((q) => q.from({ u: users })) +const display = data.map((u) => ({ + ...u, + name: u.name.toUpperCase(), + label: `${u.firstName} ${u.lastName}`, +})) +``` + +Correct: + +```typescript +const { data } = useLiveQuery((q) => + q.from({ u: users }).select(({ u }) => ({ + id: u.id, + name: upper(u.name), + label: concat(u.firstName, ' ', u.lastName), + })), +) +``` + +The library has `upper`, `lower`, `length`, `concat`, `coalesce`, `add`, +plus all aggregate functions. Every operator is incrementally maintained. +Prefer query operators over JS equivalents. + +Source: maintainer interview + +### HIGH — Using .distinct() without .select() + +Wrong: + +```typescript +q.from({ t: todos }).distinct() +``` + +Correct: + +```typescript +q.from({ t: todos }) + .select(({ t }) => ({ status: t.status })) + .distinct() +``` + +`distinct()` deduplicates by the selected object shape. Without `select()`, +the shape is the full row, and the engine throws `DistinctRequiresSelectError`. + +Source: packages/db/src/query/compiler/index.ts:218 + +### HIGH — Using .having() without .groupBy() + +Wrong: + +```typescript +q.from({ o: orders }) + .select(({ o }) => ({ total: sum(o.amount) })) + .having(({ $selected }) => gt($selected.total, 100)) +``` + +Correct: + +```typescript +q.from({ o: orders }) + .groupBy(({ o }) => o.customerId) + .select(({ o }) => ({ + customerId: o.customerId, + total: sum(o.amount), + })) + .having(({ $selected }) => gt($selected.total, 100)) +``` + +HAVING filters aggregated groups. Without GROUP BY there are no groups, +throwing `HavingRequiresGroupByError`. + +Source: packages/db/src/query/compiler/index.ts:293 + +### HIGH — Using .limit() or .offset() without .orderBy() + +Wrong: + +```typescript +q.from({ t: todos }).limit(10) +``` + +Correct: + +```typescript +q.from({ t: todos }) + .orderBy(({ t }) => t.createdAt, 'desc') + .limit(10) +``` + +Without deterministic ordering, limit/offset results are non-deterministic +and cannot be incrementally maintained. Throws `LimitOffsetRequireOrderByError`. + +Source: packages/db/src/query/compiler/index.ts:356 + +### HIGH — Join condition using operator other than eq() + +Wrong: + +```typescript +q.from({ o: orders }).join({ c: customers }, ({ o, c }) => + gt(o.amount, c.minOrder), +) +``` + +Correct: + +```typescript +q.from({ o: orders }) + .join({ c: customers }, ({ o, c }) => eq(o.customerId, c.id)) + .where(({ o, c }) => gt(o.amount, c.minOrder)) +``` + +The d2ts join operator only supports equality conditions. Non-equality +predicates throw `JoinConditionMustBeEqualityError`. Move non-equality +conditions to `.where()`. + +Source: packages/db/src/query/builder/index.ts:216 + +### MEDIUM — Passing source directly instead of {alias: collection} + +Wrong: + +```typescript +q.from(todosCollection) +``` + +Correct: + +```typescript +q.from({ todos: todosCollection }) +``` + +`.from()` and `.join()` require sources wrapped as `{alias: collection}`. +The alias is how you reference fields in subsequent clauses. Passing the +collection directly throws `InvalidSourceTypeError`. + +Source: packages/db/src/query/builder/index.ts:79-96 diff --git a/packages/playbooks/skills/db/core/mutations-optimistic/SKILL.md b/packages/playbooks/skills/db/core/mutations-optimistic/SKILL.md new file mode 100644 index 0000000..db5e18d --- /dev/null +++ b/packages/playbooks/skills/db/core/mutations-optimistic/SKILL.md @@ -0,0 +1,463 @@ +--- +name: db/core/mutations-optimistic +description: > + Writing data to collections with instant optimistic feedback. Covers + collection.insert(), collection.update() with Immer-style draft proxy, + collection.delete(). createOptimisticAction, createPacedMutations with + debounceStrategy/throttleStrategy/queueStrategy. createTransaction for + manual transaction control. getActiveTransaction for ambient context. + Transaction lifecycle (pending/persisting/completed/failed), transaction + stacking, mutation merging, PendingMutation type, rollback, isPersisted. +type: sub-skill +library: db +library_version: '0.5.29' +sources: + - 'TanStack/db:docs/guides/mutations.md' + - 'TanStack/db:packages/db/src/collection/mutations.ts' + - 'TanStack/db:packages/db/src/transactions.ts' + - 'TanStack/db:packages/db/src/optimistic-action.ts' +--- + +# Mutations & Optimistic State + +## Setup + +Mutations apply optimistically — the UI updates instantly, then the handler +persists to the backend. If the handler fails, the optimistic state +automatically rolls back. + +```typescript +import { createCollection } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (todo) => todo.id, + onInsert: async ({ transaction }) => { + const item = transaction.mutations[0].modified + await fetch('/api/todos', { + method: 'POST', + body: JSON.stringify(item), + }) + }, + onUpdate: async ({ transaction }) => { + const { original, changes } = transaction.mutations[0] + await fetch(`/api/todos/${original.id}`, { + method: 'PATCH', + body: JSON.stringify(changes), + }) + }, + onDelete: async ({ transaction }) => { + const { original } = transaction.mutations[0] + await fetch(`/api/todos/${original.id}`, { method: 'DELETE' }) + }, + }), +) +``` + +## Core Patterns + +### Insert, update, and delete + +```typescript +// Insert — pass the full object +todosCollection.insert({ + id: crypto.randomUUID(), + text: 'Buy milk', + completed: false, +}) + +// Update — mutate the draft (Immer-style proxy) +todosCollection.update(todo.id, (draft) => { + draft.completed = true + draft.completedAt = new Date() +}) + +// Delete +todosCollection.delete(todo.id) + +// Batch operations +todosCollection.insert([item1, item2, item3]) +todosCollection.delete([id1, id2, id3]) +``` + +Every mutation returns a `Transaction` object for tracking persistence. + +### Awaiting persistence + +```typescript +const tx = todosCollection.update(todo.id, (draft) => { + draft.text = 'Updated text' +}) + +// tx.isPersisted is a Deferred — await its .promise +try { + await tx.isPersisted.promise + console.log('Saved to server') +} catch (error) { + console.log('Failed — optimistic state was rolled back') +} +``` + +### Custom optimistic actions + +For complex mutations that span multiple operations or need custom +optimistic logic: + +```typescript +import { createOptimisticAction } from '@tanstack/db' + +const addTodo = createOptimisticAction({ + onMutate: (text: string) => { + // Synchronous — applies optimistic state immediately + todosCollection.insert({ + id: crypto.randomUUID(), + text, + completed: false, + }) + }, + mutationFn: async (text, { transaction }) => { + await fetch('/api/todos', { + method: 'POST', + body: JSON.stringify({ text }), + }) + // Refetch to get server-generated fields + await todosCollection.utils.refetch() + }, +}) + +// Usage +const tx = addTodo('Buy milk') +await tx.isPersisted.promise +``` + +### Manual transactions across multiple collections + +```typescript +import { createTransaction } from '@tanstack/db' + +const tx = createTransaction({ + mutationFn: async ({ transaction }) => { + for (const mutation of transaction.mutations) { + if (mutation.type === 'insert') { + await api.create(mutation.modified) + } else if (mutation.type === 'update') { + await api.update(mutation.original.id, mutation.changes) + } + } + }, +}) + +// Group mutations into one transaction +tx.mutate(() => { + todosCollection.insert({ id: '1', text: 'Task A', completed: false }) + projectsCollection.update('proj-1', (draft) => { + draft.taskCount += 1 + }) +}) + +await tx.commit() +``` + +### Paced mutations for real-time editing + +```typescript +import { createPacedMutations, debounceStrategy } from '@tanstack/db' + +const updateTitle = createPacedMutations({ + onMutate: ({ id, title }: { id: string; title: string }) => { + todosCollection.update(id, (draft) => { + draft.title = title + }) + }, + mutationFn: async ({ id, title }) => { + await fetch(`/api/todos/${id}`, { + method: 'PATCH', + body: JSON.stringify({ title }), + }) + }, + strategy: debounceStrategy({ wait: 500 }), +}) + +// Each keystroke calls this — only persists after 500ms pause +updateTitle({ id: todo.id, title: newValue }) +``` + +Strategies: `debounceStrategy`, `throttleStrategy`, `queueStrategy`. + +### Transaction lifecycle and mutation merging + +Transaction states: `pending` → `persisting` → `completed` | `failed` + +Multiple mutations on the same item within a transaction merge: + +- insert + update → insert (with merged fields) +- insert + delete → cancelled (both removed) +- update + update → update (union of changes) +- update + delete → delete + +### PendingMutation structure + +Inside `mutationFn`, access mutation details via `transaction.mutations`: + +```typescript +mutationFn: async ({ transaction }) => { + for (const mutation of transaction.mutations) { + mutation.type // 'insert' | 'update' | 'delete' + mutation.original // pre-mutation state (empty object for inserts) + mutation.modified // post-mutation state + mutation.changes // only the changed fields (for updates) + mutation.key // item key + mutation.collection // source collection + } +} +``` + +## Common Mistakes + +### CRITICAL — Passing a new object to update() instead of mutating the draft + +Wrong: + +```typescript +todosCollection.update(todo.id, { ...todo, completed: true }) +``` + +Correct: + +```typescript +todosCollection.update(todo.id, (draft) => { + draft.completed = true +}) +``` + +The update API uses an Immer-style draft proxy. The second argument must +be a callback that mutates the draft, not a replacement object. This is +the single most common mutation mistake. + +Source: maintainer interview + +### CRITICAL — Hallucinating mutation API signatures + +Wrong: + +```typescript +// Invented signatures that look plausible but are wrong +todosCollection.update(todo.id, { title: 'new' }) +todosCollection.upsert(todo) +createTransaction({ onSuccess: () => {} }) +transaction.mutations[0].data // wrong property name +``` + +Correct: + +```typescript +todosCollection.update(todo.id, (draft) => { + draft.title = 'new' +}) +todosCollection.insert(todo) +createTransaction({ mutationFn: async ({ transaction }) => {} }) +transaction.mutations[0].changes // correct property name +``` + +Read the actual API before writing mutation code. Key signatures: + +- `update(key, (draft) => void)` — draft callback, not object +- `insert(item)` — not upsert +- `createTransaction({ mutationFn })` — not onSuccess +- `mutation.changes` — not mutation.data + +Source: maintainer interview + +### CRITICAL — onMutate callback returning a Promise + +Wrong: + +```typescript +createOptimisticAction({ + onMutate: async (text) => { + const id = await generateId() + todosCollection.insert({ id, text, completed: false }) + }, + mutationFn: async (text) => { + /* ... */ + }, +}) +``` + +Correct: + +```typescript +createOptimisticAction({ + onMutate: (text) => { + const id = crypto.randomUUID() + todosCollection.insert({ id, text, completed: false }) + }, + mutationFn: async (text) => { + /* ... */ + }, +}) +``` + +`onMutate` must be synchronous — optimistic state needs to apply in the +current tick. Returning a Promise throws `OnMutateMustBeSynchronousError`. + +Source: packages/db/src/optimistic-action.ts:75 + +### CRITICAL — Calling insert/update/delete without handler or ambient transaction + +Wrong: + +```typescript +const collection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (t) => t.id, + // No onInsert handler + }), +) + +collection.insert({ id: '1', text: 'test', completed: false }) +// Throws MissingInsertHandlerError +``` + +Correct: + +```typescript +const collection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (t) => t.id, + onInsert: async ({ transaction }) => { + await api.createTodo(transaction.mutations[0].modified) + }, + }), +) + +collection.insert({ id: '1', text: 'test', completed: false }) +``` + +Collection mutations require either an `onInsert`/`onUpdate`/`onDelete` +handler or an ambient transaction from `createTransaction`. Without either, +throws `MissingInsertHandlerError` (or the Update/Delete variant). + +Source: packages/db/src/collection/mutations.ts:166 + +### HIGH — Calling .mutate() after transaction is no longer pending + +Wrong: + +```typescript +const tx = createTransaction({ mutationFn: async () => {} }) +tx.mutate(() => { + todosCollection.insert(item1) +}) +await tx.commit() +tx.mutate(() => { + todosCollection.insert(item2) +}) +// Throws TransactionNotPendingMutateError +``` + +Correct: + +```typescript +const tx = createTransaction({ + autoCommit: false, + mutationFn: async () => {}, +}) +tx.mutate(() => { + todosCollection.insert(item1) +}) +tx.mutate(() => { + todosCollection.insert(item2) +}) +await tx.commit() +``` + +Transactions only accept mutations while in `pending` state. After +`commit()` or `rollback()`, calling `mutate()` throws. Use +`autoCommit: false` to batch multiple `mutate()` calls. + +Source: packages/db/src/transactions.ts:289 + +### HIGH — Attempting to change an item's primary key via update + +Wrong: + +```typescript +todosCollection.update('old-id', (draft) => { + draft.id = 'new-id' +}) +``` + +Correct: + +```typescript +todosCollection.delete('old-id') +todosCollection.insert({ ...todo, id: 'new-id' }) +``` + +Primary keys are immutable. The update proxy detects key changes and +throws `KeyUpdateNotAllowedError`. To change a key, delete and re-insert. + +Source: packages/db/src/collection/mutations.ts:352 + +### HIGH — Inserting item with duplicate key + +Wrong: + +```typescript +todosCollection.insert({ id: 'abc', text: 'First' }) +todosCollection.insert({ id: 'abc', text: 'Second' }) +// Throws DuplicateKeyError +``` + +Correct: + +```typescript +todosCollection.insert({ id: crypto.randomUUID(), text: 'First' }) +todosCollection.insert({ id: crypto.randomUUID(), text: 'Second' }) +``` + +If an item with the same key exists (synced or optimistic), throws +`DuplicateKeyError`. Use unique IDs — `crypto.randomUUID()` or a +server-generated ID. + +Source: packages/db/src/collection/mutations.ts:181 + +### HIGH — Not awaiting refetch after mutation in query collection handler + +Wrong: + +```typescript +queryCollectionOptions({ + onInsert: async ({ transaction }) => { + await api.createTodo(transaction.mutations[0].modified) + // Handler resolves → optimistic state dropped + // Server state hasn't arrived yet → flash of missing data + }, +}) +``` + +Correct: + +```typescript +queryCollectionOptions({ + onInsert: async ({ transaction }) => { + await api.createTodo(transaction.mutations[0].modified) + await todosCollection.utils.refetch() + // Server state is now in the collection before optimistic state drops + }, +}) +``` + +Optimistic state is held until the handler resolves. If you don't await +the refetch, the optimistic state drops before server state arrives, +causing a brief flash of missing data. + +Source: docs/overview.md — optimistic state lifecycle diff --git a/packages/playbooks/skills/db/core/sync-connectivity/SKILL.md b/packages/playbooks/skills/db/core/sync-connectivity/SKILL.md new file mode 100644 index 0000000..f7b0418 --- /dev/null +++ b/packages/playbooks/skills/db/core/sync-connectivity/SKILL.md @@ -0,0 +1,438 @@ +--- +name: db/core/sync-connectivity +description: > + Managing data synchronization between collections and backends. Covers + sync modes (eager, on-demand, progressive). SyncConfig interface (begin, + write, commit, markReady, truncate). Electric txid tracking with + awaitTxId/awaitMatch. Query direct writes (writeInsert, writeUpdate, + writeDelete, writeUpsert, writeBatch). PowerSync SQLite persistence. + RxDB Observable-driven sync. TrailBase event streaming. + @tanstack/offline-transactions (OfflineExecutor, outbox, IndexedDB, + localStorage). Leader election, online detection, + collection options creator pattern. +type: sub-skill +library: db +library_version: '0.5.29' +sources: + - 'TanStack/db:docs/collections/electric-collection.md' + - 'TanStack/db:docs/collections/query-collection.md' + - 'TanStack/db:docs/guides/collection-options-creator.md' + - 'TanStack/db:packages/db/src/collection/sync.ts' +--- + +# Sync & Connectivity + +## Setup + +Every collection has a sync configuration that connects it to a data +source. The adapter options creators handle this — you rarely write +`SyncConfig` directly unless building a custom adapter. + +```typescript +import { createCollection } from '@tanstack/db' +import { electricCollectionOptions } from '@tanstack/electric-db-collection' + +const todosCollection = createCollection( + electricCollectionOptions({ + shapeOptions: { + url: 'http://localhost:3000/v1/shape', + params: { table: 'todos' }, + }, + getKey: (todo) => todo.id, + onUpdate: async ({ transaction }) => { + const { original, changes } = transaction.mutations[0] + const result = await sql` + UPDATE todos SET ${sql(changes)} + WHERE id = ${original.id} + RETURNING pg_current_xact_id()::text AS txid + ` + await todosCollection.utils.awaitTxId(result[0].txid) + }, + }), +) +``` + +## Core Patterns + +### Sync modes + +| Mode | When to use | How it works | +| ----------------- | ------------------------------------- | --------------------------------------------------------------- | +| `eager` (default) | < 10k rows of relatively static data | Loads entire collection upfront | +| `on-demand` | > 50k rows, search interfaces | Loads only what active queries request | +| `progressive` | Need immediate results + full dataset | Loads query subset first, then syncs full dataset in background | + +```typescript +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +const productsCollection = createCollection( + queryCollectionOptions({ + queryKey: ['products'], + queryFn: async (ctx) => { + // On-demand: ctx.meta.loadSubsetOptions contains query predicates + const params = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions) + return api.getProducts(params) + }, + syncMode: 'on-demand', + getKey: (p) => p.id, + }), +) +``` + +### Electric txid tracking + +With ElectricSQL, track the transaction ID to prevent optimistic state +flash — hold optimistic state until the sync stream catches up: + +```typescript +electricCollectionOptions({ + shapeOptions: { url: ELECTRIC_URL, params: { table: 'todos' } }, + getKey: (t) => t.id, + onInsert: async ({ transaction }) => { + const item = transaction.mutations[0].modified + const result = await sql` + INSERT INTO todos (id, text, completed) + VALUES (${item.id}, ${item.text}, ${item.completed}) + RETURNING pg_current_xact_id()::text AS txid + ` + // Hold optimistic state until Electric streams this txid + await todosCollection.utils.awaitTxId(result[0].txid) + }, +}) +``` + +### Query collection direct writes + +For query-backed collections, you can update the local collection without +a full refetch using direct write methods: + +```typescript +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +const collection = createCollection( + queryCollectionOptions({ + queryKey: ['items'], + queryFn: () => api.getItems(), + getKey: (item) => item.id, + onInsert: async ({ transaction }) => { + const item = transaction.mutations[0].modified + const saved = await api.createItem(item) + // Write the server response directly instead of refetching + collection.utils.writeUpsert(saved) + }, + }), +) +``` + +Direct write methods: `writeInsert`, `writeUpdate`, `writeDelete`, +`writeUpsert`, `writeBatch`. + +### Building a custom sync adapter + +Implement the `SyncConfig` interface. Key requirement: subscribe to +changes BEFORE the initial fetch to prevent race conditions. + +```typescript +function myCollectionOptions(config: MyConfig) { + return { + getKey: config.getKey, + sync: { + sync: ({ begin, write, commit, markReady }) => { + const eventBuffer: Array = [] + let initialSyncDone = false + + // 1. Subscribe to live changes FIRST + const unsub = config.subscribe((event) => { + if (!initialSyncDone) { + eventBuffer.push(event) + return + } + begin() + write({ key: event.key, type: event.type, value: event.data }) + commit() + }) + + // 2. Then fetch initial data + config.fetchAll().then((items) => { + begin() + for (const item of items) { + write({ key: config.getKey(item), type: 'insert', value: item }) + } + commit() + + // 3. Flush buffered events + initialSyncDone = true + if (eventBuffer.length > 0) { + begin() + for (const event of eventBuffer) { + write({ key: event.key, type: event.type, value: event.data }) + } + commit() + } + + // 4. ALWAYS call markReady + markReady() + }) + + return () => unsub() + }, + }, + } +} +``` + +### Offline transactions + +For apps that need offline support, `@tanstack/offline-transactions` +provides a persistent outbox integrated with the TanStack DB transaction +model: + +```typescript +import { + startOfflineExecutor, + IndexedDBAdapter, + WebLocksLeader, +} from '@tanstack/offline-transactions' + +const executor = startOfflineExecutor({ + storage: new IndexedDBAdapter({ dbName: 'my-app-offline' }), + leaderElection: new WebLocksLeader(), + retryPolicy: { maxRetries: 5, backoff: 'exponential' }, +}) +``` + +Only adopt offline transactions when you genuinely need offline support. +It adds complexity — PowerSync and RxDB handle their own local +persistence, which is a separate concern from offline transaction queuing. + +## Common Mistakes + +### CRITICAL — Electric txid queried outside mutation transaction + +Wrong: + +```typescript +onInsert: async ({ transaction }) => { + const item = transaction.mutations[0].modified + await sql`INSERT INTO todos VALUES (${item.id}, ${item.text})` + // Separate query = separate transaction = wrong txid + const result = await sql`SELECT pg_current_xact_id()::text AS txid` + await collection.utils.awaitTxId(result[0].txid) +}, +``` + +Correct: + +```typescript +onInsert: async ({ transaction }) => { + const item = transaction.mutations[0].modified + const result = await sql` + INSERT INTO todos VALUES (${item.id}, ${item.text}) + RETURNING pg_current_xact_id()::text AS txid + ` + await collection.utils.awaitTxId(result[0].txid) +}, +``` + +`pg_current_xact_id()` must be queried INSIDE the same SQL transaction +as the mutation. A separate query runs in its own transaction, returning a +different txid. `awaitTxId` then waits for a txid that will never arrive +in the sync stream — it stalls forever. + +Source: docs/collections/electric-collection.md — Debugging txid section + +### CRITICAL — Not calling markReady() in custom sync implementation + +Wrong: + +```typescript +sync: ({ begin, write, commit }) => { + fetchData().then((items) => { + begin() + items.forEach((item) => write({ type: 'insert', value: item })) + commit() + // Forgot markReady() — collection stays in 'loading' forever + }) +} +``` + +Correct: + +```typescript +sync: ({ begin, write, commit, markReady }) => { + fetchData() + .then((items) => { + begin() + items.forEach((item) => write({ type: 'insert', value: item })) + commit() + markReady() + }) + .catch(() => { + markReady() // Call even on error + }) +} +``` + +`markReady()` transitions the collection from `loading` to `ready`. Without +it, live queries never resolve and `useLiveSuspenseQuery` hangs in Suspense +forever. Always call `markReady()`, even on error. + +Source: docs/guides/collection-options-creator.md + +### CRITICAL — queryFn returning partial data without merging + +Wrong: + +```typescript +queryCollectionOptions({ + queryFn: async () => { + // Only returns items modified since last fetch + return api.getModifiedSince(lastFetchTime) + }, +}) +``` + +Correct: + +```typescript +queryCollectionOptions({ + queryFn: async () => { + // Returns the complete current state + return api.getAllItems() + }, +}) +``` + +`queryFn` result is treated as the complete server state. Returning only +new/changed items causes all non-returned items to be deleted from the +collection. For incremental fetches, use direct writes (`writeUpsert`, +`writeBatch`) instead. + +Source: docs/collections/query-collection.md — Handling Partial/Incremental Fetches + +### HIGH — Race condition: subscribing after initial fetch loses changes + +Wrong: + +```typescript +sync: ({ begin, write, commit, markReady }) => { + // Fetch first + fetchAll().then((items) => { + begin() + items.forEach((item) => write({ type: 'insert', value: item })) + commit() + markReady() + }) + // Subscribe after — changes during fetch are lost + subscribe((event) => { + begin() + write({ type: event.type, value: event.data }) + commit() + }) +} +``` + +Correct: + +```typescript +sync: ({ begin, write, commit, markReady }) => { + const buffer: any[] = [] + let ready = false + + // Subscribe FIRST, buffer events during initial fetch + const unsub = subscribe((event) => { + if (!ready) { + buffer.push(event) + return + } + begin() + write({ type: event.type, value: event.data }) + commit() + }) + + fetchAll().then((items) => { + begin() + items.forEach((item) => write({ type: 'insert', value: item })) + commit() + ready = true + buffer.forEach((event) => { + begin() + write({ type: event.type, value: event.data }) + commit() + }) + markReady() + }) + + return () => unsub() +} +``` + +Subscribe to live changes before the initial fetch. Buffer events during +the fetch, then replay them. Otherwise changes that occur during the +initial fetch window are silently lost. + +Source: docs/guides/collection-options-creator.md — Race condition prevention + +### HIGH — write() called without begin() in sync implementation + +Wrong: + +```typescript +sync: ({ write, commit, markReady }) => { + fetchAll().then((items) => { + items.forEach((item) => write({ type: 'insert', value: item })) + commit() + markReady() + }) +} +``` + +Correct: + +```typescript +sync: ({ begin, write, commit, markReady }) => { + fetchAll().then((items) => { + begin() + items.forEach((item) => write({ type: 'insert', value: item })) + commit() + markReady() + }) +} +``` + +Sync data must be written within a transaction: `begin()` → `write()` → +`commit()`. Calling `write()` without `begin()` throws +`NoPendingSyncTransactionWriteError`. + +Source: packages/db/src/collection/sync.ts:110 + +### MEDIUM — Direct writes overridden by next query sync + +Wrong: + +```typescript +// Write directly, but next queryFn execution overwrites it +collection.utils.writeInsert(newItem) +// queryFn runs on refetch interval → returns server state without newItem +// newItem disappears +``` + +Correct: + +```typescript +// Option 1: Ensure server has the item before next refetch +await api.createItem(newItem) +collection.utils.writeInsert(newItem) + +// Option 2: Coordinate with staleTime to delay refetch +queryCollectionOptions({ + staleTime: 30000, // 30s before refetch +}) +``` + +Direct writes update the collection immediately, but the next `queryFn` +execution returns the complete server state, overwriting direct writes. +Either ensure the server has the data before the next refetch, or +coordinate `staleTime` to delay it. + +Source: docs/collections/query-collection.md — Direct Writes and Query Sync diff --git a/packages/playbooks/skills/db/react/SKILL.md b/packages/playbooks/skills/db/react/SKILL.md new file mode 100644 index 0000000..6ab3e3a --- /dev/null +++ b/packages/playbooks/skills/db/react/SKILL.md @@ -0,0 +1,337 @@ +--- +name: db/react +description: > + React bindings for TanStack DB. Covers useLiveQuery, useLiveSuspenseQuery, + useLiveInfiniteQuery, usePacedMutations hooks. Dependency arrays for + reactive query parameters. React Suspense integration with Error Boundaries. + Infinite query pagination with cursor-based loading. Return shape (data, + state, collection, status, isLoading, isReady, isError). +type: framework +library: db +framework: react +library_version: '0.5.29' +requires: + - db/core +--- + +This skill builds on db-core. Read db-core first for collection setup, +query builder syntax, operators, mutations, and sync concepts. + +# TanStack DB — React + +## Setup + +Install both the core and React packages: + +```bash +npm install @tanstack/db @tanstack/react-db +``` + +Plus your sync adapter (e.g. `@tanstack/query-db-collection`). + +Collections are created outside components — typically in a dedicated +module: + +```typescript +// collections/todos.ts +import { createCollection, eq } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +export const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (todo) => todo.id, + onUpdate: async ({ transaction }) => { + const { original, changes } = transaction.mutations[0] + await fetch(`/api/todos/${original.id}`, { + method: 'PATCH', + body: JSON.stringify(changes), + }) + }, + }), +) +``` + +## Hooks + +### useLiveQuery + +Subscribes to a live query and re-renders when results change: + +```typescript +import { useLiveQuery } from '@tanstack/react-db' +import { eq, gt, and } from '@tanstack/db' + +function TodoList({ userId }: { userId: string }) { + const { data, status, isLoading, isReady, isError } = useLiveQuery( + (q) => + q + .from({ t: todosCollection }) + .where(({ t }) => and(eq(t.userId, userId), eq(t.completed, false))) + .orderBy(({ t }) => t.createdAt, 'desc'), + [userId] + ) + + if (isLoading) return
Loading...
+ if (isError) return
Error loading todos
+ + return ( +
    + {data.map((todo) => ( +
  • {todo.text}
  • + ))} +
+ ) +} +``` + +Return shape: + +- `data` — query results array (empty array while loading) +- `state` — Map of key → item for direct lookups +- `collection` — the derived collection instance (or null) +- `status` — `'idle'` | `'loading'` | `'ready'` | `'error'` | `'cleaned-up'` | `'disabled'` +- `isLoading`, `isReady`, `isError`, `isIdle`, `isCleanedUp`, `isEnabled` — boolean helpers + +### useLiveSuspenseQuery + +Same API but throws a Promise during loading (for React Suspense) and +throws errors (for Error Boundaries). `data` is always defined: + +```typescript +import { useLiveSuspenseQuery } from '@tanstack/react-db' +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' + +function TodoList() { + const { data } = useLiveSuspenseQuery((q) => + q + .from({ t: todosCollection }) + .where(({ t }) => eq(t.completed, false)) + ) + + return ( +
    + {data.map((todo) => ( +
  • {todo.text}
  • + ))} +
+ ) +} + +function App() { + return ( + Failed to load}> + Loading...}> + + + + ) +} +``` + +### useLiveInfiniteQuery + +Cursor-based pagination with live updates: + +```typescript +import { useLiveInfiniteQuery } from '@tanstack/react-db' + +function PostFeed() { + const { data, pages, fetchNextPage, hasNextPage, isFetchingNextPage } = + useLiveInfiniteQuery( + (q) => + q + .from({ p: postsCollection }) + .orderBy(({ p }) => p.createdAt, 'desc'), + { + pageSize: 20, + getNextPageParam: (lastPage, allPages, lastPageParam) => + lastPage.length === 20 ? (lastPageParam ?? 0) + 1 : undefined, + }, + [category] + ) + + return ( +
+ {data.map((post) => ( + + ))} + {hasNextPage && ( + + )} +
+ ) +} +``` + +### usePacedMutations + +React hook wrapper for `createPacedMutations`: + +```typescript +import { usePacedMutations } from '@tanstack/react-db' +import { debounceStrategy } from '@tanstack/db' + +function EditableTitle({ todoId }: { todoId: string }) { + const updateTitle = usePacedMutations({ + onMutate: ({ id, title }: { id: string; title: string }) => { + todosCollection.update(id, (draft) => { + draft.title = title + }) + }, + mutationFn: async ({ id, title }) => { + await fetch(`/api/todos/${id}`, { + method: 'PATCH', + body: JSON.stringify({ title }), + }) + }, + strategy: debounceStrategy({ wait: 500 }), + }) + + return ( + updateTitle({ id: todoId, title: e.target.value })} + /> + ) +} +``` + +## React-Specific Patterns + +### Dependency arrays for reactive parameters + +When a query uses values from props, state, or context, include them in +the dependency array. The query re-runs when any dep changes: + +```typescript +function FilteredTodos({ status, priority }: Props) { + const { data } = useLiveQuery( + (q) => + q + .from({ t: todosCollection }) + .where(({ t }) => and(eq(t.status, status), gte(t.priority, priority))), + [status, priority], + ) + // Import: import { eq, gte, and } from '@tanstack/db' + // ... +} +``` + +### Disabling queries conditionally + +Return `undefined` from the query function to disable the query: + +```typescript +function TodoDetail({ todoId }: { todoId: string | null }) { + const { data, isEnabled } = useLiveQuery( + (q) => + todoId + ? q + .from({ t: todosCollection }) + .where(({ t }) => eq(t.id, todoId)) + .findOne() + : undefined, + [todoId], + ) + // isEnabled is false when todoId is null +} +``` + +### Mutations from event handlers + +Mutations are called directly on collections — no hooks needed: + +```typescript +function TodoItem({ todo }: { todo: Todo }) { + const handleToggle = () => { + todosCollection.update(todo.id, (draft) => { + draft.completed = !draft.completed + }) + } + + const handleDelete = () => { + todosCollection.delete(todo.id) + } + + return ( +
  • + + {todo.text} + +
  • + ) +} +``` + +## Common Mistakes + +### CRITICAL — Missing external values in useLiveQuery dependency array + +Wrong: + +```typescript +function FilteredTodos({ status }: { status: string }) { + const { data } = useLiveQuery((q) => + q.from({ t: todosCollection }).where(({ t }) => eq(t.status, status)), + ) + // Query never re-runs when status prop changes +} +``` + +Correct: + +```typescript +function FilteredTodos({ status }: { status: string }) { + const { data } = useLiveQuery( + (q) => + q.from({ t: todosCollection }).where(({ t }) => eq(t.status, status)), + [status], + ) +} +``` + +When the query references external values (props, state, context), they +must be in the dependency array. Without them, the query captures the +initial value and never updates — showing stale results. + +Source: docs/framework/react/overview.md + +### HIGH — useLiveSuspenseQuery without Error Boundary + +Wrong: + +```typescript +function App() { + return ( + Loading...}> + {/* Uses useLiveSuspenseQuery */} + + ) + // Sync error crashes the entire app +} +``` + +Correct: + +```typescript +function App() { + return ( + Failed to load}> + Loading...}> + + + + ) +} +``` + +`useLiveSuspenseQuery` throws errors during rendering. Without an Error +Boundary, the error propagates up and crashes the app. Always wrap +Suspense queries with an Error Boundary. + +Source: docs/guides/live-queries.md — React Suspense section diff --git a/packages/playbooks/skills/db/solid/SKILL.md b/packages/playbooks/skills/db/solid/SKILL.md new file mode 100644 index 0000000..2114ec5 --- /dev/null +++ b/packages/playbooks/skills/db/solid/SKILL.md @@ -0,0 +1,169 @@ +--- +name: db/solid +description: > + Solid.js bindings for TanStack DB. Covers useLiveQuery with fine-grained + Solid reactivity. Signal reads must happen inside the query function for + automatic tracking. Return shape (data, state, collection, status, + isLoading, isReady, isError) as Solid signals. +type: framework +library: db +framework: solid +library_version: '0.5.29' +requires: + - db/core +--- + +This skill builds on db-core. Read db-core first for collection setup, +query builder syntax, operators, mutations, and sync concepts. + +# TanStack DB — Solid + +## Setup + +```bash +npm install @tanstack/db @tanstack/solid-db +``` + +Collections are created outside components: + +```typescript +// collections/todos.ts +import { createCollection, eq } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +export const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (todo) => todo.id, + onUpdate: async ({ transaction }) => { + const { original, changes } = transaction.mutations[0] + await fetch(`/api/todos/${original.id}`, { + method: 'PATCH', + body: JSON.stringify(changes), + }) + }, + }), +) +``` + +## Hooks and Components + +### useLiveQuery + +Returns Solid signals that update reactively: + +```typescript +import { useLiveQuery } from '@tanstack/solid-db' +import { eq } from '@tanstack/db' +import { todosCollection } from '../collections/todos' + +function TodoList() { + const query = useLiveQuery((q) => + q + .from({ t: todosCollection }) + .where(({ t }) => eq(t.completed, false)) + .orderBy(({ t }) => t.createdAt, 'desc') + ) + + return ( + Loading...}> +
      + + {(todo) => ( +
    • { + todosCollection.update(todo.id, (draft) => { + draft.completed = !draft.completed + }) + }}> + {todo.text} +
    • + )} +
      +
    +
    + ) +} +``` + +## Solid-Specific Patterns + +### Signal reads inside the query function + +Solid tracks signal reads for reactivity. Read signals INSIDE the query +function so changes are tracked automatically: + +```typescript +import { createSignal } from 'solid-js' +import { eq, gte, and } from '@tanstack/db' + +function FilteredTodos() { + const [status, setStatus] = createSignal('active') + const [minPriority, setMinPriority] = createSignal(0) + + const query = useLiveQuery((q) => + q + .from({ t: todosCollection }) + .where(({ t }) => + and( + eq(t.status, status()), // Read signal INSIDE query fn + gte(t.priority, minPriority()) // Read signal INSIDE query fn + ) + ) + ) + + return ( +
    + +
      + + {(todo) =>
    • {todo.text}
    • } +
      +
    +
    + ) +} +``` + +## Common Mistakes + +### HIGH — Reading Solid signals outside the query function + +Wrong: + +```typescript +function FilteredTodos() { + const [status, setStatus] = createSignal('active') + + // Signal read OUTSIDE query fn — not tracked + const currentStatus = status() + + const query = useLiveQuery((q) => + q + .from({ t: todosCollection }) + .where(({ t }) => eq(t.status, currentStatus)), + ) +} +``` + +Correct: + +```typescript +function FilteredTodos() { + const [status, setStatus] = createSignal('active') + + const query = useLiveQuery( + (q) => + q.from({ t: todosCollection }).where(({ t }) => eq(t.status, status())), // Read INSIDE + ) +} +``` + +Solid's reactivity tracks signal reads inside reactive scopes. Reading +a signal outside the query function and passing the value means changes +aren't tracked — the query captures the initial value and never updates. + +Source: docs/framework/solid/overview.md — fine-grained reactivity section diff --git a/packages/playbooks/skills/db/svelte/SKILL.md b/packages/playbooks/skills/db/svelte/SKILL.md new file mode 100644 index 0000000..7c1c396 --- /dev/null +++ b/packages/playbooks/skills/db/svelte/SKILL.md @@ -0,0 +1,187 @@ +--- +name: db/svelte +description: > + Svelte 5 bindings for TanStack DB. Covers useLiveQuery with Svelte 5 + runes ($state, $derived). Dependency tracking with getter functions + for props and derived values. Return shape (data, state, collection, + status, isLoading, isReady, isError). +type: framework +library: db +framework: svelte +library_version: '0.5.29' +requires: + - db/core +--- + +This skill builds on db-core. Read db-core first for collection setup, +query builder syntax, operators, mutations, and sync concepts. + +# TanStack DB — Svelte + +## Setup + +```bash +npm install @tanstack/db @tanstack/svelte-db +``` + +Requires Svelte 5+ (runes). + +Collections are created outside components: + +```typescript +// collections/todos.ts +import { createCollection, eq } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +export const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (todo) => todo.id, + onUpdate: async ({ transaction }) => { + const { original, changes } = transaction.mutations[0] + await fetch(`/api/todos/${original.id}`, { + method: 'PATCH', + body: JSON.stringify(changes), + }) + }, + }), +) +``` + +## Hooks and Components + +### useLiveQuery + +```svelte + + +{#if query.isLoading} +
    Loading...
    +{:else if query.isError} +
    Error loading todos
    +{:else} +
      + {#each query.data as todo (todo.id)} +
    • handleToggle(todo.id)}> + {todo.text} +
    • + {/each} +
    +{/if} +``` + +## Svelte-Specific Patterns + +### Reactive parameters with getter functions in deps + +In Svelte 5, props and derived values must be wrapped in getter functions +in the dependency array to maintain reactivity: + +```svelte + +``` + +## Common Mistakes + +### HIGH — Destructuring useLiveQuery result breaks reactivity + +Wrong: + +```svelte + +``` + +Correct: + +```svelte + +``` + +Direct destructuring captures values at creation time, breaking Svelte 5 +reactivity. Either use dot notation (`query.data`) or wrap with `$derived` +to maintain reactive tracking. + +Source: packages/db/svelte/src/useLiveQuery.svelte.ts + +### MEDIUM — Passing Svelte props directly instead of getter functions in deps + +Wrong: + +```svelte + +``` + +Correct: + +```svelte + +``` + +In Svelte 5, `$props()` values and `$state()` values must be wrapped in +getter functions in the dependency array. Passing values directly captures +them at creation time — the query never re-runs when props change. + +Source: docs/framework/svelte/overview.md — Props in dependencies diff --git a/packages/playbooks/skills/db/vue/SKILL.md b/packages/playbooks/skills/db/vue/SKILL.md new file mode 100644 index 0000000..bbc4239 --- /dev/null +++ b/packages/playbooks/skills/db/vue/SKILL.md @@ -0,0 +1,136 @@ +--- +name: db/vue +description: > + Vue 3 bindings for TanStack DB. Covers useLiveQuery composable with + computed refs. Reactive query parameters via Vue refs. Return shape + (data, state, collection, status, isLoading, isReady, isError) as + reactive Refs. +type: framework +library: db +framework: vue +library_version: '0.5.29' +requires: + - db/core +--- + +This skill builds on db-core. Read db-core first for collection setup, +query builder syntax, operators, mutations, and sync concepts. + +# TanStack DB — Vue + +## Setup + +```bash +npm install @tanstack/db @tanstack/vue-db +``` + +Collections are created outside components in a dedicated module: + +```typescript +// collections/todos.ts +import { createCollection, eq } from '@tanstack/db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' + +export const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ['todos'], + queryFn: () => fetch('/api/todos').then((r) => r.json()), + getKey: (todo) => todo.id, + onUpdate: async ({ transaction }) => { + const { original, changes } = transaction.mutations[0] + await fetch(`/api/todos/${original.id}`, { + method: 'PATCH', + body: JSON.stringify(changes), + }) + }, + }), +) +``` + +## Hooks and Components + +### useLiveQuery + +Returns reactive Refs that update when query results change: + +```vue + + + +``` + +All return values (`data`, `state`, `collection`, `status`, `isLoading`, +`isReady`, `isIdle`, `isError`, `isCleanedUp`) are Vue Refs. + +## Vue-Specific Patterns + +### Reactive query parameters with refs + +Vue refs used inside the query function are automatically tracked: + +```vue + +``` + +### Mutations from event handlers + +Mutations are called directly on collections — same as in any framework: + +```vue + +``` + +## Common Mistakes + +No Vue-specific mistakes beyond those covered in db-core. The Vue adapter +handles reactivity tracking automatically through Vue's composition API. +See db/core/live-queries and db/core/mutations-optimistic for common +mistakes that apply across all frameworks. diff --git a/packages/playbooks/skills/tanstack/SKILL.md b/packages/playbooks/skills/tanstack/SKILL.md new file mode 100644 index 0000000..ab8263a --- /dev/null +++ b/packages/playbooks/skills/tanstack/SKILL.md @@ -0,0 +1,117 @@ +--- +name: tanstack +description: > + TanStack ecosystem router and skill discovery. Entry point for all + TanStack AI coding agent skills. Routes to DB skills based on installed + packages. Includes feedback tracking format and end-of-session reporting. +type: router +library: tanstack +library_version: "0.1.0-alpha.1" +--- + +# TanStack Playbooks — Ecosystem Router + +You have access to TanStack Playbooks — structured skills for TanStack +libraries. This skill is always loaded. Use it to find and load the right +skill for the user's task. + +## Skill Discovery + +Run `tanstack playbook list` to see which skills are available for this +project. The output shows: + +- Which TanStack packages are installed +- Available skills grouped by library (core + framework) +- `node_modules` file paths for each skill +- Sub-skills listed under their parent + +Read skill files directly from the paths shown. No additional CLI calls +needed after discovery. + +## Decision Tree + +Use this to route to the right skill based on the user's task. + +### Available libraries + +- **Local-first database, collections, live queries, sync** + → `db/core` + `db/[framework]` + +More libraries (Router, Query, Table, Form, Virtual, Store, Start) will +be added in future releases. Run `tanstack playbook list` to see what's +currently available for your project. + +### Package not installed + +If the user's request implies a TanStack package that isn't installed: + +1. Note which package would be needed +2. Explain what it provides +3. Do NOT instruct the user to install it — just inform them + +## Loading Strategy + +1. Always load the **core** skill first for foundational concepts +2. Then load the **framework** skill for implementation specifics +3. Load **sub-skills** only when the task targets a specific domain +4. Load **composition** skills only when the task spans libraries + +Do not load every skill upfront. Use descriptions from `playbook list` +to decide which skills are relevant to the current task. + +## Feedback Tracking + +When you read and use a TanStack playbook skill file during this session, +log a feedback entry with: + +``` +- skill_name: [the skill you used] +- task_summary: [1 sentence, what the user asked to do — no personal + details, no file paths, no variable names, no project names] +- helpful: [yes | partially | no] +- what_worked: [1 sentence or "n/a"] +- what_failed: [1 sentence or "n/a"] +- missing_info: [1 sentence or "n/a"] +- self_corrections: [number of times you corrected code from this skill] +``` + +Track one entry per skill used. Keep entries in memory during the session. + +## End-of-Session Report + +Before ending this session, if you used any TanStack playbook skills, +compile all feedback entries into a single report and run: + +```bash +tanstack playbook feedback --submit +``` + +piping the report content to stdin. Use this format: + +```markdown +# TanStack Playbook Feedback + +Playbook version: [from playbook list output] +CLI version: [from playbook list output] +Date: [ISO date] +Framework: [React | Vue | Solid | Svelte | Angular] + +## Skills Used + +### [skill_name] +- Task: [task_summary] +- Helpful: [yes | partially | no] +- What worked: [what_worked] +- What failed: [what_failed] +- Missing: [missing_info] +- Self-corrections: [self_corrections] + +## User Comments (optional) + +[Ask the user if they have any comments about the skills used] +``` + +## Version + +TanStack Playbooks v0.1.0-alpha.1 (alpha — feedback collection is on by +default). diff --git a/packages/playbooks/tests/skills.test.ts b/packages/playbooks/tests/skills.test.ts new file mode 100644 index 0000000..72118f3 --- /dev/null +++ b/packages/playbooks/tests/skills.test.ts @@ -0,0 +1,250 @@ +import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs' +import { join, relative, sep } from 'node:path' +import { describe, expect, it } from 'vitest' +import { parse as parseYaml } from 'yaml' + +// ── Types ── + +interface SkillFrontmatter { + name: string + description: string + type?: string + library?: string + framework?: string + library_version?: string + requires?: Array + sources?: Array +} + +interface PackageMap { + schema_version: number + playbook: { name: string; version: string } + package_map: Record> + compositions?: Array<{ requires: Array; skills: Array }> + always_include: Array + feedback: { enabled: boolean; target: string; repo: string; category: string } +} + +// ── Helpers ── + +const SKILLS_DIR = join(__dirname, '..', 'skills') +const PACKAGE_MAP_PATH = join(__dirname, '..', 'package_map.yaml') +const MAX_LINES = 500 + +function findSkillFiles(dir: string): Array { + const files: Array = [] + if (!existsSync(dir)) return files + + for (const entry of readdirSync(dir)) { + const fullPath = join(dir, entry) + const stat = statSync(fullPath) + if (stat.isDirectory()) { + files.push(...findSkillFiles(fullPath)) + } else if (entry === 'SKILL.md') { + files.push(fullPath) + } + } + return files +} + +function extractFrontmatter( + content: string, +): { frontmatter: SkillFrontmatter; body: string } | null { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/) + if (!match) return null + + try { + const frontmatter = parseYaml(match[1]) as SkillFrontmatter + return { frontmatter, body: match[2] } + } catch { + return null + } +} + +function skillPathFromFile(filePath: string): string { + return relative(SKILLS_DIR, filePath) + .replace(/[/\\]SKILL\.md$/, '') + .split(sep) + .join('/') +} + +// ── Collect all skills ── + +const skillFiles = findSkillFiles(SKILLS_DIR) +const skills = skillFiles.map((filePath) => { + const content = readFileSync(filePath, 'utf-8') + const parsed = extractFrontmatter(content) + const relPath = skillPathFromFile(filePath) + return { filePath, content, parsed, relPath } +}) + +// ── Tests ── + +describe('skill discovery', () => { + it('should find at least one SKILL.md file', () => { + expect(skillFiles.length).toBeGreaterThan(0) + }) + + it('should include the ecosystem router (tanstack)', () => { + const router = skills.find((s) => s.relPath === 'tanstack') + expect(router).toBeDefined() + }) +}) + +describe('frontmatter', () => { + for (const skill of skills) { + describe(skill.relPath, () => { + it('should have valid frontmatter', () => { + expect(skill.parsed).not.toBeNull() + }) + + if (!skill.parsed) return + + const { frontmatter } = skill.parsed + + it('should have a name', () => { + expect(frontmatter.name).toBeTruthy() + }) + + it('should have a description', () => { + expect(frontmatter.description).toBeTruthy() + }) + + it('should have name matching directory path', () => { + expect(frontmatter.name).toBe(skill.relPath) + }) + + if (frontmatter.type === 'framework') { + it('should have requires field (framework skill)', () => { + expect(frontmatter.requires).toBeDefined() + expect(frontmatter.requires!.length).toBeGreaterThan(0) + }) + + it('should have framework field', () => { + expect(frontmatter.framework).toBeTruthy() + }) + } + + if (frontmatter.type === 'sub-skill') { + it('should have library field', () => { + expect(frontmatter.library).toBeTruthy() + }) + + it('should have library_version field', () => { + expect(frontmatter.library_version).toBeTruthy() + }) + } + }) + } +}) + +describe('content', () => { + for (const skill of skills) { + describe(skill.relPath, () => { + it(`should not exceed ${MAX_LINES} lines`, () => { + const lineCount = skill.content.split(/\r?\n/).length + expect(lineCount).toBeLessThanOrEqual(MAX_LINES) + }) + + if (!skill.parsed) return + + it('should not contain install instructions (except @tanstack packages)', () => { + const installPattern = /(?:npm|yarn|pnpm|bun)\s+(?:install|add|i)\s/i + const lines = skill.parsed.body.split(/\r?\n/) + + for (const line of lines) { + if (installPattern.test(line)) { + const isAllowed = + line.includes('npm install @tanstack/') || + line.includes('tanstack playbook') + expect(isAllowed).toBe(true) + } + } + }) + + it('should not instruct agents to fetch external URLs at runtime', () => { + const fetchPattern = /(?:curl|wget)\s+https?:\/\//i + const lines = skill.parsed.body.split(/\r?\n/) + + for (const line of lines) { + expect(fetchPattern.test(line)).toBe(false) + } + }) + }) + } +}) + +describe('cross-references', () => { + const allSkillPaths = new Set(skills.map((s) => s.relPath)) + + for (const skill of skills) { + if (!skill.parsed?.frontmatter.requires) continue + + describe(skill.relPath, () => { + for (const req of skill.parsed!.frontmatter.requires!) { + it(`requires "${req}" should reference an existing skill`, () => { + expect(allSkillPaths.has(req)).toBe(true) + }) + } + }) + } +}) + +describe('package_map.yaml', () => { + it('should exist', () => { + expect(existsSync(PACKAGE_MAP_PATH)).toBe(true) + }) + + const content = readFileSync(PACKAGE_MAP_PATH, 'utf-8') + let packageMap: PackageMap + + it('should be valid YAML', () => { + packageMap = parseYaml(content) as PackageMap + expect(packageMap).toBeDefined() + }) + + it('should have schema_version', () => { + packageMap = parseYaml(content) as PackageMap + expect(packageMap.schema_version).toBeDefined() + }) + + it('should have package_map', () => { + packageMap = parseYaml(content) as PackageMap + expect(packageMap.package_map).toBeDefined() + expect(typeof packageMap.package_map).toBe('object') + }) + + it('should have always_include', () => { + packageMap = parseYaml(content) as PackageMap + expect(packageMap.always_include).toBeDefined() + expect(Array.isArray(packageMap.always_include)).toBe(true) + }) + + it('should have feedback config', () => { + packageMap = parseYaml(content) as PackageMap + expect(packageMap.feedback).toBeDefined() + expect(packageMap.feedback.repo).toBe('tanstack/playbooks') + }) + + describe('skill directory references', () => { + packageMap = parseYaml(content) as PackageMap + const allSkillPaths = new Set(skills.map((s) => s.relPath)) + + // Collect all skill dirs referenced in package_map + const referencedDirs = new Set() + for (const dirs of Object.values(packageMap.package_map)) { + for (const dir of dirs) { + referencedDirs.add(dir) + } + } + for (const dir of packageMap.always_include) { + referencedDirs.add(dir) + } + + for (const dir of referencedDirs) { + it(`"${dir}" should have a corresponding SKILL.md`, () => { + expect(allSkillPaths.has(dir)).toBe(true) + }) + } + }) +}) diff --git a/packages/playbooks/vitest.config.ts b/packages/playbooks/vitest.config.ts new file mode 100644 index 0000000..edca3b5 --- /dev/null +++ b/packages/playbooks/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'playbooks', + include: ['tests/**/*.test.ts'], + }, +}) diff --git a/packages/router-skills/README.md b/packages/router-skills/README.md deleted file mode 100644 index b635215..0000000 --- a/packages/router-skills/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @tanstack/router/skills - -TanStack Router skill sources staged in this repo. - -Sources live under `skills/v1/` with a deterministic topic list in `skills/topics.json`. diff --git a/packages/router-skills/manifest.json b/packages/router-skills/manifest.json deleted file mode 100644 index 172e296..0000000 --- a/packages/router-skills/manifest.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "package": "@tanstack/router/skills", - "topics": "skills/topics.json", - "versions": { - "v1": { - "index": "skills/v1/index.md", - "files": [ - "skills/v1/authenticated-routes.md", - "skills/v1/custom-links.md", - "skills/v1/custom-search-serialization.md", - "skills/v1/data-loading-advanced.md", - "skills/v1/data-refresh.md", - "skills/v1/deferred-data-loading.md", - "skills/v1/document-head-management.md", - "skills/v1/error-boundaries.md", - "skills/v1/eslint-plugin-router.md", - "skills/v1/external-data-loading.md", - "skills/v1/file-based-routing.md", - "skills/v1/history-types.md", - "skills/v1/index.md", - "skills/v1/installation-guides.md", - "skills/v1/layouts.md", - "skills/v1/link-options.md", - "skills/v1/links.md", - "skills/v1/loaders.md", - "skills/v1/matching-and-location.md", - "skills/v1/navigation-blocking.md", - "skills/v1/navigation.md", - "skills/v1/not-found-boundaries.md", - "skills/v1/params.md", - "skills/v1/preloading.md", - "skills/v1/query-integration.md", - "skills/v1/redirects.md", - "skills/v1/render-optimizations.md", - "skills/v1/route-context.md", - "skills/v1/route-ids.md", - "skills/v1/route-lazy-loading.md", - "skills/v1/route-masking.md", - "skills/v1/route-trees.md", - "skills/v1/router-devtools.md", - "skills/v1/router-setup.md", - "skills/v1/router-state.md", - "skills/v1/routing-strategies.md", - "skills/v1/scroll-restoration.md", - "skills/v1/search-params.md", - "skills/v1/ssr-loaders.md", - "skills/v1/static-route-data.md", - "skills/v1/type-safety.md", - "skills/v1/view-transitions.md" - ] - } - } -} diff --git a/packages/router-skills/package.json b/packages/router-skills/package.json deleted file mode 100644 index 72a6f78..0000000 --- a/packages/router-skills/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@tanstack/router/skills", - "version": "0.0.0", - "private": true, - "description": "TanStack Router skill sources (staged in tanstack/agents)", - "license": "MIT", - "type": "module" -} diff --git a/packages/router-skills/project.json b/packages/router-skills/project.json deleted file mode 100644 index 46d0aa2..0000000 --- a/packages/router-skills/project.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "router-skills", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "library", - "sourceRoot": "packages/router-skills", - "targets": { - "build": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('build:noop\\n')\"" - } - }, - "test:eslint": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:eslint:noop\\n')\"" - } - }, - "test:types": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:types:noop\\n')\"" - } - }, - "test:lib": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:lib:noop\\n')\"" - } - } - } -} diff --git a/packages/router-skills/skills/topics.json b/packages/router-skills/skills/topics.json deleted file mode 100644 index f0fd3b0..0000000 --- a/packages/router-skills/skills/topics.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - "index.md", - "router-setup.md", - "installation-guides.md", - "routing-strategies.md", - "route-trees.md", - "layouts.md", - "route-ids.md", - "params.md", - "search-params.md", - "custom-search-serialization.md", - "loaders.md", - "route-context.md", - "data-refresh.md", - "data-loading-advanced.md", - "deferred-data-loading.md", - "external-data-loading.md", - "query-integration.md", - "error-boundaries.md", - "not-found-boundaries.md", - "links.md", - "link-options.md", - "custom-links.md", - "navigation.md", - "preloading.md", - "router-state.md", - "matching-and-location.md", - "redirects.md", - "route-masking.md", - "authenticated-routes.md", - "file-based-routing.md", - "ssr-loaders.md", - "route-lazy-loading.md", - "router-devtools.md", - "document-head-management.md", - "scroll-restoration.md", - "navigation-blocking.md", - "history-types.md", - "static-route-data.md", - "render-optimizations.md", - "type-safety.md", - "eslint-plugin-router.md", - "view-transitions.md" -] diff --git a/packages/router-skills/skills/v1/authenticated-routes.md b/packages/router-skills/skills/v1/authenticated-routes.md deleted file mode 100644 index b745f4e..0000000 --- a/packages/router-skills/skills/v1/authenticated-routes.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -id: authenticated-routes -title: Authenticated Routes -versions: - - latest - - ">=1 <2" -summary: Gate routes behind auth and redirect when unauthenticated. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes - - https://tanstack.com/router/latest/docs/framework/react/guide/redirects ---- - -# Authenticated Routes - -Purpose: - -- Gate routes behind auth and redirect when needed. - -Scope: - -- Use when a route requires a user session. - -Guidelines: - -- Check auth in loaders or route guards before render. -- Redirect early to avoid unauthorized flashes. -- Preserve intended destinations in search params. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "account", - beforeLoad: ({ context }) => { - if (!context.auth.user) throw redirect({ to: "/login" }) - }, -}) -``` - -```ts -throw redirect({ to: "/login", search: { next: "/account" } }) -``` diff --git a/packages/router-skills/skills/v1/custom-links.md b/packages/router-skills/skills/v1/custom-links.md deleted file mode 100644 index 6dd207e..0000000 --- a/packages/router-skills/skills/v1/custom-links.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: custom-links -title: Custom Links -versions: - - latest - - ">=1 <2" -summary: Build design-system links on top of Router links. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/custom-link - - https://tanstack.com/router/latest/docs/api/router/use-link-props ---- - -# Custom Links - -Purpose: - -- Build design-system links on top of Router links. - -Scope: - -- Use when standard Link does not match UI needs. - -Guidelines: - -- Use `useLinkProps` to generate typed props. -- Forward refs and props to the underlying anchor. -- Preserve active and pending states. - -Examples: - -```tsx -const props = useLinkProps({ to: "/projects" }) -return -``` - -```tsx -function NavLink(props) { - const linkProps = useLinkProps(props) - return -} -``` diff --git a/packages/router-skills/skills/v1/custom-search-serialization.md b/packages/router-skills/skills/v1/custom-search-serialization.md deleted file mode 100644 index 37b40af..0000000 --- a/packages/router-skills/skills/v1/custom-search-serialization.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -id: custom-search-serialization -title: Custom Search Serialization -versions: - - latest - - ">=1 <2" -summary: Customize how search params are parsed and serialized. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization - - https://tanstack.com/router/latest/docs/framework/react/guide/search-params ---- - -# Custom Search Serialization - -Purpose: - -- Customize how search params are parsed and serialized. - -Scope: - -- Use when you need non-default parsing or encoding. - -Guidelines: - -- Keep serialization stable and reversible. -- Prefer schema adapters for validation. -- Document custom encoding for other teams. - -Examples: - -```ts -const router = createRouter({ - routeTree, - parseSearch: (search) => customParse(search), - stringifySearch: (search) => customStringify(search), -}) -``` - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "filters", - validateSearch: searchSchema, -}) -``` diff --git a/packages/router-skills/skills/v1/data-loading-advanced.md b/packages/router-skills/skills/v1/data-loading-advanced.md deleted file mode 100644 index 660e3e3..0000000 --- a/packages/router-skills/skills/v1/data-loading-advanced.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -id: data-loading-advanced -title: Advanced Data Loading -versions: - - latest - - ">=1 <2" -summary: Handle deferred, external, and mutation-driven data flows. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/data-loading - - https://tanstack.com/router/latest/docs/framework/react/guide/deferred-data-loading - - https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading - - https://tanstack.com/router/latest/docs/framework/react/guide/data-mutations ---- - -# Advanced Data Loading - -Purpose: - -- Model deferred data, external data sources, and mutation flows. - -Scope: - -- Use when data cannot all load upfront or needs mutation handling. - -Guidelines: - -- Defer non-critical data to keep first paint fast. -- Use external caches (Query, custom stores) for shared state. -- Trigger invalidation after mutations to refresh loaders. -- Keep mutation side effects predictable and localized. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "dashboard", - loader: async () => ({ - summary: await fetchSummary(), - detailsPromise: fetchDetails() - }), -}) -``` - -```ts -await mutateProject(projectId, payload) -router.invalidate() -``` diff --git a/packages/router-skills/skills/v1/data-refresh.md b/packages/router-skills/skills/v1/data-refresh.md deleted file mode 100644 index a277089..0000000 --- a/packages/router-skills/skills/v1/data-refresh.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: data-refresh -title: Prefetching and Invalidation -versions: - - latest - - ">=1 <2" -summary: Warm data before navigation and revalidate after mutations. -resources: - - https://tanstack.com/router/latest/docs/guide/prefetching - - https://tanstack.com/router/latest/docs/guide/invalidation - - https://tanstack.com/router/latest/docs/api/router/preload - - https://tanstack.com/router/latest/docs/api/router/invalidate ---- - -# Prefetching and Invalidation - -Purpose: - -- Warm route data before navigation and revalidate after mutations. - -Scope: - -- Use when you can predict navigation or after data changes. - -Guidelines: - -- Prefetch on hover or intent to reduce perceived latency. -- Invalidate the smallest route scope that changed. -- Avoid frequent invalidation loops. -- Pair with loader caching for instant renders. - -Examples: - -```ts -router.preload({ to: "/projects/123" }) -``` - -```ts -router.invalidate({ to: "/projects" }) -``` diff --git a/packages/router-skills/skills/v1/deferred-data-loading.md b/packages/router-skills/skills/v1/deferred-data-loading.md deleted file mode 100644 index fdfed01..0000000 --- a/packages/router-skills/skills/v1/deferred-data-loading.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: deferred-data-loading -title: Deferred Data Loading -versions: - - latest - - ">=1 <2" -summary: Defer non-critical data to keep initial render fast. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/deferred-data-loading - - https://tanstack.com/router/latest/docs/framework/react/guide/data-loading ---- - -# Deferred Data Loading - -Purpose: - -- Defer non-critical data to keep initial render fast. - -Scope: - -- Use when part of the route data can load after render. - -Guidelines: - -- Load critical data in the loader immediately. -- Defer secondary data to avoid blocking. -- Handle loading states for deferred data. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "dashboard", - loader: async () => ({ - summary: await fetchSummary(), - detailsPromise: fetchDetails(), - }), -}) -``` - -```tsx -const { summary, detailsPromise } = route.useLoaderData() -``` diff --git a/packages/router-skills/skills/v1/document-head-management.md b/packages/router-skills/skills/v1/document-head-management.md deleted file mode 100644 index 3788407..0000000 --- a/packages/router-skills/skills/v1/document-head-management.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -id: document-head-management -title: Document Head Management -versions: - - latest - - ">=1 <2" -summary: Manage titles, meta tags, and head elements per route. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management ---- - -# Document Head Management - -Purpose: - -- Manage titles, meta tags, and head elements per route. - -Scope: - -- Use when routes need SEO or social metadata. - -Guidelines: - -- Define head data close to the route. -- Keep metadata derived from loader data when possible. -- Avoid duplicating global tags per route. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "projects/$projectId", - head: () => ({ - meta: [ - { - title: loaderData.project.name, - }, - ], - }), -}) -``` - -```ts -head: () => ({ meta: [{ name: "description", content: "Projects" }] }) -``` diff --git a/packages/router-skills/skills/v1/error-boundaries.md b/packages/router-skills/skills/v1/error-boundaries.md deleted file mode 100644 index 0a89393..0000000 --- a/packages/router-skills/skills/v1/error-boundaries.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -id: error-boundaries -title: Error and Not-Found Boundaries -versions: - - latest - - ">=1 <2" -summary: Localize route errors with error boundaries. -resources: - - https://tanstack.com/router/latest/docs/guide/error-handling - - https://tanstack.com/router/latest/docs/api/router/create-route ---- - -# Error Boundaries - -Purpose: - -- Provide resilient UX for loader and render errors. - -Scope: - -- Use when setting per-route error UI. - -Guidelines: - -- Define boundaries close to where errors occur. -- Keep error UI consistent within a layout. - -Examples: - -- Route-specific boundary: - ```ts - const route = createRoute({ - getParentRoute: () => rootRoute, - path: "projects/$projectId", - errorComponent: ProjectError, - }) - ``` - -- Layout-level boundary: - ```ts - const layoutRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "app", - errorComponent: AppError, - }) - ``` diff --git a/packages/router-skills/skills/v1/eslint-plugin-router.md b/packages/router-skills/skills/v1/eslint-plugin-router.md deleted file mode 100644 index 1d20a41..0000000 --- a/packages/router-skills/skills/v1/eslint-plugin-router.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: eslint-plugin-router -title: ESLint Plugin Router -versions: - - latest - - ">=1 <2" -summary: Enforce Router conventions with ESLint rules. -resources: - - https://tanstack.com/router/latest/docs/eslint/eslint-plugin-router - - https://tanstack.com/router/latest/docs/eslint/create-route-property-order ---- - -# ESLint Plugin Router - -Purpose: - -- Enforce Router conventions and prevent common mistakes. - -Scope: - -- Use when standardizing route file structure. - -Guidelines: - -- Enable the Router ESLint plugin in shared configs. -- Use rule sets to keep route definitions consistent. -- Apply property ordering to improve readability. - -Examples: - -```json -{ - "plugins": ["@tanstack/router"], - "rules": { - "@tanstack/router/create-route-property-order": "error" - } -} -``` diff --git a/packages/router-skills/skills/v1/external-data-loading.md b/packages/router-skills/skills/v1/external-data-loading.md deleted file mode 100644 index 8e22603..0000000 --- a/packages/router-skills/skills/v1/external-data-loading.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: external-data-loading -title: External Data Loading -versions: - - latest - - ">=1 <2" -summary: Integrate external caches with loader-driven routing. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading - - https://tanstack.com/router/latest/docs/framework/react/guide/data-loading ---- - -# External Data Loading - -Purpose: - -- Integrate external caches with loader-driven routing. - -Scope: - -- Use when using TanStack Query or other caches. - -Guidelines: - -- Prefetch into the external cache inside loaders. -- Read data from the cache in components. -- Invalidate routes after mutations as needed. - -Examples: - -```ts -loader: async ({ context }) => { - await context.queryClient.prefetchQuery(projectsQuery()) -} -``` - -```ts -const projects = useQuery(projectsQuery()) -``` diff --git a/packages/router-skills/skills/v1/file-based-routing.md b/packages/router-skills/skills/v1/file-based-routing.md deleted file mode 100644 index cb82ea4..0000000 --- a/packages/router-skills/skills/v1/file-based-routing.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -id: file-based-routing -title: File-Based Routing -versions: - - latest - - ">=1 <2" -summary: Organize routes by files, layouts, and route groups. -resources: - - https://tanstack.com/router/latest/docs/guide/file-based-routing ---- - -# File-Based Routing - -Purpose: - -- Organize routes with files, layouts, and route groups. - -Scope: - -- Use when adopting file-based routing in a new or existing app. - -Guidelines: - -- Group routes by feature to keep layouts and loaders close to UI. -- Use layout files for shared shells. -- Use index files for default child routes. -- Use `$param` filenames for dynamic segments. -- Use route groups for structure without URL changes. -- Document file conventions to avoid routing drift. - -Examples: - -```text -routes/ - _layout.tsx - projects/ - index.tsx - $projectId.tsx -``` - -```text -routes/ - (admin)/ - users.tsx -``` diff --git a/packages/router-skills/skills/v1/history-types.md b/packages/router-skills/skills/v1/history-types.md deleted file mode 100644 index 954e7e3..0000000 --- a/packages/router-skills/skills/v1/history-types.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: history-types -title: History Types -versions: - - latest - - ">=1 <2" -summary: Choose the right history implementation for your environment. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/history-types ---- - -# History Types - -Purpose: - -- Choose the right history implementation for your environment. - -Scope: - -- Use when targeting browser, memory, or hash routing. - -Guidelines: - -- Use browser history for normal web apps. -- Use memory history for tests or non-DOM environments. -- Use hash history when server configuration is limited. - -Examples: - -```ts -const history = createBrowserHistory() -``` - -```ts -const history = createMemoryHistory({ initialEntries: ["/"] }) -``` diff --git a/packages/router-skills/skills/v1/index.md b/packages/router-skills/skills/v1/index.md deleted file mode 100644 index 7d56fff..0000000 --- a/packages/router-skills/skills/v1/index.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -id: router-index -title: Router Skill Index -versions: - - latest - - ">=1 <2" -summary: Choose the right Router skill based on the task at hand. -resources: - - https://tanstack.com/router/latest/docs/guide/overview ---- - -# Router Skill Index - -Purpose: - -- Help pick the right Router skill quickly. - -Scope: - -- Applies to any adapter and routing style. -- Routes to focused skills for routing, search, data loading, and errors. - -How to pick a skill: - -Core setup and structure: - -- Creating and mounting the router -> `@skills/router/router-setup` -- Choosing installation tooling -> `@skills/router/installation-guides` -- Defining the overall route hierarchy -> `@skills/router/route-trees` -- Creating a shared UI shell across child routes -> `@skills/router/layouts` -- Stabilizing references when paths change -> `@skills/router/route-ids` -- Picking a routing strategy -> `@skills/router/routing-strategies` - -Routing params and search: - -- Reading path params and splats -> `@skills/router/params` -- Validating, defaulting, and reading search params -> `@skills/router/search-params` -- Customizing search param serialization -> `@skills/router/custom-search-serialization` - -Data loading and refresh: - -- Fetching route-critical data before render -> `@skills/router/loaders` -- Passing dependencies like API clients to loaders -> `@skills/router/route-context` -- Prefetching and invalidating after mutations -> `@skills/router/data-refresh` -- Handling deferred/external data or mutations -> `@skills/router/data-loading-advanced` -- Deferring non-critical data -> `@skills/router/deferred-data-loading` -- Loading data from external caches -> `@skills/router/external-data-loading` -- Integrating TanStack Query -> `@skills/router/query-integration` - -Navigation and links: - -- Adding typed, declarative navigation links -> `@skills/router/links` -- Configuring link behavior -> `@skills/router/link-options` -- Building custom design-system links -> `@skills/router/custom-links` -- Navigating programmatically after actions -> `@skills/router/navigation` -- Preloading navigation targets -> `@skills/router/preloading` - -Matching and router state: - -- Reading matches and the current location -> `@skills/router/matching-and-location` -- Showing pending UI or transition state -> `@skills/router/router-state` - -Errors, redirects, and masking: - -- Handling loader/render errors -> `@skills/router/error-boundaries` -- Handling missing data or unmatched routes -> `@skills/router/not-found-boundaries` -- Redirecting from loaders/actions -> `@skills/router/redirects` -- Presenting friendly URLs for internal routes -> `@skills/router/route-masking` -- Protecting routes behind auth -> `@skills/router/authenticated-routes` - -File-based routing: - -- Choosing file-based routing or organizing routes by files -> `@skills/router/file-based-routing` - -Rendering and runtime: - -- Ensuring loader data is serializable for SSR -> `@skills/router/ssr-loaders` -- Lazy-loading route modules for code-splitting -> `@skills/router/route-lazy-loading` -- Inspecting routes and matches during development -> `@skills/router/router-devtools` -- Managing head tags and titles -> `@skills/router/document-head-management` -- Restoring scroll positions between navigations -> `@skills/router/scroll-restoration` -- Blocking navigation on unsaved changes -> `@skills/router/navigation-blocking` -- Choosing history implementations -> `@skills/router/history-types` -- Customizing search param serialization -> `@skills/router/custom-search-serialization` -- Attaching static route data -> `@skills/router/static-route-data` -- Improving render performance -> `@skills/router/render-optimizations` -- Tightening type safety and utilities -> `@skills/router/type-safety` -- Applying Router ESLint rules -> `@skills/router/eslint-plugin-router` -- Adding view transitions -> `@skills/router/view-transitions` - -Next: - -- After picking a skill, follow its resource links and guidance. diff --git a/packages/router-skills/skills/v1/installation-guides.md b/packages/router-skills/skills/v1/installation-guides.md deleted file mode 100644 index 87481d1..0000000 --- a/packages/router-skills/skills/v1/installation-guides.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: installation-guides -title: Installation Guides -versions: - - latest - - ">=1 <2" -summary: Choose the right installation path for your tooling. -resources: - - https://tanstack.com/router/latest/docs/framework/react/installation/manual - - https://tanstack.com/router/latest/docs/framework/react/installation/with-vite - - https://tanstack.com/router/latest/docs/framework/react/installation/with-webpack - - https://tanstack.com/router/latest/docs/framework/react/installation/with-esbuild ---- - -# Installation Guides - -Purpose: - -- Choose the right installation path for your tooling. - -Scope: - -- Use when setting up Router in a new project. - -Guidelines: - -- Start with manual setup for full control. -- Use tool-specific guides for best defaults. -- Align Router CLI usage with your build tool. - -Examples: - -```text -pnpm add @tanstack/react-router -``` - -```text -pnpm dlx @tanstack/router-cli@latest init -``` diff --git a/packages/router-skills/skills/v1/layouts.md b/packages/router-skills/skills/v1/layouts.md deleted file mode 100644 index f16a061..0000000 --- a/packages/router-skills/skills/v1/layouts.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -id: layouts -title: Layout Routes -versions: - - latest - - ">=1 <2" -summary: Share UI shells, loaders, and boundaries across child routes. -resources: - - https://tanstack.com/router/latest/docs/guide/route-trees - - https://tanstack.com/router/latest/docs/guide/route-layouts ---- - -# Layout Routes - -Purpose: - -- Share UI shells, loaders, and boundaries across child routes. - -Scope: - -- Use when multiple routes need the same shell UI or shared data. - -Guidelines: - -- Place shared navigation and chrome on layout routes. -- Keep layout loaders limited to shared data. -- Use an outlet to render child routes inside the layout. -- Keep layouts stable to avoid remount churn. - -Examples: - -```ts -const layoutRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "app", - component: AppLayout, -}) -``` - -```tsx -function AppLayout() { - return ( -
    - - -
    - ) -} -``` diff --git a/packages/router-skills/skills/v1/link-options.md b/packages/router-skills/skills/v1/link-options.md deleted file mode 100644 index 8327d87..0000000 --- a/packages/router-skills/skills/v1/link-options.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: link-options -title: Link Options -versions: - - latest - - ">=1 <2" -summary: Control link behavior, active matching, and preloading. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/link-options - - https://tanstack.com/router/latest/docs/framework/react/guide/linking ---- - -# Link Options - -Purpose: - -- Control link behavior, active matching, and preloading. - -Scope: - -- Use when configuring link behavior beyond defaults. - -Guidelines: - -- Set active options to control match sensitivity. -- Use preload options for responsive navigation. -- Keep replace/state/hash consistent with UX intent. - -Examples: - -```tsx - -``` - -```tsx - -``` diff --git a/packages/router-skills/skills/v1/links.md b/packages/router-skills/skills/v1/links.md deleted file mode 100644 index 3f9138b..0000000 --- a/packages/router-skills/skills/v1/links.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -id: links -title: Links -versions: - - latest - - ">=1 <2" -summary: Build typed links, active states, and custom link components. -resources: - - https://tanstack.com/router/latest/docs/guide/linking - - https://tanstack.com/router/latest/docs/api/router/link - - https://tanstack.com/router/latest/docs/api/router/use-link-props ---- - -# Links - -Purpose: - -- Navigate declaratively with typed links and shared link behavior. - -Scope: - -- Use for anchor-style navigation, active link styling, and custom link wrappers. - -Guidelines: - -- Prefer `Link` for navigation over manual anchors. -- Pass params and search through link props. -- Use active/pending link states for UI feedback. -- Use `useLinkProps` when building design-system links. - -Examples: - -```tsx - -``` - -```tsx - -``` - -```tsx - -``` - -```tsx -const linkProps = useLinkProps({ to: "/projects" }) -``` diff --git a/packages/router-skills/skills/v1/loaders.md b/packages/router-skills/skills/v1/loaders.md deleted file mode 100644 index 73c75e2..0000000 --- a/packages/router-skills/skills/v1/loaders.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -id: loaders -title: Route Loaders -versions: - - latest - - ">=1 <2" -summary: Fetch route-critical data before render. -resources: - - https://tanstack.com/router/latest/docs/guide/loaders - - https://tanstack.com/router/latest/docs/api/router/create-route - - https://tanstack.com/router/latest/docs/api/router/use-loader-data ---- - -# Route Loaders - -Purpose: - -- Fetch route-critical data before render. - -Scope: - -- Use when data must be ready for first paint. - -Guidelines: - -- Keep loaders focused on required data. -- Return serializable values when SSR is needed. -- Use loader context for dependencies. -- Read loader results with `useLoaderData`. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "projects", - loader: ({ context }) => context.api.getProjects(), -}) -``` - -```ts -const projects = route.useLoaderData() -``` diff --git a/packages/router-skills/skills/v1/matching-and-location.md b/packages/router-skills/skills/v1/matching-and-location.md deleted file mode 100644 index 7224ff7..0000000 --- a/packages/router-skills/skills/v1/matching-and-location.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: matching-and-location -title: Matching and Location -versions: - - latest - - ">=1 <2" -summary: Read matches, route metadata, and the current location. -resources: - - https://tanstack.com/router/latest/docs/guide/matching - - https://tanstack.com/router/latest/docs/guide/location - - https://tanstack.com/router/latest/docs/api/router/use-match - - https://tanstack.com/router/latest/docs/api/router/use-matches - - https://tanstack.com/router/latest/docs/api/router/use-location ---- - -# Matching and Location - -Purpose: - -- Read route matches, metadata, and the current location. - -Scope: - -- Use when UI depends on the active route tree or URL state. - -Guidelines: - -- Use `useMatch` for a single route and `useMatches` for all matches. -- Read location for pathname or hash-driven UI. -- Avoid heavy computation on every location change. - -Examples: - -```ts -const match = useMatch({ from: "projects/$projectId" }) -``` - -```ts -const matches = useMatches() -``` - -```ts -const location = useLocation() -``` diff --git a/packages/router-skills/skills/v1/navigation-blocking.md b/packages/router-skills/skills/v1/navigation-blocking.md deleted file mode 100644 index a904539..0000000 --- a/packages/router-skills/skills/v1/navigation-blocking.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: navigation-blocking -title: Navigation Blocking -versions: - - latest - - ">=1 <2" -summary: Block navigation when unsaved work is present. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/navigation-blocking ---- - -# Navigation Blocking - -Purpose: - -- Prevent navigation when there are unsaved changes. - -Scope: - -- Use in forms or editors with draft state. - -Guidelines: - -- Keep blocking logic scoped to the route or component. -- Provide clear confirmation UI for the user. -- Disable blocking after a successful save. - -Examples: - -```ts -const blocker = useBlocker({ - shouldBlockFn: () => hasUnsavedChanges, -}) -``` - -```ts -blocker.reset() -``` diff --git a/packages/router-skills/skills/v1/navigation.md b/packages/router-skills/skills/v1/navigation.md deleted file mode 100644 index 7d2dcf9..0000000 --- a/packages/router-skills/skills/v1/navigation.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -id: navigation -title: Navigation -versions: - - latest - - ">=1 <2" -summary: Navigate imperatively with hooks and options. -resources: - - https://tanstack.com/router/latest/docs/guide/navigation - - https://tanstack.com/router/latest/docs/api/router/use-navigate - - https://tanstack.com/router/latest/docs/api/router/navigate ---- - -# Navigation - -Purpose: - -- Navigate programmatically on events or side effects. - -Scope: - -- Use when button clicks or flows need imperatively triggered navigation. - -Guidelines: - -- Prefer links for simple navigation. -- Keep navigation targets typed and stable. -- Avoid triggering navigation during render. -- Use `replace` for non-history transitions. -- Pass `state` for transient UI intent. - -Examples: - -```ts -const navigate = useNavigate() -navigate({ to: "/projects" }) -``` - -```ts -navigate({ to: "/projects", replace: true }) -``` - -```ts -navigate({ to: "/projects", state: { from: "create" } }) -``` diff --git a/packages/router-skills/skills/v1/not-found-boundaries.md b/packages/router-skills/skills/v1/not-found-boundaries.md deleted file mode 100644 index edf55fb..0000000 --- a/packages/router-skills/skills/v1/not-found-boundaries.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: not-found-boundaries -title: Not-Found Boundaries -versions: - - latest - - ">=1 <2" -summary: Handle missing data or unmatched routes. -resources: - - https://tanstack.com/router/latest/docs/guide/not-found - - https://tanstack.com/router/latest/docs/api/router/create-route - - https://tanstack.com/router/latest/docs/api/router/not-found ---- - -# Not-Found Boundaries - -Purpose: - -- Render fallbacks for missing data or unmatched routes. - -Scope: - -- Use when a route can return "not found" states. - -Guidelines: - -- Place not-found boundaries near the routes they protect. -- Keep not-found UI consistent within a layout. -- Use loaders to decide when data is missing. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "projects/$projectId", - notFoundComponent: ProjectNotFound, -}) -``` - -```ts -throw notFound({ routeId: "projects.detail" }) -``` diff --git a/packages/router-skills/skills/v1/params.md b/packages/router-skills/skills/v1/params.md deleted file mode 100644 index 50b08c2..0000000 --- a/packages/router-skills/skills/v1/params.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: params -title: Route Params -versions: - - latest - - ">=1 <2" -summary: Work with path params safely. -resources: - - https://tanstack.com/router/latest/docs/guide/route-params - - https://tanstack.com/router/latest/docs/api/router/use-params ---- - -# Route Params - -Purpose: - -- Read and type-check path params, including splats. - -Scope: - -- Use when accessing `$param` segments or splat params in routes. - -Guidelines: - -- Keep params in the path, not in search. -- Validate or coerce params in loaders if needed. -- Use router hooks to read params. -- Use splats only for catch-all routes. - -Examples: - -```ts -const { projectId } = route.useParams() -``` - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "docs/*splat", -}) -``` diff --git a/packages/router-skills/skills/v1/preloading.md b/packages/router-skills/skills/v1/preloading.md deleted file mode 100644 index aec5884..0000000 --- a/packages/router-skills/skills/v1/preloading.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: preloading -title: Preloading -versions: - - latest - - ">=1 <2" -summary: Preload route data and code before navigation. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/preloading - - https://tanstack.com/router/latest/docs/api/router/preload ---- - -# Preloading - -Purpose: - -- Preload route data and code before navigation. - -Scope: - -- Use when you can anticipate a navigation target. - -Guidelines: - -- Trigger preload on hover or intent. -- Keep preloads targeted to avoid extra work. -- Pair with lazy routes for smoother transitions. - -Examples: - -```ts -router.preload({ to: "/projects/123" }) -``` - -```tsx - -``` diff --git a/packages/router-skills/skills/v1/query-integration.md b/packages/router-skills/skills/v1/query-integration.md deleted file mode 100644 index 52f8d51..0000000 --- a/packages/router-skills/skills/v1/query-integration.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: query-integration -title: TanStack Query Integration -versions: - - latest - - ">=1 <2" -summary: Integrate Router with TanStack Query for caching. -resources: - - https://tanstack.com/router/latest/docs/integrations/query ---- - -# TanStack Query Integration - -Purpose: - -- Integrate Router with TanStack Query for caching and prefetching. - -Scope: - -- Use when you want Query-managed caches with Router loaders. - -Guidelines: - -- Provide the Query client in router context. -- Prefetch queries in loaders. -- Invalidate queries after mutations. - -Examples: - -```ts -const router = createRouter({ routeTree, context: { queryClient } }) -``` - -```ts -await queryClient.prefetchQuery(projectsQuery()) -``` diff --git a/packages/router-skills/skills/v1/redirects.md b/packages/router-skills/skills/v1/redirects.md deleted file mode 100644 index 04f8628..0000000 --- a/packages/router-skills/skills/v1/redirects.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: redirects -title: Redirects -versions: - - latest - - ">=1 <2" -summary: Redirect from loaders or actions. -resources: - - https://tanstack.com/router/latest/docs/guide/redirects - - https://tanstack.com/router/latest/docs/api/router/redirect ---- - -# Redirects - -Purpose: - -- Redirect from loaders or actions. - -Scope: - -- Use when gating routes or handling auth flows. - -Guidelines: - -- Redirect early in loaders to avoid flashes. -- Preserve intent with `search` or `hash`. -- Avoid redirect loops. - -Examples: - -```ts -throw redirect({ to: "/login" }) -``` - -```ts -throw redirect({ to: "/login", search: { next: "/projects" } }) -``` diff --git a/packages/router-skills/skills/v1/render-optimizations.md b/packages/router-skills/skills/v1/render-optimizations.md deleted file mode 100644 index 0a9801a..0000000 --- a/packages/router-skills/skills/v1/render-optimizations.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: render-optimizations -title: Render Optimizations -versions: - - latest - - ">=1 <2" -summary: Reduce rerenders and keep navigation smooth. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/render-optimizations ---- - -# Render Optimizations - -Purpose: - -- Reduce rerenders and keep navigation smooth. - -Scope: - -- Use when complex UIs re-render during navigation. - -Guidelines: - -- Prefer route-scoped hooks to avoid global rerenders. -- Avoid heavy computation in route components. -- Memoize derived UI from router state when needed. - -Examples: - -```ts -const isLoading = useRouterState({ - select: (state) => state.isLoading, -}) -``` - -```tsx -const Sidebar = memo(SidebarImpl) -``` diff --git a/packages/router-skills/skills/v1/route-context.md b/packages/router-skills/skills/v1/route-context.md deleted file mode 100644 index 40ccdac..0000000 --- a/packages/router-skills/skills/v1/route-context.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: route-context -title: Route Context -versions: - - latest - - ">=1 <2" -summary: Pass shared dependencies into loaders and routes. -resources: - - https://tanstack.com/router/latest/docs/guide/router-context - - https://tanstack.com/router/latest/docs/api/router/use-route-context ---- - -# Route Context - -Purpose: - -- Share dependencies across loaders and hooks. - -Scope: - -- Use when providing API clients, caches, or environment data. - -Guidelines: - -- Define context once at router creation. -- Keep context stable across navigations. -- Use context in loaders to avoid module globals. -- Read context with route hooks instead of imports. - -Examples: - -```ts -const router = createRouter({ - routeTree, - context: { api, queryClient }, -}) -``` - -```ts -const { api } = route.useRouteContext() -``` diff --git a/packages/router-skills/skills/v1/route-ids.md b/packages/router-skills/skills/v1/route-ids.md deleted file mode 100644 index ae1984b..0000000 --- a/packages/router-skills/skills/v1/route-ids.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: route-ids -title: Route IDs -versions: - - latest - - ">=1 <2" -summary: Keep route identifiers stable across refactors. -resources: - - https://tanstack.com/router/latest/docs/guide/route-ids - - https://tanstack.com/router/latest/docs/api/router/create-route - - https://tanstack.com/router/latest/docs/api/router/use-match ---- - -# Route IDs - -Purpose: - -- Keep route identifiers stable across path changes. - -Scope: - -- Use when linking routes or targeting route-level APIs. - -Guidelines: - -- Prefer explicit IDs for long-lived routes. -- Avoid deriving IDs from paths that may change. -- Keep IDs short and human-readable. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - id: "projects.detail", - path: "projects/$projectId", -}) -``` - -```ts -const match = useMatch({ from: "projects.detail" }) -``` diff --git a/packages/router-skills/skills/v1/route-lazy-loading.md b/packages/router-skills/skills/v1/route-lazy-loading.md deleted file mode 100644 index 73e9d27..0000000 --- a/packages/router-skills/skills/v1/route-lazy-loading.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: route-lazy-loading -title: Route Lazy Loading -versions: - - latest - - ">=1 <2" -summary: Split route code to reduce initial bundle size. -resources: - - https://tanstack.com/router/latest/docs/guide/code-splitting ---- - -# Route Lazy Loading - -Purpose: - -- Split route code to reduce initial bundle size. - -Scope: - -- Use when routes are large or rarely visited. - -Guidelines: - -- Lazy-load route components and loaders together. -- Keep critical routes eager. -- Pair with prefetching for smooth UX. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "reports", - component: () => import("./Reports").then((m) => m.Route), -}) -``` - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "analytics", - component: () => import("./Analytics").then((m) => m.AnalyticsPage), -}) -``` diff --git a/packages/router-skills/skills/v1/route-masking.md b/packages/router-skills/skills/v1/route-masking.md deleted file mode 100644 index 3772158..0000000 --- a/packages/router-skills/skills/v1/route-masking.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: route-masking -title: Route Masking -versions: - - latest - - ">=1 <2" -summary: Present friendly URLs without changing route structure. -resources: - - https://tanstack.com/router/latest/docs/guide/route-masking ---- - -# Route Masking - -Purpose: - -- Present friendly URLs without changing route structure. - -Scope: - -- Use when exposing public paths for internal routes. - -Guidelines: - -- Define masks for user-friendly URLs. -- Keep masks aligned with params and search. -- Document masks to avoid confusion. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "projects/$projectId", - mask: "/p/$projectId", -}) -``` - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "users/$userId", - mask: "/u/$userId", -}) -``` diff --git a/packages/router-skills/skills/v1/route-trees.md b/packages/router-skills/skills/v1/route-trees.md deleted file mode 100644 index e2407f6..0000000 --- a/packages/router-skills/skills/v1/route-trees.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -id: route-trees -title: Route Trees -versions: - - latest - - ">=1 <2" -summary: Define the parent-child route tree. -resources: - - https://tanstack.com/router/latest/docs/guide/route-trees - - https://tanstack.com/router/latest/docs/api/router/create-root-route - - https://tanstack.com/router/latest/docs/api/router/create-route ---- - -# Route Trees - -Purpose: - -- Define the parent-child route tree. - -Scope: - -- Use when establishing the overall route hierarchy. - -Guidelines: - -- Start with a root route, then add children. -- Keep parent routes responsible for shared UI. -- Keep the tree shallow unless layouts require depth. -- Group routes by feature for readability. - -Examples: - -```ts -const rootRoute = createRootRoute({ component: RootLayout }) -const appRoute = createRoute({ getParentRoute: () => rootRoute, path: "app" }) -const projectRoute = createRoute({ - getParentRoute: () => appRoute, - path: "projects/$projectId", -}) -``` - -```ts -const settingsRoute = createRoute({ - getParentRoute: () => appRoute, - path: "settings", -}) -``` diff --git a/packages/router-skills/skills/v1/router-devtools.md b/packages/router-skills/skills/v1/router-devtools.md deleted file mode 100644 index 9fffcb7..0000000 --- a/packages/router-skills/skills/v1/router-devtools.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: router-devtools -title: Router Devtools -versions: - - latest - - ">=1 <2" -summary: Inspect routes, matches, and loader data. -resources: - - https://tanstack.com/router/latest/docs/devtools ---- - -# Router Devtools - -Purpose: - -- Inspect routes, matches, and loader data. - -Scope: - -- Use during development to debug navigation. - -Guidelines: - -- Enable devtools only in dev builds. -- Use it to verify loader data and matches. -- Keep it out of production bundles. - -Examples: - -```tsx - -``` - -```tsx -{import.meta.env.DEV ? : null} -``` diff --git a/packages/router-skills/skills/v1/router-setup.md b/packages/router-skills/skills/v1/router-setup.md deleted file mode 100644 index fd48afe..0000000 --- a/packages/router-skills/skills/v1/router-setup.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: router-setup -title: Router Setup -versions: - - latest - - ">=1 <2" -summary: Create the router, provide context, and mount it in the app. -resources: - - https://tanstack.com/router/latest/docs/guide/installation - - https://tanstack.com/router/latest/docs/api/router/create-router - - https://tanstack.com/router/latest/docs/api/router/router-provider - - https://tanstack.com/router/latest/docs/api/router/use-router ---- - -# Router Setup - -Purpose: - -- Create the router instance, provide shared context, and mount it. - -Scope: - -- Use when initializing a Router app or adding Router to an existing app. - -Guidelines: - -- Create the router once at app startup. -- Provide shared dependencies (API clients, caches) in router context. -- Mount the provider near the root of the app. -- Use the router instance directly only for advanced cases. - -Examples: - -```ts -const router = createRouter({ routeTree, context: { api } }) -``` - -```tsx - -``` - -```ts -const router = useRouter() -``` diff --git a/packages/router-skills/skills/v1/router-state.md b/packages/router-skills/skills/v1/router-state.md deleted file mode 100644 index d81343b..0000000 --- a/packages/router-skills/skills/v1/router-state.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: router-state -title: Router State -versions: - - latest - - ">=1 <2" -summary: Read router status for pending UI and transitions. -resources: - - https://tanstack.com/router/latest/docs/guide/router-state - - https://tanstack.com/router/latest/docs/api/router/use-router-state ---- - -# Router State - -Purpose: - -- Read router status for pending UI and transitions. - -Scope: - -- Use when showing loading indicators or transitions. - -Guidelines: - -- Show global pending UI sparingly. -- Prefer route-level pending states when possible. -- Avoid blocking navigation on long transitions. -- Use the router state to drive spinners or skeletons. - -Examples: - -```ts -const state = useRouterState() -const isLoading = state.isLoading -``` - -```tsx -{isLoading ? : null} -``` diff --git a/packages/router-skills/skills/v1/routing-strategies.md b/packages/router-skills/skills/v1/routing-strategies.md deleted file mode 100644 index 271d564..0000000 --- a/packages/router-skills/skills/v1/routing-strategies.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -id: routing-strategies -title: Routing Strategies -versions: - - latest - - ">=1 <2" -summary: Choose between code-based, file-based, and virtual file routes. -resources: - - https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts - - https://tanstack.com/router/latest/docs/framework/react/routing/code-based-routing - - https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing - - https://tanstack.com/router/latest/docs/framework/react/routing/virtual-file-routes - - https://tanstack.com/router/latest/docs/framework/react/routing/file-naming-conventions ---- - -# Routing Strategies - -Purpose: - -- Pick the routing strategy that matches your project size and workflow. - -Scope: - -- Use when deciding how routes are created or organized. - -Guidelines: - -- Use code-based routing for dynamic trees or programmatic route creation. -- Use file-based routing for large apps with many pages. -- Use virtual file routes when routes are generated by tooling. -- Follow file naming conventions to keep the route tree predictable. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "settings", - component: SettingsPage, -}) -``` - -```text -routes/ - _layout.tsx - settings.tsx -``` diff --git a/packages/router-skills/skills/v1/scroll-restoration.md b/packages/router-skills/skills/v1/scroll-restoration.md deleted file mode 100644 index 05a5410..0000000 --- a/packages/router-skills/skills/v1/scroll-restoration.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: scroll-restoration -title: Scroll Restoration -versions: - - latest - - ">=1 <2" -summary: Restore or manage scroll position between navigations. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/scroll-restoration ---- - -# Scroll Restoration - -Purpose: - -- Restore scroll position between navigations. - -Scope: - -- Use in long lists or pages with deep scroll. - -Guidelines: - -- Enable restoration for list/detail flows. -- Reset scroll on route changes when needed. -- Keep custom scroll containers in sync. - -Examples: - -```tsx - -``` - -```ts -router.options.scrollRestoration = true -``` diff --git a/packages/router-skills/skills/v1/search-params.md b/packages/router-skills/skills/v1/search-params.md deleted file mode 100644 index 68ad592..0000000 --- a/packages/router-skills/skills/v1/search-params.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -id: search-params -title: Search Params -versions: - - latest - - ">=1 <2" -summary: Validate, default, and read search params. -resources: - - https://tanstack.com/router/latest/docs/guide/search-params - - https://tanstack.com/router/latest/docs/api/router/create-route - - https://tanstack.com/router/latest/docs/api/router/use-search ---- - -# Search Params - -Purpose: - -- Validate, default, and read search params consistently. - -Scope: - -- Use for filters, pagination, and optional flags in the URL. - -Guidelines: - -- Use `validateSearch` with a schema adapter for type safety. -- Define defaults in the schema for stable URLs. -- Keep search values serializable and small. -- Use route-scoped `useSearch` to read validated values. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "projects", - validateSearch: searchSchema, -}) -``` - -```ts -const searchSchema = z.object({ - page: z.number().default(1), - filter: z.string().optional(), -}) -``` - -```ts -const search = route.useSearch() -``` diff --git a/packages/router-skills/skills/v1/ssr-loaders.md b/packages/router-skills/skills/v1/ssr-loaders.md deleted file mode 100644 index afe33fb..0000000 --- a/packages/router-skills/skills/v1/ssr-loaders.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -id: ssr-loaders -title: SSR Loaders -versions: - - latest - - ">=1 <2" -summary: Keep loader data SSR-safe for hydration. -resources: - - https://tanstack.com/router/latest/docs/guide/ssr - - https://tanstack.com/router/latest/docs/guide/loaders ---- - -# SSR Loaders - -Purpose: - -- Ensure loader data is safe to serialize and hydrate. - -Scope: - -- Use when enabling SSR or streaming. - -Guidelines: - -- Return serializable values from loaders. -- Avoid browser-only APIs in loaders. -- Keep server-only dependencies in context. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "settings", - loader: ({ context }) => context.api.getSettings(), -}) -``` - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "profile", - loader: ({ context }) => ({ user: context.user }), -}) -``` diff --git a/packages/router-skills/skills/v1/static-route-data.md b/packages/router-skills/skills/v1/static-route-data.md deleted file mode 100644 index de66f1a..0000000 --- a/packages/router-skills/skills/v1/static-route-data.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: static-route-data -title: Static Route Data -versions: - - latest - - ">=1 <2" -summary: Attach static data to routes for UI hints or config. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/static-route-data ---- - -# Static Route Data - -Purpose: - -- Attach static data to routes for UI hints or configuration. - -Scope: - -- Use when data does not depend on params or loaders. - -Guidelines: - -- Keep static data serializable. -- Use static data for UI hints (labels, icons). -- Avoid mixing static data with loader results. - -Examples: - -```ts -const route = createRoute({ - getParentRoute: () => rootRoute, - path: "projects", - staticData: { title: "Projects" }, -}) -``` - -```ts -const match = useMatch({ from: "projects" }) -const title = match.staticData?.title -``` diff --git a/packages/router-skills/skills/v1/type-safety.md b/packages/router-skills/skills/v1/type-safety.md deleted file mode 100644 index 8a19a55..0000000 --- a/packages/router-skills/skills/v1/type-safety.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: type-safety -title: Type Safety and Utilities -versions: - - latest - - ">=1 <2" -summary: Keep routes, params, and search types consistent. -resources: - - https://tanstack.com/router/latest/docs/framework/react/guide/type-safety - - https://tanstack.com/router/latest/docs/framework/react/guide/type-utilities ---- - -# Type Safety and Utilities - -Purpose: - -- Keep routes, params, and search types consistent. - -Scope: - -- Use when tightening type coverage or debugging inference. - -Guidelines: - -- Prefer typed route helpers over manual strings. -- Use type utilities to extract route param or search types. -- Keep search schemas aligned with validation. - -Examples: - -```ts -type Search = typeof route.types.search -``` - -```ts -type Params = typeof route.types.params -``` diff --git a/packages/router-skills/skills/v1/view-transitions.md b/packages/router-skills/skills/v1/view-transitions.md deleted file mode 100644 index 3f83d4f..0000000 --- a/packages/router-skills/skills/v1/view-transitions.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: view-transitions -title: View Transitions -versions: - - latest - - ">=1 <2" -summary: Add view transitions to route navigations. -resources: - - https://tanstack.com/router/latest/docs/framework/react/examples/view-transitions ---- - -# View Transitions - -Purpose: - -- Add view transitions to route navigations. - -Scope: - -- Use when you want animated transitions between routes. - -Guidelines: - -- Keep transitions subtle to preserve UX. -- Avoid blocking navigation during long animations. -- Test transitions with pending UI states. - -Examples: - -```ts -document.startViewTransition(() => router.navigate({ to: "/projects" })) -``` - -```ts -const navigate = useNavigate() -document.startViewTransition(() => navigate({ to: "/projects" })) -``` diff --git a/packages/start-skills/README.md b/packages/start-skills/README.md deleted file mode 100644 index f193451..0000000 --- a/packages/start-skills/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @tanstack/start/skills - -TanStack Start skill sources staged in this repo. - -Sources live under `skills/v1/` with a deterministic topic list in `skills/topics.json`. diff --git a/packages/start-skills/manifest.json b/packages/start-skills/manifest.json deleted file mode 100644 index b288616..0000000 --- a/packages/start-skills/manifest.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "package": "@tanstack/start/skills", - "topics": "skills/topics.json", - "versions": { - "v1": { - "index": "skills/v1/index.md", - "files": [ - "skills/v1/authentication.md", - "skills/v1/databases.md", - "skills/v1/deployment.md", - "skills/v1/environment-config.md", - "skills/v1/error-boundaries.md", - "skills/v1/execution-model.md", - "skills/v1/file-structure.md", - "skills/v1/index.md", - "skills/v1/middleware.md", - "skills/v1/observability.md", - "skills/v1/path-aliases.md", - "skills/v1/routing.md", - "skills/v1/router-integration.md", - "skills/v1/selective-ssr.md", - "skills/v1/server-entry-point.md", - "skills/v1/server-functions.md", - "skills/v1/server-routes.md", - "skills/v1/start-setup.md", - "skills/v1/static-prerendering.md", - "skills/v1/streaming-ssr.md" - ] - } - } -} diff --git a/packages/start-skills/package.json b/packages/start-skills/package.json deleted file mode 100644 index 4dd6ae0..0000000 --- a/packages/start-skills/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@tanstack/start/skills", - "version": "0.0.0", - "private": true, - "description": "TanStack Start skill sources (staged in tanstack/agents)", - "license": "MIT", - "type": "module" -} diff --git a/packages/start-skills/project.json b/packages/start-skills/project.json deleted file mode 100644 index 123a753..0000000 --- a/packages/start-skills/project.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "start-skills", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "library", - "sourceRoot": "packages/start-skills", - "targets": { - "build": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('build:noop\\n')\"" - } - }, - "test:eslint": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:eslint:noop\\n')\"" - } - }, - "test:types": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:types:noop\\n')\"" - } - }, - "test:lib": { - "executor": "nx:run-commands", - "options": { - "command": "node -e \"process.stdout.write('test:lib:noop\\n')\"" - } - } - } -} diff --git a/packages/start-skills/skills/topics.json b/packages/start-skills/skills/topics.json deleted file mode 100644 index 802811d..0000000 --- a/packages/start-skills/skills/topics.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - "index.md", - "start-setup.md", - "file-structure.md", - "routing.md", - "router-integration.md", - "path-aliases.md", - "execution-model.md", - "server-entry-point.md", - "environment-config.md", - "server-functions.md", - "server-routes.md", - "databases.md", - "middleware.md", - "streaming-ssr.md", - "selective-ssr.md", - "static-prerendering.md", - "error-boundaries.md", - "authentication.md", - "observability.md", - "deployment.md" -] diff --git a/packages/start-skills/skills/v1/authentication.md b/packages/start-skills/skills/v1/authentication.md deleted file mode 100644 index 0fdd14d..0000000 --- a/packages/start-skills/skills/v1/authentication.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -id: authentication -title: Authentication -versions: - - latest -summary: Define auth boundaries and implement login flows. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Authentication - -Purpose: - -- Define authentication boundaries and implement login flows. - -Scope: - -- Use when planning sessions, cookies, providers, or token storage. - -Guidelines: - -- Keep auth checks in middleware or server functions, not client-only code. -- Use `@skills/router/authenticated-routes` for route guards and redirects. -- Align session storage with the chosen adapter runtime. - -Examples: - -```ts -export const login = serverFn(async (input) => { - const user = await verifyCredentials(input) - return createSession(user) -}) -``` diff --git a/packages/start-skills/skills/v1/databases.md b/packages/start-skills/skills/v1/databases.md deleted file mode 100644 index 14321f4..0000000 --- a/packages/start-skills/skills/v1/databases.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -id: databases -title: Databases -versions: - - latest -summary: Access databases safely in Start apps. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Databases - -Purpose: - -- Keep database access on the server while keeping types aligned. - -Scope: - -- Use when introducing database clients or query layers. - -Guidelines: - -- Initialize database clients in server-only modules. -- Expose data through server functions or `@skills/router/loaders`. -- Align connection pooling with the adapter runtime. - -Examples: - -```ts -export const listProjects = serverFn(async () => { - return db.project.findMany() -}) -``` diff --git a/packages/start-skills/skills/v1/deployment.md b/packages/start-skills/skills/v1/deployment.md deleted file mode 100644 index ff330b9..0000000 --- a/packages/start-skills/skills/v1/deployment.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -id: deployment -title: Deployment -versions: - - latest -summary: Deploy Start apps to hosting targets. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Deployment - -Purpose: - -- Ship Start apps with predictable builds and runtime behavior. - -Scope: - -- Use when targeting a new hosting provider or build pipeline. - -Guidelines: - -- Confirm adapter compatibility with the hosting provider. -- Validate SSR requirements with `@skills/router/ssr-loaders`. -- Ensure server functions are deployed to the correct runtime. diff --git a/packages/start-skills/skills/v1/environment-config.md b/packages/start-skills/skills/v1/environment-config.md deleted file mode 100644 index f0b30ce..0000000 --- a/packages/start-skills/skills/v1/environment-config.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: environment-config -title: Environment -versions: - - latest -summary: Configure environment variables and runtime helpers. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Environment - -Purpose: - -- Define environment variables and runtime helpers safely. - -Scope: - -- Use when wiring environment variables, secrets, and runtime helpers. - -Guidelines: - -- Validate required environment variables at startup. -- Keep secrets server-only and never expose them to client bundles. -- Keep environment helpers in server entry files or server functions. -- Validate helpers against the selected adapter runtime. - -Examples: - -```ts -const requireEnv = (key: string) => { - const value = process.env[key] - if (!value) throw new Error(`Missing ${key}`) - return value -} - -export const env = { - databaseUrl: requireEnv('DATABASE_URL'), -} -``` diff --git a/packages/start-skills/skills/v1/error-boundaries.md b/packages/start-skills/skills/v1/error-boundaries.md deleted file mode 100644 index daee6f9..0000000 --- a/packages/start-skills/skills/v1/error-boundaries.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: error-boundaries -title: Error Handling -versions: - - latest -summary: Handle runtime and hydration errors safely. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Error Handling - -Purpose: - -- Define how errors surface during server rendering and hydration. - -Scope: - -- Use when handling runtime failures and hydration mismatches. - -Guidelines: - -- Capture server errors in middleware or server functions. -- Use `@skills/router/error-boundaries` for route-level UI fallback. -- Pair 404 handling with `@skills/router/not-found-boundaries`. -- Ensure server-rendered data matches client expectations. diff --git a/packages/start-skills/skills/v1/execution-model.md b/packages/start-skills/skills/v1/execution-model.md deleted file mode 100644 index c8b68e0..0000000 --- a/packages/start-skills/skills/v1/execution-model.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: execution-model -title: Execution Model -versions: - - latest -summary: Understand where Start code executes. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Execution Model - -Purpose: - -- Clarify what runs on the server, client, or both. - -Scope: - -- Use when debugging execution context and data flow. - -Guidelines: - -- Keep server-only logic in entry points and server functions. -- Use server functions or `@skills/router/loaders` for data fetching. -- Avoid importing server-only modules in client entry files. -- Avoid relying on browser-only APIs during SSR. - -Examples: - -```ts -// server-only module -export async function readSecrets() { - return loadFromVault() -} - -// server function uses the server-only module -export const getSession = serverFn(async () => readSecrets()) -``` diff --git a/packages/start-skills/skills/v1/file-structure.md b/packages/start-skills/skills/v1/file-structure.md deleted file mode 100644 index 1bd0dfa..0000000 --- a/packages/start-skills/skills/v1/file-structure.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: file-structure -title: File Structure -versions: - - latest -summary: Organize Start entry points, routes, and shared modules. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# File Structure - -Purpose: - -- Clarify where Start expects app entry points, routes, and server-only modules. - -Scope: - -- Use when setting up a new app or refactoring structure. - -Guidelines: - -- Keep entry files and route modules minimal and focused. -- Separate shared utilities, server-only helpers, and client-only components. -- Group runtime-only code (server functions, middleware, server routes) together. -- Use `@skills/router/file-based-routing` if you lean on Router file routing. - -Examples: - -``` -app/ - routes/ - index.tsx - settings.tsx - entry-client.tsx - entry-server.tsx -server/ - routes/ - middleware/ -shared/ - formatting.ts -``` diff --git a/packages/start-skills/skills/v1/index.md b/packages/start-skills/skills/v1/index.md deleted file mode 100644 index 4fed3d7..0000000 --- a/packages/start-skills/skills/v1/index.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: start-index -title: Start Skill Index -versions: - - latest -summary: Choose the right Start skill based on the task at hand. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Start Skill Index - -Purpose: - -- Help pick the right Start skill quickly. - -Scope: - -- Applies to any Start adapter or deployment target. -- Routes to focused skills for setup, server functions, and deployment. - -How to pick a skill: - -Core setup and structure: - -- Bootstrapping a Start app -> `@skills/start/start-setup` -- Understanding app entry points and layout -> `@skills/start/file-structure` -- Routing and route structure -> `@skills/start/routing` -- Wiring Start with TanStack Router -> `@skills/start/router-integration` -- Shared import paths -> `@skills/start/path-aliases` -- Tailwind integration -> `@skills/start/tailwind-integration` - -Runtime and execution: - -- Execution model basics -> `@skills/start/execution-model` -- Client/server entry points -> `@skills/start/server-entry-point` -- Environment config and helpers -> `@skills/start/environment-config` -- Adapter selection -> `@skills/start/adapters` - -Server and data: - -- Typed server functions -> `@skills/start/server-functions` -- Server-only routes -> `@skills/start/server-routes` -- Database access -> `@skills/start/databases` -- Request/response middleware -> `@skills/start/middleware` -- Streaming SSR and suspense flows -> `@skills/start/streaming-ssr` -- Rendering modes -> `@skills/start/selective-ssr` -- Static generation -> `@skills/start/static-prerendering` -- Error handling and hydration -> `@skills/start/error-boundaries` - -Authentication: - -- Auth boundaries and implementation -> `@skills/start/authentication` - -Operations and deployment: - -- Observability and tracing -> `@skills/start/observability` -- Deploying to a hosting target -> `@skills/start/deployment` - -Next: - -- After picking a skill, follow its resource links and guidance. diff --git a/packages/start-skills/skills/v1/middleware.md b/packages/start-skills/skills/v1/middleware.md deleted file mode 100644 index dabf3bc..0000000 --- a/packages/start-skills/skills/v1/middleware.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -id: middleware -title: Middleware -versions: - - latest -summary: Add request/response middleware for authentication and headers. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Middleware - -Purpose: - -- Centralize auth, logging, and request transforms. - -Scope: - -- Use for cross-cutting concerns across all routes and server functions. - -Guidelines: - -- Keep middleware side-effect free where possible. -- Avoid router-specific logic; delegate to `@skills/router/authenticated-routes`. -- Ensure middleware stays compatible with the chosen adapter runtime. - -Examples: - -```ts -export const middleware = defineMiddleware(async (request, next) => { - const requestId = crypto.randomUUID() - return next(request, { headers: { 'x-request-id': requestId } }) -}) -``` diff --git a/packages/start-skills/skills/v1/observability.md b/packages/start-skills/skills/v1/observability.md deleted file mode 100644 index 286208d..0000000 --- a/packages/start-skills/skills/v1/observability.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: observability -title: Observability -versions: - - latest -summary: Instrument Start apps for logging and tracing. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Observability - -Purpose: - -- Add logging, metrics, and tracing around Start runtime logic. - -Scope: - -- Use when instrumenting server functions, middleware, and adapters. - -Guidelines: - -- Instrument middleware and server functions first. -- Use `@skills/router/router-devtools` for routing diagnostics. -- Ensure observability tooling supports the hosting runtime. - -Examples: - -```ts -export const middleware = defineMiddleware(async (request, next) => { - const start = Date.now() - const response = await next(request) - logRequest({ path: request.url, ms: Date.now() - start }) - return response -}) -``` diff --git a/packages/start-skills/skills/v1/path-aliases.md b/packages/start-skills/skills/v1/path-aliases.md deleted file mode 100644 index 160583e..0000000 --- a/packages/start-skills/skills/v1/path-aliases.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: path-aliases -title: Path Aliases -versions: - - latest -summary: Configure path aliases for Start projects. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Path Aliases - -Purpose: - -- Simplify imports across shared client and server code. - -Scope: - -- Use when standardizing module resolution. - -Guidelines: - -- Keep aliases consistent across tooling and the adapter runtime. -- Avoid aliasing server-only modules into client bundles. -- Document shared aliases for the team. - -Examples: - -```json -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@shared/*": ["shared/*"] - } - } -} -``` diff --git a/packages/start-skills/skills/v1/router-integration.md b/packages/start-skills/skills/v1/router-integration.md deleted file mode 100644 index d7acd2f..0000000 --- a/packages/start-skills/skills/v1/router-integration.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: router-integration -title: Router Integration -versions: - - latest -summary: Connect Start runtime concerns with TanStack Router setup. -resources: - - https://tanstack.com/router/latest/docs/guide/overview - - https://tanstack.com/start/latest/docs/overview ---- - -# Router Integration - -Purpose: - -- Tie Start application wiring to Router configuration and data loading. - -Scope: - -- Use when aligning Start server rendering with Router route trees and loaders. - -Guidelines: - -- Build the route tree and loaders with Router-first patterns. -- Use `@skills/router/loaders` for data that must load before render. -- Coordinate SSR requirements with `@skills/router/ssr-loaders`. - -Examples: - -```ts -const router = createRouter({ - routeTree, -}) - -export default defineStartApp({ - router, - adapter, -}) -``` diff --git a/packages/start-skills/skills/v1/routing.md b/packages/start-skills/skills/v1/routing.md deleted file mode 100644 index 49c775b..0000000 --- a/packages/start-skills/skills/v1/routing.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -id: routing -title: Routing -versions: - - latest -summary: Connect Start to Router-based routing. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Routing - -Purpose: - -- Integrate Start runtime concerns with Router routing. - -Scope: - -- Use when defining routes or deciding on routing strategies. - -Guidelines: - -- Build route definitions with `@skills/router/route-trees`. -- Use `@skills/router/file-based-routing` for file-driven routing. -- Follow `@skills/router/routing-strategies` for route structure choices. - -Examples: - -```ts -const router = createRouter({ - routeTree, - defaultPreload: 'intent', -}) -``` diff --git a/packages/start-skills/skills/v1/selective-ssr.md b/packages/start-skills/skills/v1/selective-ssr.md deleted file mode 100644 index 2cce64d..0000000 --- a/packages/start-skills/skills/v1/selective-ssr.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: selective-ssr -title: Rendering Modes -versions: - - latest -summary: Choose SSR, selective SSR, or SPA rendering. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Rendering Modes - -Purpose: - -- Tune rendering behavior per route or app. - -Scope: - -- Use when mixing SSR and client-only routes. - -Guidelines: - -- Decide SSR strategy per route with `@skills/router/ssr-loaders`. -- Ensure SPA routes load data via `@skills/router/loaders`. -- Avoid server-only APIs in client-only render paths. -- Validate adapter support for partial SSR and SPA builds. diff --git a/packages/start-skills/skills/v1/server-entry-point.md b/packages/start-skills/skills/v1/server-entry-point.md deleted file mode 100644 index dee87db..0000000 --- a/packages/start-skills/skills/v1/server-entry-point.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: server-entry-point -title: Entry Points -versions: - - latest -summary: Configure client and server entry points. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Entry Points - -Purpose: - -- Wire client hydration and server rendering. - -Scope: - -- Use when customizing client and server entry files. - -Guidelines: - -- Keep entry points focused on runtime wiring, not route definitions. -- Keep adapter wiring in the server entry layer. -- Use `@skills/router/router-setup` for Router initialization. -- Coordinate hydration data with `@skills/router/ssr-loaders`. - -Examples: - -```ts -// entry-client.tsx -hydrateRoot(document, ) - -// entry-server.tsx -const html = await renderToString() -``` diff --git a/packages/start-skills/skills/v1/server-functions.md b/packages/start-skills/skills/v1/server-functions.md deleted file mode 100644 index 4e89963..0000000 --- a/packages/start-skills/skills/v1/server-functions.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -id: server-functions -title: Server Functions -versions: - - latest -summary: Create typed server functions with secure inputs and outputs. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Server Functions - -Purpose: - -- Implement server-only logic with typed boundaries. - -Scope: - -- Use for mutations, data fetching, or sensitive operations. - -Guidelines: - -- Validate inputs before executing server logic. -- Return serializable data for hydration and streaming. -- Use `@skills/router/loaders` for route-level data reads. -- Coordinate cache invalidation with `@skills/router/data-refresh`. - -Examples: - -```ts -export const updateProfile = serverFn(async (input) => { - await requireSession() - return updateUser(input) -}) -``` diff --git a/packages/start-skills/skills/v1/server-routes.md b/packages/start-skills/skills/v1/server-routes.md deleted file mode 100644 index 891c749..0000000 --- a/packages/start-skills/skills/v1/server-routes.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: server-routes -title: Server Routes -versions: - - latest -summary: Add server routes outside the Router UI. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Server Routes - -Purpose: - -- Build server-only endpoints for APIs or webhooks. - -Scope: - -- Use when you need endpoints not tied to UI routes. - -Guidelines: - -- Keep UI routing in `@skills/router/route-trees`. -- Use server routes for webhooks, callbacks, or internal APIs. -- Protect server routes with middleware and auth checks. - -Examples: - -```ts -export const webhook = defineServerRoute({ - method: 'POST', - handler: async (request) => { - await verifySignature(request) - return new Response('ok') - }, -}) -``` diff --git a/packages/start-skills/skills/v1/start-setup.md b/packages/start-skills/skills/v1/start-setup.md deleted file mode 100644 index e741a48..0000000 --- a/packages/start-skills/skills/v1/start-setup.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: start-setup -title: Setup -versions: - - latest -summary: Initialize TanStack Start, pick an adapter, and boot the app. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Setup - -Purpose: - -- Initialize a Start application and connect it to the runtime adapter. - -Scope: - -- Use when creating a new Start app or upgrading an existing setup. - -Guidelines: - -- Decide the adapter early (Node, edge, or serverless) to match deployment constraints. -- Keep server-only modules separated from client bundles. -- Establish entry points early so routing and SSR wiring stay consistent. -- Document how environment variables map to the chosen adapter runtime. -- For routing, follow `@skills/router/router-setup` and `@skills/router/route-trees`. - -Examples: - -```ts -const router = createRouter({ - routeTree, - defaultPreload: 'intent', -}) - -export default defineStartApp({ - router, - adapter, -}) -``` diff --git a/packages/start-skills/skills/v1/static-prerendering.md b/packages/start-skills/skills/v1/static-prerendering.md deleted file mode 100644 index 7a98fde..0000000 --- a/packages/start-skills/skills/v1/static-prerendering.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -id: static-prerendering -title: Static Generation -versions: - - latest -summary: Pre-render routes and run build-time logic. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Static Generation - -Purpose: - -- Generate static HTML and build-time data for routes. - -Scope: - -- Use when deploying to static hosting or precomputing data. - -Guidelines: - -- Keep static data deterministic and serializable. -- Use `@skills/router/static-route-data` for static data needs. -- Avoid runtime-only dependencies in build-time execution. -- Confirm adapter support for prerendering. - -Examples: - -```ts -export const prerenderRoutes = ['/', '/pricing', '/docs'] -``` diff --git a/packages/start-skills/skills/v1/streaming-ssr.md b/packages/start-skills/skills/v1/streaming-ssr.md deleted file mode 100644 index 8904cf2..0000000 --- a/packages/start-skills/skills/v1/streaming-ssr.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -id: streaming-ssr -title: Streaming SSR -versions: - - latest -summary: Configure streaming SSR, suspense boundaries, and serialization. -resources: - - https://tanstack.com/start/latest/docs/overview ---- - -# Streaming SSR - -Purpose: - -- Deliver fast, streaming responses with progressive hydration. - -Scope: - -- Use when tuning render latency or adding suspense boundaries. - -Guidelines: - -- Ensure loader data is serializable with `@skills/router/ssr-loaders`. -- Split non-critical UI into suspense boundaries for streaming. -- Verify the adapter supports streaming responses. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b050aa3..228ca2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,15 @@ importers: vitest: specifier: ^4.0.17 version: 4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(yaml@2.8.2) + yaml: + specifier: ^2.7.0 + version: 2.8.2 + + packages/playbooks: + devDependencies: + yaml: + specifier: ^2.7.0 + version: 2.8.2 packages: diff --git a/rfc.md b/rfc.md new file mode 100644 index 0000000..2ae754b --- /dev/null +++ b/rfc.md @@ -0,0 +1,858 @@ +--- +title: TanStack Playbook +version: '1.4' +status: draft +owner: tanstack-core +contributors: [] +created: 2026-02-19 +last_updated: 2026-02-19 +--- + +# TanStack Playbook + +## Summary + +Coding agents now perform half of development work, but without TanStack-specific guidance they rely on outdated or incomplete knowledge — conflating packages, missing compositions, or applying wrong framework patterns. This RFC defines a playbook architecture: a single `@tanstack/playbook` repo that owns all skills, distilled from the source documentation of `@tanstack/query`, `@tanstack/router`, `@tanstack/db`, `@tanstack/form`, and `@tanstack/table`. Skills are written for agent consumption (not human reading), generated initially via a documented prompt, and kept current via a CI system using GitHub Actions as a trigger layer and Warp Oz as the autonomous agent that evaluates staleness, rewrites skills, and opens PRs. Each library has its own router skill that routes agents within that library's skills — there is no global cross-library router in v1. A CLI installs skills to `.agents/skills/` by default with other agent directories as options, and emits an AGENTS.md snippet on install. Phase 1 covers React adapter only. + +--- + +## Background + +### A Playbook is a Collection of Skills + +A **playbook** is the collection of skills, patterns, and tools someone uses in a vibe-coding context — analogous to "stack" but for the less deliberate, more aesthetic-driven way people assemble their setup now. + +`@tanstack/playbook` is a single npm package and repo that contains all TanStack skills. It draws from five source packages but owns all the content itself: + +- Skills distilled from `@tanstack/query` — async state management, fetching, caching, mutations +- Skills distilled from `@tanstack/router` — type-safe routing, search params, loaders +- Skills distilled from `@tanstack/db` — reactive client-side database, live queries, optimistic mutations +- Skills distilled from `@tanstack/form` — form state, async validation, submission +- Skills distilled from `@tanstack/table` — headless table logic, sorting, filtering, pagination + +### Skills Are Written for Agents, Not People + +Skills are not documentation. They are agent instructions — terse, procedural, example-driven. The target reader is a coding agent (Claude Code, Cursor, Copilot, Warp Oz) that already knows TypeScript and React. A skill should only contain what the agent cannot already know: TanStack-specific patterns, package-specific API shapes, common mistakes, and composition recipes. + +The Skills specification (agentskills.io) defines the standard format: + +``` +skill-name/ +├── SKILL.md # Entry point (<500 lines) +├── scripts/ # Executable code +├── references/ # On-demand detail +└── assets/ # Templates, schemas +``` + +Skills use progressive disclosure: metadata loads at session start, full instructions load when triggered, references load only when explicitly needed. This optimizes context window usage. + +### TanStack's Product Suite + +TanStack provides a composable set of headless, framework-agnostic primitives: + +- **TanStack Query** (`@tanstack/react-query`): Async state management — fetching, caching, background refetching +- **TanStack Router** (`@tanstack/react-router`): Fully type-safe routing, search params, navigation +- **TanStack DB** (`@tanstack/db`): Reactive client-side database with live queries and optimistic mutations +- **TanStack Form** (`@tanstack/react-form`): Headless form state, async validation, submission +- **TanStack Table** (`@tanstack/react-table`): Headless table logic — sorting, filtering, pagination, grouping + +Phase 1 covers React adapters only. Other framework adapters (Vue, Solid, Svelte, Angular) follow after React skills are validated. + +--- + +## Problem + +**Core hypothesis:** Developers will adopt and benefit from TanStack-specific agent skills. + +Without TanStack skills, agents rely on general knowledge that is frequently incomplete. Three specific problems: + +### 1. Fragmented Guidance Across Packages + +A developer building a typical app touches Router, Query, Form, and Table simultaneously. No unified guidance exists for how these compose. Agents stitch together five separate documentation sources and frequently miss cross-package patterns entirely. + +### 2. No Canonical Composition Recipes + +Common compositions lack clear guidance: + +- **Router + Query**: Using Router loaders to prefetch Query cache entries, avoiding waterfall fetching +- **DB + Query**: When to use TanStack DB live queries vs Query async fetching +- **Form + Query**: Submitting mutations, handling async validation against server state +- **Form + Table**: Inline editing with row-level form state + +### 3. Framework Adapter Confusion + +Each package ships multiple framework adapters. Agents frequently import from the wrong adapter or apply React patterns in non-React projects. Phase 1 eliminates this for React by explicitly scoping all skills to the React adapter. + +--- + +## Goals & Non-Goals + +### Goals + +1. **Publish `@tanstack/playbook`** as the single repo and npm package that owns all TanStack agent skills +2. **Distill skills from `@tanstack/query`** React docs into the playbook repo +3. **Distill skills from `@tanstack/router`** React docs into the playbook repo +4. **Distill skills from `@tanstack/db`** docs into the playbook repo +5. **Distill skills from `@tanstack/form`** React docs into the playbook repo +6. **Distill skills from `@tanstack/table`** React docs into the playbook repo +7. **Implement a per-library router skill** in each library group (e.g. `tanstack-query/router`, `tanstack-router/router`) that routes agents within that library's skills — no global cross-library router in v1 +8. **Implement CI automation** — GitHub Actions as trigger layer, Warp Oz as autonomous agent that evaluates staleness, rewrites skills, and opens PRs; including cross-skill reference staleness checks +9. **Implement a skill generation prompt** for bootstrapping skills from source docs for the first time +10. **CLI with selective install** — `.agents/skills/` as primary target, other agent directories as options; emits AGENTS.md snippet on install +11. **React-first** — Phase 1 covers React adapter only; other frameworks after validation + +### Non-Goals + +- **Skills co-located in package repos** — the playbook repo owns all skill content +- **Other framework adapters in Phase 1** — Vue, Solid, Svelte, Angular after React is validated +- **Evals and feedback loop** — separate effort when ready +- **Non-TypeScript languages** — TypeScript/JavaScript only +- **Instrumentation/telemetry** — rely on npm stats and qualitative feedback +- **Skill versioning strategy** — skills version with the playbook package +- **Backwards compatibility for skill structure changes** — not worth solving at this stage + +--- + +## Proposal + +### Repository Structure + +All skills live in `@tanstack/playbook`. The repo also contains the CLI, the Oz automation config, the internal staleness-check skill, and the skill generation prompt. + +``` +@tanstack/playbook/ +├── skills/ +│ ├── tanstack-query/ # Skills distilled from @tanstack/query +│ │ ├── router/ +│ │ │ └── SKILL.md # Query router: routes within Query skills only +│ │ ├── core/ +│ │ │ ├── SKILL.md +│ │ │ └── references/ +│ │ ├── caching/ +│ │ │ ├── SKILL.md +│ │ │ └── references/ +│ │ ├── infinite/ +│ │ │ └── SKILL.md +│ │ └── suspense/ +│ │ └── SKILL.md +│ │ +│ ├── tanstack-router/ # Skills distilled from @tanstack/router +│ │ ├── router/ +│ │ │ └── SKILL.md # Router router: routes within Router skills only +│ │ ├── core/ +│ │ │ ├── SKILL.md +│ │ │ └── references/ +│ │ ├── search-params/ +│ │ │ └── SKILL.md +│ │ ├── loaders/ +│ │ │ └── SKILL.md +│ │ └── with-query/ +│ │ └── SKILL.md +│ │ +│ ├── tanstack-db/ # Skills distilled from @tanstack/db +│ │ ├── router/ +│ │ │ └── SKILL.md # DB router: routes within DB skills only +│ │ ├── core/ +│ │ │ └── SKILL.md +│ │ ├── optimistic/ +│ │ │ └── SKILL.md +│ │ └── electric/ +│ │ └── SKILL.md +│ │ +│ ├── tanstack-form/ # Skills distilled from @tanstack/form +│ │ ├── router/ +│ │ │ └── SKILL.md # Form router: routes within Form skills only +│ │ ├── core/ +│ │ │ ├── SKILL.md +│ │ │ └── references/ +│ │ ├── async/ +│ │ │ └── SKILL.md +│ │ └── submission/ +│ │ └── SKILL.md +│ │ +│ ├── tanstack-table/ # Skills distilled from @tanstack/table +│ │ ├── router/ +│ │ │ └── SKILL.md # Table router: routes within Table skills only +│ │ ├── core/ +│ │ │ ├── SKILL.md +│ │ │ └── references/ +│ │ ├── features/ +│ │ │ └── SKILL.md +│ │ └── virtual/ +│ │ └── SKILL.md +│ │ +│ └── internal/ # Not installed to users +│ └── skill-staleness-check/ +│ └── SKILL.md +│ +├── prompts/ +│ └── generate-skill.md # Prompt for bootstrapping skills from source docs +│ +├── .warp/ +│ └── automations/ +│ └── skill-check.yml # Oz automation config +│ +├── src/ # CLI source +└── package.json +``` + +--- + +### Router Skill Design + +Each library group has its own router skill — `tanstack-query/router`, `tanstack-router/router`, `tanstack-db/router`, `tanstack-form/router`, `tanstack-table/router`. There is no global cross-library router in v1. + +A library router's job is narrow: when an agent is working with that library, the router reads what the agent is trying to do and points it to the correct skill within that library. It does not try to answer questions itself — it dispatches. + +Each router is installed automatically when that library's skills are installed via `--package`. It is always the first skill the agent should load when working with that library. + +**Example — `tanstack-query/router`:** + +```markdown +--- +name: tanstack-query/router +description: Entry point for all TanStack Query work. Load this first, then + follow the routing table to the correct Query skill. React adapter only. +triggers: + - react-query + - useQuery + - useMutation + - QueryClient + - tanstack query +--- + +# TanStack Query — Router + +## Routing Table + +| If you are working on... | Load this skill | +| ----------------------------------------------------- | --------------------------- | +| Initial setup, useQuery, useMutation, QueryClient | `tanstack-query/core` | +| Cache config, staleTime, gcTime, invalidation | `tanstack-query/caching` | +| Paginated or infinite scroll data | `tanstack-query/infinite` | +| Suspense boundaries with data fetching | `tanstack-query/suspense` | + +## Notes +- All skills cover the **React adapter** only (`@tanstack/react-query`) +- For Vue, Solid, or Svelte, refer to tanstack.com/docs directly +``` + +**Example — `tanstack-router/router`:** + +```markdown +--- +name: tanstack-router/router +description: Entry point for all TanStack Router work. Load this first, then + follow the routing table to the correct Router skill. React adapter only. +triggers: + - react-router + - createFileRoute + - createRootRoute + - useNavigate + - tanstack router +--- + +# TanStack Router — Router + +## Routing Table + +| If you are working on... | Load this skill | +| ----------------------------------------------------- | ------------------------------- | +| Route definitions, navigation, type-safe params | `tanstack-router/core` | +| Type-safe URL search params, filters in the URL | `tanstack-router/search-params` | +| Prefetching and loading data before a route renders | `tanstack-router/loaders` | +| Using TanStack Query inside Router loaders | `tanstack-router/with-query` | + +## Notes +- All skills cover the **React adapter** only (`@tanstack/react-router`) +``` + +The same pattern applies to `tanstack-db/router`, `tanstack-form/router`, and `tanstack-table/router` — each scoped entirely to their own library's skills. + +--- + +### Skill Design Principles + +Skills in this repo are written for agents, not humans. Every skill must follow these principles: + +#### 1. Agent-first writing + +The agent already knows TypeScript, React, and general web development. Do not explain concepts it already has. Only write what it cannot know without the skill: the specific API shape, the gotchas, the correct pattern for this package. + +```markdown +# Bad — explains what the agent already knows +React Query is a library for managing server state in React applications. +It handles caching, background refetching, and synchronization... + +# Good — gives the agent what it needs +## Setup +\`\`\`typescript +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +const queryClient = new QueryClient() +// Wrap app root with QueryClientProvider +\`\`\` +``` + +#### 2. Minimal working code examples are required + +Every skill must include at least one complete, copy-pasteable code example. Examples must be minimal — no unnecessary boilerplate, no placeholder comments that aren't needed. If the pattern requires context to work, include exactly that context and nothing more. + +```typescript +// CORRECT: minimal, complete, copy-pasteable +const { data, isPending, isError } = useQuery({ + queryKey: ['todos', userId], + queryFn: () => fetchTodos(userId), +}) + +// WRONG: incomplete — agent can't use this without guessing +const { data } = useQuery({ + queryKey: [...], + queryFn: ..., +}) +``` + +#### 3. Anti-patterns are as important as correct patterns + +Every skill must include a "Common Mistakes" section. The most valuable content in a skill is often what NOT to do — these are the cases where agents generate plausible-looking but broken code. + +```markdown +## Common Mistakes + +❌ **Don't** use stale query keys that omit dependencies: +\`\`\`typescript +// WRONG — todos won't refetch when userId changes +queryKey: ['todos'] +\`\`\` + +✅ **Do** include all dependencies in the key: +\`\`\`typescript +// CORRECT +queryKey: ['todos', userId] +\`\`\` +``` + +#### 4. Skill frontmatter must declare source refs and skill refs + +Every skill declares two metadata fields: + +- `metadata.sources` — source files in the package repos this skill is derived from; used by the Oz staleness check +- `metadata.skills` — other skills in this playbook this skill references; used to check cross-skill staleness + +```yaml +--- +name: tanstack-router/with-query +description: ... +metadata: + sources: + - 'tanstack/router:packages/react-router/src/route.ts' + - 'tanstack/router:docs/framework/react/guide/data-loading.md' + skills: + - tanstack-query/core + - tanstack-router/loaders +--- +``` + +If a referenced skill (`tanstack-query/core`, `tanstack-router/loaders`) is updated, `tanstack-router/with-query` is also flagged for a staleness check. + +#### 5. Keep SKILL.md under 500 lines + +Move detailed API option tables, exhaustive type signatures, and edge case reference material to `references/`. The SKILL.md body should be the fast path — setup, the primary pattern, common mistakes, and pointers to references. + +#### 6. Anti-patterns for skill authors + +| Anti-Pattern | Problem | Fix | +|---|---|---| +| **The Encyclopedia** | Reads like a wiki, wastes context | Split into focused skills + references | +| **The Everything Bagel** | Triggers on everything | Tighten description and triggers | +| **Missing examples** | Agent has to guess the shape | Every skill needs working code | +| **Human-facing prose** | Wastes tokens on explanation | Replace with examples and steps | +| **No anti-patterns section** | Agent generates plausible-but-wrong code | Always include Common Mistakes | + +--- + +### Skill Frontmatter Schema + +```yaml +--- +name: tanstack-query/core # library-group/skill-name +description: > # What it does + when to load it. Written + TanStack Query setup, useQuery, # for the agent, not a human reader. + useMutation, QueryClient. Load when + fetching server data or managing + loading/error/success states. +triggers: # Keywords that should activate this skill + - useQuery + - useMutation + - QueryClient + - react-query +metadata: + sources: # Source files this skill is derived from + - 'tanstack/query:packages/query-core/src/query.ts' + - 'tanstack/query:docs/framework/react/guides/queries.md' + skills: # Other playbook skills this skill references + - tanstack-query/caching # If these are updated, re-check this skill +--- +``` + +`metadata.sources` paths use the format `repo-name:relative-path`. Glob patterns are supported. + +--- + +### Cross-Skill Staleness + +When a skill is updated by the Oz automation, the system also checks whether any other skills list the updated skill in their `metadata.skills`. If they do, those skills are queued for a staleness evaluation as well. + +**Example:** + +1. `tanstack/query` merges a change to `docs/framework/react/guides/queries.md` +2. Oz identifies `tanstack-query/core` as affected via `metadata.sources` +3. Oz updates `tanstack-query/core` and opens a PR +4. Oz then scans all other skills for `skills: [tanstack-query/core, ...]` +5. Finds `tanstack-router/with-query` references `tanstack-query/core` +6. Evaluates those skills for staleness against the `tanstack-query/core` update +7. Opens additional PRs if they need updating; exits silently if not + +This cascade is bounded to one level — skills that reference `router-query` are not automatically re-checked. + +--- + +### Skill Generation Prompt + +Skills are not written by hand initially. The following prompt is used to bootstrap each skill from source documentation for the first time. After that, the Oz automation owns keeping them current. + +The prompt lives at `prompts/generate-skill.md` in the playbook repo. + +--- + +```markdown +# Skill Generation Prompt + +You are generating a SKILL.md file for the `@tanstack/playbook` agent skills repo. +Skills in this repo are written for coding agents (Claude Code, Cursor, Warp Oz), +not for human readers. Your output will be loaded into an agent's context window +and used to guide code generation. + +## Your task + +Generate a complete SKILL.md for the skill named: **{SKILL_NAME}** +(Use the format `library-group/skill-name`, e.g. `tanstack-query/core`, `tanstack-router/loaders`) + +The skill covers: **{SKILL_DESCRIPTION}** + +Source documentation to distill from: +{SOURCE_DOCS} + +## Output requirements + +Your output must be a valid SKILL.md file with: + +### 1. Frontmatter + +\`\`\`yaml +--- +name: {SKILL_NAME} +description: > + One to three sentences. What this skill covers and exactly when an agent + should load it. Written for the agent — include the keywords an agent would + encounter when it needs this skill. +triggers: + - list + - of + - keywords +metadata: + sources: + - 'repo:path/to/source/file' + skills: + - other-skill-name # only if this skill explicitly references another +--- +\`\`\` + +### 2. A minimal setup example + +A complete, copy-pasteable code block showing the minimum viable usage. +No placeholder comments. No unnecessary boilerplate. React adapter only. +Import from the correct `@tanstack/react-*` package. + +### 3. The primary pattern(s) + +The one to three most important things an agent needs to know to use this +correctly. Each pattern must include a working code example. +Do not explain what the agent already knows (TypeScript, React hooks, etc). +Only write what is specific to this TanStack package and skill. + +### 4. Common Mistakes + +A "Common Mistakes" section with at least three entries. Each entry must show: +- ❌ The wrong pattern with a code example +- ✅ The correct pattern with a code example +- A one-line explanation of why the wrong pattern fails + +Focus on mistakes that produce plausible-looking but broken or subtly incorrect +code — these are the highest-value entries. + +### 5. Resources (if needed) + +If there are detailed API options, exhaustive type signatures, or edge cases +that are important but too long for the main skill, list them as references: + +\`\`\`markdown +## Resources +- [Full option reference](references/api.md) +- [Advanced patterns](references/advanced.md) +\`\`\` + +Do NOT include this section if the skill content is already complete without it. + +## Constraints + +- Total SKILL.md must be under 500 lines +- React adapter only — no Vue, Solid, Svelte, or Angular examples +- All imports must use the real package name (e.g. `@tanstack/react-query`) +- No marketing copy, no motivational prose, no "why TanStack is great" +- No explanations of TypeScript or React concepts the agent already knows +- Every code example must be complete enough to copy-paste without modification +- The description and triggers must be written so the agent loads this skill + at the right time — not too broad (triggers on everything) and not too narrow + (never triggers) +``` + +--- + +### CI Automation + +The CI system uses GitHub Actions as a lightweight trigger layer and Warp Oz as the agent execution layer. Package repos do nothing beyond firing a webhook on merge to main. Oz handles all the intelligence: evaluating staleness, rewriting skill content where needed, checking cross-skill references, and opening (or not opening) PRs. + +#### Flow Overview + +``` +Package repo (e.g. tanstack/query) + └── merge to main + └── GitHub Action fires webhook → Warp Oz + +Warp Oz + └── Receives webhook (package name, commit SHA, changed files) + └── Oz agent (driven by skill-staleness-check skill): + 1. Match changed files against metadata.sources across all skills + 2. For each matched skill: + a. Fetch current SKILL.md + file diff + b. Evaluate: does the diff affect documented behavior? + c. If YES → rewrite skill, open PR + d. If NO → skip silently + 3. For each skill that was updated in step 2: + a. Find all skills that list it in metadata.skills + b. Evaluate those skills for cross-skill staleness + c. If stale → rewrite, open PR + d. If not → skip silently +``` + +Oz acts autonomously end-to-end. PRs contain already-updated skill content, not suggestions. If nothing needs updating, no PR is opened and no notification is sent. + +#### Package Repo Side (minimal) + +Each package repo contains one GitHub Action that fires on merge to main: + +```yaml +# .github/workflows/notify-playbook.yml +name: Notify Playbook + +on: + push: + branches: [main] + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Get changed files + id: diff + run: | + echo "files=$(git diff --name-only HEAD~1 HEAD | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT + + - name: Trigger Oz skill check + run: | + curl -X POST "${{ secrets.OZ_WEBHOOK_URL }}" \ + -H "Authorization: Bearer ${{ secrets.OZ_WEBHOOK_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d '{ + "package": "@tanstack/query", + "sha": "${{ github.sha }}", + "changed_files": ${{ steps.diff.outputs.files }} + }' +``` + +Two secrets per package repo: `OZ_WEBHOOK_URL` and `OZ_WEBHOOK_TOKEN`. No cross-repo GitHub tokens needed — Oz handles repo access independently. + +#### Oz Agent Side + +The Oz automation is configured in the playbook repo and driven by the internal `skill-staleness-check` skill. + +```yaml +# .warp/automations/skill-check.yml +name: Skill Staleness Check +trigger: webhook +skill: skills/internal/skill-staleness-check/SKILL.md +environments: + - repo: tanstack/playbook + - repo: tanstack/query + - repo: tanstack/router + - repo: tanstack/db + - repo: tanstack/form + - repo: tanstack/table +``` + +**`skill-staleness-check` SKILL.md:** + +```markdown +--- +name: skill-staleness-check +description: Internal Oz automation. Evaluates playbook skills for staleness + when source files change, rewrites stale skills, checks cross-skill + references, and opens PRs. Silent when nothing needs updating. +--- + +## Inputs +Webhook payload: package name, commit SHA, list of changed files. + +## Steps + +1. Read all SKILL.md files in skills/ — extract metadata.sources and + metadata.skills per skill +2. Match changed_files against metadata.sources (glob-aware, repo-prefixed paths) +3. For each matched skill: + a. Fetch current SKILL.md content + b. Fetch the file diff from the triggering commit in the source repo + c. Evaluate: does the diff change any behavior, API, pattern, or example + that this skill documents? + d. If YES: rewrite the skill to accurately reflect the change. Preserve + all sections (setup, patterns, common mistakes, resources). Keep under + 500 lines. React adapter only. Then go to step 4. + e. If NO: skip. Do not open a PR. +4. For each skill updated in step 3: + a. Scan all other skills for metadata.skills entries that include the + updated skill name + b. For each skill that references it: evaluate whether the update to the + referenced skill makes this skill stale or inconsistent + c. If YES: rewrite and open a separate PR + d. If NO: skip +5. For each skill that was rewritten: + a. Create branch: skill-update/- + b. Commit updated SKILL.md + c. Open PR with structured body (see PR format below) + +## PR format + +Title: skill: update (@) + +Body: + ### Triggered by + Changes to: + + ### What changed in the source + + + ### What changed in the skill + + + ### Cross-skill impact + + + ### Review checklist + - [ ] Skill content is accurate + - [ ] Code examples are complete and copy-pasteable + - [ ] No other skills need corresponding updates + - [ ] Under 500 lines + +## No-op behavior +If no changed files match any skill's metadata.sources, or if the diff does +not affect documented behavior, exit silently. No PR, no notification. +``` + +--- + +### Distribution Strategy: Thin Skills + CLI + +#### Installation + +```bash +# Install all TanStack skills +npx @tanstack/playbook install + +# Install skills for specific packages only +npx @tanstack/playbook install --package query +npx @tanstack/playbook install --package query --package router + +# Install globally (user-level agent directories) +npx @tanstack/playbook install --global +``` + +Valid `--package` values: `query`, `router`, `db`, `form`, `table` + +#### Install Target: `.agents/skills/` First + +The primary install target is `.agents/skills/` — the emerging standard agent skills directory. When installing, the CLI prompts if other agent directories are also detected: + +``` +$ npx @tanstack/playbook install --package query + +✔ Detected .agents/ directory + Installing to .agents/skills/ + + Also detected: + → .claude/ (Claude Code) + → .cursor/ (Cursor) + + Install to these as well? (Y/n) +``` + +**Supported targets:** + +| Directory | Agent | +|---|---| +| `.agents/skills/` | Primary — standard Skills spec | +| `.claude/skills/` | Claude Code | +| `.cursor/skills/` | Cursor | +| `.codex/skills/` | OpenAI Codex | +| `.windsurf/skills/` | Windsurf | +| `.github/skills/` | GitHub Copilot | + +#### AGENTS.md Snippet + +After installing, the CLI emits a snippet the developer should add to their `AGENTS.md` file. This tells agents where the playbook skills are and how to use them. + +``` +$ npx @tanstack/playbook install --package query --package router + +✔ Installed 8 skills to .agents/skills/ + +Add the following to your AGENTS.md: + +───────────────────────────────────────────────── +## Included Playbooks + +### TanStack Playbook +Skills for TanStack Query and Router are installed in `.agents/skills/`. + +Load each library's router skill first when working with that library: + +Package skills installed (load the router skill for each library first): + → tanstack-query/core (useQuery, useMutation, QueryClient) + → tanstack-query/caching (staleTime, gcTime, invalidation) + → tanstack-query/infinite (useInfiniteQuery) + → tanstack-query/suspense (useSuspenseQuery) + → tanstack-router/core (routes, navigation, params) + → tanstack-router/loaders (beforeLoad, loader, prefetching) + → tanstack-router/search-params (type-safe search params) + → tanstack-router/with-query (Router + Query composition) + +All skills cover the React adapter only. +───────────────────────────────────────────────── +``` + +The snippet is tailored to which packages were installed. Installing all packages produces a full listing; installing `--package query` only lists Query skills. + +#### Thin Skill Pattern + +The installed thin skill is a minimal shell that points back to the npm package for the full content: + +```markdown +--- +name: tanstack-query/router +description: Entry point for TanStack Query. Routes to the correct Query skill. +--- + +# TanStack Query — Router + +Full skill content is in the @tanstack/playbook npm package. + +\`\`\`bash +npx @tanstack/playbook show tanstack-query/router +\`\`\` + +Query skills: tanstack-query/router, tanstack-query/core, tanstack-query/caching, + tanstack-query/infinite, tanstack-query/suspense + +\`\`\`bash +npx @tanstack/playbook list --package query +\`\`\` +``` + +#### CLI Commands + +| Command | Description | +|---|---| +| `npx @tanstack/playbook install` | Install all skills; prompts for detected agent dirs | +| `npx @tanstack/playbook install --package ` | Install skills for one package | +| `npx @tanstack/playbook install --package

    --package

    ` | Install multiple packages | +| `npx @tanstack/playbook install --global` | Install to user-level directories | +| `npx @tanstack/playbook list` | List all available skills | +| `npx @tanstack/playbook list --package ` | List skills for one package | +| `npx @tanstack/playbook show ` | Print full skill content | + +--- + +### Release Sequence + +1. Bootstrap all skills using the generation prompt against React docs +2. Review and merge generated skills into `@tanstack/playbook` +3. Publish `@tanstack/playbook` to npm +4. Add `notify-playbook.yml` GitHub Action to each package repo +5. Configure Oz automation in playbook repo +6. Validate at onsite + +**If pressed for time, 90/10 cut:** One `router` skill + one `core` skill per library covers the primary use case. Caching, infinite, suspense, and composition skills follow once core is validated. + +--- + +## Open Questions + +| Question | Options | Resolution Path | +|---|---|---| +| **Oz multi-repo access** | Oz needs read access to 5 package repos — confirm permissions model | Check Oz docs for multi-repo environment config | +| **Electric integration ownership** | `db-electric` skill in playbook vs Electric's own playbook | Coordinate with Electric team; avoid duplicate guidance | +| **File-based vs code-based routing** | Separate skills or in `tanstack-router/core`? | Assess volume; split if content exceeds 300 lines | +| **Cross-skill cascade depth** | One level of cross-skill staleness checking vs recursive | Start at one level; expand if gaps are found in practice | + +--- + +## Definition of Success + +### Primary Hypothesis + +> We believe that publishing TanStack agent skills will help developers build TanStack apps faster and avoid common mistakes. +> +> We'll know we're right if: +> +> - `@tanstack/playbook` gets meaningful npm downloads (>100 in first month) +> - Qualitative feedback indicates skills guided correct package/adapter selection or unlocked a composition +> - AGENTS.md snippet is being added to projects (visible in public repos) +> +> We'll know we're wrong if: +> +> - Downloads are negligible +> - Feedback indicates skills are too generic or don't cover actual error cases +> - Developers ignore the router skill and rely on general agent knowledge + +### Functional Requirements + +| Requirement | Acceptance Criteria | +|---|---| +| Playbook published | `@tanstack/playbook` installable via npm | +| Router skills work | Each library's router skill routes agent to the correct skill within that library | +| All package skills present | query, router, db, form, table skills all present and generated from React docs | +| Thin skills install correctly | `npx @tanstack/playbook install` installs to `.agents/skills/` and prompts for others | +| AGENTS.md snippet emitted | Install command outputs correct snippet for installed packages | +| CLI commands work | `list`, `show`, `install --package` all work correctly | +| CI trigger works | Merge to package repo fires webhook to Oz | +| Oz staleness check works | Oz evaluates skill, rewrites if stale, opens PR; silent if not | +| Cross-skill check works | Updating a skill triggers evaluation of skills that reference it | + +### Learning Goals + +1. Do agents follow the router skill's routing table, or do they load skills directly? +2. Which package skills see the most usage? +3. Which compositions are most under-served by current skill content? +4. Does the AGENTS.md snippet get added — and does it help agents find skills? diff --git a/scripts/playbook.mjs b/scripts/playbook.mjs new file mode 100644 index 0000000..a8ffe0f --- /dev/null +++ b/scripts/playbook.mjs @@ -0,0 +1,634 @@ +#!/usr/bin/env node + +/** + * TanStack Playbooks — Test CLI + * + * Standalone script for testing playbook commands locally. + * Usage: + * node scripts/playbook.mjs list [--all] [--json] + * node scripts/playbook.mjs init + * node scripts/playbook.mjs feedback --submit [--file ] + */ + +import fs from 'fs' +import fsp from 'fs/promises' +import path from 'path' +import readline from 'readline' +import { execSync, execFileSync } from 'child_process' +import { parse as parseYaml } from 'yaml' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Parse YAML frontmatter from a SKILL.md file. + * Returns the parsed object or null if no frontmatter block found. + */ +async function parseFrontmatter(filePath) { + let content + try { + content = await fsp.readFile(filePath, 'utf8') + } catch { + return null + } + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) + if (!match) return null + try { + const parsed = parseYaml(match[1]) + // Flatten multi-line description scalars + if (typeof parsed.description === 'string') { + parsed.description = parsed.description.replace(/\s+/g, ' ').trim() + } + return parsed + } catch { + return null + } +} + +/** + * Resolve the playbooks root and detect which mode we're in. + * Returns { basePath, mode } where basePath points to the directory + * containing package_map.yaml and skills/. + */ +function resolvePlaybooksRoot() { + // Check for installed mode first (node_modules) + const installedPath = path.resolve('node_modules', '@tanstack', 'playbooks') + if ( + fs.existsSync(path.join(installedPath, 'package_map.yaml')) + ) { + return { basePath: installedPath, mode: 'installed' } + } + + // Repo mode — walk up from cwd looking for package_map.yaml inside packages/playbooks + let dir = process.cwd() + while (true) { + const candidate = path.join(dir, 'packages', 'playbooks') + if (fs.existsSync(path.join(candidate, 'package_map.yaml'))) { + return { basePath: candidate, mode: 'repo' } + } + const parent = path.dirname(dir) + if (parent === dir) break + dir = parent + } + + return null +} + +/** + * Walk up from cwd looking for package.json and return its parsed content. + */ +function findPackageJson() { + let dir = process.cwd() + while (true) { + const candidate = path.join(dir, 'package.json') + if (fs.existsSync(candidate)) { + try { + return JSON.parse(fs.readFileSync(candidate, 'utf8')) + } catch { + return null + } + } + const parent = path.dirname(dir) + if (parent === dir) break + dir = parent + } + return null +} + +/** + * Read and parse package_map.yaml from the resolved basePath. + */ +async function readPackageMap(basePath) { + const mapPath = path.join(basePath, 'package_map.yaml') + const content = await fsp.readFile(mapPath, 'utf8') + return parseYaml(content) +} + +/** + * Discover sub-skills (child dirs containing SKILL.md) for a skill directory. + */ +async function findSubSkills(skillsBase, skillDir) { + const fullDir = path.join(skillsBase, skillDir) + let entries + try { + entries = await fsp.readdir(fullDir, { withFileTypes: true }) + } catch { + return [] + } + const subs = [] + for (const entry of entries) { + if (!entry.isDirectory()) continue + const subSkillPath = path.join(fullDir, entry.name, 'SKILL.md') + if (fs.existsSync(subSkillPath)) { + subs.push({ + dirName: entry.name, + skillDir: path.join(skillDir, entry.name), + filePath: subSkillPath, + }) + } + } + return subs +} + +/** + * Prompt the user with readline and return their answer. + */ +function prompt(question) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close() + resolve(answer.trim()) + }) + }) +} + +// --------------------------------------------------------------------------- +// Command: list +// --------------------------------------------------------------------------- + +/** Discover all skill directories by scanning the skills/ tree. */ +async function discoverAllSkillDirs(skillsBase) { + const dirs = new Set() + const libraries = await fsp.readdir(skillsBase, { withFileTypes: true }).catch(() => []) + for (const lib of libraries) { + if (!lib.isDirectory()) continue + const libPath = path.join(skillsBase, lib.name) + if (fs.existsSync(path.join(libPath, 'SKILL.md'))) { + dirs.add(lib.name) + } + const children = await fsp.readdir(libPath, { withFileTypes: true }).catch(() => []) + for (const child of children) { + if (!child.isDirectory()) continue + if (fs.existsSync(path.join(libPath, child.name, 'SKILL.md'))) { + dirs.add(path.join(lib.name, child.name)) + } + } + } + return dirs +} + +/** Add dirs from package_map entries that match the project's installed deps. */ +function addDirsFromDeps(dirs, packageMap, allDeps) { + const map = packageMap?.package_map ?? {} + for (const [pkgName, pkgDirs] of Object.entries(map)) { + if (!allDeps[pkgName]) continue + const dirList = Array.isArray(pkgDirs) ? pkgDirs : [pkgDirs] + for (const d of dirList) dirs.add(d) + } +} + +/** Add composition skill dirs when all their requires are already matched. */ +function addCompositionDirs(dirs, packageMap) { + for (const comp of packageMap?.compositions ?? []) { + if (!comp?.requires) continue + if (comp.requires.every((r) => dirs.has(r)) && comp.skills) { + for (const s of comp.skills) dirs.add(s) + } + } +} + +/** Match skill directories from package.json deps against the package_map. */ +function matchSkillDirsFromDeps(packageMap) { + const pkg = findPackageJson() + if (!pkg) { + console.error( + 'Error: Could not find package.json. Run this from a project directory, or use --all to show all skills.', + ) + process.exit(1) + } + + const dirs = new Set() + const allDeps = { ...pkg.dependencies, ...pkg.devDependencies } + addDirsFromDeps(dirs, packageMap, allDeps) + addCompositionDirs(dirs, packageMap) + for (const d of packageMap?.always_include ?? []) dirs.add(d) + return dirs +} + +/** Build structured skill data from a set of skill directories. */ +async function buildSkillData(matchedDirs, skillsBase, pathPrefix) { + const skills = [] + for (const dir of matchedDirs) { + const skillFile = path.join(skillsBase, dir, 'SKILL.md') + if (!fs.existsSync(skillFile)) { + console.warn(`Warning: Skill directory '${dir}' not found, skipping`) + continue + } + const fm = await parseFrontmatter(skillFile) + if (!fm) { + console.warn(`Warning: Could not parse frontmatter in ${dir}/SKILL.md, skipping`) + continue + } + + const subSkillEntries = await findSubSkills(skillsBase, dir) + const subSkills = await Promise.all( + subSkillEntries.map(async (sub) => { + const subFm = await parseFrontmatter(sub.filePath) + return { + name: subFm?.name ?? sub.skillDir, + description: subFm?.description ?? '', + path: path.join(pathPrefix, sub.skillDir, 'SKILL.md'), + dirName: sub.dirName, + } + }), + ) + + skills.push({ + name: fm.name ?? dir, + description: fm.description ?? '', + type: fm.type ?? '', + library: fm.library ?? dir.split(path.sep)[0], + library_version: fm.library_version ?? '', + path: path.join(pathPrefix, dir, 'SKILL.md'), + requires: fm.requires ?? [], + framework: fm.framework ?? undefined, + sub_skills: subSkills, + _dir: dir, + }) + } + return skills +} + +/** Get the display group name for a skill's library. */ +function groupNameFor(library) { + return library === 'tanstack' ? 'ECOSYSTEM' : library.toUpperCase() +} + +/** Sort group names: library groups alphabetically, COMPOSITIONS and ECOSYSTEM last. */ +function sortGroupNames(names) { + const trailing = new Set(['ECOSYSTEM', 'COMPOSITIONS']) + return names.sort((a, b) => { + if (trailing.has(a) && !trailing.has(b)) return 1 + if (!trailing.has(a) && trailing.has(b)) return -1 + return a.localeCompare(b) + }) +} + +/** Print a single skill entry with its sub-skills. */ +function printSkillEntry(skill) { + const desc = skill.description.length > 60 + ? skill.description.slice(0, 60) + '…' + : skill.description + console.log(` ${skill.name.padEnd(28)} ${desc}`) + console.log(` → ${skill.path}`) + if (skill.requires.length > 0) { + console.log(` (builds on: ${skill.requires.join(', ')})`) + } + if (skill.sub_skills.length > 0) { + console.log(' sub-skills:') + for (const sub of skill.sub_skills) { + console.log(` ${sub.dirName.padEnd(24)} → ${sub.path}`) + } + } +} + +/** Group skills by library and print formatted output. */ +function printSkillGroups(skills) { + const groups = {} + for (const skill of skills) { + const name = groupNameFor(skill.library) + if (!groups[name]) groups[name] = [] + groups[name].push(skill) + } + + for (const groupName of sortGroupNames(Object.keys(groups))) { + console.log(groupName) + for (const skill of groups[groupName]) printSkillEntry(skill) + console.log() + } +} + +async function cmdList(args) { + const showAll = args.includes('--all') + const asJson = args.includes('--json') + + const root = resolvePlaybooksRoot() + if (!root) { + console.error( + 'Error: Could not find package_map.yaml. Run this from the playbooks repo or from a project with @tanstack/playbooks installed.', + ) + process.exit(1) + } + + const { basePath, mode } = root + const packageMap = await readPackageMap(basePath) + const version = packageMap?.playbook?.version ?? 'unknown' + const skillsBase = path.join(basePath, 'skills') + const pathPrefix = mode === 'installed' + ? path.join('node_modules', '@tanstack', 'playbooks', 'skills') + : 'skills' + + const matchedDirs = showAll + ? await discoverAllSkillDirs(skillsBase) + : matchSkillDirsFromDeps(packageMap) + + const skills = await buildSkillData(matchedDirs, skillsBase, pathPrefix) + + if (asJson) { + const output = skills.map(({ _dir, ...rest }) => rest) + console.log(JSON.stringify(output, null, 2)) + return + } + + console.log(`TanStack Playbooks v${version}`) + if (showAll) { + console.log('Showing all skills (not filtered by package.json)') + } + console.log() + printSkillGroups(skills) +} + +// --------------------------------------------------------------------------- +// Command: init +// --------------------------------------------------------------------------- + +const PROMPT_BLOCK = `## TanStack Playbooks + +This project uses TanStack Playbooks for AI coding agent skills. +At the start of each session, run: + + tanstack playbook list + +This returns available skills with file paths based on installed +TanStack packages. Read skill files directly from the paths shown. +Use skill descriptions to decide which to load for the current task. + +After using playbook skills during a session, generate a feedback +report. See the ecosystem router skill (tanstack/SKILL.md) for the +feedback format.` + +async function cmdInit() { + const configFiles = [ + 'AGENTS.md', + 'CLAUDE.md', + '.cursorrules', + '.github/copilot-instructions.md', + '.windsurfrules', + ] + + const found = configFiles.filter((f) => fs.existsSync(path.resolve(f))) + + if (found.length > 0) { + console.log(`Found config files: ${found.join(', ')}`) + console.log() + console.log('How would you like to add the TanStack Playbooks prompt?') + console.log() + console.log(` 1. Add to detected config files (${found.join(', ')})`) + console.log(' 2. Copy to clipboard') + console.log(" 3. I'll add it manually") + console.log() + const choice = await prompt('Choose (1/2/3): ') + + if (choice === '1') { + for (const f of found) { + const filePath = path.resolve(f) + await fsp.appendFile(filePath, '\n\n' + PROMPT_BLOCK + '\n') + console.log(`✓ Added to ${f}`) + } + } else if (choice === '2') { + copyToClipboard(PROMPT_BLOCK) + } else { + console.log() + console.log(PROMPT_BLOCK) + } + } else { + console.log('No agent config files detected in this directory.') + console.log() + console.log('How would you like to add the TanStack Playbooks prompt?') + console.log() + console.log(' 1. Create AGENTS.md with the prompt block') + console.log(' 2. Copy to clipboard') + console.log(" 3. I'll add it manually") + console.log() + const choice = await prompt('Choose (1/2/3): ') + + if (choice === '1') { + await fsp.writeFile(path.resolve('AGENTS.md'), PROMPT_BLOCK + '\n') + console.log('✓ Created AGENTS.md') + } else if (choice === '2') { + copyToClipboard(PROMPT_BLOCK) + } else { + console.log() + console.log(PROMPT_BLOCK) + } + } +} + +function copyToClipboard(text) { + try { + const platform = process.platform + if (platform === 'darwin') { + execSync('pbcopy', { input: text }) + } else if (platform === 'win32') { + execSync('clip', { input: text }) + } else { + // Linux / WSL — try clip.exe (WSL), then xclip, then xsel + let copied = false + for (const cmd of ['clip.exe', 'xclip -selection clipboard', 'xsel --clipboard --input']) { + try { + execSync(cmd, { input: text, stdio: ['pipe', 'pipe', 'pipe'] }) + copied = true + break + } catch { /* try next */ } + } + if (!copied) throw new Error('No clipboard command available') + } + console.log('✓ Copied to clipboard') + } catch { + console.log() + console.log(PROMPT_BLOCK) + console.log() + console.log('Copy the above text manually.') + } +} + +// --------------------------------------------------------------------------- +// Command: feedback +// --------------------------------------------------------------------------- + +/** Verify that the gh CLI is installed and authenticated. */ +function requireGhCli() { + try { + execSync('gh --version', { stdio: 'pipe' }) + } catch { + console.error( + 'Error: The GitHub CLI (gh) is required for feedback submission.\nInstall it from: https://cli.github.com', + ) + process.exit(1) + } + try { + execSync('gh auth status', { stdio: 'pipe' }) + } catch { + console.error('Error: Not authenticated with GitHub. Run: gh auth login') + process.exit(1) + } +} + +/** Read feedback content from --file flag or stdin. */ +async function readFeedbackContent(args) { + const fileIdx = args.indexOf('--file') + if (fileIdx !== -1 && args[fileIdx + 1]) { + const filePath = path.resolve(args[fileIdx + 1]) + try { + return await fsp.readFile(filePath, 'utf8') + } catch (err) { + console.error(`Error: Could not read file '${args[fileIdx + 1]}': ${err.message}`) + process.exit(1) + } + } + const eofHint = process.platform === 'win32' ? 'Ctrl+Z' : 'Ctrl+D' + console.log(`Paste your feedback report below, then press ${eofHint} to submit:`) + return new Promise((resolve) => { + const chunks = [] + process.stdin.on('data', (chunk) => chunks.push(chunk)) + process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + }) +} + +/** Extract skill names from feedback content (### headings and skill_name: lines). */ +function extractSkillNames(content) { + const names = [] + for (const line of content.split('\n')) { + const heading = line.match(/^###\s+(.+)/) + if (heading) { names.push(heading[1].trim()); continue } + const field = line.match(/skill_name:\s*(.+)/) + if (field) names.push(field[1].trim()) + } + return names +} + +/** Sanitize sensitive content: secrets, JWTs, user paths. Returns { text, redacted }. */ +function sanitizeContent(content) { + let redacted = false + let text = content + + text = text.replace( + /(API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\s*[:=]\s*.*/gi, + (match) => { redacted = true; return match.replace(/[:=]\s*.*/, ': [REDACTED]') }, + ) + text = text.replace( + /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, + () => { redacted = true; return '[REDACTED_TOKEN]' }, + ) + text = text.replace( + /(?:\/Users\/|\/home\/|C:\\Users\\)([^\s/\\]+)/gi, + (match, username) => { redacted = true; return match.replace(username, '[USER]') }, + ) + return { text, redacted } +} + +async function cmdFeedback(args) { + if (!args.includes('--submit')) { + console.error('Usage: node scripts/playbook.mjs feedback --submit [--file ]') + process.exit(1) + } + + requireGhCli() + + const raw = await readFeedbackContent(args) + const content = raw.trim() + if (!content) { + console.error('Error: No feedback content provided.') + process.exit(1) + } + + if (!content.includes('skill_name:') && !content.match(/^###\s/m)) { + console.warn( + 'Warning: No skill_name entries found in feedback. The feedback will still be submitted but may not be automatically triaged.', + ) + } + + const skillNames = extractSkillNames(content) + const libraries = [...new Set( + skillNames.map((s) => s.split('/')[0]).filter((lib) => lib && lib !== 'tanstack'), + )] + + let version = 'unknown' + const root = resolvePlaybooksRoot() + if (root) { + try { + const pm = await readPackageMap(root.basePath) + version = pm?.playbook?.version ?? 'unknown' + } catch { /* ignore */ } + } + + const { text: sanitized, redacted } = sanitizeContent(content) + if (redacted) { + console.log('Note: Some potentially sensitive values were redacted before submission.') + } + + const dryRun = args.includes('--dry-run') + const dateStr = new Date().toISOString().split('T')[0] + const skillLabel = skillNames.length > 0 ? skillNames.join(', ') : 'general' + const title = `Feedback: ${skillLabel} — ${dateStr}` + const body = `Playbook version: ${version}\n\n${sanitized}` + const labels = ['feedback', 'auto-submitted', ...libraries] + + if (dryRun) { + console.log('--- Dry Run ---') + console.log(`Title: ${title}`) + console.log(`Labels: ${labels.join(', ')}`) + console.log(`Body:\n${body}`) + return + } + + const ghArgs = ['issue', 'create', '--repo', 'tanstack/playbooks', '--title', title, '--body', body] + for (const l of labels) ghArgs.push('--label', l) + + try { + const result = execFileSync('gh', ghArgs, { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }) + console.log(`✓ Feedback submitted: ${result.trim()}`) + } catch (err) { + console.error(`Error submitting feedback: ${err.message}`) + process.exit(1) + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +const USAGE = `TanStack Playbooks — Test CLI + +Usage: + node scripts/playbook.mjs list [--all] [--json] + node scripts/playbook.mjs init + node scripts/playbook.mjs feedback --submit [--file ] [--dry-run] + +Commands: + list Show available skills for this project + init Add playbook prompt to agent config files + feedback Submit a feedback report as a GitHub Issue` + +const command = process.argv[2] +const args = process.argv.slice(3) + +if (!command) { + console.log(USAGE) + process.exit(0) +} + +switch (command) { + case 'list': + await cmdList(args) + break + case 'init': + await cmdInit() + break + case 'feedback': + await cmdFeedback(args) + break + default: + console.error(`Unknown command: ${command}`) + console.log() + console.log(USAGE) + process.exit(1) +} diff --git a/scripts/sync-skills.mjs b/scripts/sync-skills.mjs new file mode 100644 index 0000000..aaf1251 --- /dev/null +++ b/scripts/sync-skills.mjs @@ -0,0 +1,589 @@ +#!/usr/bin/env node + +/** + * TanStack Playbooks — Skill Sync Script + * + * Detects stale skills, bumps frontmatter versions, and marks skills as synced. + * Always scoped to a single library. + * + * Usage: + * node scripts/sync-skills.mjs # detect staleness + * node scripts/sync-skills.mjs --report # write staleness_report.yaml + * node scripts/sync-skills.mjs --mark-synced [skills..] # mark skills as synced + * node scripts/sync-skills.mjs --mark-synced --all # mark all skills as synced + * node scripts/sync-skills.mjs --bump-version [skills..] # bump frontmatter version + */ + +import fs from 'fs' +import fsp from 'fs/promises' +import path from 'path' +import { execSync, execFileSync } from 'child_process' +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml' + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const PLAYBOOKS_DIR = path.resolve('packages', 'playbooks') +const SKILLS_DIR = path.join(PLAYBOOKS_DIR, 'skills') +const TREE_GENERATOR_PATH = path.resolve('meta', 'tree-generator', 'SKILL.md') + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Run a gh api command and return parsed JSON. */ +function ghApi(endpoint) { + try { + const result = execFileSync('gh', ['api', endpoint], { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }) + return JSON.parse(result) + } catch (err) { + return null + } +} + +/** Run a gh api GraphQL query and return parsed JSON. */ +function ghGraphQL(query) { + try { + const result = execFileSync('gh', ['api', 'graphql', '-f', `query=${query}`], { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }) + return JSON.parse(result) + } catch { + return null + } +} + +/** Get the git SHA of a local file (last commit that touched it). */ +function getLocalGitSha(filePath) { + try { + return execSync(`git log -1 --format=%H -- "${filePath}"`, { + encoding: 'utf8', + cwd: path.resolve('.'), + stdio: ['pipe', 'pipe', 'pipe'], + }).trim() + } catch { + return null + } +} + +/** Get the latest commit SHA for a file in a GitHub repo. */ +function getRemoteFileSha(owner, repo, filePath) { + const data = ghApi(`repos/${owner}/${repo}/commits?path=${encodeURIComponent(filePath)}&per_page=1`) + if (data && data.length > 0) return data[0].sha + return null +} + +/** Parse YAML frontmatter from a SKILL.md file. */ +async function parseFrontmatter(filePath) { + let content + try { + content = await fsp.readFile(filePath, 'utf8') + } catch { + return null + } + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) + if (!match) return null + try { + const parsed = parseYaml(match[1]) + if (typeof parsed.description === 'string') { + parsed.description = parsed.description.replace(/\s+/g, ' ').trim() + } + return parsed + } catch { + return null + } +} + +/** Rewrite library_version in a SKILL.md file's frontmatter. */ +async function bumpFrontmatterVersion(filePath, newVersion) { + let content + try { + content = await fsp.readFile(filePath, 'utf8') + } catch { + return false + } + const updated = content.replace( + /(library_version:\s*['"]?)[^'"\n]+(["']?)/, + `$1${newVersion}$2`, + ) + if (updated === content) return false + await fsp.writeFile(filePath, updated) + return true +} + +/** Collect skills from immediate child directories that contain SKILL.md. */ +async function collectSkillsInDir(dirPath, namePrefix) { + const entries = await fsp.readdir(dirPath, { withFileTypes: true }).catch(() => []) + return entries + .filter((e) => e.isDirectory() && fs.existsSync(path.join(dirPath, e.name, 'SKILL.md'))) + .map((e) => ({ + name: path.join(namePrefix, e.name), + filePath: path.join(dirPath, e.name, 'SKILL.md'), + })) +} + +/** Discover all skill directories for a library (up to two levels deep). */ +async function discoverSkills(libDir) { + const libPath = path.join(SKILLS_DIR, libDir) + if (!fs.existsSync(path.join(libPath, 'SKILL.md'))) return [] + + const topSkills = await collectSkillsInDir(libPath, libDir) + const subSkillArrays = await Promise.all( + topSkills.map((s) => collectSkillsInDir(path.dirname(s.filePath), s.name)), + ) + return [...topSkills, ...subSkillArrays.flat()] +} + +/** Read sync-state.json for a library. Returns null if not found. */ +async function readSyncState(libDir) { + const statePath = path.join(SKILLS_DIR, libDir, 'sync-state.json') + try { + const content = await fsp.readFile(statePath, 'utf8') + return JSON.parse(content) + } catch { + return null + } +} + +/** Write sync-state.json for a library. */ +async function writeSyncState(libDir, state) { + const statePath = path.join(SKILLS_DIR, libDir, 'sync-state.json') + await fsp.writeFile(statePath, JSON.stringify(state, null, 2) + '\n') +} + +/** Parse owner/repo from a source reference like 'TanStack/db:docs/overview.md'. */ +function parseSourceRef(ref) { + const match = ref.match(/^([^:]+):(.+)$/) + if (!match) return null + const [ownerRepo, filePath] = [match[1], match[2]] + const parts = ownerRepo.split('/') + if (parts.length !== 2) return null + return { owner: parts[0], repo: parts[1], filePath } +} + +/** Classify semver drift between two versions. */ +function classifyVersionDrift(oldVer, newVer) { + if (!oldVer || !newVer || oldVer === newVer) return null + const oldParts = oldVer.replace(/[^0-9.]/g, '').split('.').map(Number) + const newParts = newVer.replace(/[^0-9.]/g, '').split('.').map(Number) + if (newParts[0] > oldParts[0]) return 'major' + if (newParts[1] > oldParts[1]) return 'minor' + if (newParts[2] > oldParts[2]) return 'patch' + return null +} + +/** Fetch the current version of a package from its source repo. */ +function fetchCurrentVersion(owner, repo, packagePath) { + // Try common package.json locations + const paths = packagePath + ? [packagePath] + : [`packages/${repo}/package.json`, 'package.json'] + + for (const p of paths) { + const data = ghApi(`repos/${owner}/${repo}/contents/${encodeURIComponent(p)}`) + if (data?.content) { + const decoded = Buffer.from(data.content, 'base64').toString('utf8') + try { + const pkg = JSON.parse(decoded) + if (pkg.version) return pkg.version + } catch { /* try next */ } + } + } + return null +} + +/** Fetch changelog/release notes between two versions. */ +function fetchChangelog(owner, repo, oldVersion, newVersion) { + // Try to get releases between versions + const releases = ghApi(`repos/${owner}/${repo}/releases?per_page=20`) + if (!releases || !Array.isArray(releases)) return [] + + return releases + .filter((r) => { + const tag = r.tag_name?.replace(/^v/, '') + if (!tag) return false + return tag > oldVersion && tag <= newVersion + }) + .map((r) => ({ version: r.tag_name, body: r.body?.slice(0, 500) ?? '' })) +} + +// --------------------------------------------------------------------------- +// Shared extracted helpers +// --------------------------------------------------------------------------- + +/** Find source repo owner/repo from skill frontmatter. */ +async function findSourceRepo(skills) { + for (const skill of skills) { + const fm = await parseFrontmatter(skill.filePath) + if (fm?.source_repository) { + const match = fm.source_repository.match(/github\.com\/([^/]+)\/([^/]+)/) + if (match) return { owner: match[1], repo: match[2] } + } + if (fm?.sources?.length > 0) { + const ref = parseSourceRef(fm.sources[0]) + if (ref) return { owner: ref.owner, repo: ref.repo } + } + } + return null +} + +/** Compute staleness reasons for a single skill. */ +async function getSkillReasons(skill, state, treeGeneratorChanged, currentVersion) { + const fm = await parseFrontmatter(skill.filePath) + const reasons = [] + + if (treeGeneratorChanged) reasons.push('tree-generator updated') + + const sources = fm?.sources ?? [] + const storedShas = state?.skills?.[skill.name]?.sources_sha ?? {} + + for (const source of sources) { + const ref = parseSourceRef(source) + if (!ref) continue + const currentSha = getRemoteFileSha(ref.owner, ref.repo, ref.filePath) + const storedSha = storedShas[ref.filePath] + + if (!storedSha) { + reasons.push(`new source (${ref.filePath})`) + } else if (currentSha && currentSha !== storedSha) { + reasons.push(`source changed (${ref.filePath})`) + } + } + + if (currentVersion && fm?.library_version && fm.library_version !== currentVersion) { + reasons.push(`version drift (${fm.library_version} → ${currentVersion})`) + } + + const needsRegen = reasons.some((r) => + r.startsWith('source changed') || r.startsWith('tree-generator') || r.startsWith('new source'), + ) + return { reasons, needsRegen, frontmatter: fm } +} + +/** Classify all skills into stale vs current. */ +async function classifySkills(skills, state, treeGeneratorChanged, currentVersion) { + const staleSkills = [] + const currentSkills = [] + + for (const skill of skills) { + const result = await getSkillReasons(skill, state, treeGeneratorChanged, currentVersion) + if (result.reasons.length > 0) { + staleSkills.push({ ...skill, ...result }) + } else { + currentSkills.push(skill) + } + } + return { staleSkills, currentSkills } +} + +/** Collect current remote source SHAs for a skill. */ +async function collectSourceShas(skill) { + const fm = await parseFrontmatter(skill.filePath) + const sources = fm?.sources ?? [] + const sourcesSha = {} + + for (const source of sources) { + const ref = parseSourceRef(source) + if (!ref) continue + const sha = getRemoteFileSha(ref.owner, ref.repo, ref.filePath) + if (sha) sourcesSha[ref.filePath] = sha + } + return sourcesSha +} + +// --------------------------------------------------------------------------- +// Output helpers +// --------------------------------------------------------------------------- + +function printVersionInfo(skillVersion, currentVersion, versionDrift, treeGeneratorChanged) { + if (currentVersion && skillVersion) { + const driftLabel = versionDrift ? ` (${versionDrift})` : '' + if (versionDrift) { + console.log(` Library version: ${skillVersion} → ${currentVersion}${driftLabel}`) + } else { + console.log(` Library version: ${skillVersion} (current)`) + } + } + console.log(` Tree-generator: ${treeGeneratorChanged ? 'CHANGED' : 'unchanged'}`) +} + +function printSourceSummary(staleSkills) { + const count = staleSkills.filter((s) => + s.reasons.some((r) => r.startsWith('source changed') || r.startsWith('new source')), + ).length + console.log(` Source changes: ${count} skills affected`) + console.log() +} + +function printStalenessDetails(staleSkills, currentSkills, totalCount) { + console.log(` Stale skills (${staleSkills.length}/${totalCount}):`) + for (const skill of staleSkills) { + const label = skill.needsRegen ? '⚠' : '↑' + console.log(` ${label} ${skill.name.padEnd(30)} ${skill.reasons.join(', ')}`) + } + console.log() + + if (currentSkills.length > 0) { + console.log(` Current skills (${currentSkills.length}/${totalCount}):`) + console.log(` ${currentSkills.map((s) => s.name).join(', ')}`) + console.log() + } +} + +function printRecommendations({ libDir, staleSkills, currentSkills, currentVersion, versionDrift }) { + const regenSkills = staleSkills.filter((s) => s.needsRegen) + const bumpOnlySkills = staleSkills.filter((s) => !s.needsRegen) + + if (regenSkills.length > 0) { + console.log(` Recommendation:`) + console.log(` ${regenSkills.length} skill(s) need regeneration (source/tree-generator changed)`) + if (bumpOnlySkills.length > 0 || versionDrift) { + console.log(` ${bumpOnlySkills.length + currentSkills.length} skill(s) need version bump only`) + } + if (currentVersion && versionDrift) { + console.log(` Run: node scripts/sync-skills.mjs ${libDir} --bump-version ${currentVersion}`) + } + } else if (versionDrift) { + console.log(` Recommendation: version bump only (no source changes)`) + console.log(` Run: node scripts/sync-skills.mjs ${libDir} --bump-version ${currentVersion}`) + } +} + +function printChangelog(sourceOwner, sourceRepo, skillVersion, currentVersion, versionDrift) { + if (!versionDrift || !skillVersion || !currentVersion) return + + const changelog = fetchChangelog(sourceOwner, sourceRepo, skillVersion, currentVersion) + if (changelog.length === 0) return + + console.log(`\n Changelog entries:`) + for (const entry of changelog) { + console.log(` ${entry.version}:`) + const lines = entry.body.split('\n').slice(0, 5) + for (const line of lines) console.log(` ${line}`) + if (entry.body.split('\n').length > 5) console.log(' ...') + } +} + +async function writeStalenessReportFile(opts) { + const report = { + library: opts.libDir, + source_repo: `${opts.sourceOwner}/${opts.sourceRepo}`, + library_version_in_skills: opts.skillVersion, + library_version_current: opts.currentVersion, + tree_generator_changed: opts.treeGeneratorChanged ?? false, + version_drift: opts.versionDrift, + stale_skills: opts.staleSkills.map((s) => ({ + skill: s.name, + reasons: s.reasons, + needs_regeneration: s.needsRegen, + })), + current_skills: opts.currentSkills.map((s) => ({ skill: s.name })), + } + const reportPath = path.join(SKILLS_DIR, opts.libDir, 'staleness_report.yaml') + await fsp.writeFile(reportPath, stringifyYaml(report)) + console.log(`\n Report written to ${reportPath}`) +} + +// --------------------------------------------------------------------------- +// Mode 1: Detect staleness +// --------------------------------------------------------------------------- + +async function detectStaleness(libDir, writeReport) { + const state = await readSyncState(libDir) + const skills = await discoverSkills(libDir) + + if (skills.length === 0) { + console.error(`Error: No skills found for library '${libDir}'`) + process.exit(1) + } + + const source = await findSourceRepo(skills) + if (!source) { + console.error(`Error: Could not determine source repository for '${libDir}'`) + process.exit(1) + } + + const { owner: sourceOwner, repo: sourceRepo } = source + console.log(`🔍 Checking ${libDir} skills against ${sourceOwner}/${sourceRepo}...\n`) + + const currentTreeSha = getLocalGitSha(TREE_GENERATOR_PATH) + const stateTreeSha = state?.tree_generator_sha + const treeGeneratorChanged = currentTreeSha && stateTreeSha && currentTreeSha !== stateTreeSha + + const currentVersion = fetchCurrentVersion(sourceOwner, sourceRepo, `packages/${sourceRepo}/package.json`) + const skillVersion = state?.library_version ?? null + const versionDrift = classifyVersionDrift(skillVersion, currentVersion) + + printVersionInfo(skillVersion, currentVersion, versionDrift, treeGeneratorChanged) + + const { staleSkills, currentSkills } = await classifySkills(skills, state, treeGeneratorChanged, currentVersion) + + printSourceSummary(staleSkills) + + if (staleSkills.length === 0) { + console.log('✓ All skills up to date') + return + } + + printStalenessDetails(staleSkills, currentSkills, skills.length) + printRecommendations({ libDir, staleSkills, currentSkills, currentVersion, versionDrift }) + printChangelog(sourceOwner, sourceRepo, skillVersion, currentVersion, versionDrift) + + if (writeReport) { + await writeStalenessReportFile({ + libDir, sourceOwner, sourceRepo, skillVersion, currentVersion, + treeGeneratorChanged, versionDrift, staleSkills, currentSkills, + }) + } +} + +// --------------------------------------------------------------------------- +// Mode 2: Mark synced +// --------------------------------------------------------------------------- + +async function markSynced(libDir, skillNames, markAll) { + const skills = await discoverSkills(libDir) + const state = (await readSyncState(libDir)) ?? { + library: libDir, + source_repo: '', + library_version: '', + tree_generator_sha: '', + synced_at: '', + skills: {}, + } + + const source = await findSourceRepo(skills) + const toSync = markAll ? skills : skills.filter((s) => skillNames.includes(s.name)) + + if (toSync.length === 0) { + console.error('Error: No matching skills found to mark as synced') + process.exit(1) + } + + const currentTreeSha = getLocalGitSha(TREE_GENERATOR_PATH) + const today = new Date().toISOString().split('T')[0] + + for (const skill of toSync) { + state.skills[skill.name] = { sources_sha: await collectSourceShas(skill) } + console.log(`✓ Marked ${skill.name} as synced`) + } + + // Update top-level state + if (source) state.source_repo = `${source.owner}/${source.repo}` + const firstFm = await parseFrontmatter(toSync[0].filePath) + if (firstFm?.library_version) state.library_version = firstFm.library_version + if (currentTreeSha) state.tree_generator_sha = currentTreeSha + state.synced_at = today + + await writeSyncState(libDir, state) + console.log(`\n✓ sync-state.json updated (${toSync.length} skills)`) +} + +// --------------------------------------------------------------------------- +// Mode 3: Bump version +// --------------------------------------------------------------------------- + +async function bumpVersion(libDir, newVersion, skillNames) { + const skills = await discoverSkills(libDir) + const toBump = skillNames.length > 0 + ? skills.filter((s) => skillNames.includes(s.name)) + : skills + + if (toBump.length === 0) { + console.error('Error: No matching skills found to bump') + process.exit(1) + } + + let bumped = 0 + for (const skill of toBump) { + const success = await bumpFrontmatterVersion(skill.filePath, newVersion) + if (success) { + bumped++ + console.log(`✓ ${skill.name} → v${newVersion}`) + } else { + console.warn(` Warning: Could not bump ${skill.name}`) + } + } + + // Update sync-state.json + const state = await readSyncState(libDir) + if (state) { + state.library_version = newVersion + state.synced_at = new Date().toISOString().split('T')[0] + await writeSyncState(libDir, state) + } + + console.log(`\n✓ ${bumped} skill(s) bumped to v${newVersion}`) +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +const USAGE = `TanStack Playbooks — Skill Sync + +Usage: + node scripts/sync-skills.mjs # detect staleness + node scripts/sync-skills.mjs --report # detect + write report + node scripts/sync-skills.mjs --mark-synced [skills..] # mark skills as synced + node scripts/sync-skills.mjs --mark-synced --all # mark all as synced + node scripts/sync-skills.mjs --bump-version [skills..] # bump frontmatter version + +Examples: + node scripts/sync-skills.mjs db + node scripts/sync-skills.mjs db --mark-synced db/core db/react + node scripts/sync-skills.mjs db --bump-version 0.6.0` + +function requireGhCli() { + try { + execSync('gh --version', { stdio: 'pipe' }) + } catch { + console.error('Error: The GitHub CLI (gh) is required.\nInstall it from: https://cli.github.com') + process.exit(1) + } + try { + execSync('gh auth status', { stdio: 'pipe' }) + } catch { + console.error('Error: Not authenticated with GitHub. Run: gh auth login') + process.exit(1) + } +} + +const libDir = process.argv[2] +const args = process.argv.slice(3) + +if (!libDir || libDir.startsWith('--')) { + console.log(USAGE) + process.exit(libDir ? 1 : 0) +} + +// Verify the library has skills +if (!fs.existsSync(path.join(SKILLS_DIR, libDir))) { + console.error(`Error: No skills directory found for '${libDir}' at ${path.join(SKILLS_DIR, libDir)}`) + process.exit(1) +} + +requireGhCli() + +if (args.includes('--mark-synced')) { + const markAll = args.includes('--all') + const skillNames = args.filter((a) => !a.startsWith('--')) + await markSynced(libDir, skillNames, markAll) +} else if (args.includes('--bump-version')) { + const bumpIdx = args.indexOf('--bump-version') + const newVersion = args[bumpIdx + 1] + if (!newVersion || newVersion.startsWith('--')) { + console.error('Error: --bump-version requires a version argument') + process.exit(1) + } + const skillNames = args.slice(bumpIdx + 2).filter((a) => !a.startsWith('--')) + await bumpVersion(libDir, newVersion, skillNames) +} else { + const writeReport = args.includes('--report') + await detectStaleness(libDir, writeReport) +} diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts new file mode 100644 index 0000000..bff167c --- /dev/null +++ b/scripts/validate-skills.ts @@ -0,0 +1,250 @@ +import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs' +import { join, relative, sep } from 'node:path' +import { parse as parseYaml } from 'yaml' + +// ── Types ── + +interface SkillFrontmatter { + name: string + description: string + type?: string + library?: string + framework?: string + library_version?: string + requires?: Array + sources?: Array +} + +interface ValidationError { + file: string + message: string +} + +// ── Constants ── + +const SKILLS_DIR = join(process.cwd(), 'packages', 'playbooks', 'skills') +const PACKAGE_MAP_PATH = join( + process.cwd(), + 'packages', + 'playbooks', + 'package_map.yaml', +) +const MAX_LINES = 500 + +const PROHIBITED_PATTERNS: Array<{ pattern: RegExp; description: string }> = [ + { + pattern: + /(?:npm|yarn|pnpm|bun)\s+(?:install|add|i)\s/i, + description: 'Install instructions', + }, + { + pattern: /(?:curl|wget|fetch)\s+https?:\/\//i, + description: 'Instructions to fetch external URLs at runtime', + }, +] + +const ALLOWED_SHELL_COMMANDS = [ + 'tanstack playbook list', + 'tanstack playbook feedback', + 'npm install @tanstack/', +] + +// ── Helpers ── + +function findSkillFiles(dir: string): Array { + const files: Array = [] + if (!existsSync(dir)) return files + + for (const entry of readdirSync(dir)) { + const fullPath = join(dir, entry) + const stat = statSync(fullPath) + if (stat.isDirectory()) { + files.push(...findSkillFiles(fullPath)) + } else if (entry === 'SKILL.md') { + files.push(fullPath) + } + } + return files +} + +function extractFrontmatter( + content: string, +): { frontmatter: SkillFrontmatter; body: string } | null { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/) + if (!match) return null + + try { + const frontmatter = parseYaml(match[1]) as SkillFrontmatter + return { frontmatter, body: match[2] } + } catch { + return null + } +} + +function skillPathFromFile(filePath: string): string { + const rel = relative(SKILLS_DIR, filePath) + // Remove trailing /SKILL.md and convert separators to / + return rel + .replace(/[/\\]SKILL\.md$/, '') + .split(sep) + .join('/') +} + +// ── Validators ── + +function validateFrontmatter( + filePath: string, + frontmatter: SkillFrontmatter, +): Array { + const errors: Array = [] + const rel = relative(process.cwd(), filePath) + + if (!frontmatter.name) { + errors.push({ file: rel, message: 'Missing required field: name' }) + } + + if (!frontmatter.description) { + errors.push({ file: rel, message: 'Missing required field: description' }) + } + + // Validate name matches directory path + if (frontmatter.name) { + const expectedPath = skillPathFromFile(filePath) + if (frontmatter.name !== expectedPath) { + errors.push({ + file: rel, + message: `name "${frontmatter.name}" does not match directory path "${expectedPath}"`, + }) + } + } + + // Framework skills must have requires + if (frontmatter.type === 'framework' && !frontmatter.requires?.length) { + errors.push({ + file: rel, + message: 'Framework skills must have a "requires" field', + }) + } + + return errors +} + +function validateContent( + filePath: string, + content: string, + body: string, +): Array { + const errors: Array = [] + const rel = relative(process.cwd(), filePath) + + // Line count + const lineCount = content.split(/\r?\n/).length + if (lineCount > MAX_LINES) { + errors.push({ + file: rel, + message: `Exceeds ${MAX_LINES} line limit (${lineCount} lines)`, + }) + } + + // Prohibited content + for (const { pattern, description } of PROHIBITED_PATTERNS) { + // Check each line to allow framework setup instructions (npm install @tanstack/...) + for (const line of body.split(/\r?\n/)) { + if (pattern.test(line)) { + const isAllowed = ALLOWED_SHELL_COMMANDS.some((cmd) => + line.includes(cmd), + ) + if (!isAllowed) { + errors.push({ + file: rel, + message: `Prohibited content: ${description} — "${line.trim().slice(0, 80)}"`, + }) + break + } + } + } + } + + return errors +} + +function validatePackageMap(): Array { + const errors: Array = [] + const rel = relative(process.cwd(), PACKAGE_MAP_PATH) + + if (!existsSync(PACKAGE_MAP_PATH)) { + errors.push({ file: rel, message: 'package_map.yaml not found' }) + return errors + } + + try { + const content = readFileSync(PACKAGE_MAP_PATH, 'utf-8') + const map = parseYaml(content) as Record + + if (!map.schema_version) { + errors.push({ file: rel, message: 'Missing schema_version' }) + } + + if (!map.package_map || typeof map.package_map !== 'object') { + errors.push({ file: rel, message: 'Missing or invalid package_map' }) + } + + if (!map.always_include || !Array.isArray(map.always_include)) { + errors.push({ + file: rel, + message: 'Missing or invalid always_include', + }) + } + } catch (e) { + const message = e instanceof Error ? e.message : String(e) + errors.push({ file: rel, message: `Invalid YAML: ${message}` }) + } + + return errors +} + +// ── Main ── + +function main(): void { + const errors: Array = [] + + // Validate package_map.yaml + errors.push(...validatePackageMap()) + + // Find and validate all SKILL.md files + const skillFiles = findSkillFiles(SKILLS_DIR) + + if (skillFiles.length === 0) { + console.error('No SKILL.md files found') + process.exit(1) + } + + for (const filePath of skillFiles) { + const content = readFileSync(filePath, 'utf-8') + const parsed = extractFrontmatter(content) + + const rel = relative(process.cwd(), filePath) + + if (!parsed) { + errors.push({ file: rel, message: 'Missing or invalid frontmatter' }) + continue + } + + errors.push(...validateFrontmatter(filePath, parsed.frontmatter)) + errors.push(...validateContent(filePath, content, parsed.body)) + } + + // Report + if (errors.length > 0) { + console.error(`\n❌ Validation failed with ${errors.length} error(s):\n`) + for (const { file, message } of errors) { + console.error(` ${file}: ${message}`) + } + console.error('') + process.exit(1) + } + + console.log(`✅ Validated ${skillFiles.length} skill files — all passed`) +} + +main() diff --git a/vitest.workspace.js b/vitest.workspace.js index 1cd6571..08c2b52 100644 --- a/vitest.workspace.js +++ b/vitest.workspace.js @@ -1,16 +1,9 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - projects: [ - './packages/pacer/vite.config.ts', - './packages/pacer-lite/vite.config.ts', - './packages/preact-pacer/vitest.config.ts', - './packages/preact-pacer-devtools/vitest.config.ts', - './packages/react-pacer/vite.config.ts', - './packages/react-pacer-devtools/vite.config.ts', - './packages/solid-pacer/vite.config.ts', - './packages/solid-pacer-devtools/vite.config.ts', - ], - }, -}) +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + projects: [ + './packages/playbooks/vitest.config.ts', + ], + }, +})