rcf-lite 0.0.1 → 0.7.1

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 (342) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +69 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/browser-verify/auth-smoke.js +109 -0
  177. package/src/browser-verify/index.js +29 -0
  178. package/src/browser-verify/invariants.js +336 -0
  179. package/src/browser-verify/manifest-writer.js +189 -0
  180. package/src/browser-verify/runner.js +171 -0
  181. package/src/build/bundle.js +198 -0
  182. package/src/build/formatters/json.js +18 -0
  183. package/src/build/formatters/markdown.js +406 -0
  184. package/src/build/index.js +14 -0
  185. package/src/build/mark.js +177 -0
  186. package/src/build/queue.js +285 -0
  187. package/src/cli/browser-verify.js +231 -0
  188. package/src/cli/build.js +584 -0
  189. package/src/cli/coverage.js +219 -0
  190. package/src/cli/create.js +355 -0
  191. package/src/cli/delete.js +127 -0
  192. package/src/cli/design.js +353 -0
  193. package/src/cli/doctor.js +548 -0
  194. package/src/cli/fbs.js +171 -0
  195. package/src/cli/finalise.js +377 -0
  196. package/src/cli/guidance.js +206 -0
  197. package/src/cli/help.js +156 -0
  198. package/src/cli/impact.js +119 -0
  199. package/src/cli/init.js +282 -0
  200. package/src/cli/intake.js +153 -0
  201. package/src/cli/link.js +128 -0
  202. package/src/cli/mcp.js +160 -0
  203. package/src/cli/preflight.js +220 -0
  204. package/src/cli/read.js +162 -0
  205. package/src/cli/req-baseline.js +269 -0
  206. package/src/cli/req-classify.js +135 -0
  207. package/src/cli/review.js +295 -0
  208. package/src/cli/test-suite.js +216 -0
  209. package/src/cli/trace.js +172 -0
  210. package/src/cli/ui-baseline.js +292 -0
  211. package/src/cli/ui-classify.js +108 -0
  212. package/src/cli/update.js +197 -0
  213. package/src/cli/validate.js +168 -0
  214. package/src/cli/view.js +452 -0
  215. package/src/core/baseline-catalog/data/auth.json +42 -0
  216. package/src/core/baseline-catalog/data/http-api.json +42 -0
  217. package/src/core/baseline-catalog/data/notifications.json +33 -0
  218. package/src/core/baseline-catalog/data/persistence.json +33 -0
  219. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  220. package/src/core/baseline-catalog/index.js +121 -0
  221. package/src/core/errors/index.js +167 -0
  222. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  223. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  224. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  225. package/src/core/isolation/index.js +60 -0
  226. package/src/core/mcp/framing.js +103 -0
  227. package/src/core/mcp/index.js +8 -0
  228. package/src/core/mcp/server.js +228 -0
  229. package/src/core/patterns/register-canary.js +209 -0
  230. package/src/core/patterns/req-shapes.js +158 -0
  231. package/src/core/patterns/services.js +358 -0
  232. package/src/core/patterns/ui-shapes.js +166 -0
  233. package/src/core/store/cn-resolve.js +134 -0
  234. package/src/core/store/derive-deps.js +93 -0
  235. package/src/core/store/ids.js +78 -0
  236. package/src/core/store/index.js +20 -0
  237. package/src/core/store/init.js +255 -0
  238. package/src/core/store/loader.js +211 -0
  239. package/src/core/store/tp-resolve.js +176 -0
  240. package/src/core/store/validator.js +191 -0
  241. package/src/core/store/walker.js +898 -0
  242. package/src/core/store/writer.js +1849 -0
  243. package/src/design/index.js +11 -0
  244. package/src/design/writer.js +271 -0
  245. package/src/finalise/detect.js +129 -0
  246. package/src/finalise/index.js +17 -0
  247. package/src/finalise/ingest.js +119 -0
  248. package/src/finalise/install.js +119 -0
  249. package/src/finalise/ship-without-verified.js +131 -0
  250. package/src/finalise/spawn.js +84 -0
  251. package/src/intake/fidelity.js +105 -0
  252. package/src/intake/index.js +6 -0
  253. package/src/intake/manifest-writer.js +100 -0
  254. package/src/intake/orchestrator.js +138 -0
  255. package/src/intake/validate.js +80 -0
  256. package/src/mcp/map-errors.js +131 -0
  257. package/src/mcp/prompts.js +49 -0
  258. package/src/mcp/resources.js +244 -0
  259. package/src/mcp/tools.js +1204 -0
  260. package/src/preflight/design-shapes.js +185 -0
  261. package/src/preflight/index.js +90 -0
  262. package/src/preflight/manifest-writer.js +264 -0
  263. package/src/preflight/scanner.js +206 -0
  264. package/src/preflight/secrets.js +134 -0
  265. package/src/preflight/session.js +246 -0
  266. package/src/query/attestation.js +285 -0
  267. package/src/query/coverage.js +308 -0
  268. package/src/query/formatters/json.js +21 -0
  269. package/src/query/formatters/mermaid.js +209 -0
  270. package/src/query/formatters/table.js +203 -0
  271. package/src/query/impact.js +173 -0
  272. package/src/query/index.js +9 -0
  273. package/src/query/trace.js +345 -0
  274. package/src/register-canary/fixture-loader.js +87 -0
  275. package/src/register-canary/index.js +10 -0
  276. package/src/register-canary/record-writer.js +132 -0
  277. package/src/register-canary/runner.js +156 -0
  278. package/src/req-baseline/gate.js +86 -0
  279. package/src/req-baseline/index.js +27 -0
  280. package/src/req-baseline/open-candidates.js +143 -0
  281. package/src/req-baseline/opt-out.js +195 -0
  282. package/src/req-baseline/sweep.js +230 -0
  283. package/src/req-detection/classifier.js +181 -0
  284. package/src/req-detection/index.js +9 -0
  285. package/src/req-detection/persist.js +55 -0
  286. package/src/review/index.js +325 -0
  287. package/src/review/mutation.js +117 -0
  288. package/src/review/ui-baseline-drift.js +138 -0
  289. package/src/server/index.js +178 -0
  290. package/src/server/routes.js +110 -0
  291. package/src/server/sse.js +118 -0
  292. package/src/setup/agent-setup.js +362 -0
  293. package/src/setup/identity-seed.js +104 -0
  294. package/src/setup/knowledge-seed.js +123 -0
  295. package/src/setup/managed-block.js +193 -0
  296. package/src/setup/managed-gitignore.js +166 -0
  297. package/src/setup/managed-markers.js +49 -0
  298. package/src/ui-baseline/defaults.js +119 -0
  299. package/src/ui-baseline/index.js +25 -0
  300. package/src/ui-baseline/manifest-writer.js +282 -0
  301. package/src/ui-baseline/session.js +178 -0
  302. package/src/ui-detection/classifier.js +192 -0
  303. package/src/verify/chain/index.js +190 -0
  304. package/src/verify/cli/cleanup.js +61 -0
  305. package/src/verify/cli/help.js +56 -0
  306. package/src/verify/cli/mcp.js +98 -0
  307. package/src/verify/cli/provision.js +71 -0
  308. package/src/verify/cli/report.js +71 -0
  309. package/src/verify/cli/run.js +155 -0
  310. package/src/verify/engine/brief.js +87 -0
  311. package/src/verify/engine/index.js +177 -0
  312. package/src/verify/engine/launcher.js +307 -0
  313. package/src/verify/mcp/tools.js +107 -0
  314. package/src/verify/profile/index.js +146 -0
  315. package/src/verify/provision/index.js +256 -0
  316. package/src/verify/report/index.js +139 -0
  317. package/src/verify/report/renderer.js +118 -0
  318. package/src/verify/verdict/index.js +246 -0
  319. package/src/view/doc-renderers/adr.js +44 -0
  320. package/src/view/doc-renderers/build-sequence.js +40 -0
  321. package/src/view/doc-renderers/fbs.js +128 -0
  322. package/src/view/doc-renderers/helpers.js +159 -0
  323. package/src/view/doc-renderers/index.js +12 -0
  324. package/src/view/doc-renderers/prd.js +45 -0
  325. package/src/view/doc-renderers/req.js +43 -0
  326. package/src/view/doc-renderers/tac.js +38 -0
  327. package/src/view/doc-renderers/tad.js +74 -0
  328. package/src/view/doc-renderers/test-suite.js +45 -0
  329. package/src/view/doc-renderers/user-story.js +63 -0
  330. package/src/view/html-page.js +462 -0
  331. package/src/view/index.js +63 -0
  332. package/src/view/live-client.js +338 -0
  333. package/src/view/mermaid-diagram.js +178 -0
  334. package/src/view/style.css +735 -0
  335. package/src/view/tree-model.js +152 -0
  336. package/src/view/vendored/mermaid.min.js +2607 -0
  337. package/src/view-supervisor/index.js +26 -0
  338. package/src/view-supervisor/logs.js +32 -0
  339. package/src/view-supervisor/manifest-writer.js +178 -0
  340. package/src/view-supervisor/persist-until.js +85 -0
  341. package/src/view-supervisor/supervisor.js +276 -0
  342. package/src/watch/index.js +152 -0
