get-tbd 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +38 -6
  2. package/dist/bin.mjs +4036 -1074
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/cli.mjs +2505 -1585
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{config-BJz1m9eN.mjs → config-B1w3pAkY.mjs} +142 -279
  7. package/dist/config-B1w3pAkY.mjs.map +1 -0
  8. package/dist/config-DXhifxOw.mjs +3 -0
  9. package/dist/doc-cache-CamtXfi4.mjs +1035 -0
  10. package/dist/doc-cache-CamtXfi4.mjs.map +1 -0
  11. package/dist/doc-cache-CiBwpDTf.mjs +3 -0
  12. package/dist/doc-fork-BMjqzfAs.mjs +3 -0
  13. package/dist/doc-fork-CoPi1G1N.mjs +304 -0
  14. package/dist/doc-fork-CoPi1G1N.mjs.map +1 -0
  15. package/dist/docref-C7g0sjvL.mjs +167 -0
  16. package/dist/docref-C7g0sjvL.mjs.map +1 -0
  17. package/dist/docs/README.md +38 -6
  18. package/dist/docs/SKILL.md +21 -5
  19. package/dist/docs/guidelines/backward-compatibility-rules.md +1 -0
  20. package/dist/docs/guidelines/bun-monorepo-patterns.md +1 -0
  21. package/dist/docs/guidelines/cli-agent-skill-patterns.md +228 -52
  22. package/dist/docs/guidelines/commit-conventions.md +1 -0
  23. package/dist/docs/guidelines/common-doc-guidelines.md +1 -0
  24. package/dist/docs/guidelines/convex-limits-best-practices.md +1 -0
  25. package/dist/docs/guidelines/convex-rules.md +1 -0
  26. package/dist/docs/guidelines/electron-app-development-patterns.md +1 -0
  27. package/dist/docs/guidelines/error-handling-rules.md +4 -0
  28. package/dist/docs/guidelines/general-coding-rules.md +3 -1
  29. package/dist/docs/guidelines/general-comment-rules.md +3 -1
  30. package/dist/docs/guidelines/general-eng-agent-principles.md +127 -0
  31. package/dist/docs/guidelines/general-tdd-guidelines.md +7 -0
  32. package/dist/docs/guidelines/general-testing-rules.md +5 -0
  33. package/dist/docs/guidelines/golden-testing-guidelines.md +1 -0
  34. package/dist/docs/guidelines/pnpm-monorepo-patterns.md +1 -0
  35. package/dist/docs/guidelines/python-cli-patterns.md +5 -0
  36. package/dist/docs/guidelines/python-modern-guidelines.md +4 -0
  37. package/dist/docs/guidelines/python-rules.md +7 -0
  38. package/dist/docs/guidelines/release-notes-guidelines.md +1 -0
  39. package/dist/docs/guidelines/supply-chain-hardening.md +3 -2
  40. package/dist/docs/guidelines/tbd-sync-troubleshooting.md +25 -2
  41. package/dist/docs/guidelines/typescript-cli-tool-rules.md +4 -3
  42. package/dist/docs/guidelines/typescript-code-coverage.md +7 -4
  43. package/dist/docs/guidelines/typescript-rules.md +8 -9
  44. package/dist/docs/guidelines/typescript-sorting-patterns.md +2 -1
  45. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +6 -5
  46. package/dist/docs/references/docmap-format.md +64 -0
  47. package/dist/docs/references/docref-format.md +71 -0
  48. package/dist/docs/shortcuts/standard/checkout-third-party-repo.md +17 -4
  49. package/dist/docs/shortcuts/standard/new-shortcut.md +17 -3
  50. package/dist/docs/shortcuts/standard/plan-implementation-with-beads.md +1 -1
  51. package/dist/docs/shortcuts/standard/setup-github-cli.md +4 -1
  52. package/dist/docs/shortcuts/standard/suggest-upstream-improvements.md +69 -0
  53. package/dist/docs/shortcuts/standard/sync-failure-recovery.md +1 -1
  54. package/dist/docs/shortcuts/standard/welcome-user.md +28 -0
  55. package/dist/docs/shortcuts/system/shortcut-explanation.md +16 -1
  56. package/dist/docs/shortcuts/system/skill-baseline.md +21 -5
  57. package/dist/docs/tbd-design.md +144 -3
  58. package/dist/docs/tbd-docs.md +169 -5
  59. package/dist/docs/tbd-prime.md +0 -1
  60. package/dist/docs/templates/qa-playbook.md +3 -1
  61. package/dist/docs/templates/research-brief.md +2 -2
  62. package/dist/fork-manifest-ByU7U2do.mjs +253 -0
  63. package/dist/fork-manifest-ByU7U2do.mjs.map +1 -0
  64. package/dist/fork-manifest-C7lGRq-6.mjs +3 -0
  65. package/dist/{id-mapping-mtoSP9Qt.mjs → id-mapping-D0iitY-F.mjs} +1 -1
  66. package/dist/{id-mapping-687_UEsy.mjs → id-mapping-DAIeLKzm.mjs} +8 -200
  67. package/dist/id-mapping-DAIeLKzm.mjs.map +1 -0
  68. package/dist/index.d.mts +90 -13
  69. package/dist/index.mjs +3 -3
  70. package/dist/lockfile-BR0laSDy.mjs +198 -0
  71. package/dist/lockfile-BR0laSDy.mjs.map +1 -0
  72. package/dist/paths-C1DpnZJW.mjs +405 -0
  73. package/dist/paths-C1DpnZJW.mjs.map +1 -0
  74. package/dist/{schemas-f0EcuAVu.mjs → schemas-lCwRk30L.mjs} +19 -6
  75. package/dist/schemas-lCwRk30L.mjs.map +1 -0
  76. package/dist/{src-BpvcrLnq.mjs → src-CxKOynr1.mjs} +3 -3
  77. package/dist/src-CxKOynr1.mjs.map +1 -0
  78. package/dist/tbd +4036 -1074
  79. package/dist/yaml-utils-BPy991by.mjs.map +1 -1
  80. package/package.json +1 -1
  81. package/dist/config-BJz1m9eN.mjs.map +0 -1
  82. package/dist/config-DlCUMyCG.mjs +0 -3
  83. package/dist/docs/guidelines/general-eng-assistant-rules.md +0 -59
  84. package/dist/id-mapping-687_UEsy.mjs.map +0 -1
  85. package/dist/schemas-f0EcuAVu.mjs.map +0 -1
  86. package/dist/src-BpvcrLnq.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths-C1DpnZJW.mjs","names":[],"sources":["../src/lib/git-env.ts","../src/lib/paths.ts"],"sourcesContent":["/**\n * Ambient-git-environment isolation for every git subprocess tbd spawns.\n *\n * tbd is a repo-local tool: it discovers its `.tbd` root by walking up from\n * cwd, so its git context must come from cwd too. But git exports GIT_DIR\n * (and related vars) into hook environments (always, when a hook runs) and\n * an inherited *absolute* GIT_DIR overrides cwd-based discovery in every git\n * child process, `-C <dir>` included. A user running tbd inside any git hook\n * (post-merge, pre-push, …) would otherwise have tbd resolve the hook's\n * repository instead of cwd's and read/write the WRONG repo's tbd data; this\n * is the product-level half of the tbd-a1lc incident (tracked as tbd-tgwi),\n * where exactly that path rewrote a real repo's data-sync state.\n *\n * Policy: tbd always operates on the repository containing cwd. Git location\n * variables are stripped from every git subprocess, and a one-line warning is\n * printed (once per process) when an ambient GIT_DIR was present, so anyone\n * setting it intentionally learns it does not redirect tbd.\n *\n * The same variable list is used by tests/scrub-git-env.ts (vitest worker\n * hygiene) and mirrored in scripts/scrub-git-env.mjs (lefthook wrapper).\n */\n\n/** Environment variables through which git overrides cwd-based discovery. */\nexport const GIT_LOCATION_VARS = [\n 'GIT_DIR',\n 'GIT_WORK_TREE',\n 'GIT_INDEX_FILE',\n 'GIT_COMMON_DIR',\n 'GIT_OBJECT_DIRECTORY',\n 'GIT_ALTERNATE_OBJECT_DIRECTORIES',\n 'GIT_PREFIX',\n 'GIT_NAMESPACE',\n] as const;\n\nlet warnedAmbientGitDir = false;\n\n/**\n * A copy of process.env with git location variables removed (plus optional\n * overrides), for use as the `env` of any spawned git process. Emits the\n * one-time ambient-GIT_DIR warning as a side effect of first use.\n */\nexport function gitSafeEnv(extra?: Record<string, string>): NodeJS.ProcessEnv {\n warnIfAmbientGitEnv();\n const env: NodeJS.ProcessEnv = { ...process.env, ...extra };\n for (const name of GIT_LOCATION_VARS) {\n delete env[name];\n }\n return env;\n}\n\n/**\n * Warn once per process when an ambient GIT_DIR/GIT_WORK_TREE is being\n * ignored. Stderr keeps piped stdout clean; once keeps hooks readable.\n */\nexport function warnIfAmbientGitEnv(): void {\n if (warnedAmbientGitDir) return;\n const ambient = process.env.GIT_DIR ?? process.env.GIT_WORK_TREE;\n if (ambient === undefined) return;\n warnedAmbientGitDir = true;\n process.stderr.write(\n `(ignoring inherited GIT_DIR: tbd operates on the repository containing the current directory)\\n`,\n );\n}\n","/**\n * Centralized path constants for tbd.\n *\n * Directory structure (per spec):\n *\n * On main/dev branches:\n * .tbd/\n * Committed to the repo:\n * config.yml - Project configuration\n * .gitignore - Controls what's gitignored below\n * workspaces/ - Persistent state (outbox, named workspaces)\n * Gitignored (local only):\n * state.yml - Local state\n * docs/ - Installed documentation (regenerated on setup)\n *\n * In the Git common dir shared by all linked worktrees:\n * $GIT_COMMON_DIR/tbd/\n * layout.yml - Shared layout metadata\n * locks/data-sync.lock/ - Repo-scoped lock directory\n * backups/ - Local repair/migration backups\n * data-sync-worktree/ - Hidden worktree checkout of tbd-sync branch\n * .tbd/data-sync/ - issues/, mappings/, attic/, meta.yml\n *\n * On tbd-sync branch:\n * .tbd/\n * data-sync/\n * issues/\n * mappings/\n * attic/\n * meta.yml\n */\n\nimport { execFile } from 'node:child_process';\n\nimport { gitSafeEnv } from './git-env.js';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport { promisify } from 'node:util';\n\n/** The tbd configuration directory on main branch */\nexport const TBD_DIR = '.tbd';\n\n/** The config file path */\nexport const CONFIG_FILE = join(TBD_DIR, 'config.yml');\n\n/** The local state file (gitignored) */\nexport const STATE_FILE = join(TBD_DIR, 'state.yml');\n\n/** The worktree directory name */\nexport const WORKTREE_DIR_NAME = 'data-sync-worktree';\n\n/** Legacy per-checkout worktree path used by f03 and earlier clients. */\nexport const LEGACY_WORKTREE_DIR = join(TBD_DIR, WORKTREE_DIR_NAME);\n\n/**\n * @internal Primary-checkout relative path to the shared sync worktree.\n *\n * Only valid when `.git` is a directory (i.e., the primary checkout). Production\n * code must call resolveSharedTbdPaths() instead: linked worktrees have a `.git`\n * file, so this constant resolves to the wrong location for them. Intended for\n * tests and the non-git fallback in resolveDataSyncDir().\n */\nexport const PRIMARY_CHECKOUT_WORKTREE_DIR = join('.git', 'tbd', WORKTREE_DIR_NAME);\n\n/** The data directory name on the sync branch */\nexport const DATA_SYNC_DIR_NAME = 'data-sync';\n\n/**\n * The data directory path as it appears on the tbd-sync branch.\n * In a normal checkout this same relative path is a legacy/wrong-location fallback;\n * production callers should resolve the absolute shared worktree path with\n * resolveDataSyncDir().\n */\nexport const DATA_SYNC_DIR = join(TBD_DIR, DATA_SYNC_DIR_NAME);\n\n/**\n * @internal Primary-checkout relative path to the synced data via the shared worktree.\n *\n * Same caveat as `PRIMARY_CHECKOUT_WORKTREE_DIR`: only valid for a primary checkout.\n * Production code should resolve the absolute path with resolveDataSyncDir(); this\n * constant is intended for tests and the non-git fallback path.\n */\nexport const PRIMARY_CHECKOUT_DATA_SYNC_DIR = join(\n PRIMARY_CHECKOUT_WORKTREE_DIR,\n TBD_DIR,\n DATA_SYNC_DIR_NAME,\n);\n\n/** Issues directory */\nexport const ISSUES_DIR = join(DATA_SYNC_DIR, 'issues');\n\n/** Mappings directory */\nexport const MAPPINGS_DIR = join(DATA_SYNC_DIR, 'mappings');\n\n/** Attic directory for conflict resolution */\nexport const ATTIC_DIR = join(DATA_SYNC_DIR, 'attic');\n\n/** Meta file for schema version */\nexport const META_FILE = join(DATA_SYNC_DIR, 'meta.yml');\n\n/** The sync branch name */\nexport const SYNC_BRANCH = 'tbd-sync';\n\n// =============================================================================\n// Git Common-Dir Shared Sync Paths\n// =============================================================================\n\nconst execFileAsync = promisify(execFile);\n\n/** Directory name under $GIT_COMMON_DIR for tbd local machinery. */\nexport const GIT_COMMON_TBD_DIR_NAME = 'tbd';\n\n/** Common-dir layout metadata file name. */\nexport const COMMON_DIR_LAYOUT_FILE_NAME = 'layout.yml';\n\n/** Shared lock directory name under $GIT_COMMON_DIR/tbd/. */\nexport const SHARED_LOCKS_DIR_NAME = 'locks';\n\n/** Shared backups directory name under $GIT_COMMON_DIR/tbd/. */\nexport const SHARED_BACKUPS_DIR_NAME = 'backups';\n\n/** Directory-lock name for shared data-sync operations. */\nexport const DATA_SYNC_LOCK_DIR_NAME = 'data-sync.lock';\n\n/**\n * Resolved Git common-dir paths for the repo-scoped sync layout.\n */\nexport interface SharedTbdPaths {\n /** Absolute Git common directory shared by all linked worktrees. */\n gitCommonDir: string;\n /** Absolute $GIT_COMMON_DIR/tbd path. */\n sharedTbdDir: string;\n /** Absolute shared hidden worktree path. */\n sharedWorktreePath: string;\n /** Absolute data-sync directory inside the shared worktree. */\n sharedDataSyncDir: string;\n /** Absolute common-dir layout metadata path. */\n sharedLayoutPath: string;\n /** Absolute shared lock directory parent. */\n sharedLocksDir: string;\n /** Absolute data-sync lock path. */\n sharedLockPath: string;\n /** Absolute shared backups directory. */\n sharedBackupsDir: string;\n}\n\n/**\n * Resolve Git's common directory from any checkout or linked worktree.\n */\nexport async function resolveGitCommonDir(cwd: string): Promise<string> {\n let output: string;\n try {\n const { stdout } = await execFileAsync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], {\n env: gitSafeEnv(),\n maxBuffer: 1024 * 1024,\n });\n output = stdout.trim();\n } catch {\n const { stdout } = await execFileAsync(\n 'git',\n ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'],\n { env: gitSafeEnv(), maxBuffer: 1024 * 1024 },\n );\n output = stdout.trim();\n }\n\n if (!output) {\n throw new Error(`Unable to resolve Git common directory from ${cwd}`);\n }\n\n const gitCommonDir = isAbsolute(output) ? output : resolve(cwd, output);\n return realpath(gitCommonDir).catch(() => gitCommonDir);\n}\n\n/**\n * Build all shared tbd paths from an absolute Git common directory.\n */\nexport function buildSharedTbdPaths(gitCommonDir: string): SharedTbdPaths {\n const sharedTbdDir = join(gitCommonDir, GIT_COMMON_TBD_DIR_NAME);\n const sharedWorktreePath = join(sharedTbdDir, WORKTREE_DIR_NAME);\n const sharedDataSyncDir = join(sharedWorktreePath, TBD_DIR, DATA_SYNC_DIR_NAME);\n const sharedLayoutPath = join(sharedTbdDir, COMMON_DIR_LAYOUT_FILE_NAME);\n const sharedLocksDir = join(sharedTbdDir, SHARED_LOCKS_DIR_NAME);\n const sharedLockPath = join(sharedLocksDir, DATA_SYNC_LOCK_DIR_NAME);\n const sharedBackupsDir = join(sharedTbdDir, SHARED_BACKUPS_DIR_NAME);\n\n return {\n gitCommonDir,\n sharedTbdDir,\n sharedWorktreePath,\n sharedDataSyncDir,\n sharedLayoutPath,\n sharedLocksDir,\n sharedLockPath,\n sharedBackupsDir,\n };\n}\n\n/**\n * Resolve the shared tbd paths for the repository containing baseDir.\n */\nexport async function resolveSharedTbdPaths(baseDir: string): Promise<SharedTbdPaths> {\n return buildSharedTbdPaths(await resolveGitCommonDir(baseDir));\n}\n\n/**\n * True when the Git common dir lives outside the project checkout, the linked\n * worktree shape where the checkout can be writable while `$GIT_COMMON_DIR/tbd`\n * (shared sync state + lock) is not. This is the generic signal behind the\n * Codex-sandbox case in #164; it does not depend on any `CODEX_*` env var, which\n * that sandbox did not expose. Used by both the `doctor` lock-writability finding\n * and the write-side `SharedLockUnwritableError` so their wording matches.\n */\nexport function isCommonDirOutsideProject(gitCommonDir: string, projectRoot: string): boolean {\n const rel = relative(resolve(projectRoot), resolve(gitCommonDir));\n return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);\n}\n\n// =============================================================================\n// Workspace Paths (for sync failure recovery, backups, bulk editing)\n// =============================================================================\n\n/** The workspaces directory name within .tbd/ */\nexport const WORKSPACES_DIR_NAME = 'workspaces';\n\n/** Full path to workspaces directory: .tbd/workspaces/ */\nexport const WORKSPACES_DIR = join(TBD_DIR, WORKSPACES_DIR_NAME);\n\n/**\n * Get the path to a named workspace directory.\n *\n * Workspaces are stored at: .tbd/workspaces/{name}/\n *\n * @param workspaceName - The name of the workspace (e.g., 'outbox', 'my-feature')\n * @returns Path to the workspace directory\n */\nexport function getWorkspaceDir(workspaceName: string): string {\n return join(WORKSPACES_DIR, workspaceName);\n}\n\n/**\n * Get the path to a workspace's issues directory.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's issues directory\n */\nexport function getWorkspaceIssuesDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'issues');\n}\n\n/**\n * Get the path to a workspace's mappings directory.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's mappings directory\n */\nexport function getWorkspaceMappingsDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'mappings');\n}\n\n/**\n * Get the path to a workspace's attic directory.\n *\n * The attic stores conflict backups during workspace save operations.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's attic directory\n */\nexport function getWorkspaceAtticDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'attic');\n}\n\n/**\n * Validate a workspace name.\n *\n * Valid workspace names:\n * - Lowercase alphanumeric characters\n * - Hyphens and underscores allowed\n * - Must not be empty\n * - Must not contain path separators or dots at start\n *\n * @param name - The workspace name to validate\n * @returns true if the name is valid\n */\nexport function isValidWorkspaceName(name: string): boolean {\n if (!name || name.length === 0) {\n return false;\n }\n\n // Must not start with dot (hidden files)\n if (name.startsWith('.')) {\n return false;\n }\n\n // Only allow lowercase alphanumeric, hyphens, and underscores\n // No spaces, path separators, or special characters\n const validPattern = /^[a-z0-9][a-z0-9_-]*$/;\n return validPattern.test(name);\n}\n\n// =============================================================================\n// Documentation/Shortcuts Paths\n// =============================================================================\n\n/** Docs directory name within .tbd/ */\nexport const DOCS_DIR = 'docs';\n\n/** Shortcuts directory name within docs/ */\nexport const SHORTCUTS_DIR = 'shortcuts';\n\n/** System shortcuts directory name (core docs like skill-baseline.md) */\nexport const SYSTEM_DIR = 'system';\n\n/** Standard shortcuts directory name (workflow shortcuts) */\nexport const STANDARD_DIR = 'standard';\n\n/** Guidelines directory name (coding rules and best practices) */\nexport const GUIDELINES_DIR = 'guidelines';\n\n/** Templates directory name (document templates) */\nexport const TEMPLATES_DIR = 'templates';\n\n/** Full path to docs directory: .tbd/docs/ */\nexport const TBD_DOCS_DIR = join(TBD_DIR, DOCS_DIR);\n\n/** Full path to shortcuts directory: .tbd/docs/shortcuts/ */\nexport const TBD_SHORTCUTS_DIR = join(TBD_DOCS_DIR, SHORTCUTS_DIR);\n\n/** Full path to system shortcuts: .tbd/docs/shortcuts/system/ */\nexport const TBD_SHORTCUTS_SYSTEM = join(TBD_SHORTCUTS_DIR, SYSTEM_DIR);\n\n/** Full path to standard shortcuts: .tbd/docs/shortcuts/standard/ */\nexport const TBD_SHORTCUTS_STANDARD = join(TBD_SHORTCUTS_DIR, STANDARD_DIR);\n\n/** Full path to guidelines: .tbd/docs/guidelines/ (top-level, not under shortcuts) */\nexport const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, GUIDELINES_DIR);\n\n/** Full path to templates: .tbd/docs/templates/ (top-level, not under shortcuts) */\nexport const TBD_TEMPLATES_DIR = join(TBD_DOCS_DIR, TEMPLATES_DIR);\n\n/** Built-in docs source paths (relative to package docs/) */\nexport const BUILTIN_SHORTCUTS_SYSTEM = join(SHORTCUTS_DIR, SYSTEM_DIR);\nexport const BUILTIN_SHORTCUTS_STANDARD = join(SHORTCUTS_DIR, STANDARD_DIR);\n\n/** Built-in guidelines source path (relative to package docs/) */\nexport const BUILTIN_GUIDELINES_DIR = GUIDELINES_DIR;\n\n/** Built-in templates source path (relative to package docs/) */\nexport const BUILTIN_TEMPLATES_DIR = TEMPLATES_DIR;\n\n/** Install directory name (header files for tool-specific installation) */\nexport const INSTALL_DIR = 'install';\n\n/** Built-in install source path (relative to package docs/) */\nexport const BUILTIN_INSTALL_DIR = INSTALL_DIR;\n\n// =============================================================================\n// Forkable docs: the visible, git-tracked fork directory\n// =============================================================================\n\n/** References directory name (tbd self-docs and format references). */\nexport const REFERENCES_DIR = 'references';\n\n/** Cached reference docs directory (docref/docmap formats, tbd-docs, tbd-design). */\nexport const TBD_REFERENCES_DIR = join(TBD_DOCS_DIR, REFERENCES_DIR);\n\n/**\n * Default fork directory (repo-relative), where forked docs are made visible.\n * A POSIX literal, not join()'d: this value is committed (manifest paths) and\n * printed, so it must be identical on every platform; fs access joins it with\n * the root via join(), which accepts forward slashes on Windows.\n */\nexport const FORK_DIR = 'docs/tbd';\n\n/** Fork-dir kind subdirectories (repo-relative). */\nexport const FORK_SHORTCUTS_DIR = `${FORK_DIR}/${SHORTCUTS_DIR}`; // docs/tbd/shortcuts/\nexport const FORK_GUIDELINES_DIR = `${FORK_DIR}/${GUIDELINES_DIR}`; // docs/tbd/guidelines/\nexport const FORK_TEMPLATES_DIR = `${FORK_DIR}/${TEMPLATES_DIR}`; // docs/tbd/templates/\nexport const FORK_REFERENCES_DIR = `${FORK_DIR}/${REFERENCES_DIR}`; // docs/tbd/references/\n\n/**\n * Cache-only lookup paths (the gitignored `.tbd/docs/` cache), used when forking\n * needs the pristine upstream content rather than a possibly-forked copy.\n */\nexport const CACHE_SHORTCUT_PATHS = [TBD_SHORTCUTS_SYSTEM, TBD_SHORTCUTS_STANDARD];\nexport const CACHE_GUIDELINES_PATHS = [TBD_GUIDELINES_DIR];\nexport const CACHE_TEMPLATE_PATHS = [TBD_TEMPLATES_DIR];\nexport const CACHE_REFERENCE_PATHS = [TBD_REFERENCES_DIR];\n\n/**\n * Default shortcut lookup paths (searched in order, relative to tbd root).\n * Earlier paths take precedence: the fork dir shadows the cache, so a forked doc\n * is served wherever the upstream one was. Missing dirs are skipped, so repos with\n * no forks behave exactly as before.\n */\nexport const DEFAULT_SHORTCUT_PATHS = [\n FORK_SHORTCUTS_DIR, // docs/tbd/shortcuts/ (forked, highest precedence)\n ...CACHE_SHORTCUT_PATHS,\n];\n\n/**\n * Default guidelines lookup paths (relative to tbd root).\n */\nexport const DEFAULT_GUIDELINES_PATHS = [\n FORK_GUIDELINES_DIR, // docs/tbd/guidelines/ (forked, highest precedence)\n ...CACHE_GUIDELINES_PATHS,\n];\n\n/**\n * Default template lookup paths (relative to tbd root).\n */\nexport const DEFAULT_TEMPLATE_PATHS = [\n FORK_TEMPLATES_DIR, // docs/tbd/templates/ (forked, highest precedence)\n ...CACHE_TEMPLATE_PATHS,\n];\n\n/**\n * Default reference-doc lookup paths (relative to tbd root).\n */\nexport const DEFAULT_REFERENCE_PATHS = [\n FORK_REFERENCES_DIR, // docs/tbd/references/ (forked, highest precedence)\n ...CACHE_REFERENCE_PATHS,\n];\n\n/**\n * Get the full path to an issue file.\n */\nexport function getIssuePath(issueId: string): string {\n return join(ISSUES_DIR, `${issueId}.md`);\n}\n\n/**\n * Get the full path to a mapping file.\n */\nexport function getMappingPath(name: string): string {\n return join(MAPPINGS_DIR, `${name}.yml`);\n}\n\n/**\n * Get the full path to an attic entry.\n */\nexport function getAtticPath(issueId: string, filename: string): string {\n return join(ATTIC_DIR, 'conflicts', issueId, filename);\n}\n\n// =============================================================================\n// Dynamic Path Resolution\n// =============================================================================\n\nimport { access, realpath } from 'node:fs/promises';\n\n/**\n * Options for resolveDataSyncDir.\n */\nexport interface ResolveDataSyncDirOptions {\n /**\n * Allow fallback to direct path when worktree is missing.\n * Set to true for test environments or diagnostic tools.\n * Default: true. When false and worktree is missing, throws WorktreeMissingError.\n */\n allowFallback?: boolean;\n}\n\n/**\n * Error thrown when worktree is missing and fallback is not allowed.\n * Defined inline to avoid circular dependency with errors.ts.\n */\nexport class WorktreeMissingError extends Error {\n constructor(\n message = \"Shared worktree not found under $GIT_COMMON_DIR/tbd/data-sync-worktree/. Run 'tbd doctor --fix' to repair.\",\n ) {\n super(message);\n this.name = 'WorktreeMissingError';\n }\n}\n\n/**\n * Cache for resolved data sync directory.\n * Reset when baseDir changes.\n */\nlet _resolvedDataSyncDir: string | null = null;\nlet _resolvedBaseDir: string | null = null;\nlet _resolvedAllowFallback: boolean | null = null;\n\n/**\n * Resolve the actual data sync directory path.\n *\n * This function detects whether we're running with a git worktree\n * (production) or in a test environment without worktree.\n *\n * Order of preference:\n * 1. Shared worktree path if it exists:\n * $GIT_COMMON_DIR/tbd/data-sync-worktree/.tbd/data-sync/\n * 2. Direct path as fallback (only if allowFallback: true, for tests/diagnostics)\n *\n * @param baseDir - The tbd root directory (from requireInit or findTbdRoot)\n * @param options - Options for path resolution\n * @returns Resolved data sync directory path\n * @throws WorktreeMissingError if worktree missing and allowFallback is false\n *\n * See: plan-2026-01-28-sync-worktree-recovery-and-hardening.md\n */\nexport async function resolveDataSyncDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const allowFallback = options?.allowFallback ?? true;\n\n // Return cached result if baseDir and options haven't changed\n if (\n _resolvedDataSyncDir &&\n _resolvedBaseDir === baseDir &&\n _resolvedAllowFallback === allowFallback\n ) {\n return _resolvedDataSyncDir;\n }\n\n let worktreePath: string | null = null;\n try {\n worktreePath = (await resolveSharedTbdPaths(baseDir)).sharedDataSyncDir;\n } catch {\n // Not in a git repository or git is unavailable. Check the static primary-checkout\n // path for unit tests before falling back to the direct diagnostic path.\n worktreePath = join(baseDir, PRIMARY_CHECKOUT_DATA_SYNC_DIR);\n }\n const directPath = join(baseDir, DATA_SYNC_DIR);\n\n // Check if worktree path exists\n if (worktreePath) {\n try {\n await access(worktreePath);\n _resolvedDataSyncDir = worktreePath;\n _resolvedBaseDir = baseDir;\n _resolvedAllowFallback = allowFallback;\n return worktreePath;\n } catch {\n // Worktree doesn't exist\n }\n }\n\n {\n // Worktree doesn't exist\n if (!allowFallback) {\n throw new WorktreeMissingError();\n }\n\n // Fallback to direct path (test mode or diagnostic tools)\n // Note: In production, sync.ts checks worktree health before calling this\n // Debug warning to help detect unintended fallback usage\n if (process.env.DEBUG || process.env.TBD_DEBUG) {\n console.warn(\n '[tbd:paths] resolveDataSyncDir: worktree not found, falling back to direct path',\n );\n }\n // Intentionally do NOT cache the fallback result: a later call after the\n // worktree is created must rediscover the real path, not keep returning\n // the stale fallback.\n return directPath;\n }\n}\n\n/**\n * Resolve issues directory path.\n */\nexport async function resolveIssuesDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'issues');\n}\n\n/**\n * Resolve mappings directory path.\n */\nexport async function resolveMappingsDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'mappings');\n}\n\n/**\n * Resolve attic directory path.\n */\nexport async function resolveAtticDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'attic');\n}\n\n/**\n * Clear the resolved path cache.\n * Call this when the repository state changes (e.g., after init).\n */\nexport function clearPathCache(): void {\n _resolvedDataSyncDir = null;\n _resolvedBaseDir = null;\n _resolvedAllowFallback = null;\n}\n\n// =============================================================================\n// Doc Path Resolution\n// =============================================================================\n\n/**\n * Resolve a doc path for consistent handling across the codebase.\n *\n * Path resolution rules:\n * - Absolute paths (starting with /): used as-is\n * - Home directory paths (starting with ~/): expanded to user home directory\n * - Relative paths: resolved from tbd root (baseDir)\n *\n * @param docPath - The path to resolve\n * @param baseDir - The tbd root directory (parent of .tbd/)\n * @returns Resolved absolute path\n *\n * @example\n * // Absolute path - returned as-is\n * resolveDocPath('/usr/local/docs/file.md') // => '/usr/local/docs/file.md'\n *\n * // Home path - expanded\n * resolveDocPath('~/docs/file.md') // => '/Users/username/docs/file.md'\n *\n * // Relative path - resolved from baseDir\n * resolveDocPath('docs/file.md', '/project') // => '/project/docs/file.md'\n */\nexport function resolveDocPath(docPath: string, baseDir: string): string {\n // Handle home directory expansion\n if (docPath.startsWith('~/')) {\n return join(homedir(), docPath.slice(2));\n }\n\n // Absolute paths used as-is\n if (isAbsolute(docPath)) {\n return docPath;\n }\n\n // Relative paths resolved from baseDir (tbd root)\n return join(baseDir, docPath);\n}\n\n// =============================================================================\n// Token Estimation Settings\n// =============================================================================\n\n/**\n * Characters per token ratio for estimating token counts.\n *\n * Based on research of OpenAI (tiktoken) and Claude tokenizers:\n * - Pure English prose: ~4-5 chars/token\n * - Code and symbols: ~3 chars/token\n * - Mixed markdown/code docs: ~3.5 chars/token\n *\n * We use 3.5 as our docs are markdown with code examples.\n * This provides ~15-20% accuracy, sufficient for cost estimation.\n */\nexport const CHARS_PER_TOKEN = 3.5;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,IAAI,sBAAsB;;;;;;AAO1B,SAAgB,WAAW,OAAmD;AAC5E,sBAAqB;CACrB,MAAM,MAAyB;EAAE,GAAG,QAAQ;EAAK,GAAG;EAAO;AAC3D,MAAK,MAAM,QAAQ,kBACjB,QAAO,IAAI;AAEb,QAAO;;;;;;AAOT,SAAgB,sBAA4B;AAC1C,KAAI,oBAAqB;AAEzB,MADgB,QAAQ,IAAI,WAAW,QAAQ,IAAI,mBACnC,OAAW;AAC3B,uBAAsB;AACtB,SAAQ,OAAO,MACb,kGACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACrBH,MAAa,UAAU;;AAGvB,MAAa,cAAc,KAAK,SAAS,aAAa;;AAGtD,MAAa,aAAa,KAAK,SAAS,YAAY;;AAGpD,MAAa,oBAAoB;;AAGjC,MAAa,sBAAsB,KAAK,SAAS,kBAAkB;;;;;;;;;AAUnE,MAAa,gCAAgC,KAAK,QAAQ,OAAO,kBAAkB;;AAGnF,MAAa,qBAAqB;;;;;;;AAQlC,MAAa,gBAAgB,KAAK,SAAS,mBAAmB;;;;;;;;AAS9D,MAAa,iCAAiC,KAC5C,+BACA,SACA,mBACD;;AAGD,MAAa,aAAa,KAAK,eAAe,SAAS;;AAGvD,MAAa,eAAe,KAAK,eAAe,WAAW;;AAG3D,MAAa,YAAY,KAAK,eAAe,QAAQ;;AAGrD,MAAa,YAAY,KAAK,eAAe,WAAW;;AAGxD,MAAa,cAAc;AAM3B,MAAM,gBAAgB,UAAU,SAAS;;AAGzC,MAAa,0BAA0B;;AAGvC,MAAa,8BAA8B;;AAG3C,MAAa,wBAAwB;;AAGrC,MAAa,0BAA0B;;AAGvC,MAAa,0BAA0B;;;;AA2BvC,eAAsB,oBAAoB,KAA8B;CACtE,IAAI;AACJ,KAAI;EACF,MAAM,EAAE,WAAW,MAAM,cAAc,OAAO;GAAC;GAAM;GAAK;GAAa;GAAmB,EAAE;GAC1F,KAAK,YAAY;GACjB,WAAW,OAAO;GACnB,CAAC;AACF,WAAS,OAAO,MAAM;SAChB;EACN,MAAM,EAAE,WAAW,MAAM,cACvB,OACA;GAAC;GAAM;GAAK;GAAa;GAA0B;GAAmB,EACtE;GAAE,KAAK,YAAY;GAAE,WAAW,OAAO;GAAM,CAC9C;AACD,WAAS,OAAO,MAAM;;AAGxB,KAAI,CAAC,OACH,OAAM,IAAI,MAAM,+CAA+C,MAAM;CAGvE,MAAM,eAAe,WAAW,OAAO,GAAG,SAAS,QAAQ,KAAK,OAAO;AACvE,QAAO,SAAS,aAAa,CAAC,YAAY,aAAa;;;;;AAMzD,SAAgB,oBAAoB,cAAsC;CACxE,MAAM,eAAe,KAAK,cAAc,wBAAwB;CAChE,MAAM,qBAAqB,KAAK,cAAc,kBAAkB;CAChE,MAAM,oBAAoB,KAAK,oBAAoB,SAAS,mBAAmB;CAC/E,MAAM,mBAAmB,KAAK,cAAc,4BAA4B;CACxE,MAAM,iBAAiB,KAAK,cAAc,sBAAsB;AAIhE,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,gBAVqB,KAAK,gBAAgB,wBAAwB;EAWlE,kBAVuB,KAAK,cAAc,wBAAwB;EAWnE;;;;;AAMH,eAAsB,sBAAsB,SAA0C;AACpF,QAAO,oBAAoB,MAAM,oBAAoB,QAAQ,CAAC;;;;;;;;;;AAWhE,SAAgB,0BAA0B,cAAsB,aAA8B;CAC5F,MAAM,MAAM,SAAS,QAAQ,YAAY,EAAE,QAAQ,aAAa,CAAC;AACjE,QAAO,QAAQ,QAAQ,IAAI,WAAW,KAAK,MAAM,IAAI,WAAW,IAAI;;;AAQtE,MAAa,sBAAsB;;AAGnC,MAAa,iBAAiB,KAAK,SAAS,oBAAoB;;;;;;;;;AAUhE,SAAgB,gBAAgB,eAA+B;AAC7D,QAAO,KAAK,gBAAgB,cAAc;;;;;;;;;;;;;;AA+C5C,SAAgB,qBAAqB,MAAuB;AAC1D,KAAI,CAAC,QAAQ,KAAK,WAAW,EAC3B,QAAO;AAIT,KAAI,KAAK,WAAW,IAAI,CACtB,QAAO;AAMT,QADqB,wBACD,KAAK,KAAK;;;AAQhC,MAAa,WAAW;;AAGxB,MAAa,gBAAgB;;AAG7B,MAAa,aAAa;;AAG1B,MAAa,eAAe;;AAG5B,MAAa,iBAAiB;;AAG9B,MAAa,gBAAgB;;AAG7B,MAAa,eAAe,KAAK,SAAS,SAAS;;AAGnD,MAAa,oBAAoB,KAAK,cAAc,cAAc;;AAGlE,MAAa,uBAAuB,KAAK,mBAAmB,WAAW;;AAGvE,MAAa,yBAAyB,KAAK,mBAAmB,aAAa;;AAG3E,MAAa,qBAAqB,KAAK,cAAc,eAAe;;AAGpE,MAAa,oBAAoB,KAAK,cAAc,cAAc;;AAGlE,MAAa,2BAA2B,KAAK,eAAe,WAAW;AACvE,MAAa,6BAA6B,KAAK,eAAe,aAAa;;AAmB3E,MAAa,iBAAiB;;AAG9B,MAAa,qBAAqB,KAAK,cAAc,eAAe;;;;;;;AAQpE,MAAa,WAAW;;AAGxB,MAAa,qBAAqB,GAAG,SAAS,GAAG;AACjD,MAAa,sBAAsB,GAAG,SAAS,GAAG;AAClD,MAAa,qBAAqB,GAAG,SAAS,GAAG;AACjD,MAAa,sBAAsB,GAAG,SAAS,GAAG;;;;;AAMlD,MAAa,uBAAuB,CAAC,sBAAsB,uBAAuB;AAClF,MAAa,yBAAyB,CAAC,mBAAmB;AAC1D,MAAa,uBAAuB,CAAC,kBAAkB;AACvD,MAAa,wBAAwB,CAAC,mBAAmB;;;;;;;AAQzD,MAAa,yBAAyB,CACpC,oBACA,GAAG,qBACJ;;;;AAKD,MAAa,2BAA2B,CACtC,qBACA,GAAG,uBACJ;;;;AAKD,MAAa,yBAAyB,CACpC,oBACA,GAAG,qBACJ;;;;AAKD,MAAa,0BAA0B,CACrC,qBACA,GAAG,sBACJ;;;;;AA6CD,IAAa,uBAAb,cAA0C,MAAM;CAC9C,YACE,UAAU,8GACV;AACA,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;;AAQhB,IAAI,uBAAsC;AAC1C,IAAI,mBAAkC;AACtC,IAAI,yBAAyC;;;;;;;;;;;;;;;;;;;AAoB7C,eAAsB,mBACpB,SACA,SACiB;CACjB,MAAM,gBAAgB,SAAS,iBAAiB;AAGhD,KACE,wBACA,qBAAqB,WACrB,2BAA2B,cAE3B,QAAO;CAGT,IAAI,eAA8B;AAClC,KAAI;AACF,kBAAgB,MAAM,sBAAsB,QAAQ,EAAE;SAChD;AAGN,iBAAe,KAAK,SAAS,+BAA+B;;CAE9D,MAAM,aAAa,KAAK,SAAS,cAAc;AAG/C,KAAI,aACF,KAAI;AACF,QAAM,OAAO,aAAa;AAC1B,yBAAuB;AACvB,qBAAmB;AACnB,2BAAyB;AACzB,SAAO;SACD;AAOR,KAAI,CAAC,cACH,OAAM,IAAI,sBAAsB;AAMlC,KAAI,QAAQ,IAAI,SAAS,QAAQ,IAAI,UACnC,SAAQ,KACN,kFACD;AAKH,QAAO;;;;;AA6BX,eAAsB,gBACpB,SACA,SACiB;AAEjB,QAAO,KADa,MAAM,mBAAmB,SAAS,QAAQ,EACrC,QAAQ;;;;;;;;;;;;;AAqEnC,MAAa,kBAAkB"}
