@contentrain/mcp 3.0.2 → 3.1.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 (162) hide show
  1. package/README.md +6 -3
  2. package/dist/{annotations-C-tQVmot.mjs → annotations-CXHPnS-M.mjs} +8 -1
  3. package/dist/annotations-CXHPnS-M.mjs.map +1 -0
  4. package/dist/{apply-manager-BybvnUVC.mjs → apply-manager-DsNq7Zhm.mjs} +7 -7
  5. package/dist/{apply-manager-BybvnUVC.mjs.map → apply-manager-DsNq7Zhm.mjs.map} +1 -1
  6. package/dist/{astro-parser-Cb9uuBpW.mjs → astro-parser-dhFgc2wA.mjs} +3 -3
  7. package/dist/{astro-parser-Cb9uuBpW.mjs.map → astro-parser-dhFgc2wA.mjs.map} +1 -1
  8. package/dist/{availability-Dgcmvv99.mjs → availability-DYHNoP0_.mjs} +5 -1
  9. package/dist/availability-DYHNoP0_.mjs.map +1 -0
  10. package/dist/{branch-lifecycle-DhzHfzs3.d.mts → branch-lifecycle-DGlgjnC7.d.mts} +32 -2
  11. package/dist/branch-lifecycle-DGlgjnC7.d.mts.map +1 -0
  12. package/dist/{branch-lifecycle-B6PXZ2N7.mjs → branch-lifecycle-DUOoyNWn.mjs} +32 -4
  13. package/dist/branch-lifecycle-DUOoyNWn.mjs.map +1 -0
  14. package/dist/{compiler-B6hNQeCC.mjs → compiler-CR9F0wZZ.mjs} +2 -2
  15. package/dist/{compiler-B6hNQeCC.mjs.map → compiler-CR9F0wZZ.mjs.map} +1 -1
  16. package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs → compiler-sfc.cjs-Cpexs92i.mjs} +2 -2
  17. package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs.map → compiler-sfc.cjs-Cpexs92i.mjs.map} +1 -1
  18. package/dist/{config-VlRKRwAS.mjs → config-B8a4JBT1.mjs} +2 -2
  19. package/dist/{config-VlRKRwAS.mjs.map → config-B8a4JBT1.mjs.map} +1 -1
  20. package/dist/{content-manager-Dn4tqDw1.d.mts → content-manager-Bx94DXSF.d.mts} +2 -2
  21. package/dist/{content-manager-Dn4tqDw1.d.mts.map → content-manager-Bx94DXSF.d.mts.map} +1 -1
  22. package/dist/{context-C3x6I3cE.mjs → context-CxpTATh4.mjs} +7 -7
  23. package/dist/context-CxpTATh4.mjs.map +1 -0
  24. package/dist/core/apply-manager.mjs +10 -11
  25. package/dist/core/config.d.mts +1 -1
  26. package/dist/core/config.mjs +2 -2
  27. package/dist/core/content-manager.d.mts +1 -1
  28. package/dist/core/content-manager.mjs +3 -3
  29. package/dist/core/context.d.mts +1 -1
  30. package/dist/core/context.mjs +5 -6
  31. package/dist/core/contracts/index.d.mts +1 -1
  32. package/dist/core/doctor.mjs +8 -8
  33. package/dist/core/graph-builder.mjs +3 -3
  34. package/dist/core/meta-manager.mjs +2 -2
  35. package/dist/core/model-manager.d.mts +1 -1
  36. package/dist/core/model-manager.mjs +3 -3
  37. package/dist/core/ops/index.d.mts +17 -29
  38. package/dist/core/ops/index.d.mts.map +1 -1
  39. package/dist/core/ops/index.mjs +9 -6
  40. package/dist/core/overlay-reader.d.mts +1 -1
  41. package/dist/core/overlay-reader.mjs +1 -1
  42. package/dist/core/scan-config.mjs +2 -2
  43. package/dist/core/scanner.mjs +4 -4
  44. package/dist/core/validator/index.d.mts +1 -1
  45. package/dist/core/validator/index.mjs +5 -5
  46. package/dist/{detect-B0oXbQaP.mjs → detect-CI32ynpq.mjs} +2 -2
  47. package/dist/{detect-B0oXbQaP.mjs.map → detect-CI32ynpq.mjs.map} +1 -1
  48. package/dist/{doctor-DNPuGgcg.mjs → doctor-B7G0oFxO.mjs} +7 -7
  49. package/dist/{doctor-DNPuGgcg.mjs.map → doctor-B7G0oFxO.mjs.map} +1 -1
  50. package/dist/errors-BmLE-JSC.mjs +55 -0
  51. package/dist/errors-BmLE-JSC.mjs.map +1 -0
  52. package/dist/{errors-CvBS12e7.mjs → errors-DLkAWnzQ.mjs} +1 -1
  53. package/dist/{errors-CvBS12e7.mjs.map → errors-DLkAWnzQ.mjs.map} +1 -1
  54. package/dist/{fs-DLbVB-Ek.mjs → fs-DJv5G8uL.mjs} +1 -1
  55. package/dist/{fs-DLbVB-Ek.mjs.map → fs-DJv5G8uL.mjs.map} +1 -1
  56. package/dist/git/branch-lifecycle.d.mts +2 -2
  57. package/dist/git/branch-lifecycle.mjs +5 -5
  58. package/dist/git/errors.d.mts +30 -0
  59. package/dist/git/errors.d.mts.map +1 -0
  60. package/dist/git/errors.mjs +2 -0
  61. package/dist/git/identity.mjs +1 -1
  62. package/dist/git/reconcile.d.mts +49 -0
  63. package/dist/git/reconcile.d.mts.map +1 -0
  64. package/dist/git/reconcile.mjs +13 -0
  65. package/dist/git/transaction.d.mts +60 -15
  66. package/dist/git/transaction.d.mts.map +1 -1
  67. package/dist/git/transaction.mjs +10 -11
  68. package/dist/{graph-builder-BEocwW13.mjs → graph-builder-C9NS7y-a.mjs} +3 -3
  69. package/dist/{graph-builder-BEocwW13.mjs.map → graph-builder-C9NS7y-a.mjs.map} +1 -1
  70. package/dist/{id-BjhaIxTL.mjs → id-CjGMPP1k.mjs} +2 -2
  71. package/dist/{id-BjhaIxTL.mjs.map → id-CjGMPP1k.mjs.map} +1 -1
  72. package/dist/{identity-D8QoGxhT.mjs → identity-CxAB9Qb8.mjs} +1 -1
  73. package/dist/{identity-D8QoGxhT.mjs.map → identity-CxAB9Qb8.mjs.map} +1 -1
  74. package/dist/index-_HJVkDJa.d.mts +85 -0
  75. package/dist/index-_HJVkDJa.d.mts.map +1 -0
  76. package/dist/index.mjs +24 -22
  77. package/dist/index.mjs.map +1 -1
  78. package/dist/{local-Db5X9mjZ.mjs → local-bnn6yrG7.mjs} +83 -8
  79. package/dist/local-bnn6yrG7.mjs.map +1 -0
  80. package/dist/{meta-manager-B_Se183X.mjs → meta-manager-g3shoZES.mjs} +2 -2
  81. package/dist/{meta-manager-B_Se183X.mjs.map → meta-manager-g3shoZES.mjs.map} +1 -1
  82. package/dist/{model-manager-DgyKMjLN.mjs → model-manager-DqwL8ptm.mjs} +66 -5
  83. package/dist/model-manager-DqwL8ptm.mjs.map +1 -0
  84. package/dist/{node-DUGiIkv1.mjs → node-CpLm3qr6.mjs} +1 -1
  85. package/dist/{node-DUGiIkv1.mjs.map → node-CpLm3qr6.mjs.map} +1 -1
  86. package/dist/{ops-BzbThweu.mjs → ops-BfrdZ6GK.mjs} +24 -34
  87. package/dist/ops-BfrdZ6GK.mjs.map +1 -0
  88. package/dist/{overlay-reader-CkU_pGXZ.mjs → overlay-reader-Bx1YxGXq.mjs} +1 -1
  89. package/dist/{overlay-reader-CkU_pGXZ.mjs.map → overlay-reader-Bx1YxGXq.mjs.map} +1 -1
  90. package/dist/{paths-CdPPWpbG.mjs → paths-C6_C1gLp.mjs} +1 -1
  91. package/dist/{paths-CdPPWpbG.mjs.map → paths-C6_C1gLp.mjs.map} +1 -1
  92. package/dist/providers/github/index.d.mts +10 -1
  93. package/dist/providers/github/index.d.mts.map +1 -1
  94. package/dist/providers/github/index.mjs +104 -4
  95. package/dist/providers/github/index.mjs.map +1 -1
  96. package/dist/providers/gitlab/index.d.mts +8 -1
  97. package/dist/providers/gitlab/index.d.mts.map +1 -1
  98. package/dist/providers/gitlab/index.mjs +15 -2
  99. package/dist/providers/gitlab/index.mjs.map +1 -1
  100. package/dist/providers/local/index.d.mts +26 -2
  101. package/dist/providers/local/index.d.mts.map +1 -1
  102. package/dist/providers/local/index.mjs +13 -12
  103. package/dist/reconcile-D5FTVW5z.mjs +1704 -0
  104. package/dist/reconcile-D5FTVW5z.mjs.map +1 -0
  105. package/dist/reconcile-M7KBZtdn.mjs +249 -0
  106. package/dist/reconcile-M7KBZtdn.mjs.map +1 -0
  107. package/dist/{scan-config-Dx49nCqk.mjs → scan-config-C5zzygeH.mjs} +2 -2
  108. package/dist/{scan-config-Dx49nCqk.mjs.map → scan-config-C5zzygeH.mjs.map} +1 -1
  109. package/dist/{scanner-BSvXjfkp.mjs → scanner-CEiT8Tgx.mjs} +7 -7
  110. package/dist/{scanner-BSvXjfkp.mjs.map → scanner-CEiT8Tgx.mjs.map} +1 -1
  111. package/dist/server/http/index.d.mts +1 -1
  112. package/dist/server/http/index.mjs +24 -22
  113. package/dist/server/http/index.mjs.map +1 -1
  114. package/dist/{server-DdxWpNBH.mjs → server-BtzVY7yp.mjs} +343 -227
  115. package/dist/server-BtzVY7yp.mjs.map +1 -0
  116. package/dist/{server-CLPNnhYL.d.mts → server-SDsD0XGM.d.mts} +2 -2
  117. package/dist/{server-CLPNnhYL.d.mts.map → server-SDsD0XGM.d.mts.map} +1 -1
  118. package/dist/server.d.mts +1 -1
  119. package/dist/server.mjs +24 -22
  120. package/dist/{source-map-generator-DhjiQdV3.mjs → source-map-generator-Clb2Eqse.mjs} +1 -1
  121. package/dist/{source-map-generator-DhjiQdV3.mjs.map → source-map-generator-Clb2Eqse.mjs.map} +1 -1
  122. package/dist/{svelte-parser-CwySu9yn.mjs → svelte-parser-CBN7Jxb7.mjs} +3 -3
  123. package/dist/{svelte-parser-CwySu9yn.mjs.map → svelte-parser-CBN7Jxb7.mjs.map} +1 -1
  124. package/dist/templates/index.mjs +1 -1
  125. package/dist/{templates-CAkTIRkE.mjs → templates-BZbohJc8.mjs} +1 -1
  126. package/dist/{templates-CAkTIRkE.mjs.map → templates-BZbohJc8.mjs.map} +1 -1
  127. package/dist/testing/memory-provider.d.mts +26 -1
  128. package/dist/testing/memory-provider.d.mts.map +1 -1
  129. package/dist/testing/memory-provider.mjs +62 -2
  130. package/dist/testing/memory-provider.mjs.map +1 -1
  131. package/dist/tools/annotations.d.mts.map +1 -1
  132. package/dist/tools/annotations.mjs +1 -1
  133. package/dist/tools/availability.d.mts.map +1 -1
  134. package/dist/tools/availability.mjs +1 -1
  135. package/dist/{transaction-BW9ektWu.mjs → transaction-BxDiiakv.mjs} +128 -64
  136. package/dist/transaction-BxDiiakv.mjs.map +1 -0
  137. package/dist/tsx-parser-DOQpJVgK.mjs +2 -0
  138. package/dist/{tsx-parser-BvsK6lyG.mjs → tsx-parser-go5V9zoc.mjs} +1 -1
  139. package/dist/{tsx-parser-BvsK6lyG.mjs.map → tsx-parser-go5V9zoc.mjs.map} +1 -1
  140. package/dist/util/detect.mjs +2 -2
  141. package/dist/util/fs.mjs +1 -1
  142. package/dist/util/id.mjs +1 -1
  143. package/dist/{validator-0sJ9vh1j.mjs → validator-B2GhPM3L.mjs} +5 -6
  144. package/dist/{validator-0sJ9vh1j.mjs.map → validator-B2GhPM3L.mjs.map} +1 -1
  145. package/dist/{vue-parser-auedZiJG.mjs → vue-parser-5Mp8w-mb.mjs} +3 -3
  146. package/dist/{vue-parser-auedZiJG.mjs.map → vue-parser-5Mp8w-mb.mjs.map} +1 -1
  147. package/package.json +13 -5
  148. package/dist/annotations-C-tQVmot.mjs.map +0 -1
  149. package/dist/availability-Dgcmvv99.mjs.map +0 -1
  150. package/dist/branch-lifecycle-B6PXZ2N7.mjs.map +0 -1
  151. package/dist/branch-lifecycle-DhzHfzs3.d.mts.map +0 -1
  152. package/dist/context-C3x6I3cE.mjs.map +0 -1
  153. package/dist/local-Db5X9mjZ.mjs.map +0 -1
  154. package/dist/model-manager-DgyKMjLN.mjs.map +0 -1
  155. package/dist/ops-BzbThweu.mjs.map +0 -1
  156. package/dist/paths-CmVw5Cw2.mjs +0 -65
  157. package/dist/paths-CmVw5Cw2.mjs.map +0 -1
  158. package/dist/serialization-tmgfFBOh.mjs +0 -2
  159. package/dist/server-DdxWpNBH.mjs.map +0 -1
  160. package/dist/transaction-BW9ektWu.mjs.map +0 -1
  161. package/dist/tsx-parser-Byc7HlJ8.mjs +0 -2
  162. /package/dist/{index-BWbTJlXm.d.mts → index-ByrVsRJE.d.mts} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-DNPuGgcg.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { createGit } from '../git/identity.js'\nimport { readdir, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel, validateModelDefinition } from './model-manager.js'\nimport { resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy } from './content-manager.js'\nimport { autoDetectSourceDirs, discoverFiles } from './scan-config.js'\nimport { checkBranchHealth, listRemoteCrBranches } from '../git/branch-lifecycle.js'\nimport { contentrainDir, pathExists, readDir, readJson, readText } from '../util/fs.js'\n\n/**\n * Doctor — project health report.\n *\n * The public entry point is `runDoctor(projectRoot, { usage? })`. It is\n * inherently local-filesystem work (Node version, git install, file\n * mtimes, orphan directory detection), so the MCP tool surface gates\n * it behind the `localWorktree` capability — same pattern as\n * `contentrain_setup` and normalize.\n *\n * The report is structured JSON so three consumers can share it:\n *\n * - The `contentrain doctor` CLI command pretty-prints the checks.\n * - The Serve UI `/api/doctor` route returns the report to the\n * Dashboard's Doctor panel.\n * - Automation (CI, Studio) gets a deterministic JSON shape it can\n * assert against.\n *\n * Usage analysis (`--usage`) is a heavier, opt-in branch — it scans\n * every source file in the repo for content-key references. Kept\n * behind the flag so the default doctor run stays fast.\n */\n\nexport type CheckSeverity = 'error' | 'warning' | 'info'\n\nexport interface DoctorCheck {\n name: string\n pass: boolean\n detail: string\n /**\n * `error` — default for failing checks. Blocks a clean bill of health.\n * `warning` — failing-but-not-blocking (e.g. pending branches above\n * threshold, stale SDK client).\n * `info` — passed check; pure informational.\n */\n severity?: CheckSeverity\n}\n\nexport interface UnusedKeyEntry {\n model: string\n kind: string\n key: string\n locale: string\n}\n\nexport interface DuplicateValueEntry {\n model: string\n locale: string\n value: string\n keys: string[]\n}\n\nexport interface MissingLocaleEntry {\n model: string\n key: string\n missingIn: string\n}\n\nexport interface DoctorUsageAnalysis {\n unusedKeys: UnusedKeyEntry[]\n duplicateValues: DuplicateValueEntry[]\n missingLocaleKeys: MissingLocaleEntry[]\n}\n\nexport interface DoctorReport {\n checks: DoctorCheck[]\n summary: {\n total: number\n passed: number\n failed: number\n warnings: number\n }\n /** Present only when `options.usage === true`. */\n usage?: DoctorUsageAnalysis\n}\n\nexport interface RunDoctorOptions {\n /** Run heavier `--usage` analysis (unused keys, duplicates, locale gaps). */\n usage?: boolean\n}\n\nexport async function runDoctor(\n projectRoot: string,\n options: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n const checks: DoctorCheck[] = []\n\n // ─── 1. Git installed ───\n try {\n const git = createGit(projectRoot)\n const version = await git.version()\n checks.push({\n name: 'Git',\n pass: true,\n detail: `v${version.major}.${version.minor}.${version.patch}`,\n })\n } catch {\n checks.push({ name: 'Git', pass: false, detail: 'Not installed or not in PATH', severity: 'error' })\n }\n\n // ─── 2. Git repo initialized ───\n const hasGit = await pathExists(join(projectRoot, '.git'))\n checks.push({\n name: 'Git repository',\n pass: hasGit,\n detail: hasGit ? projectRoot : 'No .git directory found',\n severity: hasGit ? undefined : 'error',\n })\n\n // ─── 3. Node version ───\n const nodeVersion = process.versions.node\n const [major] = nodeVersion.split('.').map(Number)\n const nodePass = (major ?? 0) >= 22\n checks.push({\n name: 'Node.js',\n pass: nodePass,\n detail: `v${nodeVersion}${nodePass ? '' : ' (requires ≥22)'}`,\n severity: nodePass ? undefined : 'error',\n })\n\n // ─── 4. .contentrain/ structure ───\n const crDir = contentrainDir(projectRoot)\n const hasCrDir = await pathExists(crDir)\n const hasConfig = await pathExists(join(crDir, 'config.json'))\n const hasModels = await pathExists(join(crDir, 'models'))\n const hasContent = await pathExists(join(crDir, 'content'))\n const structurePass = hasCrDir && hasConfig && hasModels && hasContent\n\n checks.push({\n name: '.contentrain/ structure',\n pass: structurePass,\n detail: !hasCrDir\n ? 'Not initialized — run `contentrain init`'\n : [\n hasConfig ? null : 'missing config.json',\n hasModels ? null : 'missing models/',\n hasContent ? null : 'missing content/',\n ].filter(Boolean).join(', ') || 'OK',\n severity: structurePass ? undefined : 'error',\n })\n\n // ─── 5. Config parseable ───\n let config: ContentrainConfig | null = null\n if (hasConfig) {\n config = await readConfig(projectRoot)\n checks.push({\n name: 'Config',\n pass: config !== null,\n detail: config\n ? `stack: ${config.stack}, locales: ${config.locales.supported.join(', ')}`\n : 'Failed to parse config.json',\n severity: config ? undefined : 'error',\n })\n }\n\n // ─── 6. Models parse AND satisfy the schema contract ───\n //\n // This check used to report \"all valid\" on the strength of JSON.parse alone,\n // which was a claim it had not earned. Model reads are unvalidated casts, so a\n // definition can be structurally wrong — a missing title_field, a field\n // constraint that cannot hold — and still load without a word. Doctor reports;\n // `contentrain validate --fix` is what repairs.\n if (hasCrDir) {\n try {\n // Enumerate the files, not `listModels`. `listModels` drops anything that\n // fails to parse, so routing this check through it made the \"failed to\n // parse\" branch unreachable — a corrupt model file was invisible to the\n // check whose job is to find it.\n const modelsDir = join(crDir, 'models')\n const files = (await readDir(modelsDir)).filter(f => f.endsWith('.json'))\n const parsed = await Promise.all(\n files.map(f => readJson<ModelDefinition>(join(modelsDir, f))),\n )\n const unparseable = files.filter((_, i) => parsed[i] === null)\n const invalid = parsed\n .filter((m): m is ModelDefinition => m !== null)\n .filter(m => validateModelDefinition(m).errors.length > 0)\n .map(m => m.id)\n const models = parsed.filter((m): m is ModelDefinition => m !== null)\n\n const problems = [\n ...(unparseable.length > 0 ? [`${unparseable.length} failed to parse (${unparseable.join(', ')})`] : []),\n ...(invalid.length > 0 ? [`${invalid.length} invalid (${invalid.join(', ')}) — run: contentrain validate --fix`] : []),\n ]\n checks.push({\n name: 'Models',\n pass: problems.length === 0,\n detail: `${models.length} model(s)${problems.length === 0 ? ', all valid' : `, ${problems.join('; ')}`}`,\n severity: problems.length === 0 ? undefined : 'error',\n })\n } catch {\n checks.push({ name: 'Models', pass: false, detail: 'Failed to read models', severity: 'error' })\n }\n }\n\n // ─── 7. Orphan content ───\n if (hasCrDir) {\n const orphans = await findOrphanContent(projectRoot)\n checks.push({\n name: 'Orphan content',\n pass: orphans.length === 0,\n detail: orphans.length === 0 ? 'None' : `Found: ${orphans.join(', ')}`,\n severity: orphans.length === 0 ? undefined : 'warning',\n })\n }\n\n // ─── 8. Stale contentrain branches ───\n if (hasGit) {\n try {\n const health = await checkBranchHealth(projectRoot)\n checks.push({\n name: 'Pending branches',\n pass: !health.blocked && !health.warning,\n detail: health.message\n ?? (health.unmerged === 0 ? 'None' : `${health.unmerged} active cr/* branch(es)`),\n severity: health.blocked ? 'error' : health.warning ? 'warning' : undefined,\n })\n } catch {\n checks.push({ name: 'Pending branches', pass: true, detail: 'Could not check' })\n }\n\n // ─── 8b. Remote cr/* branches ───\n // Authoritative ls-remote count (local branch pressure cannot see the\n // remote pile). Best-effort: skipped entirely without a remote, and an\n // unreachable remote is informational — doctor never fails offline.\n const remoteList = await listRemoteCrBranches(projectRoot, { timeoutMs: 5000 })\n if (remoteList) {\n if (remoteList.error) {\n checks.push({\n name: 'Remote branches',\n pass: true,\n detail: `Could not check ${remoteList.remote} (offline?)`,\n severity: 'info',\n })\n } else {\n const count = remoteList.branches.length\n const warnLimit = config?.branchWarnLimit ?? 50\n const warn = count >= warnLimit\n checks.push({\n name: 'Remote branches',\n pass: !warn,\n detail: count === 0\n ? `None on ${remoteList.remote}`\n : `${count} cr/* branch(es) on ${remoteList.remote}${warn ? ' — run `contentrain prune` to remove merged leftovers' : ''}`,\n severity: warn ? 'warning' : undefined,\n })\n }\n }\n }\n\n // ─── 9. SDK client freshness ───\n const clientDir = join(crDir, 'client')\n const modelsDir = join(crDir, 'models')\n if (await pathExists(clientDir) && await pathExists(modelsDir)) {\n try {\n const [clientMtime, modelsMtime] = await Promise.all([\n newestFileMtime(clientDir),\n newestFileMtime(modelsDir),\n ])\n if (clientMtime === null || modelsMtime === null) {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n } else {\n const fresh = clientMtime >= modelsMtime\n checks.push({\n name: 'SDK client',\n pass: fresh,\n detail: fresh ? 'Up to date' : 'Stale — run `contentrain generate`',\n severity: fresh ? undefined : 'warning',\n })\n }\n } catch {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n }\n }\n\n // ─── 10–12. Usage analysis (optional) ───\n let usage: DoctorUsageAnalysis | undefined\n if (options.usage && hasCrDir && config) {\n const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([\n analyzeUnusedKeys(projectRoot, config),\n analyzeDuplicateValues(projectRoot, config),\n analyzeMissingLocaleKeys(projectRoot, config),\n ])\n usage = { unusedKeys, duplicateValues, missingLocaleKeys }\n\n checks.push({\n name: 'Unused dictionary keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All dictionary keys referenced in source'\n : `${unusedKeys.length} dictionary key(s) not referenced in source code`,\n severity: unusedKeys.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Duplicate dictionary values',\n pass: duplicateValues.length === 0,\n detail: duplicateValues.length === 0\n ? 'No duplicate values'\n : `${duplicateValues.length} value(s) mapped to multiple keys`,\n severity: duplicateValues.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Locale key coverage',\n pass: missingLocaleKeys.length === 0,\n detail: missingLocaleKeys.length === 0\n ? 'All locales have matching keys'\n : `${missingLocaleKeys.length} key(s) missing in some locales`,\n severity: missingLocaleKeys.length === 0 ? undefined : 'warning',\n })\n }\n\n const passed = checks.filter(c => c.pass).length\n const failed = checks.length - passed\n const warnings = checks.filter(c => !c.pass && c.severity === 'warning').length\n\n const report: DoctorReport = {\n checks,\n summary: { total: checks.length, passed, failed, warnings },\n }\n if (usage) report.usage = usage\n return report\n}\n\n/**\n * Newest mtime among the files under `dir`, recursively — null if it holds none.\n *\n * Stat the files, never the directory. A directory's mtime only moves when an\n * entry is added, removed, or renamed inside it: `generate` rewrites the client\n * files in place, so `.contentrain/client` never moves after the first run,\n * while a selective sync recreates model files via `git checkout`, which does\n * move `.contentrain/models`. Comparing the two directories therefore reported\n * \"stale\" permanently after any model save.\n */\nasync function newestFileMtime(dir: string): Promise<number | null> {\n const entries: Dirent[] = await readdir(dir, { withFileTypes: true }).catch(() => [])\n const mtimes = await Promise.all(entries.map(async (entry) => {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) return newestFileMtime(full)\n return stat(full).then(s => s.mtimeMs, () => null)\n }))\n const known = mtimes.filter((m): m is number => m !== null)\n return known.length > 0 ? Math.max(...known) : null\n}\n\nasync function findOrphanContent(projectRoot: string): Promise<string[]> {\n const crDir = contentrainDir(projectRoot)\n const models = await listModels(projectRoot)\n const orphans: string[] = []\n\n const knownContentDirs = new Set<string>()\n for (const m of models) {\n const full = await readModel(projectRoot, m.id)\n const modelForPath = full\n ? {\n ...full,\n content_path: full.content_path ?? (m as { content_path?: string }).content_path,\n }\n : {\n id: m.id,\n name: m.id,\n kind: m.kind,\n domain: m.domain,\n i18n: m.i18n,\n fields: {},\n content_path: (m as { content_path?: string }).content_path,\n }\n knownContentDirs.add(resolveContentDir(projectRoot, modelForPath))\n }\n\n const contentDir = join(crDir, 'content')\n if (await pathExists(contentDir)) {\n const domains = await readDir(contentDir)\n for (const domain of domains) {\n const domainDir = join(contentDir, domain)\n const entries = await readDir(domainDir)\n for (const entry of entries) {\n if (entry === '.gitkeep') continue\n const entryDir = join(domainDir, entry)\n if (!knownContentDirs.has(entryDir)) {\n orphans.push(`${domain}/${entry}`)\n }\n }\n }\n }\n\n for (const dir of knownContentDirs) {\n if (dir.startsWith(contentDir)) continue\n if (!await pathExists(dir)) {\n orphans.push(`(missing custom path) ${dir}`)\n continue\n }\n await readDir(dir)\n }\n\n return orphans\n}\n\nasync function analyzeUnusedKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<UnusedKeyEntry[]> {\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, { paths: sourceDirs })\n if (files.length === 0) return []\n\n const chunks = await Promise.all(\n files.map(async (relPath) => {\n const content = await readText(join(projectRoot, relPath))\n return content ?? ''\n }),\n )\n const allSource = chunks.join('\\n')\n\n // Dictionaries only.\n //\n // A dictionary key IS the reference — source code writes `t('nav.home')`, so\n // a key absent from source is genuinely dead. Collection entry IDs and\n // document slugs are not: entries are fetched by query, and a hex id like\n // `0a1b2c3d4e5f` will never appear in a source file. Checking them produced\n // a report that was mostly noise — of 134 warnings on a real project, ~120\n // were collection and document rows, and the real findings were lost among\n // them.\n const models = (await listModels(projectRoot)).filter(m => m.kind === 'dictionary')\n const defaultLocale = config.locales.default\n const unused: UnusedKeyEntry[] = []\n\n for (const m of models) {\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const keys = await extractContentKeys(projectRoot, fullModel, defaultLocale)\n for (const key of keys) {\n if (!allSource.includes(key)) {\n unused.push({ model: m.id, kind: m.kind, key, locale: defaultLocale })\n }\n }\n }\n\n return unused\n}\n\n/**\n * Keys of a model that source code could plausibly reference by name.\n *\n * Only dictionaries have any: their keys are semantic addresses that appear\n * verbatim in source. Every other kind is queried, so its ids and slugs are\n * data, not identifiers, and looking for them in source finds nothing and\n * means nothing.\n */\nasync function extractContentKeys(\n projectRoot: string,\n model: ModelDefinition,\n locale: string,\n): Promise<string[]> {\n const cDir = resolveContentDir(projectRoot, model)\n if (!await pathExists(cDir)) return []\n\n switch (model.kind) {\n case 'dictionary': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, string>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'collection': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, Record<string, unknown>>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'document': {\n const strategy = resolveLocaleStrategy(model)\n const slugs: string[] = []\n if (!model.i18n) {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n } else if (strategy === 'file') {\n const dirs = await readDir(cDir)\n for (const d of dirs) if (!d.startsWith('.')) slugs.push(d)\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) if (f.endsWith(suffix)) slugs.push(f.slice(0, -suffix.length))\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n if (await pathExists(localeDir)) {\n const files = await readDir(localeDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n } else {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n return slugs\n }\n case 'singleton':\n return []\n default:\n return []\n }\n}\n\nasync function analyzeDuplicateValues(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<DuplicateValueEntry[]> {\n const models = await listModels(projectRoot)\n const result: DuplicateValueEntry[] = []\n\n for (const m of models) {\n if (m.kind !== 'dictionary') continue\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n for (const locale of config.locales.supported) {\n const filePath = resolveJsonFilePath(cDir, fullModel, locale)\n const data = await readJson<Record<string, string>>(filePath)\n if (!data) continue\n\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n\n for (const [value, keys] of valueToKeys) {\n if (keys.length > 1) {\n result.push({ model: m.id, locale, value, keys })\n }\n }\n }\n }\n\n return result\n}\n\nasync function analyzeMissingLocaleKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<MissingLocaleEntry[]> {\n if (config.locales.supported.length < 2) return []\n\n const models = await listModels(projectRoot)\n const result: MissingLocaleEntry[] = []\n const defaultLocale = config.locales.default\n const otherLocales = config.locales.supported.filter(l => l !== defaultLocale)\n\n for (const m of models) {\n if (m.kind !== 'dictionary' && m.kind !== 'collection') continue\n if (!m.i18n) continue\n\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n const defaultPath = resolveJsonFilePath(cDir, fullModel, defaultLocale)\n const defaultData = await readJson<Record<string, unknown>>(defaultPath)\n if (!defaultData) continue\n const defaultKeys = new Set(Object.keys(defaultData))\n\n for (const locale of otherLocales) {\n const localePath = resolveJsonFilePath(cDir, fullModel, locale)\n const localeData = await readJson<Record<string, unknown>>(localePath)\n const localeKeys = localeData ? new Set(Object.keys(localeData)) : new Set<string>()\n\n for (const key of defaultKeys) {\n if (!localeKeys.has(key)) {\n result.push({ model: m.id, key, missingIn: locale })\n }\n }\n }\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;AA4FA,eAAsB,UACpB,aACA,UAA4B,EAAE,EACP;CACvB,MAAM,SAAwB,EAAE;AAGhC,KAAI;EAEF,MAAM,UAAU,MADJ,UAAU,YAAY,CACR,SAAS;AACnC,SAAO,KAAK;GACV,MAAM;GACN,MAAM;GACN,QAAQ,IAAI,QAAQ,MAAM,GAAG,QAAQ,MAAM,GAAG,QAAQ;GACvD,CAAC;SACI;AACN,SAAO,KAAK;GAAE,MAAM;GAAO,MAAM;GAAO,QAAQ;GAAgC,UAAU;GAAS,CAAC;;CAItG,MAAM,SAAS,MAAM,WAAW,KAAK,aAAa,OAAO,CAAC;AAC1D,QAAO,KAAK;EACV,MAAM;EACN,MAAM;EACN,QAAQ,SAAS,cAAc;EAC/B,UAAU,SAAS,KAAA,IAAY;EAChC,CAAC;CAGF,MAAM,cAAc,QAAQ,SAAS;CACrC,MAAM,CAAC,SAAS,YAAY,MAAM,IAAI,CAAC,IAAI,OAAO;CAClD,MAAM,YAAY,SAAS,MAAM;AACjC,QAAO,KAAK;EACV,MAAM;EACN,MAAM;EACN,QAAQ,IAAI,cAAc,WAAW,KAAK;EAC1C,UAAU,WAAW,KAAA,IAAY;EAClC,CAAC;CAGF,MAAM,QAAQ,eAAe,YAAY;CACzC,MAAM,WAAW,MAAM,WAAW,MAAM;CACxC,MAAM,YAAY,MAAM,WAAW,KAAK,OAAO,cAAc,CAAC;CAC9D,MAAM,YAAY,MAAM,WAAW,KAAK,OAAO,SAAS,CAAC;CACzD,MAAM,aAAa,MAAM,WAAW,KAAK,OAAO,UAAU,CAAC;CAC3D,MAAM,gBAAgB,YAAY,aAAa,aAAa;AAE5D,QAAO,KAAK;EACV,MAAM;EACN,MAAM;EACN,QAAQ,CAAC,WACL,6CACA;GACE,YAAY,OAAO;GACnB,YAAY,OAAO;GACnB,aAAa,OAAO;GACrB,CAAC,OAAO,QAAQ,CAAC,KAAK,KAAK,IAAI;EACpC,UAAU,gBAAgB,KAAA,IAAY;EACvC,CAAC;CAGF,IAAI,SAAmC;AACvC,KAAI,WAAW;AACb,WAAS,MAAM,WAAW,YAAY;AACtC,SAAO,KAAK;GACV,MAAM;GACN,MAAM,WAAW;GACjB,QAAQ,SACJ,UAAU,OAAO,MAAM,aAAa,OAAO,QAAQ,UAAU,KAAK,KAAK,KACvE;GACJ,UAAU,SAAS,KAAA,IAAY;GAChC,CAAC;;AAUJ,KAAI,SACF,KAAI;EAKF,MAAM,YAAY,KAAK,OAAO,SAAS;EACvC,MAAM,SAAS,MAAM,QAAQ,UAAU,EAAE,QAAO,MAAK,EAAE,SAAS,QAAQ,CAAC;EACzE,MAAM,SAAS,MAAM,QAAQ,IAC3B,MAAM,KAAI,MAAK,SAA0B,KAAK,WAAW,EAAE,CAAC,CAAC,CAC9D;EACD,MAAM,cAAc,MAAM,QAAQ,GAAG,MAAM,OAAO,OAAO,KAAK;EAC9D,MAAM,UAAU,OACb,QAAQ,MAA4B,MAAM,KAAK,CAC/C,QAAO,MAAK,wBAAwB,EAAE,CAAC,OAAO,SAAS,EAAE,CACzD,KAAI,MAAK,EAAE,GAAG;EACjB,MAAM,SAAS,OAAO,QAAQ,MAA4B,MAAM,KAAK;EAErE,MAAM,WAAW,CACf,GAAI,YAAY,SAAS,IAAI,CAAC,GAAG,YAAY,OAAO,oBAAoB,YAAY,KAAK,KAAK,CAAC,GAAG,GAAG,EAAE,EACvG,GAAI,QAAQ,SAAS,IAAI,CAAC,GAAG,QAAQ,OAAO,YAAY,QAAQ,KAAK,KAAK,CAAC,qCAAqC,GAAG,EAAE,CACtH;AACD,SAAO,KAAK;GACV,MAAM;GACN,MAAM,SAAS,WAAW;GAC1B,QAAQ,GAAG,OAAO,OAAO,WAAW,SAAS,WAAW,IAAI,gBAAgB,KAAK,SAAS,KAAK,KAAK;GACpG,UAAU,SAAS,WAAW,IAAI,KAAA,IAAY;GAC/C,CAAC;SACI;AACN,SAAO,KAAK;GAAE,MAAM;GAAU,MAAM;GAAO,QAAQ;GAAyB,UAAU;GAAS,CAAC;;AAKpG,KAAI,UAAU;EACZ,MAAM,UAAU,MAAM,kBAAkB,YAAY;AACpD,SAAO,KAAK;GACV,MAAM;GACN,MAAM,QAAQ,WAAW;GACzB,QAAQ,QAAQ,WAAW,IAAI,SAAS,UAAU,QAAQ,KAAK,KAAK;GACpE,UAAU,QAAQ,WAAW,IAAI,KAAA,IAAY;GAC9C,CAAC;;AAIJ,KAAI,QAAQ;AACV,MAAI;GACF,MAAM,SAAS,MAAM,kBAAkB,YAAY;AACnD,UAAO,KAAK;IACV,MAAM;IACN,MAAM,CAAC,OAAO,WAAW,CAAC,OAAO;IACjC,QAAQ,OAAO,YACT,OAAO,aAAa,IAAI,SAAS,GAAG,OAAO,SAAS;IAC1D,UAAU,OAAO,UAAU,UAAU,OAAO,UAAU,YAAY,KAAA;IACnE,CAAC;UACI;AACN,UAAO,KAAK;IAAE,MAAM;IAAoB,MAAM;IAAM,QAAQ;IAAmB,CAAC;;EAOlF,MAAM,aAAa,MAAM,qBAAqB,aAAa,EAAE,WAAW,KAAM,CAAC;AAC/E,MAAI,WACF,KAAI,WAAW,MACb,QAAO,KAAK;GACV,MAAM;GACN,MAAM;GACN,QAAQ,mBAAmB,WAAW,OAAO;GAC7C,UAAU;GACX,CAAC;OACG;GACL,MAAM,QAAQ,WAAW,SAAS;GAElC,MAAM,OAAO,UADK,QAAQ,mBAAmB;AAE7C,UAAO,KAAK;IACV,MAAM;IACN,MAAM,CAAC;IACP,QAAQ,UAAU,IACd,WAAW,WAAW,WACtB,GAAG,MAAM,sBAAsB,WAAW,SAAS,OAAO,0DAA0D;IACxH,UAAU,OAAO,YAAY,KAAA;IAC9B,CAAC;;;CAMR,MAAM,YAAY,KAAK,OAAO,SAAS;CACvC,MAAM,YAAY,KAAK,OAAO,SAAS;AACvC,KAAI,MAAM,WAAW,UAAU,IAAI,MAAM,WAAW,UAAU,CAC5D,KAAI;EACF,MAAM,CAAC,aAAa,eAAe,MAAM,QAAQ,IAAI,CACnD,gBAAgB,UAAU,EAC1B,gBAAgB,UAAU,CAC3B,CAAC;AACF,MAAI,gBAAgB,QAAQ,gBAAgB,KAC1C,QAAO,KAAK;GAAE,MAAM;GAAc,MAAM;GAAM,QAAQ;GAAmB,CAAC;OACrE;GACL,MAAM,QAAQ,eAAe;AAC7B,UAAO,KAAK;IACV,MAAM;IACN,MAAM;IACN,QAAQ,QAAQ,eAAe;IAC/B,UAAU,QAAQ,KAAA,IAAY;IAC/B,CAAC;;SAEE;AACN,SAAO,KAAK;GAAE,MAAM;GAAc,MAAM;GAAM,QAAQ;GAAmB,CAAC;;CAK9E,IAAI;AACJ,KAAI,QAAQ,SAAS,YAAY,QAAQ;EACvC,MAAM,CAAC,YAAY,iBAAiB,qBAAqB,MAAM,QAAQ,IAAI;GACzE,kBAAkB,aAAa,OAAO;GACtC,uBAAuB,aAAa,OAAO;GAC3C,yBAAyB,aAAa,OAAO;GAC9C,CAAC;AACF,UAAQ;GAAE;GAAY;GAAiB;GAAmB;AAE1D,SAAO,KAAK;GACV,MAAM;GACN,MAAM,WAAW,WAAW;GAC5B,QAAQ,WAAW,WAAW,IAC1B,6CACA,GAAG,WAAW,OAAO;GACzB,UAAU,WAAW,WAAW,IAAI,KAAA,IAAY;GACjD,CAAC;AAEF,SAAO,KAAK;GACV,MAAM;GACN,MAAM,gBAAgB,WAAW;GACjC,QAAQ,gBAAgB,WAAW,IAC/B,wBACA,GAAG,gBAAgB,OAAO;GAC9B,UAAU,gBAAgB,WAAW,IAAI,KAAA,IAAY;GACtD,CAAC;AAEF,SAAO,KAAK;GACV,MAAM;GACN,MAAM,kBAAkB,WAAW;GACnC,QAAQ,kBAAkB,WAAW,IACjC,mCACA,GAAG,kBAAkB,OAAO;GAChC,UAAU,kBAAkB,WAAW,IAAI,KAAA,IAAY;GACxD,CAAC;;CAGJ,MAAM,SAAS,OAAO,QAAO,MAAK,EAAE,KAAK,CAAC;CAC1C,MAAM,SAAS,OAAO,SAAS;CAC/B,MAAM,WAAW,OAAO,QAAO,MAAK,CAAC,EAAE,QAAQ,EAAE,aAAa,UAAU,CAAC;CAEzE,MAAM,SAAuB;EAC3B;EACA,SAAS;GAAE,OAAO,OAAO;GAAQ;GAAQ;GAAQ;GAAU;EAC5D;AACD,KAAI,MAAO,QAAO,QAAQ;AAC1B,QAAO;;;;;;;;;;;;AAaT,eAAe,gBAAgB,KAAqC;CAClE,MAAM,UAAoB,MAAM,QAAQ,KAAK,EAAE,eAAe,MAAM,CAAC,CAAC,YAAY,EAAE,CAAC;CAMrF,MAAM,SALS,MAAM,QAAQ,IAAI,QAAQ,IAAI,OAAO,UAAU;EAC5D,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK;AAClC,MAAI,MAAM,aAAa,CAAE,QAAO,gBAAgB,KAAK;AACrD,SAAO,KAAK,KAAK,CAAC,MAAK,MAAK,EAAE,eAAe,KAAK;GAClD,CAAC,EACkB,QAAQ,MAAmB,MAAM,KAAK;AAC3D,QAAO,MAAM,SAAS,IAAI,KAAK,IAAI,GAAG,MAAM,GAAG;;AAGjD,eAAe,kBAAkB,aAAwC;CACvE,MAAM,QAAQ,eAAe,YAAY;CACzC,MAAM,SAAS,MAAM,WAAW,YAAY;CAC5C,MAAM,UAAoB,EAAE;CAE5B,MAAM,mCAAmB,IAAI,KAAa;AAC1C,MAAK,MAAM,KAAK,QAAQ;EACtB,MAAM,OAAO,MAAM,UAAU,aAAa,EAAE,GAAG;EAC/C,MAAM,eAAe,OACjB;GACE,GAAG;GACH,cAAc,KAAK,gBAAiB,EAAgC;GACrE,GACD;GACE,IAAI,EAAE;GACN,MAAM,EAAE;GACR,MAAM,EAAE;GACR,QAAQ,EAAE;GACV,MAAM,EAAE;GACR,QAAQ,EAAE;GACV,cAAe,EAAgC;GAChD;AACL,mBAAiB,IAAI,kBAAkB,aAAa,aAAa,CAAC;;CAGpE,MAAM,aAAa,KAAK,OAAO,UAAU;AACzC,KAAI,MAAM,WAAW,WAAW,EAAE;EAChC,MAAM,UAAU,MAAM,QAAQ,WAAW;AACzC,OAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,YAAY,KAAK,YAAY,OAAO;GAC1C,MAAM,UAAU,MAAM,QAAQ,UAAU;AACxC,QAAK,MAAM,SAAS,SAAS;AAC3B,QAAI,UAAU,WAAY;IAC1B,MAAM,WAAW,KAAK,WAAW,MAAM;AACvC,QAAI,CAAC,iBAAiB,IAAI,SAAS,CACjC,SAAQ,KAAK,GAAG,OAAO,GAAG,QAAQ;;;;AAM1C,MAAK,MAAM,OAAO,kBAAkB;AAClC,MAAI,IAAI,WAAW,WAAW,CAAE;AAChC,MAAI,CAAC,MAAM,WAAW,IAAI,EAAE;AAC1B,WAAQ,KAAK,yBAAyB,MAAM;AAC5C;;AAEF,QAAM,QAAQ,IAAI;;AAGpB,QAAO;;AAGT,eAAe,kBACb,aACA,QAC2B;CAE3B,MAAM,QAAQ,MAAM,cAAc,aAAa,EAAE,OAD9B,MAAM,qBAAqB,YAAY,EACU,CAAC;AACrE,KAAI,MAAM,WAAW,EAAG,QAAO,EAAE;CAQjC,MAAM,aANS,MAAM,QAAQ,IAC3B,MAAM,IAAI,OAAO,YAAY;AAE3B,SADgB,MAAM,SAAS,KAAK,aAAa,QAAQ,CAAC,IACxC;GAClB,CACH,EACwB,KAAK,KAAK;CAWnC,MAAM,UAAU,MAAM,WAAW,YAAY,EAAE,QAAO,MAAK,EAAE,SAAS,aAAa;CACnF,MAAM,gBAAgB,OAAO,QAAQ;CACrC,MAAM,SAA2B,EAAE;AAEnC,MAAK,MAAM,KAAK,QAAQ;EACtB,MAAM,YAAY,MAAM,UAAU,aAAa,EAAE,GAAG;AACpD,MAAI,CAAC,UAAW;EAEhB,MAAM,OAAO,MAAM,mBAAmB,aAAa,WAAW,cAAc;AAC5E,OAAK,MAAM,OAAO,KAChB,KAAI,CAAC,UAAU,SAAS,IAAI,CAC1B,QAAO,KAAK;GAAE,OAAO,EAAE;GAAI,MAAM,EAAE;GAAM;GAAK,QAAQ;GAAe,CAAC;;AAK5E,QAAO;;;;;;;;;;AAWT,eAAe,mBACb,aACA,OACA,QACmB;CACnB,MAAM,OAAO,kBAAkB,aAAa,MAAM;AAClD,KAAI,CAAC,MAAM,WAAW,KAAK,CAAE,QAAO,EAAE;AAEtC,SAAQ,MAAM,MAAd;EACE,KAAK,cAAc;GAEjB,MAAM,OAAO,MAAM,SADF,oBAAoB,MAAM,OAAO,OAAO,CACI;AAC7D,UAAO,OAAO,OAAO,KAAK,KAAK,GAAG,EAAE;;EAEtC,KAAK,cAAc;GAEjB,MAAM,OAAO,MAAM,SADF,oBAAoB,MAAM,OAAO,OAAO,CACqB;AAC9E,UAAO,OAAO,OAAO,KAAK,KAAK,GAAG,EAAE;;EAEtC,KAAK,YAAY;GACf,MAAM,WAAW,sBAAsB,MAAM;GAC7C,MAAM,QAAkB,EAAE;AAC1B,OAAI,CAAC,MAAM,MAAM;IACf,MAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,SAAK,MAAM,KAAK,MAAO,KAAI,EAAE,SAAS,MAAM,CAAE,OAAM,KAAK,EAAE,QAAQ,OAAO,GAAG,CAAC;cACrE,aAAa,QAAQ;IAC9B,MAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,SAAK,MAAM,KAAK,KAAM,KAAI,CAAC,EAAE,WAAW,IAAI,CAAE,OAAM,KAAK,EAAE;cAClD,aAAa,UAAU;IAChC,MAAM,QAAQ,MAAM,QAAQ,KAAK;IACjC,MAAM,SAAS,IAAI,OAAO;AAC1B,SAAK,MAAM,KAAK,MAAO,KAAI,EAAE,SAAS,OAAO,CAAE,OAAM,KAAK,EAAE,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC;cAC5E,aAAa,aAAa;IACnC,MAAM,YAAY,KAAK,MAAM,OAAO;AACpC,QAAI,MAAM,WAAW,UAAU,EAAE;KAC/B,MAAM,QAAQ,MAAM,QAAQ,UAAU;AACtC,UAAK,MAAM,KAAK,MAAO,KAAI,EAAE,SAAS,MAAM,CAAE,OAAM,KAAK,EAAE,QAAQ,OAAO,GAAG,CAAC;;UAE3E;IACL,MAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,SAAK,MAAM,KAAK,MAAO,KAAI,EAAE,SAAS,MAAM,CAAE,OAAM,KAAK,EAAE,QAAQ,OAAO,GAAG,CAAC;;AAEhF,UAAO;;EAET,KAAK,YACH,QAAO,EAAE;EACX,QACE,QAAO,EAAE;;;AAIf,eAAe,uBACb,aACA,QACgC;CAChC,MAAM,SAAS,MAAM,WAAW,YAAY;CAC5C,MAAM,SAAgC,EAAE;AAExC,MAAK,MAAM,KAAK,QAAQ;AACtB,MAAI,EAAE,SAAS,aAAc;EAC7B,MAAM,YAAY,MAAM,UAAU,aAAa,EAAE,GAAG;AACpD,MAAI,CAAC,UAAW;EAEhB,MAAM,OAAO,kBAAkB,aAAa,UAAU;AACtD,OAAK,MAAM,UAAU,OAAO,QAAQ,WAAW;GAE7C,MAAM,OAAO,MAAM,SADF,oBAAoB,MAAM,WAAW,OAAO,CACA;AAC7D,OAAI,CAAC,KAAM;GAEX,MAAM,8BAAc,IAAI,KAAuB;AAC/C,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;IAC/C,MAAM,MAAM,YAAY,IAAI,MAAM;AAClC,QAAI,IAAK,KAAI,KAAK,IAAI;QACjB,aAAY,IAAI,OAAO,CAAC,IAAI,CAAC;;AAGpC,QAAK,MAAM,CAAC,OAAO,SAAS,YAC1B,KAAI,KAAK,SAAS,EAChB,QAAO,KAAK;IAAE,OAAO,EAAE;IAAI;IAAQ;IAAO;IAAM,CAAC;;;AAMzD,QAAO;;AAGT,eAAe,yBACb,aACA,QAC+B;AAC/B,KAAI,OAAO,QAAQ,UAAU,SAAS,EAAG,QAAO,EAAE;CAElD,MAAM,SAAS,MAAM,WAAW,YAAY;CAC5C,MAAM,SAA+B,EAAE;CACvC,MAAM,gBAAgB,OAAO,QAAQ;CACrC,MAAM,eAAe,OAAO,QAAQ,UAAU,QAAO,MAAK,MAAM,cAAc;AAE9E,MAAK,MAAM,KAAK,QAAQ;AACtB,MAAI,EAAE,SAAS,gBAAgB,EAAE,SAAS,aAAc;AACxD,MAAI,CAAC,EAAE,KAAM;EAEb,MAAM,YAAY,MAAM,UAAU,aAAa,EAAE,GAAG;AACpD,MAAI,CAAC,UAAW;EAEhB,MAAM,OAAO,kBAAkB,aAAa,UAAU;EAEtD,MAAM,cAAc,MAAM,SADN,oBAAoB,MAAM,WAAW,cAAc,CACC;AACxE,MAAI,CAAC,YAAa;EAClB,MAAM,cAAc,IAAI,IAAI,OAAO,KAAK,YAAY,CAAC;AAErD,OAAK,MAAM,UAAU,cAAc;GAEjC,MAAM,aAAa,MAAM,SADN,oBAAoB,MAAM,WAAW,OAAO,CACO;GACtE,MAAM,aAAa,aAAa,IAAI,IAAI,OAAO,KAAK,WAAW,CAAC,mBAAG,IAAI,KAAa;AAEpF,QAAK,MAAM,OAAO,YAChB,KAAI,CAAC,WAAW,IAAI,IAAI,CACtB,QAAO,KAAK;IAAE,OAAO,EAAE;IAAI;IAAK,WAAW;IAAQ,CAAC;;;AAM5D,QAAO"}
1
+ {"version":3,"file":"doctor-B7G0oFxO.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { createGit } from '../git/identity.js'\nimport { readdir, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel, validateModelDefinition } from './model-manager.js'\nimport { resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy } from './content-manager.js'\nimport { autoDetectSourceDirs, discoverFiles } from './scan-config.js'\nimport { checkBranchHealth, listRemoteCrBranches } from '../git/branch-lifecycle.js'\nimport { contentrainDir, pathExists, readDir, readJson, readText } from '../util/fs.js'\n\n/**\n * Doctor — project health report.\n *\n * The public entry point is `runDoctor(projectRoot, { usage? })`. It is\n * inherently local-filesystem work (Node version, git install, file\n * mtimes, orphan directory detection), so the MCP tool surface gates\n * it behind the `localWorktree` capability — same pattern as\n * `contentrain_setup` and normalize.\n *\n * The report is structured JSON so three consumers can share it:\n *\n * - The `contentrain doctor` CLI command pretty-prints the checks.\n * - The Serve UI `/api/doctor` route returns the report to the\n * Dashboard's Doctor panel.\n * - Automation (CI, Studio) gets a deterministic JSON shape it can\n * assert against.\n *\n * Usage analysis (`--usage`) is a heavier, opt-in branch — it scans\n * every source file in the repo for content-key references. Kept\n * behind the flag so the default doctor run stays fast.\n */\n\nexport type CheckSeverity = 'error' | 'warning' | 'info'\n\nexport interface DoctorCheck {\n name: string\n pass: boolean\n detail: string\n /**\n * `error` — default for failing checks. Blocks a clean bill of health.\n * `warning` — failing-but-not-blocking (e.g. pending branches above\n * threshold, stale SDK client).\n * `info` — passed check; pure informational.\n */\n severity?: CheckSeverity\n}\n\nexport interface UnusedKeyEntry {\n model: string\n kind: string\n key: string\n locale: string\n}\n\nexport interface DuplicateValueEntry {\n model: string\n locale: string\n value: string\n keys: string[]\n}\n\nexport interface MissingLocaleEntry {\n model: string\n key: string\n missingIn: string\n}\n\nexport interface DoctorUsageAnalysis {\n unusedKeys: UnusedKeyEntry[]\n duplicateValues: DuplicateValueEntry[]\n missingLocaleKeys: MissingLocaleEntry[]\n}\n\nexport interface DoctorReport {\n checks: DoctorCheck[]\n summary: {\n total: number\n passed: number\n failed: number\n warnings: number\n }\n /** Present only when `options.usage === true`. */\n usage?: DoctorUsageAnalysis\n}\n\nexport interface RunDoctorOptions {\n /** Run heavier `--usage` analysis (unused keys, duplicates, locale gaps). */\n usage?: boolean\n}\n\nexport async function runDoctor(\n projectRoot: string,\n options: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n const checks: DoctorCheck[] = []\n\n // ─── 1. Git installed ───\n try {\n const git = createGit(projectRoot)\n const version = await git.version()\n checks.push({\n name: 'Git',\n pass: true,\n detail: `v${version.major}.${version.minor}.${version.patch}`,\n })\n } catch {\n checks.push({ name: 'Git', pass: false, detail: 'Not installed or not in PATH', severity: 'error' })\n }\n\n // ─── 2. Git repo initialized ───\n const hasGit = await pathExists(join(projectRoot, '.git'))\n checks.push({\n name: 'Git repository',\n pass: hasGit,\n detail: hasGit ? projectRoot : 'No .git directory found',\n severity: hasGit ? undefined : 'error',\n })\n\n // ─── 3. Node version ───\n const nodeVersion = process.versions.node\n const [major] = nodeVersion.split('.').map(Number)\n const nodePass = (major ?? 0) >= 22\n checks.push({\n name: 'Node.js',\n pass: nodePass,\n detail: `v${nodeVersion}${nodePass ? '' : ' (requires ≥22)'}`,\n severity: nodePass ? undefined : 'error',\n })\n\n // ─── 4. .contentrain/ structure ───\n const crDir = contentrainDir(projectRoot)\n const hasCrDir = await pathExists(crDir)\n const hasConfig = await pathExists(join(crDir, 'config.json'))\n const hasModels = await pathExists(join(crDir, 'models'))\n const hasContent = await pathExists(join(crDir, 'content'))\n const structurePass = hasCrDir && hasConfig && hasModels && hasContent\n\n checks.push({\n name: '.contentrain/ structure',\n pass: structurePass,\n detail: !hasCrDir\n ? 'Not initialized — run `contentrain init`'\n : [\n hasConfig ? null : 'missing config.json',\n hasModels ? null : 'missing models/',\n hasContent ? null : 'missing content/',\n ].filter(Boolean).join(', ') || 'OK',\n severity: structurePass ? undefined : 'error',\n })\n\n // ─── 5. Config parseable ───\n let config: ContentrainConfig | null = null\n if (hasConfig) {\n config = await readConfig(projectRoot)\n checks.push({\n name: 'Config',\n pass: config !== null,\n detail: config\n ? `stack: ${config.stack}, locales: ${config.locales.supported.join(', ')}`\n : 'Failed to parse config.json',\n severity: config ? undefined : 'error',\n })\n }\n\n // ─── 6. Models parse AND satisfy the schema contract ───\n //\n // This check used to report \"all valid\" on the strength of JSON.parse alone,\n // which was a claim it had not earned. Model reads are unvalidated casts, so a\n // definition can be structurally wrong — a missing title_field, a field\n // constraint that cannot hold — and still load without a word. Doctor reports;\n // `contentrain validate --fix` is what repairs.\n if (hasCrDir) {\n try {\n // Enumerate the files, not `listModels`. `listModels` drops anything that\n // fails to parse, so routing this check through it made the \"failed to\n // parse\" branch unreachable — a corrupt model file was invisible to the\n // check whose job is to find it.\n const modelsDir = join(crDir, 'models')\n const files = (await readDir(modelsDir)).filter(f => f.endsWith('.json'))\n const parsed = await Promise.all(\n files.map(f => readJson<ModelDefinition>(join(modelsDir, f))),\n )\n const unparseable = files.filter((_, i) => parsed[i] === null)\n const invalid = parsed\n .filter((m): m is ModelDefinition => m !== null)\n .filter(m => validateModelDefinition(m).errors.length > 0)\n .map(m => m.id)\n const models = parsed.filter((m): m is ModelDefinition => m !== null)\n\n const problems = [\n ...(unparseable.length > 0 ? [`${unparseable.length} failed to parse (${unparseable.join(', ')})`] : []),\n ...(invalid.length > 0 ? [`${invalid.length} invalid (${invalid.join(', ')}) — run: contentrain validate --fix`] : []),\n ]\n checks.push({\n name: 'Models',\n pass: problems.length === 0,\n detail: `${models.length} model(s)${problems.length === 0 ? ', all valid' : `, ${problems.join('; ')}`}`,\n severity: problems.length === 0 ? undefined : 'error',\n })\n } catch {\n checks.push({ name: 'Models', pass: false, detail: 'Failed to read models', severity: 'error' })\n }\n }\n\n // ─── 7. Orphan content ───\n if (hasCrDir) {\n const orphans = await findOrphanContent(projectRoot)\n checks.push({\n name: 'Orphan content',\n pass: orphans.length === 0,\n detail: orphans.length === 0 ? 'None' : `Found: ${orphans.join(', ')}`,\n severity: orphans.length === 0 ? undefined : 'warning',\n })\n }\n\n // ─── 8. Stale contentrain branches ───\n if (hasGit) {\n try {\n const health = await checkBranchHealth(projectRoot)\n checks.push({\n name: 'Pending branches',\n pass: !health.blocked && !health.warning,\n detail: health.message\n ?? (health.unmerged === 0 ? 'None' : `${health.unmerged} active cr/* branch(es)`),\n severity: health.blocked ? 'error' : health.warning ? 'warning' : undefined,\n })\n } catch {\n checks.push({ name: 'Pending branches', pass: true, detail: 'Could not check' })\n }\n\n // ─── 8b. Remote cr/* branches ───\n // Authoritative ls-remote count (local branch pressure cannot see the\n // remote pile). Best-effort: skipped entirely without a remote, and an\n // unreachable remote is informational — doctor never fails offline.\n const remoteList = await listRemoteCrBranches(projectRoot, { timeoutMs: 5000 })\n if (remoteList) {\n if (remoteList.error) {\n checks.push({\n name: 'Remote branches',\n pass: true,\n detail: `Could not check ${remoteList.remote} (offline?)`,\n severity: 'info',\n })\n } else {\n const count = remoteList.branches.length\n const warnLimit = config?.branchWarnLimit ?? 50\n const warn = count >= warnLimit\n checks.push({\n name: 'Remote branches',\n pass: !warn,\n detail: count === 0\n ? `None on ${remoteList.remote}`\n : `${count} cr/* branch(es) on ${remoteList.remote}${warn ? ' — run `contentrain prune` to remove merged leftovers' : ''}`,\n severity: warn ? 'warning' : undefined,\n })\n }\n }\n }\n\n // ─── 9. SDK client freshness ───\n const clientDir = join(crDir, 'client')\n const modelsDir = join(crDir, 'models')\n if (await pathExists(clientDir) && await pathExists(modelsDir)) {\n try {\n const [clientMtime, modelsMtime] = await Promise.all([\n newestFileMtime(clientDir),\n newestFileMtime(modelsDir),\n ])\n if (clientMtime === null || modelsMtime === null) {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n } else {\n const fresh = clientMtime >= modelsMtime\n checks.push({\n name: 'SDK client',\n pass: fresh,\n detail: fresh ? 'Up to date' : 'Stale — run `contentrain generate`',\n severity: fresh ? undefined : 'warning',\n })\n }\n } catch {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n }\n }\n\n // ─── 10–12. Usage analysis (optional) ───\n let usage: DoctorUsageAnalysis | undefined\n if (options.usage && hasCrDir && config) {\n const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([\n analyzeUnusedKeys(projectRoot, config),\n analyzeDuplicateValues(projectRoot, config),\n analyzeMissingLocaleKeys(projectRoot, config),\n ])\n usage = { unusedKeys, duplicateValues, missingLocaleKeys }\n\n checks.push({\n name: 'Unused dictionary keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All dictionary keys referenced in source'\n : `${unusedKeys.length} dictionary key(s) not referenced in source code`,\n severity: unusedKeys.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Duplicate dictionary values',\n pass: duplicateValues.length === 0,\n detail: duplicateValues.length === 0\n ? 'No duplicate values'\n : `${duplicateValues.length} value(s) mapped to multiple keys`,\n severity: duplicateValues.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Locale key coverage',\n pass: missingLocaleKeys.length === 0,\n detail: missingLocaleKeys.length === 0\n ? 'All locales have matching keys'\n : `${missingLocaleKeys.length} key(s) missing in some locales`,\n severity: missingLocaleKeys.length === 0 ? undefined : 'warning',\n })\n }\n\n const passed = checks.filter(c => c.pass).length\n const failed = checks.length - passed\n const warnings = checks.filter(c => !c.pass && c.severity === 'warning').length\n\n const report: DoctorReport = {\n checks,\n summary: { total: checks.length, passed, failed, warnings },\n }\n if (usage) report.usage = usage\n return report\n}\n\n/**\n * Newest mtime among the files under `dir`, recursively — null if it holds none.\n *\n * Stat the files, never the directory. A directory's mtime only moves when an\n * entry is added, removed, or renamed inside it: `generate` rewrites the client\n * files in place, so `.contentrain/client` never moves after the first run,\n * while a selective sync recreates model files via `git checkout`, which does\n * move `.contentrain/models`. Comparing the two directories therefore reported\n * \"stale\" permanently after any model save.\n */\nasync function newestFileMtime(dir: string): Promise<number | null> {\n const entries: Dirent[] = await readdir(dir, { withFileTypes: true }).catch(() => [])\n const mtimes = await Promise.all(entries.map(async (entry) => {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) return newestFileMtime(full)\n return stat(full).then(s => s.mtimeMs, () => null)\n }))\n const known = mtimes.filter((m): m is number => m !== null)\n return known.length > 0 ? Math.max(...known) : null\n}\n\nasync function findOrphanContent(projectRoot: string): Promise<string[]> {\n const crDir = contentrainDir(projectRoot)\n const models = await listModels(projectRoot)\n const orphans: string[] = []\n\n const knownContentDirs = new Set<string>()\n for (const m of models) {\n const full = await readModel(projectRoot, m.id)\n const modelForPath = full\n ? {\n ...full,\n content_path: full.content_path ?? (m as { content_path?: string }).content_path,\n }\n : {\n id: m.id,\n name: m.id,\n kind: m.kind,\n domain: m.domain,\n i18n: m.i18n,\n fields: {},\n content_path: (m as { content_path?: string }).content_path,\n }\n knownContentDirs.add(resolveContentDir(projectRoot, modelForPath))\n }\n\n const contentDir = join(crDir, 'content')\n if (await pathExists(contentDir)) {\n const domains = await readDir(contentDir)\n for (const domain of domains) {\n const domainDir = join(contentDir, domain)\n const entries = await readDir(domainDir)\n for (const entry of entries) {\n if (entry === '.gitkeep') continue\n const entryDir = join(domainDir, entry)\n if (!knownContentDirs.has(entryDir)) {\n orphans.push(`${domain}/${entry}`)\n }\n }\n }\n }\n\n for (const dir of knownContentDirs) {\n if (dir.startsWith(contentDir)) continue\n if (!await pathExists(dir)) {\n orphans.push(`(missing custom path) ${dir}`)\n continue\n }\n await readDir(dir)\n }\n\n return orphans\n}\n\nasync function analyzeUnusedKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<UnusedKeyEntry[]> {\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, { paths: sourceDirs })\n if (files.length === 0) return []\n\n const chunks = await Promise.all(\n files.map(async (relPath) => {\n const content = await readText(join(projectRoot, relPath))\n return content ?? ''\n }),\n )\n const allSource = chunks.join('\\n')\n\n // Dictionaries only.\n //\n // A dictionary key IS the reference — source code writes `t('nav.home')`, so\n // a key absent from source is genuinely dead. Collection entry IDs and\n // document slugs are not: entries are fetched by query, and a hex id like\n // `0a1b2c3d4e5f` will never appear in a source file. Checking them produced\n // a report that was mostly noise — of 134 warnings on a real project, ~120\n // were collection and document rows, and the real findings were lost among\n // them.\n const models = (await listModels(projectRoot)).filter(m => m.kind === 'dictionary')\n const defaultLocale = config.locales.default\n const unused: UnusedKeyEntry[] = []\n\n for (const m of models) {\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const keys = await extractContentKeys(projectRoot, fullModel, defaultLocale)\n for (const key of keys) {\n if (!allSource.includes(key)) {\n unused.push({ model: m.id, kind: m.kind, key, locale: defaultLocale })\n }\n }\n }\n\n return unused\n}\n\n/**\n * Keys of a model that source code could plausibly reference by name.\n *\n * Only dictionaries have any: their keys are semantic addresses that appear\n * verbatim in source. Every other kind is queried, so its ids and slugs are\n * data, not identifiers, and looking for them in source finds nothing and\n * means nothing.\n */\nasync function extractContentKeys(\n projectRoot: string,\n model: ModelDefinition,\n locale: string,\n): Promise<string[]> {\n const cDir = resolveContentDir(projectRoot, model)\n if (!await pathExists(cDir)) return []\n\n switch (model.kind) {\n case 'dictionary': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, string>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'collection': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, Record<string, unknown>>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'document': {\n const strategy = resolveLocaleStrategy(model)\n const slugs: string[] = []\n if (!model.i18n) {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n } else if (strategy === 'file') {\n const dirs = await readDir(cDir)\n for (const d of dirs) if (!d.startsWith('.')) slugs.push(d)\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) if (f.endsWith(suffix)) slugs.push(f.slice(0, -suffix.length))\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n if (await pathExists(localeDir)) {\n const files = await readDir(localeDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n } else {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n return slugs\n }\n case 'singleton':\n return []\n default:\n return []\n }\n}\n\nasync function analyzeDuplicateValues(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<DuplicateValueEntry[]> {\n const models = await listModels(projectRoot)\n const result: DuplicateValueEntry[] = []\n\n for (const m of models) {\n if (m.kind !== 'dictionary') continue\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n for (const locale of config.locales.supported) {\n const filePath = resolveJsonFilePath(cDir, fullModel, locale)\n const data = await readJson<Record<string, string>>(filePath)\n if (!data) continue\n\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n\n for (const [value, keys] of valueToKeys) {\n if (keys.length > 1) {\n result.push({ model: m.id, locale, value, keys })\n }\n }\n }\n }\n\n return result\n}\n\nasync function analyzeMissingLocaleKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<MissingLocaleEntry[]> {\n if (config.locales.supported.length < 2) return []\n\n const models = await listModels(projectRoot)\n const result: MissingLocaleEntry[] = []\n const defaultLocale = config.locales.default\n const otherLocales = config.locales.supported.filter(l => l !== defaultLocale)\n\n for (const m of models) {\n if (m.kind !== 'dictionary' && m.kind !== 'collection') continue\n if (!m.i18n) continue\n\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n const defaultPath = resolveJsonFilePath(cDir, fullModel, defaultLocale)\n const defaultData = await readJson<Record<string, unknown>>(defaultPath)\n if (!defaultData) continue\n const defaultKeys = new Set(Object.keys(defaultData))\n\n for (const locale of otherLocales) {\n const localePath = resolveJsonFilePath(cDir, fullModel, locale)\n const localeData = await readJson<Record<string, unknown>>(localePath)\n const localeKeys = localeData ? new Set(Object.keys(localeData)) : new Set<string>()\n\n for (const key of defaultKeys) {\n if (!localeKeys.has(key)) {\n result.push({ model: m.id, key, missingIn: locale })\n }\n }\n }\n }\n\n return result\n}\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { n as mapProviderError } from "./errors-DLkAWnzQ.mjs";
2
+ //#region src/git/errors.ts
3
+ /**
4
+ * Error normalization for git-backed MCP operations.
5
+ *
6
+ * `simple-git` surfaces a failed `git commit`/`push`/`merge` as a `GitError`
7
+ * whose `message` is the raw subprocess stderr — which, for hook tooling
8
+ * (commitlint, husky, lefthook), is colorized multi-line text. Returning that
9
+ * verbatim inside a JSON error produces an unreadable ANSI-escaped blob.
10
+ *
11
+ * This helper strips ANSI, preserves any structured fields the thrown error
12
+ * already carries (`code`/`agent_hint`/`developer_action`), and detects git
13
+ * hook rejections so the agent gets an actionable, structured envelope.
14
+ */
15
+ const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
16
+ function stripAnsi(input) {
17
+ return input.replace(ANSI_RE, "");
18
+ }
19
+ /**
20
+ * Normalize any thrown error into a structured, ANSI-free envelope for MCP
21
+ * tool responses. `stage` labels where it happened (e.g. 'content_save').
22
+ */
23
+ function normalizeOperationError(err, stage) {
24
+ const e = err;
25
+ const message = stripAnsi(e?.message ?? String(err)).trim();
26
+ const out = { error: message };
27
+ if (stage) out.stage = stage;
28
+ if (e?.code) out.code = e.code;
29
+ if (e?.agent_hint) out.agent_hint = e.agent_hint;
30
+ if (e?.developer_action) out.developer_action = e.developer_action;
31
+ if (!out.code) {
32
+ const mapped = mapProviderError(err);
33
+ if (mapped) {
34
+ out.error = mapped.error;
35
+ out.code = mapped.code;
36
+ out.agent_hint = mapped.agent_hint;
37
+ if (mapped.developer_action) out.developer_action = mapped.developer_action;
38
+ return out;
39
+ }
40
+ }
41
+ if (!out.code) {
42
+ const hookName = /\b(commit-msg|pre-commit|prepare-commit-msg|pre-push)\b/i.exec(message)?.[1];
43
+ const hookTool = /\b(commitlint|husky|lefthook)\b/i.exec(message)?.[1];
44
+ if (hookName || hookTool) {
45
+ out.stage = out.stage ?? "git-commit";
46
+ out.hook = hookName ?? hookTool;
47
+ out.agent_hint = out.agent_hint ?? "A git hook rejected the Contentrain commit. Contentrain commits with --no-verify, but a wrapping CLI or server-side hook may still run. Ask the developer to allow machine-generated \"[contentrain]\" commits or to exclude cr/* branches from the hook.";
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+ //#endregion
53
+ export { stripAnsi as n, normalizeOperationError as t };
54
+
55
+ //# sourceMappingURL=errors-BmLE-JSC.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-BmLE-JSC.mjs","names":[],"sources":["../src/git/errors.ts"],"sourcesContent":["/**\n * Error normalization for git-backed MCP operations.\n *\n * `simple-git` surfaces a failed `git commit`/`push`/`merge` as a `GitError`\n * whose `message` is the raw subprocess stderr — which, for hook tooling\n * (commitlint, husky, lefthook), is colorized multi-line text. Returning that\n * verbatim inside a JSON error produces an unreadable ANSI-escaped blob.\n *\n * This helper strips ANSI, preserves any structured fields the thrown error\n * already carries (`code`/`agent_hint`/`developer_action`), and detects git\n * hook rejections so the agent gets an actionable, structured envelope.\n */\n\nimport { mapProviderError } from '../providers/shared/errors.js'\n\n// Build the ANSI matcher without a literal control char so oxlint's\n// no-control-regex rule stays happy.\nconst ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\\\[[0-9;]*m`, 'g')\n\nexport function stripAnsi(input: string): string {\n return input.replace(ANSI_RE, '')\n}\n\nexport interface NormalizedError {\n error: string\n stage?: string\n hook?: string\n code?: string\n agent_hint?: string\n developer_action?: string\n}\n\n/**\n * Normalize any thrown error into a structured, ANSI-free envelope for MCP\n * tool responses. `stage` labels where it happened (e.g. 'content_save').\n */\nexport function normalizeOperationError(err: unknown, stage?: string): NormalizedError {\n const e = err as { message?: string, code?: string, agent_hint?: string, developer_action?: string } | undefined\n const rawMessage = e?.message ?? String(err)\n const message = stripAnsi(rawMessage).trim()\n\n const out: NormalizedError = { error: message }\n if (stage) out.stage = stage\n if (e?.code) out.code = e.code\n if (e?.agent_hint) out.agent_hint = e.agent_hint\n if (e?.developer_action) out.developer_action = e.developer_action\n\n // Provider SDK rejections (Octokit, Gitbeaker) carry an HTTP status but no\n // structured fields, so without this they reached the client as a bare\n // vendor string — a documentation URL and no indication of whether the\n // operation is retryable. Mapping here rather than at each `throw` inside\n // the providers means every provider error is covered, including ones added\n // later. An error that already carries a Contentrain code keeps it.\n if (!out.code) {\n const mapped = mapProviderError(err)\n if (mapped) {\n out.error = mapped.error\n out.code = mapped.code\n out.agent_hint = mapped.agent_hint\n if (mapped.developer_action) out.developer_action = mapped.developer_action\n return out\n }\n }\n\n // Detect a git hook rejection (only when the error isn't already a\n // structured Contentrain error with its own code).\n if (!out.code) {\n const hookName = /\\b(commit-msg|pre-commit|prepare-commit-msg|pre-push)\\b/i.exec(message)?.[1]\n const hookTool = /\\b(commitlint|husky|lefthook)\\b/i.exec(message)?.[1]\n if (hookName || hookTool) {\n out.stage = out.stage ?? 'git-commit'\n out.hook = hookName ?? hookTool\n out.agent_hint = out.agent_hint\n ?? 'A git hook rejected the Contentrain commit. Contentrain commits with --no-verify, but a wrapping CLI or server-side hook may still run. Ask the developer to allow machine-generated \"[contentrain]\" commits or to exclude cr/* branches from the hook.'\n }\n }\n\n return out\n}\n"],"mappings":";;;;;;;;;;;;;;AAiBA,MAAM,UAAU,IAAI,OAAO,GAAG,OAAO,aAAa,GAAG,CAAC,cAAc,IAAI;AAExE,SAAgB,UAAU,OAAuB;AAC/C,QAAO,MAAM,QAAQ,SAAS,GAAG;;;;;;AAgBnC,SAAgB,wBAAwB,KAAc,OAAiC;CACrF,MAAM,IAAI;CAEV,MAAM,UAAU,UADG,GAAG,WAAW,OAAO,IAAI,CACP,CAAC,MAAM;CAE5C,MAAM,MAAuB,EAAE,OAAO,SAAS;AAC/C,KAAI,MAAO,KAAI,QAAQ;AACvB,KAAI,GAAG,KAAM,KAAI,OAAO,EAAE;AAC1B,KAAI,GAAG,WAAY,KAAI,aAAa,EAAE;AACtC,KAAI,GAAG,iBAAkB,KAAI,mBAAmB,EAAE;AAQlD,KAAI,CAAC,IAAI,MAAM;EACb,MAAM,SAAS,iBAAiB,IAAI;AACpC,MAAI,QAAQ;AACV,OAAI,QAAQ,OAAO;AACnB,OAAI,OAAO,OAAO;AAClB,OAAI,aAAa,OAAO;AACxB,OAAI,OAAO,iBAAkB,KAAI,mBAAmB,OAAO;AAC3D,UAAO;;;AAMX,KAAI,CAAC,IAAI,MAAM;EACb,MAAM,WAAW,2DAA2D,KAAK,QAAQ,GAAG;EAC5F,MAAM,WAAW,mCAAmC,KAAK,QAAQ,GAAG;AACpE,MAAI,YAAY,UAAU;AACxB,OAAI,QAAQ,IAAI,SAAS;AACzB,OAAI,OAAO,YAAY;AACvB,OAAI,aAAa,IAAI,cAChB;;;AAIT,QAAO"}
@@ -123,4 +123,4 @@ function mapProviderError(error) {
123
123
  //#endregion
124
124
  export { mapProviderError as n, isNotFoundError as t };
125
125
 
126
- //# sourceMappingURL=errors-CvBS12e7.mjs.map
126
+ //# sourceMappingURL=errors-DLkAWnzQ.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors-CvBS12e7.mjs","names":[],"sources":["../src/providers/shared/errors.ts"],"sourcesContent":["/**\n * Error shapes for API-backed providers, and the mapping from those shapes\n * into Contentrain's structured error envelope.\n *\n * Two common error shapes in the provider SDKs we use:\n *\n * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a\n * top-level `.status` number set to the HTTP status code.\n * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose\n * `.cause` includes `{ response: { status } }`.\n */\n\n/**\n * HTTP status behind a provider SDK rejection, or `undefined` when the error\n * is not an HTTP failure (a local git error, a programming mistake, …).\n *\n * We deliberately do NOT fall back to substring matching on the error\n * message — that leniency can silently mask other failures (forbidden repo,\n * deleted project, rate limits) and produce the wrong answer. If either SDK\n * ever stops populating the status field, the regression surfaces in tests\n * rather than being papered over at the reader layer.\n */\nexport function extractHttpStatus(error: unknown): number | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const direct = (error as { status?: number }).status\n if (typeof direct === 'number') return direct\n const nested = (error as { cause?: { response?: { status?: number } } }).cause?.response?.status\n return typeof nested === 'number' ? nested : undefined\n}\n\n/** Unified \"is this a 404?\" helper for API-backed providers. */\nexport function isNotFoundError(error: unknown): boolean {\n return extractHttpStatus(error) === 404\n}\n\n/**\n * Provider SDK messages are written for a human reading a terminal, not for an\n * agent deciding what to do next. Octokit appends a documentation URL to most\n * messages and embeds the raw JSON response body on validation failures.\n *\n * The vendor text still carries the only specifics we have (\"Reference already\n * exists\", which field failed), so it is kept as a parenthetical detail rather\n * than discarded — but the doc links are noise for an agent, and an embedded\n * body can be arbitrarily long, so URLs are stripped and the result is capped.\n */\nfunction sanitiseDetail(error: unknown): string {\n const raw = (error as { message?: string } | undefined)?.message ?? String(error)\n const withoutUrls = raw.replace(/\\s*-?\\s*https?:\\/\\/\\S+/g, '')\n const collapsed = withoutUrls.replace(/\\s+/g, ' ').trim()\n return collapsed.length > 200 ? `${collapsed.slice(0, 197)}…` : collapsed\n}\n\n/**\n * GitHub signals rate limiting with 403 plus an exhausted remaining-quota\n * header (secondary limits use 403 too), and with 429 on some endpoints.\n * Checking the header first keeps a genuine permission 403 from being\n * reported as a rate limit.\n */\nfunction isRateLimited(error: unknown, status: number): boolean {\n if (status === 429) return true\n if (status !== 403) return false\n const headers = (error as { response?: { headers?: Record<string, unknown> } }).response?.headers\n const remaining = headers?.['x-ratelimit-remaining']\n if (remaining !== undefined) return String(remaining) === '0'\n return /\\brate limit\\b/i.test((error as { message?: string }).message ?? '')\n}\n\nexport interface ProviderErrorInfo {\n error: string\n code: string\n agent_hint: string\n developer_action?: string\n}\n\n/**\n * Map a provider SDK rejection onto the same structured envelope the local git\n * paths already produce (`code` + `agent_hint` + `developer_action`). Returns\n * `undefined` for anything that is not a recognisable provider HTTP failure,\n * so local git errors and ordinary exceptions pass through untouched.\n */\nexport function mapProviderError(error: unknown): ProviderErrorInfo | undefined {\n const status = extractHttpStatus(error)\n if (status === undefined) return undefined\n\n const detail = sanitiseDetail(error)\n const withDetail = (summary: string): string => (detail ? `${summary} (${detail})` : summary)\n\n if (isRateLimited(error, status)) {\n return {\n error: withDetail('The git provider rejected the request because the API rate limit is exhausted.'),\n code: 'PROVIDER_RATE_LIMITED',\n agent_hint: 'Do not retry immediately — the limit is time-based. Stop the current operation and report it to the developer.',\n developer_action: 'Wait for the rate-limit window to reset, or use a token with a higher quota.',\n }\n }\n\n switch (status) {\n case 401:\n return {\n error: withDetail('The git provider rejected the credentials.'),\n code: 'PROVIDER_UNAUTHORIZED',\n agent_hint: 'The token is missing, malformed, or expired. Not retryable — ask the developer to fix the credentials.',\n developer_action: 'Check the provider token configured for this Contentrain server and reissue it if it has expired.',\n }\n case 403:\n return {\n error: withDetail('The git provider denied access to this resource.'),\n code: 'PROVIDER_FORBIDDEN',\n agent_hint: 'The credentials are valid but lack permission for this operation. Not retryable — report which operation was refused.',\n developer_action: 'Grant the token write access to the repository, or check whether branch protection or SSO authorisation is blocking it.',\n }\n case 404:\n return {\n error: withDetail('The git provider could not find the requested resource.'),\n code: 'PROVIDER_NOT_FOUND',\n agent_hint: 'The repository, branch, or path does not exist — or the token cannot see it, which also returns 404. Verify the target before retrying.',\n developer_action: 'Confirm the repository and branch names in the Contentrain config, and that the token can reach a private repository.',\n }\n case 409:\n return {\n error: withDetail('The git provider reported a conflict.'),\n code: 'PROVIDER_CONFLICT',\n agent_hint: 'The branch moved between read and write. Re-read the current state and rebuild the change before retrying once.',\n developer_action: 'Usually a concurrent write to the same branch; retrying the operation normally resolves it.',\n }\n case 422:\n return {\n error: withDetail('The git provider rejected the request as invalid.'),\n code: 'PROVIDER_VALIDATION_FAILED',\n agent_hint: 'Well-formed but semantically rejected (e.g. the ref already exists, or a name is invalid). Do not retry unchanged.',\n developer_action: 'Inspect the detail above — a stale branch left over from an earlier run is the common cause.',\n }\n default:\n if (status >= 500) {\n return {\n error: withDetail('The git provider is unavailable.'),\n code: 'PROVIDER_UNAVAILABLE',\n agent_hint: 'A server-side failure on the provider. Safe to retry once after a short pause; if it persists, stop and report it.',\n developer_action: 'Check the provider status page.',\n }\n }\n return {\n error: withDetail(`The git provider returned HTTP ${status}.`),\n code: 'PROVIDER_REQUEST_FAILED',\n agent_hint: 'An unmapped provider failure. Report the status and detail to the developer rather than retrying blindly.',\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,kBAAkB,OAAoC;AACpE,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO,KAAA;CACxD,MAAM,SAAU,MAA8B;AAC9C,KAAI,OAAO,WAAW,SAAU,QAAO;CACvC,MAAM,SAAU,MAAyD,OAAO,UAAU;AAC1F,QAAO,OAAO,WAAW,WAAW,SAAS,KAAA;;;AAI/C,SAAgB,gBAAgB,OAAyB;AACvD,QAAO,kBAAkB,MAAM,KAAK;;;;;;;;;;;;AAatC,SAAS,eAAe,OAAwB;CAG9C,MAAM,aAFO,OAA4C,WAAW,OAAO,MAAM,EACzD,QAAQ,2BAA2B,GAAG,CAChC,QAAQ,QAAQ,IAAI,CAAC,MAAM;AACzD,QAAO,UAAU,SAAS,MAAM,GAAG,UAAU,MAAM,GAAG,IAAI,CAAC,KAAK;;;;;;;;AASlE,SAAS,cAAc,OAAgB,QAAyB;AAC9D,KAAI,WAAW,IAAK,QAAO;AAC3B,KAAI,WAAW,IAAK,QAAO;CAE3B,MAAM,aADW,MAA+D,UAAU,WAC9D;AAC5B,KAAI,cAAc,KAAA,EAAW,QAAO,OAAO,UAAU,KAAK;AAC1D,QAAO,kBAAkB,KAAM,MAA+B,WAAW,GAAG;;;;;;;;AAgB9E,SAAgB,iBAAiB,OAA+C;CAC9E,MAAM,SAAS,kBAAkB,MAAM;AACvC,KAAI,WAAW,KAAA,EAAW,QAAO,KAAA;CAEjC,MAAM,SAAS,eAAe,MAAM;CACpC,MAAM,cAAc,YAA6B,SAAS,GAAG,QAAQ,IAAI,OAAO,KAAK;AAErF,KAAI,cAAc,OAAO,OAAO,CAC9B,QAAO;EACL,OAAO,WAAW,iFAAiF;EACnG,MAAM;EACN,YAAY;EACZ,kBAAkB;EACnB;AAGH,SAAQ,QAAR;EACE,KAAK,IACH,QAAO;GACL,OAAO,WAAW,6CAA6C;GAC/D,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,mDAAmD;GACrE,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,0DAA0D;GAC5E,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,wCAAwC;GAC1D,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,oDAAoD;GACtE,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH;AACE,OAAI,UAAU,IACZ,QAAO;IACL,OAAO,WAAW,mCAAmC;IACrD,MAAM;IACN,YAAY;IACZ,kBAAkB;IACnB;AAEH,UAAO;IACL,OAAO,WAAW,kCAAkC,OAAO,GAAG;IAC9D,MAAM;IACN,YAAY;IACb"}
1
+ {"version":3,"file":"errors-DLkAWnzQ.mjs","names":[],"sources":["../src/providers/shared/errors.ts"],"sourcesContent":["/**\n * Error shapes for API-backed providers, and the mapping from those shapes\n * into Contentrain's structured error envelope.\n *\n * Two common error shapes in the provider SDKs we use:\n *\n * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a\n * top-level `.status` number set to the HTTP status code.\n * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose\n * `.cause` includes `{ response: { status } }`.\n */\n\n/**\n * HTTP status behind a provider SDK rejection, or `undefined` when the error\n * is not an HTTP failure (a local git error, a programming mistake, …).\n *\n * We deliberately do NOT fall back to substring matching on the error\n * message — that leniency can silently mask other failures (forbidden repo,\n * deleted project, rate limits) and produce the wrong answer. If either SDK\n * ever stops populating the status field, the regression surfaces in tests\n * rather than being papered over at the reader layer.\n */\nexport function extractHttpStatus(error: unknown): number | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const direct = (error as { status?: number }).status\n if (typeof direct === 'number') return direct\n const nested = (error as { cause?: { response?: { status?: number } } }).cause?.response?.status\n return typeof nested === 'number' ? nested : undefined\n}\n\n/** Unified \"is this a 404?\" helper for API-backed providers. */\nexport function isNotFoundError(error: unknown): boolean {\n return extractHttpStatus(error) === 404\n}\n\n/**\n * Provider SDK messages are written for a human reading a terminal, not for an\n * agent deciding what to do next. Octokit appends a documentation URL to most\n * messages and embeds the raw JSON response body on validation failures.\n *\n * The vendor text still carries the only specifics we have (\"Reference already\n * exists\", which field failed), so it is kept as a parenthetical detail rather\n * than discarded — but the doc links are noise for an agent, and an embedded\n * body can be arbitrarily long, so URLs are stripped and the result is capped.\n */\nfunction sanitiseDetail(error: unknown): string {\n const raw = (error as { message?: string } | undefined)?.message ?? String(error)\n const withoutUrls = raw.replace(/\\s*-?\\s*https?:\\/\\/\\S+/g, '')\n const collapsed = withoutUrls.replace(/\\s+/g, ' ').trim()\n return collapsed.length > 200 ? `${collapsed.slice(0, 197)}…` : collapsed\n}\n\n/**\n * GitHub signals rate limiting with 403 plus an exhausted remaining-quota\n * header (secondary limits use 403 too), and with 429 on some endpoints.\n * Checking the header first keeps a genuine permission 403 from being\n * reported as a rate limit.\n */\nfunction isRateLimited(error: unknown, status: number): boolean {\n if (status === 429) return true\n if (status !== 403) return false\n const headers = (error as { response?: { headers?: Record<string, unknown> } }).response?.headers\n const remaining = headers?.['x-ratelimit-remaining']\n if (remaining !== undefined) return String(remaining) === '0'\n return /\\brate limit\\b/i.test((error as { message?: string }).message ?? '')\n}\n\nexport interface ProviderErrorInfo {\n error: string\n code: string\n agent_hint: string\n developer_action?: string\n}\n\n/**\n * Map a provider SDK rejection onto the same structured envelope the local git\n * paths already produce (`code` + `agent_hint` + `developer_action`). Returns\n * `undefined` for anything that is not a recognisable provider HTTP failure,\n * so local git errors and ordinary exceptions pass through untouched.\n */\nexport function mapProviderError(error: unknown): ProviderErrorInfo | undefined {\n const status = extractHttpStatus(error)\n if (status === undefined) return undefined\n\n const detail = sanitiseDetail(error)\n const withDetail = (summary: string): string => (detail ? `${summary} (${detail})` : summary)\n\n if (isRateLimited(error, status)) {\n return {\n error: withDetail('The git provider rejected the request because the API rate limit is exhausted.'),\n code: 'PROVIDER_RATE_LIMITED',\n agent_hint: 'Do not retry immediately — the limit is time-based. Stop the current operation and report it to the developer.',\n developer_action: 'Wait for the rate-limit window to reset, or use a token with a higher quota.',\n }\n }\n\n switch (status) {\n case 401:\n return {\n error: withDetail('The git provider rejected the credentials.'),\n code: 'PROVIDER_UNAUTHORIZED',\n agent_hint: 'The token is missing, malformed, or expired. Not retryable — ask the developer to fix the credentials.',\n developer_action: 'Check the provider token configured for this Contentrain server and reissue it if it has expired.',\n }\n case 403:\n return {\n error: withDetail('The git provider denied access to this resource.'),\n code: 'PROVIDER_FORBIDDEN',\n agent_hint: 'The credentials are valid but lack permission for this operation. Not retryable — report which operation was refused.',\n developer_action: 'Grant the token write access to the repository, or check whether branch protection or SSO authorisation is blocking it.',\n }\n case 404:\n return {\n error: withDetail('The git provider could not find the requested resource.'),\n code: 'PROVIDER_NOT_FOUND',\n agent_hint: 'The repository, branch, or path does not exist — or the token cannot see it, which also returns 404. Verify the target before retrying.',\n developer_action: 'Confirm the repository and branch names in the Contentrain config, and that the token can reach a private repository.',\n }\n case 409:\n return {\n error: withDetail('The git provider reported a conflict.'),\n code: 'PROVIDER_CONFLICT',\n agent_hint: 'The branch moved between read and write. Re-read the current state and rebuild the change before retrying once.',\n developer_action: 'Usually a concurrent write to the same branch; retrying the operation normally resolves it.',\n }\n case 422:\n return {\n error: withDetail('The git provider rejected the request as invalid.'),\n code: 'PROVIDER_VALIDATION_FAILED',\n agent_hint: 'Well-formed but semantically rejected (e.g. the ref already exists, or a name is invalid). Do not retry unchanged.',\n developer_action: 'Inspect the detail above — a stale branch left over from an earlier run is the common cause.',\n }\n default:\n if (status >= 500) {\n return {\n error: withDetail('The git provider is unavailable.'),\n code: 'PROVIDER_UNAVAILABLE',\n agent_hint: 'A server-side failure on the provider. Safe to retry once after a short pause; if it persists, stop and report it.',\n developer_action: 'Check the provider status page.',\n }\n }\n return {\n error: withDetail(`The git provider returned HTTP ${status}.`),\n code: 'PROVIDER_REQUEST_FAILED',\n agent_hint: 'An unmapped provider failure. Report the status and detail to the developer rather than retrying blindly.',\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,kBAAkB,OAAoC;AACpE,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO,KAAA;CACxD,MAAM,SAAU,MAA8B;AAC9C,KAAI,OAAO,WAAW,SAAU,QAAO;CACvC,MAAM,SAAU,MAAyD,OAAO,UAAU;AAC1F,QAAO,OAAO,WAAW,WAAW,SAAS,KAAA;;;AAI/C,SAAgB,gBAAgB,OAAyB;AACvD,QAAO,kBAAkB,MAAM,KAAK;;;;;;;;;;;;AAatC,SAAS,eAAe,OAAwB;CAG9C,MAAM,aAFO,OAA4C,WAAW,OAAO,MAAM,EACzD,QAAQ,2BAA2B,GAAG,CAChC,QAAQ,QAAQ,IAAI,CAAC,MAAM;AACzD,QAAO,UAAU,SAAS,MAAM,GAAG,UAAU,MAAM,GAAG,IAAI,CAAC,KAAK;;;;;;;;AASlE,SAAS,cAAc,OAAgB,QAAyB;AAC9D,KAAI,WAAW,IAAK,QAAO;AAC3B,KAAI,WAAW,IAAK,QAAO;CAE3B,MAAM,aADW,MAA+D,UAAU,WAC9D;AAC5B,KAAI,cAAc,KAAA,EAAW,QAAO,OAAO,UAAU,KAAK;AAC1D,QAAO,kBAAkB,KAAM,MAA+B,WAAW,GAAG;;;;;;;;AAgB9E,SAAgB,iBAAiB,OAA+C;CAC9E,MAAM,SAAS,kBAAkB,MAAM;AACvC,KAAI,WAAW,KAAA,EAAW,QAAO,KAAA;CAEjC,MAAM,SAAS,eAAe,MAAM;CACpC,MAAM,cAAc,YAA6B,SAAS,GAAG,QAAQ,IAAI,OAAO,KAAK;AAErF,KAAI,cAAc,OAAO,OAAO,CAC9B,QAAO;EACL,OAAO,WAAW,iFAAiF;EACnG,MAAM;EACN,YAAY;EACZ,kBAAkB;EACnB;AAGH,SAAQ,QAAR;EACE,KAAK,IACH,QAAO;GACL,OAAO,WAAW,6CAA6C;GAC/D,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,mDAAmD;GACrE,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,0DAA0D;GAC5E,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,wCAAwC;GAC1D,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,oDAAoD;GACtE,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH;AACE,OAAI,UAAU,IACZ,QAAO;IACL,OAAO,WAAW,mCAAmC;IACrD,MAAM;IACN,YAAY;IACZ,kBAAkB;IACnB;AAEH,UAAO;IACL,OAAO,WAAW,kCAAkC,OAAO,GAAG;IAC9D,MAAM;IACN,YAAY;IACb"}
@@ -49,4 +49,4 @@ function contentrainDir(projectRoot) {
49
49
  //#endregion
50
50
  export { readJson as a, writeText as c, readDir as i, ensureDir as n, readText as o, pathExists as r, writeJson as s, contentrainDir as t };
51
51
 
52
- //# sourceMappingURL=fs-DLbVB-Ek.mjs.map
52
+ //# sourceMappingURL=fs-DJv5G8uL.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"fs-DLbVB-Ek.mjs","names":[],"sources":["../src/util/fs.ts"],"sourcesContent":["import { readFile, readdir, access, writeFile, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { canonicalStringify } from '@contentrain/types'\n\nexport async function pathExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath)\n return true\n } catch {\n return false\n }\n}\n\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await readFile(filePath, 'utf-8')\n return JSON.parse(raw) as T\n } catch {\n return null\n }\n}\n\nexport async function readDir(dirPath: string): Promise<string[]> {\n try {\n return await readdir(dirPath)\n } catch {\n return []\n }\n}\n\nexport async function writeJson(filePath: string, data: unknown, fieldOrder?: readonly string[]): Promise<void> {\n await mkdir(join(filePath, '..'), { recursive: true })\n await writeFile(filePath, canonicalStringify(data, fieldOrder), 'utf-8')\n}\n\nexport async function ensureDir(dirPath: string): Promise<void> {\n await mkdir(dirPath, { recursive: true })\n}\n\nexport async function readText(filePath: string): Promise<string | null> {\n try {\n return await readFile(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n\nexport async function writeText(filePath: string, content: string): Promise<void> {\n await mkdir(join(filePath, '..'), { recursive: true })\n await writeFile(filePath, content, 'utf-8')\n}\n\nexport function contentrainDir(projectRoot: string): string {\n return join(projectRoot, '.contentrain')\n}\n"],"mappings":";;;;AAIA,eAAsB,WAAW,UAAoC;AACnE,KAAI;AACF,QAAM,OAAO,SAAS;AACtB,SAAO;SACD;AACN,SAAO;;;AAIX,eAAsB,SAAY,UAAqC;AACrE,KAAI;EACF,MAAM,MAAM,MAAM,SAAS,UAAU,QAAQ;AAC7C,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;AAIX,eAAsB,QAAQ,SAAoC;AAChE,KAAI;AACF,SAAO,MAAM,QAAQ,QAAQ;SACvB;AACN,SAAO,EAAE;;;AAIb,eAAsB,UAAU,UAAkB,MAAe,YAA+C;AAC9G,OAAM,MAAM,KAAK,UAAU,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACtD,OAAM,UAAU,UAAU,mBAAmB,MAAM,WAAW,EAAE,QAAQ;;AAG1E,eAAsB,UAAU,SAAgC;AAC9D,OAAM,MAAM,SAAS,EAAE,WAAW,MAAM,CAAC;;AAG3C,eAAsB,SAAS,UAA0C;AACvE,KAAI;AACF,SAAO,MAAM,SAAS,UAAU,QAAQ;SAClC;AACN,SAAO;;;AAIX,eAAsB,UAAU,UAAkB,SAAgC;AAChF,OAAM,MAAM,KAAK,UAAU,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACtD,OAAM,UAAU,UAAU,SAAS,QAAQ;;AAG7C,SAAgB,eAAe,aAA6B;AAC1D,QAAO,KAAK,aAAa,eAAe"}
1
+ {"version":3,"file":"fs-DJv5G8uL.mjs","names":[],"sources":["../src/util/fs.ts"],"sourcesContent":["import { readFile, readdir, access, writeFile, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { canonicalStringify } from '@contentrain/types'\n\nexport async function pathExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath)\n return true\n } catch {\n return false\n }\n}\n\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await readFile(filePath, 'utf-8')\n return JSON.parse(raw) as T\n } catch {\n return null\n }\n}\n\nexport async function readDir(dirPath: string): Promise<string[]> {\n try {\n return await readdir(dirPath)\n } catch {\n return []\n }\n}\n\nexport async function writeJson(filePath: string, data: unknown, fieldOrder?: readonly string[]): Promise<void> {\n await mkdir(join(filePath, '..'), { recursive: true })\n await writeFile(filePath, canonicalStringify(data, fieldOrder), 'utf-8')\n}\n\nexport async function ensureDir(dirPath: string): Promise<void> {\n await mkdir(dirPath, { recursive: true })\n}\n\nexport async function readText(filePath: string): Promise<string | null> {\n try {\n return await readFile(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n\nexport async function writeText(filePath: string, content: string): Promise<void> {\n await mkdir(join(filePath, '..'), { recursive: true })\n await writeFile(filePath, content, 'utf-8')\n}\n\nexport function contentrainDir(projectRoot: string): string {\n return join(projectRoot, '.contentrain')\n}\n"],"mappings":";;;;AAIA,eAAsB,WAAW,UAAoC;AACnE,KAAI;AACF,QAAM,OAAO,SAAS;AACtB,SAAO;SACD;AACN,SAAO;;;AAIX,eAAsB,SAAY,UAAqC;AACrE,KAAI;EACF,MAAM,MAAM,MAAM,SAAS,UAAU,QAAQ;AAC7C,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;AAIX,eAAsB,QAAQ,SAAoC;AAChE,KAAI;AACF,SAAO,MAAM,QAAQ,QAAQ;SACvB;AACN,SAAO,EAAE;;;AAIb,eAAsB,UAAU,UAAkB,MAAe,YAA+C;AAC9G,OAAM,MAAM,KAAK,UAAU,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACtD,OAAM,UAAU,UAAU,mBAAmB,MAAM,WAAW,EAAE,QAAQ;;AAG1E,eAAsB,UAAU,SAAgC;AAC9D,OAAM,MAAM,SAAS,EAAE,WAAW,MAAM,CAAC;;AAG3C,eAAsB,SAAS,UAA0C;AACvE,KAAI;AACF,SAAO,MAAM,SAAS,UAAU,QAAQ;SAClC;AACN,SAAO;;;AAIX,eAAsB,UAAU,UAAkB,SAAgC;AAChF,OAAM,MAAM,KAAK,UAAU,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACtD,OAAM,UAAU,UAAU,SAAS,QAAQ;;AAG7C,SAAgB,eAAe,aAA6B;AAC1D,QAAO,KAAK,aAAa,eAAe"}
@@ -1,2 +1,2 @@
1
- import { a as RemoteDeleteResult, c as checkBranchHealth, d as deleteRemoteBranch, f as isRefMerged, i as RemoteBranchList, l as classifyMergedBranches, m as pruneMergedRemoteBranches, n as BranchHealthCheck, o as RemotePruneResult, p as listRemoteCrBranches, r as CleanupResult, s as branchDiff, t as BranchDiffResult, u as cleanupMergedBranches } from "../branch-lifecycle-DhzHfzs3.mjs";
2
- export { BranchDiffResult, BranchHealthCheck, CleanupResult, RemoteBranchList, RemoteDeleteResult, RemotePruneResult, branchDiff, checkBranchHealth, classifyMergedBranches, cleanupMergedBranches, deleteRemoteBranch, isRefMerged, listRemoteCrBranches, pruneMergedRemoteBranches };
1
+ import { _ as pruneMergedRemoteBranches, a as RemoteBranchList, c as branchDiff, d as cleanupMergedBranches, f as contentBranchRelation, g as networkGit, h as listRemoteCrBranches, i as ContentBranchRelation, l as checkBranchHealth, m as isRefMerged, n as BranchHealthCheck, o as RemoteDeleteResult, p as deleteRemoteBranch, r as CleanupResult, s as RemotePruneResult, t as BranchDiffResult, u as classifyMergedBranches } from "../branch-lifecycle-DGlgjnC7.mjs";
2
+ export { BranchDiffResult, BranchHealthCheck, CleanupResult, ContentBranchRelation, RemoteBranchList, RemoteDeleteResult, RemotePruneResult, branchDiff, checkBranchHealth, classifyMergedBranches, cleanupMergedBranches, contentBranchRelation, deleteRemoteBranch, isRefMerged, listRemoteCrBranches, networkGit, pruneMergedRemoteBranches };
@@ -1,5 +1,5 @@
1
- import "../fs-DLbVB-Ek.mjs";
2
- import "../identity-D8QoGxhT.mjs";
3
- import "../config-VlRKRwAS.mjs";
4
- import { a as deleteRemoteBranch, c as pruneMergedRemoteBranches, i as cleanupMergedBranches, n as checkBranchHealth, o as isRefMerged, r as classifyMergedBranches, s as listRemoteCrBranches, t as branchDiff } from "../branch-lifecycle-B6PXZ2N7.mjs";
5
- export { branchDiff, checkBranchHealth, classifyMergedBranches, cleanupMergedBranches, deleteRemoteBranch, isRefMerged, listRemoteCrBranches, pruneMergedRemoteBranches };
1
+ import "../fs-DJv5G8uL.mjs";
2
+ import "../config-B8a4JBT1.mjs";
3
+ import "../identity-CxAB9Qb8.mjs";
4
+ import { a as contentBranchRelation, c as listRemoteCrBranches, i as cleanupMergedBranches, l as networkGit, n as checkBranchHealth, o as deleteRemoteBranch, r as classifyMergedBranches, s as isRefMerged, t as branchDiff, u as pruneMergedRemoteBranches } from "../branch-lifecycle-DUOoyNWn.mjs";
5
+ export { branchDiff, checkBranchHealth, classifyMergedBranches, cleanupMergedBranches, contentBranchRelation, deleteRemoteBranch, isRefMerged, listRemoteCrBranches, networkGit, pruneMergedRemoteBranches };
@@ -0,0 +1,30 @@
1
+ //#region src/git/errors.d.ts
2
+ /**
3
+ * Error normalization for git-backed MCP operations.
4
+ *
5
+ * `simple-git` surfaces a failed `git commit`/`push`/`merge` as a `GitError`
6
+ * whose `message` is the raw subprocess stderr — which, for hook tooling
7
+ * (commitlint, husky, lefthook), is colorized multi-line text. Returning that
8
+ * verbatim inside a JSON error produces an unreadable ANSI-escaped blob.
9
+ *
10
+ * This helper strips ANSI, preserves any structured fields the thrown error
11
+ * already carries (`code`/`agent_hint`/`developer_action`), and detects git
12
+ * hook rejections so the agent gets an actionable, structured envelope.
13
+ */
14
+ declare function stripAnsi(input: string): string;
15
+ interface NormalizedError {
16
+ error: string;
17
+ stage?: string;
18
+ hook?: string;
19
+ code?: string;
20
+ agent_hint?: string;
21
+ developer_action?: string;
22
+ }
23
+ /**
24
+ * Normalize any thrown error into a structured, ANSI-free envelope for MCP
25
+ * tool responses. `stage` labels where it happened (e.g. 'content_save').
26
+ */
27
+ declare function normalizeOperationError(err: unknown, stage?: string): NormalizedError;
28
+ //#endregion
29
+ export { NormalizedError, normalizeOperationError, stripAnsi };
30
+ //# sourceMappingURL=errors.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/git/errors.ts"],"mappings":";;AAmBA;;;;;AAIA;;;;;;iBAJgB,SAAA,CAAU,KAAA;AAAA,UAIT,eAAA;EACf,KAAA;EACA,KAAA;EACA,IAAA;EACA,IAAA;EACA,UAAA;EACA,gBAAA;AAAA;;;;;iBAOc,uBAAA,CAAwB,GAAA,WAAc,KAAA,YAAiB,eAAA"}
@@ -0,0 +1,2 @@
1
+ import { n as stripAnsi, t as normalizeOperationError } from "../errors-BmLE-JSC.mjs";
2
+ export { normalizeOperationError, stripAnsi };
@@ -1,2 +1,2 @@
1
- import { a as resetGitBinary, i as gitBinary, n as authorConfig, r as createGit, t as NETWORK_UNSAFE } from "../identity-D8QoGxhT.mjs";
1
+ import { a as resetGitBinary, i as gitBinary, n as authorConfig, r as createGit, t as NETWORK_UNSAFE } from "../identity-CxAB9Qb8.mjs";
2
2
  export { NETWORK_UNSAFE, authorConfig, createGit, gitBinary, resetGitBinary };
@@ -0,0 +1,49 @@
1
+ import { r as ReconcilePlan } from "../index-_HJVkDJa.mjs";
2
+ import { BaseAdvance, ConflictResolution, ContextSource, RemotePush, SyncResult } from "@contentrain/types";
3
+
4
+ //#region src/git/reconcile.d.ts
5
+ interface ReconcileBranchesOptions {
6
+ /** Preview only — plan, never touch a ref. Default true. */
7
+ dryRun?: boolean;
8
+ /** Decisions for previously reported conflicts, matched by id. */
9
+ resolutions?: ConflictResolution[];
10
+ /** Stamped into the regenerated context.json. */
11
+ source?: ContextSource;
12
+ }
13
+ interface ReconcileBranchesResult {
14
+ /**
15
+ * `in_sync` — nothing to reconcile (base already contained in contentrain
16
+ * and no content differences; a base merely behind was fast-forwarded).
17
+ * `preview` — dry-run: the plan says what WOULD happen.
18
+ * `conflicts` — unresolved conflicts remain; nothing was written.
19
+ * `reconciled` — the merge commit landed and the base branch advanced.
20
+ */
21
+ action: 'in_sync' | 'preview' | 'conflicts' | 'reconciled';
22
+ plan: ReconcilePlan;
23
+ base: string;
24
+ merge_base: string | null;
25
+ commit?: string;
26
+ base_advance?: BaseAdvance;
27
+ sync?: SyncResult;
28
+ remote_push?: RemotePush;
29
+ }
30
+ /**
31
+ * Content-aware reconcile of a diverged `contentrain` ↔ base pair, local
32
+ * executor. Plans with three `GitRefReader`s (no checkout, developer tree
33
+ * untouched), then — when clean or fully resolved — performs a REAL
34
+ * `git merge --no-commit` in a temp worktree and writes the ENTIRE plan
35
+ * output over it before committing: for content-owned paths the planner is
36
+ * authoritative, never git's textual auto-merge (its line-level result can
37
+ * be non-canonical or wrong without ever surfacing as a conflict). Git
38
+ * still resolves everything the planner does not own — source files, lock
39
+ * files — exactly as a manual merge would. The commit joins both parents,
40
+ * so afterwards the base branch is an ancestor again and the ordinary
41
+ * fast-forward advance works.
42
+ *
43
+ * Reconcile never runs inside a write path: it is an explicit, separate
44
+ * step (tool, CLI, or Studio background job).
45
+ */
46
+ declare function reconcileBranches(projectRoot: string, options?: ReconcileBranchesOptions): Promise<ReconcileBranchesResult>;
47
+ //#endregion
48
+ export { ReconcileBranchesOptions, ReconcileBranchesResult, reconcileBranches };
49
+ //# sourceMappingURL=reconcile.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.mts","names":[],"sources":["../../src/git/reconcile.ts"],"mappings":";;;;UAoBiB,wBAAA;;EAEf,MAAA;EAFe;EAIf,WAAA,GAAc,kBAAA;;EAEd,MAAA,GAAS,aAAA;AAAA;AAAA,UAGM,uBAAA;EALD;;;;;AAKhB;;EAQE,MAAA;EACA,IAAA,EAAM,aAAA;EACN,IAAA;EACA,UAAA;EACA,MAAA;EACA,YAAA,GAAe,WAAA;EACf,IAAA,GAAO,UAAA;EACP,WAAA,GAAc,UAAA;AAAA;;;;;;;;;;;;;AAmBhB;;;;iBAAsB,iBAAA,CACpB,WAAA,UACA,OAAA,GAAS,wBAAA,GACR,OAAA,CAAQ,uBAAA"}
@@ -0,0 +1,13 @@
1
+ import "../reconcile-D5FTVW5z.mjs";
2
+ import "../context-CxpTATh4.mjs";
3
+ import "../model-manager-DqwL8ptm.mjs";
4
+ import "../fs-DJv5G8uL.mjs";
5
+ import "../meta-manager-g3shoZES.mjs";
6
+ import "../config-B8a4JBT1.mjs";
7
+ import "../validator-B2GhPM3L.mjs";
8
+ import "../identity-CxAB9Qb8.mjs";
9
+ import "../branch-lifecycle-DUOoyNWn.mjs";
10
+ import "../id-CjGMPP1k.mjs";
11
+ import "../transaction-BxDiiakv.mjs";
12
+ import { t as reconcileBranches } from "../reconcile-M7KBZtdn.mjs";
13
+ export { reconcileBranches };
@@ -1,37 +1,82 @@
1
- import { a as RemoteDeleteResult } from "../branch-lifecycle-DhzHfzs3.mjs";
2
- import { SyncResult, WorkflowMode } from "@contentrain/types";
1
+ import { o as RemoteDeleteResult } from "../branch-lifecycle-DGlgjnC7.mjs";
2
+ import { BaseAdvance, RemotePush, SyncResult, WorkflowMode } from "@contentrain/types";
3
+ import { SimpleGit } from "simple-git";
3
4
 
4
5
  //#region src/git/transaction.d.ts
6
+ /**
7
+ * Block-timeout for fetch/push inside transactions. These calls inherit the
8
+ * host's credential setup (see networkGit); without a timeout a hung SSH
9
+ * passphrase prompt would hang the MCP call with it.
10
+ */
11
+ declare const NETWORK_TIMEOUT_MS = 10000;
5
12
  interface ContextUpdate {
6
13
  tool: string;
7
14
  model: string;
8
15
  locale?: string;
9
16
  entries?: string[];
10
17
  }
18
+ /**
19
+ * Result of completing a write transaction.
20
+ *
21
+ * A diverged base branch is NOT an error: the content is committed and merged
22
+ * into the contentrain branch either way — only the base fast-forward is
23
+ * pending. `base_advance: 'blocked_diverged'` reports that truthfully instead
24
+ * of failing a write that in fact landed.
25
+ */
26
+ interface CompleteResult {
27
+ action: 'auto-merged' | 'pending-review';
28
+ commit: string;
29
+ sync?: SyncResult;
30
+ warning?: string;
31
+ /** Present on `auto-merged`: did the base branch advance to the contentrain tip? */
32
+ base_advance?: BaseAdvance;
33
+ /** Present on `auto-merged`: outcome of pushing the contentrain branch. */
34
+ remote_push?: RemotePush;
35
+ }
36
+ /** Result of landing a feature branch via {@link mergeBranch}. */
37
+ interface MergeBranchResult {
38
+ action: 'merged';
39
+ commit: string;
40
+ sync: SyncResult;
41
+ base_advance: BaseAdvance;
42
+ remote_push: RemotePush;
43
+ warning?: string;
44
+ remote?: RemoteDeleteResult;
45
+ }
11
46
  interface GitTransaction {
12
47
  worktree: string;
13
48
  branch: string;
14
49
  write(callback: (worktreePath: string) => Promise<void>): Promise<void>;
15
50
  commit(message: string, contextUpdate?: ContextUpdate): Promise<string>;
16
- complete(): Promise<{
17
- action: 'auto-merged' | 'pending-review';
18
- commit: string;
19
- sync?: SyncResult;
20
- warning?: string;
21
- }>;
51
+ complete(): Promise<CompleteResult>;
22
52
  cleanup(): Promise<void>;
23
53
  }
24
54
  declare function ensureContentBranch(projectRoot: string): Promise<void>;
55
+ declare function selectiveSync(projectRoot: string, _worktreePath: string, contentrainTip: string, _previousBaseRef?: string, dirtyFilesBeforeUpdate?: Set<string>): Promise<SyncResult>;
25
56
  declare function createTransaction(projectRoot: string, branchName: string, options?: {
26
57
  workflowOverride?: WorkflowMode;
27
58
  }): Promise<GitTransaction>;
28
- declare function mergeBranch(projectRoot: string, branchName: string): Promise<{
29
- action: 'merged';
30
- commit: string;
31
- sync: SyncResult;
32
- remote?: RemoteDeleteResult;
33
- }>;
59
+ declare function mergeBranch(projectRoot: string, branchName: string): Promise<MergeBranchResult>;
34
60
  declare function buildBranchName(scope: string, target: string, locale?: string): string;
61
+ /**
62
+ * True when `ancestor` is an ancestor of (or equal to) `descendant`.
63
+ * Implemented with `rev-list --count` because `merge-base --is-ancestor`
64
+ * signals via exit code with empty stderr — simple-git reports that as
65
+ * success, so it cannot express a negative verdict.
66
+ */
67
+ declare function isAncestor(git: SimpleGit, ancestor: string, descendant: string): Promise<boolean>;
68
+ /**
69
+ * Push the contentrain branch (with one fetch-merge-retry) and, when the base
70
+ * advanced, the base branch. Returns the push outcome for contentrain; the
71
+ * base push is best-effort and never demotes the outcome, because the
72
+ * contentrain branch is the content SSOT — it is what the CDN reads.
73
+ *
74
+ * The retry absorbs a concurrent remote advance (another writer pushed
75
+ * contentrain first): fetch, merge the remote tip into the local worktree's
76
+ * contentrain, push again. A conflicting retry-merge is aborted and reported
77
+ * as `rejected` — that is a real divergence, not a race.
78
+ */
79
+ declare function pushContentBranches(projectRoot: string, worktreePath: string, wtGit: SimpleGit, remoteName: string, baseBranch: string, baseAdvanced: boolean): Promise<RemotePush>;
35
80
  //#endregion
36
- export { ContextUpdate, GitTransaction, buildBranchName, createTransaction, ensureContentBranch, mergeBranch };
81
+ export { CompleteResult, ContextUpdate, GitTransaction, MergeBranchResult, NETWORK_TIMEOUT_MS, buildBranchName, createTransaction, ensureContentBranch, isAncestor, mergeBranch, pushContentBranches, selectiveSync };
37
82
  //# sourceMappingURL=transaction.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.d.mts","names":[],"sources":["../../src/git/transaction.ts"],"mappings":";;;;UAeiB,aAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;AAAA,UAGe,cAAA;EACf,QAAA;EACA,MAAA;EACA,KAAA,CAAM,QAAA,GAAW,YAAA,aAAyB,OAAA,SAAgB,OAAA;EAC1D,MAAA,CAAO,OAAA,UAAiB,aAAA,GAAgB,aAAA,GAAgB,OAAA;EACxD,QAAA,IAAY,OAAA;IAAU,MAAA;IAA0C,MAAA;IAAgB,IAAA,GAAO,UAAA;IAAY,OAAA;EAAA;EACnG,OAAA,IAAW,OAAA;AAAA;AAAA,iBAGS,mBAAA,CAAoB,WAAA,WAAsB,OAAA;AAAA,iBA4I1C,iBAAA,CACpB,WAAA,UACA,UAAA,UACA,OAAA;EAAY,gBAAA,GAAmB,YAAA;AAAA,IAC9B,OAAA,CAAQ,cAAA;AAAA,iBAuQW,WAAA,CACpB,WAAA,UACA,UAAA,WACC,OAAA;EAAU,MAAA;EAAkB,MAAA;EAAgB,IAAA,EAAM,UAAA;EAAY,MAAA,GAAS,kBAAA;AAAA;AAAA,iBAsI1D,eAAA,CAAgB,KAAA,UAAe,MAAA,UAAgB,MAAA"}
1
+ {"version":3,"file":"transaction.d.mts","names":[],"sources":["../../src/git/transaction.ts"],"mappings":";;;;;;;AAoBA;;;cAAa,kBAAA;AAAA,UAEI,aAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;AAWF;;;UAAiB,cAAA;EACf,MAAA;EACA,MAAA;EACA,IAAA,GAAO,UAAA;EACP,OAAA;EAHA;EAKA,YAAA,GAAe,WAAA;EAHf;EAKA,WAAA,GAAc,UAAA;AAAA;;UAIC,iBAAA;EACf,MAAA;EACA,MAAA;EACA,IAAA,EAAM,UAAA;EACN,YAAA,EAAc,WAAA;EACd,WAAA,EAAa,UAAA;EACb,OAAA;EACA,MAAA,GAAS,kBAAA;AAAA;AAAA,UAGM,cAAA;EACf,QAAA;EACA,MAAA;EACA,KAAA,CAAM,QAAA,GAAW,YAAA,aAAyB,OAAA,SAAgB,OAAA;EAC1D,MAAA,CAAO,OAAA,UAAiB,aAAA,GAAgB,aAAA,GAAgB,OAAA;EACxD,QAAA,IAAY,OAAA,CAAQ,cAAA;EACpB,OAAA,IAAW,OAAA;AAAA;AAAA,iBAGS,mBAAA,CAAoB,WAAA,WAAsB,OAAA;AAAA,iBAiC1C,aAAA,CACpB,WAAA,UACA,aAAA,UACA,cAAA,UACA,gBAAA,WACA,sBAAA,GAAyB,GAAA,WACxB,OAAA,CAAQ,UAAA;AAAA,iBAqGW,iBAAA,CACpB,WAAA,UACA,UAAA,UACA,OAAA;EAAY,gBAAA,GAAmB,YAAA;AAAA,IAC9B,OAAA,CAAQ,cAAA;AAAA,iBAiQW,WAAA,CACpB,WAAA,UACA,UAAA,WACC,OAAA,CAAQ,iBAAA;AAAA,iBAoKK,eAAA,CAAgB,KAAA,UAAe,MAAA,UAAgB,MAAA;;;;AAjkB/D;;;iBA+kBsB,UAAA,CAAW,GAAA,EAAK,SAAA,EAAW,QAAA,UAAkB,UAAA,WAAqB,OAAA;;;;;;;;;;;;iBAoBlE,mBAAA,CACpB,WAAA,UACA,YAAA,UACA,KAAA,EAAO,SAAA,EACP,UAAA,UACA,UAAA,UACA,YAAA,YACC,OAAA,CAAQ,UAAA"}
@@ -1,11 +1,10 @@
1
- import "../serialization-tmgfFBOh.mjs";
2
- import "../fs-DLbVB-Ek.mjs";
3
- import "../meta-manager-B_Se183X.mjs";
4
- import "../identity-D8QoGxhT.mjs";
5
- import "../config-VlRKRwAS.mjs";
6
- import "../branch-lifecycle-B6PXZ2N7.mjs";
7
- import "../model-manager-DgyKMjLN.mjs";
8
- import "../context-C3x6I3cE.mjs";
9
- import "../id-BjhaIxTL.mjs";
10
- import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "../transaction-BW9ektWu.mjs";
11
- export { buildBranchName, createTransaction, ensureContentBranch, mergeBranch };
1
+ import "../context-CxpTATh4.mjs";
2
+ import "../model-manager-DqwL8ptm.mjs";
3
+ import "../fs-DJv5G8uL.mjs";
4
+ import "../meta-manager-g3shoZES.mjs";
5
+ import "../config-B8a4JBT1.mjs";
6
+ import "../identity-CxAB9Qb8.mjs";
7
+ import "../branch-lifecycle-DUOoyNWn.mjs";
8
+ import "../id-CjGMPP1k.mjs";
9
+ import { a as isAncestor, c as selectiveSync, i as ensureContentBranch, n as buildBranchName, o as mergeBranch, r as createTransaction, s as pushContentBranches, t as NETWORK_TIMEOUT_MS } from "../transaction-BxDiiakv.mjs";
10
+ export { NETWORK_TIMEOUT_MS, buildBranchName, createTransaction, ensureContentBranch, isAncestor, mergeBranch, pushContentBranches, selectiveSync };
@@ -1,5 +1,5 @@
1
- import { o as readText, r as pathExists } from "./fs-DLbVB-Ek.mjs";
2
- import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles } from "./scan-config-Dx49nCqk.mjs";
1
+ import { o as readText, r as pathExists } from "./fs-DJv5G8uL.mjs";
2
+ import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles } from "./scan-config-C5zzygeH.mjs";
3
3
  import { dirname, extname, join, relative } from "node:path";
4
4
  //#region src/core/graph-builder.ts
5
5
  const MAX_ORPHANS = 10;
@@ -159,4 +159,4 @@ async function buildGraph(projectRoot, options) {
159
159
  //#endregion
160
160
  export { buildGraph as t };
161
161
 
162
- //# sourceMappingURL=graph-builder-BEocwW13.mjs.map
162
+ //# sourceMappingURL=graph-builder-C9NS7y-a.mjs.map