diff --git a/.claude/skills/tbd/SKILL.md b/.claude/skills/tbd/SKILL.md index 38f184e0..2c4beeba 100644 --- a/.claude/skills/tbd/SKILL.md +++ b/.claude/skills/tbd/SKILL.md @@ -173,6 +173,7 @@ or want help → run `tbd shortcut welcome-user` | `tbd guidelines ` | Load coding guidelines | | `tbd guidelines --list` | List guidelines | | `tbd template ` | Output a template | +| `tbd reference ` | Load a reference document | ## Quick Reference diff --git a/.tbd/.gitignore b/.tbd/.gitignore index 1f6cdaf8..f9205692 100644 --- a/.tbd/.gitignore +++ b/.tbd/.gitignore @@ -17,3 +17,7 @@ state.yml # Migration backups (local only, not synced) backups/ + + +# Cached external repo checkouts +repo-cache/ diff --git a/.tbd/config.yml b/.tbd/config.yml index 7913bd6a..aecfd960 100644 --- a/.tbd/config.yml +++ b/.tbd/config.yml @@ -3,7 +3,7 @@ display: # Documentation cache configuration. # files: Maps destination paths (relative to .tbd/docs/) to source locations. # Sources can be: -# - internal: prefix for bundled docs (e.g., "internal:shortcuts/standard/code-review-and-commit.md") +# - internal: prefix for bundled docs (e.g., "internal:tbd/shortcuts/code-review-and-commit.md") # - Full URL for external docs (e.g., "https://raw.githubusercontent.com/org/repo/main/file.md") # lookup_path: Search paths for doc lookup (like shell $PATH). Earlier paths take precedence. # @@ -14,71 +14,71 @@ display: # Configure with settings.doc_auto_sync_hours (0 = disabled). docs_cache: files: - guidelines/backward-compatibility-rules.md: internal:guidelines/backward-compatibility-rules.md - guidelines/bun-monorepo-patterns.md: internal:guidelines/bun-monorepo-patterns.md - guidelines/cli-agent-skill-patterns.md: internal:guidelines/cli-agent-skill-patterns.md - guidelines/commit-conventions.md: internal:guidelines/commit-conventions.md - guidelines/convex-limits-best-practices.md: internal:guidelines/convex-limits-best-practices.md - guidelines/convex-rules.md: internal:guidelines/convex-rules.md - guidelines/electron-app-development-patterns.md: internal:guidelines/electron-app-development-patterns.md - guidelines/error-handling-rules.md: internal:guidelines/error-handling-rules.md - guidelines/general-coding-rules.md: internal:guidelines/general-coding-rules.md - guidelines/general-comment-rules.md: internal:guidelines/general-comment-rules.md - guidelines/general-eng-assistant-rules.md: internal:guidelines/general-eng-assistant-rules.md - guidelines/general-style-rules.md: internal:guidelines/general-style-rules.md - guidelines/general-tdd-guidelines.md: internal:guidelines/general-tdd-guidelines.md - guidelines/general-testing-rules.md: internal:guidelines/general-testing-rules.md - guidelines/golden-testing-guidelines.md: internal:guidelines/golden-testing-guidelines.md - guidelines/pnpm-monorepo-patterns.md: internal:guidelines/pnpm-monorepo-patterns.md - guidelines/python-cli-patterns.md: internal:guidelines/python-cli-patterns.md - guidelines/python-modern-guidelines.md: internal:guidelines/python-modern-guidelines.md - guidelines/python-rules.md: internal:guidelines/python-rules.md - guidelines/release-notes-guidelines.md: internal:guidelines/release-notes-guidelines.md - guidelines/tbd-sync-troubleshooting.md: internal:guidelines/tbd-sync-troubleshooting.md - guidelines/typescript-cli-tool-rules.md: internal:guidelines/typescript-cli-tool-rules.md - guidelines/typescript-code-coverage.md: internal:guidelines/typescript-code-coverage.md - guidelines/typescript-rules.md: internal:guidelines/typescript-rules.md - guidelines/typescript-sorting-patterns.md: internal:guidelines/typescript-sorting-patterns.md - guidelines/typescript-yaml-handling-rules.md: internal:guidelines/typescript-yaml-handling-rules.md - shortcuts/standard/agent-handoff.md: internal:shortcuts/standard/agent-handoff.md - shortcuts/standard/checkout-third-party-repo.md: internal:shortcuts/standard/checkout-third-party-repo.md - shortcuts/standard/code-cleanup-all.md: internal:shortcuts/standard/code-cleanup-all.md - shortcuts/standard/code-cleanup-docstrings.md: internal:shortcuts/standard/code-cleanup-docstrings.md - shortcuts/standard/code-cleanup-tests.md: internal:shortcuts/standard/code-cleanup-tests.md - shortcuts/standard/code-review-and-commit.md: internal:shortcuts/standard/code-review-and-commit.md - shortcuts/standard/coding-spike.md: internal:shortcuts/standard/coding-spike.md - shortcuts/standard/create-or-update-pr-simple.md: internal:shortcuts/standard/create-or-update-pr-simple.md - shortcuts/standard/create-or-update-pr-with-validation-plan.md: internal:shortcuts/standard/create-or-update-pr-with-validation-plan.md - shortcuts/standard/implement-beads.md: internal:shortcuts/standard/implement-beads.md - shortcuts/standard/merge-upstream.md: internal:shortcuts/standard/merge-upstream.md - shortcuts/standard/new-architecture-doc.md: internal:shortcuts/standard/new-architecture-doc.md - shortcuts/standard/new-guideline.md: internal:shortcuts/standard/new-guideline.md - shortcuts/standard/new-plan-spec.md: internal:shortcuts/standard/new-plan-spec.md - shortcuts/standard/new-research-brief.md: internal:shortcuts/standard/new-research-brief.md - shortcuts/standard/new-shortcut.md: internal:shortcuts/standard/new-shortcut.md - shortcuts/standard/new-validation-plan.md: internal:shortcuts/standard/new-validation-plan.md - shortcuts/standard/plan-implementation-with-beads.md: internal:shortcuts/standard/plan-implementation-with-beads.md - shortcuts/standard/precommit-process.md: internal:shortcuts/standard/precommit-process.md - shortcuts/standard/review-code-python.md: internal:shortcuts/standard/review-code-python.md - shortcuts/standard/review-code-typescript.md: internal:shortcuts/standard/review-code-typescript.md - shortcuts/standard/review-code.md: internal:shortcuts/standard/review-code.md - shortcuts/standard/review-github-pr.md: internal:shortcuts/standard/review-github-pr.md - shortcuts/standard/revise-all-architecture-docs.md: internal:shortcuts/standard/revise-all-architecture-docs.md - shortcuts/standard/revise-architecture-doc.md: internal:shortcuts/standard/revise-architecture-doc.md - shortcuts/standard/setup-github-cli.md: internal:shortcuts/standard/setup-github-cli.md - shortcuts/standard/sync-failure-recovery.md: internal:shortcuts/standard/sync-failure-recovery.md - shortcuts/standard/update-specs-status.md: internal:shortcuts/standard/update-specs-status.md - shortcuts/standard/welcome-user.md: internal:shortcuts/standard/welcome-user.md - shortcuts/system/shortcut-explanation.md: internal:shortcuts/system/shortcut-explanation.md - shortcuts/system/skill-baseline.md: internal:shortcuts/system/skill-baseline.md - shortcuts/system/skill-brief.md: internal:shortcuts/system/skill-brief.md - shortcuts/system/skill-minimal.md: internal:shortcuts/system/skill-minimal.md - templates/architecture-doc.md: internal:templates/architecture-doc.md - templates/plan-spec.md: internal:templates/plan-spec.md - templates/research-brief.md: internal:templates/research-brief.md + sys/shortcuts/shortcut-explanation.md: internal:sys/shortcuts/shortcut-explanation.md + sys/shortcuts/skill-baseline.md: internal:sys/shortcuts/skill-baseline.md + sys/shortcuts/skill-brief.md: internal:sys/shortcuts/skill-brief.md + sys/shortcuts/skill-minimal.md: internal:sys/shortcuts/skill-minimal.md + tbd/guidelines/backward-compatibility-rules.md: internal:tbd/guidelines/backward-compatibility-rules.md + tbd/guidelines/bun-monorepo-patterns.md: internal:tbd/guidelines/bun-monorepo-patterns.md + tbd/guidelines/cli-agent-skill-patterns.md: internal:tbd/guidelines/cli-agent-skill-patterns.md + tbd/guidelines/commit-conventions.md: internal:tbd/guidelines/commit-conventions.md + tbd/guidelines/convex-limits-best-practices.md: internal:tbd/guidelines/convex-limits-best-practices.md + tbd/guidelines/convex-rules.md: internal:tbd/guidelines/convex-rules.md + tbd/guidelines/electron-app-development-patterns.md: internal:tbd/guidelines/electron-app-development-patterns.md + tbd/guidelines/error-handling-rules.md: internal:tbd/guidelines/error-handling-rules.md + tbd/guidelines/general-coding-rules.md: internal:tbd/guidelines/general-coding-rules.md + tbd/guidelines/general-comment-rules.md: internal:tbd/guidelines/general-comment-rules.md + tbd/guidelines/general-eng-assistant-rules.md: internal:tbd/guidelines/general-eng-assistant-rules.md + tbd/guidelines/general-style-rules.md: internal:tbd/guidelines/general-style-rules.md + tbd/guidelines/general-tdd-guidelines.md: internal:tbd/guidelines/general-tdd-guidelines.md + tbd/guidelines/general-testing-rules.md: internal:tbd/guidelines/general-testing-rules.md + tbd/guidelines/golden-testing-guidelines.md: internal:tbd/guidelines/golden-testing-guidelines.md + tbd/guidelines/pnpm-monorepo-patterns.md: internal:tbd/guidelines/pnpm-monorepo-patterns.md + tbd/guidelines/python-cli-patterns.md: internal:tbd/guidelines/python-cli-patterns.md + tbd/guidelines/python-modern-guidelines.md: internal:tbd/guidelines/python-modern-guidelines.md + tbd/guidelines/python-rules.md: internal:tbd/guidelines/python-rules.md + tbd/guidelines/release-notes-guidelines.md: internal:tbd/guidelines/release-notes-guidelines.md + tbd/guidelines/tbd-sync-troubleshooting.md: internal:tbd/guidelines/tbd-sync-troubleshooting.md + tbd/guidelines/typescript-cli-tool-rules.md: internal:tbd/guidelines/typescript-cli-tool-rules.md + tbd/guidelines/typescript-code-coverage.md: internal:tbd/guidelines/typescript-code-coverage.md + tbd/guidelines/typescript-rules.md: internal:tbd/guidelines/typescript-rules.md + tbd/guidelines/typescript-sorting-patterns.md: internal:tbd/guidelines/typescript-sorting-patterns.md + tbd/guidelines/typescript-yaml-handling-rules.md: internal:tbd/guidelines/typescript-yaml-handling-rules.md + tbd/shortcuts/agent-handoff.md: internal:tbd/shortcuts/agent-handoff.md + tbd/shortcuts/checkout-third-party-repo.md: internal:tbd/shortcuts/checkout-third-party-repo.md + tbd/shortcuts/code-cleanup-all.md: internal:tbd/shortcuts/code-cleanup-all.md + tbd/shortcuts/code-cleanup-docstrings.md: internal:tbd/shortcuts/code-cleanup-docstrings.md + tbd/shortcuts/code-cleanup-tests.md: internal:tbd/shortcuts/code-cleanup-tests.md + tbd/shortcuts/code-review-and-commit.md: internal:tbd/shortcuts/code-review-and-commit.md + tbd/shortcuts/coding-spike.md: internal:tbd/shortcuts/coding-spike.md + tbd/shortcuts/create-or-update-pr-simple.md: internal:tbd/shortcuts/create-or-update-pr-simple.md + tbd/shortcuts/create-or-update-pr-with-validation-plan.md: internal:tbd/shortcuts/create-or-update-pr-with-validation-plan.md + tbd/shortcuts/implement-beads.md: internal:tbd/shortcuts/implement-beads.md + tbd/shortcuts/merge-upstream.md: internal:tbd/shortcuts/merge-upstream.md + tbd/shortcuts/new-architecture-doc.md: internal:tbd/shortcuts/new-architecture-doc.md + tbd/shortcuts/new-guideline.md: internal:tbd/shortcuts/new-guideline.md + tbd/shortcuts/new-plan-spec.md: internal:tbd/shortcuts/new-plan-spec.md + tbd/shortcuts/new-research-brief.md: internal:tbd/shortcuts/new-research-brief.md + tbd/shortcuts/new-shortcut.md: internal:tbd/shortcuts/new-shortcut.md + tbd/shortcuts/new-validation-plan.md: internal:tbd/shortcuts/new-validation-plan.md + tbd/shortcuts/plan-implementation-with-beads.md: internal:tbd/shortcuts/plan-implementation-with-beads.md + tbd/shortcuts/precommit-process.md: internal:tbd/shortcuts/precommit-process.md + tbd/shortcuts/review-code-python.md: internal:tbd/shortcuts/review-code-python.md + tbd/shortcuts/review-code-typescript.md: internal:tbd/shortcuts/review-code-typescript.md + tbd/shortcuts/review-code.md: internal:tbd/shortcuts/review-code.md + tbd/shortcuts/review-github-pr.md: internal:tbd/shortcuts/review-github-pr.md + tbd/shortcuts/revise-all-architecture-docs.md: internal:tbd/shortcuts/revise-all-architecture-docs.md + tbd/shortcuts/revise-architecture-doc.md: internal:tbd/shortcuts/revise-architecture-doc.md + tbd/shortcuts/setup-github-cli.md: internal:tbd/shortcuts/setup-github-cli.md + tbd/shortcuts/sync-failure-recovery.md: internal:tbd/shortcuts/sync-failure-recovery.md + tbd/shortcuts/update-specs-status.md: internal:tbd/shortcuts/update-specs-status.md + tbd/shortcuts/welcome-user.md: internal:tbd/shortcuts/welcome-user.md + tbd/templates/architecture-doc.md: internal:tbd/templates/architecture-doc.md + tbd/templates/plan-spec.md: internal:tbd/templates/plan-spec.md + tbd/templates/research-brief.md: internal:tbd/templates/research-brief.md lookup_path: - - .tbd/docs/shortcuts/system - - .tbd/docs/shortcuts/standard + - .tbd/docs/sys/shortcuts + - .tbd/docs/tbd/shortcuts settings: auto_sync: false doc_auto_sync_hours: 24 @@ -86,5 +86,5 @@ settings: sync: branch: tbd-sync remote: origin -tbd_format: f03 +tbd_format: f04 tbd_version: development diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md b/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md new file mode 100644 index 00000000..bb42482a --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md @@ -0,0 +1,25 @@ +--- +child_order_hints: + - is-01kgzxe3p3qc7m2zxz0ga530vy + - is-01kgzxj12dj31rfwh0xxftttmy + - is-01kgzxkyye3gaxrebyyqzh21w7 + - is-01kgzxpbfpk8sf45cveezakydn +created_at: 2026-02-09T00:39:05.056Z +dependencies: + - target: is-01kgzyj5y0xm00qdqsgay4vfv9 + type: blocks + - target: is-01kgzypm020x8n0jgadn5g3v7x + type: blocks +id: is-01kgzxcx31b6kjdd9v8r3gt5e3 +kind: epic +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 1 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 0a: Prerequisite fixes for external docs repos" +type: is +updated_at: 2026-02-09T01:51:03.411Z +version: 10 +--- +Fix code issues before starting external repo sources work (plan-2026-02-02-external-docs-repos.md Phase 0a). Reduces risk during main phases. Contains 4 workstreams: (0a.1) Refactor shortcut.ts to use DocCommandHandler, (0a.2) Add warnings field to MigrationResult, (0a.3) Update generateShortcutDirectory() for hidden support, (0a.4) Establish shared test fixtures. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxe3p3qc7m2zxz0ga530vy.md b/.tbd/workspaces/outbox/issues/is-01kgzxe3p3qc7m2zxz0ga530vy.md new file mode 100644 index 00000000..c79cb623 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxe3p3qc7m2zxz0ga530vy.md @@ -0,0 +1,22 @@ +--- +child_order_hints: + - is-01kgzxetnqnvtf41cx6a988sy8 + - is-01kgzxfmfss9zfpj5abhgm2whz + - is-01kgzxfqrfxmt6pcdnw1t8vem6 + - is-01kgzxftzt59ardkfx9k3wj145 + - is-01kgzxfy6t76xrk1mybbg5jger +created_at: 2026-02-09T00:39:44.578Z +dependencies: [] +id: is-01kgzxe3p3qc7m2zxz0ga530vy +kind: task +labels: [] +parent_id: is-01kgzxcx31b6kjdd9v8r3gt5e3 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "0a.1: Refactor shortcut.ts to use DocCommandHandler" +type: is +updated_at: 2026-02-09T01:51:03.423Z +version: 8 +--- +shortcut.ts has its own ShortcutHandler extending BaseCommand with ~280 lines of duplicated logic (listing, querying, text wrapping) that already exists in DocCommandHandler. guidelines.ts and template.ts properly use DocCommandHandler. This refactor is a prerequisite for the prefix-based doc system since prefix-aware loading logic needs to live in DocCommandHandler. Use TDD: write characterization tests first, then refactor. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxetnqnvtf41cx6a988sy8.md b/.tbd/workspaces/outbox/issues/is-01kgzxetnqnvtf41cx6a988sy8.md new file mode 100644 index 00000000..6f3531f6 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxetnqnvtf41cx6a988sy8.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:40:08.118Z +dependencies: + - target: is-01kgzxfmfss9zfpj5abhgm2whz + type: blocks +id: is-01kgzxetnqnvtf41cx6a988sy8 +kind: task +labels: [] +parent_id: is-01kgzxe3p3qc7m2zxz0ga530vy +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED: Write characterization tests for shortcut command current behavior" +type: is +updated_at: 2026-02-09T01:51:03.431Z +version: 4 +--- +TDD Step 1: Write characterization tests capturing exact current behavior before refactoring. Tests should cover: (1) --list output format and content, (2) exact name lookup, (3) fuzzy search with score thresholds, (4) --category filtering, (5) --add mode, (6) --refresh backward compat, (7) no-query fallback showing shortcut-explanation.md, (8) SHORTCUT_AGENT_HEADER prepended to output, (9) shadowed entry display, (10) JSON output mode. Use existing doc-cache.test.ts and doc-sync.test.ts patterns. These tests must all pass against current code before any refactoring begins. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxfmfss9zfpj5abhgm2whz.md b/.tbd/workspaces/outbox/issues/is-01kgzxfmfss9zfpj5abhgm2whz.md new file mode 100644 index 00000000..435c34e5 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxfmfss9zfpj5abhgm2whz.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:40:34.552Z +dependencies: + - target: is-01kgzxfqrfxmt6pcdnw1t8vem6 + type: blocks +id: is-01kgzxfmfss9zfpj5abhgm2whz +kind: task +labels: [] +parent_id: is-01kgzxe3p3qc7m2zxz0ga530vy +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "GREEN: Migrate ShortcutHandler to extend DocCommandHandler" +type: is +updated_at: 2026-02-09T01:51:03.438Z +version: 4 +--- +TDD Step 2 (Green): Change ShortcutHandler to extend DocCommandHandler instead of BaseCommand. Map existing behavior to DocCommandHandler interface: typeName='shortcut', typeNamePlural='shortcuts', paths from config.docs_cache?.lookup_path ?? DEFAULT_SHORTCUT_PATHS, excludeFromList=['skill','skill-brief','shortcut-explanation'], noQueryDocName='shortcut-explanation', docType='shortcut'. All characterization tests from step 1 must still pass. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxfqrfxmt6pcdnw1t8vem6.md b/.tbd/workspaces/outbox/issues/is-01kgzxfqrfxmt6pcdnw1t8vem6.md new file mode 100644 index 00000000..0a226ad1 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxfqrfxmt6pcdnw1t8vem6.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:40:37.902Z +dependencies: + - target: is-01kgzxftzt59ardkfx9k3wj145 + type: blocks +id: is-01kgzxfqrfxmt6pcdnw1t8vem6 +kind: task +labels: [] +parent_id: is-01kgzxe3p3qc7m2zxz0ga530vy +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "GREEN: Move shortcut-specific behavior to DocCommandHandler overrides" +type: is +updated_at: 2026-02-09T01:51:03.445Z +version: 4 +--- +TDD Step 2b (Green): Move shortcut-specific behavior into DocCommandHandler overrides. ShortcutHandler overrides: (1) getAgentHeader() returns SHORTCUT_AGENT_HEADER, (2) handleListWithCategory() for --category filtering with ShortcutCategory type, (3) handleRefresh() for backward compat no-op. The base DocCommandHandler already handles --list, --add, query, no-query. Tests must still pass. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxftzt59ardkfx9k3wj145.md b/.tbd/workspaces/outbox/issues/is-01kgzxftzt59ardkfx9k3wj145.md new file mode 100644 index 00000000..2a0c46bc --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxftzt59ardkfx9k3wj145.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:40:41.209Z +dependencies: + - target: is-01kgzxfy6t76xrk1mybbg5jger + type: blocks +id: is-01kgzxftzt59ardkfx9k3wj145 +kind: task +labels: [] +parent_id: is-01kgzxe3p3qc7m2zxz0ga530vy +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "REFACTOR: Remove duplicate code from shortcut.ts after migration" +type: is +updated_at: 2026-02-09T01:51:03.452Z +version: 4 +--- +TDD Step 3 (Refactor): Delete all duplicated code from shortcut.ts that now lives in DocCommandHandler: extractFallbackText(), printWrappedDescription(), wrapAtWord(), handleList() (use base), handleNoQuery() (use base), handleQuery() (use base). The shortcut.ts file should shrink from ~380 lines to ~80-100 lines. Tests must still pass. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxfy6t76xrk1mybbg5jger.md b/.tbd/workspaces/outbox/issues/is-01kgzxfy6t76xrk1mybbg5jger.md new file mode 100644 index 00000000..335bf99c --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxfy6t76xrk1mybbg5jger.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T00:40:44.505Z +dependencies: [] +id: is-01kgzxfy6t76xrk1mybbg5jger +kind: task +labels: [] +parent_id: is-01kgzxe3p3qc7m2zxz0ga530vy +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "VERIFY: Run full test suite, confirm no regressions" +type: is +updated_at: 2026-02-09T01:51:03.460Z +version: 3 +--- +TDD Final verification: Run full characterization test suite. Verify identical behavior for all 10 test cases. Run pnpm test, pnpm lint, pnpm typecheck. Confirm no regressions in guidelines and template commands (they share DocCommandHandler). diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxj12dj31rfwh0xxftttmy.md b/.tbd/workspaces/outbox/issues/is-01kgzxj12dj31rfwh0xxftttmy.md new file mode 100644 index 00000000..7b8ecd6b --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxj12dj31rfwh0xxftttmy.md @@ -0,0 +1,21 @@ +--- +child_order_hints: + - is-01kgzxjqv0d9b1qrh8sf5qncv0 + - is-01kgzxjv3trypg9djykjk7v3as +created_at: 2026-02-09T00:41:52.972Z +dependencies: + - target: is-01kgzyqxkjmj2g4jpbhcegsnek + type: blocks +id: is-01kgzxj12dj31rfwh0xxftttmy +kind: task +labels: [] +parent_id: is-01kgzxcx31b6kjdd9v8r3gt5e3 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "0a.2: Add warnings field to MigrationResult" +type: is +updated_at: 2026-02-09T01:51:03.466Z +version: 6 +--- +MigrationResult in tbd-format.ts only has changes: string[]. The f03->f04 migration needs warnings: string[] for reporting preserved custom file overrides during config conversion. Small change, prerequisite for Phase 1 format bump. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxjqv0d9b1qrh8sf5qncv0.md b/.tbd/workspaces/outbox/issues/is-01kgzxjqv0d9b1qrh8sf5qncv0.md new file mode 100644 index 00000000..787e1aa4 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxjqv0d9b1qrh8sf5qncv0.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:42:16.287Z +dependencies: + - target: is-01kgzxjv3trypg9djykjk7v3as + type: blocks +id: is-01kgzxjqv0d9b1qrh8sf5qncv0 +kind: task +labels: [] +parent_id: is-01kgzxj12dj31rfwh0xxftttmy +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED: Write test for MigrationResult warnings field" +type: is +updated_at: 2026-02-09T01:51:03.474Z +version: 4 +--- +Write test in tbd-format.test.ts: (1) Test that migrateToLatest() result has warnings array, (2) Test that f01->f03 migration returns empty warnings, (3) Test that future f03->f04 migration with custom files returns warnings about preserved overrides. Tests should fail until warnings field is added. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxjv3trypg9djykjk7v3as.md b/.tbd/workspaces/outbox/issues/is-01kgzxjv3trypg9djykjk7v3as.md new file mode 100644 index 00000000..7f0e873a --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxjv3trypg9djykjk7v3as.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T00:42:19.640Z +dependencies: [] +id: is-01kgzxjv3trypg9djykjk7v3as +kind: task +labels: [] +parent_id: is-01kgzxj12dj31rfwh0xxftttmy +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "GREEN: Add warnings: string[] to MigrationResult interface and existing migration functions" +type: is +updated_at: 2026-02-09T01:51:03.481Z +version: 3 +--- +Add warnings: string[] to MigrationResult interface. Initialize warnings: [] in migrate_f01_to_f02() and migrate_f02_to_f03(). Update migrateToLatest() to aggregate warnings across migration steps (allWarnings array, same pattern as allChanges). Tests from previous step must pass. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxkyye3gaxrebyyqzh21w7.md b/.tbd/workspaces/outbox/issues/is-01kgzxkyye3gaxrebyyqzh21w7.md new file mode 100644 index 00000000..9fc8db4d --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxkyye3gaxrebyyqzh21w7.md @@ -0,0 +1,20 @@ +--- +child_order_hints: + - is-01kgzxmr6nf5s2pb631jnmeht8 + - is-01kgzxmvh2m0atzzcererq1j72 + - is-01kgzxmyvwg2dnnt751wnw2e9s +created_at: 2026-02-09T00:42:56.333Z +dependencies: [] +id: is-01kgzxkyye3gaxrebyyqzh21w7 +kind: task +labels: [] +parent_id: is-01kgzxcx31b6kjdd9v8r3gt5e3 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "0a.3: Update generateShortcutDirectory() for hidden source support" +type: is +updated_at: 2026-02-09T01:51:03.488Z +version: 6 +--- +generateShortcutDirectory() in doc-cache.ts currently hardcodes skip names (skill, skill-brief, shortcut-explanation). The prefix system introduces hidden sources that should be excluded generically. Add hidden?: boolean to CachedDoc, populate from source config, filter by doc.hidden. Keep hardcoded names as fallback during transition. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxmr6nf5s2pb631jnmeht8.md b/.tbd/workspaces/outbox/issues/is-01kgzxmr6nf5s2pb631jnmeht8.md new file mode 100644 index 00000000..499defab --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxmr6nf5s2pb631jnmeht8.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:43:22.196Z +dependencies: + - target: is-01kgzxmvh2m0atzzcererq1j72 + type: blocks +id: is-01kgzxmr6nf5s2pb631jnmeht8 +kind: task +labels: [] +parent_id: is-01kgzxkyye3gaxrebyyqzh21w7 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED: Write tests for hidden doc filtering in generateShortcutDirectory()" +type: is +updated_at: 2026-02-09T01:51:03.496Z +version: 4 +--- +Write tests: (1) generateShortcutDirectory() excludes docs where hidden=true, (2) hidden docs not shown in --list output, (3) hidden docs still accessible via direct lookup (tbd shortcut skill), (4) Backward compat: existing hardcoded skip names still work when hidden is undefined. Add to doc-cache.test.ts. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxmvh2m0atzzcererq1j72.md b/.tbd/workspaces/outbox/issues/is-01kgzxmvh2m0atzzcererq1j72.md new file mode 100644 index 00000000..2859203e --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxmvh2m0atzzcererq1j72.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:43:25.601Z +dependencies: + - target: is-01kgzxmyvwg2dnnt751wnw2e9s + type: blocks +id: is-01kgzxmvh2m0atzzcererq1j72 +kind: task +labels: [] +parent_id: is-01kgzxkyye3gaxrebyyqzh21w7 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "GREEN: Add hidden field to CachedDoc and filter in generateShortcutDirectory()" +type: is +updated_at: 2026-02-09T01:51:03.503Z +version: 4 +--- +Add hidden?: boolean to CachedDoc interface. In DocCache.loadDirectory(), accept optional hidden parameter and set on loaded docs. Update generateShortcutDirectory() to filter docs where hidden===true in addition to existing hardcoded names. Update buildTableRows() to accept and filter hidden docs. Tests must pass. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxmyvwg2dnnt751wnw2e9s.md b/.tbd/workspaces/outbox/issues/is-01kgzxmyvwg2dnnt751wnw2e9s.md new file mode 100644 index 00000000..4b0030b8 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxmyvwg2dnnt751wnw2e9s.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T00:43:29.019Z +dependencies: [] +id: is-01kgzxmyvwg2dnnt751wnw2e9s +kind: task +labels: [] +parent_id: is-01kgzxkyye3gaxrebyyqzh21w7 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "REFACTOR: Remove hardcoded skip names, use hidden field exclusively" +type: is +updated_at: 2026-02-09T01:51:03.510Z +version: 3 +--- +Once hidden field is working: remove hardcoded skip names array from generateShortcutDirectory() (skill, skill-brief, shortcut-explanation). These docs should be marked hidden when loaded from system source instead. Verify existing tests still pass. Note: keep backward compat during transition period - only remove hardcoded names once all callers set hidden properly. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxpbfpk8sf45cveezakydn.md b/.tbd/workspaces/outbox/issues/is-01kgzxpbfpk8sf45cveezakydn.md new file mode 100644 index 00000000..dcb6e0ca --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxpbfpk8sf45cveezakydn.md @@ -0,0 +1,21 @@ +--- +child_order_hints: + - is-01kgzxq3cswmbjqvn77m7gcb75 + - is-01kgzxq6s7wcczc1vrn9twsyf1 + - is-01kgzxqa3x20eqdcrv4jdfncsj + - is-01kgzxqddjcngrpcyegkqm4bb2 +created_at: 2026-02-09T00:44:14.709Z +dependencies: [] +id: is-01kgzxpbfpk8sf45cveezakydn +kind: task +labels: [] +parent_id: is-01kgzxcx31b6kjdd9v8r3gt5e3 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "0a.4: Establish shared test fixtures and helpers for doc infrastructure" +type: is +updated_at: 2026-02-09T01:51:03.516Z +version: 7 +--- +Set up reusable test fixtures and helpers for doc tests before the main Phase 1+ implementation. Reduces boilerplate in doc-sync.test.ts, doc-cache.test.ts, and future tests for repo-cache, prefix-based loading, etc. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxq3cswmbjqvn77m7gcb75.md b/.tbd/workspaces/outbox/issues/is-01kgzxq3cswmbjqvn77m7gcb75.md new file mode 100644 index 00000000..ac41b291 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxq3cswmbjqvn77m7gcb75.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:44:39.192Z +dependencies: + - target: is-01kgzxq6s7wcczc1vrn9twsyf1 + type: blocks +id: is-01kgzxq3cswmbjqvn77m7gcb75 +kind: task +labels: [] +parent_id: is-01kgzxpbfpk8sf45cveezakydn +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Create tests/fixtures/test-docs/ with sample docs for each type +type: is +updated_at: 2026-02-09T01:51:03.523Z +version: 4 +--- +Create packages/tbd/tests/fixtures/test-docs/ with sample markdown docs for each doc type: shortcuts/ (2-3 sample shortcuts with frontmatter), guidelines/ (2-3 sample guidelines), templates/ (1-2 sample templates), references/ (1-2 sample reference docs). Include docs with and without frontmatter, with various categories and tags, for comprehensive test coverage. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxq6s7wcczc1vrn9twsyf1.md b/.tbd/workspaces/outbox/issues/is-01kgzxq6s7wcczc1vrn9twsyf1.md new file mode 100644 index 00000000..73ff3970 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxq6s7wcczc1vrn9twsyf1.md @@ -0,0 +1,20 @@ +--- +created_at: 2026-02-09T00:44:42.662Z +dependencies: + - target: is-01kgzxqa3x20eqdcrv4jdfncsj + type: blocks + - target: is-01kgzxqddjcngrpcyegkqm4bb2 + type: blocks +id: is-01kgzxq6s7wcczc1vrn9twsyf1 +kind: task +labels: [] +parent_id: is-01kgzxpbfpk8sf45cveezakydn +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Create tests/helpers/doc-test-utils.ts with temp doc dir helpers +type: is +updated_at: 2026-02-09T01:51:03.530Z +version: 5 +--- +Create packages/tbd/tests/helpers/doc-test-utils.ts with: (1) createTempDocsDir() - creates temp directory with doc structure matching .tbd/docs/, (2) populateTestDocs() - copies fixture docs into temp dir with optional customization, (3) createMockConfig() - generates test config.yml with docs_cache entries, (4) cleanupTempDir() - cleanup helper. Use vitest beforeEach/afterEach patterns. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxqa3x20eqdcrv4jdfncsj.md b/.tbd/workspaces/outbox/issues/is-01kgzxqa3x20eqdcrv4jdfncsj.md new file mode 100644 index 00000000..9f0e3226 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxqa3x20eqdcrv4jdfncsj.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T00:44:46.076Z +dependencies: [] +id: is-01kgzxqa3x20eqdcrv4jdfncsj +kind: task +labels: [] +parent_id: is-01kgzxpbfpk8sf45cveezakydn +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Add helper for creating local bare git repos (for RepoCache tests) +type: is +updated_at: 2026-02-09T01:51:03.536Z +version: 3 +--- +Add createTestGitRepo() helper to doc-test-utils.ts: creates a local bare git repo using git init --bare, populates it with test markdown files, and returns the file:// URL. This enables RepoCache unit tests without network access. Include helper to add files and create commits in the test repo. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxqddjcngrpcyegkqm4bb2.md b/.tbd/workspaces/outbox/issues/is-01kgzxqddjcngrpcyegkqm4bb2.md new file mode 100644 index 00000000..93c97f40 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxqddjcngrpcyegkqm4bb2.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T00:44:49.457Z +dependencies: [] +id: is-01kgzxqddjcngrpcyegkqm4bb2 +kind: task +labels: [] +parent_id: is-01kgzxpbfpk8sf45cveezakydn +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Refactor existing doc-sync.test.ts and doc-cache.test.ts to use shared fixtures +type: is +updated_at: 2026-02-09T01:51:03.546Z +version: 3 +--- +Refactor existing doc-sync.test.ts and doc-cache.test.ts to use the new shared fixtures from test-docs/ and helpers from doc-test-utils.ts. Remove inline test doc creation where shared fixtures suffice. Verify all existing tests still pass after refactoring. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyh3ph1pfngcvyab02nhe9.md b/.tbd/workspaces/outbox/issues/is-01kgzyh3ph1pfngcvyab02nhe9.md new file mode 100644 index 00000000..b6788511 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyh3ph1pfngcvyab02nhe9.md @@ -0,0 +1,25 @@ +--- +child_order_hints: + - is-01kgzxcx31b6kjdd9v8r3gt5e3 + - is-01kgzyj5y0xm00qdqsgay4vfv9 + - is-01kgzypm020x8n0jgadn5g3v7x + - is-01kh00hr6eq3p16ebr73y7cxk1 + - is-01kh00nprzwe2hx8t0qbatyvqb + - is-01kh00r1v5stt6jeww4x7bq1pz + - is-01kh00tjaz5n4x0jdeqzgbnaq8 + - is-01kh00wq09fmchfvm0c8c2s2gg + - is-01kh00ywn96hz5rfvwm7bey6nw +created_at: 2026-02-09T00:58:51.471Z +dependencies: [] +id: is-01kgzyh3ph1pfngcvyab02nhe9 +kind: epic +labels: [] +priority: 1 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Spec: External Docs Repos" +type: is +updated_at: 2026-02-09T01:41:20.168Z +version: 11 +--- +Pull shortcuts, guidelines, templates, and references from external git repos. Enables community-maintained docs, project-specific repos, bleeding-edge guidelines. Adds prefix-based namespacing (sys, tbd, spec), shallow sparse checkout for repo sources, qualified prefix:name lookups, new tbd reference command. See spec for full design. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyj5y0xm00qdqsgay4vfv9.md b/.tbd/workspaces/outbox/issues/is-01kgzyj5y0xm00qdqsgay4vfv9.md new file mode 100644 index 00000000..32174eb9 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyj5y0xm00qdqsgay4vfv9.md @@ -0,0 +1,23 @@ +--- +child_order_hints: + - is-01kgzyk2gaa8r9xmbb9axhftaq + - is-01kgzyk5whe8q33z3zvqyq05bv + - is-01kgzyk94y5tx9y93cpmdxsr8c + - is-01kgzykcjvkwqk6mc6rx2xbjah + - is-01kgzykg105ptpmjar5yyfz045 + - is-01kgzykkgk2p3jstgr4bstm0d8 +created_at: 2026-02-09T00:59:26.526Z +dependencies: [] +id: is-01kgzyj5y0xm00qdqsgay4vfv9 +kind: task +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 0: Speculate Prep" +type: is +updated_at: 2026-02-09T01:00:13.202Z +version: 8 +--- +Prepare Speculate repo (github.com/jlevy/speculate) with tbd-compatible structure on a tbd branch. This becomes the integration test target for all subsequent phases. Restructure agent-rules/agent-guidelines/agent-shortcuts to flat guidelines/shortcuts/templates/references dirs. Rename files, update frontmatter, copy improved docs from tbd. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyk2gaa8r9xmbb9axhftaq.md b/.tbd/workspaces/outbox/issues/is-01kgzyk2gaa8r9xmbb9axhftaq.md new file mode 100644 index 00000000..bc2ec47d --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyk2gaa8r9xmbb9axhftaq.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:59:55.784Z +dependencies: + - target: is-01kgzyk5whe8q33z3zvqyq05bv + type: blocks +id: is-01kgzyk2gaa8r9xmbb9axhftaq +kind: task +labels: [] +parent_id: is-01kgzyj5y0xm00qdqsgay4vfv9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Clone jlevy/speculate and create tbd branch +type: is +updated_at: 2026-02-09T01:01:05.734Z +version: 3 +--- +Clone jlevy/speculate to repos/speculate using gh repo clone (for auth). Create and checkout tbd branch from main. Use gh CLI if direct git auth fails. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyk5whe8q33z3zvqyq05bv.md b/.tbd/workspaces/outbox/issues/is-01kgzyk5whe8q33z3zvqyq05bv.md new file mode 100644 index 00000000..8346653d --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyk5whe8q33z3zvqyq05bv.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T00:59:59.248Z +dependencies: + - target: is-01kgzyk94y5tx9y93cpmdxsr8c + type: blocks +id: is-01kgzyk5whe8q33z3zvqyq05bv +kind: task +labels: [] +parent_id: is-01kgzyj5y0xm00qdqsgay4vfv9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Restructure Speculate to flat doc type directories +type: is +updated_at: 2026-02-09T01:01:33.279Z +version: 4 +--- +Restructure Speculate on tbd branch for clean tbd-compatible layout. (1) Rename existing docs/ to old-docs/ (legacy, for owner review). (2) Create new top-level structure: guidelines/ (from agent-rules/ + agent-guidelines/), shortcuts/ (from agent-shortcuts/, strip shortcut- prefix), templates/ (from docs/project/), references/ (new). Clean {type}/{name}.md layout, no nesting. Structure should be as clean and organized as possible. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyk94y5tx9y93cpmdxsr8c.md b/.tbd/workspaces/outbox/issues/is-01kgzyk94y5tx9y93cpmdxsr8c.md new file mode 100644 index 00000000..4ac2bb19 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyk94y5tx9y93cpmdxsr8c.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:00:02.589Z +dependencies: + - target: is-01kgzykcjvkwqk6mc6rx2xbjah + type: blocks +id: is-01kgzyk94y5tx9y93cpmdxsr8c +kind: task +labels: [] +parent_id: is-01kgzyj5y0xm00qdqsgay4vfv9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Update Speculate front matter and shortcut references to tbd format +type: is +updated_at: 2026-02-09T01:01:12.645Z +version: 3 +--- +Update all Speculate docs on tbd branch: (1) Add rich frontmatter (title, description, category, tags) matching tbd conventions, (2) Update shortcut references from @shortcut-name.md to tbd shortcut name syntax, (3) Ensure all .md files have consistent frontmatter format. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzykcjvkwqk6mc6rx2xbjah.md b/.tbd/workspaces/outbox/issues/is-01kgzykcjvkwqk6mc6rx2xbjah.md new file mode 100644 index 00000000..a7272783 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzykcjvkwqk6mc6rx2xbjah.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:00:06.105Z +dependencies: + - target: is-01kgzykg105ptpmjar5yyfz045 + type: blocks +id: is-01kgzykcjvkwqk6mc6rx2xbjah +kind: task +labels: [] +parent_id: is-01kgzyj5y0xm00qdqsgay4vfv9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Copy improved docs from tbd to Speculate tbd branch +type: is +updated_at: 2026-02-09T01:01:16.034Z +version: 3 +--- +Compare tbd bundled docs with Speculate originals. For general-purpose docs (5 shortcuts + 26 guidelines + 3 templates), copy the more up-to-date version to Speculate tbd branch. tbd's docs are generally more current since they've been actively maintained. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzykg105ptpmjar5yyfz045.md b/.tbd/workspaces/outbox/issues/is-01kgzykg105ptpmjar5yyfz045.md new file mode 100644 index 00000000..5156fc9c --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzykg105ptpmjar5yyfz045.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:00:09.631Z +dependencies: + - target: is-01kgzykkgk2p3jstgr4bstm0d8 + type: blocks +id: is-01kgzykg105ptpmjar5yyfz045 +kind: task +labels: [] +parent_id: is-01kgzyj5y0xm00qdqsgay4vfv9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Push Speculate tbd branch and verify +type: is +updated_at: 2026-02-09T01:01:19.603Z +version: 3 +--- +Push Speculate tbd branch to remote. Verify branch visible at github.com/jlevy/speculate/tree/tbd. Do NOT merge to main yet - this branch is the integration test target. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzykkgk2p3jstgr4bstm0d8.md b/.tbd/workspaces/outbox/issues/is-01kgzykkgk2p3jstgr4bstm0d8.md new file mode 100644 index 00000000..f719461e --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzykkgk2p3jstgr4bstm0d8.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:00:13.202Z +dependencies: [] +id: is-01kgzykkgk2p3jstgr4bstm0d8 +kind: task +labels: [] +parent_id: is-01kgzyj5y0xm00qdqsgay4vfv9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Create sync-repos.sh script and add repos/ to .gitignore +type: is +updated_at: 2026-02-09T01:00:52.696Z +version: 2 +--- +In tbd repo: (1) Create sync-repos.sh script that clones speculate to repos/speculate for local dev, (2) Add repos/ to root .gitignore, (3) Update docs/development.md with test repo setup instructions. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzypm020x8n0jgadn5g3v7x.md b/.tbd/workspaces/outbox/issues/is-01kgzypm020x8n0jgadn5g3v7x.md new file mode 100644 index 00000000..c32a905a --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzypm020x8n0jgadn5g3v7x.md @@ -0,0 +1,28 @@ +--- +child_order_hints: + - is-01kgzyqpk2hmkj9rwpgkpjsjbj + - is-01kgzyqt5czjjjhvakcfpe5q81 + - is-01kgzyqxkjmj2g4jpbhcegsnek + - is-01kgzyr12a16cqreyy5ekn0r2k + - is-01kgzyr4fh728np85rprvp9s6e + - is-01kgzyr7y128s080n05fpnm9de + - is-01kgzyrbcf260b1791a043ccpv + - is-01kgzyretysjtans3brggwcqcj + - is-01kgzyrj4rhv2kjncymrzf01br +created_at: 2026-02-09T01:01:52.001Z +dependencies: + - target: is-01kh00hr6eq3p16ebr73y7cxk1 + type: blocks +id: is-01kgzypm020x8n0jgadn5g3v7x +kind: task +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 1: Core Infrastructure" +type: is +updated_at: 2026-02-09T01:43:24.731Z +version: 12 +--- +Build foundational components: doc-types registry, repo-url utility, format version bump f03->f04, DocsSourceSchema, RepoCache class for sparse checkouts, prefix-based DocSync rewrite. Integration checkpoint: test sync against Speculate tbd branch. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyqpk2hmkj9rwpgkpjsjbj.md b/.tbd/workspaces/outbox/issues/is-01kgzyqpk2hmkj9rwpgkpjsjbj.md new file mode 100644 index 00000000..f74b3196 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyqpk2hmkj9rwpgkpjsjbj.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:02:27.424Z +dependencies: + - target: is-01kgzyr7y128s080n05fpnm9de + type: blocks +id: is-01kgzyqpk2hmkj9rwpgkpjsjbj +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED+GREEN: Create doc-types.ts registry with unit tests" +type: is +updated_at: 2026-02-09T01:33:10.860Z +version: 3 +--- +Create src/lib/doc-types.ts with DOC_TYPES registry as single source of truth for doc types (shortcut, guideline, template, reference). Include DocTypeName type, inferDocType() for path-to-type mapping, and getDocTypeDirectories() helper. Write unit tests covering: registry entries, type inference from various path formats ({prefix}/{type}/{name}.md and flat paths), and directory name listing. TDD: write tests first, then implement to pass. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyqt5czjjjhvakcfpe5q81.md b/.tbd/workspaces/outbox/issues/is-01kgzyqt5czjjjhvakcfpe5q81.md new file mode 100644 index 00000000..497a7286 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyqt5czjjjhvakcfpe5q81.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:02:31.083Z +dependencies: + - target: is-01kgzyr4fh728np85rprvp9s6e + type: blocks +id: is-01kgzyqt5czjjjhvakcfpe5q81 +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED+GREEN: Create repo-url.ts utility with unit tests" +type: is +updated_at: 2026-02-09T01:33:07.596Z +version: 3 +--- +Create src/lib/repo-url.ts with NormalizedRepoUrl interface, normalizeRepoUrl() accepting all URL formats (short: github.com/org/repo, HTTPS, HTTPS+.git, SSH git@), repoUrlToSlug() for filesystem-safe cache paths (no @github/slugify dep — just replace / and : with -), and getCloneUrl() for git operations. Write comprehensive unit tests: all input formats, edge cases (trailing slashes, mixed case, special chars), round-trip determinism, invalid URL errors. TDD: tests first. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyqxkjmj2g4jpbhcegsnek.md b/.tbd/workspaces/outbox/issues/is-01kgzyqxkjmj2g4jpbhcegsnek.md new file mode 100644 index 00000000..f1c25d99 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyqxkjmj2g4jpbhcegsnek.md @@ -0,0 +1,20 @@ +--- +created_at: 2026-02-09T01:02:34.609Z +dependencies: + - target: is-01kgzyrbcf260b1791a043ccpv + type: blocks + - target: is-01kgzyretysjtans3brggwcqcj + type: blocks +id: is-01kgzyqxkjmj2g4jpbhcegsnek +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED+GREEN: Bump format f03->f04 with migration function" +type: is +updated_at: 2026-02-09T01:33:31.039Z +version: 4 +--- +Bump CURRENT_FORMAT from f03 to f04 in tbd-format.ts. Add FORMAT_HISTORY entry for f04 describing prefix-based sources, lookup_path removal. Implement migrate_f03_to_f04(): remove lookup_path, convert verbose files: entries to concise sources: array using isDefaultFileEntry() heuristic (source === 'internal:' + dest → default). Add convertFilesToSources() helper and getDefaultSources(). Add to migrateToLatest() chain and describeMigration(). Tests: default config migration (config becomes shorter), custom file override preservation, f04 rejection on older version. Depends on 0a.2 warnings field. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyr12a16cqreyy5ekn0r2k.md b/.tbd/workspaces/outbox/issues/is-01kgzyr12a16cqreyy5ekn0r2k.md new file mode 100644 index 00000000..a3dc2249 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyr12a16cqreyy5ekn0r2k.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:02:38.153Z +dependencies: + - target: is-01kgzyrbcf260b1791a043ccpv + type: blocks +id: is-01kgzyr12a16cqreyy5ekn0r2k +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Add DocsSourceSchema and update DocsCacheSchema in schemas.ts +type: is +updated_at: 2026-02-09T01:33:20.804Z +version: 3 +--- +Add DocsSourceSchema to schemas.ts: z.object with type (enum internal/repo), prefix (1-16 chars, lowercase alphanumeric + dash), optional url/ref/hidden, required paths array. Update DocsCacheSchema to include optional sources array alongside existing files/lookup_path (keep lookup_path in schema for migration parsing). Ensure Zod strip() mode interacts correctly with format version protection. Unit tests: valid/invalid prefixes, required fields by type, schema compatibility. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyr4fh728np85rprvp9s6e.md b/.tbd/workspaces/outbox/issues/is-01kgzyr4fh728np85rprvp9s6e.md new file mode 100644 index 00000000..0352d2fc --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyr4fh728np85rprvp9s6e.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:02:41.648Z +dependencies: + - target: is-01kgzyrbcf260b1791a043ccpv + type: blocks +id: is-01kgzyr4fh728np85rprvp9s6e +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED+GREEN: Implement RepoCache class for sparse git checkouts" +type: is +updated_at: 2026-02-09T01:33:14.213Z +version: 3 +--- +Create src/file/repo-cache.ts with RepoCache class. Constructor takes tbdRoot, derives cacheDir as .tbd/repo-cache/. ensureRepo(url, ref, paths) does shallow sparse clone on first run (git clone --depth 1 --sparse --branch ref), updates on subsequent (git fetch --depth 1 + checkout FETCH_HEAD). scanDocs(repoDir, paths) finds all .md files in path patterns. Use child_process.execFile for security (no shell injection). Fallback to gh repo clone if git fails. Tests use git init --bare local repos. TDD: tests first. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyr7y128s080n05fpnm9de.md b/.tbd/workspaces/outbox/issues/is-01kgzyr7y128s080n05fpnm9de.md new file mode 100644 index 00000000..bf291c90 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyr7y128s080n05fpnm9de.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:02:45.184Z +dependencies: + - target: is-01kgzyrbcf260b1791a043ccpv + type: blocks +id: is-01kgzyr7y128s080n05fpnm9de +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Restructure bundled docs to prefix-based layout (sys/, tbd/) +type: is +updated_at: 2026-02-09T01:33:17.512Z +version: 3 +--- +Restructure packages/tbd/docs/ from current layout (shortcuts/system/, shortcuts/standard/, guidelines/, templates/) to prefix-based layout (sys/shortcuts/, tbd/shortcuts/, tbd/guidelines/). sys/ gets system shortcuts (skill.md, skill-brief.md, shortcut-explanation.md, hidden). tbd/ gets all 29 standard shortcuts and tbd-specific guidelines (tbd-sync-troubleshooting.md). Update all import paths and references. Update generateDefaultDocCacheConfig() to scan new structure. Verify all existing tests pass with new paths. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyrbcf260b1791a043ccpv.md b/.tbd/workspaces/outbox/issues/is-01kgzyrbcf260b1791a043ccpv.md new file mode 100644 index 00000000..42159399 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyrbcf260b1791a043ccpv.md @@ -0,0 +1,20 @@ +--- +created_at: 2026-02-09T01:02:48.717Z +dependencies: + - target: is-01kgzyretysjtans3brggwcqcj + type: blocks + - target: is-01kgzyrj4rhv2kjncymrzf01br + type: blocks +id: is-01kgzyrbcf260b1791a043ccpv +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Rewrite DocSync for prefix-based storage and source-based sync +type: is +updated_at: 2026-02-09T01:33:34.451Z +version: 4 +--- +Rewrite syncDocsWithDefaults() in doc-sync.ts for source-based sync. For each source in config.docs_cache.sources: internal sources scan bundled docs at packages/tbd/docs/{prefix}/{type}/, repo sources use RepoCache.ensureRepo() + scanDocs(). Copy files to .tbd/docs/{prefix}/{type}/{name}.md. Apply files: overrides last (highest precedence). Write sources hash to .tbd/docs/.sources-hash for change detection. resolveSourcesToDocs() returns flat Record for existing DocSync compatibility. Integration tests with mock sources. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyretysjtans3brggwcqcj.md b/.tbd/workspaces/outbox/issues/is-01kgzyretysjtans3brggwcqcj.md new file mode 100644 index 00000000..34c10f20 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyretysjtans3brggwcqcj.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:02:52.253Z +dependencies: + - target: is-01kgzyrj4rhv2kjncymrzf01br + type: blocks +id: is-01kgzyretysjtans3brggwcqcj +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Implement doc cache clearing on migration or source config change +type: is +updated_at: 2026-02-09T01:33:37.814Z +version: 3 +--- +Implement doc cache clearing on migration or source config change. getSourcesHash() computes SHA256 of sources array, stores in .tbd/docs/.sources-hash. On f03→f04 migration or hash mismatch: rm -rf .tbd/docs/, trigger fresh sync, write new hash. Safe because .tbd/docs/ is gitignored and regenerable. Add to migrateToLatest() flow and syncDocsWithDefaults(). Tests: verify cache cleared on format change, cleared on source add/remove, NOT cleared when sources unchanged. diff --git a/.tbd/workspaces/outbox/issues/is-01kgzyrj4rhv2kjncymrzf01br.md b/.tbd/workspaces/outbox/issues/is-01kgzyrj4rhv2kjncymrzf01br.md new file mode 100644 index 00000000..d0d2e6b7 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzyrj4rhv2kjncymrzf01br.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:02:55.639Z +dependencies: [] +id: is-01kgzyrj4rhv2kjncymrzf01br +kind: task +labels: [] +parent_id: is-01kgzypm020x8n0jgadn5g3v7x +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Integration checkpoint: test sync against Speculate tbd branch" +type: is +updated_at: 2026-02-09T01:32:48.366Z +version: 2 +--- +Integration checkpoint: test full sync cycle against Speculate tbd branch. Run sync-repos.sh to clone jlevy/speculate (tbd branch) to repos/speculate/. Configure local tbd with spec source pointing to local repo (file:// URL or direct path). Run tbd sync --docs. Verify: files land in .tbd/docs/spec/{type}/{name}.md, all expected doc types present, content matches source. Test with jlevy/rust-porting-playbook as secondary source. Document any issues found. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00hr6eq3p16ebr73y7cxk1.md b/.tbd/workspaces/outbox/issues/is-01kh00hr6eq3p16ebr73y7cxk1.md new file mode 100644 index 00000000..e9b53b12 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00hr6eq3p16ebr73y7cxk1.md @@ -0,0 +1,26 @@ +--- +child_order_hints: + - is-01kh00j5n25sdsppd2qnv6ver4 + - is-01kh00jd80wve8jdkxn4r16vcx + - is-01kh00jm2mreweej0h9v929ae2 + - is-01kh00jvdscyj8x10n206yq9tr + - is-01kh00k2cdef3ednq6q01yn6zr + - is-01kh00ka8f786r7zxdgpg8sav1 + - is-01kh00kkcad26zfrxa83nn9fmr +created_at: 2026-02-09T01:34:09.613Z +dependencies: + - target: is-01kh00nprzwe2hx8t0qbatyvqb + type: blocks +id: is-01kh00hr6eq3p16ebr73y7cxk1 +kind: task +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 2: Prefix System and Lookup" +type: is +updated_at: 2026-02-09T01:43:28.323Z +version: 9 +--- +Implement prefix-based lookup in DocCache, update tbd setup for default sources with prefixes, add hidden source support, update --list output to show prefix when relevant, add progress indicators for repo checkout, implement error handling and recovery. Integration checkpoint: full setup + sync cycle against Speculate tbd branch. Multi-source test with rust-porting-playbook. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00j5n25sdsppd2qnv6ver4.md b/.tbd/workspaces/outbox/issues/is-01kh00j5n25sdsppd2qnv6ver4.md new file mode 100644 index 00000000..43923776 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00j5n25sdsppd2qnv6ver4.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:34:23.393Z +dependencies: + - target: is-01kh00jd80wve8jdkxn4r16vcx + type: blocks +id: is-01kh00j5n25sdsppd2qnv6ver4 +kind: task +labels: [] +parent_id: is-01kh00hr6eq3p16ebr73y7cxk1 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED+GREEN: Implement parseQualifiedName() utility with tests" +type: is +updated_at: 2026-02-09T01:35:26.500Z +version: 2 +--- +Create parseQualifiedName(name: string): { prefix?: string; baseName: string } utility. Parse 'spec:typescript-rules' → { prefix: 'spec', baseName: 'typescript-rules' }. Unqualified 'typescript-rules' → { baseName: 'typescript-rules' }. Handle edge cases: no colon, colon at start, multiple colons. TDD: write tests first covering all cases, then implement. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00jd80wve8jdkxn4r16vcx.md b/.tbd/workspaces/outbox/issues/is-01kh00jd80wve8jdkxn4r16vcx.md new file mode 100644 index 00000000..060fb8dc --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00jd80wve8jdkxn4r16vcx.md @@ -0,0 +1,22 @@ +--- +created_at: 2026-02-09T01:34:31.167Z +dependencies: + - target: is-01kh00jm2mreweej0h9v929ae2 + type: blocks + - target: is-01kh00jvdscyj8x10n206yq9tr + type: blocks + - target: is-01kh00k2cdef3ednq6q01yn6zr + type: blocks +id: is-01kh00jd80wve8jdkxn4r16vcx +kind: task +labels: [] +parent_id: is-01kh00hr6eq3p16ebr73y7cxk1 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "RED+GREEN: Update DocCache for prefix-based loading and lookup" +type: is +updated_at: 2026-02-09T01:35:40.394Z +version: 4 +--- +Rewrite DocCache constructor to accept (docsDir, sources: DocsSource[], docType: DocTypeName). Load scans .tbd/docs/{prefix}/{type}/ for each source in order. Add prefix field to CachedDoc. get() uses parseQualifiedName: qualified → direct prefix lookup, unqualified → search all prefixes in config order, throw AmbiguousLookupError if multiple matches. list() respects hidden flag. fuzzySearch updated for prefix awareness. TDD. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00jm2mreweej0h9v929ae2.md b/.tbd/workspaces/outbox/issues/is-01kh00jm2mreweej0h9v929ae2.md new file mode 100644 index 00000000..cf04b515 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00jm2mreweej0h9v929ae2.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:34:38.163Z +dependencies: + - target: is-01kh00kkcad26zfrxa83nn9fmr + type: blocks +id: is-01kh00jm2mreweej0h9v929ae2 +kind: task +labels: [] +parent_id: is-01kh00hr6eq3p16ebr73y7cxk1 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Update tbd setup --auto to configure default sources with prefixes +type: is +updated_at: 2026-02-09T01:35:43.748Z +version: 2 +--- +Update setup.ts to write default sources array: sys (internal, hidden, shortcuts), tbd (internal, shortcuts), spec (repo, github.com/jlevy/speculate, main, all types). Run migration if config is f03. Add repo-cache/ to .tbd/.gitignore. Call syncDocsWithDefaults() with new source-based logic. Test: fresh setup produces correct config.yml with sources array. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00jvdscyj8x10n206yq9tr.md b/.tbd/workspaces/outbox/issues/is-01kh00jvdscyj8x10n206yq9tr.md new file mode 100644 index 00000000..2f1cf31e --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00jvdscyj8x10n206yq9tr.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:34:45.688Z +dependencies: + - target: is-01kh00kkcad26zfrxa83nn9fmr + type: blocks +id: is-01kh00jvdscyj8x10n206yq9tr +kind: task +labels: [] +parent_id: is-01kh00hr6eq3p16ebr73y7cxk1 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Update --list output to show prefix when relevant +type: is +updated_at: 2026-02-09T01:35:47.261Z +version: 2 +--- +Update DocCommandHandler handleList to show prefix in parentheses after name when: name exists in multiple sources OR doc comes from non-default source. Format: 'typescript-rules (spec) 12.3 KB / ~3.5K tokens'. Hidden sources excluded from --list. JSON output includes prefix field. Update guidelines, template commands to use new format. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00k2cdef3ednq6q01yn6zr.md b/.tbd/workspaces/outbox/issues/is-01kh00k2cdef3ednq6q01yn6zr.md new file mode 100644 index 00000000..5f7a19ef --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00k2cdef3ednq6q01yn6zr.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:34:52.812Z +dependencies: + - target: is-01kh00kkcad26zfrxa83nn9fmr + type: blocks +id: is-01kh00k2cdef3ednq6q01yn6zr +kind: task +labels: [] +parent_id: is-01kh00hr6eq3p16ebr73y7cxk1 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Add progress indicators and error handling for repo checkout +type: is +updated_at: 2026-02-09T01:35:50.722Z +version: 2 +--- +Add progress output during repo checkout: 'Cloning spec (github.com/jlevy/speculate)...', 'Updating spec...'. Error handling: network errors → warn and skip (use cache if available), invalid URL → error at config parse time with format examples, missing ref → run git ls-remote and suggest alternatives, auth required → suggest gh auth login or SSH keys. Source removed from config → files deleted on next sync (hash change triggers clear). diff --git a/.tbd/workspaces/outbox/issues/is-01kh00ka8f786r7zxdgpg8sav1.md b/.tbd/workspaces/outbox/issues/is-01kh00ka8f786r7zxdgpg8sav1.md new file mode 100644 index 00000000..599be722 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00ka8f786r7zxdgpg8sav1.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:35:00.878Z +dependencies: + - target: is-01kh00jd80wve8jdkxn4r16vcx + type: blocks +id: is-01kh00ka8f786r7zxdgpg8sav1 +kind: task +labels: [] +parent_id: is-01kh00hr6eq3p16ebr73y7cxk1 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Implement AmbiguousLookupError with clear messaging +type: is +updated_at: 2026-02-09T01:35:30.009Z +version: 2 +--- +Create AmbiguousLookupError class for when unqualified name matches docs in multiple sources. Error message: 'typescript-rules matches docs in multiple sources: spec:typescript-rules (spec/guidelines/typescript-rules.md), myorg:typescript-rules (myorg/guidelines/typescript-rules.md). Use a qualified name: tbd guidelines spec:typescript-rules'. Tests for error formatting with 2 and 3+ matches. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00kkcad26zfrxa83nn9fmr.md b/.tbd/workspaces/outbox/issues/is-01kh00kkcad26zfrxa83nn9fmr.md new file mode 100644 index 00000000..b0530c4a --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00kkcad26zfrxa83nn9fmr.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:35:10.216Z +dependencies: [] +id: is-01kh00kkcad26zfrxa83nn9fmr +kind: task +labels: [] +parent_id: is-01kh00hr6eq3p16ebr73y7cxk1 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Integration checkpoint: full setup + sync + multi-source test" +type: is +updated_at: 2026-02-09T01:35:10.216Z +version: 1 +--- +End-to-end test: tbd setup --auto with default sources → tbd sync --docs → verify prefix directories created correctly (.tbd/docs/sys/shortcuts/, .tbd/docs/tbd/shortcuts/, .tbd/docs/spec/guidelines/). Add rust-porting-playbook as secondary source with prefix rpp. Verify both sources sync without collision. Test unqualified and qualified lookups. Verify --list shows prefixes. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00nprzwe2hx8t0qbatyvqb.md b/.tbd/workspaces/outbox/issues/is-01kh00nprzwe2hx8t0qbatyvqb.md new file mode 100644 index 00000000..8dd4c247 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00nprzwe2hx8t0qbatyvqb.md @@ -0,0 +1,26 @@ +--- +child_order_hints: + - is-01kh00pkpydj4jb8sfep3r0v7s + - is-01kh00pq2ms8emj422bbe7xmtw + - is-01kh00pthqe72f5qbsxj7c0y6y + - is-01kh00pxxjrme57t7831vsf6yh + - is-01kh00q1719mv8qg6aznscyx1t +created_at: 2026-02-09T01:36:19.230Z +dependencies: + - target: is-01kh00r1v5stt6jeww4x7bq1pz + type: blocks + - target: is-01kh00tjaz5n4x0jdeqzgbnaq8 + type: blocks +id: is-01kh00nprzwe2hx8t0qbatyvqb +kind: task +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 3: New Reference Type and CLI" +type: is +updated_at: 2026-02-09T01:43:35.495Z +version: 8 +--- +Add reference as fourth doc type. Create tbd reference command following DocCommandHandler pattern. Simplify existing commands (shortcut, guidelines, template) to use doc-types registry for path derivation. Remove hardcoded path constants from paths.ts. Add tbd doctor checks for repo cache health. Update doc-add.ts for prefix-based storage. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00pkpydj4jb8sfep3r0v7s.md b/.tbd/workspaces/outbox/issues/is-01kh00pkpydj4jb8sfep3r0v7s.md new file mode 100644 index 00000000..7453ae72 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00pkpydj4jb8sfep3r0v7s.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:36:48.861Z +dependencies: [] +id: is-01kh00pkpydj4jb8sfep3r0v7s +kind: task +labels: [] +parent_id: is-01kh00nprzwe2hx8t0qbatyvqb +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Create tbd reference command (extends DocCommandHandler) +type: is +updated_at: 2026-02-09T01:36:48.861Z +version: 1 +--- +Create src/cli/commands/reference.ts following guidelines.ts pattern. ReferenceHandler extends DocCommandHandler with typeName='reference', typeNamePlural='references', docType='reference'. Same options: --list, --all, --add, --name, query argument. Register in cli.ts: program.addCommand(referenceCommand). Tests: --list, exact lookup, fuzzy search, --add, JSON output. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00pq2ms8emj422bbe7xmtw.md b/.tbd/workspaces/outbox/issues/is-01kh00pq2ms8emj422bbe7xmtw.md new file mode 100644 index 00000000..d72095a6 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00pq2ms8emj422bbe7xmtw.md @@ -0,0 +1,22 @@ +--- +created_at: 2026-02-09T01:36:52.307Z +dependencies: + - target: is-01kh00pkpydj4jb8sfep3r0v7s + type: blocks + - target: is-01kh00pthqe72f5qbsxj7c0y6y + type: blocks + - target: is-01kh00q1719mv8qg6aznscyx1t + type: blocks +id: is-01kh00pq2ms8emj422bbe7xmtw +kind: task +labels: [] +parent_id: is-01kh00nprzwe2hx8t0qbatyvqb +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Simplify doc commands to derive paths from doc-types registry +type: is +updated_at: 2026-02-09T01:37:22.904Z +version: 4 +--- +Create getDocPaths(sources, docType, docsDir) utility that derives search paths from sources array and doc-types registry instead of hardcoded DEFAULT_*_PATHS constants. Update DocCommandHandler to use getDocPaths(). Update guidelines.ts, template.ts, shortcut.ts to pass docType from registry. Remove DEFAULT_SHORTCUT_PATHS, DEFAULT_GUIDELINES_PATHS, DEFAULT_TEMPLATE_PATHS from paths.ts. Add DEFAULT_REFERENCE_PATHS temporarily if needed for migration. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00pthqe72f5qbsxj7c0y6y.md b/.tbd/workspaces/outbox/issues/is-01kh00pthqe72f5qbsxj7c0y6y.md new file mode 100644 index 00000000..7d0feb64 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00pthqe72f5qbsxj7c0y6y.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:36:55.862Z +dependencies: [] +id: is-01kh00pthqe72f5qbsxj7c0y6y +kind: task +labels: [] +parent_id: is-01kh00nprzwe2hx8t0qbatyvqb +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Update doc-add.ts for prefix-based storage +type: is +updated_at: 2026-02-09T01:36:55.862Z +version: 1 +--- +Update addDoc() to write --add files to docs_cache.files as overrides (highest precedence, bypass prefix system). Change destination from shortcuts/custom/foo.md to flat {type}/foo.md. files: entries are written directly to .tbd/docs/{path} outside prefix directories. Update getDocTypeSubdir() to return flat type directory. Tests: verify --add writes to files section, verify precedence over source-provided docs with same name. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00pxxjrme57t7831vsf6yh.md b/.tbd/workspaces/outbox/issues/is-01kh00pxxjrme57t7831vsf6yh.md new file mode 100644 index 00000000..6c14fd15 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00pxxjrme57t7831vsf6yh.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:36:59.313Z +dependencies: [] +id: is-01kh00pxxjrme57t7831vsf6yh +kind: task +labels: [] +parent_id: is-01kh00nprzwe2hx8t0qbatyvqb +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Add tbd doctor checks for repo cache health +type: is +updated_at: 2026-02-09T01:36:59.313Z +version: 1 +--- +Add checkRepoCacheHealth() to doctor command. For each type:repo source: check if cache dir exists (.tbd/repo-cache/{slug}/), check if git repo is valid (git status), warn if missing with 'run tbd sync --docs to populate'. Report corrupted cache with 'delete and re-sync' suggestion. Also check for orphaned cache dirs (source removed from config). Tests with mock repo cache dirs. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00q1719mv8qg6aznscyx1t.md b/.tbd/workspaces/outbox/issues/is-01kh00q1719mv8qg6aznscyx1t.md new file mode 100644 index 00000000..992e0276 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00q1719mv8qg6aznscyx1t.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:37:02.687Z +dependencies: [] +id: is-01kh00q1719mv8qg6aznscyx1t +kind: task +labels: [] +parent_id: is-01kh00nprzwe2hx8t0qbatyvqb +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Remove hardcoded path constants, unify with doc-types registry +type: is +updated_at: 2026-02-09T01:37:02.687Z +version: 1 +--- +Remove or deprecate DEFAULT_SHORTCUT_PATHS, DEFAULT_GUIDELINES_PATHS, DEFAULT_TEMPLATE_PATHS from paths.ts. Replace all usages with doc-types registry-derived paths. Ensure TBD_DOCS_DIR and other core constants remain. Clean up any dead code from paths.ts. Verify no references to old shortcuts/system/ or shortcuts/standard/ paths remain outside of migration code. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00r1v5stt6jeww4x7bq1pz.md b/.tbd/workspaces/outbox/issues/is-01kh00r1v5stt6jeww4x7bq1pz.md new file mode 100644 index 00000000..0650709a --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00r1v5stt6jeww4x7bq1pz.md @@ -0,0 +1,24 @@ +--- +child_order_hints: + - is-01kh00rvs4a8gnhzkpp94adv4t + - is-01kh00rz1e17qtww14k7xcxnwq + - is-01kh00s27m7ynb0752j81gq3xj + - is-01kh00s5ejcp46p29hvqxrjb9d + - is-01kh00s8p2fhah7a9g816ctmq4 +created_at: 2026-02-09T01:37:36.099Z +dependencies: + - target: is-01kh00tjaz5n4x0jdeqzgbnaq8 + type: blocks +id: is-01kh00r1v5stt6jeww4x7bq1pz +kind: task +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 3b: Documentation Update" +type: is +updated_at: 2026-02-09T01:43:39.073Z +version: 7 +--- +Update all tbd documentation to reflect the new prefix-based architecture, external repo sources, and tbd reference command. Update development docs, docs overview, skill files, shortcuts that reference doc paths, and add migration guide. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00rvs4a8gnhzkpp94adv4t.md b/.tbd/workspaces/outbox/issues/is-01kh00rvs4a8gnhzkpp94adv4t.md new file mode 100644 index 00000000..2a0aa159 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00rvs4a8gnhzkpp94adv4t.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:38:02.659Z +dependencies: [] +id: is-01kh00rvs4a8gnhzkpp94adv4t +kind: task +labels: [] +parent_id: is-01kh00r1v5stt6jeww4x7bq1pz +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Update docs/development.md with external doc sources and test setup +type: is +updated_at: 2026-02-09T01:38:02.659Z +version: 1 +--- +Add section on external doc sources architecture, sync-repos.sh usage, test repo setup workflow, how to develop with local repo checkouts. Document prefix system, source types (internal vs repo), and how to add custom sources. Update any existing references to old doc paths. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00rz1e17qtww14k7xcxnwq.md b/.tbd/workspaces/outbox/issues/is-01kh00rz1e17qtww14k7xcxnwq.md new file mode 100644 index 00000000..4c023b1e --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00rz1e17qtww14k7xcxnwq.md @@ -0,0 +1,20 @@ +--- +created_at: 2026-02-09T01:38:05.997Z +dependencies: + - target: is-01kh00s27m7ynb0752j81gq3xj + type: blocks + - target: is-01kh00s8p2fhah7a9g816ctmq4 + type: blocks +id: is-01kh00rz1e17qtww14k7xcxnwq +kind: task +labels: [] +parent_id: is-01kh00r1v5stt6jeww4x7bq1pz +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Update docs/docs-overview.md with prefix system +type: is +updated_at: 2026-02-09T01:38:33.346Z +version: 3 +--- +Replace current doc layout description with prefix-based structure (.tbd/docs/{prefix}/{type}/{name}.md). Document tbd reference command. Update --add documentation for new flat destination paths. Document qualified lookup syntax (prefix:name). Update source type descriptions (internal, repo, files overrides). diff --git a/.tbd/workspaces/outbox/issues/is-01kh00s27m7ynb0752j81gq3xj.md b/.tbd/workspaces/outbox/issues/is-01kh00s27m7ynb0752j81gq3xj.md new file mode 100644 index 00000000..2c629aeb --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00s27m7ynb0752j81gq3xj.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:38:09.267Z +dependencies: [] +id: is-01kh00s27m7ynb0752j81gq3xj +kind: task +labels: [] +parent_id: is-01kh00r1v5stt6jeww4x7bq1pz +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Update skill.md and skill-brief.md with tbd reference command +type: is +updated_at: 2026-02-09T01:38:09.267Z +version: 1 +--- +Add tbd reference to command directory tables in skill.md and skill-brief.md. Update any doc path references from shortcuts/standard/ to tbd/shortcuts/ etc. Update generateShortcutDirectory() in doc-cache.ts to include references in the directory table. Verify skill file content matches new architecture. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00s5ejcp46p29hvqxrjb9d.md b/.tbd/workspaces/outbox/issues/is-01kh00s5ejcp46p29hvqxrjb9d.md new file mode 100644 index 00000000..ee67a7e0 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00s5ejcp46p29hvqxrjb9d.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:38:12.561Z +dependencies: [] +id: is-01kh00s5ejcp46p29hvqxrjb9d +kind: task +labels: [] +parent_id: is-01kh00r1v5stt6jeww4x7bq1pz +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Audit and update shortcuts that reference doc paths +type: is +updated_at: 2026-02-09T01:38:12.561Z +version: 1 +--- +Audit shortcuts for references to old paths (.tbd/docs/shortcuts/standard/, .tbd/docs/shortcuts/system/, etc.). Update new-shortcut.md, new-guideline.md, welcome-user.md and any others found. Replace with prefix-based paths. Verify all shortcut content is accurate for new architecture. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00s8p2fhah7a9g816ctmq4.md b/.tbd/workspaces/outbox/issues/is-01kh00s8p2fhah7a9g816ctmq4.md new file mode 100644 index 00000000..85255911 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00s8p2fhah7a9g816ctmq4.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:38:15.873Z +dependencies: [] +id: is-01kh00s8p2fhah7a9g816ctmq4 +kind: task +labels: [] +parent_id: is-01kh00r1v5stt6jeww4x7bq1pz +priority: 3 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Add migration guide for users with custom doc configs +type: is +updated_at: 2026-02-09T01:38:15.873Z +version: 1 +--- +Create migration guide documenting: f03→f04 format change, what happens to existing configs (auto-migration), how custom files: entries are preserved as overrides, how to add custom repo sources, prefix system overview. Can be a section in docs-overview.md or a standalone doc. Keep concise — most users get auto-migrated transparently. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00tjaz5n4x0jdeqzgbnaq8.md b/.tbd/workspaces/outbox/issues/is-01kh00tjaz5n4x0jdeqzgbnaq8.md new file mode 100644 index 00000000..192dfc48 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00tjaz5n4x0jdeqzgbnaq8.md @@ -0,0 +1,23 @@ +--- +child_order_hints: + - is-01kh00v81phdkqk71ac05rnvq8 + - is-01kh00vbepnwcryhwjx2n7jetb + - is-01kh00vevt9rq4d4r4mz7dscdd + - is-01kh00vj273rt2j0hkhx9r3egj +created_at: 2026-02-09T01:38:58.526Z +dependencies: + - target: is-01kh00wq09fmchfvm0c8c2s2gg + type: blocks +id: is-01kh00tjaz5n4x0jdeqzgbnaq8 +kind: task +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 4: Validation" +type: is +updated_at: 2026-02-09T01:43:42.649Z +version: 6 +--- +Verify refactored system produces identical output to current release. Create validation script comparing every shortcut, guideline, template, and reference output between npx get-tbd@latest (baseline) and local dev build with Speculate source (test). Document intentional differences, fix unintentional ones. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00v81phdkqk71ac05rnvq8.md b/.tbd/workspaces/outbox/issues/is-01kh00v81phdkqk71ac05rnvq8.md new file mode 100644 index 00000000..92e959ab --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00v81phdkqk71ac05rnvq8.md @@ -0,0 +1,20 @@ +--- +created_at: 2026-02-09T01:39:20.756Z +dependencies: + - target: is-01kh00vbepnwcryhwjx2n7jetb + type: blocks + - target: is-01kh00vevt9rq4d4r4mz7dscdd + type: blocks +id: is-01kh00v81phdkqk71ac05rnvq8 +kind: task +labels: [] +parent_id: is-01kh00tjaz5n4x0jdeqzgbnaq8 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Create validate-docs.sh comparison script +type: is +updated_at: 2026-02-09T01:39:47.458Z +version: 3 +--- +Create validate-docs.sh script that compares output between released (npx --yes get-tbd@latest) and dev build (node packages/tbd/dist/bin.mjs). For each doc type: get --list --json, iterate names, compare output of exact lookup. Report MATCH/DIFF/NOT_FOUND for each. Use diff for showing differences. Expected intentional differences: references section is new, content improvements from tbd→Speculate copy, prefix info in --list. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00vbepnwcryhwjx2n7jetb.md b/.tbd/workspaces/outbox/issues/is-01kh00vbepnwcryhwjx2n7jetb.md new file mode 100644 index 00000000..b366a28c --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00vbepnwcryhwjx2n7jetb.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:39:24.245Z +dependencies: + - target: is-01kh00vj273rt2j0hkhx9r3egj + type: blocks +id: is-01kh00vbepnwcryhwjx2n7jetb +kind: task +labels: [] +parent_id: is-01kh00tjaz5n4x0jdeqzgbnaq8 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Run validation for all shortcuts and guidelines +type: is +updated_at: 2026-02-09T01:39:50.940Z +version: 2 +--- +Execute validate-docs.sh for shortcuts and guidelines. Compare: tbd shortcut --list output, each shortcut content, tbd guidelines --list, each guideline content. All outputs should match baseline or be documented as intentional improvements. Fix any unintentional differences found. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00vevt9rq4d4r4mz7dscdd.md b/.tbd/workspaces/outbox/issues/is-01kh00vevt9rq4d4r4mz7dscdd.md new file mode 100644 index 00000000..dd6dc7e7 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00vevt9rq4d4r4mz7dscdd.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:39:27.737Z +dependencies: + - target: is-01kh00vj273rt2j0hkhx9r3egj + type: blocks +id: is-01kh00vevt9rq4d4r4mz7dscdd +kind: task +labels: [] +parent_id: is-01kh00tjaz5n4x0jdeqzgbnaq8 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Run validation for templates and test reference command +type: is +updated_at: 2026-02-09T01:39:54.343Z +version: 2 +--- +Execute validate-docs.sh for templates. Compare: tbd template --list, each template content. Test new tbd reference command: --list, exact lookup, fuzzy search. References have no baseline (new feature) — verify content is correct and complete. Document all intentional differences in validation report. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00vj273rt2j0hkhx9r3egj.md b/.tbd/workspaces/outbox/issues/is-01kh00vj273rt2j0hkhx9r3egj.md new file mode 100644 index 00000000..1544e6a7 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00vj273rt2j0hkhx9r3egj.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:39:31.014Z +dependencies: [] +id: is-01kh00vj273rt2j0hkhx9r3egj +kind: task +labels: [] +parent_id: is-01kh00tjaz5n4x0jdeqzgbnaq8 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Fix unintentional differences and document intentional ones +type: is +updated_at: 2026-02-09T01:39:31.014Z +version: 1 +--- +Review validation report. Fix any unintentional differences in output (content changes, missing docs, wrong paths). Document intentional differences: new reference type, improved content from Speculate, prefix info in --list output. Ensure all fixes pass validation re-run. Update spec with validation results. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00wq09fmchfvm0c8c2s2gg.md b/.tbd/workspaces/outbox/issues/is-01kh00wq09fmchfvm0c8c2s2gg.md new file mode 100644 index 00000000..0f9c615b --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00wq09fmchfvm0c8c2s2gg.md @@ -0,0 +1,22 @@ +--- +child_order_hints: + - is-01kh00xfgz06jh7nz2s8nvzcrh + - is-01kh00xjycx5tpddt6nd9z6kdw + - is-01kh00xpcnxh2mge03ak5fq30m +created_at: 2026-02-09T01:40:08.840Z +dependencies: + - target: is-01kh00ywn96hz5rfvwm7bey6nw + type: blocks +id: is-01kh00wq09fmchfvm0c8c2s2gg +kind: task +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 4b: Fresh Install End-to-End Test" +type: is +updated_at: 2026-02-09T01:43:46.230Z +version: 5 +--- +Final validation with clean environment. Fresh test directory, run setup --auto with prefix, verify default sources sync to correct prefix directories, add secondary source, test unqualified and qualified lookups, verify --list shows prefixes. Must pass before Phase 5. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00xfgz06jh7nz2s8nvzcrh.md b/.tbd/workspaces/outbox/issues/is-01kh00xfgz06jh7nz2s8nvzcrh.md new file mode 100644 index 00000000..e2a9730c --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00xfgz06jh7nz2s8nvzcrh.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:40:33.950Z +dependencies: + - target: is-01kh00xjycx5tpddt6nd9z6kdw + type: blocks +id: is-01kh00xfgz06jh7nz2s8nvzcrh +kind: task +labels: [] +parent_id: is-01kh00wq09fmchfvm0c8c2s2gg +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Fresh install: setup --auto with default sources" +type: is +updated_at: 2026-02-09T01:40:53.919Z +version: 2 +--- +Create fresh test directory outside tbd repo. git init. Run local dev build: tbd setup --auto --prefix=test. Verify: config.yml has sources array with sys/tbd/spec, .tbd/.gitignore includes repo-cache/ and docs/, prefix directories created (.tbd/docs/sys/shortcuts/, .tbd/docs/tbd/shortcuts/, .tbd/docs/spec/guidelines/ etc). Verify content matches expected docs. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00xjycx5tpddt6nd9z6kdw.md b/.tbd/workspaces/outbox/issues/is-01kh00xjycx5tpddt6nd9z6kdw.md new file mode 100644 index 00000000..04c53c90 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00xjycx5tpddt6nd9z6kdw.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:40:37.451Z +dependencies: + - target: is-01kh00xpcnxh2mge03ak5fq30m + type: blocks +id: is-01kh00xjycx5tpddt6nd9z6kdw +kind: task +labels: [] +parent_id: is-01kh00wq09fmchfvm0c8c2s2gg +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Fresh install: add secondary source and test multi-source" +type: is +updated_at: 2026-02-09T01:40:57.376Z +version: 2 +--- +Manually add rust-porting-playbook as secondary source with prefix rpp in config.yml. Run tbd sync --docs. Verify: .tbd/docs/rpp/references/ populated, no collision with spec/ docs. Test: tbd reference --list shows rpp docs, qualified lookup tbd reference rpp:rust-porting-guide works. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00xpcnxh2mge03ak5fq30m.md b/.tbd/workspaces/outbox/issues/is-01kh00xpcnxh2mge03ak5fq30m.md new file mode 100644 index 00000000..5ba53acf --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00xpcnxh2mge03ak5fq30m.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:40:40.979Z +dependencies: [] +id: is-01kh00xpcnxh2mge03ak5fq30m +kind: task +labels: [] +parent_id: is-01kh00wq09fmchfvm0c8c2s2gg +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Fresh install: test qualified and unqualified lookups" +type: is +updated_at: 2026-02-09T01:40:40.979Z +version: 1 +--- +Test lookup scenarios: unqualified 'tbd guidelines typescript-rules' returns spec source, qualified 'tbd guidelines spec:typescript-rules' works, 'tbd shortcut code-review-and-commit' returns tbd source, 'tbd reference --list' shows reference docs, ambiguity detection works if applicable. Verify --list output shows prefixes when relevant. Clean up test directory. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00ywn96hz5rfvwm7bey6nw.md b/.tbd/workspaces/outbox/issues/is-01kh00ywn96hz5rfvwm7bey6nw.md new file mode 100644 index 00000000..7e5458bb --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00ywn96hz5rfvwm7bey6nw.md @@ -0,0 +1,23 @@ +--- +child_order_hints: + - is-01kh00zv0qg7rsewwsr5dp16xb + - is-01kh00zyntcb780kzhhyzfx8ay + - is-01kh0102a0va61xzn1h38vxe5a + - is-01kh0105ym45bcdm1c4dms2stc + - is-01kh0109h8rey5ka2f91jeeqg3 + - is-01kh010d36t135fhyg9vj2yczj +created_at: 2026-02-09T01:41:20.168Z +dependencies: [] +id: is-01kh00ywn96hz5rfvwm7bey6nw +kind: task +labels: [] +parent_id: is-01kgzyh3ph1pfngcvyab02nhe9 +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Phase 5: Speculate Migration (Finalize)" +type: is +updated_at: 2026-02-09T01:42:09.765Z +version: 7 +--- +Finalize migration: make jlevy/speculate the upstream repo for general-purpose docs. Merge Speculate tbd branch → main. Update tbd default config to use Speculate main (ref: main). Restructure packages/tbd/docs/ to prefix-based layout. Remove general docs that now come from Speculate. Release new tbd version with prefix-based sources. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00zv0qg7rsewwsr5dp16xb.md b/.tbd/workspaces/outbox/issues/is-01kh00zv0qg7rsewwsr5dp16xb.md new file mode 100644 index 00000000..7adb60ad --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00zv0qg7rsewwsr5dp16xb.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:41:51.253Z +dependencies: + - target: is-01kh00zyntcb780kzhhyzfx8ay + type: blocks +id: is-01kh00zv0qg7rsewwsr5dp16xb +kind: task +labels: [] +parent_id: is-01kh00ywn96hz5rfvwm7bey6nw +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Audit all tbd docs and classify by prefix +type: is +updated_at: 2026-02-09T01:42:25.200Z +version: 2 +--- +Audit every doc in packages/tbd/docs/. Classify: sys (system shortcuts: skill.md, skill-brief.md, shortcut-explanation.md), tbd (tbd-specific: 24 shortcuts that reference tbd commands, tbd-sync-troubleshooting.md), spec (general-purpose: ~5 shortcuts, all 26 guidelines, all templates). Create a classification table. Verify classification matches spec Issue 11 findings. diff --git a/.tbd/workspaces/outbox/issues/is-01kh00zyntcb780kzhhyzfx8ay.md b/.tbd/workspaces/outbox/issues/is-01kh00zyntcb780kzhhyzfx8ay.md new file mode 100644 index 00000000..b878d79d --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh00zyntcb780kzhhyzfx8ay.md @@ -0,0 +1,20 @@ +--- +created_at: 2026-02-09T01:41:55.001Z +dependencies: + - target: is-01kh0102a0va61xzn1h38vxe5a + type: blocks + - target: is-01kh0109h8rey5ka2f91jeeqg3 + type: blocks +id: is-01kh00zyntcb780kzhhyzfx8ay +kind: task +labels: [] +parent_id: is-01kh00ywn96hz5rfvwm7bey6nw +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Merge Speculate tbd branch → main +type: is +updated_at: 2026-02-09T01:42:36.072Z +version: 3 +--- +Merge jlevy/speculate tbd branch into main. This makes the flat {type}/{name}.md structure the canonical layout. Resolve any merge conflicts (tbd branch was created in Phase 0 with restructured files). Push to main. Verify branch is merged at github.com/jlevy/speculate. diff --git a/.tbd/workspaces/outbox/issues/is-01kh0102a0va61xzn1h38vxe5a.md b/.tbd/workspaces/outbox/issues/is-01kh0102a0va61xzn1h38vxe5a.md new file mode 100644 index 00000000..7bf7a2b4 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh0102a0va61xzn1h38vxe5a.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:41:58.719Z +dependencies: + - target: is-01kh0105ym45bcdm1c4dms2stc + type: blocks +id: is-01kh0102a0va61xzn1h38vxe5a +kind: task +labels: [] +parent_id: is-01kh00ywn96hz5rfvwm7bey6nw +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: "Update tbd default config to use Speculate main (ref: main)" +type: is +updated_at: 2026-02-09T01:42:32.433Z +version: 2 +--- +Update getDefaultSources() to set spec source ref: main (was ref: tbd during development). Update any hardcoded refs in setup.ts, doc-sync.ts, migration code. Run tbd setup --auto to verify config writes ref: main for spec source. Test fresh sync pulls from main branch. diff --git a/.tbd/workspaces/outbox/issues/is-01kh0105ym45bcdm1c4dms2stc.md b/.tbd/workspaces/outbox/issues/is-01kh0105ym45bcdm1c4dms2stc.md new file mode 100644 index 00000000..ff568978 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh0105ym45bcdm1c4dms2stc.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:42:02.451Z +dependencies: + - target: is-01kh010d36t135fhyg9vj2yczj + type: blocks +id: is-01kh0105ym45bcdm1c4dms2stc +kind: task +labels: [] +parent_id: is-01kh00ywn96hz5rfvwm7bey6nw +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Remove general docs from tbd bundled set (now in Speculate) +type: is +updated_at: 2026-02-09T01:42:39.570Z +version: 2 +--- +Remove general-purpose docs from packages/tbd/docs/ that now come from Speculate via spec source. Remove: ~5 general shortcuts, all 26 guidelines (typescript-rules, python-rules, etc.), all templates. Keep: sys/ shortcuts, tbd/ shortcuts (24 tbd-specific ones), tbd-sync-troubleshooting.md. Verify tbd sync still provides all docs via spec source. diff --git a/.tbd/workspaces/outbox/issues/is-01kh0109h8rey5ka2f91jeeqg3.md b/.tbd/workspaces/outbox/issues/is-01kh0109h8rey5ka2f91jeeqg3.md new file mode 100644 index 00000000..9598299f --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh0109h8rey5ka2f91jeeqg3.md @@ -0,0 +1,18 @@ +--- +created_at: 2026-02-09T01:42:06.118Z +dependencies: + - target: is-01kh010d36t135fhyg9vj2yczj + type: blocks +id: is-01kh0109h8rey5ka2f91jeeqg3 +kind: task +labels: [] +parent_id: is-01kh00ywn96hz5rfvwm7bey6nw +priority: 3 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Update Speculate README with flat doc structure +type: is +updated_at: 2026-02-09T01:42:43.158Z +version: 2 +--- +Update jlevy/speculate README to document the flat {type}/{name}.md structure. Document: guidelines/ (rules and best practices), shortcuts/ (instruction templates), templates/ (document templates), references/ (reference docs). Note that these docs are consumed by tbd via the external repo source mechanism. diff --git a/.tbd/workspaces/outbox/issues/is-01kh010d36t135fhyg9vj2yczj.md b/.tbd/workspaces/outbox/issues/is-01kh010d36t135fhyg9vj2yczj.md new file mode 100644 index 00000000..f7646ec9 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kh010d36t135fhyg9vj2yczj.md @@ -0,0 +1,16 @@ +--- +created_at: 2026-02-09T01:42:09.765Z +dependencies: [] +id: is-01kh010d36t135fhyg9vj2yczj +kind: task +labels: [] +parent_id: is-01kh00ywn96hz5rfvwm7bey6nw +priority: 2 +spec_path: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +status: open +title: Release new tbd version with prefix-based sources +type: is +updated_at: 2026-02-09T01:42:09.765Z +version: 1 +--- +Prepare release: bump version in package.json, update CURRENT_FORMAT to f04, update FORMAT_HISTORY with correct introduced version. Run full test suite. Create release notes documenting: external repo sources, prefix-based namespacing, new tbd reference command, f03→f04 auto-migration. npm publish. Verify npx get-tbd@latest works with new sources. diff --git a/.tbd/workspaces/outbox/mappings/ids.yml b/.tbd/workspaces/outbox/mappings/ids.yml new file mode 100644 index 00000000..198b297a --- /dev/null +++ b/.tbd/workspaces/outbox/mappings/ids.yml @@ -0,0 +1,73 @@ +0bed: 01kgzxpbfpk8sf45cveezakydn +0c4o: 01kh00kkcad26zfrxa83nn9fmr +0fpx: 01kgzxfqrfxmt6pcdnw1t8vem6 +0pwa: 01kgzxetnqnvtf41cx6a988sy8 +1y1m: 01kgzykkgk2p3jstgr4bstm0d8 +2hip: 01kh00ka8f786r7zxdgpg8sav1 +3grz: 01kgzxqddjcngrpcyegkqm4bb2 +4npn: 01kgzxftzt59ardkfx9k3wj145 +4nz6: 01kgzyr12a16cqreyy5ekn0r2k +4onm: 01kh00k2cdef3ednq6q01yn6zr +4r02: 01kh0102a0va61xzn1h38vxe5a +4xj8: 01kh00wq09fmchfvm0c8c2s2gg +5m15: 01kh00s8p2fhah7a9g816ctmq4 +5ybv: 01kgzypm020x8n0jgadn5g3v7x +7lus: 01kh00xjycx5tpddt6nd9z6kdw +8txy: 01kh00vj273rt2j0hkhx9r3egj +97fe: 01kh00ywn96hz5rfvwm7bey6nw +9bwo: 01kh00xfgz06jh7nz2s8nvzcrh +9cmd: 01kgzyqt5czjjjhvakcfpe5q81 +aga3: 01kgzxjqv0d9b1qrh8sf5qncv0 +apb9: 01kgzyr7y128s080n05fpnm9de +ax39: 01kh00rvs4a8gnhzkpp94adv4t +b1j3: 01kh00jd80wve8jdkxn4r16vcx +bal5: 01kgzxmyvwg2dnnt751wnw2e9s +bfcu: 01kgzyqxkjmj2g4jpbhcegsnek +c1cd: 01kh00pthqe72f5qbsxj7c0y6y +c7xq: 01kh00xpcnxh2mge03ak5fq30m +d6qq: 01kgzykcjvkwqk6mc6rx2xbjah +d74a: 01kh0105ym45bcdm1c4dms2stc +d8eo: 01kh00pq2ms8emj422bbe7xmtw +di9c: 01kgzxj12dj31rfwh0xxftttmy +dzo5: 01kgzyr4fh728np85rprvp9s6e +eikw: 01kgzyj5y0xm00qdqsgay4vfv9 +ezsv: 01kgzyk2gaa8r9xmbb9axhftaq +f5qd: 01kh00q1719mv8qg6aznscyx1t +fmxo: 01kgzxfmfss9zfpj5abhgm2whz +fq7w: 01kgzyk94y5tx9y93cpmdxsr8c +fzf1: 01kh00pxxjrme57t7831vsf6yh +gr34: 01kh00j5n25sdsppd2qnv6ver4 +hcx4: 01kh00zv0qg7rsewwsr5dp16xb +hrbz: 01kgzxkyye3gaxrebyyqzh21w7 +iarz: 01kh00rz1e17qtww14k7xcxnwq +kqic: 01kgzxmvh2m0atzzcererq1j72 +kzeh: 01kgzxcx31b6kjdd9v8r3gt5e3 +l4ov: 01kh00s5ejcp46p29hvqxrjb9d +lifm: 01kgzxjv3trypg9djykjk7v3as +lvpg: 01kgzyqpk2hmkj9rwpgkpjsjbj +mdwh: 01kgzyh3ph1pfngcvyab02nhe9 +msj1: 01kgzxfy6t76xrk1mybbg5jger +mvi6: 01kh010d36t135fhyg9vj2yczj +mxvr: 01kh00zyntcb780kzhhyzfx8ay +n3zb: 01kh00hr6eq3p16ebr73y7cxk1 +nszx: 01kgzxqa3x20eqdcrv4jdfncsj +pj5q: 01kh00jm2mreweej0h9v929ae2 +pswl: 01kgzyretysjtans3brggwcqcj +pxis: 01kh00tjaz5n4x0jdeqzgbnaq8 +qhmo: 01kh00nprzwe2hx8t0qbatyvqb +qrga: 01kh0109h8rey5ka2f91jeeqg3 +r34n: 01kgzxq6s7wcczc1vrn9twsyf1 +rq6q: 01kh00r1v5stt6jeww4x7bq1pz +s72z: 01kh00s27m7ynb0752j81gq3xj +sek7: 01kh00vevt9rq4d4r4mz7dscdd +sfmk: 01kgzyrbcf260b1791a043ccpv +she8: 01kgzxe3p3qc7m2zxz0ga530vy +t7yt: 01kh00v81phdkqk71ac05rnvq8 +u182: 01kh00jvdscyj8x10n206yq9tr +v0sk: 01kgzxq3cswmbjqvn77m7gcb75 +wau7: 01kgzyrj4rhv2kjncymrzf01br +wylj: 01kh00pkpydj4jb8sfep3r0v7s +xfru: 01kgzxmr6nf5s2pb631jnmeht8 +xxj2: 01kgzyk5whe8q33z3zvqyq05bv +ycnl: 01kh00vbepnwcryhwjx2n7jetb +yuz9: 01kgzykg105ptpmjar5yyfz045 diff --git a/AGENTS.md b/AGENTS.md index 33f3200a..b2b6e024 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -193,6 +193,7 @@ or want help → run `tbd shortcut welcome-user` | `tbd guidelines ` | Load coding guidelines | | `tbd guidelines --list` | List guidelines | | `tbd template ` | Output a template | +| `tbd reference ` | Load a reference document | ## Quick Reference diff --git a/docs/development.md b/docs/development.md index 1c8d208e..6f0f72ad 100644 --- a/docs/development.md +++ b/docs/development.md @@ -51,7 +51,7 @@ The globally installed `tbd` has its own bundled docs from the published npm pac Running `tbd setup --auto` with the global installation won’t include your new files. ```bash -# 1. Add your new file to packages/tbd/docs/shortcuts/standard/my-shortcut.md +# 1. Add your new file to packages/tbd/docs/tbd/shortcuts/my-shortcut.md # 2. Build to bundle the new file into dist/docs/ pnpm build @@ -60,7 +60,7 @@ pnpm build node packages/tbd/dist/bin.mjs setup --auto # 4. Verify the file was copied to .tbd/docs/ -ls .tbd/docs/shortcuts/standard/my-shortcut.md +ls .tbd/docs/tbd/shortcuts/my-shortcut.md # 5. Test the shortcut with the local build node packages/tbd/dist/bin.mjs shortcut my-shortcut diff --git a/docs/docs-overview.md b/docs/docs-overview.md index 7fdf3b8d..5bac31f1 100644 --- a/docs/docs-overview.md +++ b/docs/docs-overview.md @@ -60,9 +60,13 @@ In addition to these repository docs, tbd provides built-in documentation via CL (typescript-rules, python-rules, general-tdd-guidelines, etc.) - `tbd template --list` / `tbd template ` — Document templates (plan-spec, research-brief, architecture) +- `tbd reference --list` / `tbd reference ` — Reference documents (API docs, data + models, etc.) These CLI-provided docs are installed locally in `.tbd/docs/` during `tbd setup --auto` and can be refreshed anytime by re-running setup. +Docs are organized in prefix-based directories: `.tbd/docs/{prefix}/{type}/` (e.g., +`.tbd/docs/sys/shortcuts/`, `.tbd/docs/tbd/guidelines/`). #### Adding external docs by URL @@ -72,10 +76,9 @@ You can register external documentation from any URL (including GitHub blob URLs tbd guidelines --add= --name= tbd shortcut --add= --name= tbd template --add= --name= +tbd reference --add= --name= ``` GitHub blob URLs are automatically converted to raw URLs. If direct fetch returns HTTP 403, the system falls back to `gh api` for authenticated -access. -User-added shortcuts are stored in `shortcuts/custom/` to keep them separate from -bundled docs. +access. User-added docs are stored in `.tbd/docs/{type}/` alongside bundled docs. diff --git a/docs/project/research/current/research-cli-as-agent-skill.md b/docs/project/research/current/research-cli-as-agent-skill.md index c14769e5..6aae4148 100644 --- a/docs/project/research/current/research-cli-as-agent-skill.md +++ b/docs/project/research/current/research-cli-as-agent-skill.md @@ -78,7 +78,7 @@ Patterns were validated through CI testing and actual agent usage. - tbd source code (`packages/tbd/src/cli/`) - Claude Code skill documentation (https://code.claude.com/docs/en/skills) - Agent Skills open standard (https://agentskills.io) -- skills.sh open ecosystem (https://skills.sh) - Vercel's skill discovery/installation +- skills.sh open ecosystem (https://skills.sh) - Vercel’s skill discovery/installation platform - Anthropic Skills repo (https://github.com/anthropics/skills) - Cursor IDE rules documentation (AGENTS.md support) @@ -1368,23 +1368,25 @@ defaults. **Details**: -The agent skills ecosystem has matured significantly since initial research. Three key -components now form the distribution infrastructure: +The agent skills ecosystem has matured significantly since initial research. +Three key components now form the distribution infrastructure: 1. **Agent Skills Open Standard** ([agentskills.io](https://agentskills.io)): Originally - developed by Anthropic, now adopted by 27+ agent products (Claude Code, Cursor, GitHub - Copilot, Codex, Gemini CLI, Windsurf, Goose, and others). Defines the SKILL.md format - with standardized frontmatter fields: `name`, `description`, `license`, - `compatibility`, `metadata`, `allowed-tools`. - -2. **skills.sh** ([skills.sh](https://skills.sh)): Vercel's open ecosystem for - discovering and installing skills. Functions as "npm for agents" with CLI: - `npx skills add `. Installs SKILL.md to `.agents/skills/` and symlinks to - agent-specific directories. Hosts a leaderboard with 47K+ installations. - -3. **Anthropic Skills Repo** ([github.com/anthropics/skills](https://github.com/anthropics/skills)): - Reference implementations (65K+ stars). Skills for document creation, creative - workflows, and technical tasks. + developed by Anthropic, now adopted by 27+ agent products (Claude Code, Cursor, + GitHub Copilot, Codex, Gemini CLI, Windsurf, Goose, and others). + Defines the SKILL.md format with standardized frontmatter fields: `name`, + `description`, `license`, `compatibility`, `metadata`, `allowed-tools`. + +2. **skills.sh** ([skills.sh](https://skills.sh)): Vercel’s open ecosystem for + discovering and installing skills. + Functions as “npm for agents” with CLI: `npx skills add `. Installs + SKILL.md to `.agents/skills/` and symlinks to agent-specific directories. + Hosts a leaderboard with 47K+ installations. + +3. **Anthropic Skills Repo** + ([github.com/anthropics/skills](https://github.com/anthropics/skills)): Reference + implementations (65K+ stars). + Skills for document creation, creative workflows, and technical tasks. **The Progressive Disclosure Hierarchy**: @@ -1397,9 +1399,9 @@ The Agent Skills spec formalizes the three-level progressive disclosure pattern: | Level 3 | Resources (`scripts/`, `references/`, `assets/`) | Unlimited | On demand only | **Implication for CLIs**: A CLI meta-skill provides Level 1-2 (the SKILL.md describing -the CLI's capabilities). The CLI's resource library (guidelines, shortcuts, templates) -provides Level 3 content on demand. This architecture aligns perfectly with the open -standard. +the CLI’s capabilities). +The CLI’s resource library (guidelines, shortcuts, templates) provides Level 3 content +on demand. This architecture aligns perfectly with the open standard. **skills.sh Installation Flow**: @@ -1451,7 +1453,7 @@ allowed-tools: Read Bash(git:*) # Optional: pre-approved tools --- ``` -**tbd's current doc frontmatter**: +**tbd’s current doc frontmatter**: ```yaml --- diff --git a/docs/project/research/current/research-skills-vs-meta-skill-architecture.md b/docs/project/research/current/research-skills-vs-meta-skill-architecture.md index b34a4faf..d3c6ee49 100644 --- a/docs/project/research/current/research-skills-vs-meta-skill-architecture.md +++ b/docs/project/research/current/research-skills-vs-meta-skill-architecture.md @@ -575,19 +575,21 @@ ecosystem for skill discovery and installation (`npx skills add `). become the primary distribution channel for Agent Skills, with 47K+ total installations across 27+ compatible agent products. -**Impact on tbd's architecture decision**: skills.sh validates the meta-skill approach. -skills.sh distributes SKILL.md files (Level 1-2 content), while tbd's CLI serves as an -on-demand Level 3 resource server. These are complementary layers: +**Impact on tbd’s architecture decision**: skills.sh validates the meta-skill approach. +skills.sh distributes SKILL.md files (Level 1-2 content), while tbd’s CLI serves as an +on-demand Level 3 resource server. +These are complementary layers: -- **skills.sh**: "How do I give my agent the ability to do X?" (capabilities) -- **tbd CLI**: "How do I give my agent knowledge about X?" (domain expertise) +- **skills.sh**: “How do I give my agent the ability to do X?” (capabilities) +- **tbd CLI**: “How do I give my agent knowledge about X?” (domain expertise) -tbd itself could be listed as a skill on skills.sh (for discovery), while `tbd source -add` handles the separate problem of distributing domain knowledge repos. +tbd itself could be listed as a skill on skills.sh (for discovery), while +`tbd source add` handles the separate problem of distributing domain knowledge repos. ### External Docs Repos Feature -The new [external docs repos spec](../../specs/active/plan-2026-02-02-external-docs-repos.md) +The new +[external docs repos spec](../../specs/active/plan-2026-02-02-external-docs-repos.md) extends the meta-skill architecture to support external knowledge sources: ```bash @@ -595,9 +597,9 @@ tbd source add github.com/jlevy/rust-porting-playbook # Adds domain expertise accessible via tbd guidelines X ``` -This is distinct from skills.sh distribution: skills.sh copies SKILL.md files once, while -`tbd source add` establishes ongoing git sync for evolving knowledge repos. See the spec -appendix for detailed comparison. +This is distinct from skills.sh distribution: skills.sh copies SKILL.md files once, +while `tbd source add` establishes ongoing git sync for evolving knowledge repos. +See the spec appendix for detailed comparison. * * * @@ -627,9 +629,10 @@ appendix for detailed comparison. discovery? This would make tbd discoverable via `npx skills add` while the actual functionality remains CLI-based. -7. **Doc repos as "knowledge skills"**: Could domain knowledge repos (like +7. **Doc repos as “knowledge skills”**: Could domain knowledge repos (like rust-porting-playbook) be listed on skills.sh with a SKILL.md that references - `tbd guidelines X` commands? This would bridge the two distribution models. + `tbd guidelines X` commands? + This would bridge the two distribution models. * * * diff --git a/docs/project/specs/active/plan-2026-02-02-external-docs-repos.md b/docs/project/specs/active/plan-2026-02-02-external-docs-repos.md index af844c30..d0be8208 100644 --- a/docs/project/specs/active/plan-2026-02-02-external-docs-repos.md +++ b/docs/project/specs/active/plan-2026-02-02-external-docs-repos.md @@ -4,10 +4,12 @@ description: Pull shortcuts, guidelines, templates, and references from external --- # Feature: External Docs Repos -**Date:** 2026-02-02 (last updated 2026-02-08) +**Date:** 2026-02-02 (last updated 2026-02-09, beads created for all phases) **Status:** Draft +**Epic:** `tbd-mdwh` — 8 phase epics, 60+ leaf beads with full dependency chains + ## Overview Enable tbd to pull documentation (shortcuts, guidelines, templates, references) from @@ -22,6 +24,8 @@ This allows: ## Goals - Allow configuring external git repos as doc sources +- Allow configuring local directories within the repo as doc sources (always-fresh via + stub pointers) - Support selective sync (only certain folders/types from a repo) - Maintain backward compatibility with existing `internal:` and URL sources - Make sync work seamlessly with repo sources (checkout on first sync, pull on @@ -139,6 +143,14 @@ docs_cache: # - guidelines/ # - references/ + # Optional: local docs from within this repo (always-fresh via stub pointers) + # - type: local + # prefix: local + # path: docs/tbd # relative to repo root + # paths: + # - shortcuts/ + # - guidelines/ + # Per-file overrides (highest precedence, applied after sources) # These bypass the prefix system - written directly to .tbd/docs/{path} files: @@ -810,10 +822,11 @@ function inferDocType(relativePath: string): DocTypeName | undefined { ```typescript // In schemas.ts export const DocsSourceSchema = z.object({ - type: z.enum(['internal', 'repo']), + type: z.enum(['internal', 'repo', 'local']), prefix: z.string().min(1).max(16).regex(/^[a-z0-9-]+$/), url: z.string().optional(), // Required for type: repo ref: z.string().optional(), // Defaults to 'main' for repos + path: z.string().optional(), // Required for type: local (repo-root-relative dir) paths: z.array(z.string()), hidden: z.boolean().optional(), // Exclude from --list }); @@ -949,7 +962,85 @@ Only Speculate ships as the default source. ## Implementation Plan -### Phase 0: Speculate Prep (Do First) +### Phase 0a: Prerequisite Fixes (Do First) + +Fix code issues that should be resolved before starting external sources work. +These are independent of the external docs design and reduce risk during the main +phases. + +#### 0a.1: Refactor `shortcut.ts` to use `DocCommandHandler` (Issue 1) + +Eliminate ~280 lines of duplicated code. +Prerequisite for prefix-aware loading since the shared base class is where prefix logic +will be added. + +**Beads** (TDD order, dependency chain): + +| Bead | Description | +| --- | --- | +| `tbd-she8` | Parent: 0a.1 Refactor shortcut.ts to use DocCommandHandler | +| `tbd-0pwa` | RED: Write characterization tests for shortcut command current behavior | +| `tbd-fmxo` | GREEN: Migrate ShortcutHandler to extend DocCommandHandler (blocked by 0pwa) | +| `tbd-0fpx` | GREEN: Move shortcut-specific behavior to DocCommandHandler overrides (blocked by fmxo) | +| `tbd-4npn` | REFACTOR: Remove duplicate code from shortcut.ts (blocked by 0fpx) | +| `tbd-msj1` | VERIFY: Run full test suite, confirm no regressions (blocked by 4npn) | + +**TDD approach:** + +1. **Red**: Write characterization tests capturing exact current behavior (--list, exact + lookup, fuzzy search, --category, --add, --refresh, no-query fallback, agent header, + shadowed entries, JSON output) +2. **Green**: Migrate `ShortcutHandler` to extend `DocCommandHandler`, mapping + `typeName='shortcut'`, + `excludeFromList=['skill','skill-brief','shortcut-explanation']`, + `noQueryDocName='shortcut-explanation'` +3. **Refactor**: Delete all duplicated methods (extractFallbackText, + printWrappedDescription, wrapAtWord, handleList, handleNoQuery, handleQuery) — file + shrinks from ~~380 → ~~80 lines +4. **Verify**: Full test suite passes, no regressions in guidelines/template commands + +#### 0a.2: Add `warnings` field to `MigrationResult` (Issue 3) + +The `MigrationResult` type in `tbd-format.ts` only has `changes: string[]`. The f03→f04 +migration needs `warnings: string[]` for reporting preserved custom file overrides. + +**Beads** (TDD order): + +| Bead | Description | +| --- | --- | +| `tbd-di9c` | Parent: 0a.2 Add warnings field to MigrationResult | +| `tbd-aga3` | RED: Write test for MigrationResult warnings field | +| `tbd-lifm` | GREEN: Add warnings: string[] to interface and existing functions (blocked by aga3) | + +#### 0a.3: Update `generateShortcutDirectory()` for `hidden` support (Issue 12) + +Currently hardcodes skip names (`skill`, `skill-brief`, `shortcut-explanation`). The +prefix system introduces `hidden` sources that should be excluded generically. + +**Beads** (TDD order): + +| Bead | Description | +| --- | --- | +| `tbd-hrbz` | Parent: 0a.3 Update generateShortcutDirectory() for hidden support | +| `tbd-xfru` | RED: Write tests for hidden doc filtering in generateShortcutDirectory() | +| `tbd-kqic` | GREEN: Add hidden field to CachedDoc and filter (blocked by xfru) | +| `tbd-bal5` | REFACTOR: Remove hardcoded skip names, use hidden field (blocked by kqic) | + +#### 0a.4: Establish test patterns for doc infrastructure (Issue 13) + +Set up reusable test fixtures and helpers for doc tests before the main implementation. + +**Beads** (dependency chain): + +| Bead | Description | +| --- | --- | +| `tbd-0bed` | Parent: 0a.4 Establish shared test fixtures and helpers | +| `tbd-v0sk` | Create tests/fixtures/test-docs/ with sample docs for each type | +| `tbd-r34n` | Create tests/helpers/doc-test-utils.ts with temp doc dir helpers (blocked by v0sk) | +| `tbd-nszx` | Add helper for creating local bare git repos (blocked by r34n) | +| `tbd-3grz` | Refactor existing doc-sync/doc-cache tests to use shared fixtures (blocked by r34n) | + +### Phase 0: Speculate Prep Prepare Speculate repo with tbd-compatible structure on a `tbd` branch. This enables end-to-end testing throughout development rather than a big-bang migration @@ -986,6 +1077,18 @@ Ensure `gh auth login` has been run in the environment. This branch becomes the test target for all integration testing in subsequent phases. The agent can iterate on the `tbd` branch as needed. +**Beads** (epic: `tbd-eikw`, parent: `tbd-mdwh`): + +| Bead | Description | +| --- | --- | +| `tbd-eikw` | Parent: Phase 0 Speculate Prep | +| `tbd-ezsv` | Clone jlevy/speculate and create tbd branch | +| `tbd-xxj2` | Restructure Speculate to flat doc type directories (blocked by ezsv) | +| `tbd-fq7w` | Update Speculate front matter and shortcut references (blocked by xxj2) | +| `tbd-d6qq` | Copy improved docs from tbd to Speculate tbd branch (blocked by fq7w) | +| `tbd-yuz9` | Push Speculate tbd branch and verify (blocked by d6qq) | +| `tbd-1y1m` | Create sync-repos.sh script and add repos/ to .gitignore | + ### Phase 1: Core Infrastructure - [ ] Create `doc-types.ts` registry (single source of truth for doc types) @@ -1001,6 +1104,21 @@ The agent can iterate on the `tbd` branch as needed. - [ ] Clear `.tbd/docs/` during migration (fresh sync after format or source changes) - [ ] **Integration checkpoint**: Test sync against Speculate `tbd` branch +**Beads** (epic: `tbd-5ybv`, parent: `tbd-mdwh`, blocked by Phase 0a): + +| Bead | Description | +| --- | --- | +| `tbd-5ybv` | Parent: Phase 1 Core Infrastructure | +| `tbd-lvpg` | RED+GREEN: Create doc-types.ts registry with unit tests | +| `tbd-9cmd` | RED+GREEN: Create repo-url.ts utility with unit tests | +| `tbd-bfcu` | RED+GREEN: Bump format f03→f04 with migration (blocked by tbd-di9c) | +| `tbd-4nz6` | Add DocsSourceSchema and update DocsCacheSchema in schemas.ts | +| `tbd-dzo5` | RED+GREEN: Implement RepoCache class for sparse git checkouts (blocked by 9cmd) | +| `tbd-apb9` | Restructure bundled docs to prefix-based layout (blocked by lvpg) | +| `tbd-sfmk` | Rewrite DocSync for prefix-based storage and source-based sync (blocked by dzo5, apb9, 4nz6, bfcu) | +| `tbd-pswl` | Implement doc cache clearing on migration or source config change (blocked by sfmk, bfcu) | +| `tbd-wau7` | Integration checkpoint: test sync against Speculate tbd branch (blocked by sfmk, pswl) | + ### Phase 2: Prefix System and Lookup - [ ] Update `DocCache` for prefix-based lookup: @@ -1016,6 +1134,19 @@ The agent can iterate on the `tbd` branch as needed. - [ ] **Integration checkpoint**: Full setup + sync cycle against Speculate `tbd` branch - [ ] **Multi-source test**: Add `rust-porting-playbook` as secondary source +**Beads** (epic: `tbd-n3zb`, parent: `tbd-mdwh`, blocked by Phase 1): + +| Bead | Description | +| --- | --- | +| `tbd-n3zb` | Parent: Phase 2 Prefix System and Lookup | +| `tbd-gr34` | RED+GREEN: Implement parseQualifiedName() utility with tests | +| `tbd-2hip` | Implement AmbiguousLookupError with clear messaging | +| `tbd-b1j3` | RED+GREEN: Update DocCache for prefix-based loading and lookup (blocked by gr34, 2hip) | +| `tbd-pj5q` | Update tbd setup --auto to configure default sources with prefixes (blocked by b1j3) | +| `tbd-u182` | Update --list output to show prefix when relevant (blocked by b1j3) | +| `tbd-4onm` | Add progress indicators and error handling for repo checkout (blocked by b1j3) | +| `tbd-0c4o` | Integration checkpoint: full setup + sync + multi-source test (blocked by pj5q, u182, 4onm) | + ### Phase 3: New Reference Type and CLI - [ ] Add `reference` to DOC_TYPES registry @@ -1025,6 +1156,17 @@ The agent can iterate on the `tbd` branch as needed. - [ ] Remove hardcoded path constants from `paths.ts` - [ ] Add `tbd doctor` checks for repo cache health +**Beads** (epic: `tbd-qhmo`, parent: `tbd-mdwh`, blocked by Phase 2): + +| Bead | Description | +| --- | --- | +| `tbd-qhmo` | Parent: Phase 3 New Reference Type and CLI | +| `tbd-d8eo` | Simplify doc commands to derive paths from doc-types registry | +| `tbd-wylj` | Create tbd reference command (extends DocCommandHandler) (blocked by d8eo) | +| `tbd-c1cd` | Update doc-add.ts for prefix-based storage (blocked by d8eo) | +| `tbd-f5qd` | Remove hardcoded path constants, unify with doc-types registry (blocked by d8eo) | +| `tbd-fzf1` | Add tbd doctor checks for repo cache health | + ### Phase 3b: Documentation Update Update all tbd documentation to reflect the new architecture: @@ -1037,6 +1179,17 @@ Update all tbd documentation to reflect the new architecture: - [ ] Update README if it references doc structure - [ ] Add migration guide for users with custom doc configs +**Beads** (epic: `tbd-rq6q`, parent: `tbd-mdwh`, blocked by Phase 3): + +| Bead | Description | +| --- | --- | +| `tbd-rq6q` | Parent: Phase 3b Documentation Update | +| `tbd-ax39` | Update docs/development.md with external doc sources and test setup | +| `tbd-iarz` | Update docs/docs-overview.md with prefix system | +| `tbd-s72z` | Update skill.md and skill-brief.md with tbd reference command (blocked by iarz) | +| `tbd-l4ov` | Audit and update shortcuts that reference doc paths | +| `tbd-5m15` | Add migration guide for users with custom doc configs (blocked by iarz) | + ### Phase 4: Validation Verify that the refactored system produces identical output to the current release. @@ -1052,6 +1205,16 @@ Verify that the refactored system produces identical output to the current relea - [ ] Document any intentional differences (e.g., improved content from tbd) - [ ] Fix any unintentional differences +**Beads** (epic: `tbd-pxis`, parent: `tbd-mdwh`, blocked by Phases 3 and 3b): + +| Bead | Description | +| --- | --- | +| `tbd-pxis` | Parent: Phase 4 Validation | +| `tbd-t7yt` | Create validate-docs.sh comparison script | +| `tbd-ycnl` | Run validation for all shortcuts and guidelines (blocked by t7yt) | +| `tbd-sek7` | Run validation for templates and test reference command (blocked by t7yt) | +| `tbd-8txy` | Fix unintentional differences and document intentional ones (blocked by ycnl, sek7) | + ### Phase 4b: Fresh Install End-to-End Test Final validation with a clean environment to ensure the full user experience works. @@ -1088,6 +1251,15 @@ Final validation with a clean environment to ensure the full user experience wor - [ ] Verify `--list` shows prefixes appropriately - [ ] Clean up test directory +**Beads** (epic: `tbd-4xj8`, parent: `tbd-mdwh`, blocked by Phase 4): + +| Bead | Description | +| --- | --- | +| `tbd-4xj8` | Parent: Phase 4b Fresh Install End-to-End Test | +| `tbd-9bwo` | Fresh install: setup --auto with default sources | +| `tbd-7lus` | Fresh install: add secondary source and test multi-source (blocked by 9bwo) | +| `tbd-c7xq` | Fresh install: test qualified and unqualified lookups (blocked by 7lus) | + ### Phase 5: Speculate Migration (Finalize) Once validation passes, finalize the migration. @@ -1207,6 +1379,18 @@ are primarily consumed via tbd. - [ ] Update Speculate README with flat `{type}/{name}.md` structure - [ ] Release new tbd version with prefix-based sources +**Beads** (epic: `tbd-97fe`, parent: `tbd-mdwh`, blocked by Phase 4b): + +| Bead | Description | +| --- | --- | +| `tbd-97fe` | Parent: Phase 5 Speculate Migration (Finalize) | +| `tbd-hcx4` | Audit all tbd docs and classify by prefix | +| `tbd-mxvr` | Merge Speculate tbd branch → main (blocked by hcx4) | +| `tbd-4r02` | Update tbd default config to use Speculate main (ref: main) (blocked by mxvr) | +| `tbd-d74a` | Remove general docs from tbd bundled set (now in Speculate) (blocked by 4r02) | +| `tbd-qrga` | Update Speculate README with flat doc structure (blocked by mxvr) | +| `tbd-mvi6` | Release new tbd version with prefix-based sources (blocked by d74a, qrga) | + #### Speculate CLI Future Once tbd can pull docs from Speculate, the Speculate CLI’s main value is diminished. @@ -1258,49 +1442,58 @@ tool (`speculate init`), while tbd handles all doc/shortcut/guideline management ## Rollout Plan -**Phase 0: Speculate Prep** +**Top-level epic:** `tbd-mdwh` — Spec: External Docs Repos -1. Create Speculate `tbd` branch with flat `{type}/{name}.md` structure -2. This becomes the integration test target for all subsequent phases +**Phase 0a: Prerequisite Fixes** (`tbd-kzeh`) -**Phase 1: Core Infrastructure** +1. Refactor shortcut.ts to use DocCommandHandler (TDD, beads tbd-she8 chain) +2. Add `warnings` field to `MigrationResult` (tbd-di9c) +3. Update `generateShortcutDirectory()` for `hidden` support (tbd-hrbz) +4. Establish shared test fixtures and helpers for doc infrastructure (tbd-0bed) -3. Implement doc-types registry, repo-url utility, format bump -4. Implement prefix-based storage and sync +**Phase 0: Speculate Prep** (`tbd-eikw`, blocked by 0a) -**Phase 2: Prefix System and Lookup** +5. Create Speculate `tbd` branch with flat `{type}/{name}.md` structure +6. This becomes the integration test target for all subsequent phases -5. Implement qualified (`prefix:name`) and unqualified lookup -6. Add hidden source support -7. Test against Speculate `tbd` branch +**Phase 1: Core Infrastructure** (`tbd-5ybv`, blocked by 0a) -**Phase 3: New Reference Type and CLI** +7. Implement doc-types registry, repo-url utility, format bump +8. Implement prefix-based storage and sync -8. Add `tbd reference` command -9. Simplify existing commands to use doc-types registry +**Phase 2: Prefix System and Lookup** (`tbd-n3zb`, blocked by Phase 1) -**Phase 3b: Documentation Update** +9. Implement qualified (`prefix:name`) and unqualified lookup +10. Add hidden source support +11. Test against Speculate `tbd` branch -10. Update all tbd docs to reflect new architecture -11. Add migration guide for users +**Phase 3: New Reference Type and CLI** (`tbd-qhmo`, blocked by Phase 2) -**Phase 4: Validation** +12. Add `tbd reference` command +13. Simplify existing commands to use doc-types registry -12. Run validation script comparing all output with `get-tbd@latest` -13. Document intentional differences, fix unintentional ones +**Phase 3b: Documentation Update** (`tbd-rq6q`, blocked by Phase 3) -**Phase 4b: Fresh Install E2E** +14. Update all tbd docs to reflect new architecture +15. Add migration guide for users -14. Fresh install test with prefix-based sources -15. Test qualified and unqualified lookups -16. All tests must pass before proceeding +**Phase 4: Validation** (`tbd-pxis`, blocked by Phases 3 and 3b) -**Phase 5: Speculate Migration (Finalize)** +16. Run validation script comparing all output with `get-tbd@latest` +17. Document intentional differences, fix unintentional ones -17. Merge Speculate `tbd` → `main` -18. Update tbd default config to use Speculate `main` -19. Remove duplicated general docs from tbd bundled set -20. Release new tbd version +**Phase 4b: Fresh Install E2E** (`tbd-4xj8`, blocked by Phase 4) + +18. Fresh install test with prefix-based sources +19. Test qualified and unqualified lookups +20. All tests must pass before proceeding + +**Phase 5: Speculate Migration (Finalize)** (`tbd-97fe`, blocked by Phase 4b) + +21. Merge Speculate `tbd` → `main` +22. Update tbd default config to use Speculate `main` +23. Remove duplicated general docs from tbd bundled set +24. Release new tbd version ## Open Questions @@ -1346,16 +1539,16 @@ tool (`speculate init`), while tbd handles all doc/shortcut/guideline management - Ambiguous names require `prefix:name` qualification - `hidden: true` sources excluded from `--list` but accessible via lookup -## Future Work +## Implemented: Source Management CLI -**CLI commands for managing sources** (not in initial implementation): +**CLI commands for managing sources** (implemented): ```bash # Add a repo source with prefix tbd source add github.com/org/guidelines --prefix myorg -# Add with specific ref -tbd source add github.com/org/guidelines --prefix myorg --ref v2.0 +# Add with specific ref and selective paths +tbd source add github.com/org/guidelines --prefix myorg --ref v2.0 --paths guidelines,references # List configured sources with prefixes tbd source list @@ -1364,11 +1557,222 @@ tbd source list tbd source remove myorg ``` -These commands would: -- Require `--prefix` for new sources -- Validate prefix is unique +These commands: +- Require `--prefix` for new sources (validated: 1-16 lowercase alphanumeric + dashes) +- Validate prefix is unique (rejects duplicates) - Default `ref` to `main` but always write it explicitly to config.yml -- Validate the repo is accessible before adding +- Default `paths` to all doc type directories (shortcuts, guidelines, templates, + references) +- Prevent removal of internal sources (only repo and local sources can be removed) +- 12 unit tests in `source.test.ts` + +**Local source support** (designed, pending implementation — see “Design: Local Repo Doc +Sources” section above): +- `tbd source add docs/tbd --prefix local` — auto-detects local directories +- Uses stub pointer files for always-fresh content +- No re-sync needed after editing local docs + +## Design: Local Repo Doc Sources (`type: local`) + +### Motivation + +A common use case: a project has its own shortcuts, guidelines, or templates checked +into its Git repo (e.g., at `docs/tbd/shortcuts/`). These should be managed just like +external repo sources — discoverable via `tbd shortcut --list`, accessible via +`tbd shortcut name` — but without any cloning or fetching. +The source is the current repo itself. + +### Config Format + +```yaml +docs_cache: + sources: + - type: local + prefix: local # user-chosen, 'local' as convention/default + path: docs/tbd # REQUIRED, relative to repo root + paths: [shortcuts, guidelines, templates] +``` + +Key differences from `type: repo`: +- `path` (singular, required): the directory in the repo, always repo-root-relative +- No `url` or `ref` — those are repo-only +- `paths` (plural): which doc-type subdirs to scan (same as `repo` and `internal`) + +### Stub Pointer Files (Always-Fresh Mechanism) + +Instead of copying file content into `.tbd/docs/` (which would go stale), sync creates +**stub files** containing only YAML frontmatter that points back to the source: + +```markdown +--- +_source: local +_path: docs/tbd/shortcuts/my-shortcut.md +--- +``` + +The stub lives at `.tbd/docs/local/shortcuts/my-shortcut.md` just like any other cached +doc. But instead of containing the actual content, it’s a pointer. +DocCache follows the pointer and reads the real file on every load. + +**Why stubs (not symlinks, not copies):** + +| Approach | Pros | Cons | +| --- | --- | --- | +| Copy on sync | Consistent with repo sources | Stale until next sync | +| Symlink | Always fresh, no duplication | Windows issues, git tracking weirdness | +| **Stub pointer** | **Always fresh, cross-platform, explicit** | Requires DocCache change | + +Stubs are the best of both worlds: the `.tbd/docs/` directory structure is maintained +(consistent with repo/internal sources), but content is always read fresh from the +source file. No re-sync needed after editing local docs. + +### DocCache Change + +In `loadDirectory()`, after reading a file, check for the pointer: + +```typescript +// In loadDirectory(), after reading raw content: +const frontmatter = this.parseFrontmatterData(content); + +if (frontmatter?._source === 'local' && frontmatter._path) { + // Follow the pointer — read the real file + const tbdRoot = await findTbdRoot(this.baseDir); + const realPath = join(tbdRoot, frontmatter._path); + try { + content = await readFile(realPath, 'utf-8'); + } catch { + // Source file deleted — skip this doc with warning + console.warn(`Local source missing: ${frontmatter._path}`); + continue; + } +} +``` + +This is the only DocCache change needed. +All other behavior (shadowing, qualified lookups, fuzzy search, `--list`) works +unchanged because the loaded `CachedDoc` has real content, real frontmatter from the +source file, and proper prefix/name fields. + +### DocFrontmatter Extension + +```typescript +export interface DocFrontmatter { + title?: string; + description?: string; + category?: string; + tags?: string[]; + // Internal pointer fields (stripped before exposing to users) + _source?: string; // 'local' for local source stubs + _path?: string; // repo-root-relative path to actual file +} +``` + +The `_` prefix convention signals internal/hidden metadata. +These fields are parsed but not displayed in `--list` output or shortcut directory +tables. + +### Sync Behavior + +In `resolveSourcesToDocs()`, for `type: local` sources: + +```typescript +if (source.type === 'local') { + // Scan {repoRoot}/{source.path}/{docTypeDir}/ for .md files + for (const pathPattern of source.paths) { + const scanDir = join(repoRoot, source.path, pathPattern); + const files = await scanMdFiles(scanDir); + for (const file of files) { + const destPath = `${source.prefix}/${pathPattern}/${file}`; + // Store as 'local:' source — DocSync writes a stub, not a copy + result[destPath] = `local:${source.path}/${pathPattern}/${file}`; + } + } +} +``` + +`DocSync.parseSource()` gains a third source type: + +```typescript +if (source.startsWith('local:')) { + return { type: 'local', location: source.slice(6) }; +} +``` + +For `type: 'local'`, `fetchContent()` returns the stub YAML frontmatter (not the actual +file content), because the real content is read at DocCache load time: + +```typescript +if (source.type === 'local') { + return `---\n_source: local\n_path: ${source.location}\n---\n`; +} +``` + +### CLI: `tbd source add` for Local Sources + +```bash +# Auto-detected as local (resolves to existing directory): +tbd source add docs/tbd --prefix local + +# With explicit paths filter: +tbd source add docs/tbd --prefix local --paths shortcuts,guidelines + +# Still works for repos (existing behavior): +tbd source add github.com/acme/docs --prefix acme +``` + +**Auto-detection heuristic** in `source.ts`: +- Resolve the argument relative to repo root +- If it’s an existing directory → `type: local` +- Otherwise → `type: repo` (existing behavior) + +**Validation when adding a local source:** +1. Resolve path relative to repo root (not cwd) +2. Verify directory exists +3. Verify it’s inside the repo (no `../../escape` — reject paths that resolve outside) +4. Normalize: strip leading `./`, ensure no trailing `/` +5. Store as repo-root-relative (e.g., `docs/tbd`, not `./docs/tbd`) + +### Source List Display + +```bash +$ tbd source list +sys [internal] (hidden) + paths: shortcuts +tbd [internal] + paths: shortcuts +local [local] + path: docs/tbd + paths: shortcuts, guidelines +``` + +The `[local]` label distinguishes from `[repo]` and `[internal]`. + +### Removal Behavior + +```bash +tbd source remove local +# → Removes source from config +# → Next sync removes stub files from .tbd/docs/local/ +# → Source files in docs/tbd/ are untouched (they're part of the repo) +``` + +Only `repo` and `local` sources can be removed (same guard as existing: internal sources +are protected). + +### Implementation Changes Summary + +| File | Change | +| --- | --- | +| `schemas.ts` | Add `'local'` to type enum, add optional `path` field | +| `source.ts` | Auto-detect local vs repo, validate local path, display `[local]` | +| `doc-sync.ts` | Handle `local:` prefix in `parseSource()`, write stubs in `resolveSourcesToDocs()` | +| `doc-cache.ts` | Follow `_source`/`_path` pointers in `loadDirectory()` | +| `doc-types.ts` | No changes needed | +| `source.test.ts` | Tests for local source add/list/remove, path validation | +| `doc-sync.test.ts` | Tests for stub generation, local source resolution | +| `doc-cache.test.ts` | Tests for pointer following, missing source graceful degradation | + +## Future Work **Cross-prefix search** (not in initial implementation): @@ -1398,6 +1802,968 @@ The DOC_TYPES registry makes adding new types straightforward: - `recipes/` - Step-by-step guides for specific tasks - `glossary/` - Term definitions +## Detailed Implementation Notes + +This section provides code-level implementation details for each phase, derived from a +thorough review of the current codebase (as of 2026-02-08). + +### Current Codebase Inventory + +**Files that will be modified or serve as reference:** + +| File | Role | Impact | +| --- | --- | --- | +| `src/lib/tbd-format.ts` | Format versioning, migration | Add f04, migration function | +| `src/lib/schemas.ts` | Zod schemas | Add `DocsSourceSchema`, update `DocsCacheSchema` | +| `src/lib/paths.ts` | Path constants | Simplify, derive from doc-types registry | +| `src/file/doc-sync.ts` | Sync docs from sources | Major rewrite for prefix-based sync, repo sources | +| `src/file/doc-cache.ts` | Cache + lookup | Major rewrite for prefix-aware recursive loading | +| `src/file/doc-add.ts` | `--add` URL handler | Update for prefix-based storage | +| `src/file/config.ts` | Config read/write | Update for new schema | +| `src/cli/commands/shortcut.ts` | Shortcut command | Migrate to DocCommandHandler or update paths | +| `src/cli/commands/guidelines.ts` | Guidelines command | Update paths to use registry | +| `src/cli/commands/template.ts` | Template command | Update paths to use registry | +| `src/cli/commands/sync.ts` | Sync command | Add repo checkout handling | +| `src/cli/commands/setup.ts` | Setup command | Configure default sources, add repo-cache gitignore | +| `src/cli/lib/doc-command-handler.ts` | Shared doc command base | Update for prefix-aware loading | + +**New files to create:** + +| File | Purpose | +| --- | --- | +| `src/lib/doc-types.ts` | Doc type registry (single source of truth) | +| `src/lib/repo-url.ts` | URL normalization and slugification | +| `src/file/repo-cache.ts` | Git sparse checkout operations | +| `src/cli/commands/reference.ts` | New `tbd reference` command | +| `tests/repo-url.test.ts` | Unit tests for URL utility | +| `tests/doc-types.test.ts` | Unit tests for doc type registry | + +### Phase 0: Speculate Prep — Detailed Steps + +**Precondition:** The Speculate repo at `github.com/jlevy/speculate` does NOT have a +`tbd` branch yet (confirmed 2026-02-08). It has branches: `main`, `tbd-sync`. + +**Current Speculate structure** (confirmed from repo): + +``` +docs/general/ + agent-rules/ # 12 files (general-coding-rules.md, typescript-rules.md, etc.) + agent-guidelines/ # 5 files (general-tdd-guidelines.md, golden-testing-guidelines.md, etc.) + agent-shortcuts/ # 21+ files (shortcut-commit-code.md, shortcut-create-pr-simple.md, etc.) + agent-setup/ # 2 files (github-cli-setup.md, shortcut-setup-beads.md) +``` + +**Restructuring plan for Speculate `tbd` branch:** + +1. `agent-rules/` → `guidelines/` (12 files, rename) +2. `agent-guidelines/` → `guidelines/` (5 files, merge with above) +3. `agent-shortcuts/shortcut-*.md` → `shortcuts/*.md` (strip `shortcut-` prefix) +4. `agent-setup/github-cli-setup.md` → `shortcuts/setup-github-cli.md` +5. Create `templates/` from docs/project/ templates +6. Create `references/` for reference docs + +**File mapping (Speculate old → new):** + +| Old Path | New Path | +| --- | --- | +| `agent-rules/typescript-rules.md` | `guidelines/typescript-rules.md` | +| `agent-rules/python-rules.md` | `guidelines/python-rules.md` | +| `agent-rules/general-coding-rules.md` | `guidelines/general-coding-rules.md` | +| `agent-guidelines/general-tdd-guidelines.md` | `guidelines/general-tdd-guidelines.md` | +| `agent-shortcuts/shortcut-commit-code.md` | `shortcuts/review-code.md` (or similar) | +| `agent-setup/github-cli-setup.md` | `shortcuts/setup-github-cli.md` | + +**Important:** Content from tbd’s bundled docs should be copied to Speculate `tbd` +branch for any general-purpose docs that are more up to date in tbd. +Compare file-by-file. + +**tbd repo changes (in Phase 0):** + +- Add `sync-repos.sh` to repo root +- Add `repos/` to root `.gitignore` +- Run `sync-repos.sh` to verify it works + +### Phase 1: Core Infrastructure — Detailed Steps + +#### Step 1.1: Create `src/lib/doc-types.ts` + +```typescript +// Single source of truth for doc types +export const DOC_TYPES = { + shortcut: { + directory: 'shortcuts', + command: 'shortcut', + singular: 'shortcut', + plural: 'shortcuts', + description: 'Reusable instruction templates for common tasks', + }, + guideline: { + directory: 'guidelines', + command: 'guidelines', + singular: 'guideline', + plural: 'guidelines', + description: 'Coding rules and best practices', + }, + template: { + directory: 'templates', + command: 'template', + singular: 'template', + plural: 'templates', + description: 'Document templates for specs and research', + }, + reference: { + directory: 'references', + command: 'reference', + singular: 'reference', + plural: 'references', + description: 'Reference documentation and API specs', + }, +} as const; + +export type DocTypeName = keyof typeof DOC_TYPES; + +// Infer doc type from a path like "spec/guidelines/typescript-rules.md" +export function inferDocType(relativePath: string): DocTypeName | undefined { + const segments = relativePath.split('/'); + // In prefix-based storage: {prefix}/{type-dir}/{name}.md + // typeDir is segments[1] if prefix-based, or segments[0] if flat + for (const [name, config] of Object.entries(DOC_TYPES)) { + for (const segment of segments) { + if (config.directory === segment) { + return name as DocTypeName; + } + } + } + return undefined; +} + +// Get all directory names from registry +export function getDocTypeDirectories(): string[] { + return Object.values(DOC_TYPES).map((dt) => dt.directory); +} +``` + +#### Step 1.2: Create `src/lib/repo-url.ts` + +**Note on slugification:** The spec mentions `@github/slugify` but this package is +designed for title → URL slug conversion. +For repo URL → filesystem slug, a simpler approach is better: replace `/` and `:` with +`-`, strip protocol. +This avoids an unnecessary dependency. + +```typescript +export interface NormalizedRepoUrl { + host: string; // 'github.com' + owner: string; // 'jlevy' + repo: string; // 'speculate' + https: string; // 'https://github.com/jlevy/speculate.git' + ssh: string; // 'git@github.com:jlevy/speculate.git' +} + +// Normalize any git URL format to canonical form +export function normalizeRepoUrl(url: string): NormalizedRepoUrl { ... } + +// Convert URL to filesystem-safe slug +// 'github.com/jlevy/speculate' → 'github.com-jlevy-speculate' +export function repoUrlToSlug(url: string): string { + const normalized = normalizeRepoUrl(url); + return `${normalized.host}-${normalized.owner}-${normalized.repo}`; +} + +// Get clone URL for git operations +export function getCloneUrl(url: string, preferSsh: boolean = false): string { ... } +``` + +**Test cases for `repo-url.test.ts`:** + +- `github.com/org/repo` → normalized form +- `https://github.com/org/repo` → same normalized form +- `https://github.com/org/repo.git` → same +- `git@github.com:org/repo.git` → same +- Trailing slashes stripped +- Invalid URLs throw descriptive errors +- Round-trip: `slug(normalize(x))` is deterministic +- Special characters in repo names + +#### Step 1.3: Update `src/lib/tbd-format.ts` + +**Current state:** `CURRENT_FORMAT = 'f03'`, `RawConfig` type, `MigrationResult` type. + +**Changes needed:** + +1. Add `warnings: string[]` to `MigrationResult` interface (currently missing, but tests + in spec expect it) +2. Add `f04` to `FORMAT_HISTORY` +3. Add `sources` to `RawConfig.docs_cache` type +4. Implement `migrate_f03_to_f04()`: + - Remove `lookup_path` from `docs_cache` + - Convert `files:` to `sources:` using `convertFilesToSources()` + - See spec body for detailed algorithm +5. Add to `migrateToLatest()` chain +6. Update `CURRENT_FORMAT` to `'f04'` +7. Add to `describeMigration()` + +**`getExpectedDefaultFiles()` implementation:** This function needs to return what +`generateDefaultDocCacheConfig()` in `doc-sync.ts` would produce. +It can: +- Call `generateDefaultDocCacheConfig()` directly (requires async), OR +- Hardcode the expected pattern: any `files` entry where `source === 'internal:' + dest` + is a default entry. This is simpler and doesn’t require filesystem access. + +**Recommended approach for identifying defaults:** + +```typescript +function isDefaultFileEntry(dest: string, source: string): boolean { + return source === `internal:${dest}`; +} +``` + +This avoids needing to enumerate bundled docs and correctly identifies any entry where +the destination path matches the internal source path (which is always true for +defaults). + +#### Step 1.4: Update `src/lib/schemas.ts` + +Add to existing schemas: + +```typescript +export const DocsSourceSchema = z.object({ + type: z.enum(['internal', 'repo', 'local']), + prefix: z.string().min(1).max(16).regex(/^[a-z0-9-]+$/), + url: z.string().optional(), // Required for type: repo + ref: z.string().optional(), // Defaults to 'main' for repos + path: z.string().optional(), // Required for type: local (repo-root-relative dir) + paths: z.array(z.string()), + hidden: z.boolean().optional(), +}); + +// Update DocsCacheSchema - keep backward compatibility during migration +export const DocsCacheSchema = z.object({ + sources: z.array(DocsSourceSchema).optional(), + files: z.record(z.string(), z.string()).optional(), + // REMOVED: lookup_path (replaced by prefix system) + // Keep in schema for migration parsing but don't use + lookup_path: z.array(z.string()).optional(), +}); +``` + +**Important:** Since `ConfigSchema` uses Zod’s default `strip()` mode, removing +`lookup_path` from the schema would cause it to be silently stripped from existing f03 +configs. +This is fine ONLY because the format version bump to f04 prevents older versions +from seeing the stripped field. +The migration explicitly removes it. + +#### Step 1.5: Create `src/file/repo-cache.ts` + +```typescript +export class RepoCache { + private readonly cacheDir: string; // .tbd/repo-cache/ + + constructor(tbdRoot: string) { + this.cacheDir = join(tbdRoot, '.tbd', 'repo-cache'); + } + + // Check out or update a repo + async ensureRepo(url: string, ref: string, paths: string[]): Promise { + const slug = repoUrlToSlug(url); + const repoDir = join(this.cacheDir, slug); + + if (await this.isCloned(repoDir)) { + await this.updateRepo(repoDir, ref, paths); + } else { + await this.cloneRepo(url, repoDir, ref, paths); + } + + return repoDir; + } + + private async cloneRepo(url: string, dir: string, ref: string, paths: string[]): Promise { + const cloneUrl = getCloneUrl(url); + await mkdir(dirname(dir), { recursive: true }); + + // Shallow sparse clone + await execGit(['clone', '--depth', '1', '--sparse', '--branch', ref, cloneUrl, dir]); + + // Set sparse checkout paths + await execGit(['-C', dir, 'sparse-checkout', 'set', ...paths]); + } + + private async updateRepo(dir: string, ref: string, paths: string[]): Promise { + // Update sparse checkout paths (in case config changed) + await execGit(['-C', dir, 'sparse-checkout', 'set', ...paths]); + + // Fetch and checkout the ref + await execGit(['-C', dir, 'fetch', '--depth', '1', 'origin', ref]); + await execGit(['-C', dir, 'checkout', 'FETCH_HEAD']); + } + + // Scan for .md files matching paths patterns + async scanDocs(repoDir: string, paths: string[]): Promise> { + const docs = new Map(); // relativePath → absolutePath + + for (const pathPattern of paths) { + const dir = join(repoDir, pathPattern.replace(/\/$/, '')); + // Recursively find all .md files + const files = await glob('**/*.md', { cwd: dir }); + for (const file of files) { + const relativePath = join(pathPattern.replace(/\/$/, ''), file); + docs.set(relativePath, join(dir, file)); + } + } + + return docs; + } +} +``` + +**Git execution:** Use `child_process.execFile` (not `exec`) for security. +Shell injection is not possible with `execFile` since arguments are passed as an array. + +**Fallback when git fails:** If `git` is not available, try `gh repo clone` as fallback +(since gh CLI is typically available in agent environments). + +#### Step 1.6: Update `src/file/doc-sync.ts` + +Major changes needed: + +1. **`syncDocsWithDefaults()` rewrite:** Currently this function: + - Reads config + - Generates defaults from bundled docs + - Merges and prunes + - Syncs files + - Writes config + + New behavior: + - Read config (now has `sources` array) + - For each source, resolve docs (internal → scan bundled, repo → checkout + scan) + - Copy files to `.tbd/docs/{prefix}/{type}/{name}.md` + - Apply `files:` overrides last + - Write sources hash for change detection + +2. **`DocSync` class:** The constructor currently takes + `config: Record`. This needs to accept the new source-based config. + Consider either: + - (a) Expanding `DocSync` to handle sources directly, OR + - (b) Resolving sources to a flat file map first, then passing to existing `DocSync` + + **Recommendation:** Option (b) for minimal disruption. + Create a `resolveSourcesToDocs()` function that returns the same + `Record` format (dest → source), where source is either + `internal:path` or a local file path from the repo cache. + +3. **`generateDefaultDocCacheConfig()`:** This function becomes less important since + defaults are now expressed as `sources`. It may still be needed for migration + (`getExpectedDefaultFiles()`). + +#### Step 1.7: Update `src/file/doc-cache.ts` + +**Current behavior:** `DocCache` loads flat directories via `paths: string[]`. Each path +is a directory like `.tbd/docs/shortcuts/standard/`. + +**New behavior:** `DocCache` needs to: +1. Accept prefix-based paths (scan `.tbd/docs/{prefix}/{type}/`) +2. Support `prefix:name` qualified lookups +3. Detect ambiguous unqualified lookups +4. Support `hidden` sources for excluding from `--list` + +**Recommended approach:** + +```typescript +// New constructor signature +constructor( + private readonly docsDir: string, // .tbd/docs/ + private readonly sources: DocsSource[], // From config + private readonly docType: DocTypeName, // Which type to load +) {} + +// Updated load: scan {prefix}/{type}/ for each source +async load(): Promise { + for (const source of this.sources) { + const dir = join(this.docsDir, source.prefix, DOC_TYPES[this.docType].directory); + await this.loadDirectory(dir, source.prefix, source.hidden); + } +} + +// Updated get: support prefix:name syntax +get(name: string): DocMatch | null { + const { prefix, baseName } = parseQualifiedName(name); + if (prefix) { + // Direct lookup in specific prefix + return this.docs.find(d => d.prefix === prefix && d.name === baseName) ?? null; + } + // Unqualified: search all, error if ambiguous + const matches = this.docs.filter(d => d.name === baseName); + if (matches.length > 1) { + throw new AmbiguousLookupError(baseName, matches.map(m => m.prefix)); + } + return matches[0] ?? null; +} +``` + +**New constructor signature:** + +```typescript +// DocCache becomes general: accepts base dir, source names, and doc types +constructor( + baseDir: string, // e.g., '/project/.tbd/docs/' + sourceNames: string[], // e.g., ['sys', 'tbd', 'spec'] (precedence order) + docTypes: string[], // e.g., ['shortcuts'] for shortcut command +) + +// Internally constructs paths: +// {baseDir}/{sourceName}/{docType}/ for each combination +// Scans in sourceNames order (earlier = higher precedence) +``` + +**Breaking change:** The `CachedDoc` interface needs a `prefix` field. + +### Phase 2: Prefix System and Lookup — Detailed Steps + +#### Step 2.1: Prefix parsing utility + +```typescript +// In doc-cache.ts or a new utility +export function parseQualifiedName(name: string): { prefix?: string; baseName: string } { + const colonIndex = name.indexOf(':'); + if (colonIndex > 0) { + return { + prefix: name.slice(0, colonIndex), + baseName: name.slice(colonIndex + 1), + }; + } + return { baseName: name }; +} +``` + +#### Step 2.2: Update `tbd setup --auto` + +Current setup in `setup.ts` calls `syncDocsWithDefaults()` which generates the verbose +`files:` config. New setup should: + +1. If config is f03 or has no `sources`: run migration (f03 → f04) +2. Write default `sources` array to config +3. Add `repo-cache/` to `.tbd/.gitignore` +4. Run `syncDocsWithDefaults()` with new source-based logic + +**Default sources (written to config.yml):** + +```yaml +docs_cache: + sources: + - type: internal + prefix: sys + hidden: true + paths: + - shortcuts/ + - type: internal + prefix: tbd + paths: + - shortcuts/ + - type: repo + prefix: spec + url: github.com/jlevy/speculate + ref: main + paths: + - shortcuts/ + - guidelines/ + - templates/ + - references/ +``` + +**Important consideration:** For `type: internal` sources, the `paths` field indicates +which doc types to include. +The `prefix` determines where they’re stored on disk. +The internal doc bundling needs to know which bundled docs belong to `sys` vs `tbd`. + +**Classification of bundled shortcuts (from codebase analysis):** + +| Prefix | Bundled Files | +| --- | --- | +| `sys` (hidden) | `skill.md`, `skill-brief.md`, `shortcut-explanation.md` | +| `tbd` | All 29 standard shortcuts (code-review-and-commit, implement-beads, etc.) | + +The classification is simple: `shortcuts/system/` → `sys`, `shortcuts/standard/` → +`tbd`. + +**After migration to Speculate (Phase 5):** + +Some shortcuts currently in `tbd` will move to `spec` (the ~5 general-purpose ones). +But during Phase 1-3, all standard shortcuts remain in `tbd` prefix. + +#### Step 2.3: Update `.tbd/.gitignore` + +Add `repo-cache/` entry: + +``` +# Git checkouts for external doc repos +repo-cache/ +``` + +#### Step 2.4: Update `--list` output format + +Current `--list` shows: `name (size)` + description. + +New format when prefixes are relevant: + +``` +typescript-rules (spec) 12.3 KB / ~3.5K tokens + TypeScript coding rules and best practices +code-review-and-commit (tbd) 8.1 KB / ~2.3K tokens + Run pre-commit checks, review changes, and commit code +``` + +Show prefix in parentheses after name when: +- The name exists in multiple sources, OR +- A non-default source provides the doc (for clarity) + +#### Step 2.5: Error messages for ambiguous lookups + +``` +Error: "typescript-rules" matches docs in multiple sources: + spec:typescript-rules (spec/guidelines/typescript-rules.md) + myorg:typescript-rules (myorg/guidelines/typescript-rules.md) + +Use a qualified name: tbd guidelines spec:typescript-rules +``` + +### Phase 3: New Reference Type and CLI — Detailed Steps + +#### Step 3.1: Create `src/cli/commands/reference.ts` + +Follow the same pattern as `guidelines.ts` (extends `DocCommandHandler`): + +```typescript +class ReferenceHandler extends DocCommandHandler { + constructor(command: Command) { + super(command, { + typeName: 'reference', + typeNamePlural: 'references', + paths: DEFAULT_REFERENCE_PATHS, // Derive from doc-types registry + docType: 'reference', + }); + } + // ... same pattern as guidelines +} + +export const referenceCommand = new Command('reference') + .description('Find and output reference documentation') + .argument('[query]', 'Reference name or description to search for') + .option('--list', 'List all available references') + // ... same options +``` + +Register in `cli.ts`: + +```typescript +import { referenceCommand } from './commands/reference.js'; +program.addCommand(referenceCommand); +``` + +#### Step 3.2: Simplify commands to use doc-types registry + +Currently each command hardcodes its paths: + +- `shortcut.ts`: `config.docs_cache?.lookup_path ?? DEFAULT_SHORTCUT_PATHS` +- `guidelines.ts`: `DEFAULT_GUIDELINES_PATHS` +- `template.ts`: `DEFAULT_TEMPLATE_PATHS` + +With the doc-types registry, all commands derive paths from the registry and config +sources: + +```typescript +function getDocPaths(sources: DocsSource[], docType: DocTypeName, docsDir: string): string[] { + const typeDir = DOC_TYPES[docType].directory; + return sources + .filter(s => s.paths.some(p => p.replace(/\/$/, '') === typeDir)) + .map(s => join(docsDir, s.prefix, typeDir)); +} +``` + +#### Step 3.3: Unify `shortcut.ts` with `DocCommandHandler` + +**Current issue:** The `shortcut.ts` command has its own +`ShortcutHandler extends BaseCommand` with duplicated logic for listing, querying, +wrapping text, etc. The `guidelines.ts` and `template.ts` commands use +`DocCommandHandler` properly. + +**This refactoring is a prerequisite** for the prefix system because the prefix-aware +loading logic should be in `DocCommandHandler`, not duplicated in each command. + +Steps: +1. Migrate `ShortcutHandler` to extend `DocCommandHandler` +2. Move category filtering to the base class (or keep as override) +3. Shortcut-specific behavior (agent header, shortcut-explanation fallback) via + overrides + +#### Step 3.4: Update `doc-add.ts` + +Currently `doc-add.ts` adds to `shortcuts/custom/`. In the new system: +- `--add` still writes to `docs_cache.files` (per-file overrides) +- The destination path should be `{type}/{name}.md` (flat, no `custom/` subdir) +- OR the destination goes into a dedicated prefix directory + +**Recommendation:** Keep `--add` writing to `files:` as overrides, but change the +destination from `shortcuts/custom/foo.md` to just `guidelines/foo.md` or +`shortcuts/foo.md` (flat). +The `files:` section is the highest precedence, so the doc will be found before any +source-provided doc with the same name. + +#### Step 3.5: Doctor checks + +Add to `tbd doctor`: + +```typescript +// Check repo-cache health +async function checkRepoCacheHealth(tbdRoot: string, sources: DocsSource[]): Promise { + for (const source of sources.filter(s => s.type === 'repo')) { + const slug = repoUrlToSlug(source.url!); + const cacheDir = join(tbdRoot, '.tbd', 'repo-cache', slug); + + // Check if cache exists + if (!await exists(cacheDir)) { + warn(`Repo cache missing for ${source.url} - run 'tbd sync --docs' to populate`); + continue; + } + + // Check if git repo is valid + try { + await execGit(['-C', cacheDir, 'status']); + } catch { + error(`Repo cache corrupted for ${source.url} - delete and re-sync`); + } + } +} +``` + +### Phase 3b: Documentation Update — Detailed Steps + +Files to update: + +1. **`docs/development.md`**: Add section on test repo setup, external doc sources, and + development workflow for testing with local repos. + +2. **`docs/docs-overview.md`**: Replace the current doc layout description with + prefix-based structure. + Update `tbd reference` command. + Update the `--add` documentation. + +3. **Skill files** (`packages/tbd/docs/shortcuts/system/skill.md`, `skill-brief.md`): + Add `tbd reference` to the command directory tables. + Update any doc path references. + +4. **`generateShortcutDirectory()` in `doc-cache.ts`**: Update to include references in + the directory table. + This function generates the shortcut/guideline directory that appears in skill files. + +5. **Shortcuts that reference doc paths**: Several shortcuts reference paths like + `.tbd/docs/shortcuts/standard/`. Audit and update: + - `new-shortcut.md` + - `new-guideline.md` + - `welcome-user.md` + +### Phase 4: Validation — Detailed Steps + +**Validation script approach:** + +```bash +#!/bin/bash +# validate-docs.sh - Compare output between released and dev builds +set -e + +BASELINE_CMD="npx --yes get-tbd@latest" +TEST_CMD="node packages/tbd/dist/bin.mjs" + +# Compare all shortcuts +echo "=== Comparing Shortcuts ===" +for name in $($TEST_CMD shortcut --list --json | jq -r '.[].name'); do + baseline=$($BASELINE_CMD shortcut "$name" 2>/dev/null || echo "NOT_FOUND") + test=$($TEST_CMD shortcut "$name" 2>/dev/null || echo "NOT_FOUND") + if [ "$baseline" != "$test" ]; then + echo "DIFF: shortcut $name" + diff <(echo "$baseline") <(echo "$test") || true + fi +done + +# Similar for guidelines, templates, references +``` + +**Expected intentional differences:** +- References section is new (no baseline) +- Content improvements from tbd → Speculate copy +- Prefix information in `--list` output + +### Phase 4b: Fresh Install E2E — Detailed Steps + +```bash +# Create temp directory +TEST_DIR=$(mktemp -d) +cd "$TEST_DIR" +git init + +# Install and setup +npm install -g /path/to/local/get-tbd/tarball +tbd setup --auto --prefix=test + +# Verify directory structure +ls .tbd/docs/sys/shortcuts/ # skill.md, skill-brief.md +ls .tbd/docs/tbd/shortcuts/ # code-review-and-commit.md, etc. +ls .tbd/docs/spec/guidelines/ # typescript-rules.md, etc. + +# Test lookups +tbd guidelines typescript-rules # Should work (unqualified) +tbd guidelines spec:typescript-rules # Should work (qualified) +tbd shortcut code-review-and-commit # Should work +tbd reference --list # Should show reference docs + +# Cleanup +cd / +rm -rf "$TEST_DIR" +npm uninstall -g get-tbd +``` + +## Issues, Ambiguities, and Recommendations + +The following issues were identified during the code-level review of the spec against +the current codebase. +They are ordered by severity/impact. + +### Issue 1: `shortcut.ts` Doesn’t Use `DocCommandHandler` (Medium) + +**Problem:** The shortcut command (`shortcut.ts`) has its own +`ShortcutHandler extends BaseCommand` with ~280 lines of duplicated logic for listing, +querying, text wrapping, etc. +Meanwhile `guidelines.ts` and `template.ts` properly extend `DocCommandHandler`. + +**Impact:** The prefix-aware loading logic needs to be in one place. +Without unifying the commands first, the shortcut command will need separate prefix +support. + +**Recommendation:** Add a prerequisite step to Phase 2 or Phase 3 to refactor +`shortcut.ts` to use `DocCommandHandler`. The shortcut-specific behavior (agent header, +category filtering, refresh mode, `shortcut-explanation` no-query fallback) can be +handled via method overrides. + +### Issue 2: `lookup_path` Is Shortcut-Only, Not Per-Doc-Type (Low) + +**Problem:** The current `docs_cache.lookup_path` in config only applies to shortcuts +(the shortcut command reads it). +Guidelines and templates use hardcoded `DEFAULT_GUIDELINES_PATHS` and +`DEFAULT_TEMPLATE_PATHS`. The spec says “Removed docs_cache.lookup_path: replaced by +prefix system” but doesn’t detail how per-doc-type path resolution works with the prefix +system. + +**Impact:** Low — the prefix system naturally replaces this by having each source +declare which doc types it provides (via `paths`), and each command scanning +`.tbd/docs/{prefix}/{type-dir}/` for all relevant prefixes. + +**Recommendation:** Already addressed by the design. +The `paths` array in each source declaration replaces `lookup_path`. No action needed +beyond what’s already in the spec, but the implementation should be clear that each doc +command derives its search paths from the sources array. + +### Issue 3: `MigrationResult` Missing `warnings` Field (Low) + +**Problem:** The spec’s test code expects `result.warnings` but the current +`MigrationResult` type only has `changes: string[]`. There is no `warnings` field. + +**Impact:** Test code won’t compile. + +**Recommendation:** Add `warnings: string[]` to `MigrationResult`. This is already shown +in the migration function code but not called out as a schema change. + +### Issue 4: `@github/slugify` Dependency Is Overkill (Low) + +**Problem:** The spec mentions using `@github/slugify` npm package for URL +slugification. This package is designed for title → URL slug conversion, not URL → +filesystem path. +The actual transformation needed is simple: `github.com/jlevy/speculate` +→ `github.com-jlevy-speculate` (just replace `/` with `-`). + +**Impact:** Unnecessary dependency. + +**Recommendation:** Implement the slug function directly in `repo-url.ts` (~5 lines of +code). No external dependency needed. + +### Issue 5: `shortcuts/custom/` Path Not Addressed in Migration (Low) + +**Problem:** Users who added shortcuts via `--add` have entries like +`shortcuts/custom/my-shortcut.md: https://example.com/...` in their config. +The migration logic identifies “default” entries as those where +`source === 'internal:' + dest`. Custom URL entries will correctly be preserved in +`files:` overrides. + +**Impact:** None if the heuristic is `source.startsWith('internal:')` → default, +anything else → custom. +But `shortcuts/custom/` as a destination path doesn’t map to any prefix in the new +system. + +**Recommendation:** During migration, preserve custom `files:` entries as-is. +They’ll be written to `.tbd/docs/{dest}` outside the prefix directories. +The `files:` lookup should check these paths with highest precedence. +This is already described in the spec but should be tested explicitly. + +### Issue 6: `DocCache` Flat Directory Scanning — RESOLVED + +**Problem (original):** `DocCache.loadDirectory()` currently scans single flat +directories. The new prefix-based storage is nested: +`.tbd/docs/{prefix}/{type}/{name}.md`. + +**Resolution:** `DocCache` should be generalized to accept a base dir, a list of source +names (prefixes), and the doc type(s) to load. +It constructs paths as `{baseDir}/{name}/{docType}/` and scans each. +This preserves ordered-path semantics (earlier sources = higher precedence). + +```typescript +// New constructor: +constructor( + baseDir: string, // .tbd/docs/ + sourceNames: string[], // ['sys', 'tbd', 'spec'] (in precedence order) + docTypes: string[], // ['shortcuts'] or ['guidelines'] etc. +) + +// Constructs and scans: +// .tbd/docs/sys/shortcuts/ +// .tbd/docs/tbd/shortcuts/ +// .tbd/docs/spec/shortcuts/ +``` + +The `sourceNames` are the prefix values from the sources array. +The `docTypes` are the directory names from the doc-types registry. +Each command passes the appropriate doc type(s) for its domain. + +### Issue 7: Shallow Clone + Sparse Checkout Git Commands (Low) + +**Problem:** The spec shows `git clone --depth 1 --filter=blob:none --sparse`. Using +both `--depth 1` and `--filter=blob:none` is redundant. +`--depth 1` already limits the clone to the latest commit with all blobs for that +commit. `--filter=blob:none` creates a partial clone that fetches blobs on demand, which +is useful for full-history clones but not for depth-1 clones. + +**Impact:** Minor — the clone will work either way. + +**Recommendation:** Use `git clone --depth 1 --sparse --branch ` (without +`--filter=blob:none`). For updates, use `git fetch --depth 1 origin ` followed by +`git checkout FETCH_HEAD`, since a shallow clone may not be able to do `git pull` for +arbitrary refs. + +### Issue 8: `tbd sync --docs` vs `tbd setup --auto` for First-Time Checkout (Low) + +**Problem:** The spec doesn’t explicitly clarify whether `tbd sync --docs` handles +first-time repo checkout or if that’s only done during `tbd setup --auto`. + +**Impact:** User experience — if someone adds a new source to config.yml manually and +runs `tbd sync --docs`, it should clone the repo. + +**Recommendation:** `tbd sync --docs` should handle first-time checkout. +`tbd setup --auto` should also run doc sync. +Both paths should use the same underlying `RepoCache.ensureRepo()`. + +### Issue 9: Config YAML Field Ordering (Low) + +**Problem:** YAML output field ordering matters for readability. +The spec doesn’t specify the order of fields when writing the new `sources` array to +config.yml. + +**Impact:** Readability of config.yml. + +**Recommendation:** Use a YAML serializer that preserves insertion order. +Write fields in this order: `type`, `prefix`, `hidden` (if true), `url` (if repo), `ref` +(if repo), `paths`. This matches the spec examples and reads naturally. + +### Issue 10: Internal Source Bundled Doc Paths — RESOLVED + +**Problem (original):** For `type: internal` sources, the `paths` field alone isn’t +sufficient since bundled docs currently live at `shortcuts/system/` and +`shortcuts/standard/`. + +**Resolution:** `sys:` and `tbd:` (or `std:`) are both `type: internal` sources that use +the same mechanism as external repos. +The bundled docs directory structure should be reorganized to match the prefix +convention: + +``` +packages/tbd/docs/ + sys/ # System docs (prefix: sys, hidden) + shortcuts/ + skill.md + skill-brief.md + shortcut-explanation.md + tbd/ # tbd-specific docs (prefix: tbd) + shortcuts/ + code-review-and-commit.md + implement-beads.md + ... + guidelines/ # tbd-specific guidelines (if any) + tbd-sync-troubleshooting.md +``` + +This way `type: internal` sources work identically to `type: repo` sources — the +`prefix` field directly maps to a subdirectory name under both the bundled docs root AND +`.tbd/docs/`. No special mapping code needed. +The bundled docs restructuring should be done in Phase 1 alongside the format migration. + +### Issue 11: Most Shortcuts Are tbd-Specific (Informational) + +**Finding:** From analyzing the bundled shortcuts, 24 out of 29 standard shortcuts +reference `tbd` commands and are tbd-specific. +Only 5 are general-purpose: + +- `checkout-third-party-repo.md` +- `code-cleanup-docstrings.md` +- `merge-upstream.md` +- `new-validation-plan.md` +- `revise-architecture-doc.md` + +**Impact:** The `spec` prefix source will primarily provide **guidelines** (26 files) +and **templates** (3 files), not shortcuts. +Most shortcuts stay in `tbd`. + +**Recommendation:** This is fine for the design — the prefix system handles it +correctly. But Phase 5 (Speculate Migration) should note that only ~5 shortcuts move to +Speculate, while the bulk of what moves is guidelines. +The doc classification table in the spec (prefix → doc types → examples) should be +updated to reflect this. + +### Issue 12: `generateShortcutDirectory()` Needs Update (Low) + +**Problem:** The `generateShortcutDirectory()` function in `doc-cache.ts` generates the +markdown table of shortcuts/guidelines that appears in skill files. +It currently hardcodes skip names (`skill`, `skill-brief`, `shortcut-explanation`). With +the `hidden` source concept, hidden sources should be automatically excluded instead. + +**Impact:** The function needs to be aware of which docs come from hidden sources. + +**Recommendation:** Pass `hidden` information through `CachedDoc` (add `hidden: boolean` +field) and filter hidden docs in `generateShortcutDirectory()` instead of hardcoding +names. + +### Issue 13: Test Strategy Gaps (Medium) + +**Problem:** The spec describes unit and integration tests but doesn’t address: +- How to test `RepoCache` without network access (unit tests) +- How to mock git operations in tests +- Whether existing tests in `doc-sync.test.ts` and `doc-cache.test.ts` need updating + +**Recommendation:** + +For `RepoCache` unit tests, use `git init --bare` to create local test repositories: + +```typescript +// In test setup +const testRepo = await createTestRepo({ + 'guidelines/test-guide.md': '---\ntitle: Test\n---\n# Test', + 'shortcuts/test-shortcut.md': '---\ntitle: Shortcut\n---\n# SC', +}); + +// Test sparse checkout against local repo +const cache = new RepoCache(tmpDir); +await cache.ensureRepo(`file://${testRepo}`, 'main', ['guidelines/']); +``` + +Existing tests (`doc-sync.test.ts`, `doc-cache.test.ts`, `tbd-format.test.ts`) need +updates for the new format, schemas, and prefix-based paths. + ## References - Current doc sync implementation: diff --git a/packages/tbd/docs/shortcuts/system/shortcut-explanation.md b/packages/tbd/docs/sys/shortcuts/shortcut-explanation.md similarity index 88% rename from packages/tbd/docs/shortcuts/system/shortcut-explanation.md rename to packages/tbd/docs/sys/shortcuts/shortcut-explanation.md index 4f6e46f4..9da6f749 100644 --- a/packages/tbd/docs/shortcuts/system/shortcut-explanation.md +++ b/packages/tbd/docs/sys/shortcuts/shortcut-explanation.md @@ -37,8 +37,8 @@ Agent: Shortcuts are loaded from directories in the doc path (searched in order): -- `.tbd/docs/shortcuts/system/` - Core system docs (skill.md, etc.) -- `.tbd/docs/shortcuts/standard/` - Standard workflow shortcuts +- `.tbd/docs/sys/shortcuts/` - Core system docs (skill.md, etc.) +- `.tbd/docs/tbd/shortcuts/` - Standard workflow shortcuts Directories earlier in the doc path take precedence. If you add a shortcut with the same name in an earlier directory, it will take @@ -46,7 +46,7 @@ precedence over a same-named shortcut in a later directory. ## Creating Custom Shortcuts -1. Create a markdown file in `.tbd/docs/shortcuts/standard/` or a custom directory +1. Create a markdown file in `.tbd/docs/tbd/shortcuts/` or a custom directory 2. Add YAML frontmatter with `title` and `description` for searchability 3. Write your instructions in the body diff --git a/packages/tbd/docs/shortcuts/system/skill-baseline.md b/packages/tbd/docs/sys/shortcuts/skill-baseline.md similarity index 99% rename from packages/tbd/docs/shortcuts/system/skill-baseline.md rename to packages/tbd/docs/sys/shortcuts/skill-baseline.md index f273badd..f6ecd852 100644 --- a/packages/tbd/docs/shortcuts/system/skill-baseline.md +++ b/packages/tbd/docs/sys/shortcuts/skill-baseline.md @@ -161,6 +161,7 @@ or want help → run `tbd shortcut welcome-user` | `tbd guidelines ` | Load coding guidelines | | `tbd guidelines --list` | List guidelines | | `tbd template ` | Output a template | +| `tbd reference ` | Load a reference document | ## Quick Reference diff --git a/packages/tbd/docs/shortcuts/system/skill-brief.md b/packages/tbd/docs/sys/shortcuts/skill-brief.md similarity index 100% rename from packages/tbd/docs/shortcuts/system/skill-brief.md rename to packages/tbd/docs/sys/shortcuts/skill-brief.md diff --git a/packages/tbd/docs/shortcuts/system/skill-minimal.md b/packages/tbd/docs/sys/shortcuts/skill-minimal.md similarity index 100% rename from packages/tbd/docs/shortcuts/system/skill-minimal.md rename to packages/tbd/docs/sys/shortcuts/skill-minimal.md diff --git a/packages/tbd/docs/tbd-docs.md b/packages/tbd/docs/tbd-docs.md index 8900ec59..f3bd99f4 100644 --- a/packages/tbd/docs/tbd-docs.md +++ b/packages/tbd/docs/tbd-docs.md @@ -696,8 +696,7 @@ Options: GitHub blob URLs are automatically converted to raw.githubusercontent.com URLs. On HTTP 403, fetching falls back to `gh api` for authenticated access. -User-added shortcuts go to `shortcuts/custom/` (separate from bundled -`shortcuts/standard/`). +User-added shortcuts go to `.tbd/docs/shortcuts/` alongside bundled shortcuts. ### uninstall diff --git a/packages/tbd/docs/guidelines/backward-compatibility-rules.md b/packages/tbd/docs/tbd/guidelines/backward-compatibility-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/backward-compatibility-rules.md rename to packages/tbd/docs/tbd/guidelines/backward-compatibility-rules.md diff --git a/packages/tbd/docs/guidelines/bun-monorepo-patterns.md b/packages/tbd/docs/tbd/guidelines/bun-monorepo-patterns.md similarity index 100% rename from packages/tbd/docs/guidelines/bun-monorepo-patterns.md rename to packages/tbd/docs/tbd/guidelines/bun-monorepo-patterns.md diff --git a/packages/tbd/docs/guidelines/cli-agent-skill-patterns.md b/packages/tbd/docs/tbd/guidelines/cli-agent-skill-patterns.md similarity index 100% rename from packages/tbd/docs/guidelines/cli-agent-skill-patterns.md rename to packages/tbd/docs/tbd/guidelines/cli-agent-skill-patterns.md diff --git a/packages/tbd/docs/guidelines/commit-conventions.md b/packages/tbd/docs/tbd/guidelines/commit-conventions.md similarity index 100% rename from packages/tbd/docs/guidelines/commit-conventions.md rename to packages/tbd/docs/tbd/guidelines/commit-conventions.md diff --git a/packages/tbd/docs/guidelines/convex-limits-best-practices.md b/packages/tbd/docs/tbd/guidelines/convex-limits-best-practices.md similarity index 100% rename from packages/tbd/docs/guidelines/convex-limits-best-practices.md rename to packages/tbd/docs/tbd/guidelines/convex-limits-best-practices.md diff --git a/packages/tbd/docs/guidelines/convex-rules.md b/packages/tbd/docs/tbd/guidelines/convex-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/convex-rules.md rename to packages/tbd/docs/tbd/guidelines/convex-rules.md diff --git a/packages/tbd/docs/guidelines/electron-app-development-patterns.md b/packages/tbd/docs/tbd/guidelines/electron-app-development-patterns.md similarity index 100% rename from packages/tbd/docs/guidelines/electron-app-development-patterns.md rename to packages/tbd/docs/tbd/guidelines/electron-app-development-patterns.md diff --git a/packages/tbd/docs/guidelines/error-handling-rules.md b/packages/tbd/docs/tbd/guidelines/error-handling-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/error-handling-rules.md rename to packages/tbd/docs/tbd/guidelines/error-handling-rules.md diff --git a/packages/tbd/docs/guidelines/general-coding-rules.md b/packages/tbd/docs/tbd/guidelines/general-coding-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/general-coding-rules.md rename to packages/tbd/docs/tbd/guidelines/general-coding-rules.md diff --git a/packages/tbd/docs/guidelines/general-comment-rules.md b/packages/tbd/docs/tbd/guidelines/general-comment-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/general-comment-rules.md rename to packages/tbd/docs/tbd/guidelines/general-comment-rules.md diff --git a/packages/tbd/docs/guidelines/general-eng-assistant-rules.md b/packages/tbd/docs/tbd/guidelines/general-eng-assistant-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/general-eng-assistant-rules.md rename to packages/tbd/docs/tbd/guidelines/general-eng-assistant-rules.md diff --git a/packages/tbd/docs/guidelines/general-style-rules.md b/packages/tbd/docs/tbd/guidelines/general-style-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/general-style-rules.md rename to packages/tbd/docs/tbd/guidelines/general-style-rules.md diff --git a/packages/tbd/docs/guidelines/general-tdd-guidelines.md b/packages/tbd/docs/tbd/guidelines/general-tdd-guidelines.md similarity index 100% rename from packages/tbd/docs/guidelines/general-tdd-guidelines.md rename to packages/tbd/docs/tbd/guidelines/general-tdd-guidelines.md diff --git a/packages/tbd/docs/guidelines/general-testing-rules.md b/packages/tbd/docs/tbd/guidelines/general-testing-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/general-testing-rules.md rename to packages/tbd/docs/tbd/guidelines/general-testing-rules.md diff --git a/packages/tbd/docs/guidelines/golden-testing-guidelines.md b/packages/tbd/docs/tbd/guidelines/golden-testing-guidelines.md similarity index 100% rename from packages/tbd/docs/guidelines/golden-testing-guidelines.md rename to packages/tbd/docs/tbd/guidelines/golden-testing-guidelines.md diff --git a/packages/tbd/docs/guidelines/pnpm-monorepo-patterns.md b/packages/tbd/docs/tbd/guidelines/pnpm-monorepo-patterns.md similarity index 100% rename from packages/tbd/docs/guidelines/pnpm-monorepo-patterns.md rename to packages/tbd/docs/tbd/guidelines/pnpm-monorepo-patterns.md diff --git a/packages/tbd/docs/guidelines/python-cli-patterns.md b/packages/tbd/docs/tbd/guidelines/python-cli-patterns.md similarity index 100% rename from packages/tbd/docs/guidelines/python-cli-patterns.md rename to packages/tbd/docs/tbd/guidelines/python-cli-patterns.md diff --git a/packages/tbd/docs/guidelines/python-modern-guidelines.md b/packages/tbd/docs/tbd/guidelines/python-modern-guidelines.md similarity index 100% rename from packages/tbd/docs/guidelines/python-modern-guidelines.md rename to packages/tbd/docs/tbd/guidelines/python-modern-guidelines.md diff --git a/packages/tbd/docs/guidelines/python-rules.md b/packages/tbd/docs/tbd/guidelines/python-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/python-rules.md rename to packages/tbd/docs/tbd/guidelines/python-rules.md diff --git a/packages/tbd/docs/guidelines/release-notes-guidelines.md b/packages/tbd/docs/tbd/guidelines/release-notes-guidelines.md similarity index 100% rename from packages/tbd/docs/guidelines/release-notes-guidelines.md rename to packages/tbd/docs/tbd/guidelines/release-notes-guidelines.md diff --git a/packages/tbd/docs/guidelines/tbd-sync-troubleshooting.md b/packages/tbd/docs/tbd/guidelines/tbd-sync-troubleshooting.md similarity index 100% rename from packages/tbd/docs/guidelines/tbd-sync-troubleshooting.md rename to packages/tbd/docs/tbd/guidelines/tbd-sync-troubleshooting.md diff --git a/packages/tbd/docs/guidelines/typescript-cli-tool-rules.md b/packages/tbd/docs/tbd/guidelines/typescript-cli-tool-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/typescript-cli-tool-rules.md rename to packages/tbd/docs/tbd/guidelines/typescript-cli-tool-rules.md diff --git a/packages/tbd/docs/guidelines/typescript-code-coverage.md b/packages/tbd/docs/tbd/guidelines/typescript-code-coverage.md similarity index 100% rename from packages/tbd/docs/guidelines/typescript-code-coverage.md rename to packages/tbd/docs/tbd/guidelines/typescript-code-coverage.md diff --git a/packages/tbd/docs/guidelines/typescript-rules.md b/packages/tbd/docs/tbd/guidelines/typescript-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/typescript-rules.md rename to packages/tbd/docs/tbd/guidelines/typescript-rules.md diff --git a/packages/tbd/docs/guidelines/typescript-sorting-patterns.md b/packages/tbd/docs/tbd/guidelines/typescript-sorting-patterns.md similarity index 100% rename from packages/tbd/docs/guidelines/typescript-sorting-patterns.md rename to packages/tbd/docs/tbd/guidelines/typescript-sorting-patterns.md diff --git a/packages/tbd/docs/guidelines/typescript-yaml-handling-rules.md b/packages/tbd/docs/tbd/guidelines/typescript-yaml-handling-rules.md similarity index 100% rename from packages/tbd/docs/guidelines/typescript-yaml-handling-rules.md rename to packages/tbd/docs/tbd/guidelines/typescript-yaml-handling-rules.md diff --git a/packages/tbd/docs/shortcuts/standard/agent-handoff.md b/packages/tbd/docs/tbd/shortcuts/agent-handoff.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/agent-handoff.md rename to packages/tbd/docs/tbd/shortcuts/agent-handoff.md diff --git a/packages/tbd/docs/shortcuts/standard/checkout-third-party-repo.md b/packages/tbd/docs/tbd/shortcuts/checkout-third-party-repo.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/checkout-third-party-repo.md rename to packages/tbd/docs/tbd/shortcuts/checkout-third-party-repo.md diff --git a/packages/tbd/docs/shortcuts/standard/code-cleanup-all.md b/packages/tbd/docs/tbd/shortcuts/code-cleanup-all.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/code-cleanup-all.md rename to packages/tbd/docs/tbd/shortcuts/code-cleanup-all.md diff --git a/packages/tbd/docs/shortcuts/standard/code-cleanup-docstrings.md b/packages/tbd/docs/tbd/shortcuts/code-cleanup-docstrings.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/code-cleanup-docstrings.md rename to packages/tbd/docs/tbd/shortcuts/code-cleanup-docstrings.md diff --git a/packages/tbd/docs/shortcuts/standard/code-cleanup-tests.md b/packages/tbd/docs/tbd/shortcuts/code-cleanup-tests.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/code-cleanup-tests.md rename to packages/tbd/docs/tbd/shortcuts/code-cleanup-tests.md diff --git a/packages/tbd/docs/shortcuts/standard/code-review-and-commit.md b/packages/tbd/docs/tbd/shortcuts/code-review-and-commit.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/code-review-and-commit.md rename to packages/tbd/docs/tbd/shortcuts/code-review-and-commit.md diff --git a/packages/tbd/docs/shortcuts/standard/coding-spike.md b/packages/tbd/docs/tbd/shortcuts/coding-spike.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/coding-spike.md rename to packages/tbd/docs/tbd/shortcuts/coding-spike.md diff --git a/packages/tbd/docs/shortcuts/standard/create-or-update-pr-simple.md b/packages/tbd/docs/tbd/shortcuts/create-or-update-pr-simple.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/create-or-update-pr-simple.md rename to packages/tbd/docs/tbd/shortcuts/create-or-update-pr-simple.md diff --git a/packages/tbd/docs/shortcuts/standard/create-or-update-pr-with-validation-plan.md b/packages/tbd/docs/tbd/shortcuts/create-or-update-pr-with-validation-plan.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/create-or-update-pr-with-validation-plan.md rename to packages/tbd/docs/tbd/shortcuts/create-or-update-pr-with-validation-plan.md diff --git a/packages/tbd/docs/shortcuts/standard/implement-beads.md b/packages/tbd/docs/tbd/shortcuts/implement-beads.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/implement-beads.md rename to packages/tbd/docs/tbd/shortcuts/implement-beads.md diff --git a/packages/tbd/docs/shortcuts/standard/merge-upstream.md b/packages/tbd/docs/tbd/shortcuts/merge-upstream.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/merge-upstream.md rename to packages/tbd/docs/tbd/shortcuts/merge-upstream.md diff --git a/packages/tbd/docs/shortcuts/standard/new-architecture-doc.md b/packages/tbd/docs/tbd/shortcuts/new-architecture-doc.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/new-architecture-doc.md rename to packages/tbd/docs/tbd/shortcuts/new-architecture-doc.md diff --git a/packages/tbd/docs/shortcuts/standard/new-guideline.md b/packages/tbd/docs/tbd/shortcuts/new-guideline.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/new-guideline.md rename to packages/tbd/docs/tbd/shortcuts/new-guideline.md diff --git a/packages/tbd/docs/shortcuts/standard/new-plan-spec.md b/packages/tbd/docs/tbd/shortcuts/new-plan-spec.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/new-plan-spec.md rename to packages/tbd/docs/tbd/shortcuts/new-plan-spec.md diff --git a/packages/tbd/docs/shortcuts/standard/new-research-brief.md b/packages/tbd/docs/tbd/shortcuts/new-research-brief.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/new-research-brief.md rename to packages/tbd/docs/tbd/shortcuts/new-research-brief.md diff --git a/packages/tbd/docs/shortcuts/standard/new-shortcut.md b/packages/tbd/docs/tbd/shortcuts/new-shortcut.md similarity index 91% rename from packages/tbd/docs/shortcuts/standard/new-shortcut.md rename to packages/tbd/docs/tbd/shortcuts/new-shortcut.md index 90181452..704c5d58 100644 --- a/packages/tbd/docs/shortcuts/standard/new-shortcut.md +++ b/packages/tbd/docs/tbd/shortcuts/new-shortcut.md @@ -8,8 +8,8 @@ Create a new shortcut for `tbd shortcut `. ## Locations -- **Official** (bundled with tbd): `packages/tbd/docs/shortcuts/standard/.md` -- **Project-level** (custom): `.tbd/docs/shortcuts/standard/.md` +- **Official** (bundled with tbd): `packages/tbd/docs/tbd/shortcuts/.md` +- **Project-level** (custom): `.tbd/docs/tbd/shortcuts/.md` ## Format @@ -64,7 +64,7 @@ For official shortcuts: `pnpm build` in packages/tbd/ ## Documentation Updates (Official Shortcuts) -For official shortcuts added to `packages/tbd/docs/shortcuts/standard/`: +For official shortcuts added to `packages/tbd/docs/tbd/shortcuts/`: 1. **Update root README.md** — Add to the “Available shortcuts” table (grouped by category: Planning, Documentation, Review, Git, Cleanup, Session, Meta) diff --git a/packages/tbd/docs/shortcuts/standard/new-validation-plan.md b/packages/tbd/docs/tbd/shortcuts/new-validation-plan.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/new-validation-plan.md rename to packages/tbd/docs/tbd/shortcuts/new-validation-plan.md diff --git a/packages/tbd/docs/shortcuts/standard/plan-implementation-with-beads.md b/packages/tbd/docs/tbd/shortcuts/plan-implementation-with-beads.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/plan-implementation-with-beads.md rename to packages/tbd/docs/tbd/shortcuts/plan-implementation-with-beads.md diff --git a/packages/tbd/docs/shortcuts/standard/precommit-process.md b/packages/tbd/docs/tbd/shortcuts/precommit-process.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/precommit-process.md rename to packages/tbd/docs/tbd/shortcuts/precommit-process.md diff --git a/packages/tbd/docs/shortcuts/standard/review-code-python.md b/packages/tbd/docs/tbd/shortcuts/review-code-python.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/review-code-python.md rename to packages/tbd/docs/tbd/shortcuts/review-code-python.md diff --git a/packages/tbd/docs/shortcuts/standard/review-code-typescript.md b/packages/tbd/docs/tbd/shortcuts/review-code-typescript.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/review-code-typescript.md rename to packages/tbd/docs/tbd/shortcuts/review-code-typescript.md diff --git a/packages/tbd/docs/shortcuts/standard/review-code.md b/packages/tbd/docs/tbd/shortcuts/review-code.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/review-code.md rename to packages/tbd/docs/tbd/shortcuts/review-code.md diff --git a/packages/tbd/docs/shortcuts/standard/review-github-pr.md b/packages/tbd/docs/tbd/shortcuts/review-github-pr.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/review-github-pr.md rename to packages/tbd/docs/tbd/shortcuts/review-github-pr.md diff --git a/packages/tbd/docs/shortcuts/standard/revise-all-architecture-docs.md b/packages/tbd/docs/tbd/shortcuts/revise-all-architecture-docs.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/revise-all-architecture-docs.md rename to packages/tbd/docs/tbd/shortcuts/revise-all-architecture-docs.md diff --git a/packages/tbd/docs/shortcuts/standard/revise-architecture-doc.md b/packages/tbd/docs/tbd/shortcuts/revise-architecture-doc.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/revise-architecture-doc.md rename to packages/tbd/docs/tbd/shortcuts/revise-architecture-doc.md diff --git a/packages/tbd/docs/shortcuts/standard/setup-github-cli.md b/packages/tbd/docs/tbd/shortcuts/setup-github-cli.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/setup-github-cli.md rename to packages/tbd/docs/tbd/shortcuts/setup-github-cli.md diff --git a/packages/tbd/docs/shortcuts/standard/sync-failure-recovery.md b/packages/tbd/docs/tbd/shortcuts/sync-failure-recovery.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/sync-failure-recovery.md rename to packages/tbd/docs/tbd/shortcuts/sync-failure-recovery.md diff --git a/packages/tbd/docs/shortcuts/standard/update-specs-status.md b/packages/tbd/docs/tbd/shortcuts/update-specs-status.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/update-specs-status.md rename to packages/tbd/docs/tbd/shortcuts/update-specs-status.md diff --git a/packages/tbd/docs/shortcuts/standard/welcome-user.md b/packages/tbd/docs/tbd/shortcuts/welcome-user.md similarity index 100% rename from packages/tbd/docs/shortcuts/standard/welcome-user.md rename to packages/tbd/docs/tbd/shortcuts/welcome-user.md diff --git a/packages/tbd/docs/templates/architecture-doc.md b/packages/tbd/docs/tbd/templates/architecture-doc.md similarity index 100% rename from packages/tbd/docs/templates/architecture-doc.md rename to packages/tbd/docs/tbd/templates/architecture-doc.md diff --git a/packages/tbd/docs/templates/plan-spec.md b/packages/tbd/docs/tbd/templates/plan-spec.md similarity index 100% rename from packages/tbd/docs/templates/plan-spec.md rename to packages/tbd/docs/tbd/templates/plan-spec.md diff --git a/packages/tbd/docs/templates/research-brief.md b/packages/tbd/docs/tbd/templates/research-brief.md similarity index 100% rename from packages/tbd/docs/templates/research-brief.md rename to packages/tbd/docs/tbd/templates/research-brief.md diff --git a/packages/tbd/scripts/copy-docs.mjs b/packages/tbd/scripts/copy-docs.mjs index 80e208e2..1108776c 100644 --- a/packages/tbd/scripts/copy-docs.mjs +++ b/packages/tbd/scripts/copy-docs.mjs @@ -29,12 +29,10 @@ const root = join(__dirname, '..'); const repoRoot = join(root, '..', '..'); // Source documentation directory (packages/tbd/docs/) +// Prefix-based layout: sys/ for system shortcuts, tbd/ for standard docs const DOCS_DIR = join(root, 'docs'); const INSTALL_DIR = join(DOCS_DIR, 'install'); -const SHORTCUTS_DIR = join(DOCS_DIR, 'shortcuts'); -const SHORTCUTS_SYSTEM_DIR = join(SHORTCUTS_DIR, 'system'); -const GUIDELINES_DIR = join(DOCS_DIR, 'guidelines'); -const TEMPLATES_DIR = join(DOCS_DIR, 'templates'); +const SYS_SHORTCUTS_DIR = join(DOCS_DIR, 'sys', 'shortcuts'); /** * Packaged documentation files (in packages/tbd/docs/). @@ -96,29 +94,20 @@ if (phase === 'prebuild') { // Note: The full skill file with shortcuts is dynamically generated at setup time. // This is a minimal version without shortcuts for prime --full output. const claudeHeader = readFileSync(join(INSTALL_DIR, 'claude-header.md'), 'utf-8'); - const skillContent = readFileSync(join(SHORTCUTS_SYSTEM_DIR, 'skill-baseline.md'), 'utf-8'); + const skillContent = readFileSync(join(SYS_SHORTCUTS_DIR, 'skill-baseline.md'), 'utf-8'); await writeFile(join(distDocs, 'SKILL.md'), claudeHeader + skillContent); - // Copy skill-brief.md from shortcuts/system to dist/docs + // Copy skill-brief.md from sys/shortcuts to dist/docs // (needed by `tbd skill --brief` command) - await atomicCopy(join(SHORTCUTS_SYSTEM_DIR, 'skill-brief.md'), join(distDocs, 'skill-brief.md')); + await atomicCopy(join(SYS_SHORTCUTS_DIR, 'skill-brief.md'), join(distDocs, 'skill-brief.md')); // Copy README.md to dist/docs await atomicCopy(join(root, 'README.md'), join(distDocs, 'README.md')); - // Copy shortcuts directories to dist/docs for bundled CLI + // Copy prefix-based doc directories to dist/docs for bundled CLI // These are used by `tbd setup` to copy built-in docs to user's project - await copyDir(SHORTCUTS_DIR, join(distDocs, 'shortcuts')); - - // Copy guidelines directory to dist/docs (top-level, not under shortcuts) - if (existsSync(GUIDELINES_DIR)) { - await copyDir(GUIDELINES_DIR, join(distDocs, 'guidelines')); - } - - // Copy templates directory to dist/docs (top-level, not under shortcuts) - if (existsSync(TEMPLATES_DIR)) { - await copyDir(TEMPLATES_DIR, join(distDocs, 'templates')); - } + await copyDir(join(DOCS_DIR, 'sys'), join(distDocs, 'sys')); + await copyDir(join(DOCS_DIR, 'tbd'), join(distDocs, 'tbd')); // Copy install directory to dist/docs (headers for composing skill files) await copyDir(INSTALL_DIR, join(distDocs, 'install')); diff --git a/packages/tbd/src/cli/cli.ts b/packages/tbd/src/cli/cli.ts index 5f68b8be..60824f98 100644 --- a/packages/tbd/src/cli/cli.ts +++ b/packages/tbd/src/cli/cli.ts @@ -43,6 +43,8 @@ import { skillCommand } from './commands/skill.js'; import { shortcutCommand } from './commands/shortcut.js'; import { guidelinesCommand } from './commands/guidelines.js'; import { templateCommand } from './commands/template.js'; +import { referenceCommand } from './commands/reference.js'; +import { sourceCommand } from './commands/source.js'; import { setupCommand } from './commands/setup.js'; import { saveCommand } from './commands/save.js'; import { workspaceCommand } from './commands/workspace.js'; @@ -84,6 +86,7 @@ function createProgram(): Command { program.addCommand(shortcutCommand); program.addCommand(guidelinesCommand); program.addCommand(templateCommand); + program.addCommand(referenceCommand); program.addCommand(closeProtocolCommand); program.addCommand(docsCommand); program.addCommand(designCommand); @@ -92,6 +95,7 @@ function createProgram(): Command { program.addCommand(initCommand); program.addCommand(configCommand); program.addCommand(setupCommand); + program.addCommand(sourceCommand); program.commandsGroup('Working With Issues:'); diff --git a/packages/tbd/src/cli/commands/doctor.ts b/packages/tbd/src/cli/commands/doctor.ts index 99fecd55..e8d6f1b8 100644 --- a/packages/tbd/src/cli/commands/doctor.ts +++ b/packages/tbd/src/cli/commands/doctor.ts @@ -132,6 +132,10 @@ class DoctorHandler extends BaseCommand { // Check 15: Sync consistency (worktree matches local, ahead/behind counts) healthChecks.push(await this.checkSyncConsistency()); + // Check 15: Repo cache health + const sources = this.config?.docs_cache?.sources ?? []; + healthChecks.push(await checkRepoCacheHealth(this.cwd, sources)); + // Run integration checks (optional IDE/agent integrations) const integrationChecks: DiagnosticResult[] = []; @@ -1050,6 +1054,85 @@ class DoctorHandler extends BaseCommand { } } +/** + * Check health of repo cache directories. + * + * For each type:repo source, checks if the cache directory exists. + * Also detects orphaned cache directories (source removed from config). + */ +export async function checkRepoCacheHealth( + tbdRoot: string, + sources: { type: string; prefix: string; url?: string; ref?: string; paths: string[] }[], +): Promise { + const repoSources = sources.filter((s) => s.type === 'repo' && s.url); + + // Check for orphaned cache dirs + const cacheBaseDir = join(tbdRoot, '.tbd', 'repo-cache'); + let existingDirs: string[] = []; + try { + existingDirs = await readdir(cacheBaseDir); + } catch { + // No cache dir at all + } + + if (repoSources.length === 0 && existingDirs.length === 0) { + return { name: 'Repo cache', status: 'ok', message: 'no repo sources configured' }; + } + + // Import slug utility dynamically to avoid circular deps + const { repoUrlToSlug } = await import('../../lib/repo-url.js'); + + const expectedSlugs = new Set(); + const missingDirs: string[] = []; + + for (const source of repoSources) { + const slug = repoUrlToSlug(source.url!); + expectedSlugs.add(slug); + try { + await access(join(cacheBaseDir, slug)); + } catch { + missingDirs.push(`${source.prefix} (${source.url})`); + } + } + + // Detect orphaned dirs + const orphanedDirs = existingDirs.filter((d) => !expectedSlugs.has(d)); + + const details: string[] = []; + if (missingDirs.length > 0) { + details.push(...missingDirs.map((d) => `missing: ${d}`)); + } + if (orphanedDirs.length > 0) { + details.push(...orphanedDirs.map((d) => `orphaned: ${d}`)); + } + + if (missingDirs.length > 0) { + return { + name: 'Repo cache', + status: 'warn', + message: `${missingDirs.length} missing cache dir(s)`, + details, + suggestion: 'Run: tbd sync --docs to populate', + }; + } + + if (orphanedDirs.length > 0) { + return { + name: 'Repo cache', + status: 'warn', + message: `${orphanedDirs.length} orphaned cache dir(s)`, + details, + suggestion: 'Delete orphaned dirs from .tbd/repo-cache/', + }; + } + + return { + name: 'Repo cache', + status: 'ok', + message: `${repoSources.length} repo source(s) cached`, + }; +} + export const doctorCommand = new Command('doctor') .description('Diagnose and repair repository') .option('--fix', 'Attempt to fix issues') diff --git a/packages/tbd/src/cli/commands/guidelines.ts b/packages/tbd/src/cli/commands/guidelines.ts index a48fda4f..ecf82bad 100644 --- a/packages/tbd/src/cli/commands/guidelines.ts +++ b/packages/tbd/src/cli/commands/guidelines.ts @@ -10,7 +10,7 @@ import pc from 'picocolors'; import { DocCommandHandler, type DocCommandOptions } from '../lib/doc-command-handler.js'; import { CLIError } from '../lib/errors.js'; -import { DEFAULT_GUIDELINES_PATHS } from '../../lib/paths.js'; +import { getDefaultDocPaths } from '../../lib/paths.js'; import { truncate } from '../../lib/truncate.js'; import { formatDocSize } from '../../lib/format-utils.js'; import { getTerminalWidth } from '../lib/output.js'; @@ -65,7 +65,7 @@ class GuidelinesHandler extends DocCommandHandler { super(command, { typeName: 'guideline', typeNamePlural: 'guidelines', - paths: DEFAULT_GUIDELINES_PATHS, + paths: getDefaultDocPaths('guideline'), docType: 'guideline', }); } diff --git a/packages/tbd/src/cli/commands/init.ts b/packages/tbd/src/cli/commands/init.ts index 27b75551..fefd5cc6 100644 --- a/packages/tbd/src/cli/commands/init.ts +++ b/packages/tbd/src/cli/commands/init.ts @@ -120,6 +120,9 @@ class InitHandler extends BaseCommand { '# Installed documentation (regenerated on setup)', 'docs/', '', + '# Cached external repo checkouts', + 'repo-cache/', + '', '# Hidden worktree for tbd-sync branch', `${WORKTREE_DIR_NAME}/`, '', diff --git a/packages/tbd/src/cli/commands/prime.ts b/packages/tbd/src/cli/commands/prime.ts index 1408e005..ebe43e1f 100644 --- a/packages/tbd/src/cli/commands/prime.ts +++ b/packages/tbd/src/cli/commands/prime.ts @@ -19,11 +19,7 @@ import { findTbdRoot, readConfig, hasSeenWelcome, markWelcomeSeen } from '../../ import { stripFrontmatter } from '../../utils/markdown-utils.js'; import { VERSION } from '../lib/version.js'; import { listIssues } from '../../file/storage.js'; -import { - resolveDataSyncDir, - DEFAULT_SHORTCUT_PATHS, - DEFAULT_GUIDELINES_PATHS, -} from '../../lib/paths.js'; +import { resolveDataSyncDir, getDefaultDocPaths } from '../../lib/paths.js'; import { getClaudePaths } from '../../lib/integration-paths.js'; import type { Issue } from '../../lib/types.js'; import { DocCache, generateShortcutDirectory } from '../../file/doc-cache.js'; @@ -409,12 +405,12 @@ class PrimeHandler extends BaseCommand { */ private async getShortcutDirectory(tbdRoot: string): Promise { // Load shortcuts - const shortcutCache = new DocCache(DEFAULT_SHORTCUT_PATHS, tbdRoot); + const shortcutCache = new DocCache(getDefaultDocPaths('shortcut'), tbdRoot); await shortcutCache.load({ quiet: this.ctx.quiet }); const shortcuts = shortcutCache.list(); // Load guidelines - const guidelinesCache = new DocCache(DEFAULT_GUIDELINES_PATHS, tbdRoot); + const guidelinesCache = new DocCache(getDefaultDocPaths('guideline'), tbdRoot); await guidelinesCache.load({ quiet: this.ctx.quiet }); const guidelines = guidelinesCache.list(); diff --git a/packages/tbd/src/cli/commands/reference.ts b/packages/tbd/src/cli/commands/reference.ts new file mode 100644 index 00000000..d02c1a2b --- /dev/null +++ b/packages/tbd/src/cli/commands/reference.ts @@ -0,0 +1,65 @@ +/** + * `tbd reference` - Find and output reference documents. + * + * References are API docs, data model docs, and other reference material. + * Give a name or description and tbd will find the matching reference. + */ + +import { Command } from 'commander'; + +import { DocCommandHandler, type DocCommandOptions } from '../lib/doc-command-handler.js'; +import { CLIError } from '../lib/errors.js'; +import { getDefaultDocPaths } from '../../lib/paths.js'; + +class ReferenceHandler extends DocCommandHandler { + constructor(command: Command) { + super(command, { + typeName: 'reference', + typeNamePlural: 'references', + paths: getDefaultDocPaths('reference'), + docType: 'reference', + }); + } + + async run(query: string | undefined, options: DocCommandOptions): Promise { + await this.execute(async () => { + // Add mode + if (options.add) { + if (!options.name) { + throw new CLIError('--name is required when using --add'); + } + await this.handleAdd(options.add, options.name); + return; + } + + await this.initCache(); + + // List mode + if (options.list) { + await this.handleList(options.all); + return; + } + + // No query: show help + if (!query) { + await this.handleNoQuery(); + return; + } + + // Query provided: try exact match first, then fuzzy + await this.handleQuery(query); + }, 'Failed to find reference'); + } +} + +export const referenceCommand = new Command('reference') + .description('Find and output reference documents') + .argument('[query]', 'Reference name or description to search for') + .option('--list', 'List all available references') + .option('--all', 'Include shadowed references (use with --list)') + .option('--add ', 'Add a reference from a URL') + .option('--name ', 'Name for the added reference (required with --add)') + .action(async (query: string | undefined, options: DocCommandOptions, command) => { + const handler = new ReferenceHandler(command); + await handler.run(query, options); + }); diff --git a/packages/tbd/src/cli/commands/setup.ts b/packages/tbd/src/cli/commands/setup.ts index 1a29adff..a9fb5927 100644 --- a/packages/tbd/src/cli/commands/setup.ts +++ b/packages/tbd/src/cli/commands/setup.ts @@ -44,8 +44,7 @@ import { TBD_DOCS_DIR, WORKTREE_DIR_NAME, DATA_SYNC_DIR_NAME, - DEFAULT_SHORTCUT_PATHS, - DEFAULT_GUIDELINES_PATHS, + getDefaultDocPaths, TBD_SHORTCUTS_SYSTEM, TBD_SHORTCUTS_STANDARD, TBD_GUIDELINES_DIR, @@ -71,12 +70,12 @@ async function getShortcutDirectory(quiet = false): Promise { } // Load shortcuts - const shortcutCache = new DocCache(DEFAULT_SHORTCUT_PATHS, tbdRoot); + const shortcutCache = new DocCache(getDefaultDocPaths('shortcut'), tbdRoot); await shortcutCache.load({ quiet }); const shortcuts = shortcutCache.list(); // Load guidelines - const guidelinesCache = new DocCache(DEFAULT_GUIDELINES_PATHS, tbdRoot); + const guidelinesCache = new DocCache(getDefaultDocPaths('guideline'), tbdRoot); await guidelinesCache.load({ quiet }); const guidelines = guidelinesCache.list(); @@ -1145,6 +1144,9 @@ class SetupDefaultHandler extends BaseCommand { '# Synced documentation cache (regenerated by tbd sync --docs)', 'docs/', '', + '# Cached external repo checkouts', + 'repo-cache/', + '', '# Hidden worktree for tbd-sync branch', `${WORKTREE_DIR_NAME}/`, '', @@ -1400,6 +1402,9 @@ class SetupDefaultHandler extends BaseCommand { '# Synced documentation cache (regenerated by tbd sync --docs)', 'docs/', '', + '# Cached external repo checkouts', + 'repo-cache/', + '', '# Hidden worktree for tbd-sync branch', `${WORKTREE_DIR_NAME}/`, '', @@ -1646,7 +1651,7 @@ class SetupAutoHandler extends BaseCommand { private async syncDocs(cwd: string): Promise { const colors = this.output.getColors(); - // Ensure docs directories exist + // Ensure prefix-based docs directories exist await mkdir(join(cwd, TBD_SHORTCUTS_SYSTEM), { recursive: true }); await mkdir(join(cwd, TBD_SHORTCUTS_STANDARD), { recursive: true }); await mkdir(join(cwd, TBD_GUIDELINES_DIR), { recursive: true }); diff --git a/packages/tbd/src/cli/commands/shortcut.ts b/packages/tbd/src/cli/commands/shortcut.ts index d2d7c564..ce63667a 100644 --- a/packages/tbd/src/cli/commands/shortcut.ts +++ b/packages/tbd/src/cli/commands/shortcut.ts @@ -16,7 +16,7 @@ import { requireInit, CLIError } from '../lib/errors.js'; import { DocCache, SCORE_PREFIX_MATCH } from '../../file/doc-cache.js'; import { addDoc } from '../../file/doc-add.js'; import { readConfig } from '../../file/config.js'; -import { DEFAULT_SHORTCUT_PATHS } from '../../lib/paths.js'; +import { getDefaultDocPaths } from '../../lib/paths.js'; import { truncate } from '../../lib/truncate.js'; import { formatDocSize } from '../../lib/format-utils.js'; import { getTerminalWidth } from '../lib/output.js'; @@ -75,7 +75,7 @@ class ShortcutHandler extends BaseCommand { // Read config to get lookup paths (fall back to defaults) const config = await readConfig(tbdRoot); - const lookupPaths = config.docs_cache?.lookup_path ?? DEFAULT_SHORTCUT_PATHS; + const lookupPaths = config.docs_cache?.lookup_path ?? getDefaultDocPaths('shortcut'); // Create and load the doc cache with proper base directory const cache = new DocCache(lookupPaths, tbdRoot); diff --git a/packages/tbd/src/cli/commands/skill.ts b/packages/tbd/src/cli/commands/skill.ts index b08776b5..847246e5 100644 --- a/packages/tbd/src/cli/commands/skill.ts +++ b/packages/tbd/src/cli/commands/skill.ts @@ -14,7 +14,7 @@ import { shouldUseInteractiveOutput } from '../lib/context.js'; import { renderMarkdownWithFrontmatter, paginateOutput } from '../lib/output.js'; import { findTbdRoot } from '../../file/config.js'; import { DocCache, generateShortcutDirectory } from '../../file/doc-cache.js'; -import { DEFAULT_SHORTCUT_PATHS, DEFAULT_GUIDELINES_PATHS } from '../../lib/paths.js'; +import { getDefaultDocPaths } from '../../lib/paths.js'; interface SkillOptions { brief?: boolean; @@ -87,7 +87,7 @@ class SkillHandler extends BaseCommand { const header = await loadDocContent('install/claude-header.md'); // Load base skill content - const baseSkill = await loadDocContent('shortcuts/system/skill-baseline.md'); + const baseSkill = await loadDocContent('sys/shortcuts/skill-baseline.md'); // Get shortcut directory const directory = await this.getShortcutDirectory(); @@ -112,12 +112,12 @@ class SkillHandler extends BaseCommand { } // Load shortcuts - const shortcutCache = new DocCache(DEFAULT_SHORTCUT_PATHS, tbdRoot); + const shortcutCache = new DocCache(getDefaultDocPaths('shortcut'), tbdRoot); await shortcutCache.load({ quiet: this.ctx.quiet }); const shortcuts = shortcutCache.list(); // Load guidelines - const guidelinesCache = new DocCache(DEFAULT_GUIDELINES_PATHS, tbdRoot); + const guidelinesCache = new DocCache(getDefaultDocPaths('guideline'), tbdRoot); await guidelinesCache.load({ quiet: this.ctx.quiet }); const guidelines = guidelinesCache.list(); diff --git a/packages/tbd/src/cli/commands/source.ts b/packages/tbd/src/cli/commands/source.ts new file mode 100644 index 00000000..7493d097 --- /dev/null +++ b/packages/tbd/src/cli/commands/source.ts @@ -0,0 +1,200 @@ +/** + * `tbd source` - Manage doc sources (repos and internal bundles). + * + * Subcommands: + * - add: Add an external repo source + * - list: List configured sources + * - remove: Remove a source by prefix + * + * See: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md + */ + +import { Command } from 'commander'; +import pc from 'picocolors'; + +import { BaseCommand } from '../lib/base-command.js'; +import { requireInit, CLIError } from '../lib/errors.js'; +import { readConfig, writeConfig } from '../../file/config.js'; +import { getAllDocTypeDirectories } from '../../lib/doc-types.js'; + +// ============================================================================= +// Source Management Functions (exported for testing) +// ============================================================================= + +export interface AddSourceOptions { + url: string; + prefix: string; + ref?: string; + paths?: string[]; +} + +/** + * Add an external repo source to config. + */ +export async function addSource(tbdRoot: string, options: AddSourceOptions): Promise { + const { url, prefix, ref = 'main', paths } = options; + + // Validate prefix format (1-16 lowercase alphanumeric + dash) + if (!/^[a-z0-9-]+$/.test(prefix) || prefix.length < 1 || prefix.length > 16) { + throw new CLIError( + `Invalid prefix "${prefix}": must be 1-16 lowercase alphanumeric characters or dashes`, + ); + } + + const config = await readConfig(tbdRoot); + config.docs_cache ??= { files: {}, lookup_path: [] }; + config.docs_cache.sources ??= []; + + // Check for duplicate prefix + if (config.docs_cache.sources.some((s) => s.prefix === prefix)) { + throw new CLIError(`Source with prefix "${prefix}" already exists`); + } + + // Default paths to all doc type directories + const sourcePaths = paths ?? getAllDocTypeDirectories(); + + config.docs_cache.sources.push({ + type: 'repo', + prefix, + url, + ref, + paths: sourcePaths, + }); + + await writeConfig(tbdRoot, config); +} + +/** + * List all configured sources. + */ +export async function listSources( + tbdRoot: string, +): Promise<{ type: string; prefix: string; url?: string; ref?: string; paths: string[] }[]> { + const config = await readConfig(tbdRoot); + return config.docs_cache?.sources ?? []; +} + +/** + * Remove a source by prefix. + */ +export async function removeSource(tbdRoot: string, prefix: string): Promise { + const config = await readConfig(tbdRoot); + const sources = config.docs_cache?.sources ?? []; + + const idx = sources.findIndex((s) => s.prefix === prefix); + if (idx === -1) { + throw new CLIError(`No source with prefix "${prefix}" found`); + } + + if (sources[idx]!.type === 'internal') { + throw new CLIError( + `Cannot remove internal source "${prefix}". Only repo sources can be removed.`, + ); + } + + sources.splice(idx, 1); + await writeConfig(tbdRoot, config); +} + +// ============================================================================= +// CLI Command Handlers +// ============================================================================= + +class SourceAddHandler extends BaseCommand { + async run(url: string, options: { prefix: string; ref?: string; paths?: string }): Promise { + await this.execute(async () => { + const tbdRoot = await requireInit(); + + const paths = options.paths ? options.paths.split(',').map((p) => p.trim()) : undefined; + + await addSource(tbdRoot, { + url, + prefix: options.prefix, + ref: options.ref, + paths, + }); + + console.log(pc.green(`Added source "${options.prefix}" from ${url}`)); + console.log(pc.dim(` ref: ${options.ref ?? 'main'}`)); + console.log(pc.dim(` paths: ${paths ? paths.join(', ') : 'all doc types'}`)); + console.log(''); + console.log('Run `tbd setup --auto` to sync docs from this source.'); + }, 'Failed to add source'); + } +} + +class SourceListHandler extends BaseCommand { + async run(): Promise { + await this.execute(async () => { + const tbdRoot = await requireInit(); + const sources = await listSources(tbdRoot); + + if (sources.length === 0) { + console.log('No sources configured.'); + return; + } + + this.output.data(sources, () => { + for (const source of sources) { + const typeLabel = source.type === 'internal' ? pc.dim('[internal]') : pc.cyan('[repo]'); + const hidden = (source as { hidden?: boolean }).hidden ? pc.dim(' (hidden)') : ''; + console.log(`${pc.bold(source.prefix)} ${typeLabel}${hidden}`); + if (source.url) { + console.log(pc.dim(` url: ${source.url}`)); + } + if (source.ref) { + console.log(pc.dim(` ref: ${source.ref}`)); + } + console.log(pc.dim(` paths: ${source.paths.join(', ')}`)); + } + }); + }, 'Failed to list sources'); + } +} + +class SourceRemoveHandler extends BaseCommand { + async run(prefix: string): Promise { + await this.execute(async () => { + const tbdRoot = await requireInit(); + await removeSource(tbdRoot, prefix); + console.log(pc.green(`Removed source "${prefix}"`)); + console.log('Run `tbd setup --auto` to update docs.'); + }, 'Failed to remove source'); + } +} + +// ============================================================================= +// Command Registration +// ============================================================================= + +const addCommand = new Command('add') + .description('Add an external repo source') + .argument('', 'Repository URL (e.g., github.com/org/repo)') + .requiredOption('--prefix ', 'Namespace prefix for this source') + .option('--ref ', 'Git ref to checkout (default: main)') + .option('--paths ', 'Comma-separated doc type directories to include') + .action(async (url: string, options, command) => { + const handler = new SourceAddHandler(command); + await handler.run(url, options); + }); + +const listCommand = new Command('list') + .description('List configured doc sources') + .action(async (_options, command) => { + const handler = new SourceListHandler(command); + await handler.run(); + }); + +const removeCommand = new Command('remove') + .description('Remove a source by prefix') + .argument('', 'Prefix of the source to remove') + .action(async (prefix: string, _options, command) => { + const handler = new SourceRemoveHandler(command); + await handler.run(prefix); + }); + +export const sourceCommand = new Command('source') + .description('Manage doc sources (repos and internal bundles)') + .addCommand(addCommand) + .addCommand(listCommand) + .addCommand(removeCommand); diff --git a/packages/tbd/src/cli/commands/template.ts b/packages/tbd/src/cli/commands/template.ts index 94440cc6..e97b7af1 100644 --- a/packages/tbd/src/cli/commands/template.ts +++ b/packages/tbd/src/cli/commands/template.ts @@ -9,14 +9,14 @@ import { Command } from 'commander'; import { DocCommandHandler, type DocCommandOptions } from '../lib/doc-command-handler.js'; import { CLIError } from '../lib/errors.js'; -import { DEFAULT_TEMPLATE_PATHS } from '../../lib/paths.js'; +import { getDefaultDocPaths } from '../../lib/paths.js'; class TemplateHandler extends DocCommandHandler { constructor(command: Command) { super(command, { typeName: 'template', typeNamePlural: 'templates', - paths: DEFAULT_TEMPLATE_PATHS, + paths: getDefaultDocPaths('template'), docType: 'template', }); } diff --git a/packages/tbd/src/file/config.ts b/packages/tbd/src/file/config.ts index 0bd13954..0d4c4676 100644 --- a/packages/tbd/src/file/config.ts +++ b/packages/tbd/src/file/config.ts @@ -169,7 +169,7 @@ export async function writeConfig(baseDir: string, config: Config): Promise 0) { + return { + prefix: cleanName.slice(0, colonIndex), + baseName: cleanName.slice(colonIndex + 1), + }; + } + + return { baseName: cleanName }; } /** @@ -218,6 +254,7 @@ export class DocCache { frontmatter, content, sourceDir, + prefix: inferPrefix(sourceDir), sizeBytes, approxTokens, }; @@ -265,14 +302,24 @@ export class DocCache { /** * Get a document by exact name match. * - * @param name - Filename to match (with or without .md extension) + * Supports qualified names (e.g., 'spec:typescript-rules') for prefix-specific lookup. + * Unqualified names return the first match in path order. + * + * @param name - Filename to match (may include prefix and/or .md extension) * @returns Match with score SCORE_EXACT_MATCH, or null if not found */ get(name: string): DocMatch | null { - // Strip .md extension if present - const lookupName = name.endsWith('.md') ? name.slice(0, -3) : name; + const { prefix, baseName } = parseQualifiedName(name); + + if (prefix) { + // Qualified lookup: search all docs (including shadowed) for specific prefix + const doc = this.allDocs.find((d) => d.name === baseName && d.prefix === prefix); + if (!doc) return null; + return { doc, score: SCORE_EXACT_MATCH }; + } - const doc = this.docs.find((d) => d.name === lookupName); + // Unqualified: return first match in path order + const doc = this.docs.find((d) => d.name === baseName); if (!doc) return null; return { doc, score: SCORE_EXACT_MATCH }; @@ -394,13 +441,14 @@ const SHORTCUT_DIRECTORY_END = ''; /** * Build table rows from docs (shared helper for shortcuts and guidelines). + * Filters out hidden docs and any explicitly skipped names. */ function buildTableRows(docs: CachedDoc[], skipNames: string[] = []): string[] { const sortedDocs = [...docs].sort((a, b) => a.name.localeCompare(b.name)); const rows: string[] = []; for (const doc of sortedDocs) { - if (skipNames.includes(doc.name)) { + if (doc.hidden || skipNames.includes(doc.name)) { continue; } @@ -462,7 +510,7 @@ export function generateShortcutDirectory( lines.push(''); if (shortcutRows.length === 0) { - lines.push('No shortcuts available. Create shortcuts in `.tbd/docs/shortcuts/standard/`.'); + lines.push('No shortcuts available. Create shortcuts in `.tbd/docs/tbd/shortcuts/`.'); } else { lines.push('| Name | Description |'); lines.push('| --- | --- |'); @@ -490,3 +538,22 @@ export function generateShortcutDirectory( return lines.join('\n'); } + +/** + * Infer the source prefix from a directory path. + * + * Extracts the prefix from paths like: + * - '.tbd/docs/tbd/shortcuts' → 'tbd' + * - '.tbd/docs/sys/shortcuts' → 'sys' + * - '.tbd/docs/spec/guidelines' → 'spec' + * + * The prefix is the segment after 'docs/' in the path. + */ +function inferPrefix(sourceDir: string): string | undefined { + const parts = sourceDir.replace(/\\/g, '/').split('/'); + const docsIndex = parts.indexOf('docs'); + if (docsIndex >= 0 && docsIndex + 1 < parts.length) { + return parts[docsIndex + 1]; + } + return undefined; +} diff --git a/packages/tbd/src/file/doc-sync.ts b/packages/tbd/src/file/doc-sync.ts index efd702ba..0874ed0e 100644 --- a/packages/tbd/src/file/doc-sync.ts +++ b/packages/tbd/src/file/doc-sync.ts @@ -8,6 +8,7 @@ import { readdir, readFile, rm, mkdir, access } from 'node:fs/promises'; import { join, dirname } from 'node:path'; +import { createHash } from 'node:crypto'; import { writeFile } from 'atomically'; import { fileURLToPath } from 'node:url'; @@ -93,8 +94,8 @@ export class DocSync { * Parse a source string into a DocSource. * * @example - * parseSource('internal:shortcuts/standard/code-review-and-commit.md') - * // => { type: 'internal', location: 'shortcuts/standard/code-review-and-commit.md' } + * parseSource('internal:tbd/shortcuts/code-review-and-commit.md') + * // => { type: 'internal', location: 'tbd/shortcuts/code-review-and-commit.md' } * * @example * parseSource('https://raw.githubusercontent.com/org/repo/main/file.md') @@ -332,12 +333,12 @@ export async function generateDefaultDocCacheConfig(): Promise, +): Promise> { + const result: Record = {}; + + for (const source of sources) { + if (source.type === 'internal') { + await resolveInternalSource(source, result); + } + // repo type will be added in Phase 2 + } + + // Apply files overrides last (highest precedence) + if (filesOverrides) { + for (const [dest, src] of Object.entries(filesOverrides)) { + result[dest] = src; + } + } + + return result; +} + +/** + * Resolve an internal source by scanning bundled docs. + */ +async function resolveInternalSource( + source: SourceEntry, + result: Record, +): Promise { + const basePaths = getDocsBasePath(); + + for (const pathPattern of source.paths) { + // Try each base path to find bundled docs + for (const basePath of basePaths) { + const scanDir = join(basePath, source.prefix, pathPattern); + try { + await access(scanDir); + } catch { + continue; // Try next base path + } + + // Scan for .md files + const files = await scanMdFiles(scanDir); + for (const file of files) { + const destPath = `${source.prefix}/${pathPattern}${file}`; + const sourcePath = `internal:${source.prefix}/${pathPattern}${file}`; + result[destPath] = sourcePath; + } + break; // Found in this base path, no need to check others + } + } +} + +/** + * Recursively scan a directory for .md files. + * Returns paths relative to the given directory. + */ +async function scanMdFiles(dirPath: string): Promise { + const results: string[] = []; + + try { + const entries = await readdir(dirPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + const subResults = await scanMdFiles(join(dirPath, entry.name)); + for (const sub of subResults) { + results.push(`${entry.name}/${sub}`); + } + } else if (entry.isFile() && entry.name.endsWith('.md')) { + results.push(entry.name); + } + } + } catch { + // Directory doesn't exist or not readable + } + + return results; +} + +/** + * Compute a deterministic hash of a sources array. + * + * Used to detect when source configuration changes, triggering a cache clear. + * Returns the first 8 hex characters of a SHA256 hash. + */ +export function getSourcesHash(sources: SourceEntry[]): string { + const hash = createHash('sha256'); + hash.update(JSON.stringify(sources)); + return hash.digest('hex').slice(0, 8); +} + +/** Path to the sources hash file within .tbd/docs/. */ +const SOURCES_HASH_FILE = '.sources-hash'; + +/** + * Read the stored sources hash from .tbd/docs/.sources-hash. + * Returns undefined if the file doesn't exist. + */ +export async function readSourcesHash(tbdRoot: string): Promise { + const hashPath = join(tbdRoot, TBD_DOCS_DIR, SOURCES_HASH_FILE); + try { + const content = await readFile(hashPath, 'utf-8'); + return content.trim(); + } catch { + return undefined; + } +} + +/** + * Write the sources hash to .tbd/docs/.sources-hash. + */ +export async function writeSourcesHash(tbdRoot: string, hash: string): Promise { + const docsDir = join(tbdRoot, TBD_DOCS_DIR); + await mkdir(docsDir, { recursive: true }); + const hashPath = join(docsDir, SOURCES_HASH_FILE); + await writeFile(hashPath, hash + '\n'); +} + +/** + * Check if the docs cache should be cleared. + * + * Returns true if: + * - No hash file exists (first sync or post-migration) + * - Hash doesn't match current sources config + * + * .tbd/docs/ is gitignored and fully regenerable, so clearing is safe. + */ +export async function shouldClearDocsCache( + tbdRoot: string, + sources: SourceEntry[], +): Promise { + const storedHash = await readSourcesHash(tbdRoot); + if (!storedHash) { + return true; + } + const currentHash = getSourcesHash(sources); + return storedHash !== currentHash; +} + /** * Deep equality check for config objects. */ @@ -555,18 +723,18 @@ export async function syncDocsWithDefaults( const docSync = new DocSync(tbdRoot, prunedConfig); const syncResult = await docSync.sync({ dryRun: options.dryRun }); - // 6. Check if config changed - const configChanged = !configsEqual(prunedConfig, originalFiles); + // 6. Check if config changed (files or lookup_path) + const defaultLookupPath = ['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts']; + const currentLookupPath = config.docs_cache?.lookup_path ?? []; + const lookupPathChanged = + currentLookupPath.length !== defaultLookupPath.length || + currentLookupPath.some((p, i) => p !== defaultLookupPath[i]); + const configChanged = !configsEqual(prunedConfig, originalFiles) || lookupPathChanged; // 7. Write config if changed (and not dry run) if (configChanged && !options.dryRun) { - // Preserve existing lookup_path or use default - const lookupPath = config.docs_cache?.lookup_path ?? [ - '.tbd/docs/shortcuts/system', - '.tbd/docs/shortcuts/standard', - ]; config.docs_cache = { - lookup_path: lookupPath, + lookup_path: defaultLookupPath, files: prunedConfig, }; await writeConfig(tbdRoot, config); diff --git a/packages/tbd/src/file/repo-cache.ts b/packages/tbd/src/file/repo-cache.ts new file mode 100644 index 00000000..6f5c88e5 --- /dev/null +++ b/packages/tbd/src/file/repo-cache.ts @@ -0,0 +1,213 @@ +/** + * RepoCache: Sparse git checkout caching for external doc repos. + * + * Manages local clones of external doc repositories, using shallow sparse + * checkouts to minimize disk usage. Each repo is cached under + * .tbd/repo-cache/{slug}/ where slug is derived from the repo URL. + */ + +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { join, isAbsolute } from 'node:path'; +import { createHash } from 'node:crypto'; +import { mkdir, readFile, readdir, access, stat } from 'node:fs/promises'; +import { repoUrlToSlug, getCloneUrl } from '../lib/repo-url.js'; + +const execFileAsync = promisify(execFile); + +/** A scanned doc file with its relative path and content. */ +export interface ScannedDoc { + relativePath: string; + content: string; +} + +/** + * Cache for external git repository checkouts. + * + * Uses shallow sparse clones to efficiently cache external doc repos + * under .tbd/repo-cache/. + */ +export class RepoCache { + readonly cacheDir: string; + + constructor(tbdRoot: string) { + this.cacheDir = join(tbdRoot, '.tbd', 'repo-cache'); + } + + /** + * Get the deterministic directory path for a repo URL. + * Handles both remote URLs and local filesystem paths. + */ + getRepoDir(url: string): string { + if (this.isLocalPath(url)) { + // Local paths: hash to get a deterministic slug + const hash = createHash('sha256').update(url).digest('hex').slice(0, 12); + return join(this.cacheDir, `local-${hash}`); + } + const slug = repoUrlToSlug(url); + return join(this.cacheDir, slug); + } + + /** + * Ensure a repo is cloned and up-to-date. + * + * On first access, performs a shallow clone. On subsequent accesses, + * fetches and updates to the latest ref. + * + * @param url - Repository URL (any format: short, HTTPS, SSH) + * @param ref - Git ref to checkout (branch/tag, defaults to 'main') + * @param paths - Directory paths to include in sparse checkout + * @param onProgress - Optional callback for progress messages + * @returns Path to the local checkout directory + */ + async ensureRepo( + url: string, + ref: string, + paths: string[], + onProgress?: (message: string) => void, + ): Promise { + const repoDir = this.getRepoDir(url); + await mkdir(this.cacheDir, { recursive: true }); + + const exists = await this.isCloned(repoDir); + + if (!exists) { + onProgress?.(`Cloning ${url}...`); + await this.cloneRepo(url, ref, repoDir, paths); + } else { + onProgress?.(`Updating ${url}...`); + await this.updateRepo(repoDir, ref, paths); + } + + return repoDir; + } + + /** + * Scan a checked-out repo for .md files in specified paths. + * + * @param repoDir - Path to the local checkout + * @param paths - Directory paths to scan (e.g., ['shortcuts/', 'guidelines/']) + * @returns Array of scanned docs with relative paths and content + */ + async scanDocs(repoDir: string, paths: string[]): Promise { + const docs: ScannedDoc[] = []; + + for (const pathPattern of paths) { + const dirPath = join(repoDir, pathPattern); + try { + await access(dirPath); + } catch { + continue; // Directory doesn't exist, skip + } + + const entries = await this.findMdFiles(dirPath); + for (const relativeMdPath of entries) { + const fullPath = join(dirPath, relativeMdPath); + const content = await readFile(fullPath, 'utf-8'); + // relativePath is relative to repoDir, always uses forward slashes + const relativePath = join(pathPattern, relativeMdPath).replace(/\\/g, '/'); + docs.push({ relativePath, content }); + } + } + + return docs; + } + + private async isCloned(repoDir: string): Promise { + try { + const s = await stat(join(repoDir, '.git')); + return s.isDirectory(); + } catch { + return false; + } + } + + private async cloneRepo( + url: string, + ref: string, + repoDir: string, + paths: string[], + ): Promise { + const cloneUrl = this.resolveCloneUrl(url); + + // Shallow clone with sparse checkout + await execFileAsync('git', [ + 'clone', + '--depth', + '1', + '--branch', + ref, + '--sparse', + cloneUrl, + repoDir, + ]); + + // Set sparse checkout paths + if (paths.length > 0) { + await execFileAsync('git', ['-C', repoDir, 'sparse-checkout', 'set', ...paths]); + } + } + + private async updateRepo(repoDir: string, ref: string, paths: string[]): Promise { + // Update sparse checkout paths if needed + if (paths.length > 0) { + await execFileAsync('git', ['-C', repoDir, 'sparse-checkout', 'set', ...paths]); + } + + // Fetch latest + await execFileAsync('git', ['-C', repoDir, 'fetch', '--depth', '1', 'origin', ref]); + await execFileAsync('git', ['-C', repoDir, 'checkout', 'FETCH_HEAD']); + } + + /** + * Check if a URL is a local filesystem path. + */ + private isLocalPath(url: string): boolean { + return ( + url.startsWith('/') || url.startsWith('.') || url.startsWith('file://') || isAbsolute(url) + ); + } + + /** + * Resolve a URL to a clone-able format. + * Local paths use file:// protocol to support --depth. + */ + private resolveCloneUrl(url: string): string { + // Already a file:// URL + if (url.startsWith('file://')) { + return url; + } + // Local paths (absolute or relative, including Windows drive letters) - convert to file:// for --depth support + if (url.startsWith('/') || url.startsWith('.') || isAbsolute(url)) { + return `file://${url}`; + } + // Already a full URL or SSH format + if (url.startsWith('https://') || url.startsWith('http://') || url.startsWith('git@')) { + return url; + } + // Short format (github.com/org/repo) - convert to HTTPS + return getCloneUrl(url); + } + + /** + * Recursively find all .md files in a directory. + * Returns paths relative to the given directory. + */ + private async findMdFiles(dirPath: string): Promise { + const results: string[] = []; + + const entries = await readdir(dirPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + const subResults = await this.findMdFiles(join(dirPath, entry.name)); + for (const sub of subResults) { + results.push(join(entry.name, sub)); + } + } else if (entry.isFile() && entry.name.endsWith('.md')) { + results.push(entry.name); + } + } + + return results; + } +} diff --git a/packages/tbd/src/lib/doc-types.ts b/packages/tbd/src/lib/doc-types.ts new file mode 100644 index 00000000..33115200 --- /dev/null +++ b/packages/tbd/src/lib/doc-types.ts @@ -0,0 +1,85 @@ +/** + * Doc type registry — single source of truth for doc types. + * + * All doc types (shortcut, guideline, template, reference) are defined here. + * Commands, sync, and cache should derive paths and names from this registry + * rather than hardcoding them. + */ + +/** The supported doc type names. */ +export type DocTypeName = 'shortcut' | 'guideline' | 'template' | 'reference'; + +/** Metadata for a doc type. */ +export interface DocTypeInfo { + /** Singular name (matches the DocTypeName key) */ + singular: string; + /** Plural name (used in commands and display) */ + plural: string; + /** Directory name on disk (e.g., 'shortcuts') */ + directory: string; +} + +/** Registry of all doc types and their metadata. */ +export const DOC_TYPES: Record = { + shortcut: { + singular: 'shortcut', + plural: 'shortcuts', + directory: 'shortcuts', + }, + guideline: { + singular: 'guideline', + plural: 'guidelines', + directory: 'guidelines', + }, + template: { + singular: 'template', + plural: 'templates', + directory: 'templates', + }, + reference: { + singular: 'reference', + plural: 'references', + directory: 'references', + }, +}; + +/** Map from directory name to doc type name for reverse lookup. */ +const DIRECTORY_TO_TYPE: Record = Object.fromEntries( + Object.entries(DOC_TYPES).map(([name, info]) => [info.directory, name as DocTypeName]), +) as Record; + +/** + * Infer the doc type from a file path. + * + * Recognizes paths in these formats: + * - `{prefix}/{type-dir}/{name}.md` (new prefix-based) + * - `{type-dir}/{name}.md` (flat) + * - `.tbd/docs/{...}/{type-dir}/{...}/{name}.md` (old-style nested) + */ +export function inferDocType(path: string): DocTypeName | undefined { + const parts = path.replace(/\\/g, '/').split('/'); + + // Check each path segment for a known doc type directory + for (const part of parts) { + if (part in DIRECTORY_TO_TYPE) { + return DIRECTORY_TO_TYPE[part]; + } + } + + return undefined; +} + +/** Get the directory name for a doc type. */ +export function getDocTypeDirectory(typeName: DocTypeName): string { + return DOC_TYPES[typeName].directory; +} + +/** Get all doc type names. */ +export function getAllDocTypeNames(): DocTypeName[] { + return Object.keys(DOC_TYPES) as DocTypeName[]; +} + +/** Get all doc type directory names. */ +export function getAllDocTypeDirectories(): string[] { + return Object.values(DOC_TYPES).map((t) => t.directory); +} diff --git a/packages/tbd/src/lib/paths.ts b/packages/tbd/src/lib/paths.ts index d3f2df0d..9e649446 100644 --- a/packages/tbd/src/lib/paths.ts +++ b/packages/tbd/src/lib/paths.ts @@ -27,6 +27,7 @@ */ import { join } from 'node:path'; +import { type DocTypeName, getDocTypeDirectory } from './doc-types.js'; /** The tbd configuration directory on main branch */ export const TBD_DIR = '.tbd'; @@ -168,38 +169,33 @@ export function isValidWorkspaceName(name: string): boolean { /** Docs directory name within .tbd/ */ export const DOCS_DIR = 'docs'; -/** Shortcuts directory name within docs/ */ +/** Doc type directory names */ export const SHORTCUTS_DIR = 'shortcuts'; +export const GUIDELINES_DIR = 'guidelines'; +export const TEMPLATES_DIR = 'templates'; -/** System shortcuts directory name (core docs like skill-baseline.md) */ -export const SYSTEM_DIR = 'system'; +/** Prefix names for doc sources */ +export const SYS_PREFIX = 'sys'; +export const TBD_PREFIX = 'tbd'; -/** Standard shortcuts directory name (workflow shortcuts) */ +/** @deprecated Legacy directory names kept for backward compatibility */ +export const SYSTEM_DIR = 'system'; export const STANDARD_DIR = 'standard'; -/** Guidelines directory name (coding rules and best practices) */ -export const GUIDELINES_DIR = 'guidelines'; - -/** Templates directory name (document templates) */ -export const TEMPLATES_DIR = 'templates'; - /** Full path to docs directory: .tbd/docs/ */ export const TBD_DOCS_DIR = join(TBD_DIR, DOCS_DIR); -/** Full path to shortcuts directory: .tbd/docs/shortcuts/ */ -export const TBD_SHORTCUTS_DIR = join(TBD_DOCS_DIR, SHORTCUTS_DIR); +/** Full path to system shortcuts: .tbd/docs/sys/shortcuts/ */ +export const TBD_SHORTCUTS_SYSTEM = join(TBD_DOCS_DIR, SYS_PREFIX, SHORTCUTS_DIR); -/** Full path to system shortcuts: .tbd/docs/shortcuts/system/ */ -export const TBD_SHORTCUTS_SYSTEM = join(TBD_SHORTCUTS_DIR, SYSTEM_DIR); +/** Full path to standard shortcuts: .tbd/docs/tbd/shortcuts/ */ +export const TBD_SHORTCUTS_STANDARD = join(TBD_DOCS_DIR, TBD_PREFIX, SHORTCUTS_DIR); -/** Full path to standard shortcuts: .tbd/docs/shortcuts/standard/ */ -export const TBD_SHORTCUTS_STANDARD = join(TBD_SHORTCUTS_DIR, STANDARD_DIR); +/** Full path to guidelines: .tbd/docs/tbd/guidelines/ */ +export const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, TBD_PREFIX, GUIDELINES_DIR); -/** Full path to guidelines: .tbd/docs/guidelines/ (top-level, not under shortcuts) */ -export const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, GUIDELINES_DIR); - -/** Full path to templates: .tbd/docs/templates/ (top-level, not under shortcuts) */ -export const TBD_TEMPLATES_DIR = join(TBD_DOCS_DIR, TEMPLATES_DIR); +/** Full path to templates: .tbd/docs/tbd/templates/ */ +export const TBD_TEMPLATES_DIR = join(TBD_DOCS_DIR, TBD_PREFIX, TEMPLATES_DIR); /** @deprecated Use TBD_GUIDELINES_DIR instead */ export const TBD_SHORTCUTS_GUIDELINES = TBD_GUIDELINES_DIR; @@ -207,14 +203,22 @@ export const TBD_SHORTCUTS_GUIDELINES = TBD_GUIDELINES_DIR; /** @deprecated Use TBD_TEMPLATES_DIR instead */ export const TBD_SHORTCUTS_TEMPLATES = TBD_TEMPLATES_DIR; -/** Built-in docs source paths (relative to package docs/) */ -export const BUILTIN_SHORTCUTS_SYSTEM = join(SHORTCUTS_DIR, SYSTEM_DIR); -export const BUILTIN_SHORTCUTS_STANDARD = join(SHORTCUTS_DIR, STANDARD_DIR); +/** @deprecated Legacy path constant */ +export const TBD_SHORTCUTS_DIR = join(TBD_DOCS_DIR, SHORTCUTS_DIR); -/** Built-in guidelines source path (relative to package docs/) */ +/** Built-in docs source paths (relative to package docs/, prefix-based) */ +export const BUILTIN_SYS_SHORTCUTS = join(SYS_PREFIX, SHORTCUTS_DIR); +export const BUILTIN_TBD_SHORTCUTS = join(TBD_PREFIX, SHORTCUTS_DIR); +export const BUILTIN_TBD_GUIDELINES = join(TBD_PREFIX, GUIDELINES_DIR); +export const BUILTIN_TBD_TEMPLATES = join(TBD_PREFIX, TEMPLATES_DIR); + +/** @deprecated Use BUILTIN_SYS_SHORTCUTS instead */ +export const BUILTIN_SHORTCUTS_SYSTEM = BUILTIN_SYS_SHORTCUTS; +/** @deprecated Use BUILTIN_TBD_SHORTCUTS instead */ +export const BUILTIN_SHORTCUTS_STANDARD = BUILTIN_TBD_SHORTCUTS; +/** @deprecated Use BUILTIN_TBD_GUIDELINES instead */ export const BUILTIN_GUIDELINES_DIR = GUIDELINES_DIR; - -/** Built-in templates source path (relative to package docs/) */ +/** @deprecated Use BUILTIN_TBD_TEMPLATES instead */ export const BUILTIN_TEMPLATES_DIR = TEMPLATES_DIR; /** Install directory name (header files for tool-specific installation) */ @@ -224,28 +228,21 @@ export const INSTALL_DIR = 'install'; export const BUILTIN_INSTALL_DIR = INSTALL_DIR; /** - * Default shortcut lookup paths (searched in order, relative to tbd root). - * Earlier paths take precedence over later paths. - * Note: Guidelines and templates are now separate top-level directories. - */ -export const DEFAULT_SHORTCUT_PATHS = [ - TBD_SHORTCUTS_SYSTEM, // .tbd/docs/shortcuts/system/ - TBD_SHORTCUTS_STANDARD, // .tbd/docs/shortcuts/standard/ -]; - -/** - * Default guidelines lookup paths (relative to tbd root). - */ -export const DEFAULT_GUIDELINES_PATHS = [ - TBD_GUIDELINES_DIR, // .tbd/docs/guidelines/ -]; - -/** - * Default template lookup paths (relative to tbd root). + * Get default lookup paths for a doc type, derived from the doc-types registry. + * + * Shortcuts get two lookup directories (sys + tbd prefixes). + * All other types get a single tbd-prefixed directory. + * + * @param typeName - The doc type name from the registry + * @returns Array of paths relative to tbd root (e.g., '.tbd/docs/tbd/guidelines') */ -export const DEFAULT_TEMPLATE_PATHS = [ - TBD_TEMPLATES_DIR, // .tbd/docs/templates/ -]; +export function getDefaultDocPaths(typeName: DocTypeName): string[] { + const dir = getDocTypeDirectory(typeName); + if (typeName === 'shortcut') { + return [join(TBD_DOCS_DIR, SYS_PREFIX, dir), join(TBD_DOCS_DIR, TBD_PREFIX, dir)]; + } + return [join(TBD_DOCS_DIR, TBD_PREFIX, dir)]; +} /** * Get the full path to an issue file. diff --git a/packages/tbd/src/lib/repo-url.ts b/packages/tbd/src/lib/repo-url.ts new file mode 100644 index 00000000..546f7f84 --- /dev/null +++ b/packages/tbd/src/lib/repo-url.ts @@ -0,0 +1,85 @@ +/** + * Repository URL normalization and slugification. + * + * Accepts all common repo URL formats (short, HTTPS, SSH) and normalizes + * them to a canonical form for consistent handling. + */ + +/** Normalized repo URL components. */ +export interface NormalizedRepoUrl { + host: string; + owner: string; + repo: string; +} + +/** + * Normalize a repo URL to its canonical components. + * + * Accepts: + * - Short: `github.com/org/repo` + * - HTTPS: `https://github.com/org/repo` or `https://github.com/org/repo.git` + * - SSH: `git@github.com:org/repo.git` + */ +export function normalizeRepoUrl(url: string): NormalizedRepoUrl { + const trimmed = url.trim(); + if (!trimmed) { + throw new Error('Repository URL cannot be empty'); + } + + let host: string; + let pathPart: string; + + // SSH format: git@github.com:org/repo.git + const sshMatch = /^git@([^:]+):(.+)$/.exec(trimmed); + if (sshMatch) { + host = sshMatch[1]!.toLowerCase(); + pathPart = sshMatch[2]!; + } else { + // Strip protocol + let cleaned = trimmed; + if (cleaned.startsWith('https://') || cleaned.startsWith('http://')) { + cleaned = cleaned.replace(/^https?:\/\//, ''); + } + + // Split host from path + const slashIndex = cleaned.indexOf('/'); + if (slashIndex === -1) { + throw new Error(`Invalid repository URL: ${url} (missing owner/repo path)`); + } + host = cleaned.slice(0, slashIndex).toLowerCase(); + pathPart = cleaned.slice(slashIndex + 1); + } + + // Clean up path: strip .git suffix and trailing slashes + pathPart = pathPart.replace(/\.git$/, '').replace(/\/+$/, ''); + + const parts = pathPart.split('/').filter(Boolean); + if (parts.length < 2) { + throw new Error(`Invalid repository URL: ${url} (need owner/repo)`); + } + + return { + host, + owner: parts[0]!, + repo: parts[1]!, + }; +} + +/** + * Convert a repo URL to a filesystem-safe slug for cache directories. + * + * All URL formats produce the same deterministic slug: + * `github.com-jlevy-speculate` + */ +export function repoUrlToSlug(url: string): string { + const { host, owner, repo } = normalizeRepoUrl(url); + return `${host}-${owner}-${repo}`; +} + +/** + * Get the HTTPS clone URL for a repo. + */ +export function getCloneUrl(url: string): string { + const { host, owner, repo } = normalizeRepoUrl(url); + return `https://${host}/${owner}/${repo}.git`; +} diff --git a/packages/tbd/src/lib/schemas.ts b/packages/tbd/src/lib/schemas.ts index d793a5a0..ace846ac 100644 --- a/packages/tbd/src/lib/schemas.ts +++ b/packages/tbd/src/lib/schemas.ts @@ -185,20 +185,44 @@ export const GitRemoteName = z /** * Doc cache configuration - maps destination paths to source locations. * - * Keys are destination paths relative to .tbd/docs/ (e.g., "shortcuts/standard/code-review-and-commit.md") + * Keys are destination paths relative to .tbd/docs/ (e.g., "tbd/shortcuts/code-review-and-commit.md") * Values are source locations: - * - internal: prefix for bundled docs (e.g., "internal:shortcuts/standard/code-review-and-commit.md") + * - internal: prefix for bundled docs (e.g., "internal:tbd/shortcuts/code-review-and-commit.md") * - Full URL for external docs (e.g., "https://raw.githubusercontent.com/org/repo/main/file.md") * * Example: * ```yaml * doc_cache: - * shortcuts/standard/code-review-and-commit.md: internal:shortcuts/standard/code-review-and-commit.md - * shortcuts/custom/my-shortcut.md: https://raw.githubusercontent.com/org/repo/main/shortcuts/my-shortcut.md + * tbd/shortcuts/code-review-and-commit.md: internal:tbd/shortcuts/code-review-and-commit.md + * guidelines/custom.md: https://example.com/custom.md * ``` */ export const DocCacheConfigSchema = z.record(z.string(), z.string()); +/** + * A documentation source: internal (bundled) or external (git repo). + * + * Sources are listed in precedence order in docs_cache.sources[]. + * See: docs/project/specs/active/plan-2026-02-02-external-docs-repos.md + */ +export const DocsSourceSchema = z.object({ + type: z.enum(['internal', 'repo']), + /** Namespace prefix for this source (1-16 lowercase alphanumeric + dash). */ + prefix: z + .string() + .min(1) + .max(16) + .regex(/^[a-z0-9-]+$/), + /** Repository URL (required for type: repo). */ + url: z.string().optional(), + /** Git ref to checkout (defaults to 'main' for repos). */ + ref: z.string().optional(), + /** Doc type directories to include from this source. */ + paths: z.array(z.string()), + /** Exclude from --list output. */ + hidden: z.boolean().optional(), +}); + /** * Documentation cache configuration (consolidated structure). * @@ -206,11 +230,16 @@ export const DocCacheConfigSchema = z.record(z.string(), z.string()); * See: docs/project/specs/active/plan-2026-01-26-docs-cache-config-restructure.md */ export const DocsCacheSchema = z.object({ + /** + * Ordered list of doc sources (internal bundles and external repos). + * Earlier sources take precedence on name collisions for unqualified lookups. + */ + sources: z.array(DocsSourceSchema).optional(), /** * Files to sync: maps destination paths to source locations. * Keys are destination paths relative to .tbd/docs/ * Values are source locations: - * - internal: prefix for bundled docs (e.g., "internal:shortcuts/standard/code-review-and-commit.md") + * - internal: prefix for bundled docs (e.g., "internal:tbd/shortcuts/code-review-and-commit.md") * - Full URL for external docs (e.g., "https://raw.githubusercontent.com/org/repo/main/file.md") */ files: z.record(z.string(), z.string()).optional(), @@ -218,9 +247,7 @@ export const DocsCacheSchema = z.object({ * Search paths for doc lookup (like shell $PATH). * Earlier paths take precedence when names conflict. */ - lookup_path: z - .array(z.string()) - .default(['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard']), + lookup_path: z.array(z.string()).default(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts']), }); /** diff --git a/packages/tbd/src/lib/tbd-format.ts b/packages/tbd/src/lib/tbd-format.ts index b493b77c..5d7986b0 100644 --- a/packages/tbd/src/lib/tbd-format.ts +++ b/packages/tbd/src/lib/tbd-format.ts @@ -37,7 +37,7 @@ * Current format version. * Bump this ONLY for breaking changes that require migration. */ -export const CURRENT_FORMAT = 'f03'; +export const CURRENT_FORMAT = 'f04'; /** * Initial format version for configs that don't have tbd_format field. @@ -84,6 +84,17 @@ export const FORMAT_HISTORY = { ], migration: 'Migrates old config keys to new docs_cache structure', }, + f04: { + introduced: '0.2.0', + description: 'Prefix-based doc sources with external repo support', + changes: [ + 'Added docs_cache.sources: array for internal and external repo sources', + 'Removed docs_cache.lookup_path: (replaced by source ordering)', + 'Converted default internal files: entries to sources: array', + 'Preserved custom file overrides in docs_cache.files:', + ], + migration: 'Converts files/lookup_path to sources array, preserves custom overrides', + }, } as const; export type FormatVersion = keyof typeof FORMAT_HISTORY; @@ -119,6 +130,15 @@ export interface RawConfig { docs_cache?: { files?: Record; lookup_path?: string[]; + // f04+: prefix-based sources + sources?: { + type: 'internal' | 'repo'; + prefix: string; + url?: string; + ref?: string; + paths: string[]; + hidden?: boolean; + }[]; }; } @@ -136,6 +156,8 @@ export interface MigrationResult { changed: boolean; /** Description of changes made */ changes: string[]; + /** Non-fatal warnings (e.g., preserved custom overrides) */ + warnings: string[]; } // ============================================================================= @@ -173,6 +195,7 @@ function migrate_f01_to_f02(config: RawConfig): MigrationResult { toFormat: 'f02', changed: changes.length > 0, changes, + warnings: [], }; } @@ -219,6 +242,91 @@ function migrate_f02_to_f03(config: RawConfig): MigrationResult { toFormat: 'f03', changed: changes.length > 0, changes, + warnings: [], + }; +} + +/** + * Check if a files: entry is a default internal mapping (source === 'internal:' + dest). + */ +function isDefaultFileEntry(dest: string, source: string): boolean { + return source === `internal:${dest}`; +} + +/** + * Get default sources for a fresh f04 config. + */ +function getDefaultSources(): NonNullable['sources']> { + return [ + { + type: 'internal' as const, + prefix: 'sys', + hidden: true, + paths: ['shortcuts/'], + }, + { + type: 'internal' as const, + prefix: 'tbd', + paths: ['shortcuts/', 'guidelines/', 'templates/'], + }, + ]; +} + +/** + * Migrate from f03 to f04. + * - Converts files: entries to sources: array + * - Removes lookup_path: (replaced by source ordering) + * - Preserves custom file overrides (non-internal entries) + */ +function migrate_f03_to_f04(config: RawConfig): MigrationResult { + const changes: string[] = []; + const migrated = { ...config }; + + // Update format version + migrated.tbd_format = 'f04'; + changes.push('Updated tbd_format: f04'); + + // Initialize docs_cache if needed + migrated.docs_cache = { ...migrated.docs_cache }; + + // Remove lookup_path + if (migrated.docs_cache.lookup_path) { + delete migrated.docs_cache.lookup_path; + changes.push('Removed docs_cache.lookup_path (replaced by source ordering)'); + } + + // Separate default internal files from custom overrides + const customFiles: Record = {}; + if (migrated.docs_cache.files) { + for (const [dest, source] of Object.entries(migrated.docs_cache.files)) { + if (!isDefaultFileEntry(dest, source)) { + customFiles[dest] = source; + } + } + } + + // Set up default sources + migrated.docs_cache.sources = getDefaultSources(); + changes.push('Added docs_cache.sources with default internal sources'); + + // Keep custom files if any, otherwise remove the files key + if (Object.keys(customFiles).length > 0) { + migrated.docs_cache.files = customFiles; + changes.push('Preserved custom file overrides in docs_cache.files'); + } else { + delete migrated.docs_cache.files; + if (config.docs_cache?.files && Object.keys(config.docs_cache.files).length > 0) { + changes.push('Removed default internal entries from docs_cache.files (now in sources)'); + } + } + + return { + config: migrated, + fromFormat: 'f03', + toFormat: 'f04', + changed: changes.length > 0, + changes, + warnings: [], }; } @@ -270,12 +378,14 @@ export function migrateToLatest(config: RawConfig): MigrationResult { toFormat: CURRENT_FORMAT, changed: false, changes: [], + warnings: [], }; } let current = config; let currentFormat: FormatVersion = fromFormat; const allChanges: string[] = []; + const allWarnings: string[] = []; // Apply migrations in sequence if (currentFormat === 'f01') { @@ -283,6 +393,7 @@ export function migrateToLatest(config: RawConfig): MigrationResult { current = result.config; currentFormat = 'f02' as FormatVersion; allChanges.push(...result.changes); + allWarnings.push(...result.warnings); } if (currentFormat === 'f02') { @@ -290,6 +401,15 @@ export function migrateToLatest(config: RawConfig): MigrationResult { current = result.config; currentFormat = 'f03' as FormatVersion; allChanges.push(...result.changes); + allWarnings.push(...result.warnings); + } + + if (currentFormat === 'f03') { + const result = migrate_f03_to_f04(current); + current = result.config; + currentFormat = 'f04' as FormatVersion; + allChanges.push(...result.changes); + allWarnings.push(...result.warnings); } // Add more migrations here as new format versions are added @@ -300,6 +420,7 @@ export function migrateToLatest(config: RawConfig): MigrationResult { toFormat: currentFormat, changed: allChanges.length > 0, changes: allChanges, + warnings: allWarnings, }; } @@ -338,6 +459,11 @@ export function describeMigration(fromFormat: FormatVersion): string[] { current = 'f03'; } + if (current === 'f03') { + descriptions.push('f03 → f04: Add prefix-based doc sources with external repo support'); + current = 'f04'; + } + // Add more migration descriptions here return descriptions; diff --git a/packages/tbd/tests/cli-advanced.tryscript.md b/packages/tbd/tests/cli-advanced.tryscript.md index ed9dfe11..2021f720 100644 --- a/packages/tbd/tests/cli-advanced.tryscript.md +++ b/packages/tbd/tests/cli-advanced.tryscript.md @@ -274,7 +274,7 @@ settings: ```console $ tbd config show --json { - "tbd_format": "f03", + "tbd_format": "f04", "tbd_version": "[..]", "sync": { "branch": "tbd-sync", diff --git a/packages/tbd/tests/cli-no-cache.tryscript.md b/packages/tbd/tests/cli-no-cache.tryscript.md index c74cc587..e25e607e 100644 --- a/packages/tbd/tests/cli-no-cache.tryscript.md +++ b/packages/tbd/tests/cli-no-cache.tryscript.md @@ -79,7 +79,7 @@ All set! # Test: Docs directory has shortcuts ```console -$ ls .tbd/docs/shortcuts/standard/ | wc -l | tr -d ' ' +$ ls .tbd/docs/tbd/shortcuts/ | wc -l | tr -d ' ' [..] ? 0 ``` diff --git a/packages/tbd/tests/cli-orientation-golden.tryscript.md b/packages/tbd/tests/cli-orientation-golden.tryscript.md index a1aa1376..9230826f 100644 --- a/packages/tbd/tests/cli-orientation-golden.tryscript.md +++ b/packages/tbd/tests/cli-orientation-golden.tryscript.md @@ -115,6 +115,7 @@ HEALTH CHECKS Run: tbd sync to push issues to remote ✓ Clone status ✓ Sync consistency +✓ Repo cache - no repo sources configured ⚠ Issues found that may require manual intervention. ? 0 diff --git a/packages/tbd/tests/cli-setup.tryscript.md b/packages/tbd/tests/cli-setup.tryscript.md index 928fcc16..e4fcc5c8 100644 --- a/packages/tbd/tests/cli-setup.tryscript.md +++ b/packages/tbd/tests/cli-setup.tryscript.md @@ -57,6 +57,7 @@ Documentation: shortcut [options] [query] Find and output documentation shortcuts guidelines [options] [query] Find and output coding guidelines template [options] [query] Find and output document templates + reference [options] [query] Find and output reference documents closing Display the session closing protocol reminder docs [options] [topic] Display CLI documentation (use tbd sync --docs for doc cache sync) @@ -67,6 +68,7 @@ Setup & Configuration: init [options] Initialize tbd in a git repository config Manage configuration setup [options] Configure tbd integration with editors and tools + source Manage doc sources (repos and internal bundles) Working With Issues: create [options] [title] Create a new issue diff --git a/packages/tbd/tests/config.test.ts b/packages/tbd/tests/config.test.ts index 7caf28b0..081d34fa 100644 --- a/packages/tbd/tests/config.test.ts +++ b/packages/tbd/tests/config.test.ts @@ -77,7 +77,7 @@ describe('config operations', () => { sync: { branch: 'custom-branch', remote: 'upstream' }, display: { id_prefix: 'td' }, settings: { auto_sync: true, doc_auto_sync_hours: 24, use_gh_cli: true }, - docs_cache: { lookup_path: ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'] }, + docs_cache: { lookup_path: ['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'] }, }; await writeConfig(tempDir, config); diff --git a/packages/tbd/tests/doc-add-e2e.test.ts b/packages/tbd/tests/doc-add-e2e.test.ts index ae448f2d..5d7dd2e4 100644 --- a/packages/tbd/tests/doc-add-e2e.test.ts +++ b/packages/tbd/tests/doc-add-e2e.test.ts @@ -78,13 +78,14 @@ describe('doc --add end-to-end', () => { } expect(addResult.status).toBe(0); - expect(addResult.stdout).toContain('Added to guidelines/modern-bun-monorepo-patterns.md'); + expect(addResult.stdout).toContain('Added to tbd/guidelines/modern-bun-monorepo-patterns.md'); - // Verify file exists + // Verify file exists in prefix-based path const docPath = join( tempDir, '.tbd', 'docs', + 'tbd', 'guidelines', 'modern-bun-monorepo-patterns.md', ); @@ -127,7 +128,7 @@ describe('doc --add end-to-end', () => { }); describe('tbd shortcut --add', () => { - it('adds a shortcut to shortcuts/custom/', async () => { + it('adds a shortcut to shortcuts/', async () => { initGitAndTbd(); const addResult = runTbd([ @@ -142,10 +143,10 @@ describe('doc --add end-to-end', () => { } expect(addResult.status).toBe(0); - expect(addResult.stdout).toContain('Added to shortcuts/custom/my-custom-shortcut.md'); + expect(addResult.stdout).toContain('Added to tbd/shortcuts/my-custom-shortcut.md'); - // Verify the file went to shortcuts/custom/ (not shortcuts/standard/) - const docPath = join(tempDir, '.tbd', 'docs', 'shortcuts', 'custom', 'my-custom-shortcut.md'); + // Verify the file went to tbd/shortcuts/ + const docPath = join(tempDir, '.tbd', 'docs', 'tbd', 'shortcuts', 'my-custom-shortcut.md'); await access(docPath); }); }); @@ -166,7 +167,7 @@ describe('doc --add end-to-end', () => { } expect(addResult.status).toBe(0); - expect(addResult.stdout).toContain('Added to templates/my-custom-template.md'); + expect(addResult.stdout).toContain('Added to tbd/templates/my-custom-template.md'); }); }); }); diff --git a/packages/tbd/tests/doc-add.test.ts b/packages/tbd/tests/doc-add.test.ts index 5992acb1..8ac77df4 100644 --- a/packages/tbd/tests/doc-add.test.ts +++ b/packages/tbd/tests/doc-add.test.ts @@ -122,8 +122,12 @@ describe('getDocTypeSubdir', () => { expect(getDocTypeSubdir('guideline')).toBe('guidelines'); }); - it('returns shortcuts/custom for shortcut type', () => { - expect(getDocTypeSubdir('shortcut')).toBe('shortcuts/custom'); + it('returns shortcuts for shortcut type', () => { + expect(getDocTypeSubdir('shortcut')).toBe('shortcuts'); + }); + + it('returns references for reference type', () => { + expect(getDocTypeSubdir('reference')).toBe('references'); }); it('returns templates for template type', () => { @@ -148,7 +152,7 @@ describe('addDoc', () => { tempDir = join(tmpdir(), `tbd-doc-add-test-${randomBytes(4).toString('hex')}`); await mkdir(tempDir, { recursive: true }); await mkdir(join(tempDir, '.tbd', 'docs', 'guidelines'), { recursive: true }); - await mkdir(join(tempDir, '.tbd', 'docs', 'shortcuts', 'custom'), { recursive: true }); + await mkdir(join(tempDir, '.tbd', 'docs', 'shortcuts'), { recursive: true }); await mkdir(join(tempDir, '.tbd', 'docs', 'templates'), { recursive: true }); // Create a minimal config.yml @@ -177,7 +181,7 @@ describe('addDoc', () => { docType: 'guideline', }); - expect(result.destPath).toBe('guidelines/modern-bun-monorepo-patterns.md'); + expect(result.destPath).toBe('tbd/guidelines/modern-bun-monorepo-patterns.md'); expect(result.rawUrl).toContain('raw.githubusercontent.com'); // Verify fetchWithGhFallback was called with the URL @@ -185,9 +189,9 @@ describe('addDoc', () => { 'https://raw.githubusercontent.com/org/repo/main/docs/file.md', ); - // Verify file was written + // Verify file was written to prefix-based path const content = await readFile( - join(tempDir, '.tbd', 'docs', 'guidelines', 'modern-bun-monorepo-patterns.md'), + join(tempDir, '.tbd', 'docs', 'tbd', 'guidelines', 'modern-bun-monorepo-patterns.md'), 'utf-8', ); expect(content).toBe('# Mocked Document\n\nThis is mocked content for testing.\n'); @@ -218,21 +222,21 @@ describe('addDoc', () => { }); // Should not double the .md - expect(result.destPath).toBe('guidelines/modern-bun-monorepo-patterns.md'); + expect(result.destPath).toBe('tbd/guidelines/modern-bun-monorepo-patterns.md'); }); - it('uses shortcuts/custom subdir for shortcut type', async () => { + it('uses shortcuts subdir for shortcut type', async () => { const result = await addDoc(tempDir, { url: 'https://raw.githubusercontent.com/org/repo/main/docs/file.md', name: 'test-shortcut', docType: 'shortcut', }); - expect(result.destPath).toBe('shortcuts/custom/test-shortcut.md'); + expect(result.destPath).toBe('tbd/shortcuts/test-shortcut.md'); // Verify file went to the right place const content = await readFile( - join(tempDir, '.tbd', 'docs', 'shortcuts', 'custom', 'test-shortcut.md'), + join(tempDir, '.tbd', 'docs', 'tbd', 'shortcuts', 'test-shortcut.md'), 'utf-8', ); expect(content).toBe('# Mocked Document\n\nThis is mocked content for testing.\n'); @@ -245,7 +249,7 @@ describe('addDoc', () => { docType: 'template', }); - expect(result.destPath).toBe('templates/test-template.md'); + expect(result.destPath).toBe('tbd/templates/test-template.md'); }); it('adds lookup_path entry if not already present', async () => { @@ -256,7 +260,7 @@ describe('addDoc', () => { }); const configContent = await readFile(join(tempDir, '.tbd', 'config.yml'), 'utf-8'); - expect(configContent).toContain('.tbd/docs/shortcuts/custom'); + expect(configContent).toContain('.tbd/docs/tbd/shortcuts'); }); it('does not duplicate lookup_path entry on second add', async () => { @@ -273,8 +277,8 @@ describe('addDoc', () => { }); const configContent = await readFile(join(tempDir, '.tbd', 'config.yml'), 'utf-8'); - // Count occurrences of .tbd/docs/guidelines - const matches = configContent.match(/\.tbd\/docs\/guidelines/g); + // Count occurrences of .tbd/docs/tbd/guidelines + const matches = configContent.match(/\.tbd\/docs\/tbd\/guidelines/g); expect(matches?.length).toBe(1); }); diff --git a/packages/tbd/tests/doc-cache.test.ts b/packages/tbd/tests/doc-cache.test.ts index 73c5f940..e73dea2a 100644 --- a/packages/tbd/tests/doc-cache.test.ts +++ b/packages/tbd/tests/doc-cache.test.ts @@ -8,6 +8,9 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { DocCache, + generateShortcutDirectory, + parseQualifiedName, + type CachedDoc, SCORE_EXACT_MATCH, SCORE_PREFIX_MATCH, SCORE_CONTAINS_ALL, @@ -23,8 +26,8 @@ describe('DocCache', () => { beforeEach(async () => { // Create temp directories for testing testDir = join(tmpdir(), `doc-cache-test-${Date.now()}`); - systemDir = join(testDir, '.tbd', 'docs', 'shortcuts', 'system'); - standardDir = join(testDir, '.tbd', 'docs', 'shortcuts', 'standard'); + systemDir = join(testDir, '.tbd', 'docs', 'sys', 'shortcuts'); + standardDir = join(testDir, '.tbd', 'docs', 'tbd', 'shortcuts'); await mkdir(systemDir, { recursive: true }); await mkdir(standardDir, { recursive: true }); }); @@ -39,10 +42,7 @@ describe('DocCache', () => { await writeFile(join(systemDir, 'skill-baseline.md'), '# Skill\n\nContent here.'); await writeFile(join(standardDir, 'workflow.md'), '# Workflow\n\nContent here.'); - const cache = new DocCache( - ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], - testDir, - ); + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const docs = cache.list(); @@ -56,7 +56,7 @@ describe('DocCache', () => { await writeFile(join(systemDir, 'readme.txt'), 'Not markdown'); await writeFile(join(systemDir, 'config.yml'), 'yaml: true'); - const cache = new DocCache(['.tbd/docs/shortcuts/system'], testDir); + const cache = new DocCache(['.tbd/docs/sys/shortcuts'], testDir); await cache.load(); const docs = cache.list(); @@ -66,7 +66,7 @@ describe('DocCache', () => { it('handles missing directories gracefully', async () => { const cache = new DocCache( - ['.tbd/docs/shortcuts/nonexistent', '.tbd/docs/shortcuts/system'], + ['.tbd/docs/shortcuts/nonexistent', '.tbd/docs/sys/shortcuts'], testDir, ); await cache.load(); @@ -90,7 +90,7 @@ Content here.`; await writeFile(join(standardDir, 'new-plan-spec.md'), content); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const docs = cache.list(); @@ -103,7 +103,7 @@ Content here.`; it('handles files without frontmatter', async () => { await writeFile(join(standardDir, 'simple.md'), '# Simple\n\nNo frontmatter.'); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const docs = cache.list(); @@ -116,7 +116,7 @@ Content here.`; it('finds document by exact name', async () => { await writeFile(join(standardDir, 'new-plan-spec.md'), '# Plan Spec'); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const match = cache.get('new-plan-spec'); @@ -128,7 +128,7 @@ Content here.`; it('finds document with .md extension in query', async () => { await writeFile(join(standardDir, 'workflow.md'), '# Workflow'); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const match = cache.get('workflow.md'); @@ -139,7 +139,7 @@ Content here.`; it('returns null for non-existent document', async () => { await writeFile(join(standardDir, 'exists.md'), '# Exists'); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const match = cache.get('nonexistent'); @@ -179,7 +179,7 @@ description: Quick exploration of a technical approach }); it('returns exact matches with highest score', async () => { - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search('new-plan-spec'); @@ -189,7 +189,7 @@ description: Quick exploration of a technical approach }); it('returns prefix matches with high score', async () => { - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search('new-plan'); @@ -199,7 +199,7 @@ description: Quick exploration of a technical approach }); it('matches documents containing all query words', async () => { - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search('plan spec'); @@ -210,7 +210,7 @@ description: Quick exploration of a technical approach }); it('matches against title and description', async () => { - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search('feature planning'); @@ -220,7 +220,7 @@ description: Quick exploration of a technical approach }); it('returns empty array for no matches', async () => { - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search('xyznonexistent123'); @@ -228,7 +228,7 @@ description: Quick exploration of a technical approach }); it('respects limit parameter', async () => { - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search('spec', 1); @@ -236,7 +236,7 @@ description: Quick exploration of a technical approach }); it('sorts results by score descending', async () => { - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search('spec'); @@ -250,7 +250,7 @@ description: Quick exploration of a technical approach it('handles empty query', async () => { await writeFile(join(standardDir, 'test.md'), '# Test'); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search(''); @@ -260,7 +260,7 @@ description: Quick exploration of a technical approach it('handles whitespace-only query', async () => { await writeFile(join(standardDir, 'test.md'), '# Test'); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search(' '); @@ -277,7 +277,7 @@ description: A test file [with brackets] # Test`, ); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); // Should not throw, may or may not find matches @@ -294,7 +294,7 @@ title: My Workflow # Content`, ); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const lowerMatches = cache.search('myworkflow'); @@ -316,7 +316,7 @@ title: New Plan Spec # Content`, ); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search(' new plan '); @@ -328,7 +328,7 @@ title: New Plan Spec await writeFile(join(standardDir, 'specification.md'), '# Spec'); await writeFile(join(standardDir, 'spec.md'), '# Spec'); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const matches = cache.search('spec'); @@ -349,7 +349,7 @@ not: closed: properly ); await writeFile(join(standardDir, 'valid.md'), '# Valid file'); - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); // Should load without throwing @@ -364,16 +364,13 @@ not: closed: properly await writeFile(join(systemDir, 'shared.md'), '# System version'); await writeFile(join(standardDir, 'shared.md'), '# Standard version'); - const cache = new DocCache( - ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], - testDir, - ); + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const match = cache.get('shared'); expect(match).not.toBeNull(); expect(match!.doc.content).toContain('System version'); - expect(match!.doc.sourceDir).toBe('.tbd/docs/shortcuts/system'); + expect(match!.doc.sourceDir).toBe('.tbd/docs/sys/shortcuts'); }); it('list() returns only active docs by default', async () => { @@ -381,10 +378,7 @@ not: closed: properly await writeFile(join(standardDir, 'shared.md'), '# Standard'); await writeFile(join(standardDir, 'unique.md'), '# Unique'); - const cache = new DocCache( - ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], - testDir, - ); + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const docs = cache.list(); @@ -396,10 +390,7 @@ not: closed: properly await writeFile(join(standardDir, 'shared.md'), '# Standard'); await writeFile(join(standardDir, 'unique.md'), '# Unique'); - const cache = new DocCache( - ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], - testDir, - ); + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const allDocs = cache.list(true); @@ -410,15 +401,12 @@ not: closed: properly await writeFile(join(systemDir, 'shared.md'), '# System'); await writeFile(join(standardDir, 'shared.md'), '# Standard'); - const cache = new DocCache( - ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], - testDir, - ); + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const allDocs = cache.list(true); - const systemDoc = allDocs.find((d) => d.sourceDir === '.tbd/docs/shortcuts/system'); - const standardDoc = allDocs.find((d) => d.sourceDir === '.tbd/docs/shortcuts/standard'); + const systemDoc = allDocs.find((d) => d.sourceDir === '.tbd/docs/sys/shortcuts'); + const standardDoc = allDocs.find((d) => d.sourceDir === '.tbd/docs/tbd/shortcuts'); expect(cache.isShadowed(systemDoc!)).toBe(false); expect(cache.isShadowed(standardDoc!)).toBe(true); @@ -442,3 +430,183 @@ not: closed: properly }); }); }); + +describe('generateShortcutDirectory', () => { + function makeCachedDoc(name: string, description: string, hidden?: boolean): CachedDoc { + return { + path: `/test/${name}.md`, + name, + frontmatter: { description }, + content: `# ${name}`, + sourceDir: '/test', + sizeBytes: 100, + approxTokens: 30, + hidden: hidden ?? false, + }; + } + + it('excludes docs with hidden=true from shortcut table', () => { + const shortcuts = [ + makeCachedDoc('skill', 'System skill file', true), + makeCachedDoc('skill-brief', 'Brief skill file', true), + makeCachedDoc('code-review', 'Review code changes'), + ]; + + const result = generateShortcutDirectory(shortcuts); + + expect(result).toContain('code-review'); + expect(result).not.toContain('| skill |'); + expect(result).not.toContain('| skill-brief |'); + }); + + it('excludes docs with hidden=true from guidelines table', () => { + const shortcuts = [makeCachedDoc('workflow', 'A workflow')]; + const guidelines = [ + makeCachedDoc('internal-guide', 'Internal only', true), + makeCachedDoc('typescript-rules', 'TypeScript best practices'), + ]; + + const result = generateShortcutDirectory(shortcuts, guidelines); + + expect(result).toContain('typescript-rules'); + expect(result).not.toContain('internal-guide'); + }); + + it('includes all docs when none are hidden', () => { + const shortcuts = [ + makeCachedDoc('review', 'Review code'), + makeCachedDoc('commit', 'Commit changes'), + ]; + + const result = generateShortcutDirectory(shortcuts); + + expect(result).toContain('review'); + expect(result).toContain('commit'); + }); + + it('shows empty message when all shortcuts are hidden', () => { + const shortcuts = [ + makeCachedDoc('skill', 'Hidden', true), + makeCachedDoc('skill-brief', 'Hidden', true), + ]; + + const result = generateShortcutDirectory(shortcuts); + + expect(result).toContain('No shortcuts available'); + }); +}); + +describe('parseQualifiedName', () => { + it('parses unqualified name', () => { + const result = parseQualifiedName('typescript-rules'); + expect(result).toEqual({ baseName: 'typescript-rules' }); + }); + + it('parses qualified name with prefix', () => { + const result = parseQualifiedName('spec:typescript-rules'); + expect(result).toEqual({ prefix: 'spec', baseName: 'typescript-rules' }); + }); + + it('handles names with multiple colons (first colon is separator)', () => { + const result = parseQualifiedName('spec:some:doc-name'); + expect(result).toEqual({ prefix: 'spec', baseName: 'some:doc-name' }); + }); + + it('handles empty prefix (colon at start)', () => { + const result = parseQualifiedName(':typescript-rules'); + expect(result).toEqual({ baseName: ':typescript-rules' }); + }); + + it('strips .md extension', () => { + const result = parseQualifiedName('typescript-rules.md'); + expect(result).toEqual({ baseName: 'typescript-rules' }); + }); + + it('strips .md extension from qualified name', () => { + const result = parseQualifiedName('spec:typescript-rules.md'); + expect(result).toEqual({ prefix: 'spec', baseName: 'typescript-rules' }); + }); +}); + +describe('DocCache prefix-aware lookup', () => { + let testDir: string; + + beforeEach(async () => { + testDir = join(tmpdir(), `doc-cache-prefix-test-${Date.now()}`); + // Create prefix-based directories + await mkdir(join(testDir, '.tbd', 'docs', 'sys', 'shortcuts'), { recursive: true }); + await mkdir(join(testDir, '.tbd', 'docs', 'tbd', 'shortcuts'), { recursive: true }); + await mkdir(join(testDir, '.tbd', 'docs', 'spec', 'shortcuts'), { recursive: true }); + }); + + afterEach(async () => { + await rm(testDir, { recursive: true, force: true }); + }); + + it('loads docs with prefix from directory structure', async () => { + await writeFile( + join(testDir, '.tbd', 'docs', 'tbd', 'shortcuts', 'commit.md'), + '---\ntitle: Commit\n---\n# Commit', + ); + + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const match = cache.get('commit'); + expect(match).not.toBeNull(); + expect(match!.doc.prefix).toBe('tbd'); + }); + + it('resolves qualified name to specific prefix', async () => { + // Same name in two prefixes + await writeFile(join(testDir, '.tbd', 'docs', 'tbd', 'shortcuts', 'review.md'), '# TBD Review'); + await writeFile( + join(testDir, '.tbd', 'docs', 'spec', 'shortcuts', 'review.md'), + '# Spec Review', + ); + + const cache = new DocCache(['.tbd/docs/tbd/shortcuts', '.tbd/docs/spec/shortcuts'], testDir); + await cache.load(); + + // Qualified lookup should return specific prefix + const match = cache.get('spec:review'); + expect(match).not.toBeNull(); + expect(match!.doc.content).toContain('Spec Review'); + expect(match!.doc.prefix).toBe('spec'); + }); + + it('unqualified name returns first match in path order', async () => { + await writeFile(join(testDir, '.tbd', 'docs', 'tbd', 'shortcuts', 'review.md'), '# TBD Review'); + await writeFile( + join(testDir, '.tbd', 'docs', 'spec', 'shortcuts', 'review.md'), + '# Spec Review', + ); + + const cache = new DocCache(['.tbd/docs/tbd/shortcuts', '.tbd/docs/spec/shortcuts'], testDir); + await cache.load(); + + // Unqualified should return first (tbd) + const match = cache.get('review'); + expect(match).not.toBeNull(); + expect(match!.doc.content).toContain('TBD Review'); + expect(match!.doc.prefix).toBe('tbd'); + }); + + it('list() filters out hidden docs by default', async () => { + await writeFile( + join(testDir, '.tbd', 'docs', 'sys', 'shortcuts', 'skill.md'), + '---\ntitle: Skill\n---\n# Skill', + ); + await writeFile( + join(testDir, '.tbd', 'docs', 'tbd', 'shortcuts', 'commit.md'), + '---\ntitle: Commit\n---\n# Commit', + ); + + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + // sys docs are not hidden by default (hidden is set at source level, not directory level) + const docs = cache.list(); + expect(docs.length).toBe(2); + }); +}); diff --git a/packages/tbd/tests/doc-sync.test.ts b/packages/tbd/tests/doc-sync.test.ts index 44b6cfcf..11e75b5e 100644 --- a/packages/tbd/tests/doc-sync.test.ts +++ b/packages/tbd/tests/doc-sync.test.ts @@ -14,6 +14,11 @@ import { mergeDocCacheConfig, internalDocExists, pruneStaleInternals, + resolveSourcesToDocs, + getSourcesHash, + readSourcesHash, + writeSourcesHash, + shouldClearDocsCache, } from '../src/file/doc-sync.js'; describe('doc-sync', () => { @@ -32,10 +37,10 @@ describe('doc-sync', () => { describe('DocSync.parseSource', () => { it('parses internal source', () => { const sync = new DocSync(tempDir, {}); - const source = sync.parseSource('internal:shortcuts/standard/code-review-and-commit.md'); + const source = sync.parseSource('internal:tbd/shortcuts/code-review-and-commit.md'); expect(source.type).toBe('internal'); - expect(source.location).toBe('shortcuts/standard/code-review-and-commit.md'); + expect(source.location).toBe('tbd/shortcuts/code-review-and-commit.md'); }); it('parses URL source', () => { @@ -244,8 +249,8 @@ describe('doc-sync', () => { describe('internalDocExists', () => { it('returns true for existing bundled docs', async () => { // This tests against the actual bundled docs in the package - // shortcuts/standard/code-review-and-commit.md should exist - const exists = await internalDocExists('shortcuts/standard/code-review-and-commit.md'); + // tbd/shortcuts/code-review-and-commit.md should exist + const exists = await internalDocExists('tbd/shortcuts/code-review-and-commit.md'); expect(exists).toBe(true); }); @@ -264,14 +269,14 @@ describe('doc-sync', () => { describe('pruneStaleInternals', () => { it('keeps entries with existing internal sources', async () => { const config = { - 'shortcuts/standard/code-review-and-commit.md': - 'internal:shortcuts/standard/code-review-and-commit.md', + 'tbd/shortcuts/code-review-and-commit.md': + 'internal:tbd/shortcuts/code-review-and-commit.md', }; const result = await pruneStaleInternals(config); - expect(result.config['shortcuts/standard/code-review-and-commit.md']).toBe( - 'internal:shortcuts/standard/code-review-and-commit.md', + expect(result.config['tbd/shortcuts/code-review-and-commit.md']).toBe( + 'internal:tbd/shortcuts/code-review-and-commit.md', ); expect(result.pruned).toEqual([]); }); @@ -300,8 +305,8 @@ describe('doc-sync', () => { it('handles mixed configs correctly', async () => { const config = { - 'shortcuts/standard/code-review-and-commit.md': - 'internal:shortcuts/standard/code-review-and-commit.md', // exists + 'tbd/shortcuts/code-review-and-commit.md': + 'internal:tbd/shortcuts/code-review-and-commit.md', // exists 'stale/doc.md': 'internal:nonexistent/fake-doc.md', // doesn't exist 'external/doc.md': 'https://example.com/doc.md', // URL, always kept }; @@ -309,8 +314,8 @@ describe('doc-sync', () => { const result = await pruneStaleInternals(config); // Existing internal kept - expect(result.config['shortcuts/standard/code-review-and-commit.md']).toBe( - 'internal:shortcuts/standard/code-review-and-commit.md', + expect(result.config['tbd/shortcuts/code-review-and-commit.md']).toBe( + 'internal:tbd/shortcuts/code-review-and-commit.md', ); // Non-existent internal pruned expect(result.config['stale/doc.md']).toBeUndefined(); @@ -330,4 +335,409 @@ describe('doc-sync', () => { expect(result.pruned).toEqual([]); }); }); + + describe('resolveSourcesToDocs', () => { + it('resolves internal source to file entries', async () => { + const sources = [ + { + type: 'internal' as const, + prefix: 'tbd', + paths: ['shortcuts/'], + }, + ]; + + const result = await resolveSourcesToDocs(sources); + + // Should contain bundled tbd shortcuts with prefix-based keys + expect(result['tbd/shortcuts/code-review-and-commit.md']).toBe( + 'internal:tbd/shortcuts/code-review-and-commit.md', + ); + }); + + it('resolves internal source with hidden flag', async () => { + const sources = [ + { + type: 'internal' as const, + prefix: 'sys', + hidden: true, + paths: ['shortcuts/'], + }, + ]; + + const result = await resolveSourcesToDocs(sources); + + // Should contain sys shortcuts + expect(result['sys/shortcuts/skill-baseline.md']).toBe( + 'internal:sys/shortcuts/skill-baseline.md', + ); + }); + + it('resolves multiple internal sources', async () => { + const sources = [ + { + type: 'internal' as const, + prefix: 'sys', + hidden: true, + paths: ['shortcuts/'], + }, + { + type: 'internal' as const, + prefix: 'tbd', + paths: ['shortcuts/', 'guidelines/', 'templates/'], + }, + ]; + + const result = await resolveSourcesToDocs(sources); + + // sys shortcuts + expect(result['sys/shortcuts/skill-baseline.md']).toBe( + 'internal:sys/shortcuts/skill-baseline.md', + ); + // tbd shortcuts + expect(result['tbd/shortcuts/code-review-and-commit.md']).toBe( + 'internal:tbd/shortcuts/code-review-and-commit.md', + ); + // tbd guidelines + expect(result['tbd/guidelines/typescript-rules.md']).toBe( + 'internal:tbd/guidelines/typescript-rules.md', + ); + // tbd templates + expect(result['tbd/templates/plan-spec.md']).toBe('internal:tbd/templates/plan-spec.md'); + }); + + it('applies files overrides last', async () => { + const sources = [ + { + type: 'internal' as const, + prefix: 'tbd', + paths: ['shortcuts/'], + }, + ]; + + const filesOverrides = { + 'tbd/shortcuts/code-review-and-commit.md': 'https://example.com/custom-commit-shortcut.md', + 'custom/my-doc.md': 'https://example.com/my-doc.md', + }; + + const result = await resolveSourcesToDocs(sources, filesOverrides); + + // Override should win over internal source + expect(result['tbd/shortcuts/code-review-and-commit.md']).toBe( + 'https://example.com/custom-commit-shortcut.md', + ); + // Custom file entry should be added + expect(result['custom/my-doc.md']).toBe('https://example.com/my-doc.md'); + }); + + it('returns empty map for empty sources', async () => { + const result = await resolveSourcesToDocs([]); + + expect(Object.keys(result)).toEqual([]); + }); + + it('returns only files overrides when no sources', async () => { + const filesOverrides = { + 'custom/doc.md': 'https://example.com/doc.md', + }; + + const result = await resolveSourcesToDocs([], filesOverrides); + + expect(result['custom/doc.md']).toBe('https://example.com/doc.md'); + expect(Object.keys(result).length).toBe(1); + }); + + it('handles non-existent internal path gracefully', async () => { + const sources = [ + { + type: 'internal' as const, + prefix: 'custom', + paths: ['nonexistent-dir/'], + }, + ]; + + const result = await resolveSourcesToDocs(sources); + + // No entries for non-existent paths + expect(Object.keys(result).length).toBe(0); + }); + }); + + describe('getSourcesHash', () => { + it('returns deterministic hash for same sources', () => { + const sources = [{ type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }]; + + const hash1 = getSourcesHash(sources); + const hash2 = getSourcesHash(sources); + + expect(hash1).toBe(hash2); + }); + + it('returns 8-character hex string', () => { + const sources = [{ type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }]; + + const hash = getSourcesHash(sources); + + expect(hash).toMatch(/^[a-f0-9]{8}$/); + }); + + it('returns different hash for different sources', () => { + const sources1 = [{ type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }]; + const sources2 = [{ type: 'internal' as const, prefix: 'sys', paths: ['shortcuts/'] }]; + + const hash1 = getSourcesHash(sources1); + const hash2 = getSourcesHash(sources2); + + expect(hash1).not.toBe(hash2); + }); + + it('returns different hash when source order changes', () => { + const sources1 = [ + { type: 'internal' as const, prefix: 'sys', paths: ['shortcuts/'] }, + { type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }, + ]; + const sources2 = [ + { type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }, + { type: 'internal' as const, prefix: 'sys', paths: ['shortcuts/'] }, + ]; + + const hash1 = getSourcesHash(sources1); + const hash2 = getSourcesHash(sources2); + + expect(hash1).not.toBe(hash2); + }); + + it('returns empty string for empty sources', () => { + const hash = getSourcesHash([]); + + // Empty sources should produce a consistent hash + expect(hash).toMatch(/^[a-f0-9]{8}$/); + }); + }); + + describe('readSourcesHash / writeSourcesHash', () => { + it('returns undefined when no hash file exists', async () => { + const hash = await readSourcesHash(tempDir); + + expect(hash).toBeUndefined(); + }); + + it('writes and reads hash', async () => { + await writeSourcesHash(tempDir, 'abcd1234'); + const hash = await readSourcesHash(tempDir); + + expect(hash).toBe('abcd1234'); + }); + + it('overwrites existing hash', async () => { + await writeSourcesHash(tempDir, 'abcd1234'); + await writeSourcesHash(tempDir, 'efgh5678'); + const hash = await readSourcesHash(tempDir); + + expect(hash).toBe('efgh5678'); + }); + }); + + describe('shouldClearDocsCache', () => { + it('returns true when no hash file exists (first sync)', async () => { + const sources = [{ type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }]; + + const result = await shouldClearDocsCache(tempDir, sources); + + expect(result).toBe(true); + }); + + it('returns false when hash matches current sources', async () => { + const sources = [{ type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }]; + + const hash = getSourcesHash(sources); + await writeSourcesHash(tempDir, hash); + + const result = await shouldClearDocsCache(tempDir, sources); + + expect(result).toBe(false); + }); + + it('returns true when sources have changed', async () => { + const oldSources = [{ type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }]; + const newSources = [ + { type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }, + { type: 'internal' as const, prefix: 'sys', paths: ['shortcuts/'] }, + ]; + + const hash = getSourcesHash(oldSources); + await writeSourcesHash(tempDir, hash); + + const result = await shouldClearDocsCache(tempDir, newSources); + + expect(result).toBe(true); + }); + + it('returns true when source order changes', async () => { + const sources1 = [ + { type: 'internal' as const, prefix: 'sys', paths: ['shortcuts/'] }, + { type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }, + ]; + const sources2 = [ + { type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }, + { type: 'internal' as const, prefix: 'sys', paths: ['shortcuts/'] }, + ]; + + const hash = getSourcesHash(sources1); + await writeSourcesHash(tempDir, hash); + + const result = await shouldClearDocsCache(tempDir, sources2); + + expect(result).toBe(true); + }); + + it('returns false for empty sources when hash matches', async () => { + const sources: { type: 'internal' | 'repo'; prefix: string; paths: string[] }[] = []; + const hash = getSourcesHash(sources); + await writeSourcesHash(tempDir, hash); + + const result = await shouldClearDocsCache(tempDir, sources); + + expect(result).toBe(false); + }); + }); + + describe('integration: resolveSourcesToDocs → DocSync cycle', () => { + it('resolves default sources and syncs files to .tbd/docs/', async () => { + // Default sources (matching what f04 migration produces) + const sources = [ + { type: 'internal' as const, prefix: 'sys', hidden: true, paths: ['shortcuts/'] }, + { + type: 'internal' as const, + prefix: 'tbd', + paths: ['shortcuts/', 'guidelines/', 'templates/'], + }, + ]; + + // Resolve sources to flat file map + const fileMap = await resolveSourcesToDocs(sources); + + // Verify resolution produced expected entries + expect(Object.keys(fileMap).length).toBeGreaterThan(0); + expect(fileMap['sys/shortcuts/skill-baseline.md']).toBe( + 'internal:sys/shortcuts/skill-baseline.md', + ); + expect(fileMap['tbd/shortcuts/code-review-and-commit.md']).toBe( + 'internal:tbd/shortcuts/code-review-and-commit.md', + ); + expect(fileMap['tbd/guidelines/typescript-rules.md']).toBe( + 'internal:tbd/guidelines/typescript-rules.md', + ); + + // Sync using the resolved file map + const sync = new DocSync(tempDir, fileMap); + const result = await sync.sync(); + + // Should have added files + expect(result.added.length).toBeGreaterThan(0); + expect(result.errors.length).toBe(0); + expect(result.success).toBe(true); + + // Verify files actually exist on disk + const skillContent = await readFile( + join(tempDir, '.tbd', 'docs', 'sys', 'shortcuts', 'skill-baseline.md'), + 'utf-8', + ); + expect(skillContent).toContain('tbd'); + + const commitContent = await readFile( + join(tempDir, '.tbd', 'docs', 'tbd', 'shortcuts', 'code-review-and-commit.md'), + 'utf-8', + ); + expect(commitContent.length).toBeGreaterThan(0); + }); + + it('sources hash detects change and enables cache clear', async () => { + const sources1 = [{ type: 'internal' as const, prefix: 'tbd', paths: ['shortcuts/'] }]; + + // Write initial hash + const hash1 = getSourcesHash(sources1); + await writeSourcesHash(tempDir, hash1); + + // Same sources: no clear needed + expect(await shouldClearDocsCache(tempDir, sources1)).toBe(false); + + // Add a new source: clear needed + const sources2 = [ + ...sources1, + { type: 'internal' as const, prefix: 'sys', paths: ['shortcuts/'] }, + ]; + expect(await shouldClearDocsCache(tempDir, sources2)).toBe(true); + + // After writing new hash: no clear needed + await writeSourcesHash(tempDir, getSourcesHash(sources2)); + expect(await shouldClearDocsCache(tempDir, sources2)).toBe(false); + }); + + it('end-to-end: default f04 sources produce correct prefix directories', async () => { + // Simulate the default sources from f04 migration + const defaultSources = [ + { type: 'internal' as const, prefix: 'sys', hidden: true, paths: ['shortcuts/'] }, + { + type: 'internal' as const, + prefix: 'tbd', + paths: ['shortcuts/', 'guidelines/', 'templates/'], + }, + ]; + + // Step 1: Resolve sources to file map + const fileMap = await resolveSourcesToDocs(defaultSources); + + // Step 2: Verify all expected prefix/type combos are present + const prefixTypes = new Set(); + for (const key of Object.keys(fileMap)) { + const parts = key.split('/'); + if (parts.length >= 2) { + prefixTypes.add(`${parts[0]}/${parts[1]}`); + } + } + expect(prefixTypes.has('sys/shortcuts')).toBe(true); + expect(prefixTypes.has('tbd/shortcuts')).toBe(true); + expect(prefixTypes.has('tbd/guidelines')).toBe(true); + expect(prefixTypes.has('tbd/templates')).toBe(true); + + // Step 3: Sync to disk + const sync = new DocSync(tempDir, fileMap); + const result = await sync.sync(); + + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + expect(result.added.length).toBeGreaterThan(50); // Should sync 50+ docs + + // Step 4: Verify directory structure + const { readdir: rd } = await import('node:fs/promises'); + + const sysShortcuts = await rd(join(tempDir, '.tbd', 'docs', 'sys', 'shortcuts')); + expect(sysShortcuts.length).toBeGreaterThan(0); + expect(sysShortcuts).toContain('skill-baseline.md'); + + const tbdShortcuts = await rd(join(tempDir, '.tbd', 'docs', 'tbd', 'shortcuts')); + expect(tbdShortcuts.length).toBeGreaterThan(0); + expect(tbdShortcuts).toContain('code-review-and-commit.md'); + + const tbdGuidelines = await rd(join(tempDir, '.tbd', 'docs', 'tbd', 'guidelines')); + expect(tbdGuidelines.length).toBeGreaterThan(0); + expect(tbdGuidelines).toContain('typescript-rules.md'); + + const tbdTemplates = await rd(join(tempDir, '.tbd', 'docs', 'tbd', 'templates')); + expect(tbdTemplates.length).toBeGreaterThan(0); + expect(tbdTemplates).toContain('plan-spec.md'); + + // Step 5: Write and verify sources hash + const hash = getSourcesHash(defaultSources); + await writeSourcesHash(tempDir, hash); + expect(await shouldClearDocsCache(tempDir, defaultSources)).toBe(false); + + // Step 6: Resync should report no changes + const resync = new DocSync(tempDir, fileMap); + const result2 = await resync.sync(); + expect(result2.added).toEqual([]); + expect(result2.updated).toEqual([]); + expect(result2.removed).toEqual([]); + }); + }); }); diff --git a/packages/tbd/tests/doc-types.test.ts b/packages/tbd/tests/doc-types.test.ts new file mode 100644 index 00000000..8ba587d7 --- /dev/null +++ b/packages/tbd/tests/doc-types.test.ts @@ -0,0 +1,159 @@ +/** + * Tests for doc-types.ts - doc type registry. + */ + +import { describe, it, expect } from 'vitest'; +import { + DOC_TYPES, + inferDocType, + getDocTypeDirectory, + getAllDocTypeNames, + getAllDocTypeDirectories, +} from '../src/lib/doc-types.js'; +import { getDefaultDocPaths, TBD_DOCS_DIR } from '../src/lib/paths.js'; +import { join } from 'node:path'; + +describe('doc-types', () => { + describe('DOC_TYPES registry', () => { + it('has all four doc types', () => { + expect(DOC_TYPES.shortcut).toBeDefined(); + expect(DOC_TYPES.guideline).toBeDefined(); + expect(DOC_TYPES.template).toBeDefined(); + expect(DOC_TYPES.reference).toBeDefined(); + }); + + it('each type has directory and plural fields', () => { + for (const [name, type] of Object.entries(DOC_TYPES)) { + expect(type.directory).toBeDefined(); + expect(type.directory.length).toBeGreaterThan(0); + expect(type.plural).toBeDefined(); + expect(type.plural.length).toBeGreaterThan(0); + expect(type.singular).toBe(name); + } + }); + + it('shortcut type maps to shortcuts/ directory', () => { + expect(DOC_TYPES.shortcut.directory).toBe('shortcuts'); + expect(DOC_TYPES.shortcut.plural).toBe('shortcuts'); + }); + + it('guideline type maps to guidelines/ directory', () => { + expect(DOC_TYPES.guideline.directory).toBe('guidelines'); + expect(DOC_TYPES.guideline.plural).toBe('guidelines'); + }); + + it('template type maps to templates/ directory', () => { + expect(DOC_TYPES.template.directory).toBe('templates'); + expect(DOC_TYPES.template.plural).toBe('templates'); + }); + + it('reference type maps to references/ directory', () => { + expect(DOC_TYPES.reference.directory).toBe('references'); + expect(DOC_TYPES.reference.plural).toBe('references'); + }); + }); + + describe('inferDocType', () => { + it('infers shortcut from prefix-based path', () => { + expect(inferDocType('sys/shortcuts/skill.md')).toBe('shortcut'); + expect(inferDocType('tbd/shortcuts/code-review.md')).toBe('shortcut'); + }); + + it('infers guideline from prefix-based path', () => { + expect(inferDocType('spec/guidelines/typescript-rules.md')).toBe('guideline'); + }); + + it('infers template from prefix-based path', () => { + expect(inferDocType('tbd/templates/plan-spec.md')).toBe('template'); + }); + + it('infers reference from prefix-based path', () => { + expect(inferDocType('spec/references/api-ref.md')).toBe('reference'); + }); + + it('infers from flat paths', () => { + expect(inferDocType('shortcuts/code-review.md')).toBe('shortcut'); + expect(inferDocType('guidelines/typescript-rules.md')).toBe('guideline'); + expect(inferDocType('templates/plan-spec.md')).toBe('template'); + expect(inferDocType('references/api-ref.md')).toBe('reference'); + }); + + it('infers from old-style nested paths', () => { + expect(inferDocType('.tbd/docs/tbd/shortcuts/code-review.md')).toBe('shortcut'); + expect(inferDocType('.tbd/docs/sys/shortcuts/skill.md')).toBe('shortcut'); + }); + + it('returns undefined for unrecognized paths', () => { + expect(inferDocType('unknown/foo.md')).toBeUndefined(); + expect(inferDocType('foo.md')).toBeUndefined(); + }); + }); + + describe('getDocTypeDirectory', () => { + it('returns directory name for each type', () => { + expect(getDocTypeDirectory('shortcut')).toBe('shortcuts'); + expect(getDocTypeDirectory('guideline')).toBe('guidelines'); + expect(getDocTypeDirectory('template')).toBe('templates'); + expect(getDocTypeDirectory('reference')).toBe('references'); + }); + }); + + describe('getAllDocTypeNames', () => { + it('returns all type names', () => { + const names = getAllDocTypeNames(); + expect(names).toContain('shortcut'); + expect(names).toContain('guideline'); + expect(names).toContain('template'); + expect(names).toContain('reference'); + expect(names).toHaveLength(4); + }); + }); + + describe('getAllDocTypeDirectories', () => { + it('returns all directory names', () => { + const dirs = getAllDocTypeDirectories(); + expect(dirs).toContain('shortcuts'); + expect(dirs).toContain('guidelines'); + expect(dirs).toContain('templates'); + expect(dirs).toContain('references'); + expect(dirs).toHaveLength(4); + }); + }); + + describe('getDefaultDocPaths', () => { + it('returns sys + tbd prefixed paths for shortcuts', () => { + const paths = getDefaultDocPaths('shortcut'); + expect(paths).toEqual([ + join(TBD_DOCS_DIR, 'sys', 'shortcuts'), + join(TBD_DOCS_DIR, 'tbd', 'shortcuts'), + ]); + }); + + it('returns tbd-prefixed path for guidelines', () => { + const paths = getDefaultDocPaths('guideline'); + expect(paths).toEqual([join(TBD_DOCS_DIR, 'tbd', 'guidelines')]); + }); + + it('returns tbd-prefixed path for templates', () => { + const paths = getDefaultDocPaths('template'); + expect(paths).toEqual([join(TBD_DOCS_DIR, 'tbd', 'templates')]); + }); + + it('returns tbd-prefixed path for references', () => { + const paths = getDefaultDocPaths('reference'); + expect(paths).toEqual([join(TBD_DOCS_DIR, 'tbd', 'references')]); + }); + + it('uses directory names from DOC_TYPES registry', () => { + // Verify the function derives paths from the registry, not hardcoded + for (const typeName of getAllDocTypeNames()) { + const paths = getDefaultDocPaths(typeName); + const dir = DOC_TYPES[typeName].directory; + // Every path should contain the doc type's directory + for (const p of paths) { + expect(p).toContain(dir); + } + } + }); + }); +}); diff --git a/packages/tbd/tests/doctor-sync.test.ts b/packages/tbd/tests/doctor-sync.test.ts index 5649b6aa..4acce920 100644 --- a/packages/tbd/tests/doctor-sync.test.ts +++ b/packages/tbd/tests/doctor-sync.test.ts @@ -188,6 +188,74 @@ vb4g: 01aaaaaaaaaaaaaaaaaaaaaa04`; }); }); +describe('checkRepoCacheHealth', () => { + let testDir: string; + + beforeEach(async () => { + testDir = join(tmpdir(), `tbd-repocache-test-${randomBytes(4).toString('hex')}`); + await mkdir(join(testDir, '.tbd'), { recursive: true }); + }); + + afterEach(async () => { + await rm(testDir, { recursive: true, force: true }); + }); + + it('returns ok with no repo sources', async () => { + const { checkRepoCacheHealth } = await import('../src/cli/commands/doctor.js'); + const result = await checkRepoCacheHealth(testDir, []); + expect(result.status).toBe('ok'); + expect(result.message).toContain('no repo sources'); + }); + + it('warns when repo source cache dir is missing', async () => { + const { checkRepoCacheHealth } = await import('../src/cli/commands/doctor.js'); + const sources = [ + { + type: 'repo' as const, + prefix: 'ext', + url: 'https://github.com/org/repo', + ref: 'main', + paths: ['guidelines'], + }, + ]; + const result = await checkRepoCacheHealth(testDir, sources); + expect(result.status).toBe('warn'); + expect(result.message).toContain('missing'); + expect(result.suggestion).toContain('tbd sync --docs'); + }); + + it('returns ok when repo cache dir exists', async () => { + const { checkRepoCacheHealth } = await import('../src/cli/commands/doctor.js'); + const { repoUrlToSlug } = await import('../src/lib/repo-url.js'); + const url = 'https://github.com/org/repo'; + const slug = repoUrlToSlug(url); + await mkdir(join(testDir, '.tbd', 'repo-cache', slug), { recursive: true }); + const sources = [ + { type: 'repo' as const, prefix: 'ext', url, ref: 'main', paths: ['guidelines'] }, + ]; + const result = await checkRepoCacheHealth(testDir, sources); + expect(result.status).toBe('ok'); + }); + + it('warns about orphaned cache dirs', async () => { + const { checkRepoCacheHealth } = await import('../src/cli/commands/doctor.js'); + // Create a cache dir that's not referenced by any source + await mkdir(join(testDir, '.tbd', 'repo-cache', 'orphaned-slug'), { recursive: true }); + const result = await checkRepoCacheHealth(testDir, []); + expect(result.status).toBe('warn'); + expect(result.details).toBeDefined(); + expect(result.details!.some((d: string) => d.includes('orphaned'))).toBe(true); + }); + + it('skips internal sources', async () => { + const { checkRepoCacheHealth } = await import('../src/cli/commands/doctor.js'); + const sources = [{ type: 'internal' as const, prefix: 'sys', paths: ['shortcuts'] }]; + const result = await checkRepoCacheHealth(testDir, sources); + expect(result.status).toBe('ok'); + expect(result.message).toContain('no repo sources'); + }); +}); + describe('sync status logic', () => { let testDir: string; const issuesDir = DATA_SYNC_DIR; diff --git a/packages/tbd/tests/fixtures/test-docs/guidelines/test-python-rules.md b/packages/tbd/tests/fixtures/test-docs/guidelines/test-python-rules.md new file mode 100644 index 00000000..99aedc4f --- /dev/null +++ b/packages/tbd/tests/fixtures/test-docs/guidelines/test-python-rules.md @@ -0,0 +1,11 @@ +--- +title: Test Python Rules +description: Test guideline for Python best practices +category: language +tags: + - python + - testing +--- +# Test Python Rules + +Use type hints. Follow PEP 8. diff --git a/packages/tbd/tests/fixtures/test-docs/guidelines/test-typescript-rules.md b/packages/tbd/tests/fixtures/test-docs/guidelines/test-typescript-rules.md new file mode 100644 index 00000000..ebd6142d --- /dev/null +++ b/packages/tbd/tests/fixtures/test-docs/guidelines/test-typescript-rules.md @@ -0,0 +1,12 @@ +--- +title: Test TypeScript Rules +description: Test guideline for TypeScript best practices +category: language +tags: + - typescript + - testing +--- +# Test TypeScript Rules + +Use strict TypeScript settings. +Prefer const over let. diff --git a/packages/tbd/tests/fixtures/test-docs/references/test-api-ref.md b/packages/tbd/tests/fixtures/test-docs/references/test-api-ref.md new file mode 100644 index 00000000..8e02718a --- /dev/null +++ b/packages/tbd/tests/fixtures/test-docs/references/test-api-ref.md @@ -0,0 +1,15 @@ +--- +title: Test API Reference +description: Test reference document for API endpoints +category: api +tags: + - api + - reference +--- +# Test API Reference + +## Endpoints + +- `GET /api/items` - List all items +- `POST /api/items` - Create an item +- `GET /api/items/:id` - Get item by ID diff --git a/packages/tbd/tests/fixtures/test-docs/references/test-config-ref.md b/packages/tbd/tests/fixtures/test-docs/references/test-config-ref.md new file mode 100644 index 00000000..d4e7d30d --- /dev/null +++ b/packages/tbd/tests/fixtures/test-docs/references/test-config-ref.md @@ -0,0 +1,12 @@ +--- +title: Test Config Reference +description: Test reference for configuration options +--- +# Configuration Reference + +## Options + +| Option | Default | Description | +| --- | --- | --- | +| `auto_sync` | `true` | Enable automatic syncing | +| `prefix` | `tbd` | ID prefix for issues | diff --git a/packages/tbd/tests/fixtures/test-docs/shortcuts/test-commit.md b/packages/tbd/tests/fixtures/test-docs/shortcuts/test-commit.md new file mode 100644 index 00000000..e90ad476 --- /dev/null +++ b/packages/tbd/tests/fixtures/test-docs/shortcuts/test-commit.md @@ -0,0 +1,8 @@ +--- +title: Test Commit +description: Test shortcut for committing code +category: git +--- +# Test Commit Shortcut + +Instructions for committing code with proper checks. diff --git a/packages/tbd/tests/fixtures/test-docs/shortcuts/test-no-frontmatter.md b/packages/tbd/tests/fixtures/test-docs/shortcuts/test-no-frontmatter.md new file mode 100644 index 00000000..ed0067b1 --- /dev/null +++ b/packages/tbd/tests/fixtures/test-docs/shortcuts/test-no-frontmatter.md @@ -0,0 +1,3 @@ +# Simple Shortcut + +A shortcut without frontmatter for testing edge cases. diff --git a/packages/tbd/tests/fixtures/test-docs/shortcuts/test-review.md b/packages/tbd/tests/fixtures/test-docs/shortcuts/test-review.md new file mode 100644 index 00000000..3d500fab --- /dev/null +++ b/packages/tbd/tests/fixtures/test-docs/shortcuts/test-review.md @@ -0,0 +1,15 @@ +--- +title: Test Review +description: Test shortcut for reviewing code changes +category: review +tags: + - testing + - review +--- +# Test Review Shortcut + +Instructions for reviewing test code changes. + +1. Check test coverage +2. Review assertions +3. Verify edge cases diff --git a/packages/tbd/tests/fixtures/test-docs/templates/test-spec.md b/packages/tbd/tests/fixtures/test-docs/templates/test-spec.md new file mode 100644 index 00000000..9bf7e65a --- /dev/null +++ b/packages/tbd/tests/fixtures/test-docs/templates/test-spec.md @@ -0,0 +1,17 @@ +--- +title: Test Spec Template +description: Test template for creating specifications +category: planning +--- +# {{title}} + +**Date:** {{date}} +**Status:** Draft + +## Summary + +{{summary}} + +## Implementation + +TBD diff --git a/packages/tbd/tests/helpers/doc-test-utils.ts b/packages/tbd/tests/helpers/doc-test-utils.ts new file mode 100644 index 00000000..e7725e4b --- /dev/null +++ b/packages/tbd/tests/helpers/doc-test-utils.ts @@ -0,0 +1,129 @@ +/** + * Test utilities for doc infrastructure tests. + * + * Provides helpers for creating temp doc directories, populating with fixture + * docs, and generating mock configs. + */ + +import { mkdir, writeFile, cp, rm } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const fixturesDir = join(__dirname, '..', 'fixtures', 'test-docs'); + +const execFileAsync = promisify(execFile); + +/** + * Create a temporary directory with .tbd/docs/ structure. + * Returns the test root dir (parent of .tbd/). + */ +export async function createTempDocsDir(prefix = 'doc-test'): Promise { + const testDir = join( + tmpdir(), + `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); + await mkdir(join(testDir, '.tbd', 'docs'), { recursive: true }); + return testDir; +} + +/** + * Populate a temp doc directory with fixture docs. + * Copies from tests/fixtures/test-docs/ into the target .tbd/docs/ structure. + */ +export async function populateTestDocs( + testDir: string, + options?: { + /** Which doc types to include (default: all) */ + types?: ('shortcuts' | 'guidelines' | 'templates' | 'references')[]; + /** Subdirectory under .tbd/docs/ to copy into (default: direct to type dirs) */ + subDir?: string; + }, +): Promise { + const types = options?.types ?? ['shortcuts', 'guidelines', 'templates', 'references']; + const baseDir = options?.subDir + ? join(testDir, '.tbd', 'docs', options.subDir) + : join(testDir, '.tbd', 'docs'); + + for (const type of types) { + const srcDir = join(fixturesDir, type); + const destDir = join(baseDir, type); + await mkdir(destDir, { recursive: true }); + try { + await cp(srcDir, destDir, { recursive: true }); + } catch { + // Source may not exist for some types + } + } +} + +/** + * Create a mock config object for testing. + */ +export function createMockConfig(overrides?: { + files?: Record; + lookupPath?: string[]; +}): Record { + return { + tbd_format: 'f03', + tbd_version: '0.1.17', + display: { id_prefix: 'test' }, + settings: { auto_sync: false, doc_auto_sync_hours: 24 }, + docs_cache: { + files: overrides?.files ?? {}, + lookup_path: overrides?.lookupPath ?? ['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], + }, + }; +} + +/** + * Clean up a temp directory. + */ +export async function cleanupTempDir(testDir: string): Promise { + await rm(testDir, { recursive: true, force: true }); +} + +/** + * Create a local bare git repo for RepoCache testing. + * Returns the path to the bare repo. + */ +export async function createTestBareRepo(files: Record): Promise { + const repoDir = join( + tmpdir(), + `test-repo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); + await mkdir(repoDir, { recursive: true }); + + // Initialize a normal repo, add files, then clone to bare + const workDir = join( + tmpdir(), + `test-repo-work-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); + await mkdir(workDir, { recursive: true }); + + await execFileAsync('git', ['init', '-b', 'main', workDir]); + await execFileAsync('git', ['-C', workDir, 'config', 'user.email', 'test@test.com']); + await execFileAsync('git', ['-C', workDir, 'config', 'user.name', 'Test']); + await execFileAsync('git', ['-C', workDir, 'config', 'commit.gpgsign', 'false']); + + // Write files + for (const [filePath, content] of Object.entries(files)) { + const fullPath = join(workDir, filePath); + await mkdir(dirname(fullPath), { recursive: true }); + await writeFile(fullPath, content); + } + + await execFileAsync('git', ['-C', workDir, 'add', '-A']); + await execFileAsync('git', ['-C', workDir, 'commit', '-m', 'Initial commit']); + + // Clone to bare + await execFileAsync('git', ['clone', '--bare', workDir, repoDir]); + + // Clean up work dir + await rm(workDir, { recursive: true, force: true }); + + return repoDir; +} diff --git a/packages/tbd/tests/integration-files.test.ts b/packages/tbd/tests/integration-files.test.ts index c284c050..b6deacbe 100644 --- a/packages/tbd/tests/integration-files.test.ts +++ b/packages/tbd/tests/integration-files.test.ts @@ -5,7 +5,7 @@ * Note: SKILL.md is NOT pre-built in dist/docs. * It is dynamically generated at setup/install time by combining: * - Header (from dist/docs/install/claude-header.md) - * - Base skill content (from dist/docs/shortcuts/system/skill-baseline.md) + * - Base skill content (from dist/docs/sys/shortcuts/skill-baseline.md) * - Shortcut directory (generated from available shortcuts) */ @@ -19,7 +19,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // Source files are in dist/docs after build const docsDir = join(__dirname, '..', 'dist', 'docs'); const installDir = join(docsDir, 'install'); -const shortcutsSystemDir = join(docsDir, 'shortcuts', 'system'); +const shortcutsSystemDir = join(docsDir, 'sys', 'shortcuts'); describe('integration file formats', () => { describe('claude-header.md (source for SKILL.md)', () => { diff --git a/packages/tbd/tests/reference.test.ts b/packages/tbd/tests/reference.test.ts new file mode 100644 index 00000000..b1105c60 --- /dev/null +++ b/packages/tbd/tests/reference.test.ts @@ -0,0 +1,91 @@ +/** + * Tests for reference command behavior. + * + * References are documentation files (API references, etc.) that follow + * the same DocCommandHandler pattern as guidelines and templates. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdir, writeFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { DocCache } from '../src/file/doc-cache.js'; +import { getDefaultDocPaths } from '../src/lib/paths.js'; + +describe('reference command behavior', () => { + let testDir: string; + let referencesDir: string; + + beforeEach(async () => { + testDir = join(tmpdir(), `reference-test-${Date.now()}`); + referencesDir = join(testDir, '.tbd', 'docs', 'tbd', 'references'); + await mkdir(referencesDir, { recursive: true }); + + await writeFile( + join(referencesDir, 'api-reference.md'), + `--- +title: API Reference +description: REST API documentation +--- +# API Reference + +Endpoints and methods.`, + ); + await writeFile( + join(referencesDir, 'data-model.md'), + `--- +title: Data Model +description: Database schema reference +--- +# Data Model + +Tables and relationships.`, + ); + }); + + afterEach(async () => { + await rm(testDir, { recursive: true, force: true }); + }); + + it('loads references from default doc paths', async () => { + const paths = getDefaultDocPaths('reference'); + const cache = new DocCache(paths, testDir); + await cache.load({ quiet: true }); + const docs = cache.list(); + expect(docs.length).toBe(2); + const names = docs.map((d) => d.name).sort(); + expect(names).toEqual(['api-reference', 'data-model']); + }); + + it('finds reference by exact name', async () => { + const paths = getDefaultDocPaths('reference'); + const cache = new DocCache(paths, testDir); + await cache.load({ quiet: true }); + const match = cache.get('api-reference'); + expect(match).not.toBeNull(); + expect(match!.doc.name).toBe('api-reference'); + expect(match!.doc.content).toContain('Endpoints and methods'); + }); + + it('fuzzy searches references', async () => { + const paths = getDefaultDocPaths('reference'); + const cache = new DocCache(paths, testDir); + await cache.load({ quiet: true }); + const results = cache.search('database'); + expect(results.length).toBeGreaterThan(0); + expect(results[0]!.doc.name).toBe('data-model'); + }); + + it('returns JSON-serializable doc list', async () => { + const paths = getDefaultDocPaths('reference'); + const cache = new DocCache(paths, testDir); + await cache.load({ quiet: true }); + const docs = cache.list(); + for (const doc of docs) { + expect(doc.name).toBeDefined(); + expect(doc.path).toBeDefined(); + expect(doc.frontmatter?.title).toBeDefined(); + expect(doc.frontmatter?.description).toBeDefined(); + } + }); +}); diff --git a/packages/tbd/tests/repo-cache.test.ts b/packages/tbd/tests/repo-cache.test.ts new file mode 100644 index 00000000..0aa01a0f --- /dev/null +++ b/packages/tbd/tests/repo-cache.test.ts @@ -0,0 +1,141 @@ +/** + * Tests for repo-cache.ts - sparse git repo checkout caching. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { join } from 'node:path'; +import { mkdir, readFile, rm, readdir } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { createTestBareRepo } from './helpers/doc-test-utils.js'; +import { RepoCache } from '../src/file/repo-cache.js'; + +describe('repo-cache', () => { + let tbdRoot: string; + let bareRepoPath: string; + + beforeEach(async () => { + tbdRoot = join( + tmpdir(), + `repo-cache-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); + await mkdir(join(tbdRoot, '.tbd'), { recursive: true }); + + // Create a bare repo with docs content + bareRepoPath = await createTestBareRepo({ + 'shortcuts/code-review.md': + '---\nname: code-review\ndescription: Review code\n---\n# Code Review\n', + 'shortcuts/commit.md': '---\nname: commit\ndescription: Commit code\n---\n# Commit\n', + 'guidelines/typescript-rules.md': + '---\nname: typescript-rules\ndescription: TS rules\n---\n# TypeScript Rules\n', + 'templates/spec.md': '---\nname: spec\ndescription: Spec template\n---\n# Spec\n', + 'README.md': '# Test Repo\n', + 'src/index.ts': 'console.log("hello");\n', + }); + }); + + afterEach(async () => { + await rm(tbdRoot, { recursive: true, force: true }); + await rm(bareRepoPath, { recursive: true, force: true }); + }); + + describe('constructor', () => { + it('creates RepoCache with cacheDir under .tbd/', () => { + const cache = new RepoCache(tbdRoot); + expect(cache.cacheDir).toBe(join(tbdRoot, '.tbd', 'repo-cache')); + }); + }); + + describe('ensureRepo', () => { + it('clones a repo on first access', async () => { + const cache = new RepoCache(tbdRoot); + const repoDir = await cache.ensureRepo(bareRepoPath, 'main', ['shortcuts/']); + expect(repoDir).toContain('repo-cache'); + + // Verify files were checked out + const content = await readFile(join(repoDir, 'shortcuts', 'code-review.md'), 'utf-8'); + expect(content).toContain('# Code Review'); + }); + + it('returns same dir on second access (cached)', async () => { + const cache = new RepoCache(tbdRoot); + const dir1 = await cache.ensureRepo(bareRepoPath, 'main', ['shortcuts/']); + const dir2 = await cache.ensureRepo(bareRepoPath, 'main', ['shortcuts/']); + expect(dir1).toBe(dir2); + }); + + it('clones multiple paths', async () => { + const cache = new RepoCache(tbdRoot); + const repoDir = await cache.ensureRepo(bareRepoPath, 'main', [ + 'shortcuts/', + 'guidelines/', + 'templates/', + ]); + + const shortcuts = await readdir(join(repoDir, 'shortcuts')); + expect(shortcuts).toContain('code-review.md'); + expect(shortcuts).toContain('commit.md'); + + const guidelines = await readdir(join(repoDir, 'guidelines')); + expect(guidelines).toContain('typescript-rules.md'); + }); + + it('throws on invalid repo URL', async () => { + const cache = new RepoCache(tbdRoot); + await expect( + cache.ensureRepo('/nonexistent/repo.git', 'main', ['shortcuts/']), + ).rejects.toThrow(); + }); + }); + + describe('scanDocs', () => { + it('finds all .md files in specified paths', async () => { + const cache = new RepoCache(tbdRoot); + const repoDir = await cache.ensureRepo(bareRepoPath, 'main', ['shortcuts/', 'guidelines/']); + + const docs = await cache.scanDocs(repoDir, ['shortcuts/', 'guidelines/']); + expect(docs.length).toBe(3); // 2 shortcuts + 1 guideline + expect(docs.map((d) => d.relativePath).sort()).toEqual([ + 'guidelines/typescript-rules.md', + 'shortcuts/code-review.md', + 'shortcuts/commit.md', + ]); + }); + + it('returns empty array for paths with no .md files', async () => { + const cache = new RepoCache(tbdRoot); + const repoDir = await cache.ensureRepo(bareRepoPath, 'main', ['shortcuts/']); + const docs = await cache.scanDocs(repoDir, ['nonexistent/']); + expect(docs).toEqual([]); + }); + + it('does not include non-.md files', async () => { + const cache = new RepoCache(tbdRoot); + const repoDir = await cache.ensureRepo(bareRepoPath, 'main', ['src/']); + const docs = await cache.scanDocs(repoDir, ['src/']); + expect(docs).toEqual([]); + }); + + it('includes relativePath and content for each doc', async () => { + const cache = new RepoCache(tbdRoot); + const repoDir = await cache.ensureRepo(bareRepoPath, 'main', ['guidelines/']); + const docs = await cache.scanDocs(repoDir, ['guidelines/']); + + expect(docs).toHaveLength(1); + expect(docs[0]!.relativePath).toBe('guidelines/typescript-rules.md'); + expect(docs[0]!.content).toContain('# TypeScript Rules'); + }); + }); + + describe('getRepoDir', () => { + it('returns deterministic directory for a repo URL', () => { + const cache = new RepoCache(tbdRoot); + const dir1 = cache.getRepoDir('github.com/jlevy/speculate'); + const dir2 = cache.getRepoDir('github.com/jlevy/speculate'); + expect(dir1).toBe(dir2); + + // Different URL should give different dir + const dir3 = cache.getRepoDir('github.com/jlevy/other'); + expect(dir3).not.toBe(dir1); + }); + }); +}); diff --git a/packages/tbd/tests/repo-url.test.ts b/packages/tbd/tests/repo-url.test.ts new file mode 100644 index 00000000..c4c50940 --- /dev/null +++ b/packages/tbd/tests/repo-url.test.ts @@ -0,0 +1,118 @@ +/** + * Tests for repo-url.ts - URL normalization and slugification. + */ + +import { describe, it, expect } from 'vitest'; +import { normalizeRepoUrl, repoUrlToSlug, getCloneUrl } from '../src/lib/repo-url.js'; + +describe('repo-url', () => { + describe('normalizeRepoUrl', () => { + it('normalizes short format (github.com/org/repo)', () => { + const result = normalizeRepoUrl('github.com/jlevy/speculate'); + expect(result.host).toBe('github.com'); + expect(result.owner).toBe('jlevy'); + expect(result.repo).toBe('speculate'); + }); + + it('normalizes HTTPS URL', () => { + const result = normalizeRepoUrl('https://github.com/jlevy/speculate'); + expect(result.host).toBe('github.com'); + expect(result.owner).toBe('jlevy'); + expect(result.repo).toBe('speculate'); + }); + + it('normalizes HTTPS URL with .git suffix', () => { + const result = normalizeRepoUrl('https://github.com/jlevy/speculate.git'); + expect(result.host).toBe('github.com'); + expect(result.owner).toBe('jlevy'); + expect(result.repo).toBe('speculate'); + }); + + it('normalizes SSH URL (git@)', () => { + const result = normalizeRepoUrl('git@github.com:jlevy/speculate.git'); + expect(result.host).toBe('github.com'); + expect(result.owner).toBe('jlevy'); + expect(result.repo).toBe('speculate'); + }); + + it('normalizes SSH URL without .git suffix', () => { + const result = normalizeRepoUrl('git@github.com:jlevy/speculate'); + expect(result.host).toBe('github.com'); + expect(result.owner).toBe('jlevy'); + expect(result.repo).toBe('speculate'); + }); + + it('strips trailing slash', () => { + const result = normalizeRepoUrl('github.com/jlevy/speculate/'); + expect(result.repo).toBe('speculate'); + }); + + it('handles mixed case host', () => { + const result = normalizeRepoUrl('GitHub.com/jlevy/speculate'); + expect(result.host).toBe('github.com'); + }); + + it('throws on invalid URL', () => { + expect(() => normalizeRepoUrl('')).toThrow(); + expect(() => normalizeRepoUrl('not-a-url')).toThrow(); + expect(() => normalizeRepoUrl('github.com')).toThrow(); + expect(() => normalizeRepoUrl('github.com/only-owner')).toThrow(); + }); + }); + + describe('repoUrlToSlug', () => { + it('converts short URL to filesystem slug', () => { + expect(repoUrlToSlug('github.com/jlevy/speculate')).toBe('github.com-jlevy-speculate'); + }); + + it('converts HTTPS URL to slug', () => { + expect(repoUrlToSlug('https://github.com/jlevy/speculate')).toBe( + 'github.com-jlevy-speculate', + ); + }); + + it('converts SSH URL to slug', () => { + expect(repoUrlToSlug('git@github.com:jlevy/speculate.git')).toBe( + 'github.com-jlevy-speculate', + ); + }); + + it('is deterministic (same input always produces same output)', () => { + const inputs = [ + 'github.com/jlevy/speculate', + 'https://github.com/jlevy/speculate', + 'https://github.com/jlevy/speculate.git', + 'git@github.com:jlevy/speculate.git', + ]; + const slugs = inputs.map(repoUrlToSlug); + // All should produce the same slug + expect(new Set(slugs).size).toBe(1); + }); + + it('produces different slugs for different repos', () => { + const slug1 = repoUrlToSlug('github.com/jlevy/speculate'); + const slug2 = repoUrlToSlug('github.com/jlevy/tbd'); + expect(slug1).not.toBe(slug2); + }); + }); + + describe('getCloneUrl', () => { + it('returns HTTPS clone URL from short format', () => { + expect(getCloneUrl('github.com/jlevy/speculate')).toBe( + 'https://github.com/jlevy/speculate.git', + ); + }); + + it('returns HTTPS clone URL from SSH format', () => { + expect(getCloneUrl('git@github.com:jlevy/speculate.git')).toBe( + 'https://github.com/jlevy/speculate.git', + ); + }); + + it('returns normalized HTTPS clone URL', () => { + expect(getCloneUrl('https://github.com/jlevy/speculate')).toBe( + 'https://github.com/jlevy/speculate.git', + ); + }); + }); +}); diff --git a/packages/tbd/tests/schemas.test.ts b/packages/tbd/tests/schemas.test.ts index 5250e404..db27acd3 100644 --- a/packages/tbd/tests/schemas.test.ts +++ b/packages/tbd/tests/schemas.test.ts @@ -9,6 +9,8 @@ import { ShortId, ExternalIssueIdInput, ConfigSchema, + DocsSourceSchema, + DocsCacheSchema, } from '../src/lib/schemas.js'; // Sample valid ULID for testing (26 lowercase alphanumeric chars) @@ -277,8 +279,8 @@ describe('ConfigSchema', () => { display: { id_prefix: 'proj' }, docs_cache: { files: { - 'shortcuts/standard/code-review-and-commit.md': - 'internal:shortcuts/standard/code-review-and-commit.md', + 'tbd/shortcuts/code-review-and-commit.md': + 'internal:tbd/shortcuts/code-review-and-commit.md', 'custom/my-doc.md': 'https://example.com/my-doc.md', }, }, @@ -288,8 +290,8 @@ describe('ConfigSchema', () => { expect(result.success).toBe(true); if (result.success) { expect(result.data.docs_cache?.files).toEqual({ - 'shortcuts/standard/code-review-and-commit.md': - 'internal:shortcuts/standard/code-review-and-commit.md', + 'tbd/shortcuts/code-review-and-commit.md': + 'internal:tbd/shortcuts/code-review-and-commit.md', 'custom/my-doc.md': 'https://example.com/my-doc.md', }); } @@ -302,8 +304,8 @@ describe('ConfigSchema', () => { docs_cache: { lookup_path: [ '.tbd/docs/shortcuts/custom', - '.tbd/docs/shortcuts/system', - '.tbd/docs/shortcuts/standard', + '.tbd/docs/sys/shortcuts', + '.tbd/docs/tbd/shortcuts', ], }, }; @@ -313,8 +315,8 @@ describe('ConfigSchema', () => { if (result.success) { expect(result.data.docs_cache?.lookup_path).toEqual([ '.tbd/docs/shortcuts/custom', - '.tbd/docs/shortcuts/system', - '.tbd/docs/shortcuts/standard', + '.tbd/docs/sys/shortcuts', + '.tbd/docs/tbd/shortcuts', ]); } }); @@ -325,10 +327,10 @@ describe('ConfigSchema', () => { display: { id_prefix: 'proj' }, docs_cache: { files: { - 'shortcuts/standard/code-review-and-commit.md': - 'internal:shortcuts/standard/code-review-and-commit.md', + 'tbd/shortcuts/code-review-and-commit.md': + 'internal:tbd/shortcuts/code-review-and-commit.md', }, - lookup_path: ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], + lookup_path: ['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], }, }; @@ -351,10 +353,240 @@ describe('ConfigSchema', () => { expect(result.success).toBe(true); if (result.success) { expect(result.data.docs_cache?.lookup_path).toEqual([ - '.tbd/docs/shortcuts/system', - '.tbd/docs/shortcuts/standard', + '.tbd/docs/sys/shortcuts', + '.tbd/docs/tbd/shortcuts', ]); } }); }); }); + +describe('DocsSourceSchema', () => { + describe('valid internal sources', () => { + it('accepts minimal internal source', () => { + const source = { + type: 'internal', + prefix: 'sys', + paths: ['shortcuts/'], + }; + const result = DocsSourceSchema.safeParse(source); + expect(result.success).toBe(true); + }); + + it('accepts internal source with hidden flag', () => { + const source = { + type: 'internal', + prefix: 'sys', + hidden: true, + paths: ['shortcuts/'], + }; + const result = DocsSourceSchema.safeParse(source); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.hidden).toBe(true); + } + }); + + it('accepts internal source with multiple paths', () => { + const source = { + type: 'internal', + prefix: 'tbd', + paths: ['shortcuts/', 'guidelines/', 'templates/'], + }; + const result = DocsSourceSchema.safeParse(source); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.paths).toHaveLength(3); + } + }); + }); + + describe('valid repo sources', () => { + it('accepts minimal repo source', () => { + const source = { + type: 'repo', + prefix: 'spec', + url: 'github.com/jlevy/speculate', + paths: ['shortcuts/'], + }; + const result = DocsSourceSchema.safeParse(source); + expect(result.success).toBe(true); + }); + + it('accepts repo source with ref', () => { + const source = { + type: 'repo', + prefix: 'spec', + url: 'github.com/jlevy/speculate', + ref: 'main', + paths: ['shortcuts/', 'guidelines/', 'templates/', 'references/'], + }; + const result = DocsSourceSchema.safeParse(source); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.ref).toBe('main'); + expect(result.data.paths).toHaveLength(4); + } + }); + + it('accepts repo source with SSH URL', () => { + const source = { + type: 'repo', + prefix: 'myorg', + url: 'git@github.com:myorg/coding-standards.git', + paths: ['guidelines/'], + }; + const result = DocsSourceSchema.safeParse(source); + expect(result.success).toBe(true); + }); + + it('accepts repo source with HTTPS URL', () => { + const source = { + type: 'repo', + prefix: 'myorg', + url: 'https://github.com/myorg/coding-standards', + paths: ['guidelines/'], + }; + const result = DocsSourceSchema.safeParse(source); + expect(result.success).toBe(true); + }); + }); + + describe('prefix validation', () => { + it('accepts lowercase alphanumeric prefixes', () => { + expect( + DocsSourceSchema.safeParse({ type: 'internal', prefix: 'a', paths: ['x/'] }).success, + ).toBe(true); + expect( + DocsSourceSchema.safeParse({ type: 'internal', prefix: 'sys', paths: ['x/'] }).success, + ).toBe(true); + expect( + DocsSourceSchema.safeParse({ type: 'internal', prefix: 'my-org', paths: ['x/'] }).success, + ).toBe(true); + expect( + DocsSourceSchema.safeParse({ type: 'internal', prefix: 'abc123', paths: ['x/'] }).success, + ).toBe(true); + }); + + it('rejects empty prefix', () => { + const result = DocsSourceSchema.safeParse({ type: 'internal', prefix: '', paths: ['x/'] }); + expect(result.success).toBe(false); + }); + + it('rejects prefix longer than 16 chars', () => { + const result = DocsSourceSchema.safeParse({ + type: 'internal', + prefix: 'a'.repeat(17), + paths: ['x/'], + }); + expect(result.success).toBe(false); + }); + + it('rejects uppercase prefix', () => { + const result = DocsSourceSchema.safeParse({ type: 'internal', prefix: 'SYS', paths: ['x/'] }); + expect(result.success).toBe(false); + }); + + it('rejects prefix with invalid characters', () => { + expect( + DocsSourceSchema.safeParse({ type: 'internal', prefix: 'my_org', paths: ['x/'] }).success, + ).toBe(false); + expect( + DocsSourceSchema.safeParse({ type: 'internal', prefix: 'my.org', paths: ['x/'] }).success, + ).toBe(false); + expect( + DocsSourceSchema.safeParse({ type: 'internal', prefix: 'my org', paths: ['x/'] }).success, + ).toBe(false); + }); + }); + + describe('required fields', () => { + it('rejects missing type', () => { + const result = DocsSourceSchema.safeParse({ prefix: 'sys', paths: ['x/'] }); + expect(result.success).toBe(false); + }); + + it('rejects missing prefix', () => { + const result = DocsSourceSchema.safeParse({ type: 'internal', paths: ['x/'] }); + expect(result.success).toBe(false); + }); + + it('rejects missing paths', () => { + const result = DocsSourceSchema.safeParse({ type: 'internal', prefix: 'sys' }); + expect(result.success).toBe(false); + }); + + it('rejects invalid type', () => { + const result = DocsSourceSchema.safeParse({ type: 'local', prefix: 'sys', paths: ['x/'] }); + expect(result.success).toBe(false); + }); + }); + + describe('optional fields default correctly', () => { + it('hidden defaults to undefined when not specified', () => { + const result = DocsSourceSchema.safeParse({ + type: 'internal', + prefix: 'sys', + paths: ['shortcuts/'], + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.hidden).toBeUndefined(); + } + }); + + it('url and ref default to undefined for internal', () => { + const result = DocsSourceSchema.safeParse({ + type: 'internal', + prefix: 'sys', + paths: ['shortcuts/'], + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.url).toBeUndefined(); + expect(result.data.ref).toBeUndefined(); + } + }); + }); +}); + +describe('DocsCacheSchema with sources', () => { + it('accepts docs_cache with sources array', () => { + const result = DocsCacheSchema.safeParse({ + sources: [ + { type: 'internal', prefix: 'sys', hidden: true, paths: ['shortcuts/'] }, + { type: 'repo', prefix: 'spec', url: 'github.com/jlevy/speculate', paths: ['shortcuts/'] }, + ], + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sources).toHaveLength(2); + } + }); + + it('accepts docs_cache with both sources and files', () => { + const result = DocsCacheSchema.safeParse({ + sources: [{ type: 'internal', prefix: 'tbd', paths: ['shortcuts/'] }], + files: { + 'guidelines/custom.md': 'https://example.com/custom.md', + }, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sources).toHaveLength(1); + expect(result.data.files).toBeDefined(); + } + }); + + it('accepts docs_cache without sources (backward compat)', () => { + const result = DocsCacheSchema.safeParse({ + files: { + 'tbd/shortcuts/code-review.md': 'internal:tbd/shortcuts/code-review.md', + }, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sources).toBeUndefined(); + } + }); +}); diff --git a/packages/tbd/tests/shortcut.test.ts b/packages/tbd/tests/shortcut.test.ts new file mode 100644 index 00000000..30595d5c --- /dev/null +++ b/packages/tbd/tests/shortcut.test.ts @@ -0,0 +1,270 @@ +/** + * Characterization tests for shortcut command behavior. + * + * These tests capture the exact current behavior before any refactoring, + * ensuring that the shortcut-to-DocCommandHandler migration preserves + * all observable behavior. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdir, writeFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { DocCache, generateShortcutDirectory, SCORE_PREFIX_MATCH } from '../src/file/doc-cache.js'; +import { SHORTCUT_AGENT_HEADER, GUIDELINES_AGENT_HEADER } from '../src/cli/lib/doc-prompts.js'; + +describe('shortcut command behavior', () => { + let testDir: string; + let systemDir: string; + let standardDir: string; + + beforeEach(async () => { + testDir = join(tmpdir(), `shortcut-test-${Date.now()}`); + systemDir = join(testDir, '.tbd', 'docs', 'sys', 'shortcuts'); + standardDir = join(testDir, '.tbd', 'docs', 'tbd', 'shortcuts'); + await mkdir(systemDir, { recursive: true }); + await mkdir(standardDir, { recursive: true }); + + // System shortcuts (hidden in real setup) + await writeFile( + join(systemDir, 'skill-baseline.md'), + `--- +title: Skill Baseline +description: Main skill file +--- +# Skill Content`, + ); + await writeFile( + join(systemDir, 'skill-brief.md'), + `--- +title: Skill Brief +description: Brief skill file +--- +# Brief`, + ); + await writeFile( + join(systemDir, 'shortcut-explanation.md'), + `--- +title: Shortcut Explanation +description: How shortcuts work +--- +# Shortcut Explanation + +Shortcuts are reusable instruction templates.`, + ); + + // Standard shortcuts + await writeFile( + join(standardDir, 'code-review.md'), + `--- +title: Code Review +description: Review code changes and commit +category: review +tags: + - review + - git +--- +# Code Review Shortcut + +Review all changes carefully.`, + ); + await writeFile( + join(standardDir, 'new-plan-spec.md'), + `--- +title: New Plan Spec +description: Create a new feature planning specification +category: planning +--- +# New Plan Spec + +Instructions for creating a plan.`, + ); + await writeFile( + join(standardDir, 'implement-beads.md'), + `--- +title: Implement Beads +description: Implement beads from a spec following TDD +category: planning +--- +# Implement Beads + +Follow TDD to implement beads.`, + ); + }); + + afterEach(async () => { + await rm(testDir, { recursive: true, force: true }); + }); + + describe('exact lookup', () => { + it('finds shortcut by exact name and returns content', async () => { + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const match = cache.get('code-review'); + expect(match).not.toBeNull(); + expect(match!.doc.name).toBe('code-review'); + expect(match!.doc.content).toContain('Review all changes carefully'); + }); + + it('finds system shortcuts by exact name', async () => { + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const match = cache.get('shortcut-explanation'); + expect(match).not.toBeNull(); + expect(match!.doc.content).toContain('Shortcuts are reusable'); + }); + }); + + describe('fuzzy search with score thresholds', () => { + it('prefix match returns score >= SCORE_PREFIX_MATCH', async () => { + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const matches = cache.search('code-rev'); + expect(matches.length).toBeGreaterThan(0); + expect(matches[0]!.doc.name).toBe('code-review'); + expect(matches[0]!.score).toBeGreaterThanOrEqual(SCORE_PREFIX_MATCH); + }); + + it('word-based match returns lower score than prefix match', async () => { + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const matches = cache.search('review code'); + expect(matches.length).toBeGreaterThan(0); + // Word match has lower score than prefix match + expect(matches[0]!.score).toBeLessThan(SCORE_PREFIX_MATCH); + }); + }); + + describe('--category filtering', () => { + it('filters docs by category from frontmatter', async () => { + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const allDocs = cache.list(); + const planningDocs = allDocs.filter((d) => d.frontmatter?.category === 'planning'); + const reviewDocs = allDocs.filter((d) => d.frontmatter?.category === 'review'); + + expect(planningDocs.length).toBe(2); // new-plan-spec, implement-beads + expect(reviewDocs.length).toBe(1); // code-review + }); + }); + + describe('no-query fallback', () => { + it('shortcut-explanation.md is accessible for no-query mode', async () => { + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const explanation = cache.get('shortcut-explanation'); + expect(explanation).not.toBeNull(); + expect(explanation!.doc.content).toContain('Shortcut Explanation'); + }); + }); + + describe('agent header', () => { + it('SHORTCUT_AGENT_HEADER is defined and non-empty', () => { + expect(SHORTCUT_AGENT_HEADER).toBeDefined(); + expect(SHORTCUT_AGENT_HEADER.length).toBeGreaterThan(0); + expect(SHORTCUT_AGENT_HEADER).toContain('Agent instructions'); + }); + + it('GUIDELINES_AGENT_HEADER is defined and non-empty', () => { + expect(GUIDELINES_AGENT_HEADER).toBeDefined(); + expect(GUIDELINES_AGENT_HEADER.length).toBeGreaterThan(0); + }); + }); + + describe('path ordering and shadowing', () => { + it('system dir takes precedence over standard dir for same name', async () => { + // Add a file with same name to both dirs + await writeFile(join(standardDir, 'skill-baseline.md'), '# Standard skill'); + + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const match = cache.get('skill-baseline'); + expect(match).not.toBeNull(); + expect(match!.doc.content).toContain('Skill Content'); // System version + expect(match!.doc.sourceDir).toBe('.tbd/docs/sys/shortcuts'); + }); + + it('shadowed entries are identifiable', async () => { + await writeFile(join(standardDir, 'skill-baseline.md'), '# Standard skill'); + + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const allDocs = cache.list(true); // include shadowed + const standardSkill = allDocs.find( + (d) => d.name === 'skill-baseline' && d.sourceDir === '.tbd/docs/tbd/shortcuts', + ); + expect(standardSkill).toBeDefined(); + expect(cache.isShadowed(standardSkill!)).toBe(true); + }); + }); + + describe('JSON output shape', () => { + it('list output has expected fields', async () => { + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const docs = cache.list(); + for (const doc of docs) { + // These are the fields the shortcut --list --json uses + expect(doc).toHaveProperty('name'); + expect(doc).toHaveProperty('path'); + expect(doc).toHaveProperty('sourceDir'); + expect(doc).toHaveProperty('sizeBytes'); + expect(doc).toHaveProperty('approxTokens'); + expect(doc).toHaveProperty('content'); + } + }); + + it('search result has expected fields', async () => { + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const matches = cache.search('code-review'); + expect(matches.length).toBeGreaterThan(0); + expect(matches[0]).toHaveProperty('doc'); + expect(matches[0]).toHaveProperty('score'); + expect(matches[0]!.doc).toHaveProperty('name'); + expect(matches[0]!.doc).toHaveProperty('content'); + }); + }); + + describe('generateShortcutDirectory', () => { + it('generates directory excluding system shortcuts by name', async () => { + const cache = new DocCache(['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const docs = cache.list(); + const directory = generateShortcutDirectory(docs); + + // Standard shortcuts should be included + expect(directory).toContain('code-review'); + expect(directory).toContain('new-plan-spec'); + expect(directory).toContain('implement-beads'); + + // System shortcuts should be excluded (by hardcoded skip names) + expect(directory).not.toContain('| skill-baseline |'); + expect(directory).not.toContain('| skill-brief |'); + expect(directory).not.toContain('| skill-minimal |'); + expect(directory).not.toContain('| shortcut-explanation |'); + }); + + it('wraps with directory markers', async () => { + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); + await cache.load(); + + const docs = cache.list(); + const directory = generateShortcutDirectory(docs); + + expect(directory).toContain(''); + expect(directory).toContain(''); + }); + }); +}); diff --git a/packages/tbd/tests/source.test.ts b/packages/tbd/tests/source.test.ts new file mode 100644 index 00000000..1fc0307f --- /dev/null +++ b/packages/tbd/tests/source.test.ts @@ -0,0 +1,179 @@ +/** + * Tests for `tbd source` command - manage doc sources. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { randomBytes } from 'node:crypto'; +import { stringify as stringifyYaml } from 'yaml'; + +import { readConfig } from '../src/file/config.js'; +import { addSource, listSources, removeSource } from '../src/cli/commands/source.js'; + +describe('source command logic', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = join(tmpdir(), `tbd-source-test-${randomBytes(4).toString('hex')}`); + await mkdir(join(tempDir, '.tbd'), { recursive: true }); + + // Minimal f04 config + const config = { + tbd_format: 'f04', + tbd_version: '0.1.17', + sync: { branch: 'tbd-sync', remote: 'origin' }, + display: { id_prefix: 'test' }, + settings: { auto_sync: false }, + docs_cache: { + sources: [ + { type: 'internal', prefix: 'sys', paths: ['shortcuts'], hidden: true }, + { type: 'internal', prefix: 'tbd', paths: ['shortcuts', 'guidelines', 'templates'] }, + ], + files: {}, + lookup_path: [], + }, + }; + await mkdir(join(tempDir, '.tbd'), { recursive: true }); + const { writeFile: atomicWrite } = await import('atomically'); + await atomicWrite(join(tempDir, '.tbd', 'config.yml'), stringifyYaml(config)); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + describe('addSource', () => { + it('adds a repo source to config', async () => { + await addSource(tempDir, { + url: 'github.com/org/guidelines', + prefix: 'myorg', + }); + + const config = await readConfig(tempDir); + const sources = config.docs_cache?.sources ?? []; + const added = sources.find((s) => s.prefix === 'myorg'); + expect(added).toBeDefined(); + expect(added!.type).toBe('repo'); + expect(added!.url).toContain('github.com/org/guidelines'); + expect(added!.ref).toBe('main'); + }); + + it('uses custom ref when specified', async () => { + await addSource(tempDir, { + url: 'github.com/org/repo', + prefix: 'ext', + ref: 'v2.0', + }); + + const config = await readConfig(tempDir); + const sources = config.docs_cache?.sources ?? []; + const added = sources.find((s) => s.prefix === 'ext'); + expect(added!.ref).toBe('v2.0'); + }); + + it('uses custom paths when specified', async () => { + await addSource(tempDir, { + url: 'github.com/org/repo', + prefix: 'ext', + paths: ['guidelines', 'references'], + }); + + const config = await readConfig(tempDir); + const sources = config.docs_cache?.sources ?? []; + const added = sources.find((s) => s.prefix === 'ext'); + expect(added!.paths).toEqual(['guidelines', 'references']); + }); + + it('defaults paths to all doc types', async () => { + await addSource(tempDir, { + url: 'github.com/org/repo', + prefix: 'ext', + }); + + const config = await readConfig(tempDir); + const sources = config.docs_cache?.sources ?? []; + const added = sources.find((s) => s.prefix === 'ext'); + expect(added!.paths).toContain('shortcuts'); + expect(added!.paths).toContain('guidelines'); + expect(added!.paths).toContain('templates'); + expect(added!.paths).toContain('references'); + }); + + it('rejects duplicate prefix', async () => { + await expect( + addSource(tempDir, { + url: 'github.com/org/repo', + prefix: 'tbd', // already exists + }), + ).rejects.toThrow(/prefix.*already exists/i); + }); + + it('validates prefix format', async () => { + await expect( + addSource(tempDir, { + url: 'github.com/org/repo', + prefix: 'INVALID', + }), + ).rejects.toThrow(/prefix/i); + }); + + it('appends source after existing sources', async () => { + await addSource(tempDir, { + url: 'github.com/org/repo', + prefix: 'ext', + }); + + const config = await readConfig(tempDir); + const sources = config.docs_cache?.sources ?? []; + // Should be appended at end (after sys and tbd) + expect(sources.length).toBe(3); + expect(sources[2]!.prefix).toBe('ext'); + }); + }); + + describe('listSources', () => { + it('returns all configured sources', async () => { + const sources = await listSources(tempDir); + expect(sources.length).toBe(2); + expect(sources[0]!.prefix).toBe('sys'); + expect(sources[1]!.prefix).toBe('tbd'); + }); + + it('includes newly added sources', async () => { + await addSource(tempDir, { + url: 'github.com/org/repo', + prefix: 'ext', + }); + + const sources = await listSources(tempDir); + expect(sources.length).toBe(3); + expect(sources[2]!.prefix).toBe('ext'); + }); + }); + + describe('removeSource', () => { + it('removes a source by prefix', async () => { + await addSource(tempDir, { + url: 'github.com/org/repo', + prefix: 'ext', + }); + + await removeSource(tempDir, 'ext'); + + const config = await readConfig(tempDir); + const sources = config.docs_cache?.sources ?? []; + expect(sources.find((s) => s.prefix === 'ext')).toBeUndefined(); + expect(sources.length).toBe(2); // back to sys + tbd + }); + + it('throws when removing non-existent prefix', async () => { + await expect(removeSource(tempDir, 'nonexistent')).rejects.toThrow(/no source.*nonexistent/i); + }); + + it('prevents removing internal sources', async () => { + await expect(removeSource(tempDir, 'sys')).rejects.toThrow(/internal/i); + }); + }); +}); diff --git a/packages/tbd/tests/tbd-format.test.ts b/packages/tbd/tests/tbd-format.test.ts index 4e07f5d9..b2a63b34 100644 --- a/packages/tbd/tests/tbd-format.test.ts +++ b/packages/tbd/tests/tbd-format.test.ts @@ -18,7 +18,7 @@ import { describe('tbd-format', () => { describe('constants', () => { it('has current format', () => { - expect(CURRENT_FORMAT).toBe('f03'); + expect(CURRENT_FORMAT).toBe('f04'); }); it('has initial format', () => { @@ -29,6 +29,7 @@ describe('tbd-format', () => { expect(FORMAT_HISTORY.f01).toBeDefined(); expect(FORMAT_HISTORY.f02).toBeDefined(); expect(FORMAT_HISTORY.f03).toBeDefined(); + expect(FORMAT_HISTORY.f04).toBeDefined(); }); }); @@ -75,7 +76,7 @@ describe('tbd-format', () => { }); describe('migrateToLatest', () => { - it('migrates f01 to f03 (through f02)', () => { + it('migrates f01 to f04 (through f02, f03)', () => { const config: RawConfig = { tbd_version: '0.1.0', display: { id_prefix: 'test' }, @@ -86,16 +87,17 @@ describe('tbd-format', () => { const result = migrateToLatest(config); expect(result.fromFormat).toBe('f01'); - expect(result.toFormat).toBe('f03'); + expect(result.toFormat).toBe('f04'); expect(result.changed).toBe(true); - expect(result.config.tbd_format).toBe('f03'); + expect(result.config.tbd_format).toBe('f04'); expect(result.config.settings?.doc_auto_sync_hours).toBe(24); expect(result.changes).toContain('Added tbd_format: f02'); expect(result.changes).toContain('Added settings.doc_auto_sync_hours: 24'); expect(result.changes).toContain('Updated tbd_format: f03'); + expect(result.changes).toContain('Updated tbd_format: f04'); }); - it('migrates f02 to f03', () => { + it('migrates f02 through f03 to f04', () => { const config: RawConfig = { tbd_format: 'f02', tbd_version: '0.1.5', @@ -108,43 +110,149 @@ describe('tbd-format', () => { const result = migrateToLatest(config); expect(result.fromFormat).toBe('f02'); - expect(result.toFormat).toBe('f03'); + expect(result.toFormat).toBe('f04'); expect(result.changed).toBe(true); - expect(result.config.tbd_format).toBe('f03'); - // doc_cache moved to docs_cache.files + expect(result.config.tbd_format).toBe('f04'); + // doc_cache and docs should be gone after f02→f03 expect(result.config.doc_cache).toBeUndefined(); - expect(result.config.docs_cache?.files).toEqual({ - 'shortcuts/test.md': 'internal:shortcuts/test.md', - }); - // docs.paths moved to docs_cache.lookup_path expect(result.config.docs).toBeUndefined(); - expect(result.config.docs_cache?.lookup_path).toEqual([ - '.tbd/docs/custom', - '.tbd/docs/standard', - ]); + // After f03→f04, should have sources and no lookup_path + expect(result.config.docs_cache?.sources).toBeDefined(); + expect(result.config.docs_cache?.lookup_path).toBeUndefined(); }); - it('does not modify already current config', () => { + it('migrates f03 to f04 with default files', () => { const config: RawConfig = { tbd_format: 'f03', tbd_version: '0.1.6', display: { id_prefix: 'test' }, settings: { auto_sync: false, doc_auto_sync_hours: 12 }, docs_cache: { - files: { 'shortcuts/test.md': 'internal:shortcuts/test.md' }, - lookup_path: ['.tbd/docs/shortcuts/system'], + files: { + 'sys/shortcuts/skill.md': 'internal:sys/shortcuts/skill.md', + 'guidelines/standard/typescript-rules.md': + 'internal:guidelines/standard/typescript-rules.md', + }, + lookup_path: ['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], }, }; const result = migrateToLatest(config); expect(result.fromFormat).toBe('f03'); - expect(result.toFormat).toBe('f03'); + expect(result.toFormat).toBe('f04'); + expect(result.changed).toBe(true); + expect(result.config.tbd_format).toBe('f04'); + // lookup_path should be removed + expect(result.config.docs_cache?.lookup_path).toBeUndefined(); + // Default internal files converted to sources + expect(result.config.docs_cache?.sources).toBeDefined(); + expect(result.config.docs_cache?.sources?.length).toBeGreaterThan(0); + // Default files should be removed (handled by sources now) + expect(result.config.docs_cache?.files).toBeUndefined(); + }); + + it('migrates f03 to f04 preserving custom file overrides', () => { + const config: RawConfig = { + tbd_format: 'f03', + tbd_version: '0.1.6', + display: { id_prefix: 'test' }, + docs_cache: { + files: { + 'sys/shortcuts/skill.md': 'internal:sys/shortcuts/skill.md', + 'guidelines/custom.md': 'https://example.com/custom.md', + }, + lookup_path: ['.tbd/docs/sys/shortcuts'], + }, + }; + + const result = migrateToLatest(config); + + expect(result.config.tbd_format).toBe('f04'); + // Custom file override should be preserved + expect(result.config.docs_cache?.files).toBeDefined(); + expect(result.config.docs_cache?.files?.['guidelines/custom.md']).toBe( + 'https://example.com/custom.md', + ); + // Default internal entries should NOT be in files anymore + expect(result.config.docs_cache?.files?.['sys/shortcuts/skill.md']).toBeUndefined(); + }); + + it('migrates f03 to f04 with empty docs_cache', () => { + const config: RawConfig = { + tbd_format: 'f03', + tbd_version: '0.1.6', + display: { id_prefix: 'test' }, + docs_cache: {}, + }; + + const result = migrateToLatest(config); + + expect(result.config.tbd_format).toBe('f04'); + expect(result.toFormat).toBe('f04'); + expect(result.config.docs_cache?.sources).toBeDefined(); + }); + + it('does not modify already current config', () => { + const config: RawConfig = { + tbd_format: 'f04', + tbd_version: '0.2.0', + display: { id_prefix: 'test' }, + settings: { auto_sync: false, doc_auto_sync_hours: 12 }, + docs_cache: { + sources: [ + { type: 'internal', prefix: 'sys', hidden: true, paths: ['shortcuts/'] }, + { type: 'internal', prefix: 'tbd', paths: ['shortcuts/'] }, + ], + }, + }; + + const result = migrateToLatest(config); + + expect(result.fromFormat).toBe('f04'); + expect(result.toFormat).toBe('f04'); expect(result.changed).toBe(false); expect(result.changes).toHaveLength(0); expect(result.config.settings?.doc_auto_sync_hours).toBe(12); }); + it('returns warnings array on migration result', () => { + const config: RawConfig = { + tbd_version: '0.1.0', + display: { id_prefix: 'test' }, + }; + + const result = migrateToLatest(config); + + expect(result.warnings).toBeDefined(); + expect(Array.isArray(result.warnings)).toBe(true); + }); + + it('returns empty warnings for standard migrations', () => { + const config: RawConfig = { + tbd_version: '0.1.0', + display: { id_prefix: 'test' }, + sync: { branch: 'tbd-sync', remote: 'origin' }, + settings: { auto_sync: false }, + }; + + const result = migrateToLatest(config); + + expect(result.warnings).toEqual([]); + }); + + it('returns empty warnings for no-op migration', () => { + const config: RawConfig = { + tbd_format: 'f04', + tbd_version: '0.2.0', + display: { id_prefix: 'test' }, + }; + + const result = migrateToLatest(config); + + expect(result.warnings).toEqual([]); + }); + it('preserves existing settings when migrating', () => { const config: RawConfig = { tbd_version: '0.1.0', @@ -175,27 +283,39 @@ describe('tbd-format', () => { expect(isCompatibleFormat('f03')).toBe(true); }); + it('returns true for f04', () => { + expect(isCompatibleFormat('f04')).toBe(true); + }); + it('returns false for unknown future format', () => { expect(isCompatibleFormat('f99')).toBe(false); }); }); describe('describeMigration', () => { - it('describes f01 migration (two steps)', () => { + it('describes f01 migration (three steps)', () => { const descriptions = describeMigration('f01'); - expect(descriptions).toHaveLength(2); + expect(descriptions).toHaveLength(3); expect(descriptions[0]).toContain('f01 → f02'); expect(descriptions[1]).toContain('f02 → f03'); + expect(descriptions[2]).toContain('f03 → f04'); }); - it('describes f02 migration', () => { + it('describes f02 migration (two steps)', () => { const descriptions = describeMigration('f02'); - expect(descriptions).toHaveLength(1); + expect(descriptions).toHaveLength(2); expect(descriptions[0]).toContain('f02 → f03'); + expect(descriptions[1]).toContain('f03 → f04'); }); - it('returns empty for current format', () => { + it('describes f03 migration', () => { const descriptions = describeMigration('f03'); + expect(descriptions).toHaveLength(1); + expect(descriptions[0]).toContain('f03 → f04'); + }); + + it('returns empty for current format', () => { + const descriptions = describeMigration('f04'); expect(descriptions).toHaveLength(0); }); }); diff --git a/scripts/validate-docs.sh b/scripts/validate-docs.sh new file mode 100755 index 00000000..c47caa9a --- /dev/null +++ b/scripts/validate-docs.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# validate-docs.sh - Compare doc output between released and dev builds. +# +# Usage: ./scripts/validate-docs.sh +# +# Compares output of shortcut/guidelines/template/reference commands between: +# - Released: npx --yes get-tbd@latest (if available) +# - Dev: node packages/tbd/dist/bin.mjs (local build) +# +# Reports MATCH/DIFF/NEW for each doc entry. + +set -euo pipefail + +DEV_CMD="node packages/tbd/dist/bin.mjs" +RELEASED_CMD="npx --yes get-tbd@latest" + +# Check if we're in the right directory +if [[ ! -f packages/tbd/dist/bin.mjs ]]; then + echo "Error: Run from repo root after 'pnpm build'" + exit 1 +fi + +# Check if released version is available +HAS_RELEASED=true +if ! command -v npx &>/dev/null; then + HAS_RELEASED=false + echo "Warning: npx not available, skipping released comparison" +fi + +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT + +echo "=== Doc Validation Report ===" +echo "" + +for TYPE in shortcut guidelines template reference; do + echo "--- ${TYPE}s ---" + + # Get dev listing + TBD_DEV_VERSION=dev $DEV_CMD $TYPE --list 2>/dev/null | grep -oP '^\S+' > "$TMPDIR/dev-$TYPE.txt" || true + + if [[ "$HAS_RELEASED" == "true" ]]; then + # Get released listing (may not have reference command) + TBD_DEV_VERSION=dev $RELEASED_CMD $TYPE --list 2>/dev/null | grep -oP '^\S+' > "$TMPDIR/rel-$TYPE.txt" || true + fi + + # Report each doc + while IFS= read -r name; do + [[ -z "$name" ]] && continue + DEV_OUT=$( TBD_DEV_VERSION=dev $DEV_CMD $TYPE "$name" 2>/dev/null || echo "[NOT FOUND]" ) + + if [[ "$HAS_RELEASED" == "true" ]] && grep -qxF "$name" "$TMPDIR/rel-$TYPE.txt" 2>/dev/null; then + REL_OUT=$( TBD_DEV_VERSION=dev $RELEASED_CMD $TYPE "$name" 2>/dev/null || echo "[NOT FOUND]" ) + if [[ "$DEV_OUT" == "$REL_OUT" ]]; then + echo " MATCH: $name" + else + echo " DIFF: $name" + fi + else + echo " NEW: $name" + fi + done < "$TMPDIR/dev-$TYPE.txt" + + # Check for removed docs + if [[ "$HAS_RELEASED" == "true" ]] && [[ -f "$TMPDIR/rel-$TYPE.txt" ]]; then + while IFS= read -r name; do + [[ -z "$name" ]] && continue + if ! grep -qxF "$name" "$TMPDIR/dev-$TYPE.txt" 2>/dev/null; then + echo " REMOVED: $name" + fi + done < "$TMPDIR/rel-$TYPE.txt" + fi + + echo "" +done + +echo "=== Done ==="