@@ -163,15 +163,17 @@ const SyncStorage = z.enum(["git-common-dir-v1"]);
163
163
  * Doc cache configuration - maps destination paths to source locations.
164
164
  *
165
165
  * Keys are destination paths relative to .tbd/docs/ (e.g., "shortcuts/standard/code-review-and-commit.md")
166
- * Values are source locations:
167
- * - internal: prefix for bundled docs (e.g., "internal:shortcuts/standard/code-review-and-commit.md")
168
- * - Full URL for external docs (e.g., "https://raw.githubusercontent.com/org/repo/main/file.md")
166
+ * Values are source docrefs:
167
+ * - internal: bundled docs (e.g., "internal:guidelines/typescript-rules.md")
168
+ * - git: a file in a repo (e.g., "github:owner/repo@main//docs/file.md"; gitlab: too)
169
+ * - url: a plain URL kept verbatim (e.g., "https://example.com/file.md")
170
+ * See `tbd docs show docref-format` for the full grammar.
169
171
  *
170
172
  * Example:
171
173
  * ```yaml
172
174
  * doc_cache:
173
175
  * shortcuts/standard/code-review-and-commit.md: internal:shortcuts/standard/code-review-and-commit.md
174
- * shortcuts/custom/my-shortcut.md: https://raw.githubusercontent.com/org/repo/main/shortcuts/my-shortcut.md
176
+ * shortcuts/custom/my-shortcut.md: github:org/repo@main//shortcuts/my-shortcut.md
175
177
  * ```
