@contentrain/mcp 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/{apply-manager-a5l93F4O.mjs → apply-manager-SLCRLHN_.mjs} +10 -6
  2. package/dist/apply-manager-SLCRLHN_.mjs.map +1 -0
  3. package/dist/{astro-parser-BVoGjA6M.mjs → astro-parser-LNjEspnf.mjs} +3 -3
  4. package/dist/{astro-parser-BVoGjA6M.mjs.map → astro-parser-LNjEspnf.mjs.map} +1 -1
  5. package/dist/{branch-lifecycle-AwBJIheA.d.mts → branch-lifecycle-D4W0WIs4.d.mts} +1 -1
  6. package/dist/{branch-lifecycle-AwBJIheA.d.mts.map → branch-lifecycle-D4W0WIs4.d.mts.map} +1 -1
  7. package/dist/{compiler-BZXAN4Ir.mjs → compiler-CNzm2Y2I.mjs} +2 -2
  8. package/dist/{compiler-BZXAN4Ir.mjs.map → compiler-CNzm2Y2I.mjs.map} +1 -1
  9. package/dist/{compiler-sfc.cjs-tFlmtjrJ.mjs → compiler-sfc.cjs-y0B0L3MH.mjs} +2 -2
  10. package/dist/{compiler-sfc.cjs-tFlmtjrJ.mjs.map → compiler-sfc.cjs-y0B0L3MH.mjs.map} +1 -1
  11. package/dist/{content-manager-BgxF3dHu.d.mts → content-manager-DWo3G64y.d.mts} +2 -2
  12. package/dist/{content-manager-BgxF3dHu.d.mts.map → content-manager-DWo3G64y.d.mts.map} +1 -1
  13. package/dist/core/apply-manager.d.mts +2 -0
  14. package/dist/core/apply-manager.d.mts.map +1 -1
  15. package/dist/core/apply-manager.mjs +2 -2
  16. package/dist/core/config.d.mts +1 -1
  17. package/dist/core/content-manager.d.mts +1 -1
  18. package/dist/core/context.d.mts +1 -1
  19. package/dist/core/contracts/index.d.mts +1 -1
  20. package/dist/core/doctor.mjs +2 -2
  21. package/dist/core/graph-builder.mjs +2 -2
  22. package/dist/core/model-manager.d.mts +1 -1
  23. package/dist/core/ops/index.d.mts +2 -2
  24. package/dist/core/overlay-reader.d.mts +1 -1
  25. package/dist/core/overlay-reader.mjs +1 -1
  26. package/dist/core/scan-config.mjs +1 -1
  27. package/dist/core/scanner.mjs +3 -3
  28. package/dist/core/validator/index.d.mts +1 -1
  29. package/dist/core/validator/index.mjs +1 -1
  30. package/dist/{doctor-pw8EWRFR.mjs → doctor-BwJ_nmqS.mjs} +2 -2
  31. package/dist/{doctor-pw8EWRFR.mjs.map → doctor-BwJ_nmqS.mjs.map} +1 -1
  32. package/dist/errors-e0YdjooK.mjs +126 -0
  33. package/dist/errors-e0YdjooK.mjs.map +1 -0
  34. package/dist/git/branch-lifecycle.d.mts +1 -1
  35. package/dist/git/transaction.d.mts +1 -1
  36. package/dist/git/transaction.d.mts.map +1 -1
  37. package/dist/git/transaction.mjs +1 -1
  38. package/dist/{graph-builder-CRUX_8mA.mjs → graph-builder-DK4Mh8Tn.mjs} +2 -2
  39. package/dist/{graph-builder-CRUX_8mA.mjs.map → graph-builder-DK4Mh8Tn.mjs.map} +1 -1
  40. package/dist/index.mjs +10 -10
  41. package/dist/{local-EJTxd5MR.mjs → local-8dJ5NIWL.mjs} +2 -2
  42. package/dist/{local-EJTxd5MR.mjs.map → local-8dJ5NIWL.mjs.map} +1 -1
  43. package/dist/{node-wC-z3NoZ.mjs → node-BxujauDw.mjs} +1 -1
  44. package/dist/{node-wC-z3NoZ.mjs.map → node-BxujauDw.mjs.map} +1 -1
  45. package/dist/{overlay-reader-BOb105gS.mjs → overlay-reader-DNaVsgiS.mjs} +1 -1
  46. package/dist/{overlay-reader-BOb105gS.mjs.map → overlay-reader-DNaVsgiS.mjs.map} +1 -1
  47. package/dist/paths-enT2coeX.mjs +27 -0
  48. package/dist/paths-enT2coeX.mjs.map +1 -0
  49. package/dist/providers/github/index.d.mts +1 -1
  50. package/dist/providers/github/index.mjs +2 -1
  51. package/dist/providers/github/index.mjs.map +1 -1
  52. package/dist/providers/gitlab/index.d.mts +1 -1
  53. package/dist/providers/gitlab/index.mjs +2 -1
  54. package/dist/providers/gitlab/index.mjs.map +1 -1
  55. package/dist/providers/local/index.d.mts +1 -1
  56. package/dist/providers/local/index.mjs +2 -2
  57. package/dist/{scan-config-BGUflS8t.mjs → scan-config-BlNLRCMx.mjs} +1 -1
  58. package/dist/{scan-config-BGUflS8t.mjs.map → scan-config-BlNLRCMx.mjs.map} +1 -1
  59. package/dist/{scanner-VOwrKLGC.mjs → scanner-CGWhmpDz.mjs} +6 -6
  60. package/dist/{scanner-VOwrKLGC.mjs.map → scanner-CGWhmpDz.mjs.map} +1 -1
  61. package/dist/server/http/index.d.mts +1 -1
  62. package/dist/server/http/index.mjs +10 -10
  63. package/dist/{server-vA1tM7DA.d.mts → server-DLauBiu2.d.mts} +2 -2
  64. package/dist/{server-vA1tM7DA.d.mts.map → server-DLauBiu2.d.mts.map} +1 -1
  65. package/dist/{server-DAVoOVit.mjs → server-I6sqKvO8.mjs} +22 -11
  66. package/dist/server-I6sqKvO8.mjs.map +1 -0
  67. package/dist/server.d.mts +1 -1
  68. package/dist/server.mjs +10 -10
  69. package/dist/{source-map-generator-BIEiz3If.mjs → source-map-generator-Bvbu-kM5.mjs} +1 -1
  70. package/dist/{source-map-generator-BIEiz3If.mjs.map → source-map-generator-Bvbu-kM5.mjs.map} +1 -1
  71. package/dist/{svelte-parser-Bll-1SK0.mjs → svelte-parser-DaFK9iY0.mjs} +3 -3
  72. package/dist/{svelte-parser-Bll-1SK0.mjs.map → svelte-parser-DaFK9iY0.mjs.map} +1 -1
  73. package/dist/{transaction-DC_dNON9.mjs → transaction-1SPznNt3.mjs} +12 -4
  74. package/dist/transaction-1SPznNt3.mjs.map +1 -0
  75. package/dist/tsx-parser-C3XsIrwU.mjs +2 -0
  76. package/dist/{tsx-parser-B_aI_C2r.mjs → tsx-parser-md1N0Niu.mjs} +1 -1
  77. package/dist/{tsx-parser-B_aI_C2r.mjs.map → tsx-parser-md1N0Niu.mjs.map} +1 -1
  78. package/dist/{validator-D5VncJ8M.mjs → validator-ChiYk6ap.mjs} +1 -1
  79. package/dist/{validator-D5VncJ8M.mjs.map → validator-ChiYk6ap.mjs.map} +1 -1
  80. package/dist/{vue-parser-COig4Y1a.mjs → vue-parser-CHxDohd8.mjs} +3 -3
  81. package/dist/{vue-parser-COig4Y1a.mjs.map → vue-parser-CHxDohd8.mjs.map} +1 -1
  82. package/package.json +1 -1
  83. package/dist/apply-manager-a5l93F4O.mjs.map +0 -1
  84. package/dist/paths-BU6E-oDs.mjs +0 -52
  85. package/dist/paths-BU6E-oDs.mjs.map +0 -1
  86. package/dist/server-DAVoOVit.mjs.map +0 -1
  87. package/dist/transaction-DC_dNON9.mjs.map +0 -1
  88. package/dist/tsx-parser-ChduVwKJ.mjs +0 -2
  89. /package/dist/{index-DDX-qYNw.d.mts → index-w8QHThNS.d.mts} +0 -0
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader } from "./index-DDX-qYNw.mjs";
1
+ import { _ as RepoReader } from "./index-w8QHThNS.mjs";
2
2
  import { ContentrainConfig, LocaleStrategy, ModelDefinition, Vocabulary, parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types";
3
3
 
4
4
  //#region src/core/content-manager.d.ts
@@ -62,4 +62,4 @@ declare function readContent(projectRoot: string, model: ModelDefinition, opts:
62
62
  }): Promise<unknown>;
