From 4f927936e0f1efff5cea00f73a4c08388e00a22b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Feb 2026 23:42:35 +0000 Subject: [PATCH 01/21] docs: Add detailed implementation notes and issues to external docs repos spec Review the spec against the actual codebase, adding: - Detailed implementation steps for each phase with code-level specifics - Current codebase inventory (files to modify/create) - 13 issues/ambiguities identified from code review - Key findings: shortcut.ts doesn't use DocCommandHandler, most shortcuts are tbd-specific (24/29), MigrationResult needs warnings field, @github/slugify is unnecessary, internal source bundled path mapping needs resolution https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .../plan-2026-02-02-external-docs-repos.md | 922 +++++++++++++++++- 1 file changed, 921 insertions(+), 1 deletion(-) 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 e6cba548..ee4f3953 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,7 +4,7 @@ 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-08, implementation details added) **Status:** Draft @@ -1398,6 +1398,926 @@ 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']), + prefix: z.string().min(1).max(16).regex(/^[a-z0-9-]+$/), + url: z.string().optional(), + ref: z.string().optional(), + 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; +} +``` + +**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 (Medium) + +**Problem:** `DocCache.loadDirectory()` currently scans a single flat directory for `.md` +files. The new prefix-based storage is nested: `.tbd/docs/{prefix}/{type}/{name}.md`. +The `DocCache` needs to either: +- (a) Accept multiple directory paths computed from sources, OR +- (b) Scan recursively from the docs root + +**Impact:** Core data access pattern changes. + +**Recommendation:** Option (a). For each doc command (e.g., `tbd guidelines`), compute +the list of directories to scan: + +``` +sources.map(s => `.tbd/docs/${s.prefix}/guidelines/`) +``` + +This preserves the ordered-paths semantics (earlier sources = higher precedence) and the +existing `DocCache` flat-scan behavior, just with different directory inputs. + +### 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 (Medium) + +**Problem:** For `type: internal` sources, the spec shows: + +```yaml +- type: internal + prefix: sys + hidden: true + paths: + - shortcuts/ +``` + +But in the current codebase, bundled system shortcuts are at `packages/tbd/docs/shortcuts/system/` +while standard shortcuts are at `packages/tbd/docs/shortcuts/standard/`. The internal source +needs to know which bundled subdirectory maps to which prefix. + +**Impact:** The `paths` field alone isn't sufficient for internal sources — we also need to +know which bundled subdirectory to read from. + +**Recommendation:** For internal sources, add implicit mapping: +- `sys` prefix reads from `shortcuts/system/` in bundled docs +- `tbd` prefix reads from `shortcuts/standard/` in bundled docs + +This can be handled by a convention: internal sources with `prefix: sys` map to +`shortcuts/system/`, and `prefix: tbd` maps to `shortcuts/standard/`. Or better, add a +`bundled_path` field for internal sources: + +```yaml +- type: internal + prefix: sys + hidden: true + paths: + - shortcuts/ + bundled_subdir: shortcuts/system # Where to find these in the bundled package +``` + +Alternatively, keep the mapping in code (since internal sources are well-known): + +```typescript +const INTERNAL_SOURCE_MAPPING: Record = { + sys: 'shortcuts/system', + tbd: 'shortcuts/standard', +}; +``` + +This is simpler and avoids exposing internal implementation details in user-facing config. + +### 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: From 3046a9650230825cbe0891861802adc66830aaa3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 00:29:26 +0000 Subject: [PATCH 02/21] docs: Add Phase 0a prereq fixes, update per design clarifications - Add Phase 0a with beads (tbd-m87y chain) for shortcut.ts refactor, MigrationResult warnings field, hidden source support, test fixtures - Update Issue 10 (internal source paths): resolved by reorganizing bundled docs to match prefix convention (sys/, tbd/ subdirs) - Update Issue 6 (DocCache): resolved by generalizing constructor to accept base dir, source names, and doc types - Add Phase 0a to rollout plan summary https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .../plan-2026-02-02-external-docs-repos.md | 462 +++++++++++------- 1 file changed, 288 insertions(+), 174 deletions(-) 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 ee4f3953..96d3456a 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 @@ -949,7 +949,73 @@ 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-m87y` | Parent: Refactor shortcut.ts to use DocCommandHandler | +| `tbd-ejrz` | Write characterization tests for shortcut command current behavior | +| `tbd-bs6f` | Migrate ShortcutHandler to extend DocCommandHandler (blocked by ejrz) | +| `tbd-plld` | Move shortcut-specific behavior to DocCommandHandler overrides (blocked by bs6f) | +| `tbd-xce7` | Remove duplicate code from shortcut.ts after migration (blocked by plld) | +| `tbd-my23` | Verify all characterization tests still pass after refactor (blocked by xce7) | + +**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. + +- [ ] Add `warnings: string[]` to `MigrationResult` interface +- [ ] Initialize `warnings: []` in existing migration functions +- [ ] Add test for `warnings` field in `tbd-format.test.ts` + +#### 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. + +- [ ] Add `hidden?: boolean` field to `CachedDoc` interface in `doc-cache.ts` +- [ ] Populate `hidden` from source config when loading docs +- [ ] Update `generateShortcutDirectory()` to filter by `doc.hidden` instead of + hardcoded names +- [ ] Keep hardcoded names as fallback for backward compatibility during transition + +#### 0a.4: Establish test patterns for doc infrastructure (Issue 13) + +Set up reusable test fixtures and helpers for doc tests before the main implementation. + +- [ ] Create `tests/fixtures/test-docs/` with sample docs for each type +- [ ] Create `tests/helpers/doc-test-utils.ts` with helpers for creating temp doc dirs +- [ ] Add helper for creating local bare git repos (for RepoCache tests without network) +- [ ] Update existing `doc-sync.test.ts` and `doc-cache.test.ts` to use shared fixtures + +### 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 @@ -1258,15 +1324,22 @@ tool (`speculate init`), while tbd handles all doc/shortcut/guideline management ## Rollout Plan +**Phase 0a: Prerequisite Fixes** + +1. Refactor shortcut.ts to use DocCommandHandler (TDD, beads tbd-m87y chain) +2. Add `warnings` field to `MigrationResult` +3. Update `generateShortcutDirectory()` for `hidden` support +4. Establish shared test fixtures and helpers for doc infrastructure + **Phase 0: Speculate Prep** -1. Create Speculate `tbd` branch with flat `{type}/{name}.md` structure -2. This becomes the integration test target for all subsequent phases +5. Create Speculate `tbd` branch with flat `{type}/{name}.md` structure +6. This becomes the integration test target for all subsequent phases **Phase 1: Core Infrastructure** -3. Implement doc-types registry, repo-url utility, format bump -4. Implement prefix-based storage and sync +7. Implement doc-types registry, repo-url utility, format bump +8. Implement prefix-based storage and sync **Phase 2: Prefix System and Lookup** @@ -1400,8 +1473,8 @@ The DOC_TYPES registry makes adding new types straightforward: ## 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). +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 @@ -1436,8 +1509,8 @@ a thorough review of the current codebase (as of 2026-02-08). ### 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`. +**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): @@ -1469,8 +1542,9 @@ docs/general/ | `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. +**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):** @@ -1540,9 +1614,11 @@ export function getDocTypeDirectories(): string[] { #### 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. +**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 { @@ -1584,8 +1660,8 @@ export function getCloneUrl(url: string, preferSsh: boolean = false): string { . **Changes needed:** -1. Add `warnings: string[]` to `MigrationResult` interface (currently missing, but - tests in spec expect it) +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()`: @@ -1597,10 +1673,11 @@ export function getCloneUrl(url: string, preferSsh: boolean = false): string { . 7. Add to `describeMigration()` **`getExpectedDefaultFiles()` implementation:** This function needs to return what -`generateDefaultDocCacheConfig()` in `doc-sync.ts` would produce. It can: +`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. + is a default entry. This is simpler and doesn’t require filesystem access. **Recommended approach for identifying defaults:** @@ -1610,8 +1687,9 @@ function isDefaultFileEntry(dest: string, source: string): boolean { } ``` -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). +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` @@ -1637,10 +1715,12 @@ export const DocsCacheSchema = z.object({ }); ``` -**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. +**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` @@ -1705,8 +1785,8 @@ export class RepoCache { } ``` -**Git execution:** Use `child_process.execFile` (not `exec`) for security. Shell injection -is not possible with `execFile` since arguments are passed as an array. +**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). @@ -1729,14 +1809,16 @@ Major changes needed: - 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: +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. + **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 @@ -1744,8 +1826,8 @@ Major changes needed: #### 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/`. +**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}/`) @@ -1787,6 +1869,21 @@ get(name: string): DocMatch | 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 @@ -1842,9 +1939,10 @@ docs_cache: - 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`. +**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):** @@ -1853,12 +1951,13 @@ doc bundling needs to know which bundled docs belong to `sys` vs `tbd`. | `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`. +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. +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` @@ -1937,7 +2036,8 @@ Currently each command hardcodes its 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: +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[] { @@ -1950,9 +2050,10 @@ function getDocPaths(sources: DocsSource[], docType: DocTypeName, docsDir: strin #### 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. +**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. @@ -1960,7 +2061,8 @@ 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 +3. Shortcut-specific behavior (agent header, shortcut-explanation fallback) via + overrides #### Step 3.4: Update `doc-add.ts` @@ -1971,8 +2073,9 @@ Currently `doc-add.ts` adds to `shortcuts/custom/`. In the new system: **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. +`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 @@ -2005,18 +2108,21 @@ async function checkRepoCacheHealth(tbdRoot: string, sources: DocsSource[]): Pro Files to update: -1. **`docs/development.md`**: Add section on test repo setup, external doc sources, - and development workflow for testing with local repos. +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. +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. + 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. + 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: @@ -2086,55 +2192,61 @@ 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. +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) +### 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. +**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. +**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. +**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 +(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. +**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 +**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. +**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 `-`). +**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. @@ -2144,46 +2256,57 @@ 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. +`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. +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. +**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 (Medium) +### Issue 6: `DocCache` Flat Directory Scanning — RESOLVED -**Problem:** `DocCache.loadDirectory()` currently scans a single flat directory for `.md` -files. The new prefix-based storage is nested: `.tbd/docs/{prefix}/{type}/{name}.md`. -The `DocCache` needs to either: -- (a) Accept multiple directory paths computed from sources, OR -- (b) Scan recursively from the docs root +**Problem (original):** `DocCache.loadDirectory()` currently scans single flat +directories. The new prefix-based storage is nested: +`.tbd/docs/{prefix}/{type}/{name}.md`. -**Impact:** Core data access pattern changes. +**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). -**Recommendation:** Option (a). For each doc command (e.g., `tbd guidelines`), compute -the list of directories to scan: - -``` -sources.map(s => `.tbd/docs/${s.prefix}/guidelines/`) +```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/ ``` -This preserves the ordered-paths semantics (earlier sources = higher precedence) and the -existing `DocCache` flat-scan behavior, just with different directory inputs. +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. +**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. @@ -2194,77 +2317,65 @@ 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`. +**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. +**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()`. +**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. +**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 (Medium) - -**Problem:** For `type: internal` sources, the spec shows: - -```yaml -- type: internal - prefix: sys - hidden: true - paths: - - shortcuts/ -``` +**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. -But in the current codebase, bundled system shortcuts are at `packages/tbd/docs/shortcuts/system/` -while standard shortcuts are at `packages/tbd/docs/shortcuts/standard/`. The internal source -needs to know which bundled subdirectory maps to which prefix. +### Issue 10: Internal Source Bundled Doc Paths — RESOLVED -**Impact:** The `paths` field alone isn't sufficient for internal sources — we also need to -know which bundled subdirectory to read from. +**Problem (original):** For `type: internal` sources, the `paths` field alone isn’t +sufficient since bundled docs currently live at `shortcuts/system/` and +`shortcuts/standard/`. -**Recommendation:** For internal sources, add implicit mapping: -- `sys` prefix reads from `shortcuts/system/` in bundled docs -- `tbd` prefix reads from `shortcuts/standard/` in bundled docs +**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: -This can be handled by a convention: internal sources with `prefix: sys` map to -`shortcuts/system/`, and `prefix: tbd` maps to `shortcuts/standard/`. Or better, add a -`bundled_path` field for internal sources: - -```yaml -- type: internal - prefix: sys - hidden: true - paths: - - shortcuts/ - bundled_subdir: shortcuts/system # Where to find these in the bundled package ``` - -Alternatively, keep the mapping in code (since internal sources are well-known): - -```typescript -const INTERNAL_SOURCE_MAPPING: Record = { - sys: 'shortcuts/system', - tbd: 'shortcuts/standard', -}; +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 is simpler and avoids exposing internal implementation details in user-facing config. +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: +**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` @@ -2272,29 +2383,32 @@ This is simpler and avoids exposing internal implementation details in user-faci - `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`. +**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. +**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. +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. +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: +**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 @@ -2315,8 +2429,8 @@ 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. +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 From 8469bb2b4291a44dccde51b663c08b008b58f6bf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 00:47:12 +0000 Subject: [PATCH 03/21] docs: Update spec Phase 0a with implementation beads and bead IDs Create complete bead breakdown for all Phase 0a prerequisite fixes: - tbd-kzeh: Epic for Phase 0a - 0a.1 (tbd-she8): Refactor shortcut.ts to DocCommandHandler (5 TDD beads) - 0a.2 (tbd-di9c): Add warnings to MigrationResult (2 TDD beads) - 0a.3 (tbd-hrbz): Hidden source support in generateShortcutDirectory (3 TDD beads) - 0a.4 (tbd-0bed): Shared test fixtures and helpers (4 beads) All beads have descriptions, TDD ordering, and dependency chains. https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .../plan-2026-02-02-external-docs-repos.md | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) 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 96d3456a..a1abd79e 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 @@ -965,12 +965,12 @@ will be added. | Bead | Description | | --- | --- | -| `tbd-m87y` | Parent: Refactor shortcut.ts to use DocCommandHandler | -| `tbd-ejrz` | Write characterization tests for shortcut command current behavior | -| `tbd-bs6f` | Migrate ShortcutHandler to extend DocCommandHandler (blocked by ejrz) | -| `tbd-plld` | Move shortcut-specific behavior to DocCommandHandler overrides (blocked by bs6f) | -| `tbd-xce7` | Remove duplicate code from shortcut.ts after migration (blocked by plld) | -| `tbd-my23` | Verify all characterization tests still pass after refactor (blocked by xce7) | +| `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:** @@ -991,29 +991,41 @@ will be added. The `MigrationResult` type in `tbd-format.ts` only has `changes: string[]`. The f03→f04 migration needs `warnings: string[]` for reporting preserved custom file overrides. -- [ ] Add `warnings: string[]` to `MigrationResult` interface -- [ ] Initialize `warnings: []` in existing migration functions -- [ ] Add test for `warnings` field in `tbd-format.test.ts` +**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. -- [ ] Add `hidden?: boolean` field to `CachedDoc` interface in `doc-cache.ts` -- [ ] Populate `hidden` from source config when loading docs -- [ ] Update `generateShortcutDirectory()` to filter by `doc.hidden` instead of - hardcoded names -- [ ] Keep hardcoded names as fallback for backward compatibility during transition +**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. -- [ ] Create `tests/fixtures/test-docs/` with sample docs for each type -- [ ] Create `tests/helpers/doc-test-utils.ts` with helpers for creating temp doc dirs -- [ ] Add helper for creating local bare git repos (for RepoCache tests without network) -- [ ] Update existing `doc-sync.test.ts` and `doc-cache.test.ts` to use shared fixtures +**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 From 4f3ea2d82e307fd0fe3307185ba8e9c4535ca550 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 00:49:36 +0000 Subject: [PATCH 04/21] tbd: save outbox from sync (19 issues) Sync push to tbd-sync branch failed with 403 in this environment. Issues saved to outbox for recovery on next successful sync. https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .../issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md | 20 +++++++++++++++++ .../issues/is-01kgzxe3p3qc7m2zxz0ga530vy.md | 22 +++++++++++++++++++ .../issues/is-01kgzxetnqnvtf41cx6a988sy8.md | 18 +++++++++++++++ .../issues/is-01kgzxfmfss9zfpj5abhgm2whz.md | 18 +++++++++++++++ .../issues/is-01kgzxfqrfxmt6pcdnw1t8vem6.md | 18 +++++++++++++++ .../issues/is-01kgzxftzt59ardkfx9k3wj145.md | 18 +++++++++++++++ .../issues/is-01kgzxfy6t76xrk1mybbg5jger.md | 16 ++++++++++++++ .../issues/is-01kgzxj12dj31rfwh0xxftttmy.md | 19 ++++++++++++++++ .../issues/is-01kgzxjqv0d9b1qrh8sf5qncv0.md | 18 +++++++++++++++ .../issues/is-01kgzxjv3trypg9djykjk7v3as.md | 16 ++++++++++++++ .../issues/is-01kgzxkyye3gaxrebyyqzh21w7.md | 20 +++++++++++++++++ .../issues/is-01kgzxmr6nf5s2pb631jnmeht8.md | 18 +++++++++++++++ .../issues/is-01kgzxmvh2m0atzzcererq1j72.md | 18 +++++++++++++++ .../issues/is-01kgzxmyvwg2dnnt751wnw2e9s.md | 16 ++++++++++++++ .../issues/is-01kgzxpbfpk8sf45cveezakydn.md | 21 ++++++++++++++++++ .../issues/is-01kgzxq3cswmbjqvn77m7gcb75.md | 18 +++++++++++++++ .../issues/is-01kgzxq6s7wcczc1vrn9twsyf1.md | 20 +++++++++++++++++ .../issues/is-01kgzxqa3x20eqdcrv4jdfncsj.md | 16 ++++++++++++++ .../issues/is-01kgzxqddjcngrpcyegkqm4bb2.md | 16 ++++++++++++++ .tbd/workspaces/outbox/mappings/ids.yml | 19 ++++++++++++++++ 20 files changed, 365 insertions(+) create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxe3p3qc7m2zxz0ga530vy.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxetnqnvtf41cx6a988sy8.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxfmfss9zfpj5abhgm2whz.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxfqrfxmt6pcdnw1t8vem6.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxftzt59ardkfx9k3wj145.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxfy6t76xrk1mybbg5jger.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxj12dj31rfwh0xxftttmy.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxjqv0d9b1qrh8sf5qncv0.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxjv3trypg9djykjk7v3as.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxkyye3gaxrebyyqzh21w7.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxmr6nf5s2pb631jnmeht8.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxmvh2m0atzzcererq1j72.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxmyvwg2dnnt751wnw2e9s.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxpbfpk8sf45cveezakydn.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxq3cswmbjqvn77m7gcb75.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxq6s7wcczc1vrn9twsyf1.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxqa3x20eqdcrv4jdfncsj.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzxqddjcngrpcyegkqm4bb2.md create mode 100644 .tbd/workspaces/outbox/mappings/ids.yml diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md b/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md new file mode 100644 index 00000000..dae27ce9 --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md @@ -0,0 +1,20 @@ +--- +child_order_hints: + - is-01kgzxe3p3qc7m2zxz0ga530vy + - is-01kgzxj12dj31rfwh0xxftttmy + - is-01kgzxkyye3gaxrebyyqzh21w7 + - is-01kgzxpbfpk8sf45cveezakydn +created_at: 2026-02-09T00:39:05.056Z +dependencies: [] +id: is-01kgzxcx31b6kjdd9v8r3gt5e3 +kind: epic +labels: [] +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-09T00:44:14.709Z +version: 6 +--- +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..dbef4878 --- /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-09T00:40:44.505Z +version: 7 +--- +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..78bee681 --- /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-09T00:41:26.181Z +version: 3 +--- +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..3bf344f3 --- /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-09T00:41:29.414Z +version: 3 +--- +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..15a0d7d4 --- /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-09T00:41:32.732Z +version: 3 +--- +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..39111ae3 --- /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-09T00:41:35.988Z +version: 3 +--- +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..e2aa7d31 --- /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-09T00:41:14.920Z +version: 2 +--- +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..cfa1fc9e --- /dev/null +++ b/.tbd/workspaces/outbox/issues/is-01kgzxj12dj31rfwh0xxftttmy.md @@ -0,0 +1,19 @@ +--- +child_order_hints: + - is-01kgzxjqv0d9b1qrh8sf5qncv0 + - is-01kgzxjv3trypg9djykjk7v3as +created_at: 2026-02-09T00:41:52.972Z +dependencies: [] +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-09T00:42:19.640Z +version: 4 +--- +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..9446d0eb --- /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-09T00:42:41.489Z +version: 3 +--- +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..64841e0f --- /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-09T00:42:38.208Z +version: 2 +--- +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..13a71bd8 --- /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-09T00:43:29.019Z +version: 5 +--- +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..352469e5 --- /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-09T00:43:56.278Z +version: 3 +--- +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..3f899197 --- /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-09T00:43:59.559Z +version: 3 +--- +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..7460a637 --- /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-09T00:43:53.060Z +version: 2 +--- +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..fa2dbc0b --- /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-09T00:44:49.457Z +version: 6 +--- +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..fd06d997 --- /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-09T00:45:22.060Z +version: 3 +--- +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..3245bb2e --- /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-09T00:45:28.811Z +version: 4 +--- +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..01cb3549 --- /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-09T00:45:15.372Z +version: 2 +--- +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..25456d54 --- /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-09T00:45:18.717Z +version: 2 +--- +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/mappings/ids.yml b/.tbd/workspaces/outbox/mappings/ids.yml new file mode 100644 index 00000000..fbae7c71 --- /dev/null +++ b/.tbd/workspaces/outbox/mappings/ids.yml @@ -0,0 +1,19 @@ +0bed: 01kgzxpbfpk8sf45cveezakydn +0fpx: 01kgzxfqrfxmt6pcdnw1t8vem6 +0pwa: 01kgzxetnqnvtf41cx6a988sy8 +3grz: 01kgzxqddjcngrpcyegkqm4bb2 +4npn: 01kgzxftzt59ardkfx9k3wj145 +aga3: 01kgzxjqv0d9b1qrh8sf5qncv0 +bal5: 01kgzxmyvwg2dnnt751wnw2e9s +di9c: 01kgzxj12dj31rfwh0xxftttmy +fmxo: 01kgzxfmfss9zfpj5abhgm2whz +hrbz: 01kgzxkyye3gaxrebyyqzh21w7 +kqic: 01kgzxmvh2m0atzzcererq1j72 +kzeh: 01kgzxcx31b6kjdd9v8r3gt5e3 +lifm: 01kgzxjv3trypg9djykjk7v3as +msj1: 01kgzxfy6t76xrk1mybbg5jger +nszx: 01kgzxqa3x20eqdcrv4jdfncsj +r34n: 01kgzxq6s7wcczc1vrn9twsyf1 +she8: 01kgzxe3p3qc7m2zxz0ga530vy +v0sk: 01kgzxq3cswmbjqvn77m7gcb75 +xfru: 01kgzxmr6nf5s2pb631jnmeht8 From 79de19a10222da1e09fc69d388ea96bb2c86f1db Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 01:48:50 +0000 Subject: [PATCH 05/21] docs: Add beads for all phases (0-5) of external docs repos spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created 60+ beads across 8 phases with full dependency chains: - Phase 0 (tbd-eikw): 6 beads - Speculate repo prep - Phase 1 (tbd-5ybv): 9 beads - Core infrastructure (doc-types, repo-url, format bump, RepoCache, DocSync rewrite) - Phase 2 (tbd-n3zb): 7 beads - Prefix system and lookup (parseQualifiedName, DocCache rewrite, setup, --list, errors) - Phase 3 (tbd-qhmo): 5 beads - Reference type and CLI (tbd reference command, registry unification, doctor checks) - Phase 3b (tbd-rq6q): 5 beads - Documentation update - Phase 4 (tbd-pxis): 4 beads - Validation (comparison script, run validation, fix diffs) - Phase 4b (tbd-4xj8): 3 beads - Fresh install E2E test - Phase 5 (tbd-97fe): 6 beads - Speculate migration finalize All beads have detailed descriptions, TDD annotations, and dependency chains. Cross-phase dependencies enforce ordering: 0a→0→1→2→3→3b→4→4b→5. Top-level epic: tbd-mdwh https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .../plan-2026-02-02-external-docs-repos.md | 157 ++++++++++++++---- 1 file changed, 127 insertions(+), 30 deletions(-) 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 a1abd79e..3e5452c9 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, implementation details added) +**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 @@ -1064,6 +1066,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) @@ -1079,6 +1093,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: @@ -1094,6 +1123,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 @@ -1103,6 +1145,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: @@ -1115,6 +1168,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. @@ -1130,6 +1194,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. @@ -1166,6 +1240,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. @@ -1285,6 +1368,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. @@ -1336,56 +1431,58 @@ tool (`speculate init`), while tbd handles all doc/shortcut/guideline management ## Rollout Plan -**Phase 0a: Prerequisite Fixes** +**Top-level epic:** `tbd-mdwh` — Spec: External Docs Repos + +**Phase 0a: Prerequisite Fixes** (`tbd-kzeh`) -1. Refactor shortcut.ts to use DocCommandHandler (TDD, beads tbd-m87y chain) -2. Add `warnings` field to `MigrationResult` -3. Update `generateShortcutDirectory()` for `hidden` support -4. Establish shared test fixtures and helpers for doc 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) -**Phase 0: Speculate Prep** +**Phase 0: Speculate Prep** (`tbd-eikw`, blocked by 0a) 5. Create Speculate `tbd` branch with flat `{type}/{name}.md` structure 6. This becomes the integration test target for all subsequent phases -**Phase 1: Core Infrastructure** +**Phase 1: Core Infrastructure** (`tbd-5ybv`, blocked by 0a) 7. Implement doc-types registry, repo-url utility, format bump 8. Implement prefix-based storage and sync -**Phase 2: Prefix System and Lookup** +**Phase 2: Prefix System and Lookup** (`tbd-n3zb`, blocked by Phase 1) -5. Implement qualified (`prefix:name`) and unqualified lookup -6. Add hidden source support -7. Test against Speculate `tbd` branch +9. Implement qualified (`prefix:name`) and unqualified lookup +10. Add hidden source support +11. Test against Speculate `tbd` branch -**Phase 3: New Reference Type and CLI** +**Phase 3: New Reference Type and CLI** (`tbd-qhmo`, blocked by Phase 2) -8. Add `tbd reference` command -9. Simplify existing commands to use doc-types registry +12. Add `tbd reference` command +13. Simplify existing commands to use doc-types registry -**Phase 3b: Documentation Update** +**Phase 3b: Documentation Update** (`tbd-rq6q`, blocked by Phase 3) -10. Update all tbd docs to reflect new architecture -11. Add migration guide for users +14. Update all tbd docs to reflect new architecture +15. Add migration guide for users -**Phase 4: Validation** +**Phase 4: Validation** (`tbd-pxis`, blocked by Phases 3 and 3b) -12. Run validation script comparing all output with `get-tbd@latest` -13. Document intentional differences, fix unintentional ones +16. Run validation script comparing all output with `get-tbd@latest` +17. Document intentional differences, fix unintentional ones -**Phase 4b: Fresh Install E2E** +**Phase 4b: Fresh Install E2E** (`tbd-4xj8`, blocked by Phase 4) -14. Fresh install test with prefix-based sources -15. Test qualified and unqualified lookups -16. All tests must pass before proceeding +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)** +**Phase 5: Speculate Migration (Finalize)** (`tbd-97fe`, blocked by Phase 4b) -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 +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 From 35bda641dae591f5cd4eaf1a3bda0a002b155cce Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 01:51:15 +0000 Subject: [PATCH 06/21] tbd: save outbox from sync (92 issues) https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .../issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md | 11 ++-- .../issues/is-01kgzxe3p3qc7m2zxz0ga530vy.md | 4 +- .../issues/is-01kgzxetnqnvtf41cx6a988sy8.md | 4 +- .../issues/is-01kgzxfmfss9zfpj5abhgm2whz.md | 4 +- .../issues/is-01kgzxfqrfxmt6pcdnw1t8vem6.md | 4 +- .../issues/is-01kgzxftzt59ardkfx9k3wj145.md | 4 +- .../issues/is-01kgzxfy6t76xrk1mybbg5jger.md | 4 +- .../issues/is-01kgzxj12dj31rfwh0xxftttmy.md | 8 +-- .../issues/is-01kgzxjqv0d9b1qrh8sf5qncv0.md | 4 +- .../issues/is-01kgzxjv3trypg9djykjk7v3as.md | 4 +- .../issues/is-01kgzxkyye3gaxrebyyqzh21w7.md | 4 +- .../issues/is-01kgzxmr6nf5s2pb631jnmeht8.md | 4 +- .../issues/is-01kgzxmvh2m0atzzcererq1j72.md | 4 +- .../issues/is-01kgzxmyvwg2dnnt751wnw2e9s.md | 4 +- .../issues/is-01kgzxpbfpk8sf45cveezakydn.md | 4 +- .../issues/is-01kgzxq3cswmbjqvn77m7gcb75.md | 4 +- .../issues/is-01kgzxq6s7wcczc1vrn9twsyf1.md | 4 +- .../issues/is-01kgzxqa3x20eqdcrv4jdfncsj.md | 4 +- .../issues/is-01kgzxqddjcngrpcyegkqm4bb2.md | 4 +- .../issues/is-01kgzyh3ph1pfngcvyab02nhe9.md | 25 +++++++++ .../issues/is-01kgzyj5y0xm00qdqsgay4vfv9.md | 23 ++++++++ .../issues/is-01kgzyk2gaa8r9xmbb9axhftaq.md | 18 +++++++ .../issues/is-01kgzyk5whe8q33z3zvqyq05bv.md | 18 +++++++ .../issues/is-01kgzyk94y5tx9y93cpmdxsr8c.md | 18 +++++++ .../issues/is-01kgzykcjvkwqk6mc6rx2xbjah.md | 18 +++++++ .../issues/is-01kgzykg105ptpmjar5yyfz045.md | 18 +++++++ .../issues/is-01kgzykkgk2p3jstgr4bstm0d8.md | 16 ++++++ .../issues/is-01kgzypm020x8n0jgadn5g3v7x.md | 28 ++++++++++ .../issues/is-01kgzyqpk2hmkj9rwpgkpjsjbj.md | 18 +++++++ .../issues/is-01kgzyqt5czjjjhvakcfpe5q81.md | 18 +++++++ .../issues/is-01kgzyqxkjmj2g4jpbhcegsnek.md | 20 +++++++ .../issues/is-01kgzyr12a16cqreyy5ekn0r2k.md | 18 +++++++ .../issues/is-01kgzyr4fh728np85rprvp9s6e.md | 18 +++++++ .../issues/is-01kgzyr7y128s080n05fpnm9de.md | 18 +++++++ .../issues/is-01kgzyrbcf260b1791a043ccpv.md | 20 +++++++ .../issues/is-01kgzyretysjtans3brggwcqcj.md | 18 +++++++ .../issues/is-01kgzyrj4rhv2kjncymrzf01br.md | 16 ++++++ .../issues/is-01kh00hr6eq3p16ebr73y7cxk1.md | 26 +++++++++ .../issues/is-01kh00j5n25sdsppd2qnv6ver4.md | 18 +++++++ .../issues/is-01kh00jd80wve8jdkxn4r16vcx.md | 22 ++++++++ .../issues/is-01kh00jm2mreweej0h9v929ae2.md | 18 +++++++ .../issues/is-01kh00jvdscyj8x10n206yq9tr.md | 18 +++++++ .../issues/is-01kh00k2cdef3ednq6q01yn6zr.md | 18 +++++++ .../issues/is-01kh00ka8f786r7zxdgpg8sav1.md | 18 +++++++ .../issues/is-01kh00kkcad26zfrxa83nn9fmr.md | 16 ++++++ .../issues/is-01kh00nprzwe2hx8t0qbatyvqb.md | 26 +++++++++ .../issues/is-01kh00pkpydj4jb8sfep3r0v7s.md | 16 ++++++ .../issues/is-01kh00pq2ms8emj422bbe7xmtw.md | 22 ++++++++ .../issues/is-01kh00pthqe72f5qbsxj7c0y6y.md | 16 ++++++ .../issues/is-01kh00pxxjrme57t7831vsf6yh.md | 16 ++++++ .../issues/is-01kh00q1719mv8qg6aznscyx1t.md | 16 ++++++ .../issues/is-01kh00r1v5stt6jeww4x7bq1pz.md | 24 +++++++++ .../issues/is-01kh00rvs4a8gnhzkpp94adv4t.md | 16 ++++++ .../issues/is-01kh00rz1e17qtww14k7xcxnwq.md | 20 +++++++ .../issues/is-01kh00s27m7ynb0752j81gq3xj.md | 16 ++++++ .../issues/is-01kh00s5ejcp46p29hvqxrjb9d.md | 16 ++++++ .../issues/is-01kh00s8p2fhah7a9g816ctmq4.md | 16 ++++++ .../issues/is-01kh00tjaz5n4x0jdeqzgbnaq8.md | 23 ++++++++ .../issues/is-01kh00v81phdkqk71ac05rnvq8.md | 20 +++++++ .../issues/is-01kh00vbepnwcryhwjx2n7jetb.md | 18 +++++++ .../issues/is-01kh00vevt9rq4d4r4mz7dscdd.md | 18 +++++++ .../issues/is-01kh00vj273rt2j0hkhx9r3egj.md | 16 ++++++ .../issues/is-01kh00wq09fmchfvm0c8c2s2gg.md | 22 ++++++++ .../issues/is-01kh00xfgz06jh7nz2s8nvzcrh.md | 18 +++++++ .../issues/is-01kh00xjycx5tpddt6nd9z6kdw.md | 18 +++++++ .../issues/is-01kh00xpcnxh2mge03ak5fq30m.md | 16 ++++++ .../issues/is-01kh00ywn96hz5rfvwm7bey6nw.md | 23 ++++++++ .../issues/is-01kh00zv0qg7rsewwsr5dp16xb.md | 18 +++++++ .../issues/is-01kh00zyntcb780kzhhyzfx8ay.md | 20 +++++++ .../issues/is-01kh0102a0va61xzn1h38vxe5a.md | 18 +++++++ .../issues/is-01kh0105ym45bcdm1c4dms2stc.md | 18 +++++++ .../issues/is-01kh0109h8rey5ka2f91jeeqg3.md | 18 +++++++ .../issues/is-01kh010d36t135fhyg9vj2yczj.md | 16 ++++++ .tbd/workspaces/outbox/mappings/ids.yml | 54 +++++++++++++++++++ 74 files changed, 1121 insertions(+), 40 deletions(-) create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyh3ph1pfngcvyab02nhe9.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyj5y0xm00qdqsgay4vfv9.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyk2gaa8r9xmbb9axhftaq.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyk5whe8q33z3zvqyq05bv.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyk94y5tx9y93cpmdxsr8c.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzykcjvkwqk6mc6rx2xbjah.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzykg105ptpmjar5yyfz045.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzykkgk2p3jstgr4bstm0d8.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzypm020x8n0jgadn5g3v7x.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyqpk2hmkj9rwpgkpjsjbj.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyqt5czjjjhvakcfpe5q81.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyqxkjmj2g4jpbhcegsnek.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyr12a16cqreyy5ekn0r2k.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyr4fh728np85rprvp9s6e.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyr7y128s080n05fpnm9de.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyrbcf260b1791a043ccpv.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyretysjtans3brggwcqcj.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kgzyrj4rhv2kjncymrzf01br.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00hr6eq3p16ebr73y7cxk1.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00j5n25sdsppd2qnv6ver4.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00jd80wve8jdkxn4r16vcx.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00jm2mreweej0h9v929ae2.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00jvdscyj8x10n206yq9tr.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00k2cdef3ednq6q01yn6zr.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00ka8f786r7zxdgpg8sav1.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00kkcad26zfrxa83nn9fmr.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00nprzwe2hx8t0qbatyvqb.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00pkpydj4jb8sfep3r0v7s.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00pq2ms8emj422bbe7xmtw.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00pthqe72f5qbsxj7c0y6y.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00pxxjrme57t7831vsf6yh.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00q1719mv8qg6aznscyx1t.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00r1v5stt6jeww4x7bq1pz.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00rvs4a8gnhzkpp94adv4t.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00rz1e17qtww14k7xcxnwq.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00s27m7ynb0752j81gq3xj.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00s5ejcp46p29hvqxrjb9d.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00s8p2fhah7a9g816ctmq4.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00tjaz5n4x0jdeqzgbnaq8.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00v81phdkqk71ac05rnvq8.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00vbepnwcryhwjx2n7jetb.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00vevt9rq4d4r4mz7dscdd.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00vj273rt2j0hkhx9r3egj.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00wq09fmchfvm0c8c2s2gg.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00xfgz06jh7nz2s8nvzcrh.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00xjycx5tpddt6nd9z6kdw.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00xpcnxh2mge03ak5fq30m.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00ywn96hz5rfvwm7bey6nw.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00zv0qg7rsewwsr5dp16xb.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh00zyntcb780kzhhyzfx8ay.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh0102a0va61xzn1h38vxe5a.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh0105ym45bcdm1c4dms2stc.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh0109h8rey5ka2f91jeeqg3.md create mode 100644 .tbd/workspaces/outbox/issues/is-01kh010d36t135fhyg9vj2yczj.md diff --git a/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md b/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md index dae27ce9..bb42482a 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxcx31b6kjdd9v8r3gt5e3.md @@ -5,16 +5,21 @@ child_order_hints: - is-01kgzxkyye3gaxrebyyqzh21w7 - is-01kgzxpbfpk8sf45cveezakydn created_at: 2026-02-09T00:39:05.056Z -dependencies: [] +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-09T00:44:14.709Z -version: 6 +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 index dbef4878..c79cb623 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxe3p3qc7m2zxz0ga530vy.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxe3p3qc7m2zxz0ga530vy.md @@ -16,7 +16,7 @@ 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-09T00:40:44.505Z -version: 7 +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 index 78bee681..6f3531f6 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxetnqnvtf41cx6a988sy8.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxetnqnvtf41cx6a988sy8.md @@ -12,7 +12,7 @@ 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-09T00:41:26.181Z -version: 3 +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 index 3bf344f3..435c34e5 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxfmfss9zfpj5abhgm2whz.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxfmfss9zfpj5abhgm2whz.md @@ -12,7 +12,7 @@ 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-09T00:41:29.414Z -version: 3 +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 index 15a0d7d4..0a226ad1 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxfqrfxmt6pcdnw1t8vem6.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxfqrfxmt6pcdnw1t8vem6.md @@ -12,7 +12,7 @@ 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-09T00:41:32.732Z -version: 3 +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 index 39111ae3..2a0c46bc 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxftzt59ardkfx9k3wj145.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxftzt59ardkfx9k3wj145.md @@ -12,7 +12,7 @@ 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-09T00:41:35.988Z -version: 3 +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 index e2aa7d31..335bf99c 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxfy6t76xrk1mybbg5jger.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxfy6t76xrk1mybbg5jger.md @@ -10,7 +10,7 @@ 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-09T00:41:14.920Z -version: 2 +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 index cfa1fc9e..7b8ecd6b 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxj12dj31rfwh0xxftttmy.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxj12dj31rfwh0xxftttmy.md @@ -3,7 +3,9 @@ child_order_hints: - is-01kgzxjqv0d9b1qrh8sf5qncv0 - is-01kgzxjv3trypg9djykjk7v3as created_at: 2026-02-09T00:41:52.972Z -dependencies: [] +dependencies: + - target: is-01kgzyqxkjmj2g4jpbhcegsnek + type: blocks id: is-01kgzxj12dj31rfwh0xxftttmy kind: task labels: [] @@ -13,7 +15,7 @@ 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-09T00:42:19.640Z -version: 4 +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 index 9446d0eb..787e1aa4 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxjqv0d9b1qrh8sf5qncv0.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxjqv0d9b1qrh8sf5qncv0.md @@ -12,7 +12,7 @@ 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-09T00:42:41.489Z -version: 3 +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 index 64841e0f..7f0e873a 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxjv3trypg9djykjk7v3as.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxjv3trypg9djykjk7v3as.md @@ -10,7 +10,7 @@ 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-09T00:42:38.208Z -version: 2 +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 index 13a71bd8..9fc8db4d 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxkyye3gaxrebyyqzh21w7.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxkyye3gaxrebyyqzh21w7.md @@ -14,7 +14,7 @@ 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-09T00:43:29.019Z -version: 5 +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 index 352469e5..499defab 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxmr6nf5s2pb631jnmeht8.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxmr6nf5s2pb631jnmeht8.md @@ -12,7 +12,7 @@ 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-09T00:43:56.278Z -version: 3 +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 index 3f899197..2859203e 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxmvh2m0atzzcererq1j72.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxmvh2m0atzzcererq1j72.md @@ -12,7 +12,7 @@ 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-09T00:43:59.559Z -version: 3 +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 index 7460a637..4b0030b8 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxmyvwg2dnnt751wnw2e9s.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxmyvwg2dnnt751wnw2e9s.md @@ -10,7 +10,7 @@ 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-09T00:43:53.060Z -version: 2 +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 index fa2dbc0b..dcb6e0ca 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxpbfpk8sf45cveezakydn.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxpbfpk8sf45cveezakydn.md @@ -15,7 +15,7 @@ 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-09T00:44:49.457Z -version: 6 +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 index fd06d997..ac41b291 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxq3cswmbjqvn77m7gcb75.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxq3cswmbjqvn77m7gcb75.md @@ -12,7 +12,7 @@ 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-09T00:45:22.060Z -version: 3 +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 index 3245bb2e..73ff3970 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxq6s7wcczc1vrn9twsyf1.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxq6s7wcczc1vrn9twsyf1.md @@ -14,7 +14,7 @@ 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-09T00:45:28.811Z -version: 4 +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 index 01cb3549..9f0e3226 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxqa3x20eqdcrv4jdfncsj.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxqa3x20eqdcrv4jdfncsj.md @@ -10,7 +10,7 @@ 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-09T00:45:15.372Z -version: 2 +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 index 25456d54..93c97f40 100644 --- a/.tbd/workspaces/outbox/issues/is-01kgzxqddjcngrpcyegkqm4bb2.md +++ b/.tbd/workspaces/outbox/issues/is-01kgzxqddjcngrpcyegkqm4bb2.md @@ -10,7 +10,7 @@ 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-09T00:45:18.717Z -version: 2 +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 index fbae7c71..198b297a 100644 --- a/.tbd/workspaces/outbox/mappings/ids.yml +++ b/.tbd/workspaces/outbox/mappings/ids.yml @@ -1,19 +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 From 99e710ca7c6d158659fbaab697e9a0ff44d866a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 05:12:23 +0000 Subject: [PATCH 07/21] feat: Phase 0a prerequisite fixes (TDD) MigrationResult warnings field (tbd-di9c): - RED: Added tests for warnings array on MigrationResult - GREEN: Added warnings: string[] to MigrationResult type and all migration functions, aggregated in migrateToLatest() Hidden source support (tbd-hrbz): - RED: Added tests for hidden doc filtering in generateShortcutDirectory() - GREEN: Added hidden?: boolean to CachedDoc, buildTableRows filters hidden docs Test infrastructure (tbd-0bed): - Created tests/fixtures/test-docs/ with sample docs for all 4 types - Created tests/helpers/doc-test-utils.ts with createTempDocsDir, populateTestDocs, createMockConfig, createTestBareRepo helpers - Added shortcut.test.ts with 14 characterization tests 982 tests passing (21 new). https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/src/file/doc-cache.ts | 5 +- packages/tbd/src/lib/tbd-format.ts | 9 + packages/tbd/tests/doc-cache.test.ts | 67 ++++ .../test-docs/guidelines/test-python-rules.md | 11 + .../guidelines/test-typescript-rules.md | 12 + .../test-docs/references/test-api-ref.md | 15 + .../test-docs/references/test-config-ref.md | 12 + .../test-docs/shortcuts/test-commit.md | 8 + .../shortcuts/test-no-frontmatter.md | 3 + .../test-docs/shortcuts/test-review.md | 15 + .../fixtures/test-docs/templates/test-spec.md | 17 ++ packages/tbd/tests/helpers/doc-test-utils.ts | 131 ++++++++ packages/tbd/tests/shortcut.test.ts | 287 ++++++++++++++++++ packages/tbd/tests/tbd-format.test.ts | 38 +++ 14 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 packages/tbd/tests/fixtures/test-docs/guidelines/test-python-rules.md create mode 100644 packages/tbd/tests/fixtures/test-docs/guidelines/test-typescript-rules.md create mode 100644 packages/tbd/tests/fixtures/test-docs/references/test-api-ref.md create mode 100644 packages/tbd/tests/fixtures/test-docs/references/test-config-ref.md create mode 100644 packages/tbd/tests/fixtures/test-docs/shortcuts/test-commit.md create mode 100644 packages/tbd/tests/fixtures/test-docs/shortcuts/test-no-frontmatter.md create mode 100644 packages/tbd/tests/fixtures/test-docs/shortcuts/test-review.md create mode 100644 packages/tbd/tests/fixtures/test-docs/templates/test-spec.md create mode 100644 packages/tbd/tests/helpers/doc-test-utils.ts create mode 100644 packages/tbd/tests/shortcut.test.ts diff --git a/packages/tbd/src/file/doc-cache.ts b/packages/tbd/src/file/doc-cache.ts index 08c172fc..560f390f 100644 --- a/packages/tbd/src/file/doc-cache.ts +++ b/packages/tbd/src/file/doc-cache.ts @@ -74,6 +74,8 @@ export interface CachedDoc { sizeBytes: number; /** Estimated token count (based on ~3.5 chars/token) */ approxTokens: number; + /** If true, excluded from --list and directory tables */ + hidden?: boolean; } /** @@ -394,13 +396,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; } diff --git a/packages/tbd/src/lib/tbd-format.ts b/packages/tbd/src/lib/tbd-format.ts index b493b77c..35a995d9 100644 --- a/packages/tbd/src/lib/tbd-format.ts +++ b/packages/tbd/src/lib/tbd-format.ts @@ -136,6 +136,8 @@ export interface MigrationResult { changed: boolean; /** Description of changes made */ changes: string[]; + /** Non-fatal warnings (e.g., preserved custom overrides) */ + warnings: string[]; } // ============================================================================= @@ -173,6 +175,7 @@ function migrate_f01_to_f02(config: RawConfig): MigrationResult { toFormat: 'f02', changed: changes.length > 0, changes, + warnings: [], }; } @@ -219,6 +222,7 @@ function migrate_f02_to_f03(config: RawConfig): MigrationResult { toFormat: 'f03', changed: changes.length > 0, changes, + warnings: [], }; } @@ -270,12 +274,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 +289,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 +297,7 @@ export function migrateToLatest(config: RawConfig): MigrationResult { current = result.config; currentFormat = 'f03' as FormatVersion; allChanges.push(...result.changes); + allWarnings.push(...result.warnings); } // Add more migrations here as new format versions are added @@ -300,6 +308,7 @@ export function migrateToLatest(config: RawConfig): MigrationResult { toFormat: currentFormat, changed: allChanges.length > 0, changes: allChanges, + warnings: allWarnings, }; } diff --git a/packages/tbd/tests/doc-cache.test.ts b/packages/tbd/tests/doc-cache.test.ts index c7e071bd..c62a617c 100644 --- a/packages/tbd/tests/doc-cache.test.ts +++ b/packages/tbd/tests/doc-cache.test.ts @@ -8,6 +8,8 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { DocCache, + generateShortcutDirectory, + type CachedDoc, SCORE_EXACT_MATCH, SCORE_PREFIX_MATCH, SCORE_CONTAINS_ALL, @@ -442,3 +444,68 @@ 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'); + }); +}); 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..03a539c3 --- /dev/null +++ b/packages/tbd/tests/helpers/doc-test-utils.ts @@ -0,0 +1,131 @@ +/** + * 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/shortcuts/system', + '.tbd/docs/shortcuts/standard', + ], + }, + }; +} + +/** + * 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', workDir]); + await execFileAsync('git', ['-C', workDir, 'config', 'user.email', 'test@test.com']); + await execFileAsync('git', ['-C', workDir, 'config', 'user.name', 'Test']); + + // 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/shortcut.test.ts b/packages/tbd/tests/shortcut.test.ts new file mode 100644 index 00000000..db952bd3 --- /dev/null +++ b/packages/tbd/tests/shortcut.test.ts @@ -0,0 +1,287 @@ +/** + * 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', 'shortcuts', 'system'); + standardDir = join(testDir, '.tbd', 'docs', 'shortcuts', 'standard'); + await mkdir(systemDir, { recursive: true }); + await mkdir(standardDir, { recursive: true }); + + // System shortcuts (hidden in real setup) + await writeFile( + join(systemDir, 'skill.md'), + `--- +title: Skill +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/shortcuts/system', '.tbd/docs/shortcuts/standard'], + 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/shortcuts/system', '.tbd/docs/shortcuts/standard'], + 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/shortcuts/standard'], 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/shortcuts/standard'], 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/shortcuts/standard'], 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/shortcuts/system', '.tbd/docs/shortcuts/standard'], + 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.md'), '# Standard skill'); + + const cache = new DocCache( + ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], + testDir, + ); + await cache.load(); + + const match = cache.get('skill'); + expect(match).not.toBeNull(); + expect(match!.doc.content).toContain('Skill Content'); // System version + expect(match!.doc.sourceDir).toBe('.tbd/docs/shortcuts/system'); + }); + + it('shadowed entries are identifiable', async () => { + await writeFile(join(standardDir, 'skill.md'), '# Standard skill'); + + const cache = new DocCache( + ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], + testDir, + ); + await cache.load(); + + const allDocs = cache.list(true); // include shadowed + const standardSkill = allDocs.find( + (d) => d.name === 'skill' && d.sourceDir === '.tbd/docs/shortcuts/standard', + ); + 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/shortcuts/standard'], 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/shortcuts/standard'], 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/shortcuts/system', '.tbd/docs/shortcuts/standard'], + 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 |'); + expect(directory).not.toContain('| skill-brief |'); + expect(directory).not.toContain('| shortcut-explanation |'); + }); + + it('wraps with directory markers', async () => { + const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + await cache.load(); + + const docs = cache.list(); + const directory = generateShortcutDirectory(docs); + + expect(directory).toContain(''); + expect(directory).toContain(''); + }); + }); +}); diff --git a/packages/tbd/tests/tbd-format.test.ts b/packages/tbd/tests/tbd-format.test.ts index 4e07f5d9..3d93b788 100644 --- a/packages/tbd/tests/tbd-format.test.ts +++ b/packages/tbd/tests/tbd-format.test.ts @@ -145,6 +145,44 @@ describe('tbd-format', () => { 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); + + // MigrationResult should always include a warnings array + 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: 'f03', + tbd_version: '0.1.6', + 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', From 4e83dd5167e6b621e6d70d89d30414ee02af4277 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 05:27:21 +0000 Subject: [PATCH 08/21] feat: Phase 1 core infrastructure (TDD) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - doc-types.ts: Doc type registry (shortcut/guideline/template/reference) - repo-url.ts: URL normalization (short/HTTPS/SSH), slugification, clone URL - DocsSourceSchema: Zod schema for internal/repo sources with prefix validation - Format bump f03→f04: Migration converts files+lookup_path to sources array - 57 new tests (16 doc-types + 16 repo-url + 20 schema + 5 format) https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/src/lib/doc-types.ts | 85 ++++++++++ packages/tbd/src/lib/repo-url.ts | 85 ++++++++++ packages/tbd/src/lib/schemas.ts | 29 ++++ packages/tbd/src/lib/tbd-format.ts | 119 ++++++++++++- packages/tbd/tests/doc-types.test.ts | 120 +++++++++++++ packages/tbd/tests/repo-url.test.ts | 118 +++++++++++++ packages/tbd/tests/schemas.test.ts | 232 ++++++++++++++++++++++++++ packages/tbd/tests/tbd-format.test.ts | 138 +++++++++++---- 8 files changed, 897 insertions(+), 29 deletions(-) create mode 100644 packages/tbd/src/lib/doc-types.ts create mode 100644 packages/tbd/src/lib/repo-url.ts create mode 100644 packages/tbd/tests/doc-types.test.ts create mode 100644 packages/tbd/tests/repo-url.test.ts 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/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..cddea1d4 100644 --- a/packages/tbd/src/lib/schemas.ts +++ b/packages/tbd/src/lib/schemas.ts @@ -199,6 +199,30 @@ export const GitRemoteName = z */ 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,6 +230,11 @@ 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/ diff --git a/packages/tbd/src/lib/tbd-format.ts b/packages/tbd/src/lib/tbd-format.ts index 35a995d9..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; + }[]; }; } @@ -226,6 +246,90 @@ function migrate_f02_to_f03(config: RawConfig): MigrationResult { }; } +/** + * 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: [], + }; +} + // ============================================================================= // Public API // ============================================================================= @@ -300,6 +404,14 @@ export function migrateToLatest(config: RawConfig): MigrationResult { 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 return { @@ -347,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/doc-types.test.ts b/packages/tbd/tests/doc-types.test.ts new file mode 100644 index 00000000..aa7b6e7e --- /dev/null +++ b/packages/tbd/tests/doc-types.test.ts @@ -0,0 +1,120 @@ +/** + * 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'; + +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/shortcuts/standard/code-review.md')).toBe('shortcut'); + expect(inferDocType('.tbd/docs/shortcuts/system/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); + }); + }); +}); 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..b6c114d0 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) @@ -358,3 +360,233 @@ describe('ConfigSchema', () => { }); }); }); + +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: { + 'shortcuts/standard/code-review.md': 'internal:shortcuts/standard/code-review.md', + }, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sources).toBeUndefined(); + } + }); +}); diff --git a/packages/tbd/tests/tbd-format.test.ts b/packages/tbd/tests/tbd-format.test.ts index 3d93b788..9f45b0c0 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,38 +110,107 @@ 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: { + 'shortcuts/system/skill.md': 'internal:shortcuts/system/skill.md', + 'guidelines/standard/typescript-rules.md': + 'internal:guidelines/standard/typescript-rules.md', + }, + lookup_path: ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], }, }; 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: { + 'shortcuts/system/skill.md': 'internal:shortcuts/system/skill.md', + 'guidelines/custom.md': 'https://example.com/custom.md', + }, + lookup_path: ['.tbd/docs/shortcuts/system'], + }, + }; + + 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?.['shortcuts/system/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); @@ -153,7 +224,6 @@ describe('tbd-format', () => { const result = migrateToLatest(config); - // MigrationResult should always include a warnings array expect(result.warnings).toBeDefined(); expect(Array.isArray(result.warnings)).toBe(true); }); @@ -173,8 +243,8 @@ describe('tbd-format', () => { it('returns empty warnings for no-op migration', () => { const config: RawConfig = { - tbd_format: 'f03', - tbd_version: '0.1.6', + tbd_format: 'f04', + tbd_version: '0.2.0', display: { id_prefix: 'test' }, }; @@ -213,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); }); }); From be428d363674212d83831514b306fdf06e0447d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 06:23:02 +0000 Subject: [PATCH 09/21] feat: Phase 1 completion - prefix-based docs, DocSync rewrite, cache clearing (TDD) Restructure bundled docs to prefix-based layout (sys/, tbd/), add resolveSourcesToDocs() and getSourcesHash() for source-based sync, implement readSourcesHash/writeSourcesHash/shouldClearDocsCache for cache invalidation. RepoCache class for sparse git checkouts. Fix lookup_path staleness detection in syncDocsWithDefaults(). All 1072 tests pass. https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .tbd/config.yml | 130 +++---- .../shortcuts}/shortcut-explanation.md | 0 .../system => sys/shortcuts}/skill-brief.md | 0 .../system => sys/shortcuts}/skill.md | 0 .../backward-compatibility-rules.md | 0 .../guidelines/bun-monorepo-patterns.md | 0 .../guidelines/cli-agent-skill-patterns.md | 0 .../guidelines/commit-conventions.md | 0 .../convex-limits-best-practices.md | 0 .../docs/{ => tbd}/guidelines/convex-rules.md | 0 .../electron-app-development-patterns.md | 0 .../guidelines/error-handling-rules.md | 0 .../guidelines/general-coding-rules.md | 0 .../guidelines/general-comment-rules.md | 0 .../guidelines/general-eng-assistant-rules.md | 0 .../guidelines/general-style-rules.md | 0 .../guidelines/general-tdd-guidelines.md | 0 .../guidelines/general-testing-rules.md | 0 .../guidelines/golden-testing-guidelines.md | 0 .../guidelines/pnpm-monorepo-patterns.md | 0 .../guidelines/python-cli-patterns.md | 0 .../guidelines/python-modern-guidelines.md | 0 .../docs/{ => tbd}/guidelines/python-rules.md | 0 .../guidelines/release-notes-guidelines.md | 0 .../guidelines/tbd-sync-troubleshooting.md | 0 .../guidelines/typescript-cli-tool-rules.md | 0 .../guidelines/typescript-code-coverage.md | 0 .../{ => tbd}/guidelines/typescript-rules.md | 0 .../guidelines/typescript-sorting-patterns.md | 0 .../typescript-yaml-handling-rules.md | 0 .../shortcuts}/agent-handoff.md | 0 .../shortcuts}/checkout-third-party-repo.md | 0 .../shortcuts}/code-cleanup-all.md | 0 .../shortcuts}/code-cleanup-docstrings.md | 0 .../shortcuts}/code-cleanup-tests.md | 0 .../shortcuts}/code-review-and-commit.md | 0 .../shortcuts}/coding-spike.md | 0 .../shortcuts}/create-or-update-pr-simple.md | 0 ...reate-or-update-pr-with-validation-plan.md | 0 .../shortcuts}/implement-beads.md | 0 .../shortcuts}/merge-upstream.md | 0 .../shortcuts}/new-architecture-doc.md | 0 .../shortcuts}/new-guideline.md | 0 .../shortcuts}/new-plan-spec.md | 0 .../shortcuts}/new-research-brief.md | 0 .../shortcuts}/new-shortcut.md | 0 .../shortcuts}/new-validation-plan.md | 0 .../plan-implementation-with-beads.md | 0 .../shortcuts}/precommit-process.md | 0 .../shortcuts}/review-code-python.md | 0 .../shortcuts}/review-code-typescript.md | 0 .../standard => tbd/shortcuts}/review-code.md | 0 .../shortcuts}/review-github-pr.md | 0 .../revise-all-architecture-docs.md | 0 .../shortcuts}/revise-architecture-doc.md | 0 .../shortcuts}/setup-github-cli.md | 0 .../shortcuts}/sync-failure-recovery.md | 0 .../shortcuts}/update-specs-status.md | 0 .../shortcuts}/welcome-user.md | 0 .../{ => tbd}/templates/architecture-doc.md | 0 .../tbd/docs/{ => tbd}/templates/plan-spec.md | 0 .../{ => tbd}/templates/research-brief.md | 0 packages/tbd/scripts/copy-docs.mjs | 30 +- packages/tbd/src/cli/commands/skill.ts | 2 +- packages/tbd/src/file/config.ts | 2 +- packages/tbd/src/file/doc-cache.ts | 2 +- packages/tbd/src/file/doc-sync.ts | 198 +++++++++- packages/tbd/src/file/repo-cache.ts | 189 +++++++++ packages/tbd/src/lib/paths.ts | 66 ++-- packages/tbd/src/lib/schemas.ts | 14 +- packages/tbd/tests/config.test.ts | 2 +- packages/tbd/tests/doc-cache.test.ts | 77 ++-- packages/tbd/tests/doc-sync.test.ts | 361 +++++++++++++++++- packages/tbd/tests/doc-types.test.ts | 4 +- packages/tbd/tests/helpers/doc-test-utils.ts | 8 +- packages/tbd/tests/integration-files.test.ts | 4 +- packages/tbd/tests/repo-cache.test.ts | 141 +++++++ packages/tbd/tests/schemas.test.ts | 28 +- packages/tbd/tests/shortcut.test.ts | 50 +-- packages/tbd/tests/tbd-format.test.ts | 10 +- 80 files changed, 1055 insertions(+), 263 deletions(-) rename packages/tbd/docs/{shortcuts/system => sys/shortcuts}/shortcut-explanation.md (100%) rename packages/tbd/docs/{shortcuts/system => sys/shortcuts}/skill-brief.md (100%) rename packages/tbd/docs/{shortcuts/system => sys/shortcuts}/skill.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/backward-compatibility-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/bun-monorepo-patterns.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/cli-agent-skill-patterns.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/commit-conventions.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/convex-limits-best-practices.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/convex-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/electron-app-development-patterns.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/error-handling-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/general-coding-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/general-comment-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/general-eng-assistant-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/general-style-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/general-tdd-guidelines.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/general-testing-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/golden-testing-guidelines.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/pnpm-monorepo-patterns.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/python-cli-patterns.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/python-modern-guidelines.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/python-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/release-notes-guidelines.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/tbd-sync-troubleshooting.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/typescript-cli-tool-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/typescript-code-coverage.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/typescript-rules.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/typescript-sorting-patterns.md (100%) rename packages/tbd/docs/{ => tbd}/guidelines/typescript-yaml-handling-rules.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/agent-handoff.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/checkout-third-party-repo.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/code-cleanup-all.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/code-cleanup-docstrings.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/code-cleanup-tests.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/code-review-and-commit.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/coding-spike.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/create-or-update-pr-simple.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/create-or-update-pr-with-validation-plan.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/implement-beads.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/merge-upstream.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/new-architecture-doc.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/new-guideline.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/new-plan-spec.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/new-research-brief.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/new-shortcut.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/new-validation-plan.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/plan-implementation-with-beads.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/precommit-process.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/review-code-python.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/review-code-typescript.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/review-code.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/review-github-pr.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/revise-all-architecture-docs.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/revise-architecture-doc.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/setup-github-cli.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/sync-failure-recovery.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/update-specs-status.md (100%) rename packages/tbd/docs/{shortcuts/standard => tbd/shortcuts}/welcome-user.md (100%) rename packages/tbd/docs/{ => tbd}/templates/architecture-doc.md (100%) rename packages/tbd/docs/{ => tbd}/templates/plan-spec.md (100%) rename packages/tbd/docs/{ => tbd}/templates/research-brief.md (100%) create mode 100644 packages/tbd/src/file/repo-cache.ts create mode 100644 packages/tbd/tests/repo-cache.test.ts diff --git a/.tbd/config.yml b/.tbd/config.yml index 1fa02bb7..ae3e43dd 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,70 +14,70 @@ 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-brief.md: internal:shortcuts/system/skill-brief.md - shortcuts/system/skill.md: internal:shortcuts/system/skill.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-brief.md: internal:sys/shortcuts/skill-brief.md + sys/shortcuts/skill.md: internal:sys/shortcuts/skill.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 @@ -85,5 +85,5 @@ settings: sync: branch: tbd-sync remote: origin -tbd_format: f03 +tbd_format: f04 tbd_version: development diff --git a/packages/tbd/docs/shortcuts/system/shortcut-explanation.md b/packages/tbd/docs/sys/shortcuts/shortcut-explanation.md similarity index 100% rename from packages/tbd/docs/shortcuts/system/shortcut-explanation.md rename to packages/tbd/docs/sys/shortcuts/shortcut-explanation.md 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.md b/packages/tbd/docs/sys/shortcuts/skill.md similarity index 100% rename from packages/tbd/docs/shortcuts/system/skill.md rename to packages/tbd/docs/sys/shortcuts/skill.md 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 100% rename from packages/tbd/docs/shortcuts/standard/new-shortcut.md rename to packages/tbd/docs/tbd/shortcuts/new-shortcut.md 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 9b11490e..48e0d3e6 100644 --- a/packages/tbd/scripts/copy-docs.mjs +++ b/packages/tbd/scripts/copy-docs.mjs @@ -29,12 +29,13 @@ 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'); +const TBD_SHORTCUTS_DIR = join(DOCS_DIR, 'tbd', 'shortcuts'); +const TBD_GUIDELINES_DIR = join(DOCS_DIR, 'tbd', 'guidelines'); +const TBD_TEMPLATES_DIR = join(DOCS_DIR, 'tbd', 'templates'); /** * Packaged documentation files (in packages/tbd/docs/). @@ -96,29 +97,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.md'), 'utf-8'); + const skillContent = readFileSync(join(SYS_SHORTCUTS_DIR, 'skill.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/commands/skill.ts b/packages/tbd/src/cli/commands/skill.ts index 409e4b6b..c5df9835 100644 --- a/packages/tbd/src/cli/commands/skill.ts +++ b/packages/tbd/src/cli/commands/skill.ts @@ -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.md'); + const baseSkill = await loadDocContent('sys/shortcuts/skill.md'); // Get shortcut directory const directory = await this.getShortcutDirectory(); 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 { 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..34baf926 --- /dev/null +++ b/packages/tbd/src/file/repo-cache.ts @@ -0,0 +1,189 @@ +/** + * 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 } from 'node:path'; +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. + */ + getRepoDir(url: string): string { + 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 + * @returns Path to the local checkout directory + */ + async ensureRepo(url: string, ref: string, paths: string[]): Promise { + const repoDir = this.getRepoDir(url); + await mkdir(this.cacheDir, { recursive: true }); + + const exists = await this.isCloned(repoDir); + + if (!exists) { + await this.cloneRepo(url, ref, repoDir, paths); + } else { + 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 + const relativePath = join(pathPattern, relativeMdPath); + 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']); + } + + /** + * 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) - convert to file:// for --depth support + if (url.startsWith('/') || url.startsWith('.')) { + 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/paths.ts b/packages/tbd/src/lib/paths.ts index e12a10df..d3fb802c 100644 --- a/packages/tbd/src/lib/paths.ts +++ b/packages/tbd/src/lib/paths.ts @@ -168,38 +168,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.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/shortcuts/system/ */ -export const TBD_SHORTCUTS_SYSTEM = join(TBD_SHORTCUTS_DIR, SYSTEM_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 standard shortcuts: .tbd/docs/shortcuts/standard/ */ -export const TBD_SHORTCUTS_STANDARD = join(TBD_SHORTCUTS_DIR, STANDARD_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 guidelines: .tbd/docs/guidelines/ (top-level, not under shortcuts) */ -export const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, GUIDELINES_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 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 +202,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) */ @@ -226,25 +229,24 @@ 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/ + TBD_SHORTCUTS_SYSTEM, // .tbd/docs/sys/shortcuts/ + TBD_SHORTCUTS_STANDARD, // .tbd/docs/tbd/shortcuts/ ]; /** * Default guidelines lookup paths (relative to tbd root). */ export const DEFAULT_GUIDELINES_PATHS = [ - TBD_GUIDELINES_DIR, // .tbd/docs/guidelines/ + TBD_GUIDELINES_DIR, // .tbd/docs/tbd/guidelines/ ]; /** * Default template lookup paths (relative to tbd root). */ export const DEFAULT_TEMPLATE_PATHS = [ - TBD_TEMPLATES_DIR, // .tbd/docs/templates/ + TBD_TEMPLATES_DIR, // .tbd/docs/tbd/templates/ ]; /** diff --git a/packages/tbd/src/lib/schemas.ts b/packages/tbd/src/lib/schemas.ts index cddea1d4..ace846ac 100644 --- a/packages/tbd/src/lib/schemas.ts +++ b/packages/tbd/src/lib/schemas.ts @@ -185,16 +185,16 @@ 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()); @@ -239,7 +239,7 @@ export const DocsCacheSchema = z.object({ * 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(), @@ -247,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/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-cache.test.ts b/packages/tbd/tests/doc-cache.test.ts index c62a617c..001c6ff2 100644 --- a/packages/tbd/tests/doc-cache.test.ts +++ b/packages/tbd/tests/doc-cache.test.ts @@ -25,8 +25,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 }); }); @@ -41,10 +41,7 @@ describe('DocCache', () => { await writeFile(join(systemDir, 'skill.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(); @@ -58,7 +55,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(); @@ -68,7 +65,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(); @@ -92,7 +89,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(); @@ -105,7 +102,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(); @@ -118,7 +115,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'); @@ -130,7 +127,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'); @@ -141,7 +138,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'); @@ -181,7 +178,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'); @@ -191,7 +188,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'); @@ -201,7 +198,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'); @@ -212,7 +209,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'); @@ -222,7 +219,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'); @@ -230,7 +227,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); @@ -238,7 +235,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'); @@ -252,7 +249,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(''); @@ -262,7 +259,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(' '); @@ -279,7 +276,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 @@ -296,7 +293,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'); @@ -318,7 +315,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 '); @@ -330,7 +327,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'); @@ -351,7 +348,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 @@ -366,16 +363,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 () => { @@ -383,10 +377,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(); @@ -398,10 +389,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); @@ -412,15 +400,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); diff --git a/packages/tbd/tests/doc-sync.test.ts b/packages/tbd/tests/doc-sync.test.ts index 44b6cfcf..27c1424e 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,336 @@ 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.md']).toBe('internal:sys/shortcuts/skill.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.md']).toBe('internal:sys/shortcuts/skill.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.md']).toBe('internal:sys/shortcuts/skill.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.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); + }); + }); }); diff --git a/packages/tbd/tests/doc-types.test.ts b/packages/tbd/tests/doc-types.test.ts index aa7b6e7e..0727f483 100644 --- a/packages/tbd/tests/doc-types.test.ts +++ b/packages/tbd/tests/doc-types.test.ts @@ -77,8 +77,8 @@ describe('doc-types', () => { }); it('infers from old-style nested paths', () => { - expect(inferDocType('.tbd/docs/shortcuts/standard/code-review.md')).toBe('shortcut'); - expect(inferDocType('.tbd/docs/shortcuts/system/skill.md')).toBe('shortcut'); + 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', () => { diff --git a/packages/tbd/tests/helpers/doc-test-utils.ts b/packages/tbd/tests/helpers/doc-test-utils.ts index 03a539c3..e7725e4b 100644 --- a/packages/tbd/tests/helpers/doc-test-utils.ts +++ b/packages/tbd/tests/helpers/doc-test-utils.ts @@ -74,10 +74,7 @@ export function createMockConfig(overrides?: { settings: { auto_sync: false, doc_auto_sync_hours: 24 }, docs_cache: { files: overrides?.files ?? {}, - lookup_path: overrides?.lookupPath ?? [ - '.tbd/docs/shortcuts/system', - '.tbd/docs/shortcuts/standard', - ], + lookup_path: overrides?.lookupPath ?? ['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], }, }; } @@ -107,9 +104,10 @@ export async function createTestBareRepo(files: Record): Promise ); await mkdir(workDir, { recursive: true }); - await execFileAsync('git', ['init', workDir]); + 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)) { diff --git a/packages/tbd/tests/integration-files.test.ts b/packages/tbd/tests/integration-files.test.ts index 0c351bf4..2a5dd26b 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.md) + * - Base skill content (from dist/docs/sys/shortcuts/skill.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/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/schemas.test.ts b/packages/tbd/tests/schemas.test.ts index b6c114d0..db27acd3 100644 --- a/packages/tbd/tests/schemas.test.ts +++ b/packages/tbd/tests/schemas.test.ts @@ -279,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', }, }, @@ -290,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', }); } @@ -304,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', ], }, }; @@ -315,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', ]); } }); @@ -327,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'], }, }; @@ -353,8 +353,8 @@ 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', ]); } }); @@ -581,7 +581,7 @@ describe('DocsCacheSchema with sources', () => { it('accepts docs_cache without sources (backward compat)', () => { const result = DocsCacheSchema.safeParse({ files: { - 'shortcuts/standard/code-review.md': 'internal:shortcuts/standard/code-review.md', + 'tbd/shortcuts/code-review.md': 'internal:tbd/shortcuts/code-review.md', }, }); expect(result.success).toBe(true); diff --git a/packages/tbd/tests/shortcut.test.ts b/packages/tbd/tests/shortcut.test.ts index db952bd3..22438ef6 100644 --- a/packages/tbd/tests/shortcut.test.ts +++ b/packages/tbd/tests/shortcut.test.ts @@ -20,8 +20,8 @@ describe('shortcut command behavior', () => { beforeEach(async () => { testDir = join(tmpdir(), `shortcut-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 }); @@ -98,10 +98,7 @@ Follow TDD to implement beads.`, describe('exact lookup', () => { it('finds shortcut by exact name and returns content', async () => { - 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('code-review'); @@ -111,10 +108,7 @@ Follow TDD to implement beads.`, }); it('finds system shortcuts by exact name', async () => { - 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('shortcut-explanation'); @@ -125,7 +119,7 @@ Follow TDD to implement beads.`, describe('fuzzy search with score thresholds', () => { it('prefix match returns score >= SCORE_PREFIX_MATCH', 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('code-rev'); @@ -135,7 +129,7 @@ Follow TDD to implement beads.`, }); it('word-based match returns lower score than prefix match', 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('review code'); @@ -147,7 +141,7 @@ Follow TDD to implement beads.`, describe('--category filtering', () => { it('filters docs by category from frontmatter', async () => { - const cache = new DocCache(['.tbd/docs/shortcuts/standard'], testDir); + const cache = new DocCache(['.tbd/docs/tbd/shortcuts'], testDir); await cache.load(); const allDocs = cache.list(); @@ -161,10 +155,7 @@ Follow TDD to implement beads.`, describe('no-query fallback', () => { it('shortcut-explanation.md is accessible for no-query mode', async () => { - 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 explanation = cache.get('shortcut-explanation'); @@ -191,30 +182,24 @@ Follow TDD to implement beads.`, // Add a file with same name to both dirs await writeFile(join(standardDir, 'skill.md'), '# Standard skill'); - 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('skill'); expect(match).not.toBeNull(); expect(match!.doc.content).toContain('Skill Content'); // System version - expect(match!.doc.sourceDir).toBe('.tbd/docs/shortcuts/system'); + expect(match!.doc.sourceDir).toBe('.tbd/docs/sys/shortcuts'); }); it('shadowed entries are identifiable', async () => { await writeFile(join(standardDir, 'skill.md'), '# Standard skill'); - 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); // include shadowed const standardSkill = allDocs.find( - (d) => d.name === 'skill' && d.sourceDir === '.tbd/docs/shortcuts/standard', + (d) => d.name === 'skill' && d.sourceDir === '.tbd/docs/tbd/shortcuts', ); expect(standardSkill).toBeDefined(); expect(cache.isShadowed(standardSkill!)).toBe(true); @@ -223,7 +208,7 @@ Follow TDD to implement beads.`, describe('JSON output shape', () => { it('list output has expected fields', async () => { - 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(); @@ -239,7 +224,7 @@ Follow TDD to implement beads.`, }); it('search result has expected fields', 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('code-review'); @@ -253,10 +238,7 @@ Follow TDD to implement beads.`, describe('generateShortcutDirectory', () => { it('generates directory excluding system shortcuts by name', async () => { - 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(); @@ -274,7 +256,7 @@ Follow TDD to implement beads.`, }); it('wraps with directory markers', async () => { - 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(); diff --git a/packages/tbd/tests/tbd-format.test.ts b/packages/tbd/tests/tbd-format.test.ts index 9f45b0c0..b2a63b34 100644 --- a/packages/tbd/tests/tbd-format.test.ts +++ b/packages/tbd/tests/tbd-format.test.ts @@ -129,11 +129,11 @@ describe('tbd-format', () => { settings: { auto_sync: false, doc_auto_sync_hours: 12 }, docs_cache: { files: { - 'shortcuts/system/skill.md': 'internal:shortcuts/system/skill.md', + 'sys/shortcuts/skill.md': 'internal:sys/shortcuts/skill.md', 'guidelines/standard/typescript-rules.md': 'internal:guidelines/standard/typescript-rules.md', }, - lookup_path: ['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard'], + lookup_path: ['.tbd/docs/sys/shortcuts', '.tbd/docs/tbd/shortcuts'], }, }; @@ -159,10 +159,10 @@ describe('tbd-format', () => { display: { id_prefix: 'test' }, docs_cache: { files: { - 'shortcuts/system/skill.md': 'internal:shortcuts/system/skill.md', + 'sys/shortcuts/skill.md': 'internal:sys/shortcuts/skill.md', 'guidelines/custom.md': 'https://example.com/custom.md', }, - lookup_path: ['.tbd/docs/shortcuts/system'], + lookup_path: ['.tbd/docs/sys/shortcuts'], }, }; @@ -175,7 +175,7 @@ describe('tbd-format', () => { 'https://example.com/custom.md', ); // Default internal entries should NOT be in files anymore - expect(result.config.docs_cache?.files?.['shortcuts/system/skill.md']).toBeUndefined(); + expect(result.config.docs_cache?.files?.['sys/shortcuts/skill.md']).toBeUndefined(); }); it('migrates f03 to f04 with empty docs_cache', () => { From a72464f7549b3dfb50743fb9f716cc4762244398 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 06:42:01 +0000 Subject: [PATCH 10/21] feat: Phase 2 - prefix-aware DocCache, parseQualifiedName, repo-cache gitignore (TDD) Add prefix field to CachedDoc with path-based inference, qualified name lookup (spec:name), parseQualifiedName() utility, repo-cache/ gitignore in all setup paths, onProgress callback for RepoCache. All 1083 tests pass. https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/src/cli/commands/init.ts | 3 + packages/tbd/src/cli/commands/setup.ts | 8 +- packages/tbd/src/file/doc-cache.ts | 72 ++++++++++++++- packages/tbd/src/file/repo-cache.ts | 10 ++- packages/tbd/tests/doc-cache.test.ts | 116 +++++++++++++++++++++++++ packages/tbd/tests/doc-sync.test.ts | 67 ++++++++++++++ 6 files changed, 270 insertions(+), 6 deletions(-) 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/setup.ts b/packages/tbd/src/cli/commands/setup.ts index 5be453c6..1b4798e2 100644 --- a/packages/tbd/src/cli/commands/setup.ts +++ b/packages/tbd/src/cli/commands/setup.ts @@ -1145,6 +1145,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 +1403,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 +1652,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/file/doc-cache.ts b/packages/tbd/src/file/doc-cache.ts index 20235200..93a1c7d1 100644 --- a/packages/tbd/src/file/doc-cache.ts +++ b/packages/tbd/src/file/doc-cache.ts @@ -70,6 +70,8 @@ export interface CachedDoc { content: string; /** Which directory in the path this doc came from */ sourceDir: string; + /** Source prefix (e.g., 'tbd', 'sys', 'spec') inferred from path */ + prefix?: string; /** File size in bytes */ sizeBytes: number; /** Estimated token count (based on ~3.5 chars/token) */ @@ -78,6 +80,38 @@ export interface CachedDoc { hidden?: boolean; } +/** + * Parsed qualified name result. + */ +export interface QualifiedName { + /** Source prefix (e.g., 'spec' from 'spec:typescript-rules') */ + prefix?: string; + /** Base document name (without prefix or .md extension) */ + baseName: string; +} + +/** + * Parse a potentially qualified document name. + * + * Supports formats: + * - 'typescript-rules' → { baseName: 'typescript-rules' } + * - 'spec:typescript-rules' → { prefix: 'spec', baseName: 'typescript-rules' } + * - 'typescript-rules.md' → { baseName: 'typescript-rules' } + */ +export function parseQualifiedName(name: string): QualifiedName { + const cleanName = name.endsWith('.md') ? name.slice(0, -3) : name; + + const colonIndex = cleanName.indexOf(':'); + if (colonIndex > 0) { + return { + prefix: cleanName.slice(0, colonIndex), + baseName: cleanName.slice(colonIndex + 1), + }; + } + + return { baseName: cleanName }; +} + /** * A document match with relevance score. */ @@ -220,6 +254,7 @@ export class DocCache { frontmatter, content, sourceDir, + prefix: inferPrefix(sourceDir), sizeBytes, approxTokens, }; @@ -267,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 }; @@ -488,3 +533,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/repo-cache.ts b/packages/tbd/src/file/repo-cache.ts index 34baf926..4c6f9538 100644 --- a/packages/tbd/src/file/repo-cache.ts +++ b/packages/tbd/src/file/repo-cache.ts @@ -50,17 +50,25 @@ export class RepoCache { * @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[]): Promise { + 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); } diff --git a/packages/tbd/tests/doc-cache.test.ts b/packages/tbd/tests/doc-cache.test.ts index 001c6ff2..6787f183 100644 --- a/packages/tbd/tests/doc-cache.test.ts +++ b/packages/tbd/tests/doc-cache.test.ts @@ -9,6 +9,7 @@ import { tmpdir } from 'node:os'; import { DocCache, generateShortcutDirectory, + parseQualifiedName, type CachedDoc, SCORE_EXACT_MATCH, SCORE_PREFIX_MATCH, @@ -494,3 +495,118 @@ describe('generateShortcutDirectory', () => { 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 27c1424e..4abc2d8f 100644 --- a/packages/tbd/tests/doc-sync.test.ts +++ b/packages/tbd/tests/doc-sync.test.ts @@ -666,5 +666,72 @@ describe('doc-sync', () => { 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.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([]); + }); }); }); From d00c8fbc9a76a8a3ab1fe2f8913417c2c9abbef9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 17:14:21 +0000 Subject: [PATCH 11/21] feat: Phase 3-5 - reference cmd, doc-types registry paths, doctor checks, doc validation (TDD) - Add `tbd reference` command extending DocCommandHandler - Replace hardcoded DEFAULT_*_PATHS with getDefaultDocPaths() from doc-types registry - Add checkRepoCacheHealth() to doctor command with orphan detection - Simplify doc-add getDocTypeSubdir to flat type directories - Add 'reference' to DocType union in doc-add.ts - Update all doc path references from old system/standard layout to prefix-based - Add validate-docs.sh comparison script - 14 new tests (doc-types, reference, doctor, doc-add) https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- docs/development.md | 4 +- docs/docs-overview.md | 9 +- .../sys/shortcuts/shortcut-explanation.md | 6 +- packages/tbd/docs/sys/shortcuts/skill.md | 1 + packages/tbd/docs/tbd-docs.md | 3 +- .../tbd/docs/tbd/shortcuts/new-shortcut.md | 6 +- packages/tbd/src/cli/cli.ts | 2 + packages/tbd/src/cli/commands/doctor.ts | 83 +++++++++++++++++ packages/tbd/src/cli/commands/guidelines.ts | 4 +- packages/tbd/src/cli/commands/prime.ts | 10 +- packages/tbd/src/cli/commands/reference.ts | 65 +++++++++++++ packages/tbd/src/cli/commands/setup.ts | 7 +- packages/tbd/src/cli/commands/shortcut.ts | 4 +- packages/tbd/src/cli/commands/skill.ts | 6 +- packages/tbd/src/cli/commands/template.ts | 4 +- packages/tbd/src/file/doc-add.ts | 6 +- packages/tbd/src/lib/paths.ts | 35 +++---- packages/tbd/tests/doc-add-e2e.test.ts | 8 +- packages/tbd/tests/doc-add.test.ts | 18 ++-- packages/tbd/tests/doc-types.test.ts | 39 ++++++++ packages/tbd/tests/doctor-sync.test.ts | 68 ++++++++++++++ packages/tbd/tests/reference.test.ts | 91 +++++++++++++++++++ scripts/validate-docs.sh | 77 ++++++++++++++++ 23 files changed, 490 insertions(+), 66 deletions(-) create mode 100644 packages/tbd/src/cli/commands/reference.ts create mode 100644 packages/tbd/tests/reference.test.ts create mode 100755 scripts/validate-docs.sh 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/packages/tbd/docs/sys/shortcuts/shortcut-explanation.md b/packages/tbd/docs/sys/shortcuts/shortcut-explanation.md index 4f6e46f4..9da6f749 100644 --- a/packages/tbd/docs/sys/shortcuts/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/sys/shortcuts/skill.md b/packages/tbd/docs/sys/shortcuts/skill.md index f273badd..f6ecd852 100644 --- a/packages/tbd/docs/sys/shortcuts/skill.md +++ b/packages/tbd/docs/sys/shortcuts/skill.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/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/tbd/shortcuts/new-shortcut.md b/packages/tbd/docs/tbd/shortcuts/new-shortcut.md index 90181452..704c5d58 100644 --- a/packages/tbd/docs/tbd/shortcuts/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/src/cli/cli.ts b/packages/tbd/src/cli/cli.ts index f1c4d7c9..773d2f00 100644 --- a/packages/tbd/src/cli/cli.ts +++ b/packages/tbd/src/cli/cli.ts @@ -43,6 +43,7 @@ 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 { setupCommand } from './commands/setup.js'; import { saveCommand } from './commands/save.js'; import { workspaceCommand } from './commands/workspace.js'; @@ -84,6 +85,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); diff --git a/packages/tbd/src/cli/commands/doctor.ts b/packages/tbd/src/cli/commands/doctor.ts index 92c0cf42..8bf1a4de 100644 --- a/packages/tbd/src/cli/commands/doctor.ts +++ b/packages/tbd/src/cli/commands/doctor.ts @@ -128,6 +128,10 @@ class DoctorHandler extends BaseCommand { // Check 14: 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[] = []; @@ -989,6 +993,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/prime.ts b/packages/tbd/src/cli/commands/prime.ts index e4126696..bc5e712a 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 1b4798e2..15dac81d 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(); diff --git a/packages/tbd/src/cli/commands/shortcut.ts b/packages/tbd/src/cli/commands/shortcut.ts index 373833ca..238071ea 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 c5df9835..2f47a979 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; @@ -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/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/doc-add.ts b/packages/tbd/src/file/doc-add.ts index 792fc32a..fcfe9244 100644 --- a/packages/tbd/src/file/doc-add.ts +++ b/packages/tbd/src/file/doc-add.ts @@ -21,7 +21,7 @@ import { TBD_DOCS_DIR } from '../lib/paths.js'; /** * The type of document being added. */ -export type DocType = 'guideline' | 'shortcut' | 'template'; +export type DocType = 'guideline' | 'shortcut' | 'template' | 'reference'; /** * Options for adding a document. @@ -94,9 +94,11 @@ export function getDocTypeSubdir(docType: DocType): string { case 'guideline': return 'guidelines'; case 'shortcut': - return 'shortcuts/custom'; + return 'shortcuts'; case 'template': return 'templates'; + case 'reference': + return 'references'; } } diff --git a/packages/tbd/src/lib/paths.ts b/packages/tbd/src/lib/paths.ts index d3fb802c..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'; @@ -227,27 +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. - */ -export const DEFAULT_SHORTCUT_PATHS = [ - TBD_SHORTCUTS_SYSTEM, // .tbd/docs/sys/shortcuts/ - TBD_SHORTCUTS_STANDARD, // .tbd/docs/tbd/shortcuts/ -]; - -/** - * Default guidelines lookup paths (relative to tbd root). - */ -export const DEFAULT_GUIDELINES_PATHS = [ - TBD_GUIDELINES_DIR, // .tbd/docs/tbd/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/tbd/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/tests/doc-add-e2e.test.ts b/packages/tbd/tests/doc-add-e2e.test.ts index ae448f2d..45c97a12 100644 --- a/packages/tbd/tests/doc-add-e2e.test.ts +++ b/packages/tbd/tests/doc-add-e2e.test.ts @@ -127,7 +127,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 +142,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 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 shortcuts/ + const docPath = join(tempDir, '.tbd', 'docs', 'shortcuts', 'my-custom-shortcut.md'); await access(docPath); }); }); diff --git a/packages/tbd/tests/doc-add.test.ts b/packages/tbd/tests/doc-add.test.ts index 5992acb1..b73250c2 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 @@ -221,18 +225,18 @@ describe('addDoc', () => { expect(result.destPath).toBe('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('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', 'shortcuts', 'test-shortcut.md'), 'utf-8', ); expect(content).toBe('# Mocked Document\n\nThis is mocked content for testing.\n'); @@ -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/shortcuts'); }); it('does not duplicate lookup_path entry on second add', async () => { diff --git a/packages/tbd/tests/doc-types.test.ts b/packages/tbd/tests/doc-types.test.ts index 0727f483..8ba587d7 100644 --- a/packages/tbd/tests/doc-types.test.ts +++ b/packages/tbd/tests/doc-types.test.ts @@ -10,6 +10,8 @@ import { 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', () => { @@ -117,4 +119,41 @@ describe('doc-types', () => { 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 3c3081c6..f2dbc2cd 100644 --- a/packages/tbd/tests/doctor-sync.test.ts +++ b/packages/tbd/tests/doctor-sync.test.ts @@ -151,6 +151,74 @@ describe('doctor command logic', () => { }); }); +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/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/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 ===" From 9398e59c5c87e1a392719732e8252c731b037476 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 17:23:12 +0000 Subject: [PATCH 12/21] fix: increase setup-hooks test timeout to 15000ms (matches setup-flows pattern) The tests spawn tbd setup --auto subprocesses that take 2-3s each. Under load during pre-push hook, they exceeded the default 5000ms timeout. https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/tests/setup-hooks.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tbd/tests/setup-hooks.test.ts b/packages/tbd/tests/setup-hooks.test.ts index 165a0b9f..ed15fe1c 100644 --- a/packages/tbd/tests/setup-hooks.test.ts +++ b/packages/tbd/tests/setup-hooks.test.ts @@ -18,7 +18,7 @@ import { execSync, spawnSync } from 'node:child_process'; // Shell script hooks are Unix-only; skip entire suite on Windows const describeUnix = platform() === 'win32' ? describe.skip : describe; -describeUnix('setup hooks (project-local)', () => { +describeUnix('setup hooks (project-local)', { timeout: 15000 }, () => { let tempDir: string; let fakeHome: string; let originalHome: string | undefined; From 14148a777c422eefeffaca59ee47e4891f32afbb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 17:25:58 +0000 Subject: [PATCH 13/21] chore: update auto-generated skill and gitignore files https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .claude/skills/tbd/SKILL.md | 1 + .tbd/.gitignore | 4 ++++ AGENTS.md | 1 + 3 files changed, 6 insertions(+) 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/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 From 6fe6c2c3e57aed8996f6e351ac63ac5afec7975a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 18:39:32 +0000 Subject: [PATCH 14/21] feat: add `tbd source add/list/remove` commands for managing doc sources (TDD) - `tbd source add --prefix ` adds external repo source to config - `tbd source list` shows all configured sources (internal + repo) - `tbd source remove ` removes a repo source by prefix - Validates prefix format, rejects duplicates, prevents removing internal sources - Defaults to all doc type directories, supports --ref and --paths options - 12 new tests covering add/list/remove with edge cases https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/src/cli/cli.ts | 2 + packages/tbd/src/cli/commands/source.ts | 200 ++++++++++++++++++++++++ packages/tbd/tests/source.test.ts | 179 +++++++++++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 packages/tbd/src/cli/commands/source.ts create mode 100644 packages/tbd/tests/source.test.ts diff --git a/packages/tbd/src/cli/cli.ts b/packages/tbd/src/cli/cli.ts index 773d2f00..5b7cade8 100644 --- a/packages/tbd/src/cli/cli.ts +++ b/packages/tbd/src/cli/cli.ts @@ -44,6 +44,7 @@ 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'; @@ -94,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/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/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); + }); + }); +}); From 6388fac620c4f7ae6209474a127e4d0c2a4b2311 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 19:05:43 +0000 Subject: [PATCH 15/21] docs: update spec to reflect tbd source commands are implemented The source management CLI (add/list/remove) was listed as "Future Work" but has been fully implemented with 12 tests. Move to "Implemented" section. https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .../plan-2026-02-02-external-docs-repos.md | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) 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 3e5452c9..9d42e4dc 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 @@ -1528,16 +1528,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 @@ -1546,11 +1546,16 @@ 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 sources can be removed) +- 12 unit tests in `source.test.ts` + +## Future Work **Cross-prefix search** (not in initial implementation): From d90ddb0cb1bd4ef32f49496e94e4465386bd62d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 19:28:22 +0000 Subject: [PATCH 16/21] fix: doc-add uses prefix-based paths, remove unused vars in copy-docs - addDoc now writes to tbd/{type}/ prefix path instead of flat {type}/ - Updated lookup_path to use .tbd/docs/tbd/{type} pattern - Removed unused TBD_SHORTCUTS_DIR, TBD_GUIDELINES_DIR, TBD_TEMPLATES_DIR from copy-docs.mjs (lint error on CI) - Updated unit and e2e tests for prefix-based paths https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/scripts/copy-docs.mjs | 3 --- packages/tbd/src/file/doc-add.ts | 11 ++++++----- packages/tbd/tests/doc-add-e2e.test.ts | 13 +++++++------ packages/tbd/tests/doc-add.test.ts | 20 ++++++++++---------- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/tbd/scripts/copy-docs.mjs b/packages/tbd/scripts/copy-docs.mjs index 48e0d3e6..0e07e5f9 100644 --- a/packages/tbd/scripts/copy-docs.mjs +++ b/packages/tbd/scripts/copy-docs.mjs @@ -33,9 +33,6 @@ const repoRoot = join(root, '..', '..'); const DOCS_DIR = join(root, 'docs'); const INSTALL_DIR = join(DOCS_DIR, 'install'); const SYS_SHORTCUTS_DIR = join(DOCS_DIR, 'sys', 'shortcuts'); -const TBD_SHORTCUTS_DIR = join(DOCS_DIR, 'tbd', 'shortcuts'); -const TBD_GUIDELINES_DIR = join(DOCS_DIR, 'tbd', 'guidelines'); -const TBD_TEMPLATES_DIR = join(DOCS_DIR, 'tbd', 'templates'); /** * Packaged documentation files (in packages/tbd/docs/). diff --git a/packages/tbd/src/file/doc-add.ts b/packages/tbd/src/file/doc-add.ts index fcfe9244..e93379ee 100644 --- a/packages/tbd/src/file/doc-add.ts +++ b/packages/tbd/src/file/doc-add.ts @@ -12,7 +12,7 @@ import { writeFile } from 'atomically'; import { readConfig, writeConfig } from './config.js'; import { githubBlobToRawUrl, fetchWithGhFallback } from './github-fetch.js'; -import { TBD_DOCS_DIR } from '../lib/paths.js'; +import { TBD_DOCS_DIR, TBD_PREFIX } from '../lib/paths.js'; // ============================================================================= // Types @@ -125,7 +125,8 @@ export async function addDoc(tbdRoot: string, options: AddDocOptions): Promise { } 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', ); @@ -142,10 +143,10 @@ describe('doc --add end-to-end', () => { } expect(addResult.status).toBe(0); - expect(addResult.stdout).toContain('Added to shortcuts/my-custom-shortcut.md'); + expect(addResult.stdout).toContain('Added to tbd/shortcuts/my-custom-shortcut.md'); - // Verify the file went to shortcuts/ - const docPath = join(tempDir, '.tbd', 'docs', 'shortcuts', '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 b73250c2..8ac77df4 100644 --- a/packages/tbd/tests/doc-add.test.ts +++ b/packages/tbd/tests/doc-add.test.ts @@ -181,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 @@ -189,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'); @@ -222,7 +222,7 @@ 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 subdir for shortcut type', async () => { @@ -232,11 +232,11 @@ describe('addDoc', () => { docType: 'shortcut', }); - expect(result.destPath).toBe('shortcuts/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', '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'); @@ -249,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 () => { @@ -260,7 +260,7 @@ describe('addDoc', () => { }); const configContent = await readFile(join(tempDir, '.tbd', 'config.yml'), 'utf-8'); - expect(configContent).toContain('.tbd/docs/shortcuts'); + expect(configContent).toContain('.tbd/docs/tbd/shortcuts'); }); it('does not duplicate lookup_path entry on second add', async () => { @@ -277,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); }); From 59930d4ab2d52341b63e951f8f37b8902cdb3141 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 19:44:23 +0000 Subject: [PATCH 17/21] =?UTF-8?q?fix:=20CI=20failures=20=E2=80=94=20Window?= =?UTF-8?q?s=20paths,=20tryscript=20golden=20tests,=20format=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - repo-cache: Handle local filesystem paths (including Windows drive letters) by hashing them instead of URL-normalizing - cli-no-cache.tryscript: Update shortcuts path from shortcuts/standard/ to tbd/shortcuts/ - cli-setup.tryscript: Add reference and source commands to help output - cli-orientation-golden.tryscript: Add Repo cache health check line - cli-advanced.tryscript: Update config format from f03 to f04 https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/src/file/repo-cache.ts | 22 ++++++++++++++++--- packages/tbd/tests/cli-advanced.tryscript.md | 5 +++-- packages/tbd/tests/cli-no-cache.tryscript.md | 2 +- .../tests/cli-orientation-golden.tryscript.md | 1 + packages/tbd/tests/cli-setup.tryscript.md | 2 ++ 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/tbd/src/file/repo-cache.ts b/packages/tbd/src/file/repo-cache.ts index 4c6f9538..a91b992e 100644 --- a/packages/tbd/src/file/repo-cache.ts +++ b/packages/tbd/src/file/repo-cache.ts @@ -8,7 +8,8 @@ import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; -import { join } from 'node:path'; +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'; @@ -35,8 +36,14 @@ export class RepoCache { /** * 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); } @@ -152,6 +159,15 @@ export class RepoCache { 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. @@ -161,8 +177,8 @@ export class RepoCache { if (url.startsWith('file://')) { return url; } - // Local paths (absolute or relative) - convert to file:// for --depth support - if (url.startsWith('/') || url.startsWith('.')) { + // 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 diff --git a/packages/tbd/tests/cli-advanced.tryscript.md b/packages/tbd/tests/cli-advanced.tryscript.md index cda4901f..3960db23 100644 --- a/packages/tbd/tests/cli-advanced.tryscript.md +++ b/packages/tbd/tests/cli-advanced.tryscript.md @@ -258,7 +258,7 @@ settings: ```console $ tbd config show --json { - "tbd_format": "f03", + "tbd_format": "f04", "tbd_version": "[..]", "sync": { "branch": "tbd-sync", @@ -271,7 +271,8 @@ $ tbd config show --json "auto_sync": false, "doc_auto_sync_hours": 24, "use_gh_cli": true - } + }, +... } ? 0 ``` 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 c4028c5a..41f8d49c 100644 --- a/packages/tbd/tests/cli-orientation-golden.tryscript.md +++ b/packages/tbd/tests/cli-orientation-golden.tryscript.md @@ -114,6 +114,7 @@ HEALTH CHECKS Run: tbd sync to push issues to remote ✓ Clone status ✓ Sync consistency +✓ Repo cache ⚠ 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 From 119c94b0c656dcb3398eadece786b4c5423068ba Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 19:52:19 +0000 Subject: [PATCH 18/21] fix: Windows path separators in scanDocs, config JSON golden test - repo-cache scanDocs: normalize relativePath to forward slashes on all platforms (Windows join() produces backslashes) - cli-advanced.tryscript: remove trailing comma and ... pattern after settings (tbd init doesn't add docs_cache) https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/src/file/repo-cache.ts | 4 ++-- packages/tbd/tests/cli-advanced.tryscript.md | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/tbd/src/file/repo-cache.ts b/packages/tbd/src/file/repo-cache.ts index a91b992e..6f5c88e5 100644 --- a/packages/tbd/src/file/repo-cache.ts +++ b/packages/tbd/src/file/repo-cache.ts @@ -104,8 +104,8 @@ export class RepoCache { for (const relativeMdPath of entries) { const fullPath = join(dirPath, relativeMdPath); const content = await readFile(fullPath, 'utf-8'); - // relativePath is relative to repoDir - const relativePath = join(pathPattern, relativeMdPath); + // relativePath is relative to repoDir, always uses forward slashes + const relativePath = join(pathPattern, relativeMdPath).replace(/\\/g, '/'); docs.push({ relativePath, content }); } } diff --git a/packages/tbd/tests/cli-advanced.tryscript.md b/packages/tbd/tests/cli-advanced.tryscript.md index 3960db23..303d3b16 100644 --- a/packages/tbd/tests/cli-advanced.tryscript.md +++ b/packages/tbd/tests/cli-advanced.tryscript.md @@ -271,8 +271,7 @@ $ tbd config show --json "auto_sync": false, "doc_auto_sync_hours": 24, "use_gh_cli": true - }, -... + } } ? 0 ``` From 97aa4327396d9f06f842922ae43bd6c72c4e7eeb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 20:01:55 +0000 Subject: [PATCH 19/21] =?UTF-8?q?fix:=20orientation=20golden=20test=20?= =?UTF-8?q?=E2=80=94=20Repo=20cache=20check=20includes=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doctor Repo cache health check outputs "no repo sources configured" message, not just the check name. https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- packages/tbd/tests/cli-orientation-golden.tryscript.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tbd/tests/cli-orientation-golden.tryscript.md b/packages/tbd/tests/cli-orientation-golden.tryscript.md index 41f8d49c..c596f86c 100644 --- a/packages/tbd/tests/cli-orientation-golden.tryscript.md +++ b/packages/tbd/tests/cli-orientation-golden.tryscript.md @@ -114,7 +114,7 @@ HEALTH CHECKS Run: tbd sync to push issues to remote ✓ Clone status ✓ Sync consistency -✓ Repo cache +✓ Repo cache - no repo sources configured ⚠ Issues found that may require manual intervention. ? 0 From eb550e645f0c78cb83a6586546b9b9603dfad14e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 22:27:30 +0000 Subject: [PATCH 20/21] chore: save tbd outbox From b2901ac6142f4b3bfdcc27a069bc7ee40b5f61d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Feb 2026 18:01:45 +0000 Subject: [PATCH 21/21] spec: design local repo doc sources (type: local) with stub pointers Add comprehensive design section for local sources to the external docs repos spec. Local sources allow docs from within the current repo to be managed like external sources, using stub pointer files in .tbd/docs/ with _source/_path frontmatter that DocCache follows for always-fresh content. Updates: schema examples (add 'local' type + 'path' field), config format example, goals, implemented section. Creates Phase 6 epic (tbd-1nrf) with 5 implementation beads and dependency chain. https://claude.ai/code/session_017CMXPneJTPdZrJC5wnd53k --- .../plan-2026-02-02-external-docs-repos.md | 228 +++++++++++++++++- 1 file changed, 223 insertions(+), 5 deletions(-) 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 5d785a51..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 @@ -24,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 @@ -141,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: @@ -812,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 }); @@ -1552,9 +1563,215 @@ These commands: - Default `ref` to `main` but always write it explicitly to config.yml - Default `paths` to all doc type directories (shortcuts, guidelines, templates, references) -- Prevent removal of internal sources (only repo sources can be removed) +- 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): @@ -1811,10 +2028,11 @@ Add to existing schemas: ```typescript 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(), - ref: z.string().optional(), + 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(), });