176
178
  */
177
179
  const DocCacheConfigSchema = z.record(z.string(), z.string());
@@ -183,6 +185,7 @@ const DocCacheConfigSchema = z.record(z.string(), z.string());
183
185
  */
184
186
  const DocsCacheSchema = z.object({
185
187
  files: z.record(z.string(), z.string()).optional(),
188
+ local_dirs: z.array(z.string()).optional(),
186
189
  lookup_path: z.array(z.string()).default([".tbd/docs/shortcuts/system", ".tbd/docs/shortcuts/standard"])
187
190
  });
188
191
  /**
@@ -205,9 +208,18 @@ const DocsCacheSchema = z.object({
205
208
  *
206
209
  * See tbd-format.ts for format version history and migration rules.
207
210
  */
211
+ /**
212
+ * One entry in the config upgrade history (`tbd_upgrades`).
213
+ * Records a tbd version that ran `tbd setup` in this repo.
214
+ */
215
+ const UpgradeEntrySchema = z.object({
216
+ version: z.string(),
217
+ at: Timestamp.optional()
218
+ });
208
219
  const ConfigSchema = z.object({
209
220
  tbd_format: z.string().default("f01"),
210
221
  tbd_version: z.string(),
222
+ tbd_upgrades: z.array(UpgradeEntrySchema).default([]),
211
223
  sync: z.object({
212
224
  branch: GitBranchName.default("tbd-sync"),
213
225
  remote: GitRemoteName.default("origin"),
@@ -314,6 +326,7 @@ const ISSUE_FIELD_ORDER = [
314
326
  const CONFIG_FIELD_ORDER = [
315
327
  "tbd_format",
316
328
  "tbd_version",
329
+ "tbd_upgrades",
317
330
  "display",
318
331
  "sync",
319
332
  "settings",
@@ -356,5 +369,5 @@ const LOCAL_STATE_FIELD_ORDER = [
356
369
  ];
357
370
 
358
371
  //#endregion
359
- export { Priority as A, IssueSchema as C, LocalStateSchema as D, LOCAL_STATE_FIELD_ORDER as E, Version as F, SyncStorage as M, Timestamp as N, META_FIELD_ORDER as O, Ulid as P, IssueKind as S, IssueTitle as T, ISSUE_BODY_MAX_LENGTH as _, CONFIG_FIELD_ORDER as a, IdMappingYamlSchema as b, DATA_SYNC_SCHEMA_VERSION as c, DocCacheConfigSchema as d, DocsCacheSchema as f, GitRemoteName as g, GitBranchName as h, COMMON_DIR_LAYOUT_FIELD_ORDER as i, ShortId as j, MetaSchema as k, Dependency as l, ExternalIssueIdInput as m, AtticEntrySchema as n, CommonDirLayoutSchema as o, EntityType as p, BaseEntity as r, ConfigSchema as s, ATTIC_ENTRY_FIELD_ORDER as t, DependencyRelationType as u, ISSUE_FIELD_ORDER as v, IssueStatus as w, IssueId as x, ISSUE_TITLE_MAX_LENGTH as y };
360
- //# sourceMappingURL=schemas-f0EcuAVu.mjs.map
372
+ export { Priority as A, IssueSchema as C, LocalStateSchema as D, LOCAL_STATE_FIELD_ORDER as E, UpgradeEntrySchema as F, Version as I, SyncStorage as M, Timestamp as N, META_FIELD_ORDER as O, Ulid as P, IssueKind as S, IssueTitle as T, ISSUE_BODY_MAX_LENGTH as _, CONFIG_FIELD_ORDER as a, IdMappingYamlSchema as b, DATA_SYNC_SCHEMA_VERSION as c, DocCacheConfigSchema as d, DocsCacheSchema as f, GitRemoteName as g, GitBranchName as h, COMMON_DIR_LAYOUT_FIELD_ORDER as i, ShortId as j, MetaSchema as k, Dependency as l, ExternalIssueIdInput as m, AtticEntrySchema as n, CommonDirLayoutSchema as o, EntityType as p, BaseEntity as r, ConfigSchema as s, ATTIC_ENTRY_FIELD_ORDER as t, DependencyRelationType as u, ISSUE_FIELD_ORDER as v, IssueStatus as w, IssueId as x, ISSUE_TITLE_MAX_LENGTH as y };
373
+ //# sourceMappingURL=schemas-lCwRk30L.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas-lCwRk30L.mjs","names":[],"sources":["../src/lib/schemas.ts"],"sourcesContent":["/**\n * Zod schemas for tbd entities.\n *\n * These schemas are the normative specification for the file format.\n * See: tbd-design.md §2.6 Schemas\n */\n\nimport { z } from 'zod';\n\n// =============================================================================\n// Common Types (§2.6.1)\n// =============================================================================\n\n/**\n * ISO8601 timestamp with Z suffix (UTC).\n */\nexport const Timestamp = z.string().datetime();\n\n/**\n * Issue ID: prefix + 26 lowercase alphanumeric characters (ULID format).\n * Format: is-{ulid} where ulid is 26 chars (a-z, 0-9).\n * Example: is-01hx5zzkbkactav9wevgemmvrz\n */\nexport const IssueId = z.string().regex(/^is-[0-9a-z]{26}$/);\n\n/**\n * Short ID: 1+ base36 characters used for external/display IDs.\n * Typically 4 chars for new IDs (e.g., a7k2, b3m9).\n * Imports may preserve longer numeric IDs (e.g., \"100\" from \"tbd-100\").\n * Legacy imports may include dots (e.g., \"208.1\" from hierarchical numbering).\n * Imports may include dashes/underscores (e.g., \"stat-in_progress\" from JSONL).\n */\nexport const ShortId = z.string().regex(/^[0-9a-z._-]+$/);\n\n/**\n * ULID: 26 lowercase alphanumeric characters.\n * Used in internal IDs and ID mappings.\n * Example: 01hx5zzkbkactav9wevgemmvrz\n */\nexport const Ulid = z.string().regex(/^[0-9a-z]{26}$/);\n\n/**\n * External Issue ID input: accepts {prefix}-{short} or just {short}.\n * Examples: bd-a7k2, a7k2, bd-100, 100\n */\nexport const ExternalIssueIdInput = z.string().regex(/^([a-z]+-)?[0-9a-z]+$/);\n\n/**\n * Edit counter - incremented on every local change.\n * NOTE: Version is NOT used for conflict detection (Git push rejection is used).\n * Content hash is used as tiebreaker during merge resolution.\n * Version is informational only - set to max(local, remote) + 1 after merges.\n */\nexport const Version = z.number().int().nonnegative();\n\n/**\n * Entity type discriminator.\n */\nexport const EntityType = z.literal('is');\n\n// =============================================================================\n// BaseEntity (§2.6.2)\n// =============================================================================\n\n/**\n * All entities share common fields.\n */\nexport const BaseEntity = z.object({\n type: EntityType,\n id: IssueId,\n version: Version,\n created_at: Timestamp,\n updated_at: Timestamp,\n\n // Extensibility namespace for third-party data\n extensions: z.record(z.string(), z.unknown()).optional(),\n});\n\n// =============================================================================\n// Issue Schema (§2.6.3)\n// =============================================================================\n\n/**\n * Issue status values matching Beads.\n */\nexport const IssueStatus = z.enum(['open', 'in_progress', 'blocked', 'deferred', 'closed']);\n\n/**\n * Issue kind/type values matching Beads.\n * Note: CLI uses --type flag, which maps to this `kind` field.\n */\nexport const IssueKind = z.enum(['bug', 'feature', 'task', 'epic', 'chore']);\n\n/**\n * Maximum issue title length before detail belongs in the description body.\n */\nexport const ISSUE_TITLE_MAX_LENGTH = 500;\n\n/**\n * Maximum issue body section length to keep issue files practical to review and sync.\n */\nexport const ISSUE_BODY_MAX_LENGTH = 50_000;\n\n/**\n * Issue title text as persisted in issue files.\n */\nexport const IssueTitle = z.string().min(1).max(ISSUE_TITLE_MAX_LENGTH);\n\n/**\n * Priority: 0 (highest/critical) to 4 (lowest).\n */\nexport const Priority = z.number().int().min(0).max(4);\n\n/**\n * Dependency types - only \"blocks\" supported initially.\n */\nexport const DependencyRelationType = z.enum(['blocks']);\n\n/**\n * A dependency relationship.\n */\nexport const Dependency = z.object({\n type: DependencyRelationType,\n target: IssueId,\n});\n\n/**\n * Full issue schema.\n *\n * Field order is canonical and mirrored by ISSUE_FIELD_ORDER below:\n * type, id, title, kind, status, priority, version (the \"header seven\"),\n * then linkages, assignment, hierarchy, scheduling, provenance,\n * timestamps, lifecycle, and extensions.\n *\n * Note: Fields use .nullable() in addition to .optional() because\n * YAML parses `field: null` as JavaScript null, not undefined.\n *\n * Design note: We could add the short ID to this schema. We didn't originally\n * because it's one more field to maintain consistency around across files.\n * Having it here might make recovery of lost ID mappings far easier, but for\n * now we have more reliable management of the mappings file (ids.yml) and\n * consider it authoritative. See IdMappingYamlSchema (§2.6.8).\n */\nexport const IssueSchema = BaseEntity.extend({\n // Header seven: the fields you always want to see at a glance\n type: z.literal('is'),\n // id, version inherited from BaseEntity\n title: IssueTitle,\n kind: IssueKind.default('task'),\n status: IssueStatus.default('open'),\n priority: Priority.default(2),\n\n // Body content (serialized outside frontmatter)\n description: z.string().max(ISSUE_BODY_MAX_LENGTH).nullable().optional(),\n notes: z.string().max(ISSUE_BODY_MAX_LENGTH).nullable().optional(),\n\n // Linkages\n spec_path: z.string().nullable().optional(),\n\n // Assignment and categorization\n assignee: z.string().nullable().optional(),\n labels: z.array(z.string()).default([]),\n dependencies: z.array(Dependency).default([]),\n\n // Hierarchical issues\n parent_id: IssueId.nullable().optional(),\n\n // Child ordering hints - soft ordering for children under this parent.\n // Array of internal IssueIds in preferred display order.\n // May contain stale IDs; display logic filters for actual children.\n child_order_hints: z.array(IssueId).nullable().optional(),\n\n // Scheduling\n due_date: Timestamp.nullable().optional(),\n deferred_until: Timestamp.nullable().optional(),\n\n // Provenance and lifecycle\n created_by: z.string().nullable().optional(),\n closed_at: Timestamp.nullable().optional(),\n close_reason: z.string().nullable().optional(),\n});\n\n// =============================================================================\n// Config Schema (§2.6.4)\n// =============================================================================\n\n/**\n * Git branch name - restricted to safe characters.\n * Allows: alphanumeric, hyphens, underscores, forward slashes, and dots.\n * Prevents shell injection in git commands.\n */\nexport const GitBranchName = z\n .string()\n .min(1)\n .max(255)\n .regex(\n /^[a-zA-Z0-9._/-]+$/,\n 'Invalid branch name: only alphanumeric, dots, underscores, hyphens, and slashes allowed',\n );\n\n/**\n * Git remote name - restricted to safe characters.\n * Allows: alphanumeric, hyphens, underscores, and dots.\n * Prevents shell injection in git commands.\n */\nexport const GitRemoteName = z\n .string()\n .min(1)\n .max(255)\n .regex(\n /^[a-zA-Z0-9._-]+$/,\n 'Invalid remote name: only alphanumeric, dots, underscores, and hyphens allowed',\n );\n\n/**\n * Canonical local storage backend for issue sync machinery.\n */\nexport const SyncStorage = z.enum(['git-common-dir-v1']);\n\n/**\n * Doc cache configuration - maps destination paths to source locations.\n *\n * Keys are destination paths relative to .tbd/docs/ (e.g., \"shortcuts/standard/code-review-and-commit.md\")\n * Values are source docrefs:\n * - internal: bundled docs (e.g., \"internal:guidelines/typescript-rules.md\")\n * - git: a file in a repo (e.g., \"github:owner/repo@main//docs/file.md\"; gitlab: too)\n * - url: a plain URL kept verbatim (e.g., \"https://example.com/file.md\")\n * See `tbd docs show docref-format` for the full grammar.\n *\n * Example:\n * ```yaml\n * doc_cache:\n * shortcuts/standard/code-review-and-commit.md: internal:shortcuts/standard/code-review-and-commit.md\n * shortcuts/custom/my-shortcut.md: github:org/repo@main//shortcuts/my-shortcut.md\n * ```\n */\nexport const DocCacheConfigSchema = z.record(z.string(), z.string());\n\n/**\n * Documentation cache configuration (consolidated structure).\n *\n * Combines file sync mappings and lookup paths into a single config block.\n * See: docs/project/specs/active/plan-2026-01-26-docs-cache-config-restructure.md\n */\nexport const DocsCacheSchema = z.object({\n /**\n * Files to sync: maps destination paths to source locations.\n * Keys are destination paths relative to .tbd/docs/\n * Values are source docrefs:\n * - internal: bundled docs (e.g., \"internal:guidelines/typescript-rules.md\")\n * - git: a file in a repo (e.g., \"github:owner/repo@main//docs/file.md\"; gitlab: too)\n * - url: a plain URL kept verbatim (e.g., \"https://example.com/file.md\")\n * See `tbd docs show docref-format` for the full grammar.\n */\n files: z.record(z.string(), z.string()).optional(),\n /**\n * Ordered local-path docrefs (./-prefixed) naming additional in-repo doc\n * directories. They slot into the effective lookup order between the fork\n * dir and the cache; docs found there serve with state `local` and are not\n * forkable or updatable (they already live in the repo).\n */\n local_dirs: z.array(z.string()).optional(),\n /**\n * Search paths for doc lookup (like shell $PATH).\n * Earlier paths take precedence when names conflict.\n */\n lookup_path: z\n .array(z.string())\n .default(['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard']),\n});\n\n/**\n * Project configuration stored in .tbd/config.yml\n *\n * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.\n * The tbd_format field tracks breaking changes to this schema.\n *\n * ⚠️ FORWARD COMPATIBILITY POLICY:\n * This schema uses Zod's default strip() mode, which discards unknown fields.\n * To prevent data loss when users mix tbd versions:\n *\n * 1. **When changing config schema (adding, removing, or modifying fields), ALWAYS\n * bump the format version** (e.g., f03 → f04)\n * 2. Older tbd versions will error when they see an unknown format version\n * 3. The error message tells users to upgrade tbd\n *\n * This ensures older versions fail fast rather than silently corrupting config.\n * The format version check happens in config.ts via isCompatibleFormat().\n *\n * See tbd-format.ts for format version history and migration rules.\n */\n/**\n * One entry in the config upgrade history (`tbd_upgrades`).\n * Records a tbd version that ran `tbd setup` in this repo.\n */\nexport const UpgradeEntrySchema = z.object({\n /** The tbd version string (e.g. \"0.3.0\", or a dev build string). */\n version: z.string(),\n /**\n * When this version first ran setup (ISO 8601). Optional: the entry seeded from a\n * pre-f06 config has no timestamp because the original install time is unknown.\n */\n at: Timestamp.optional(),\n});\n\nexport const ConfigSchema = z.object({\n /**\n * Format version for the .tbd/ directory structure.\n * See tbd-format.ts for version history and migration rules.\n * Only bumped for breaking changes that require migration.\n */\n tbd_format: z.string().default('f01'),\n\n /**\n * The tbd version that last ran `tbd setup` in this repo. Updated on every setup.\n * Informational only; functional version gating is via `tbd_format`. See\n * `tbd_upgrades` for the full history. (As of format f06; before f06 this was the\n * install-time version and never changed.)\n */\n tbd_version: z.string(),\n /**\n * Ordered history (oldest first) of tbd versions that have run setup here. Appended\n * on each setup when the version changes; informational. Seeded on the f06 migration\n * from the prior `tbd_version` (without a timestamp). See tbd-format.ts.\n */\n tbd_upgrades: z.array(UpgradeEntrySchema).default([]),\n sync: z\n .object({\n branch: GitBranchName.default('tbd-sync'),\n remote: GitRemoteName.default('origin'),\n storage: SyncStorage.default('git-common-dir-v1'),\n })\n .default({}),\n display: z.object({\n id_prefix: z.string().min(1).max(20), // Required: set during init --prefix or import\n }),\n settings: z\n .object({\n auto_sync: z.boolean().default(false),\n /**\n * How often to automatically sync documentation cache (in hours).\n * - Default: 24 (sync once per day when actively using tbd)\n * - Set to 0 to disable auto-sync\n * - Only triggers when accessing docs (shortcut, guidelines, template commands)\n */\n doc_auto_sync_hours: z.number().default(24),\n /**\n * Whether to install the ensure-gh-cli.sh hook script during setup.\n * When true (default), `tbd setup` installs a SessionStart hook that\n * ensures the GitHub CLI is available in agent sessions.\n * Set to false or use `tbd setup --no-gh-cli` to disable.\n */\n use_gh_cli: z.boolean().default(true),\n })\n .default({}),\n /**\n * Documentation cache configuration (consolidated).\n * Contains files to sync and lookup paths.\n * See DocsCacheSchema for structure details.\n */\n docs_cache: DocsCacheSchema.optional(),\n});\n\n// =============================================================================\n// Common-Dir Layout Schema\n// =============================================================================\n\n/**\n * Local layout metadata stored in $GIT_COMMON_DIR/tbd/layout.yml.\n *\n * This uses the same tbd_format IDs as .tbd/config.yml so local checkout config\n * and Git common-dir machinery advance together during layout migrations.\n */\nexport const CommonDirLayoutSchema = z.object({\n tbd_format: z.string(),\n sync_storage: SyncStorage.default('git-common-dir-v1'),\n data_sync_worktree: z.literal('data-sync-worktree').default('data-sync-worktree'),\n lock_profile: z.literal('data-sync-v1').default('data-sync-v1'),\n created_at: Timestamp,\n updated_at: Timestamp,\n});\n\n// =============================================================================\n// Meta Schema (§2.6.5)\n// =============================================================================\n\n/**\n * Current schema version for synced payloads on the tbd-sync branch.\n *\n * This is intentionally separate from tbd_format, which tracks local checkout\n * and Git common-dir layout compatibility.\n */\nexport const DATA_SYNC_SCHEMA_VERSION = 1;\n\n/**\n * Shared metadata stored in .tbd/data-sync/meta.yml\n */\nexport const MetaSchema = z.object({\n schema_version: z.number().int(),\n created_at: Timestamp,\n});\n\n// =============================================================================\n// Local State Schema (§2.6.6)\n// =============================================================================\n\n/**\n * Per-node state stored in .tbd/state.yml (gitignored).\n * Tracks local timing information that shouldn't be shared across nodes.\n */\nexport const LocalStateSchema = z.object({\n /** When this node last synced issues successfully */\n last_sync_at: Timestamp.optional(),\n /** When this node last synced the doc cache successfully */\n last_doc_sync_at: Timestamp.optional(),\n /** Whether the user has seen the welcome message */\n welcome_seen: z.boolean().optional(),\n});\n\n// =============================================================================\n// Attic Entry Schema (§2.6.7)\n// =============================================================================\n\n/**\n * Preserved conflict losers.\n */\nexport const AtticEntrySchema = z.object({\n entity_id: IssueId,\n timestamp: Timestamp,\n field: z.string(),\n lost_value: z.string(),\n winner_source: z.enum(['local', 'remote']),\n loser_source: z.enum(['local', 'remote']),\n context: z.object({\n local_version: Version,\n remote_version: Version,\n local_updated_at: Timestamp,\n remote_updated_at: Timestamp,\n }),\n});\n\n// =============================================================================\n// ID Mapping Schema (§2.6.8)\n// =============================================================================\n\n/**\n * ID mapping YAML file schema for ids.yml.\n * Maps short IDs to ULIDs.\n * Format: { \"a7k2\": \"01hx5zzkbkactav9wevgemmvrz\", ... }\n */\nexport const IdMappingYamlSchema = z.record(ShortId, Ulid);\n\n// =============================================================================\n// Field Order Constants for YAML Serialization\n// =============================================================================\n//\n// Each array defines the canonical field order for YAML output.\n// Order mirrors the Zod schema definition above: identity first,\n// human-relevant fields next, bookkeeping last.\n//\n// Used with sortKeys() from yaml-utils.ts and ordering.manual()\n// from comparison-chain.ts. Fields not listed sort to the end.\n\n/**\n * Canonical field order for issue YAML frontmatter.\n * (description and notes are body content, not frontmatter)\n */\nexport const ISSUE_FIELD_ORDER = [\n // Header seven: the fields you always want to see at a glance\n 'type',\n 'id',\n 'title',\n 'kind',\n 'status',\n 'priority',\n 'version',\n\n // Linkages\n 'spec_path',\n\n // Assignment and categorization\n 'assignee',\n 'labels',\n 'dependencies',\n\n // Hierarchy\n 'parent_id',\n 'child_order_hints',\n\n // Scheduling\n 'due_date',\n 'deferred_until',\n\n // Provenance\n 'created_by',\n\n // Timestamps\n 'created_at',\n 'updated_at',\n\n // Lifecycle (closure)\n 'closed_at',\n 'close_reason',\n\n // Extensibility\n 'extensions',\n] as const;\n\n/**\n * Canonical field order for config YAML.\n */\nexport const CONFIG_FIELD_ORDER = [\n 'tbd_format',\n 'tbd_version',\n 'tbd_upgrades',\n 'display',\n 'sync',\n 'settings',\n 'docs_cache',\n] as const;\n\n/**\n * Canonical field order for $GIT_COMMON_DIR/tbd/layout.yml.\n */\nexport const COMMON_DIR_LAYOUT_FIELD_ORDER = [\n 'tbd_format',\n 'sync_storage',\n 'data_sync_worktree',\n 'lock_profile',\n 'created_at',\n 'updated_at',\n] as const;\n\n/**\n * Canonical field order for attic entry YAML.\n */\nexport const ATTIC_ENTRY_FIELD_ORDER = [\n 'entity_id',\n 'timestamp',\n 'field',\n 'lost_value',\n 'winner_source',\n 'loser_source',\n 'context',\n] as const;\n\n/**\n * Canonical field order for meta YAML.\n */\nexport const META_FIELD_ORDER = ['schema_version', 'created_at'] as const;\n\n/**\n * Canonical field order for local state YAML.\n */\nexport const LOCAL_STATE_FIELD_ORDER = [\n 'last_sync_at',\n 'last_doc_sync_at',\n 'welcome_seen',\n] as const;\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { C as IssueSchema, v as ISSUE_FIELD_ORDER } from "./schemas-f0EcuAVu.mjs";
1
+ import { C as IssueSchema, v as ISSUE_FIELD_ORDER } from "./schemas-lCwRk30L.mjs";
2
2
  import { c as stringifyYamlCompact, o as sortKeys, s as stringifyYaml } from "./yaml-utils-BPy991by.mjs";
3
3
  import matter from "gray-matter";
4
4
  import { parse } from "yaml";
@@ -183,8 +183,8 @@ function serializeIssue(issue) {
183
183
  * Package version, derived from git at build time.
184
184
  * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
185
185
  */
186
- const VERSION = "0.2.2";
186
+ const VERSION = "0.3.0";
187
187
 
188
188
  //#endregion
189
189
  export { insertAfterFrontmatter as a, noopLogger as c, serializeIssue as i, parseIssue as n, parseMarkdown as o, parseMarkdownWithFrontmatter as r, stripFrontmatter as s, VERSION as t };
190
- //# sourceMappingURL=src-BpvcrLnq.mjs.map
190
+ //# sourceMappingURL=src-CxKOynr1.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src-CxKOynr1.mjs","names":["parseYaml"],"sources":["../src/lib/types.ts","../src/utils/markdown-utils.ts","../src/file/parser.ts","../src/index.ts"],"sourcesContent":["/**\n * TypeScript types derived from Zod schemas.\n *\n * These types are the canonical TypeScript interface for tbd entities.\n */\n\nimport type { z } from 'zod';\n\nimport type {\n IssueSchema,\n IssueStatus,\n IssueKind,\n Priority,\n Dependency,\n ConfigSchema,\n CommonDirLayoutSchema,\n MetaSchema,\n LocalStateSchema,\n AtticEntrySchema,\n} from './schemas.js';\n\n// =============================================================================\n// Entity Types\n// =============================================================================\n\n/**\n * A tbd issue entity.\n */\nexport type Issue = z.infer<typeof IssueSchema>;\n\n/**\n * Issue status enum values.\n */\nexport type IssueStatusType = z.infer<typeof IssueStatus>;\n\n/**\n * Issue kind enum values.\n */\nexport type IssueKindType = z.infer<typeof IssueKind>;\n\n/**\n * Priority level (0-4).\n */\nexport type PriorityType = z.infer<typeof Priority>;\n\n/**\n * A dependency relationship.\n */\nexport type DependencyType = z.infer<typeof Dependency>;\n\n// =============================================================================\n// Configuration Types\n// =============================================================================\n\n/**\n * Project configuration.\n */\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * Git common-dir local layout metadata.\n */\nexport type CommonDirLayout = z.infer<typeof CommonDirLayoutSchema>;\n\n/**\n * Shared metadata.\n */\nexport type Meta = z.infer<typeof MetaSchema>;\n\n/**\n * Per-node local state.\n */\nexport type LocalState = z.infer<typeof LocalStateSchema>;\n\n/**\n * Attic entry for conflict losers.\n */\nexport type AtticEntry = z.infer<typeof AtticEntrySchema>;\n\n// =============================================================================\n// Input Types for Commands\n// =============================================================================\n\n/**\n * Options for creating an issue.\n */\nexport interface CreateIssueOptions {\n title: string;\n description?: string;\n kind?: IssueKindType;\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent_id?: string;\n due_date?: string;\n deferred_until?: string;\n}\n\n/**\n * Options for updating an issue.\n */\nexport interface UpdateIssueOptions {\n title?: string;\n description?: string;\n notes?: string;\n kind?: IssueKindType;\n status?: IssueStatusType;\n priority?: PriorityType;\n assignee?: string | null;\n addLabels?: string[];\n removeLabels?: string[];\n parent_id?: string | null;\n due_date?: string | null;\n deferred_until?: string | null;\n}\n\n/**\n * Options for listing issues.\n */\nexport interface ListIssuesOptions {\n status?: IssueStatusType | IssueStatusType[];\n kind?: IssueKindType | IssueKindType[];\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent?: string;\n all?: boolean;\n sort?: 'priority' | 'created' | 'updated';\n limit?: number;\n}\n\n/**\n * Options for searching issues.\n */\nexport interface SearchIssuesOptions {\n query: string;\n status?: IssueStatusType | IssueStatusType[];\n limit?: number;\n}\n\n// =============================================================================\n// CLI Utility Types\n// =============================================================================\n\n/**\n * A documentation section with title and slug.\n * Used by docs and design commands.\n */\nexport interface DocSection {\n title: string;\n slug: string;\n}\n\n/**\n * Logger interface for long-running operations in non-CLI layers.\n *\n * Allows core logic (file/, lib/) to report progress without depending on\n * the CLI output layer. CLI commands create an OperationLogger via\n * `OutputManager.logger(spinner)` and pass it to core functions.\n *\n * All methods are required. Use `noopLogger` when no logging is needed.\n */\nexport interface OperationLogger {\n /** Key milestones; drives the spinner in CLI context */\n progress: (message: string) => void;\n /** Operational detail (shown with --verbose or --debug) */\n info: (message: string) => void;\n /** Non-fatal warnings */\n warn: (message: string) => void;\n /** Internal state for troubleshooting (shown with --debug only) */\n debug: (message: string) => void;\n}\n\n/**\n * No-op logger for when no logging is needed.\n * Analogous to noopSpinner in the CLI layer.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\nexport const noopLogger: OperationLogger = {\n progress: noop,\n info: noop,\n warn: noop,\n debug: noop,\n};\n","/**\n * Markdown utilities for processing markdown content.\n *\n * Uses gray-matter for parsing and centralized yaml-utils for stringify to ensure\n * proper handling of special YAML characters (colons, quotes, etc.).\n */\n\nimport matter from 'gray-matter';\n\nimport { stringifyYamlCompact } from './yaml-utils.js';\n\nexport interface ParsedMarkdown {\n /** Raw frontmatter string (without --- delimiters), or null if no frontmatter */\n frontmatter: string | null;\n /** Body content after frontmatter, with leading newlines trimmed */\n body: string;\n}\n\n/**\n * Normalize line endings to LF.\n */\nexport function normalizeLineEndings(content: string): string {\n return content.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n}\n\n/**\n * Parse markdown content into frontmatter and body.\n * Handles both LF and CRLF line endings.\n *\n * @returns Object with frontmatter (null if none) and body\n */\nexport function parseMarkdown(content: string): ParsedMarkdown {\n const normalized = normalizeLineEndings(content);\n\n if (!matter.test(normalized)) {\n return { frontmatter: null, body: content };\n }\n\n try {\n const parsed = matter(normalized);\n\n // Extract frontmatter from parsed.data by stringifying back to YAML\n // The matter property is unreliable, so we reconstruct from data\n const data = parsed.data;\n let frontmatter: string | null = null;\n\n if (data && Object.keys(data).length > 0) {\n // Use centralized yaml-utils for proper handling of special characters\n // (colons, quotes, multiline strings, etc.)\n frontmatter = stringifyYamlCompact(data).trimEnd();\n } else {\n // Empty frontmatter (just --- followed by ---)\n frontmatter = '';\n }\n\n // Body with leading newlines trimmed\n const body = parsed.content.replace(/^\\n+/, '');\n\n return { frontmatter, body };\n } catch {\n // Invalid/unclosed frontmatter - treat as no frontmatter\n return { frontmatter: null, body: content };\n }\n}\n\n/**\n * Parse YAML frontmatter from markdown content.\n * Returns the frontmatter content (without delimiters) or null if no valid frontmatter.\n * Handles both LF and CRLF line endings.\n */\nexport function parseFrontmatter(content: string): string | null {\n return parseMarkdown(content).frontmatter;\n}\n\n/**\n * Strip YAML frontmatter from markdown content.\n * Returns the body content without frontmatter, with leading newlines trimmed.\n * Handles both LF and CRLF line endings.\n */\nexport function stripFrontmatter(content: string): string {\n return parseMarkdown(content).body;\n}\n\n/**\n * Insert content after YAML frontmatter.\n * If no frontmatter exists, prepends the content.\n * Content is inserted directly after ---. Include leading newlines in toInsert if needed.\n */\nexport function insertAfterFrontmatter(content: string, toInsert: string): string {\n const { frontmatter, body } = parseMarkdown(content);\n\n if (frontmatter === null) {\n return toInsert + content;\n }\n\n const frontmatterBlock = frontmatter ? `---\\n${frontmatter}\\n---` : '---\\n---';\n return `${frontmatterBlock}\\n${toInsert}\\n\\n${body}`;\n}\n","/**\n * YAML front matter parser and serializer for issue files.\n *\n * Issues are stored as Markdown files with YAML front matter:\n * ---\n * type: is\n * id: is-a1b2c3\n * ...\n * ---\n *\n * Description body here.\n *\n * ## Notes\n *\n * Working notes here.\n *\n * See: tbd-design.md §2.1 Markdown + YAML Front Matter Format\n */\n\nimport matter from 'gray-matter';\nimport { parse as parseYaml } from 'yaml';\n\nimport { normalizeLineEndings } from '../utils/markdown-utils.js';\nimport { sortKeys, stringifyYaml } from '../utils/yaml-utils.js';\nimport type { Issue } from '../lib/types.js';\nimport { IssueSchema, ISSUE_FIELD_ORDER } from '../lib/schemas.js';\n\n/**\n * gray-matter options using the 'yaml' package as engine.\n * This preserves date strings instead of converting them to Date objects.\n */\nexport const matterOptions = {\n engines: {\n yaml: {\n parse: (str: string): object => parseYaml(str) as object,\n stringify: (obj: object): string => stringifyYaml(obj),\n },\n },\n};\n\n/**\n * Parsed issue file content.\n */\nexport interface ParsedIssueFile {\n frontmatter: Record<string, unknown>;\n description: string;\n notes: string;\n}\n\n/**\n * Parse a Markdown file with YAML front matter.\n * Uses gray-matter for consistent frontmatter parsing.\n * Handles both LF and CRLF line endings.\n */\nexport function parseMarkdownWithFrontmatter(content: string): ParsedIssueFile {\n // Normalize CRLF to LF before parsing\n const normalizedContent = normalizeLineEndings(content);\n\n // Check for valid frontmatter\n if (!matter.test(normalizedContent)) {\n throw new Error('Invalid format: missing front matter opening delimiter');\n }\n\n const parsed = matter(normalizedContent, matterOptions);\n\n // gray-matter returns empty object if no closing delimiter found\n // but the raw matter string will be empty if parsing failed\n if (parsed.matter === '' && !normalizedContent.includes('---\\n---')) {\n // Check if there's actually a closing delimiter\n const lines = normalizedContent.split('\\n');\n let hasClosing = false;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i]?.trim() === '---') {\n hasClosing = true;\n break;\n }\n }\n if (!hasClosing) {\n throw new Error('Invalid format: missing front matter closing delimiter');\n }\n }\n\n const frontmatter = parsed.data as Record<string, unknown>;\n\n // Parse body - split into description and notes\n const body = parsed.content.trim();\n\n // Find notes section\n const notesMatch = /\\n## Notes\\n/i.exec(body);\n let description = body;\n let notes = '';\n\n if (notesMatch?.index !== undefined) {\n description = body.slice(0, notesMatch.index).trim();\n notes = body.slice(notesMatch.index + notesMatch[0].length).trim();\n }\n\n return { frontmatter, description, notes };\n}\n\n/**\n * Parse an issue from Markdown file content.\n */\nexport function parseIssue(content: string): Issue {\n const { frontmatter, description, notes } = parseMarkdownWithFrontmatter(content);\n\n // Merge body content into frontmatter\n const data = {\n ...frontmatter,\n description: description || undefined,\n notes: notes || undefined,\n };\n\n // Validate and parse with Zod\n return IssueSchema.parse(data);\n}\n\n/**\n * Serialize an issue to Markdown file content.\n * Uses canonical serialization for deterministic output.\n */\nexport function serializeIssue(issue: Issue): string {\n // Extract body fields\n const { description, notes, ...metadata } = issue;\n\n // Sort keys using canonical field order (not alphabetical)\n const sortedMetadata = sortKeys(metadata, ISSUE_FIELD_ORDER);\n\n // Serialize YAML with compact output for frontmatter.\n // sortMapEntries: false preserves our manual ordering.\n const yaml = stringifyYaml(sortedMetadata, {\n lineWidth: 0,\n nullStr: 'null',\n sortMapEntries: false,\n });\n\n // Build the file content\n // Note: No blank line between closing --- and body content\n const parts = ['---', yaml.trim(), '---'];\n\n if (description) {\n parts.push(description.trim());\n }\n\n if (notes) {\n parts.push('');\n parts.push('## Notes');\n parts.push('');\n parts.push(notes.trim());\n }\n\n // Single newline at end\n return parts.join('\\n') + '\\n';\n}\n","/**\n * tbd: Git-native issue tracking for AI agents and humans\n *\n * This is the library entry point. All exports here should be node-free\n * to support browser/edge runtime usage. CLI-specific code is in ./cli/.\n */\n\n// Version injected at build time\ndeclare const __TBD_VERSION__: string;\n\n/**\n * Package version, derived from git at build time.\n * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.\n */\nexport const VERSION: string =\n typeof __TBD_VERSION__ !== 'undefined' ? __TBD_VERSION__ : 'development';\n\n// Re-export schemas for library consumers\nexport * from './lib/schemas.js';\nexport * from './lib/types.js';\n\n// Re-export core operations (these should be node-free)\nexport { parseIssue, serializeIssue } from './file/parser.js';\n"],"mappings":"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"}