63
63
  //#endregion
64
64
  export { writeContent as _, deleteContent as a, readContent as c, resolveLocaleStrategy as d, resolveMdFilePath as f, validateSlug$1 as g, validateLocale$1 as h, WriteResult as i, resolveContentDir as l, validateEntryId$1 as m, DeleteOpts as n, listContent as o, serializeFrontmatter as p, ListOpts as r, parseFrontmatter as s, ContentEntry as t, resolveJsonFilePath as u };
65
- //# sourceMappingURL=content-manager-BgxF3dHu.d.mts.map
65
+ //# sourceMappingURL=content-manager-DWo3G64y.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"content-manager-BgxF3dHu.d.mts","names":[],"sources":["../src/core/content-manager.ts"],"mappings":";;;;iBAgBgB,iBAAA,CAAkB,WAAA,UAAqB,KAAA,EAAO,eAAA;AAAA,iBAO9C,qBAAA,CAAsB,KAAA,EAAO,eAAA,GAAkB,cAAA;;iBAK/C,mBAAA,CAAoB,GAAA,UAAa,KAAA,EAAO,eAAA,EAAiB,MAAA;;iBAczD,iBAAA,CAAkB,GAAA,UAAa,KAAA,EAAO,eAAA,EAAiB,MAAA,UAAgB,IAAA;AAAA,UAetE,YAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;AAAA;AAAA,UAGS,WAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EAhDuE;EAkDvE,aAAA;AAAA;AAAA,UAGe,QAAA;EACf,MAAA;EACA,MAAA,GAAS,MAAA;EACT,OAAA;EACA,KAAA;EACA,MAAA;AAAA;AAAA,iBAKoB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,OAAA,EAAS,YAAA,IACT,MAAA,EAAQ,iBAAA,EACR,UAAA,GAAa,UAAA,UACZ,OAAA,CAAQ,WAAA;AAAA,iBA6IW,aAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,UAAA,GACL,OAAA;;;AAzLH;;;;;;;;;;;AAOA;;iBA+TgB,WAAA,CACd,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBACa,WAAA,CACd,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBA0QmB,WAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA;EAAQ,MAAA;EAAgB,OAAA;EAAkB,IAAA;AAAA,IACzC,OAAA"}
1
+ {"version":3,"file":"content-manager-DWo3G64y.d.mts","names":[],"sources":["../src/core/content-manager.ts"],"mappings":";;;;iBAgBgB,iBAAA,CAAkB,WAAA,UAAqB,KAAA,EAAO,eAAA;AAAA,iBAO9C,qBAAA,CAAsB,KAAA,EAAO,eAAA,GAAkB,cAAA;;iBAK/C,mBAAA,CAAoB,GAAA,UAAa,KAAA,EAAO,eAAA,EAAiB,MAAA;;iBAczD,iBAAA,CAAkB,GAAA,UAAa,KAAA,EAAO,eAAA,EAAiB,MAAA,UAAgB,IAAA;AAAA,UAetE,YAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;AAAA;AAAA,UAGS,WAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EAhDuE;EAkDvE,aAAA;AAAA;AAAA,UAGe,QAAA;EACf,MAAA;EACA,MAAA,GAAS,MAAA;EACT,OAAA;EACA,KAAA;EACA,MAAA;AAAA;AAAA,iBAKoB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,OAAA,EAAS,YAAA,IACT,MAAA,EAAQ,iBAAA,EACR,UAAA,GAAa,UAAA,UACZ,OAAA,CAAQ,WAAA;AAAA,iBA6IW,aAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,UAAA,GACL,OAAA;;;AAzLH;;;;;;;;;;;AAOA;;iBA+TgB,WAAA,CACd,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBACa,WAAA,CACd,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBA0QmB,WAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA;EAAQ,MAAA;EAAgB,OAAA;EAAkB,IAAA;AAAA,IACzC,OAAA"}
@@ -55,6 +55,7 @@ interface ExtractionResult {
55
55
  branch: string;
56
56
  action: string;
57
57
  commit: string;
58
+ warning?: string;
58
59
  };
59
60
  context_updated?: boolean;
60
61
  next_steps: string[];
@@ -106,6 +107,7 @@ interface ReuseResult {
106
107
  branch: string;
107
108
  action: string;
108
109
  commit: string;
110
+ warning?: string;
109
111
  };
110
112
  next_steps: string[];