@@ -0,0 +1,93 @@
1
+ // Phase 10 (X2 CodeNode bridge, spec D5): optional dev-time assist that
2
+ // shells out to `dependency-cruiser` for file-level dependency
3
+ // auto-derivation. NEVER a runtime dependency - `rcf` carries a public
4
+ // zero-third-party-runtime-deps claim (Phase 9 D14) that this module does
5
+ // not touch: it only ever invokes an external binary via child_process,
6
+ // only when the caller opts in with `--derive-deps`, and it never appears
7
+ // in package.json.
8
+ //
9
+ // PoC exp 3 evidence: dependency-cruiser beat hand-declaration at file
10
+ // level (8/8 agreement on hand-declared edges, plus 3 real edges the
11
+ // human missed - 73% human recall). Symbol-level derivation is out of
12
+ // reach for this tool (file-granular only, PoC-confirmed); symbol
13
+ // dependencies stay hand-declared or omitted (D5, out of scope SS6).
14
+ //
15
+ // `--no-install` on the npx invocation is load-bearing: this module must
16
+ // never trigger a network install as a side effect of a CLI flag. When
17
+ // the tool is not resolvable (no local install, npx would need to
18
+ // fetch it), the caller gets a helpful error, not a silent hang or a
19
+ // surprise download.
20
+
21
+ import { execFile } from 'node:child_process';
22
+ import { promisify } from 'node:util';
23
+
24
+ const execFileAsync = promisify(execFile);
25
+
26
+ export const NOT_RESOLVABLE_MESSAGE =
27
+ 'dependency-cruiser is not resolvable (no local install, and npx --no-install will not fetch it). '
28
+ + 'Install it as a dev dependency in this project (npm install --save-dev dependency-cruiser) to use '
29
+ + '--derive-deps, or declare --deps by hand.';
30
+
31
+ /**
32
+ * Derive the file-level dependencies of `filePath` (repo-relative, no
33
+ * `#symbol` suffix) via `dependency-cruiser`. Resolved locally or via
34
+ * `npx --no-install`; never attempts to install anything.
35
+ *
36
+ * @param {object} args
37
+ * @param {string} args.projectRoot - absolute path to project root
38
+ * @param {string} args.filePath - repo-relative source path (no #symbol)
39
+ * @param {(cmd: string, cmdArgs: string[], opts: object) => Promise<{stdout: string}>} [args.exec]
40
+ * Injection seam for tests - defaults to the real child_process call.
41
+ * @returns {Promise<{ ok: true, deps: string[] } | { ok: false, message: string }>}
42
+ */
43
+ export async function deriveFileDeps({ projectRoot, filePath, exec = execFileAsync }) {
44
+ let stdout;
45
+ try {
46
+ ({ stdout } = await exec('npx', ['--no-install', 'dependency-cruiser', '--output-type', 'json', filePath], {
47
+ cwd: projectRoot,
48
+ }));
49
+ } catch {
50
+ return { ok: false, message: NOT_RESOLVABLE_MESSAGE };
51
+ }
52
+ let parsed;
53
+ try {
54
+ parsed = JSON.parse(stdout);
55
+ } catch (err) {
56
+ return { ok: false, message: `dependency-cruiser produced unparseable output: ${err.message}` };
57
+ }
58
+ const modules = Array.isArray(parsed?.modules) ? parsed.modules : [];
59
+ const self = modules.find((m) => m.source === filePath);
60
+ const deps = (self?.dependencies ?? [])
61
+ .filter((d) => d.resolved && d.dependencyTypes?.every((t) => t !== 'npm' && t !== 'core'))
62
+ .map((d) => d.resolved)
63
+ .sort();
64
+ return { ok: true, deps: [...new Set(deps)] };
65
+ }
66
+
67
+ /**
68
+ * Map derived file-level dependency paths to existing Code Node ids
69
+ * whose `path` (file part, ignoring any `#symbol` suffix) matches. A
70
+ * derived file with no matching CN cannot become a dependency edge (D3:
71
+ * `dependencies[]` is CN -> CN, not CN -> bare file) - such files are
72
+ * reported separately so the caller can surface them informationally.
73
+ *
74
+ * @param {object} tree - walker TreeModel
75
+ * @param {string[]} filePaths
76
+ * @returns {{ cnIds: string[], unmatched: string[] }}
77
+ */
78
+ export function mapDerivedDepsToCnIds(tree, filePaths) {
79
+ const cnIds = new Set();
80
+ const unmatched = [];
81
+ for (const filePath of filePaths) {
82
+ const matches = (tree.codeNodes ?? [])
83
+ .filter((cn) => (cn.path ?? '').split('#')[0] === filePath)
84
+ .map((cn) => cn.cnId)
85
+ .sort();
86
+ if (matches.length === 0) {
87
+ unmatched.push(filePath);
88
+ } else {
89
+ for (const id of matches) cnIds.add(id);
90
+ }
91
+ }
92
+ return { cnIds: [...cnIds].sort(), unmatched };
93
+ }
@@ -0,0 +1,78 @@
1
+ // Id normalisation. The RCF id patterns in `@stravica-ai/rcf-schemas`
2
+ // admit a variable-width numeric run (`^REQ-\d{3,}$`, `^US-\d{3,}$`,
3
+ // `^AC-\d{3,}(-\d+)?$`, ...), so `REQ-001` and `REQ-0001` are BOTH legal
4
+ // and BOTH name requirement number 1. The schema is right to permit the
5
+ // widths -- an id space that outgrows three digits has to be expressible
6
+ // -- but two spellings of one number are one identity, not two.
7
+ //
8
+ // This module owns the single definition of "the same id" used by the
9
+ // walker's uniqueness rule (`globallyUniqueIds`) and by the writer's id
10
+ // allocator. Both sides MUST agree: detection that normalises while
11
+ // allocation does not just moves the collision one step later.
12
+ //
13
+ // Normalisation is per hyphen-delimited segment and only touches
14
+ // segments that are entirely digits:
15
+ //
16
+ // REQ-001 -> REQ-1
17
+ // REQ-0001 -> REQ-1 (collides with REQ-001, correctly)
18
+ // AC-101-01 -> AC-101-1 (collides with AC-101-1, correctly)
19
+ // TC-001-step2 -> TC-1-step2 (the slug segment is left alone)
20
+ //
21
+ // Leaving non-numeric segments untouched is deliberate: a TC slug like
22
+ // `step02` is a word, not a number, and must not be folded into `step2`.
23
+
24
+ /**
25
+ * Strip leading zeros from a run of digits without going through Number
26
+ * (precision-safe for arbitrarily long numeric segments).
27
+ *
28
+ * @param {string} digits
29
+ * @returns {string}
30
+ */
31
+ function stripLeadingZeros(digits) {
32
+ const trimmed = digits.replace(/^0+(?=\d)/, '');
33
+ return trimmed.length > 0 ? trimmed : '0';
34
+ }
35
+
36
+ /**
37
+ * Canonical form of an RCF id for identity comparison. Two ids are the
38
+ * same id if and only if their normalised forms are equal.
39
+ *
40
+ * @param {unknown} id
41
+ * @returns {string} normalised id, or '' when the input is not a string
42
+ */
43
+ export function normaliseId(id) {
44
+ if (typeof id !== 'string') return '';
45
+ return id
46
+ .split('-')
47
+ .map((segment) => (/^\d+$/.test(segment) ? stripLeadingZeros(segment) : segment))
48
+ .join('-');
49
+ }
50
+
51
+ /**
52
+ * Identity comparison for two RCF ids, tolerant of leading-zero spelling.
53
+ *
54
+ * @param {unknown} a
55
+ * @param {unknown} b
56
+ * @returns {boolean}
57
+ */
58
+ export function sameId(a, b) {
59
+ const na = normaliseId(a);
60
+ return na.length > 0 && na === normaliseId(b);
61
+ }
62
+
63
+ /**
64
+ * Numeric value of an id's first numeric segment after the prefix, or
65
+ * null when the id does not match `<PREFIX>-<digits>`. Used by the
66
+ * allocator to reason about id numbers independently of their spelling.
67
+ *
68
+ * @param {unknown} id
69
+ * @param {string} prefix - e.g. 'REQ', 'US'
70
+ * @returns {number|null}
71
+ */
72
+ export function idNumber(id, prefix) {
73
+ if (typeof id !== 'string') return null;
74
+ const m = new RegExp(`^${prefix}-(\\d+)(?:-|$)`).exec(id);
75
+ if (!m) return null;
76
+ const n = Number(m[1]);
77
+ return Number.isSafeInteger(n) ? n : null;
78
+ }
@@ -0,0 +1,20 @@
1
+ // Document store - public surface for the Phase 3 dispatch.
2
+ //
3
+ // Every consumer (the view, future CLI verbs, future MCP tools) reaches the
4
+ // filesystem through this module. No other layer reads from or writes to
5
+ // rcf/ directly.
6
+
7
+ export { loadDocument, loadRootDocument, pathForId, rootPathFor, subdirFor } from './loader.js';
8
+ // w-2026-07-28-017: id identity (leading-zero-tolerant). Shared by the
9
+ // walker's globallyUniqueIds rule and the writer's allocator.
10
+ export { idNumber, normaliseId, sameId } from './ids.js';
11
+ export { validateDocument, idFieldFor, documentIdOf, knownKinds } from './validator.js';
12
+ export { netNewErrors, simulateWriteErrors, walkTree } from './walker.js';
13
+ export { initProject } from './init.js';
14
+ // Phase 10 (X2 CodeNode bridge): Code Node working-tree staleness check.
15
+ export { checkCodeNodeResolution, splitCnPath } from './cn-resolve.js';
16
+ // w-2026-07-28-005: Test Case pointer working-tree resolution (test-axis
17
+ // twin of cn-resolve). Coverage consumes this - an unresolved pointer is
18
+ // never counted as covering an AC.
19
+ export { resolveTestPointers, splitTestPointer, testCaseKey } from './tp-resolve.js';
20
+ export { nextIdForKind, createDocument, updateDocument, deleteDocument, deriveSlug } from './writer.js';
@@ -0,0 +1,255 @@
1
+ // Project scaffolding. Creates a minimum valid rcf/ tree (manifest plus a
2
+ // placeholder PRD, REQ, US, TAD, TAC, ADR, BS, FBS) so a fresh repo has a
3
+ // schema-clean starting point. Every required field carries a TODO marker
4
+ // so the owner can see what they need to fill in.
5
+ //
6
+ // AC-101-2: refuses to overwrite an existing project. If rcf/manifest.json
7
+ // already exists, this function writes nothing and returns a usage error.
8
+ //
9
+ // Phase 3.7 D14 shape: parent-child edges live on the child. The PRD
10
+ // no longer carries `requirementIds`; each REQ carries `prdId` (already
11
+ // present). The TAD no longer carries `componentIds` /
12
+ // `architecturalDecisionIds`; each TAC / ADR carries `tadId`. The BS
13
+ // no longer carries `fbs[]`; each FBS carries `bsId`, `buildOrder`,
14
+ // `executionStatus` and `dependsOnFbsIds`. The empty `rcf/test-suites/`
15
+ // directory is scaffolded so a future authored TS drops into a
16
+ // pre-existing shape.
17
+ //
18
+ // Phase 4 D5: `seed` accepts overrides for the four interactive prompts
19
+ // (`prdProblemStatement`, `reqTitle`, `usTitle`, and any of the values
20
+ // the interactive `rcf init` UX collects). When `seed.interactive` is
21
+ // truthy the ADR-001 template starts in `draft` rather than `proposed`,
22
+ // matching interactive-mode intent (D22 amendment).
23
+
24
+ import { mkdir, stat, writeFile } from 'node:fs/promises';
25
+ import { dirname, join } from 'node:path';
26
+
27
+ import { rcfError } from '../errors/index.js';
28
+
29
+ const TIMESTAMP = '2026-01-01T00:00:00Z';
30
+
31
+ function manifestTemplate(projectName) {
32
+ return {
33
+ version: '2.0.0',
34
+ projectName,
35
+ description: 'RCF project manifest. Roots are declared here; children are walked from the roots.',
36
+ prd: { id: 'PRD-001', path: 'prd.json' },
37
+ tad: { id: 'TAD-001', path: 'tad.json' },
38
+ bs: { id: 'BS-001', path: 'build-sequence.json' },
39
+ };
40
+ }
41
+
42
+ function prdTemplate(projectName, seed) {
43
+ return {
44
+ prdId: 'PRD-001',
45
+ productName: projectName,
46
+ version: '0.1.0',
47
+ status: 'draft',
48
+ problemStatement: seed?.prdProblemStatement ?? 'TODO: state the problem this product solves.',
49
+ objectives: seed?.prdProblemStatement
50
+ ? [seed.prdProblemStatement]
51
+ : ['TODO: add at least one objective.'],
52
+ createdAt: TIMESTAMP,
53
+ updatedAt: TIMESTAMP,
54
+ };
55
+ }
56
+
57
+ function reqTemplate(seed) {
58
+ return {
59
+ reqId: 'REQ-001',
60
+ prdId: 'PRD-001',
61
+ title: seed?.reqTitle ?? 'TODO: name this requirement',
62
+ description: seed?.reqTitle ?? 'TODO: describe this requirement.',
63
+ category: 'functional',
64
+ domain: 'todo',
65
+ priority: 'must',
66
+ version: '0.1.0',
67
+ status: 'draft',
68
+ createdAt: TIMESTAMP,
69
+ updatedAt: TIMESTAMP,
70
+ };
71
+ }
72
+
73
+ function userStoryTemplate(seed) {
74
+ const interactive = Boolean(seed?.interactive);
75
+ return {
76
+ usId: 'US-101',
77
+ prdId: 'PRD-001',
78
+ reqId: 'REQ-001',
79
+ version: '0.1.0',
80
+ status: 'draft',
81
+ title: seed?.usTitle ?? 'TODO: name this user story',
82
+ // Interactive mode leaves the As-a / I want / So that fields as
83
+ // minimal "-" placeholders (schema requires minLength: 1 so an empty
84
+ // string would fail validation; the spec's "empty is allowed"
85
+ // claim on this field was inaccurate on inspection).
86
+ asA: interactive ? '-' : 'TODO: name the user',
87
+ iWant: interactive ? '-' : 'TODO: state the want',
88
+ soThat: interactive ? '-' : 'TODO: state the value',
89
+ acceptanceCriteria: [
90
+ {
91
+ id: 'AC-101-1',
92
+ description: 'TODO: describe the first acceptance criterion',
93
+ testable: true,
94
+ },
95
+ ],
96
+ createdAt: TIMESTAMP,
97
+ updatedAt: TIMESTAMP,
98
+ };
99
+ }
100
+
101
+ function tadTemplate() {
102
+ return {
103
+ tadId: 'TAD-001',
104
+ prdId: 'PRD-001',
105
+ version: '0.1.0',
106
+ status: 'draft',
107
+ systemOverview: {
108
+ executiveSummary: 'TODO: one-paragraph system overview.',
109
+ systemPurpose: 'TODO: state the system purpose.',
110
+ architecturalApproach: 'TODO: state the architectural approach.',
111
+ keyCapabilities: ['TODO: list at least one key capability.'],
112
+ },
113
+ createdAt: TIMESTAMP,
114
+ updatedAt: TIMESTAMP,
115
+ };
116
+ }
117
+
118
+ function tacTemplate() {
119
+ return {
120
+ tacId: 'TAC-001',
121
+ prdId: 'PRD-001',
122
+ tadId: 'TAD-001',
123
+ version: '0.1.0',
124
+ status: 'draft',
125
+ name: 'TODO: name this component',
126
+ purpose: 'TODO: state the purpose of this component.',
127
+ responsibilities: ['TODO: list at least one responsibility.'],
128
+ createdAt: TIMESTAMP,
129
+ updatedAt: TIMESTAMP,
130
+ };
131
+ }
132
+
133
+ function adrTemplate(seed) {
134
+ return {
135
+ adrId: 'ADR-001',
136
+ prdId: 'PRD-001',
137
+ tadId: 'TAD-001',
138
+ version: '0.1.0',
139
+ // Interactive-mode `rcf init` seeds ADR-001 in `draft`
140
+ // (Phase 3.7 D2 grew the enum; Phase 4 D22 uses it here);
141
+ // non-interactive mode keeps the historical `proposed` value.
142
+ status: seed?.interactive ? 'draft' : 'proposed',
143
+ title: 'TODO: name this architectural decision',
144
+ context: 'TODO: describe the context.',
145
+ decision: 'TODO: describe the decision.',
146
+ consequences: 'TODO: describe the consequences.',
147
+ createdAt: TIMESTAMP,
148
+ updatedAt: TIMESTAMP,
149
+ };
150
+ }
151
+
152
+ function buildSequenceTemplate() {
153
+ return {
154
+ bsId: 'BS-001',
155
+ prdId: 'PRD-001',
156
+ version: '0.1.0',
157
+ status: 'draft',
158
+ title: 'Initial build sequence',
159
+ buildPhilosophy: 'TODO: describe the build philosophy.',
160
+ generationStrategy: 'dependencyFirst',
161
+ createdAt: TIMESTAMP,
162
+ updatedAt: TIMESTAMP,
163
+ };
164
+ }
165
+
166
+ function fbsTemplate() {
167
+ return {
168
+ fbsId: 'FBS-001',
169
+ prdId: 'PRD-001',
170
+ bsId: 'BS-001',
171
+ buildOrder: 1,
172
+ executionStatus: 'notStarted',
173
+ title: 'TODO: name this build session',
174
+ summary: 'TODO: describe what this build session delivers.',
175
+ acIds: ['AC-101-1'],
176
+ dependsOnFbsIds: [],
177
+ createdAt: TIMESTAMP,
178
+ updatedAt: TIMESTAMP,
179
+ };
180
+ }
181
+
182
+ async function exists(path) {
183
+ try {
184
+ await stat(path);
185
+ return true;
186
+ } catch (err) {
187
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return false;
188
+ throw err;
189
+ }
190
+ }
191
+
192
+ async function writeJson(path, data) {
193
+ await mkdir(dirname(path), { recursive: true });
194
+ await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
195
+ }
196
+
197
+ /**
198
+ * Scaffold a minimum valid rcf/ tree at `projectRoot`. Idempotent against an
199
+ * empty target; refuses if rcf/manifest.json already exists.
200
+ *
201
+ * @param {object} args
202
+ * @param {string} args.projectRoot - absolute path; created if missing
203
+ * @param {string} [args.projectName] - written into manifest and PRD
204
+ * @param {object} [args.seed] - optional interactive-mode overrides
205
+ * @param {boolean} [args.seed.interactive]
206
+ * @param {string} [args.seed.prdProblemStatement]
207
+ * @param {string} [args.seed.reqTitle]
208
+ * @param {string} [args.seed.usTitle]
209
+ * @returns {Promise<{ created: string[] } | import('../errors/index.js').RcfError>}
210
+ */
211
+ export async function initProject({ projectRoot, projectName = 'New RCF Project', seed = null }) {
212
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
213
+ return rcfError({
214
+ kind: 'usage',
215
+ message: 'initProject requires projectRoot',
216
+ });
217
+ }
218
+ const manifestPath = join(projectRoot, 'rcf', 'manifest.json');
219
+ if (await exists(manifestPath)) {
220
+ return rcfError({
221
+ kind: 'usage',
222
+ message: 'An RCF project already exists at this path (rcf/manifest.json present)',
223
+ filePath: 'rcf/manifest.json',
224
+ });
225
+ }
226
+ const dirs = [
227
+ 'rcf',
228
+ 'rcf/requirements',
229
+ 'rcf/user-stories',
230
+ 'rcf/tacs',
231
+ 'rcf/adrs',
232
+ 'rcf/fbs',
233
+ 'rcf/test-suites',
234
+ ];
235
+ for (const d of dirs) {
236
+ await mkdir(join(projectRoot, d), { recursive: true });
237
+ }
238
+
239
+ const files = [
240
+ ['rcf/manifest.json', manifestTemplate(projectName)],
241
+ ['rcf/prd.json', prdTemplate(projectName, seed)],
242
+ ['rcf/requirements/req-001.json', reqTemplate(seed)],
243
+ ['rcf/user-stories/us-101.json', userStoryTemplate(seed)],
244
+ ['rcf/tad.json', tadTemplate()],
245
+ ['rcf/tacs/tac-001.json', tacTemplate()],
246
+ ['rcf/adrs/adr-001.json', adrTemplate(seed)],
247
+ ['rcf/build-sequence.json', buildSequenceTemplate()],
248
+ ['rcf/fbs/fbs-001.json', fbsTemplate()],
249
+ ];
250
+ for (const [relPath, data] of files) {
251
+ await writeJson(join(projectRoot, relPath), data);
252
+ }
253
+
254
+ return { created: files.map(([p]) => p) };
255
+ }
@@ -0,0 +1,211 @@
1
+ // Document loader. Resolves an id (or a manifest-relative path) under the
2
+ // project's rcf/ tree, reads the file, parses JSON, validates against the
3
+ // matching schema, and returns the document or a structured error.
4
+ //
5
+ // This is the only place that touches the filesystem on the read path; the
6
+ // walker, the validate command and the view layer all go through it.
7
+
8
+ import { readdir, readFile } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+
11
+ import { rcfError } from '../errors/index.js';
12
+ import { validateDocument } from './validator.js';
13
+
14
+ /**
15
+ * Map a document kind to the subdirectory under rcf/ it lives in. The root
16
+ * documents (manifest, prd, tad, buildSequence) live at the rcf root and
17
+ * return null.
18
+ *
19
+ * @param {string} kind
20
+ * @returns {string|null}
21
+ */
22
+ export function subdirFor(kind) {
23
+ switch (kind) {
24
+ case 'req': return 'requirements';
25
+ case 'userStory': return 'user-stories';
26
+ case 'tac': return 'tacs';
27
+ case 'adr': return 'adrs';
28
+ case 'fbs': return 'fbs';
29
+ case 'testSuite': return 'test-suites';
30
+ // Phase 10 (X2 CodeNode bridge): Code Node document type.
31
+ case 'codeNode': return 'code-nodes';
32
+ default: return null;
33
+ }
34
+ }
35
+
36
+ const ROOT_FILENAMES = {
37
+ manifest: 'manifest.json',
38
+ prd: 'prd.json',
39
+ tad: 'tad.json',
40
+ buildSequence: 'build-sequence.json',
41
+ };
42
+
43
+ /**
44
+ * Resolve an id like "REQ-002" to a path under rcf/.
45
+ *
46
+ * @param {string} id - canonical id, e.g. "REQ-002", "US-201", "FBS-003"
47
+ * @returns {{ kind: string, relPath: string } | null} null if the id pattern is unknown
48
+ */
49
+ export function pathForId(id) {
50
+ if (typeof id !== 'string') return null;
51
+ if (id.startsWith('REQ-')) return { kind: 'req', relPath: `requirements/${id.toLowerCase()}.json` };
52
+ if (id.startsWith('US-')) return { kind: 'userStory', relPath: `user-stories/${id.toLowerCase()}.json` };
53
+ if (id.startsWith('TAC-')) return { kind: 'tac', relPath: `tacs/${id.toLowerCase()}.json` };
54
+ if (id.startsWith('ADR-')) return { kind: 'adr', relPath: `adrs/${id.toLowerCase()}.json` };
55
+ if (id.startsWith('FBS-')) return { kind: 'fbs', relPath: `fbs/${id.toLowerCase()}.json` };
56
+ if (id.startsWith('TS-')) return { kind: 'testSuite', relPath: `test-suites/${id.toLowerCase()}.json` };
57
+ // Phase 10 (X2 CodeNode bridge): Code Node document type.
58
+ if (id.startsWith('CN-')) return { kind: 'codeNode', relPath: `code-nodes/${id.toLowerCase()}.json` };
59
+ if (id === 'PRD-001' || id.startsWith('PRD-')) return { kind: 'prd', relPath: 'prd.json' };
60
+ if (id === 'TAD-001' || id.startsWith('TAD-')) return { kind: 'tad', relPath: 'tad.json' };
61
+ if (id === 'BS-001' || id.startsWith('BS-')) return { kind: 'buildSequence', relPath: 'build-sequence.json' };
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Path for a known root document.
67
+ *
68
+ * @param {keyof typeof ROOT_FILENAMES} kind
69
+ * @returns {string}
70
+ */
71
+ export function rootPathFor(kind) {
72
+ const name = ROOT_FILENAMES[kind];
73
+ if (!name) throw new TypeError(`Not a root kind: ${kind}`);
74
+ return name;
75
+ }
76
+
77
+ /**
78
+ * Read a file under the rcf root and parse JSON.
79
+ *
80
+ * @param {string} projectRoot - absolute path to project root
81
+ * @param {string} relPath - path relative to <projectRoot>/rcf/
82
+ * @returns {Promise<{ raw: string, doc: object } | import('../errors/index.js').RcfError>}
83
+ */
84
+ async function readJson(projectRoot, relPath) {
85
+ const filePath = join(projectRoot, 'rcf', relPath);
86
+ let raw;
87
+ try {
88
+ raw = await readFile(filePath, 'utf8');
89
+ } catch (err) {
90
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') {
91
+ return rcfError({
92
+ kind: 'missingFile',
93
+ message: `File not found: rcf/${relPath}`,
94
+ filePath: `rcf/${relPath}`,
95
+ });
96
+ }
97
+ return rcfError({
98
+ kind: 'ioFailure',
99
+ message: `Failed to read file: ${/** @type {Error} */ (err).message}`,
100
+ filePath: `rcf/${relPath}`,
101
+ });
102
+ }
103
+ try {
104
+ const doc = JSON.parse(raw);
105
+ return { raw, doc };
106
+ } catch (err) {
107
+ return rcfError({
108
+ kind: 'parseFailure',
109
+ message: `JSON parse failed: ${/** @type {Error} */ (err).message}`,
110
+ filePath: `rcf/${relPath}`,
111
+ });
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Load and validate one document by id.
117
+ *
118
+ * @param {object} args
119
+ * @param {string} args.projectRoot
120
+ * @param {string} args.id
121
+ * @returns {Promise<{ doc: object, raw: string, kind: string, filePath: string } | import('../errors/index.js').RcfError>}
122
+ */
123
+ export async function loadDocument({ projectRoot, id }) {
124
+ const resolved = pathForId(id);
125
+ if (!resolved) {
126
+ return rcfError({
127
+ kind: 'usage',
128
+ message: `Unrecognised document id: ${id}`,
129
+ documentId: id,
130
+ });
131
+ }
132
+ const result = await readJson(projectRoot, resolved.relPath);
133
+ if ('kind' in result && 'message' in result) {
134
+ const err = /** @type {import('../errors/index.js').RcfError} */ (result);
135
+ return { ...err, documentId: id };
136
+ }
137
+ const { doc, raw } = result;
138
+ const validationError = validateDocument({
139
+ doc,
140
+ kind: resolved.kind,
141
+ filePath: `rcf/${resolved.relPath}`,
142
+ });
143
+ if (validationError) {
144
+ // B5 (post-write validation): the parsed body rides along on the
145
+ // error so the walker can keep schema-invalid documents addressable
146
+ // (tree.invalidDocs) - a wedged doc must stay repairable/deletable.
147
+ return { ...validationError, documentId: id, doc, raw };
148
+ }
149
+ return { doc, raw, kind: resolved.kind, filePath: `rcf/${resolved.relPath}` };
150
+ }
151
+
152
+ /**
153
+ * Enumerate every `*.json` filename under `rcf/<subdir>/`, sorted. Not a
154
+ * discovery mechanism for tree topology (topology comes from parent-id
155
+ * fields); this is just the load-time enumeration required to bring every
156
+ * on-disk file into memory. Callers derive the document id from the
157
+ * filename stem in upper case (per the layout convention).
158
+ *
159
+ * Returns `{ files: string[] }` on success. Missing subdir returns
160
+ * `{ files: [] }` (an empty children collection is a valid tree state).
161
+ * IO failure returns `{ error: RcfError }`.
162
+ *
163
+ * @param {object} args
164
+ * @param {string} args.projectRoot
165
+ * @param {string} args.subdir - subdir under `rcf/`, e.g. `requirements`
166
+ * @returns {Promise<{ files: string[] } | { error: import('../errors/index.js').RcfError }>}
167
+ */
168
+ export async function listSubdirJsonFiles({ projectRoot, subdir }) {
169
+ let entries;
170
+ try {
171
+ entries = await readdir(join(projectRoot, 'rcf', subdir));
172
+ } catch (err) {
173
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') {
174
+ return { files: [] };
175
+ }
176
+ return {
177
+ error: rcfError({
178
+ kind: 'ioFailure',
179
+ message: `Failed to read directory: ${/** @type {Error} */ (err).message}`,
180
+ filePath: `rcf/${subdir}`,
181
+ }),
182
+ };
183
+ }
184
+ const files = entries.filter((e) => e.endsWith('.json')).sort();
185
+ return { files };
186
+ }
187
+
188
+ /**
189
+ * Load and validate a root document (manifest / prd / tad / buildSequence).
190
+ *
191
+ * @param {object} args
192
+ * @param {string} args.projectRoot
193
+ * @param {keyof typeof ROOT_FILENAMES} args.kind
194
+ * @returns {Promise<{ doc: object, raw: string, kind: string, filePath: string } | import('../errors/index.js').RcfError>}
195
+ */
196
+ export async function loadRootDocument({ projectRoot, kind }) {
197
+ const relPath = rootPathFor(kind);
198
+ const result = await readJson(projectRoot, relPath);
199
+ if ('kind' in result && 'message' in result) {
200
+ return /** @type {import('../errors/index.js').RcfError} */ (result);
201
+ }
202
+ const { doc, raw } = result;
203
+ const validationError = validateDocument({
204
+ doc,
205
+ kind,
206
+ filePath: `rcf/${relPath}`,
207
+ });
208
+ // B5: parsed body rides along on validation errors (see loadDocument).
209
+ if (validationError) return { ...validationError, doc, raw };
210
+ return { doc, raw, kind, filePath: `rcf/${relPath}` };
211
+ }