111
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"apply-manager.d.mts","names":[],"sources":["../../src/core/apply-manager.ts"],"mappings":";;;UAYiB,eAAA;EACf,KAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA,GAAS,MAAA,SAAe,QAAA;EACxB,OAAA,EAAS,KAAA;IACP,MAAA;IACA,IAAA;IACA,IAAA,EAAM,MAAA;IACN,MAAA;MAAW,IAAA;MAAc,IAAA;MAAc,KAAA;IAAA;IACvC,OAAA,GAAU,KAAA;MAAQ,IAAA;MAAc,IAAA;MAAc,GAAA;MAAa,KAAA;IAAA;EAAA;AAAA;AAAA,UAI9C,eAAA;EACf,WAAA,EAAa,eAAA;EACb,OAAA;AAAA;AAAA,UAGe,iBAAA;EACf,gBAAA;EACA,gBAAA;EACA,aAAA;EACA,aAAA;AAAA;AAAA,UAGe,gBAAA;EACf,OAAA;EACA,OAAA,GAAU,iBAAA;EACV,KAAA;EACA,iBAAA;EACA,OAAA;IACE,cAAA;IACA,cAAA;IACA,eAAA;IACA,UAAA,EAAY,KAAA;MAAQ,KAAA;MAAe,MAAA;MAAgB,KAAA;MAAe,IAAA;MAAc,IAAA;IAAA;EAAA;EAElF,GAAA;IAAQ,MAAA;IAAgB,MAAA;IAAgB,MAAA;EAAA;EACxC,eAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,IAAA;EACA,IAAA;EACA,SAAA;EACA,cAAA;EACA,gBAAA;AAAA;AAAA,UAGe,UAAA;EACf,KAAA;IAAS,KAAA;IAAgB,MAAA;EAAA;EACzB,OAAA,EAAS,UAAA;EACT,OAAA;AAAA;AAAA,UAGe,WAAA;EACf,IAAA;EACA,KAAA;AAAA;AAAA,UAGe,WAAA;EACf,OAAA;EACA,KAAA;EACA,cAAA;EACA,OAAA;IACE,eAAA;IACA,aAAA;IACA,cAAA;EAAA;EAEF,OAAA;IACE,cAAA;IACA,eAAA;IACA,eAAA,EAAiB,KAAA;MAAQ,IAAA;MAAc,IAAA;MAAc,MAAA;IAAA;IACrD,aAAA;IACA,kBAAA,GAAqB,KAAA;MAAQ,IAAA;MAAc,OAAA;IAAA;IAC3C,aAAA,GAAgB,WAAA;EAAA;EAElB,GAAA;IAAQ,MAAA;IAAgB,MAAA;IAAgB,MAAA;EAAA;EACxC,UAAA;AAAA;;cAQW,oBAAA,EAAoB,GAAA;AAAA,iBAajB,mBAAA,CAAoB,QAAA,WAAmB,eAAA;AAvCvD;;;;AAAA,iBA0DgB,2BAAA,CACd,QAAA,UACA,aAAA,UACA,OAAA;;;;;iBAyCc,iBAAA,CAAkB,QAAA;;;;;iBAoClB,WAAA,CAAY,QAAA,UAAkB,OAAA;AAAA,iBA8JxB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,GACN,OAAA,CAAQ,gBAAA;AAAA,iBAoRW,UAAA,CACpB,WAAA,UACA,KAAA,EAAO,UAAA,GACN,OAAA,CAAQ,WAAA;;;;;;;;;iBAqYK,aAAA,CAAc,IAAA,UAAc,QAAA,UAAkB,aAAA"}
1
+ {"version":3,"file":"apply-manager.d.mts","names":[],"sources":["../../src/core/apply-manager.ts"],"mappings":";;;UAYiB,eAAA;EACf,KAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA,GAAS,MAAA,SAAe,QAAA;EACxB,OAAA,EAAS,KAAA;IACP,MAAA;IACA,IAAA;IACA,IAAA,EAAM,MAAA;IACN,MAAA;MAAW,IAAA;MAAc,IAAA;MAAc,KAAA;IAAA;IACvC,OAAA,GAAU,KAAA;MAAQ,IAAA;MAAc,IAAA;MAAc,GAAA;MAAa,KAAA;IAAA;EAAA;AAAA;AAAA,UAI9C,eAAA;EACf,WAAA,EAAa,eAAA;EACb,OAAA;AAAA;AAAA,UAGe,iBAAA;EACf,gBAAA;EACA,gBAAA;EACA,aAAA;EACA,aAAA;AAAA;AAAA,UAGe,gBAAA;EACf,OAAA;EACA,OAAA,GAAU,iBAAA;EACV,KAAA;EACA,iBAAA;EACA,OAAA;IACE,cAAA;IACA,cAAA;IACA,eAAA;IACA,UAAA,EAAY,KAAA;MAAQ,KAAA;MAAe,MAAA;MAAgB,KAAA;MAAe,IAAA;MAAc,IAAA;IAAA;EAAA;EAElF,GAAA;IAAQ,MAAA;IAAgB,MAAA;IAAgB,MAAA;IAAgB,OAAA;EAAA;EACxD,eAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,IAAA;EACA,IAAA;EACA,SAAA;EACA,cAAA;EACA,gBAAA;AAAA;AAAA,UAGe,UAAA;EACf,KAAA;IAAS,KAAA;IAAgB,MAAA;EAAA;EACzB,OAAA,EAAS,UAAA;EACT,OAAA;AAAA;AAAA,UAGe,WAAA;EACf,IAAA;EACA,KAAA;AAAA;AAAA,UAGe,WAAA;EACf,OAAA;EACA,KAAA;EACA,cAAA;EACA,OAAA;IACE,eAAA;IACA,aAAA;IACA,cAAA;EAAA;EAEF,OAAA;IACE,cAAA;IACA,eAAA;IACA,eAAA,EAAiB,KAAA;MAAQ,IAAA;MAAc,IAAA;MAAc,MAAA;IAAA;IACrD,aAAA;IACA,kBAAA,GAAqB,KAAA;MAAQ,IAAA;MAAc,OAAA;IAAA;IAC3C,aAAA,GAAgB,WAAA;EAAA;EAElB,GAAA;IAAQ,MAAA;IAAgB,MAAA;IAAgB,MAAA;IAAgB,OAAA;EAAA;EACxD,UAAA;AAAA;;cAQW,oBAAA,EAAoB,GAAA;AAAA,iBAajB,mBAAA,CAAoB,QAAA,WAAmB,eAAA;;;;;iBAmBvC,2BAAA,CACd,QAAA,UACA,aAAA,UACA,OAAA;;;;;iBAyCc,iBAAA,CAAkB,QAAA;;;;;iBAoClB,WAAA,CAAY,QAAA,UAAkB,OAAA;AAAA,iBA8JxB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,GACN,OAAA,CAAQ,gBAAA;AAAA,iBA+RW,UAAA,CACpB,WAAA,UACA,KAAA,EAAO,UAAA,GACN,OAAA,CAAQ,WAAA;;;;;;;;;iBAgZK,aAAA,CAAc,IAAA,UAAc,QAAA,UAAkB,aAAA"}
@@ -6,6 +6,6 @@ import "../model-manager-DP2CZiMT.mjs";
6
6
  import "../context-DjglDPvj.mjs";
7
7
  import "../branch-lifecycle-BAfgSQBv.mjs";
8
8
  import "../id-DV_T9Ic8.mjs";
9
- import "../transaction-DC_dNON9.mjs";
10
- import { a as detectFileFramework, c as validatePatchPath, i as checkSyntax, n as applyExtract, o as replaceInLine, r as applyReuse, s as validateFrameworkExpression, t as PATCHABLE_EXTENSIONS } from "../apply-manager-a5l93F4O.mjs";
9
+ import "../transaction-1SPznNt3.mjs";
10
+ import { a as detectFileFramework, c as validatePatchPath, i as checkSyntax, n as applyExtract, o as replaceInLine, r as applyReuse, s as validateFrameworkExpression, t as PATCHABLE_EXTENSIONS } from "../apply-manager-SLCRLHN_.mjs";
11
11
  export { PATCHABLE_EXTENSIONS, applyExtract, applyReuse, checkSyntax, detectFileFramework, replaceInLine, validateFrameworkExpression, validatePatchPath };
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader } from "../index-DDX-qYNw.mjs";
1
+ import { _ as RepoReader } from "../index-w8QHThNS.mjs";
2
2
  import { ContentrainConfig, Vocabulary } from "@contentrain/types";
3
3
 
4
4
  //#region src/core/config.d.ts
@@ -1,2 +1,2 @@
1
- import { _ as writeContent, a as deleteContent, c as readContent, d as resolveLocaleStrategy, f as resolveMdFilePath, g as validateSlug, h as validateLocale, i as WriteResult, l as resolveContentDir, m as validateEntryId, n as DeleteOpts, o as listContent, p as serializeFrontmatter, r as ListOpts, s as parseFrontmatter, t as ContentEntry, u as resolveJsonFilePath } from "../content-manager-BgxF3dHu.mjs";
1
+ import { _ as writeContent, a as deleteContent, c as readContent, d as resolveLocaleStrategy, f as resolveMdFilePath, g as validateSlug, h as validateLocale, i as WriteResult, l as resolveContentDir, m as validateEntryId, n as DeleteOpts, o as listContent, p as serializeFrontmatter, r as ListOpts, s as parseFrontmatter, t as ContentEntry, u as resolveJsonFilePath } from "../content-manager-DWo3G64y.mjs";
2
2
  export { ContentEntry, DeleteOpts, ListOpts, WriteResult, deleteContent, listContent, parseFrontmatter, readContent, resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy, resolveMdFilePath, serializeFrontmatter, validateEntryId, validateLocale, validateSlug, writeContent };
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader, a as FileChange } from "../index-DDX-qYNw.mjs";
1
+ import { _ as RepoReader, a as FileChange } from "../index-w8QHThNS.mjs";
2
2
  import { ContextJson, ContextSource } from "@contentrain/types";
3
3
 
4
4
  //#region src/core/context.d.ts
@@ -1,2 +1,2 @@
1
- import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter } from "../../index-DDX-qYNw.mjs";
1
+ import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter } from "../../index-w8QHThNS.mjs";
2
2
  export { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities, RepoProvider, RepoReader, RepoWriter };
@@ -3,6 +3,6 @@ import "../meta-manager-CJUiTgP2.mjs";
3
3
  import "../config-oxxgznz7.mjs";
4
4
  import "../model-manager-DP2CZiMT.mjs";
5
5
  import "../branch-lifecycle-BAfgSQBv.mjs";
6
- import "../scan-config-BGUflS8t.mjs";
7
- import { t as runDoctor } from "../doctor-pw8EWRFR.mjs";
6
+ import "../scan-config-BlNLRCMx.mjs";
7
+ import { t as runDoctor } from "../doctor-BwJ_nmqS.mjs";
8
8
  export { runDoctor };
@@ -1,4 +1,4 @@
1
1
  import "../fs-DLbVB-Ek.mjs";
2
- import "../scan-config-BGUflS8t.mjs";
3
- import { t as buildGraph } from "../graph-builder-CRUX_8mA.mjs";
2
+ import "../scan-config-BlNLRCMx.mjs";
3
+ import { t as buildGraph } from "../graph-builder-DK4Mh8Tn.mjs";
4
4
  export { buildGraph };
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader } from "../index-DDX-qYNw.mjs";
1
+ import { _ as RepoReader } from "../index-w8QHThNS.mjs";
2
2
  import { ModelDefinition, ModelSummary, ModelSummary as ModelSummary$1 } from "@contentrain/types";
3
3
  import { z } from "zod";
4
4
 
@@ -1,5 +1,5 @@
1
- import { _ as RepoReader, a as FileChange } from "../../index-DDX-qYNw.mjs";
2
- import { t as ContentEntry } from "../../content-manager-BgxF3dHu.mjs";
1
+ import { _ as RepoReader, a as FileChange } from "../../index-w8QHThNS.mjs";
2
+ import { t as ContentEntry } from "../../content-manager-DWo3G64y.mjs";
3
3
  import { ContentrainConfig, ModelDefinition, Vocabulary } from "@contentrain/types";
4
4
 
5
5
  //#region src/core/ops/apply-to-worktree.d.ts
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader, a as FileChange } from "../index-DDX-qYNw.mjs";
1
+ import { _ as RepoReader, a as FileChange } from "../index-w8QHThNS.mjs";
2
2
 
3
3
  //#region src/core/overlay-reader.d.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { t as OverlayReader } from "../overlay-reader-BOb105gS.mjs";
1
+ import { t as OverlayReader } from "../overlay-reader-DNaVsgiS.mjs";
2
2
  export { OverlayReader };
@@ -1,3 +1,3 @@
1
1
  import "../fs-DLbVB-Ek.mjs";
2
- import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles, r as SCAN_IGNORE_DIRS, t as MAX_SCAN_FILES } from "../scan-config-BGUflS8t.mjs";
2
+ import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles, r as SCAN_IGNORE_DIRS, t as MAX_SCAN_FILES } from "../scan-config-BlNLRCMx.mjs";
3
3
  export { MAX_SCAN_FILES, SCAN_EXTENSIONS, SCAN_IGNORE_DIRS, autoDetectSourceDirs, classifyFile, discoverFiles };
@@ -1,5 +1,5 @@
1
1
  import "../fs-DLbVB-Ek.mjs";
2
- import "../scan-config-BGUflS8t.mjs";
3
- import { n as scanSummary, t as scanCandidates } from "../scanner-VOwrKLGC.mjs";
4
- import "../tsx-parser-B_aI_C2r.mjs";
2
+ import "../scan-config-BlNLRCMx.mjs";
3
+ import { n as scanSummary, t as scanCandidates } from "../scanner-CGWhmpDz.mjs";
4
+ import "../tsx-parser-md1N0Niu.mjs";
5
5
  export { scanCandidates, scanSummary };
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader } from "../../index-DDX-qYNw.mjs";
1
+ import { _ as RepoReader } from "../../index-w8QHThNS.mjs";
2
2
  import { EntryMeta, FieldDef, ModelDefinition, ValidationError, ValidationResult } from "@contentrain/types";
3
3
 
4
4
  //#region src/core/validator/entry.d.ts
@@ -2,5 +2,5 @@ import "../../fs-DLbVB-Ek.mjs";
2
2
  import "../../meta-manager-CJUiTgP2.mjs";
3
3
  import "../../config-oxxgznz7.mjs";
4
4
  import "../../model-manager-DP2CZiMT.mjs";
5
- import { i as validateContent, n as validateScheduleFields, r as checkRelationIntegrity, t as validateProject } from "../../validator-D5VncJ8M.mjs";
5
+ import { i as validateContent, n as validateScheduleFields, r as checkRelationIntegrity, t as validateProject } from "../../validator-ChiYk6ap.mjs";
6
6
  export { checkRelationIntegrity, validateContent, validateProject, validateScheduleFields };
@@ -2,7 +2,7 @@ import { a as readJson, i as readDir, o as readText, r as pathExists, t as conte
2
2
  import { t as readConfig } from "./config-oxxgznz7.mjs";
3
3
  import { g as resolveLocaleStrategy, h as resolveJsonFilePath, m as resolveContentDir, o as listModels, s as readModel } from "./model-manager-DP2CZiMT.mjs";
4
4
  import { n as checkBranchHealth, s as listRemoteCrBranches } from "./branch-lifecycle-BAfgSQBv.mjs";
5
- import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-BGUflS8t.mjs";
5
+ import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-BlNLRCMx.mjs";
6
6
  import { join } from "node:path";
7
7
  import { readdir, stat } from "node:fs/promises";
8
8
  import { simpleGit } from "simple-git";
@@ -380,4 +380,4 @@ async function analyzeMissingLocaleKeys(projectRoot, config) {
380
380
  //#endregion
381
381
  export { runDoctor as t };
382
382
 
383
- //# sourceMappingURL=doctor-pw8EWRFR.mjs.map
383
+ //# sourceMappingURL=doctor-BwJ_nmqS.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-pw8EWRFR.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { readdir, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport { simpleGit } from 'simple-git'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel } 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 = simpleGit(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 all parseable ───\n if (hasCrDir) {\n try {\n const models = await listModels(projectRoot)\n const parseResults = await Promise.all(models.map(m => readModel(projectRoot, m.id)))\n const allParseable = parseResults.every(r => r !== null)\n checks.push({\n name: 'Models',\n pass: allParseable,\n detail: `${models.length} model(s)${allParseable ? ', all valid' : ', some failed to parse'}`,\n severity: allParseable ? 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 content keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All keys referenced in source'\n : `${unusedKeys.length} 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 const models = await listModels(projectRoot)\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\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;;AAIJ,KAAI,SACF,KAAI;EACF,MAAM,SAAS,MAAM,WAAW,YAAY;EAE5C,MAAM,gBADe,MAAM,QAAQ,IAAI,OAAO,KAAI,MAAK,UAAU,aAAa,EAAE,GAAG,CAAC,CAAC,EACnD,OAAM,MAAK,MAAM,KAAK;AACxD,SAAO,KAAK;GACV,MAAM;GACN,MAAM;GACN,QAAQ,GAAG,OAAO,OAAO,WAAW,eAAe,gBAAgB;GACnE,UAAU,eAAe,KAAA,IAAY;GACtC,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,kCACA,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;CAEnC,MAAM,SAAS,MAAM,WAAW,YAAY;CAC5C,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;;AAGT,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-BwJ_nmqS.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { readdir, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport { simpleGit } from 'simple-git'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel } 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 = simpleGit(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 all parseable ───\n if (hasCrDir) {\n try {\n const models = await listModels(projectRoot)\n const parseResults = await Promise.all(models.map(m => readModel(projectRoot, m.id)))\n const allParseable = parseResults.every(r => r !== null)\n checks.push({\n name: 'Models',\n pass: allParseable,\n detail: `${models.length} model(s)${allParseable ? ', all valid' : ', some failed to parse'}`,\n severity: allParseable ? 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 content keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All keys referenced in source'\n : `${unusedKeys.length} 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 const models = await listModels(projectRoot)\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\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,126 @@
1
+ //#region src/providers/shared/errors.ts
2
+ /**
3
+ * Error shapes for API-backed providers, and the mapping from those shapes
4
+ * into Contentrain's structured error envelope.
5
+ *
6
+ * Two common error shapes in the provider SDKs we use:
7
+ *
8
+ * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a
9
+ * top-level `.status` number set to the HTTP status code.
10
+ * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose
11
+ * `.cause` includes `{ response: { status } }`.
12
+ */
13
+ /**
14
+ * HTTP status behind a provider SDK rejection, or `undefined` when the error
15
+ * is not an HTTP failure (a local git error, a programming mistake, …).
16
+ *
17
+ * We deliberately do NOT fall back to substring matching on the error
18
+ * message — that leniency can silently mask other failures (forbidden repo,
19
+ * deleted project, rate limits) and produce the wrong answer. If either SDK
20
+ * ever stops populating the status field, the regression surfaces in tests
21
+ * rather than being papered over at the reader layer.
22
+ */
23
+ function extractHttpStatus(error) {
24
+ if (typeof error !== "object" || error === null) return void 0;
25
+ const direct = error.status;
26
+ if (typeof direct === "number") return direct;
27
+ const nested = error.cause?.response?.status;
28
+ return typeof nested === "number" ? nested : void 0;
29
+ }
30
+ /** Unified "is this a 404?" helper for API-backed providers. */
31
+ function isNotFoundError(error) {
32
+ return extractHttpStatus(error) === 404;
33
+ }
34
+ /**
35
+ * Provider SDK messages are written for a human reading a terminal, not for an
36
+ * agent deciding what to do next. Octokit appends a documentation URL to most
37
+ * messages and embeds the raw JSON response body on validation failures.
38
+ *
39
+ * The vendor text still carries the only specifics we have ("Reference already
40
+ * exists", which field failed), so it is kept as a parenthetical detail rather
41
+ * than discarded — but the doc links are noise for an agent, and an embedded
42
+ * body can be arbitrarily long, so URLs are stripped and the result is capped.
43
+ */
44
+ function sanitiseDetail(error) {
45
+ const collapsed = (error?.message ?? String(error)).replace(/\s*-?\s*https?:\/\/\S+/g, "").replace(/\s+/g, " ").trim();
46
+ return collapsed.length > 200 ? `${collapsed.slice(0, 197)}…` : collapsed;
47
+ }
48
+ /**
49
+ * GitHub signals rate limiting with 403 plus an exhausted remaining-quota
50
+ * header (secondary limits use 403 too), and with 429 on some endpoints.
51
+ * Checking the header first keeps a genuine permission 403 from being
52
+ * reported as a rate limit.
53
+ */
54
+ function isRateLimited(error, status) {
55
+ if (status === 429) return true;
56
+ if (status !== 403) return false;
57
+ const remaining = (error.response?.headers)?.["x-ratelimit-remaining"];
58
+ if (remaining !== void 0) return String(remaining) === "0";
59
+ return /\brate limit\b/i.test(error.message ?? "");
60
+ }
61
+ /**
62
+ * Map a provider SDK rejection onto the same structured envelope the local git
63
+ * paths already produce (`code` + `agent_hint` + `developer_action`). Returns
64
+ * `undefined` for anything that is not a recognisable provider HTTP failure,
65
+ * so local git errors and ordinary exceptions pass through untouched.
66
+ */
67
+ function mapProviderError(error) {
68
+ const status = extractHttpStatus(error);
69
+ if (status === void 0) return void 0;
70
+ const detail = sanitiseDetail(error);
71
+ const withDetail = (summary) => detail ? `${summary} (${detail})` : summary;
72
+ if (isRateLimited(error, status)) return {
73
+ error: withDetail("The git provider rejected the request because the API rate limit is exhausted."),
74
+ code: "PROVIDER_RATE_LIMITED",
75
+ agent_hint: "Do not retry immediately — the limit is time-based. Stop the current operation and report it to the developer.",
76
+ developer_action: "Wait for the rate-limit window to reset, or use a token with a higher quota."
77
+ };
78
+ switch (status) {
79
+ case 401: return {
80
+ error: withDetail("The git provider rejected the credentials."),
81
+ code: "PROVIDER_UNAUTHORIZED",
82
+ agent_hint: "The token is missing, malformed, or expired. Not retryable — ask the developer to fix the credentials.",
83
+ developer_action: "Check the provider token configured for this Contentrain server and reissue it if it has expired."
84
+ };
85
+ case 403: return {
86
+ error: withDetail("The git provider denied access to this resource."),
87
+ code: "PROVIDER_FORBIDDEN",
88
+ agent_hint: "The credentials are valid but lack permission for this operation. Not retryable — report which operation was refused.",
89
+ developer_action: "Grant the token write access to the repository, or check whether branch protection or SSO authorisation is blocking it."
90
+ };
91
+ case 404: return {
92
+ error: withDetail("The git provider could not find the requested resource."),
93
+ code: "PROVIDER_NOT_FOUND",
94
+ 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.",
95
+ developer_action: "Confirm the repository and branch names in the Contentrain config, and that the token can reach a private repository."
96
+ };
97
+ case 409: return {
98
+ error: withDetail("The git provider reported a conflict."),
99
+ code: "PROVIDER_CONFLICT",
100
+ agent_hint: "The branch moved between read and write. Re-read the current state and rebuild the change before retrying once.",
101
+ developer_action: "Usually a concurrent write to the same branch; retrying the operation normally resolves it."
102
+ };
103
+ case 422: return {
104
+ error: withDetail("The git provider rejected the request as invalid."),
105
+ code: "PROVIDER_VALIDATION_FAILED",
106
+ agent_hint: "Well-formed but semantically rejected (e.g. the ref already exists, or a name is invalid). Do not retry unchanged.",
107
+ developer_action: "Inspect the detail above — a stale branch left over from an earlier run is the common cause."
108
+ };
109
+ default:
110
+ if (status >= 500) return {
111
+ error: withDetail("The git provider is unavailable."),
112
+ code: "PROVIDER_UNAVAILABLE",
113
+ agent_hint: "A server-side failure on the provider. Safe to retry once after a short pause; if it persists, stop and report it.",
114
+ developer_action: "Check the provider status page."
115
+ };
116
+ return {
117
+ error: withDetail(`The git provider returned HTTP ${status}.`),
118
+ code: "PROVIDER_REQUEST_FAILED",
119
+ agent_hint: "An unmapped provider failure. Report the status and detail to the developer rather than retrying blindly."
120
+ };
121
+ }
122
+ }
123
+ //#endregion
124
+ export { mapProviderError as n, isNotFoundError as t };
125
+
126
+ //# sourceMappingURL=errors-e0YdjooK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-e0YdjooK.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,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-AwBJIheA.mjs";
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-D4W0WIs4.mjs";
2
2
  export { BranchDiffResult, BranchHealthCheck, CleanupResult, RemoteBranchList, RemoteDeleteResult, RemotePruneResult, branchDiff, checkBranchHealth, classifyMergedBranches, cleanupMergedBranches, deleteRemoteBranch, isRefMerged, listRemoteCrBranches, pruneMergedRemoteBranches };
@@ -1,4 +1,4 @@
1
- import { a as RemoteDeleteResult } from "../branch-lifecycle-AwBJIheA.mjs";
1
+ import { a as RemoteDeleteResult } from "../branch-lifecycle-D4W0WIs4.mjs";
2
2
  import { SyncResult, WorkflowMode } from "@contentrain/types";
3
3
 
4
4
  //#region src/git/transaction.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.d.mts","names":[],"sources":["../../src/git/transaction.ts"],"mappings":";;;;UAciB,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,iBA4OW,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":";;;;UAciB,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"}
@@ -6,5 +6,5 @@ import "../model-manager-DP2CZiMT.mjs";
6
6
  import "../context-DjglDPvj.mjs";
7
7
  import "../branch-lifecycle-BAfgSQBv.mjs";
8
8
  import "../id-DV_T9Ic8.mjs";
9
- import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "../transaction-DC_dNON9.mjs";
9
+ import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "../transaction-1SPznNt3.mjs";
10
10
  export { buildBranchName, createTransaction, ensureContentBranch, mergeBranch };
@@ -1,5 +1,5 @@
1
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-BGUflS8t.mjs";
2
+ import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles } from "./scan-config-BlNLRCMx.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-CRUX_8mA.mjs.map
162
+ //# sourceMappingURL=graph-builder-DK4Mh8Tn.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-builder-CRUX_8mA.mjs","names":[],"sources":["../src/core/graph-builder.ts"],"sourcesContent":["import type { FileCategory, GraphNode, ProjectGraph } from '@contentrain/types'\nimport { join, relative, dirname, extname } from 'node:path'\nimport { readText, pathExists } from '../util/fs.js'\nimport {\n SCAN_EXTENSIONS,\n classifyFile,\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\n\nexport interface BuildGraphOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n}\n\nconst MAX_ORPHANS = 10\n\n// ---------------------------------------------------------------------------\n// Import extraction\n// ---------------------------------------------------------------------------\n\nconst IMPORT_RE = /(?:import|export)\\s+.*?from\\s+['\"]([^'\"]+)['\"]|(?:import|require)\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g\n\nfunction extractImportPaths(content: string): string[] {\n const paths: string[] = []\n let m: RegExpExecArray | null\n // Reset lastIndex before use since we reuse the regex\n IMPORT_RE.lastIndex = 0\n while ((m = IMPORT_RE.exec(content)) !== null) {\n const p = m[1] ?? m[2]\n if (p && isLocalImport(p)) {\n paths.push(p)\n }\n }\n return paths\n}\n\nfunction isLocalImport(p: string): boolean {\n return p.startsWith('.') || p.startsWith('/')\n}\n\n// ---------------------------------------------------------------------------\n// Component name extraction from imports\n// ---------------------------------------------------------------------------\n\nconst NAMED_IMPORT_RE = /import\\s+(?:type\\s+)?(?:(\\w+)(?:\\s*,\\s*)?)?(?:\\{([^}]*)\\})?\\s+from\\s+['\"][^'\"]+['\"]/g\nconst DEFAULT_IMPORT_RE = /import\\s+(?:type\\s+)?(\\w+)\\s+from\\s+['\"][^'\"]+['\"]/g\n\nfunction isPascalCase(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name)\n}\n\nfunction extractComponentNames(content: string): string[] {\n const names = new Set<string>()\n\n // Default imports: `import Button from '...'`\n DEFAULT_IMPORT_RE.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = DEFAULT_IMPORT_RE.exec(content)) !== null) {\n const name = m[1]\n if (name && isPascalCase(name)) {\n names.add(name)\n }\n }\n\n // Named imports: `import { Button, useHook } from '...'`\n NAMED_IMPORT_RE.lastIndex = 0\n while ((m = NAMED_IMPORT_RE.exec(content)) !== null) {\n // Default part before destructuring\n const defaultName = m[1]\n if (defaultName && isPascalCase(defaultName)) {\n names.add(defaultName)\n }\n // Destructured names\n const namedPart = m[2]\n if (namedPart) {\n for (const segment of namedPart.split(',')) {\n // Handle `Foo as Bar` — take the local name (Bar)\n const parts = segment.trim().split(/\\s+as\\s+/)\n const localName = (parts[1] ?? parts[0])?.trim()\n if (localName && isPascalCase(localName)) {\n names.add(localName)\n }\n }\n }\n }\n\n return [...names]\n}\n\n// ---------------------------------------------------------------------------\n// String counting (estimate)\n// ---------------------------------------------------------------------------\n\nconst STRING_SINGLE_RE = /'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'/g\nconst STRING_DOUBLE_RE = /\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"/g\nconst STRING_TEMPLATE_RE = /`[^`\\\\]*(?:\\\\.[^`\\\\]*)*`/g\n\nfunction countStrings(content: string): number {\n // Strip import/export/require lines first so their string literals don't count\n const stripped = content.replace(/^(?:import|export)\\s+.*$/gm, '')\n .replace(/\\brequire\\s*\\([^)]*\\)/g, '')\n\n const singles = stripped.match(STRING_SINGLE_RE)?.length ?? 0\n const doubles = stripped.match(STRING_DOUBLE_RE)?.length ?? 0\n const templates = stripped.match(STRING_TEMPLATE_RE)?.length ?? 0\n\n return singles + doubles + templates\n}\n\n// ---------------------------------------------------------------------------\n// Import path resolution\n// ---------------------------------------------------------------------------\n\nconst RESOLVE_EXTENSIONS = [...SCAN_EXTENSIONS]\nconst RESOLVE_INDEX_FILES = ['index.ts', 'index.tsx', 'index.js']\n\nasync function resolveImportPath(\n importPath: string,\n importerDir: string,\n projectRoot: string,\n): Promise<string | null> {\n const base = importPath.startsWith('/')\n ? join(projectRoot, importPath)\n : join(importerDir, importPath)\n\n // If it already has an extension, check directly\n if (extname(base)) {\n if (await pathExists(base)) {\n return base\n }\n return null\n }\n\n // Try adding extensions\n for (const ext of RESOLVE_EXTENSIONS) {\n const candidate = base + ext\n if (await pathExists(candidate)) {\n return candidate\n }\n }\n\n // Try index files inside directory\n for (const idx of RESOLVE_INDEX_FILES) {\n const candidate = join(base, idx)\n if (await pathExists(candidate)) {\n return candidate\n }\n }\n\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\ninterface FileInfo {\n relPath: string\n imports: string[]\n components: string[]\n strings: number\n category: FileCategory\n}\n\nexport async function buildGraph(\n projectRoot: string,\n options?: BuildGraphOptions,\n): Promise<ProjectGraph> {\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n\n // ---- File discovery (shared with scanner) ----\n const filePaths = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // ---- Pass 1: Parse each file ----\n const fileMap = new Map<string, FileInfo>()\n\n const parsePromises = filePaths.map(async (relPath) => {\n const absPath = join(projectRoot, relPath)\n const content = await readText(absPath)\n if (content === null) return\n\n const imports = extractImportPaths(content)\n const components = extractComponentNames(content)\n const strings = countStrings(content)\n const category = classifyFile(relPath)\n\n fileMap.set(relPath, { relPath, imports, components, strings, category })\n })\n\n await Promise.all(parsePromises)\n\n // ---- Pass 2: Resolve imports and build reverse lookup ----\n const usedByMap = new Map<string, Set<string>>()\n\n // Initialize usedBy sets\n for (const relPath of fileMap.keys()) {\n usedByMap.set(relPath, new Set())\n }\n\n const resolvePromises: Promise<void>[] = []\n\n for (const [relPath, info] of fileMap) {\n const importerAbsDir = dirname(join(projectRoot, relPath))\n\n for (const rawImport of info.imports) {\n resolvePromises.push(\n resolveImportPath(rawImport, importerAbsDir, projectRoot).then((resolved) => {\n if (!resolved) return\n const resolvedRel = relative(projectRoot, resolved)\n const targetSet = usedByMap.get(resolvedRel)\n if (targetSet) {\n targetSet.add(relPath)\n }\n }),\n )\n }\n }\n\n await Promise.all(resolvePromises)\n\n // ---- Build nodes ----\n const pages: GraphNode[] = []\n const components: GraphNode[] = []\n const layouts: GraphNode[] = []\n const orphanCandidates: string[] = []\n\n let totalStrings = 0\n\n for (const [relPath, info] of fileMap) {\n const usedBy = [...(usedByMap.get(relPath) ?? [])].toSorted((a, b) => a.localeCompare(b))\n totalStrings += info.strings\n\n // Filter out nodes with 0 strings — not relevant for content extraction\n if (info.strings === 0 && info.category !== 'other') continue\n\n const node: GraphNode = {\n file: relPath,\n category: info.category,\n imports: info.imports,\n used_by: usedBy,\n strings: info.strings,\n }\n\n // Only attach components list for pages\n if (info.category === 'page' && info.components.length > 0) {\n node.components = info.components\n }\n\n switch (info.category) {\n case 'page':\n pages.push(node)\n break\n case 'component':\n components.push(node)\n break\n case 'layout':\n layouts.push(node)\n break\n default:\n // Orphan = \"other\" category with no inbound references and no outbound imports\n if (usedBy.length === 0 && info.imports.length === 0) {\n orphanCandidates.push(relPath)\n }\n break\n }\n }\n\n return {\n pages: pages.toSorted((a, b) => a.file.localeCompare(b.file)),\n components: components.toSorted((a, b) => a.file.localeCompare(b.file)),\n layouts: layouts.toSorted((a, b) => a.file.localeCompare(b.file)),\n orphan_files: orphanCandidates.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_ORPHANS),\n stats: {\n total_files: fileMap.size,\n total_components: components.length,\n total_pages: pages.length,\n total_strings_estimate: totalStrings,\n },\n }\n}\n"],"mappings":";;;;AAgBA,MAAM,cAAc;AAMpB,MAAM,YAAY;AAElB,SAAS,mBAAmB,SAA2B;CACrD,MAAM,QAAkB,EAAE;CAC1B,IAAI;AAEJ,WAAU,YAAY;AACtB,SAAQ,IAAI,UAAU,KAAK,QAAQ,MAAM,MAAM;EAC7C,MAAM,IAAI,EAAE,MAAM,EAAE;AACpB,MAAI,KAAK,cAAc,EAAE,CACvB,OAAM,KAAK,EAAE;;AAGjB,QAAO;;AAGT,SAAS,cAAc,GAAoB;AACzC,QAAO,EAAE,WAAW,IAAI,IAAI,EAAE,WAAW,IAAI;;AAO/C,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAE1B,SAAS,aAAa,MAAuB;AAC3C,QAAO,sBAAsB,KAAK,KAAK;;AAGzC,SAAS,sBAAsB,SAA2B;CACxD,MAAM,wBAAQ,IAAI,KAAa;AAG/B,mBAAkB,YAAY;CAC9B,IAAI;AACJ,SAAQ,IAAI,kBAAkB,KAAK,QAAQ,MAAM,MAAM;EACrD,MAAM,OAAO,EAAE;AACf,MAAI,QAAQ,aAAa,KAAK,CAC5B,OAAM,IAAI,KAAK;;AAKnB,iBAAgB,YAAY;AAC5B,SAAQ,IAAI,gBAAgB,KAAK,QAAQ,MAAM,MAAM;EAEnD,MAAM,cAAc,EAAE;AACtB,MAAI,eAAe,aAAa,YAAY,CAC1C,OAAM,IAAI,YAAY;EAGxB,MAAM,YAAY,EAAE;AACpB,MAAI,UACF,MAAK,MAAM,WAAW,UAAU,MAAM,IAAI,EAAE;GAE1C,MAAM,QAAQ,QAAQ,MAAM,CAAC,MAAM,WAAW;GAC9C,MAAM,aAAa,MAAM,MAAM,MAAM,KAAK,MAAM;AAChD,OAAI,aAAa,aAAa,UAAU,CACtC,OAAM,IAAI,UAAU;;;AAM5B,QAAO,CAAC,GAAG,MAAM;;AAOnB,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAE3B,SAAS,aAAa,SAAyB;CAE7C,MAAM,WAAW,QAAQ,QAAQ,8BAA8B,GAAG,CAC/D,QAAQ,0BAA0B,GAAG;CAExC,MAAM,UAAU,SAAS,MAAM,iBAAiB,EAAE,UAAU;CAC5D,MAAM,UAAU,SAAS,MAAM,iBAAiB,EAAE,UAAU;CAC5D,MAAM,YAAY,SAAS,MAAM,mBAAmB,EAAE,UAAU;AAEhE,QAAO,UAAU,UAAU;;AAO7B,MAAM,qBAAqB,CAAC,GAAG,gBAAgB;AAC/C,MAAM,sBAAsB;CAAC;CAAY;CAAa;CAAW;AAEjE,eAAe,kBACb,YACA,aACA,aACwB;CACxB,MAAM,OAAO,WAAW,WAAW,IAAI,GACnC,KAAK,aAAa,WAAW,GAC7B,KAAK,aAAa,WAAW;AAGjC,KAAI,QAAQ,KAAK,EAAE;AACjB,MAAI,MAAM,WAAW,KAAK,CACxB,QAAO;AAET,SAAO;;AAIT,MAAK,MAAM,OAAO,oBAAoB;EACpC,MAAM,YAAY,OAAO;AACzB,MAAI,MAAM,WAAW,UAAU,CAC7B,QAAO;;AAKX,MAAK,MAAM,OAAO,qBAAqB;EACrC,MAAM,YAAY,KAAK,MAAM,IAAI;AACjC,MAAI,MAAM,WAAW,UAAU,CAC7B,QAAO;;AAIX,QAAO;;AAeT,eAAsB,WACpB,aACA,SACuB;CAIvB,MAAM,YAAY,MAAM,cAAc,aAAa;EACjD,OAJe,SAAS,SAAS,MAAM,qBAAqB,YAAY;EAKxE,SAAS,SAAS;EAClB,SAAS,SAAS;EACnB,CAAC;CAGF,MAAM,0BAAU,IAAI,KAAuB;CAE3C,MAAM,gBAAgB,UAAU,IAAI,OAAO,YAAY;EAErD,MAAM,UAAU,MAAM,SADN,KAAK,aAAa,QAAQ,CACH;AACvC,MAAI,YAAY,KAAM;EAEtB,MAAM,UAAU,mBAAmB,QAAQ;EAC3C,MAAM,aAAa,sBAAsB,QAAQ;EACjD,MAAM,UAAU,aAAa,QAAQ;EACrC,MAAM,WAAW,aAAa,QAAQ;AAEtC,UAAQ,IAAI,SAAS;GAAE;GAAS;GAAS;GAAY;GAAS;GAAU,CAAC;GACzE;AAEF,OAAM,QAAQ,IAAI,cAAc;CAGhC,MAAM,4BAAY,IAAI,KAA0B;AAGhD,MAAK,MAAM,WAAW,QAAQ,MAAM,CAClC,WAAU,IAAI,yBAAS,IAAI,KAAK,CAAC;CAGnC,MAAM,kBAAmC,EAAE;AAE3C,MAAK,MAAM,CAAC,SAAS,SAAS,SAAS;EACrC,MAAM,iBAAiB,QAAQ,KAAK,aAAa,QAAQ,CAAC;AAE1D,OAAK,MAAM,aAAa,KAAK,QAC3B,iBAAgB,KACd,kBAAkB,WAAW,gBAAgB,YAAY,CAAC,MAAM,aAAa;AAC3E,OAAI,CAAC,SAAU;GACf,MAAM,cAAc,SAAS,aAAa,SAAS;GACnD,MAAM,YAAY,UAAU,IAAI,YAAY;AAC5C,OAAI,UACF,WAAU,IAAI,QAAQ;IAExB,CACH;;AAIL,OAAM,QAAQ,IAAI,gBAAgB;CAGlC,MAAM,QAAqB,EAAE;CAC7B,MAAM,aAA0B,EAAE;CAClC,MAAM,UAAuB,EAAE;CAC/B,MAAM,mBAA6B,EAAE;CAErC,IAAI,eAAe;AAEnB,MAAK,MAAM,CAAC,SAAS,SAAS,SAAS;EACrC,MAAM,SAAS,CAAC,GAAI,UAAU,IAAI,QAAQ,IAAI,EAAE,CAAE,CAAC,UAAU,GAAG,MAAM,EAAE,cAAc,EAAE,CAAC;AACzF,kBAAgB,KAAK;AAGrB,MAAI,KAAK,YAAY,KAAK,KAAK,aAAa,QAAS;EAErD,MAAM,OAAkB;GACtB,MAAM;GACN,UAAU,KAAK;GACf,SAAS,KAAK;GACd,SAAS;GACT,SAAS,KAAK;GACf;AAGD,MAAI,KAAK,aAAa,UAAU,KAAK,WAAW,SAAS,EACvD,MAAK,aAAa,KAAK;AAGzB,UAAQ,KAAK,UAAb;GACE,KAAK;AACH,UAAM,KAAK,KAAK;AAChB;GACF,KAAK;AACH,eAAW,KAAK,KAAK;AACrB;GACF,KAAK;AACH,YAAQ,KAAK,KAAK;AAClB;GACF;AAEE,QAAI,OAAO,WAAW,KAAK,KAAK,QAAQ,WAAW,EACjD,kBAAiB,KAAK,QAAQ;AAEhC;;;AAIN,QAAO;EACL,OAAO,MAAM,UAAU,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,KAAK,CAAC;EAC7D,YAAY,WAAW,UAAU,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,KAAK,CAAC;EACvE,SAAS,QAAQ,UAAU,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,KAAK,CAAC;EACjE,cAAc,iBAAiB,UAAU,GAAG,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC,MAAM,GAAG,YAAY;EAC3F,OAAO;GACL,aAAa,QAAQ;GACrB,kBAAkB,WAAW;GAC7B,aAAa,MAAM;GACnB,wBAAwB;GACzB;EACF"}
1
+ {"version":3,"file":"graph-builder-DK4Mh8Tn.mjs","names":[],"sources":["../src/core/graph-builder.ts"],"sourcesContent":["import type { FileCategory, GraphNode, ProjectGraph } from '@contentrain/types'\nimport { join, relative, dirname, extname } from 'node:path'\nimport { readText, pathExists } from '../util/fs.js'\nimport {\n SCAN_EXTENSIONS,\n classifyFile,\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\n\nexport interface BuildGraphOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n}\n\nconst MAX_ORPHANS = 10\n\n// ---------------------------------------------------------------------------\n// Import extraction\n// ---------------------------------------------------------------------------\n\nconst IMPORT_RE = /(?:import|export)\\s+.*?from\\s+['\"]([^'\"]+)['\"]|(?:import|require)\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g\n\nfunction extractImportPaths(content: string): string[] {\n const paths: string[] = []\n let m: RegExpExecArray | null\n // Reset lastIndex before use since we reuse the regex\n IMPORT_RE.lastIndex = 0\n while ((m = IMPORT_RE.exec(content)) !== null) {\n const p = m[1] ?? m[2]\n if (p && isLocalImport(p)) {\n paths.push(p)\n }\n }\n return paths\n}\n\nfunction isLocalImport(p: string): boolean {\n return p.startsWith('.') || p.startsWith('/')\n}\n\n// ---------------------------------------------------------------------------\n// Component name extraction from imports\n// ---------------------------------------------------------------------------\n\nconst NAMED_IMPORT_RE = /import\\s+(?:type\\s+)?(?:(\\w+)(?:\\s*,\\s*)?)?(?:\\{([^}]*)\\})?\\s+from\\s+['\"][^'\"]+['\"]/g\nconst DEFAULT_IMPORT_RE = /import\\s+(?:type\\s+)?(\\w+)\\s+from\\s+['\"][^'\"]+['\"]/g\n\nfunction isPascalCase(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name)\n}\n\nfunction extractComponentNames(content: string): string[] {\n const names = new Set<string>()\n\n // Default imports: `import Button from '...'`\n DEFAULT_IMPORT_RE.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = DEFAULT_IMPORT_RE.exec(content)) !== null) {\n const name = m[1]\n if (name && isPascalCase(name)) {\n names.add(name)\n }\n }\n\n // Named imports: `import { Button, useHook } from '...'`\n NAMED_IMPORT_RE.lastIndex = 0\n while ((m = NAMED_IMPORT_RE.exec(content)) !== null) {\n // Default part before destructuring\n const defaultName = m[1]\n if (defaultName && isPascalCase(defaultName)) {\n names.add(defaultName)\n }\n // Destructured names\n const namedPart = m[2]\n if (namedPart) {\n for (const segment of namedPart.split(',')) {\n // Handle `Foo as Bar` — take the local name (Bar)\n const parts = segment.trim().split(/\\s+as\\s+/)\n const localName = (parts[1] ?? parts[0])?.trim()\n if (localName && isPascalCase(localName)) {\n names.add(localName)\n }\n }\n }\n }\n\n return [...names]\n}\n\n// ---------------------------------------------------------------------------\n// String counting (estimate)\n// ---------------------------------------------------------------------------\n\nconst STRING_SINGLE_RE = /'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'/g\nconst STRING_DOUBLE_RE = /\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"/g\nconst STRING_TEMPLATE_RE = /`[^`\\\\]*(?:\\\\.[^`\\\\]*)*`/g\n\nfunction countStrings(content: string): number {\n // Strip import/export/require lines first so their string literals don't count\n const stripped = content.replace(/^(?:import|export)\\s+.*$/gm, '')\n .replace(/\\brequire\\s*\\([^)]*\\)/g, '')\n\n const singles = stripped.match(STRING_SINGLE_RE)?.length ?? 0\n const doubles = stripped.match(STRING_DOUBLE_RE)?.length ?? 0\n const templates = stripped.match(STRING_TEMPLATE_RE)?.length ?? 0\n\n return singles + doubles + templates\n}\n\n// ---------------------------------------------------------------------------\n// Import path resolution\n// ---------------------------------------------------------------------------\n\nconst RESOLVE_EXTENSIONS = [...SCAN_EXTENSIONS]\nconst RESOLVE_INDEX_FILES = ['index.ts', 'index.tsx', 'index.js']\n\nasync function resolveImportPath(\n importPath: string,\n importerDir: string,\n projectRoot: string,\n): Promise<string | null> {\n const base = importPath.startsWith('/')\n ? join(projectRoot, importPath)\n : join(importerDir, importPath)\n\n // If it already has an extension, check directly\n if (extname(base)) {\n if (await pathExists(base)) {\n return base\n }\n return null\n }\n\n // Try adding extensions\n for (const ext of RESOLVE_EXTENSIONS) {\n const candidate = base + ext\n if (await pathExists(candidate)) {\n return candidate\n }\n }\n\n // Try index files inside directory\n for (const idx of RESOLVE_INDEX_FILES) {\n const candidate = join(base, idx)\n if (await pathExists(candidate)) {\n return candidate\n }\n }\n\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\ninterface FileInfo {\n relPath: string\n imports: string[]\n components: string[]\n strings: number\n category: FileCategory\n}\n\nexport async function buildGraph(\n projectRoot: string,\n options?: BuildGraphOptions,\n): Promise<ProjectGraph> {\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n\n // ---- File discovery (shared with scanner) ----\n const filePaths = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // ---- Pass 1: Parse each file ----\n const fileMap = new Map<string, FileInfo>()\n\n const parsePromises = filePaths.map(async (relPath) => {\n const absPath = join(projectRoot, relPath)\n const content = await readText(absPath)\n if (content === null) return\n\n const imports = extractImportPaths(content)\n const components = extractComponentNames(content)\n const strings = countStrings(content)\n const category = classifyFile(relPath)\n\n fileMap.set(relPath, { relPath, imports, components, strings, category })\n })\n\n await Promise.all(parsePromises)\n\n // ---- Pass 2: Resolve imports and build reverse lookup ----\n const usedByMap = new Map<string, Set<string>>()\n\n // Initialize usedBy sets\n for (const relPath of fileMap.keys()) {\n usedByMap.set(relPath, new Set())\n }\n\n const resolvePromises: Promise<void>[] = []\n\n for (const [relPath, info] of fileMap) {\n const importerAbsDir = dirname(join(projectRoot, relPath))\n\n for (const rawImport of info.imports) {\n resolvePromises.push(\n resolveImportPath(rawImport, importerAbsDir, projectRoot).then((resolved) => {\n if (!resolved) return\n const resolvedRel = relative(projectRoot, resolved)\n const targetSet = usedByMap.get(resolvedRel)\n if (targetSet) {\n targetSet.add(relPath)\n }\n }),\n )\n }\n }\n\n await Promise.all(resolvePromises)\n\n // ---- Build nodes ----\n const pages: GraphNode[] = []\n const components: GraphNode[] = []\n const layouts: GraphNode[] = []\n const orphanCandidates: string[] = []\n\n let totalStrings = 0\n\n for (const [relPath, info] of fileMap) {\n const usedBy = [...(usedByMap.get(relPath) ?? [])].toSorted((a, b) => a.localeCompare(b))\n totalStrings += info.strings\n\n // Filter out nodes with 0 strings — not relevant for content extraction\n if (info.strings === 0 && info.category !== 'other') continue\n\n const node: GraphNode = {\n file: relPath,\n category: info.category,\n imports: info.imports,\n used_by: usedBy,\n strings: info.strings,\n }\n\n // Only attach components list for pages\n if (info.category === 'page' && info.components.length > 0) {\n node.components = info.components\n }\n\n switch (info.category) {\n case 'page':\n pages.push(node)\n break\n case 'component':\n components.push(node)\n break\n case 'layout':\n layouts.push(node)\n break\n default:\n // Orphan = \"other\" category with no inbound references and no outbound imports\n if (usedBy.length === 0 && info.imports.length === 0) {\n orphanCandidates.push(relPath)\n }\n break\n }\n }\n\n return {\n pages: pages.toSorted((a, b) => a.file.localeCompare(b.file)),\n components: components.toSorted((a, b) => a.file.localeCompare(b.file)),\n layouts: layouts.toSorted((a, b) => a.file.localeCompare(b.file)),\n orphan_files: orphanCandidates.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_ORPHANS),\n stats: {\n total_files: fileMap.size,\n total_components: components.length,\n total_pages: pages.length,\n total_strings_estimate: totalStrings,\n },\n }\n}\n"],"mappings":"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"}