rcf-lite 0.0.1 → 0.8.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 (349) hide show
  1. package/CHANGELOG.md +344 -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 +71 -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/admissibility/enforce.js +142 -0
  177. package/src/admissibility/index.js +8 -0
  178. package/src/admissibility/markers.js +104 -0
  179. package/src/admissibility/scope-lint.js +163 -0
  180. package/src/browser-verify/auth-smoke.js +109 -0
  181. package/src/browser-verify/index.js +29 -0
  182. package/src/browser-verify/invariants.js +336 -0
  183. package/src/browser-verify/manifest-writer.js +189 -0
  184. package/src/browser-verify/runner.js +171 -0
  185. package/src/build/bundle.js +198 -0
  186. package/src/build/formatters/json.js +18 -0
  187. package/src/build/formatters/markdown.js +406 -0
  188. package/src/build/index.js +14 -0
  189. package/src/build/mark.js +177 -0
  190. package/src/build/queue.js +285 -0
  191. package/src/cli/browser-verify.js +231 -0
  192. package/src/cli/build.js +584 -0
  193. package/src/cli/coverage.js +219 -0
  194. package/src/cli/create.js +358 -0
  195. package/src/cli/delete.js +127 -0
  196. package/src/cli/design.js +353 -0
  197. package/src/cli/doctor.js +548 -0
  198. package/src/cli/fbs.js +171 -0
  199. package/src/cli/finalise.js +377 -0
  200. package/src/cli/guidance.js +206 -0
  201. package/src/cli/help.js +156 -0
  202. package/src/cli/impact.js +119 -0
  203. package/src/cli/init.js +282 -0
  204. package/src/cli/intake.js +153 -0
  205. package/src/cli/link.js +128 -0
  206. package/src/cli/mcp.js +160 -0
  207. package/src/cli/preflight.js +220 -0
  208. package/src/cli/read.js +168 -0
  209. package/src/cli/req-baseline.js +269 -0
  210. package/src/cli/req-classify.js +135 -0
  211. package/src/cli/review.js +295 -0
  212. package/src/cli/test-suite.js +221 -0
  213. package/src/cli/trace.js +172 -0
  214. package/src/cli/ui-baseline.js +292 -0
  215. package/src/cli/ui-classify.js +108 -0
  216. package/src/cli/update.js +197 -0
  217. package/src/cli/validate.js +168 -0
  218. package/src/cli/view.js +452 -0
  219. package/src/core/baseline-catalog/data/auth.json +42 -0
  220. package/src/core/baseline-catalog/data/http-api.json +42 -0
  221. package/src/core/baseline-catalog/data/notifications.json +33 -0
  222. package/src/core/baseline-catalog/data/persistence.json +33 -0
  223. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  224. package/src/core/baseline-catalog/index.js +121 -0
  225. package/src/core/errors/index.js +167 -0
  226. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  227. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  228. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  229. package/src/core/isolation/index.js +60 -0
  230. package/src/core/mcp/framing.js +103 -0
  231. package/src/core/mcp/index.js +8 -0
  232. package/src/core/mcp/server.js +228 -0
  233. package/src/core/patterns/register-canary.js +209 -0
  234. package/src/core/patterns/req-shapes.js +158 -0
  235. package/src/core/patterns/services.js +358 -0
  236. package/src/core/patterns/ui-shapes.js +166 -0
  237. package/src/core/store/cn-resolve.js +134 -0
  238. package/src/core/store/derive-deps.js +93 -0
  239. package/src/core/store/ids.js +78 -0
  240. package/src/core/store/index.js +20 -0
  241. package/src/core/store/init.js +255 -0
  242. package/src/core/store/loader.js +214 -0
  243. package/src/core/store/tp-resolve.js +176 -0
  244. package/src/core/store/validator.js +191 -0
  245. package/src/core/store/walker.js +944 -0
  246. package/src/core/store/writer.js +1879 -0
  247. package/src/design/index.js +11 -0
  248. package/src/design/writer.js +271 -0
  249. package/src/finalise/detect.js +151 -0
  250. package/src/finalise/index.js +31 -0
  251. package/src/finalise/ingest.js +160 -0
  252. package/src/finalise/install.js +119 -0
  253. package/src/finalise/ship-without-verified.js +131 -0
  254. package/src/finalise/spawn.js +84 -0
  255. package/src/intake/fidelity.js +105 -0
  256. package/src/intake/index.js +6 -0
  257. package/src/intake/manifest-writer.js +100 -0
  258. package/src/intake/orchestrator.js +138 -0
  259. package/src/intake/validate.js +80 -0
  260. package/src/mcp/map-errors.js +131 -0
  261. package/src/mcp/prompts.js +49 -0
  262. package/src/mcp/resources.js +244 -0
  263. package/src/mcp/tools.js +1212 -0
  264. package/src/preflight/design-shapes.js +185 -0
  265. package/src/preflight/index.js +90 -0
  266. package/src/preflight/manifest-writer.js +264 -0
  267. package/src/preflight/scanner.js +206 -0
  268. package/src/preflight/secrets.js +134 -0
  269. package/src/preflight/session.js +246 -0
  270. package/src/query/attestation.js +285 -0
  271. package/src/query/coverage.js +308 -0
  272. package/src/query/formatters/json.js +21 -0
  273. package/src/query/formatters/mermaid.js +209 -0
  274. package/src/query/formatters/table.js +203 -0
  275. package/src/query/impact.js +173 -0
  276. package/src/query/index.js +13 -0
  277. package/src/query/refuse-on-admissibility.js +73 -0
  278. package/src/query/trace.js +345 -0
  279. package/src/register-canary/fixture-loader.js +87 -0
  280. package/src/register-canary/index.js +10 -0
  281. package/src/register-canary/record-writer.js +132 -0
  282. package/src/register-canary/runner.js +156 -0
  283. package/src/req-baseline/gate.js +86 -0
  284. package/src/req-baseline/index.js +27 -0
  285. package/src/req-baseline/open-candidates.js +143 -0
  286. package/src/req-baseline/opt-out.js +195 -0
  287. package/src/req-baseline/sweep.js +230 -0
  288. package/src/req-detection/classifier.js +181 -0
  289. package/src/req-detection/index.js +9 -0
  290. package/src/req-detection/persist.js +55 -0
  291. package/src/review/index.js +325 -0
  292. package/src/review/mutation.js +117 -0
  293. package/src/review/ui-baseline-drift.js +138 -0
  294. package/src/ruleset/index.js +140 -0
  295. package/src/ruleset/ruleset.json +146 -0
  296. package/src/server/index.js +178 -0
  297. package/src/server/routes.js +110 -0
  298. package/src/server/sse.js +118 -0
  299. package/src/setup/agent-setup.js +362 -0
  300. package/src/setup/identity-seed.js +104 -0
  301. package/src/setup/knowledge-seed.js +123 -0
  302. package/src/setup/managed-block.js +193 -0
  303. package/src/setup/managed-gitignore.js +166 -0
  304. package/src/setup/managed-markers.js +49 -0
  305. package/src/ui-baseline/defaults.js +119 -0
  306. package/src/ui-baseline/index.js +25 -0
  307. package/src/ui-baseline/manifest-writer.js +282 -0
  308. package/src/ui-baseline/session.js +178 -0
  309. package/src/ui-detection/classifier.js +192 -0
  310. package/src/verify/chain/index.js +221 -0
  311. package/src/verify/cli/cleanup.js +61 -0
  312. package/src/verify/cli/help.js +56 -0
  313. package/src/verify/cli/mcp.js +98 -0
  314. package/src/verify/cli/provision.js +71 -0
  315. package/src/verify/cli/report.js +71 -0
  316. package/src/verify/cli/run.js +155 -0
  317. package/src/verify/engine/brief.js +87 -0
  318. package/src/verify/engine/index.js +177 -0
  319. package/src/verify/engine/launcher.js +307 -0
  320. package/src/verify/mcp/tools.js +107 -0
  321. package/src/verify/profile/index.js +146 -0
  322. package/src/verify/provision/index.js +256 -0
  323. package/src/verify/report/index.js +139 -0
  324. package/src/verify/report/renderer.js +118 -0
  325. package/src/verify/verdict/index.js +313 -0
  326. package/src/view/doc-renderers/adr.js +44 -0
  327. package/src/view/doc-renderers/build-sequence.js +40 -0
  328. package/src/view/doc-renderers/fbs.js +128 -0
  329. package/src/view/doc-renderers/helpers.js +159 -0
  330. package/src/view/doc-renderers/index.js +12 -0
  331. package/src/view/doc-renderers/prd.js +45 -0
  332. package/src/view/doc-renderers/req.js +43 -0
  333. package/src/view/doc-renderers/tac.js +38 -0
  334. package/src/view/doc-renderers/tad.js +74 -0
  335. package/src/view/doc-renderers/test-suite.js +45 -0
  336. package/src/view/doc-renderers/user-story.js +63 -0
  337. package/src/view/html-page.js +462 -0
  338. package/src/view/index.js +63 -0
  339. package/src/view/live-client.js +338 -0
  340. package/src/view/mermaid-diagram.js +178 -0
  341. package/src/view/style.css +735 -0
  342. package/src/view/tree-model.js +152 -0
  343. package/src/view/vendored/mermaid.min.js +2607 -0
  344. package/src/view-supervisor/index.js +26 -0
  345. package/src/view-supervisor/logs.js +32 -0
  346. package/src/view-supervisor/manifest-writer.js +178 -0
  347. package/src/view-supervisor/persist-until.js +85 -0
  348. package/src/view-supervisor/supervisor.js +276 -0
  349. package/src/watch/index.js +152 -0
@@ -0,0 +1,119 @@
1
+ // Absent-verify handling (spec §8.3, amendment 5). When `rcf-verify` is not
2
+ // resolvable, the finalise gate MUST NOT silently skip - that recreates the
3
+ // false-confidence failure the whole programme exists to prevent. The
4
+ // sanctioned paths are exactly two, and nothing else:
5
+ // - interactive TTY -> prompt the operator to install now (y/N);
6
+ // - non-interactive -> require an explicit --install-verify flag.
7
+ // A silent skip and a silent auto-install are BOTH violations. Declining the
8
+ // prompt (or omitting the flag off a TTY) aborts finalise - it does not pass
9
+ // the gate.
10
+
11
+ import { spawn } from 'node:child_process';
12
+ import { createInterface } from 'node:readline';
13
+
14
+ import { VERIFY_PACKAGE } from './detect.js';
15
+
16
+ /**
17
+ * Interactive yes/no prompt over stdin/stdout. Resolves true only on an
18
+ * explicit affirmative; EOF / anything else is a decline (safe default: do
19
+ * not install, do not skip the gate).
20
+ *
21
+ * @param {string} question
22
+ * @param {object} [io]
23
+ * @param {NodeJS.ReadableStream} [io.input]
24
+ * @param {NodeJS.WritableStream} [io.output]
25
+ * @returns {Promise<boolean>}
26
+ */
27
+ export function promptYesNo(question, { input = process.stdin, output = process.stdout } = {}) {
28
+ return new Promise((resolvePromise) => {
29
+ const rl = createInterface({ input, output });
30
+ rl.question(`${question} `, (answer) => {
31
+ rl.close();
32
+ resolvePromise(/^y(es)?$/i.test((answer ?? '').trim()));
33
+ });
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Install rcf-verify globally (matches the install-together default of two
39
+ * global bins). Streams npm's output to the operator. Returns the install
40
+ * process exit code.
41
+ *
42
+ * @param {object} [deps]
43
+ * @param {typeof spawn} [deps.spawn]
44
+ * @param {NodeJS.WritableStream} [deps.stdout]
45
+ * @param {NodeJS.WritableStream} [deps.stderr]
46
+ * @param {string} [deps.packageSpec]
47
+ * @returns {Promise<number>}
48
+ */
49
+ export function installVerify(deps = {}) {
50
+ const spawnFn = deps.spawn ?? spawn;
51
+ const spec = deps.packageSpec ?? VERIFY_PACKAGE;
52
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
53
+ return new Promise((resolvePromise, reject) => {
54
+ const child = spawnFn(npm, ['install', '-g', spec], { stdio: 'inherit' });
55
+ child.on('error', reject);
56
+ child.on('close', (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
57
+ });
58
+ }
59
+
60
+ /**
61
+ * The absent-verify decision (§8.3). Given the runtime shape (TTY? explicit
62
+ * flag?) decides among: install-then-proceed, or abort. NEVER skip.
63
+ *
64
+ * @param {object} args
65
+ * @param {boolean} args.installFlag - --install-verify was passed
66
+ * @param {boolean} args.isTty - stdin+stdout are a TTY
67
+ * @param {object} io - { stdout, stderr, input, output }
68
+ * @param {object} [deps]
69
+ * @param {typeof promptYesNo} [deps.promptYesNo]
70
+ * @param {typeof installVerify} [deps.installVerify]
71
+ * @returns {Promise<{ action: 'installed' } | { action: 'abort', reason: string, code: number }>}
72
+ */
73
+ export async function resolveAbsentVerify({ installFlag, isTty }, io, deps = {}) {
74
+ const prompt = deps.promptYesNo ?? promptYesNo;
75
+ const install = deps.installVerify ?? installVerify;
76
+
77
+ let wantsInstall = false;
78
+ if (installFlag) {
79
+ // Explicit flag: the sanctioned non-interactive install path.
80
+ io.stdout.write(`[finalise] rcf-verify not found; installing ${VERIFY_PACKAGE} (--install-verify)...\n`);
81
+ wantsInstall = true;
82
+ } else if (isTty) {
83
+ // Interactive: prompt. Declining aborts (never a silent skip).
84
+ io.stderr.write(`[finalise] rcf-verify is not installed. The ship gate cannot run without it.\n`);
85
+ wantsInstall = await prompt(`Install ${VERIFY_PACKAGE} now? [y/N]`, {
86
+ input: io.input, output: io.output,
87
+ });
88
+ if (!wantsInstall) {
89
+ return {
90
+ action: 'abort',
91
+ code: 4,
92
+ reason: `finalise refused: rcf-verify is required and install was declined. `
93
+ + `Install it (npm i -g ${VERIFY_PACKAGE}) or re-run with --install-verify. `
94
+ + `The ship gate is never skipped.`,
95
+ };
96
+ }
97
+ } else {
98
+ // Non-interactive without the flag: cannot prompt, must not auto-install
99
+ // silently, must not skip. Abort with the exact remedy.
100
+ return {
101
+ action: 'abort',
102
+ code: 4,
103
+ reason: `finalise refused: rcf-verify is not installed and no TTY is available to prompt. `
104
+ + `Install it (npm i -g ${VERIFY_PACKAGE}) or re-run with --install-verify. `
105
+ + `The ship gate is never silently skipped.`,
106
+ };
107
+ }
108
+
109
+ const installCode = await install({ stdout: io.stdout, stderr: io.stderr });
110
+ if (installCode !== 0) {
111
+ return {
112
+ action: 'abort',
113
+ code: 1,
114
+ reason: `finalise aborted: installing ${VERIFY_PACKAGE} failed (npm exit ${installCode}). `
115
+ + `Install it manually and re-run finalise.`,
116
+ };
117
+ }
118
+ return { action: 'installed' };
119
+ }
@@ -0,0 +1,131 @@
1
+ // Ship-without-verified acknowledgement writer
2
+ // (verification-integrity-cluster-spec section 5.2, review finding B-1).
3
+ //
4
+ // When `rcf finalise --ship-without-verified` acknowledges MOCK-ONLY-DECLARED
5
+ // or BLOCKED-BY-DECLARATION per-AC verdicts on a verify report and leaves
6
+ // the FBS at 'complete' (spec section 5.2), the ack lands on the manifest
7
+ // under the optional `shipWithoutVerified[]` array added in
8
+ // @stravica-ai/rcf-schemas 0.4.1. Stdout is not durable and not greppable
9
+ // at ship time; the manifest record is both. Schema shape:
10
+ //
11
+ // { id: swv-<fbsId>-<n>, fbsId, ackedAt,
12
+ // declaredAcs[{acId, verdict, reason?}], reportPath }
13
+ //
14
+ // Id allocation mirrors reviewAudit's `ra-<fbsId>-<n>` monotonic pattern
15
+ // (packages/rcf-lite/src/review/index.js:nextReviewAuditId) so a re-ack of
16
+ // the same FBS increments deterministically.
17
+
18
+ import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
19
+ import { dirname, join } from 'node:path';
20
+
21
+ import { rcfError } from '#core/errors';
22
+ import { validateDocument } from '#core/store';
23
+
24
+ /**
25
+ * @typedef {object} ShipWithoutVerifiedDeclaredAc
26
+ * @property {string} acId
27
+ * @property {'MOCK-ONLY-DECLARED'|'BLOCKED-BY-DECLARATION'} verdict
28
+ * @property {string} [reason]
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} ShipWithoutVerifiedRecord
33
+ * @property {string} id `swv-<fbsId>-<n>` monotonic per FBS.
34
+ * @property {string} fbsId
35
+ * @property {string} ackedAt ISO timestamp.
36
+ * @property {ShipWithoutVerifiedDeclaredAc[]} declaredAcs minItems 1.
37
+ * @property {string} reportPath
38
+ */
39
+
40
+ /**
41
+ * Monotonic id allocator for the shipWithoutVerified array. Mirrors
42
+ * `nextReviewAuditId` (review/index.js).
43
+ *
44
+ * @param {object|null} manifest
45
+ * @param {string} fbsId
46
+ * @returns {string}
47
+ */
48
+ export function nextShipWithoutVerifiedId(manifest, fbsId) {
49
+ const prefix = `swv-${fbsId}-`;
50
+ const existing = Array.isArray(manifest?.shipWithoutVerified) ? manifest.shipWithoutVerified : [];
51
+ let maxN = 0;
52
+ for (const rec of existing) {
53
+ if (typeof rec?.id !== 'string' || !rec.id.startsWith(prefix)) continue;
54
+ const n = Number.parseInt(rec.id.slice(prefix.length), 10);
55
+ if (Number.isFinite(n) && n > maxN) maxN = n;
56
+ }
57
+ return `${prefix}${maxN + 1}`;
58
+ }
59
+
60
+ /**
61
+ * Compose the record. Timestamps at UTC ISO.
62
+ *
63
+ * @param {object} args
64
+ * @param {object|null} args.manifest
65
+ * @param {string} args.fbsId
66
+ * @param {ShipWithoutVerifiedDeclaredAc[]} args.declaredAcs
67
+ * @param {string} args.reportPath
68
+ * @param {Date} [args.now]
69
+ * @returns {ShipWithoutVerifiedRecord}
70
+ */
71
+ export function composeShipWithoutVerifiedRecord({
72
+ manifest, fbsId, declaredAcs, reportPath, now = new Date(),
73
+ }) {
74
+ return {
75
+ id: nextShipWithoutVerifiedId(manifest, fbsId),
76
+ fbsId,
77
+ ackedAt: now.toISOString(),
78
+ declaredAcs: declaredAcs.map((a) => {
79
+ const out = { acId: a.acId, verdict: a.verdict };
80
+ if (typeof a.reason === 'string' && a.reason.length > 0) out.reason = a.reason;
81
+ return out;
82
+ }),
83
+ reportPath,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Persist a ship-without-verified acknowledgement onto the manifest.
89
+ * Reads `tree.manifest` for the current state, appends the composed
90
+ * record to `manifest.shipWithoutVerified[]`, validates the composed
91
+ * manifest against the 0.4.1 schema, and writes it atomically. On
92
+ * validation failure the on-disk manifest is untouched (mirrors the
93
+ * preflight manifest-writer discipline).
94
+ *
95
+ * @param {object} args
96
+ * @param {string} args.projectRoot
97
+ * @param {object} args.tree
98
+ * @param {ShipWithoutVerifiedRecord} args.record
99
+ * @returns {Promise<{ record: ShipWithoutVerifiedRecord } | import('#core/errors').RcfError>}
100
+ */
101
+ export async function writeShipWithoutVerifiedRecord({ projectRoot, tree, record }) {
102
+ const manifest = tree.manifest ?? {};
103
+ const nextManifest = { ...manifest };
104
+ const existing = Array.isArray(nextManifest.shipWithoutVerified) ? nextManifest.shipWithoutVerified : [];
105
+ nextManifest.shipWithoutVerified = [...existing, record];
106
+
107
+ const relPath = 'rcf/manifest.json';
108
+ const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: relPath });
109
+ if (validation) return validation;
110
+
111
+ const absPath = join(projectRoot, 'rcf', 'manifest.json');
112
+ try {
113
+ await mkdir(dirname(absPath), { recursive: true });
114
+ const tmp = `${absPath}.tmp`;
115
+ await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
116
+ try {
117
+ await rename(tmp, absPath);
118
+ } catch (err) {
119
+ try { await unlink(tmp); } catch { /* ignore */ }
120
+ throw err;
121
+ }
122
+ } catch (err) {
123
+ return rcfError({
124
+ kind: 'ioFailure',
125
+ message: `finalise: ship-without-verified manifest write failed: ${err.message}`,
126
+ filePath: relPath,
127
+ stack: err.stack,
128
+ });
129
+ }
130
+ return { record };
131
+ }
@@ -0,0 +1,84 @@
1
+ // Fresh-subprocess invocation of `rcf-verify run` (spec §8.2). This is the
2
+ // load-bearing independence guarantee: build-lite's finalise step launches
3
+ // verify as a SEPARATE OS PROCESS with the isolation env - it MUST NOT import
4
+ // verify's engine in-process, because the verifier agent must start from a
5
+ // cold session with zero build context (§9). The subprocess exit code is the
6
+ // gate; findings flow back via the --out report file, never stdout scraping.
7
+ //
8
+ // The isolation env (core's ISOLATION_RECIPE, §7.3) is layered onto the child
9
+ // env here so the recipe travels with the process boundary exactly as §8.2
10
+ // specifies ("spawn ... with the isolation env").
11
+
12
+ import { spawn } from 'node:child_process';
13
+
14
+ import { isolationEnv } from '#core/isolation';
15
+
16
+ /**
17
+ * Assemble the `rcf-verify run` argument vector from finalise options.
18
+ * Deterministic and pure so it is directly unit-testable; the exact shape is
19
+ * the §8.2 invocation contract.
20
+ *
21
+ * @param {object} opts
22
+ * @param {string} opts.repo - RCF chain source (the project root)
23
+ * @param {string} opts.url - the running deployed app
24
+ * @param {string} opts.profile - deployed | ci | local-dev
25
+ * @param {string} opts.out - report artifact path (always written by verify)
26
+ * @param {string} opts.severityGate - PASS|COSMETIC|DEGRADED|BROKEN
27
+ * @param {boolean} [opts.parityEnv]
28
+ * @param {string} [opts.provision] - provisioning FILE path
29
+ * @param {string} [opts.chain] - PRD/chain ref
30
+ * @param {string} [opts.persona]
31
+ * @returns {string[]}
32
+ */
33
+ export function buildVerifyArgs(opts) {
34
+ const args = [
35
+ 'run',
36
+ '--repo', opts.repo,
37
+ '--profile', opts.profile,
38
+ '--url', opts.url,
39
+ '--severity-gate', opts.severityGate,
40
+ '--out', opts.out,
41
+ ];
42
+ if (opts.chain) args.push('--chain', opts.chain);
43
+ if (opts.parityEnv) args.push('--parity-env');
44
+ if (opts.provision) args.push('--provision', opts.provision);
45
+ if (opts.persona) args.push('--persona', opts.persona);
46
+ return args;
47
+ }
48
+
49
+ /**
50
+ * Spawn rcf-verify as a fresh subprocess and resolve with its exit code.
51
+ * stdio is inherited by default so the operator sees verify's live progress
52
+ * and its verdict line; the report artifact is read separately by the caller.
53
+ *
54
+ * The child env is the isolation recipe layered over the current env (§8.2 +
55
+ * §7.3): the recipe wins on conflict so a leaked parent value cannot re-enable
56
+ * auto-memory or non-essential traffic in the verifier session.
57
+ *
58
+ * @param {object} args
59
+ * @param {import('./detect.js').VerifyInvocation} args.invocation
60
+ * @param {string[]} args.verifyArgs - from buildVerifyArgs()
61
+ * @param {string} args.cwd
62
+ * @param {object} [deps]
63
+ * @param {typeof spawn} [deps.spawn]
64
+ * @param {NodeJS.ProcessEnv} [deps.baseEnv]
65
+ * @param {'inherit'|'pipe'|'ignore'} [deps.stdio]
66
+ * @returns {Promise<{ code: number, signal: NodeJS.Signals | null, env: NodeJS.ProcessEnv }>}
67
+ */
68
+ export function spawnVerify({ invocation, verifyArgs, cwd }, deps = {}) {
69
+ const spawnFn = deps.spawn ?? spawn;
70
+ const childEnv = isolationEnv(deps.baseEnv ?? process.env);
71
+ const argv = [...invocation.prefixArgs, ...verifyArgs];
72
+ return new Promise((resolvePromise, reject) => {
73
+ const child = spawnFn(invocation.command, argv, {
74
+ cwd,
75
+ env: childEnv,
76
+ stdio: deps.stdio ?? 'inherit',
77
+ });
78
+ child.on('error', reject);
79
+ child.on('close', (code, signal) => {
80
+ // A signalled child (no numeric code) is a failure, not a pass.
81
+ resolvePromise({ code: code ?? (signal ? 1 : 0), signal, env: childEnv });
82
+ });
83
+ });
84
+ }
@@ -0,0 +1,105 @@
1
+ // Intake-fidelity classifier (spec §3.7, §6.4 phase 1).
2
+ //
3
+ // Deterministic word-count + signal-count classifier over the raw text of
4
+ // a supplied artefact. The bands are the ones the spec ratified:
5
+ // none → nothing supplied (caller passes an empty artefact list)
6
+ // napkin → very short: one paragraph or a sketchy note
7
+ // briefLight → short brief: names problem and capabilities, unstated NFRs
8
+ // briefStrong → full product brief with capabilities enumerated, NFRs named,
9
+ // out-of-scope called out. The watchpost brief lives here.
10
+ // prd → a full PRD or PRD-adjacent doc (front-matter, requirement
11
+ // sections, acceptance-shaped bullets)
12
+ // prdPlusTad → a PRD plus a TAD (architecture, ADR, TAC signals)
13
+ //
14
+ // The classifier is not a scoring engine; it uses cheap keyword signals
15
+ // (headings, capability lists, NFR keywords, out-of-scope markers,
16
+ // architecture markers) plus word count to assign a band. The operator
17
+ // always confirms the verdict at phase-1 handoff.
18
+
19
+ export const FIDELITY_LEVELS = Object.freeze([
20
+ 'none',
21
+ 'napkin',
22
+ 'briefLight',
23
+ 'briefStrong',
24
+ 'prd',
25
+ 'prdPlusTad',
26
+ ]);
27
+
28
+ const CAP_HEADINGS = /^(?:\s*#+\s*)(?:capabilit(?:y|ies)|must-have|should-have|features?|user story|user stories|acceptance criteria)\b/im;
29
+ const NFR_HEADINGS = /^(?:\s*#+\s*)(?:non[-\s]?functional|nfr|constraints?|performance|reliability|security|scalability|budgets?)\b/im;
30
+ const OUT_OF_SCOPE = /^(?:\s*#+\s*)?(?:out[-\s]?of[-\s]?scope|non[-\s]?goals?|will not do|not (?:in|for) v[0-9])\b/im;
31
+ const REQ_MARKERS = /^\s*(?:REQ|US|AC)-?[0-9]+/im;
32
+ const PRD_MARKERS = /^(?:\s*#+\s*)?(?:product\s+requirements\s+document|prd|problem statement|users?\s*&?\s*personas?)\b/im;
33
+ const TAD_MARKERS = /^(?:\s*#+\s*)?(?:technical\s+architecture|architecture decision record|adr[-\s]?[0-9]+|tac[-\s]?[0-9]+|system architecture|component diagram|api boundaries)\b/im;
34
+
35
+ /**
36
+ * @typedef {'none'|'napkin'|'briefLight'|'briefStrong'|'prd'|'prdPlusTad'} FidelityBand
37
+ */
38
+
39
+ /**
40
+ * @typedef {object} FidelitySignals
41
+ * @property {number} wordCount
42
+ * @property {boolean} capabilitiesEnumerated
43
+ * @property {boolean} nfrsNamed
44
+ * @property {boolean} outOfScopeListed
45
+ * @property {boolean} requirementMarkers
46
+ * @property {boolean} prdShape
47
+ * @property {boolean} tadShape
48
+ */
49
+
50
+ /**
51
+ * Classify one artefact into a fidelity band. Accepts null/empty text
52
+ * ("nothing supplied").
53
+ *
54
+ * @param {string|null} text
55
+ * @param {object} [opts]
56
+ * @param {string|null} [opts.kindHint] operator hint: napkin|productBrief|prd|prdPlusTad|other
57
+ * @returns {{ fidelity: FidelityBand, signals: FidelitySignals }}
58
+ */
59
+ export function classifyFidelity(text, opts = {}) {
60
+ const hint = opts.kindHint ?? null;
61
+ const t = typeof text === 'string' ? text : '';
62
+ const wordCount = t.trim().length === 0 ? 0 : t.trim().split(/\s+/).length;
63
+ const signals = {
64
+ wordCount,
65
+ capabilitiesEnumerated: CAP_HEADINGS.test(t),
66
+ nfrsNamed: NFR_HEADINGS.test(t),
67
+ outOfScopeListed: OUT_OF_SCOPE.test(t),
68
+ requirementMarkers: REQ_MARKERS.test(t),
69
+ prdShape: PRD_MARKERS.test(t),
70
+ tadShape: TAD_MARKERS.test(t),
71
+ };
72
+
73
+ // Operator hint takes precedence when it is explicit and unambiguous.
74
+ if (hint === 'napkin') return { fidelity: 'napkin', signals };
75
+ if (hint === 'prd') return { fidelity: 'prd', signals };
76
+ if (hint === 'prdPlusTad') return { fidelity: 'prdPlusTad', signals };
77
+
78
+ if (wordCount === 0) return { fidelity: 'none', signals };
79
+
80
+ // Structural markers (PRD / TAD / requirement ids) win over the word
81
+ // count band: an operator supplying an already-structured short doc
82
+ // (a partial PRD, an ADR, a REQ-ids list) is at the prd/prdPlusTad
83
+ // band regardless of length.
84
+ if (signals.tadShape && (signals.prdShape || signals.requirementMarkers)) {
85
+ return { fidelity: 'prdPlusTad', signals };
86
+ }
87
+ if (signals.tadShape) return { fidelity: 'prdPlusTad', signals };
88
+ if (signals.requirementMarkers || (signals.prdShape && signals.capabilitiesEnumerated)) {
89
+ return { fidelity: 'prd', signals };
90
+ }
91
+
92
+ if (wordCount < 100) return { fidelity: 'napkin', signals };
93
+
94
+ // Brief band: capabilities enumerated + NFRs named + out-of-scope
95
+ // listed → briefStrong; otherwise briefLight.
96
+ if (signals.capabilitiesEnumerated && signals.nfrsNamed && signals.outOfScopeListed) {
97
+ return { fidelity: 'briefStrong', signals };
98
+ }
99
+ if (signals.capabilitiesEnumerated || signals.nfrsNamed || signals.outOfScopeListed) {
100
+ return { fidelity: 'briefLight', signals };
101
+ }
102
+ // Default: prose of some length but no signals — treat as napkin
103
+ // upgraded to briefLight over 400 words.
104
+ return { fidelity: wordCount >= 400 ? 'briefLight' : 'napkin', signals };
105
+ }
@@ -0,0 +1,6 @@
1
+ // Public surface for the intake module (spec §6).
2
+
3
+ export { classifyFidelity, FIDELITY_LEVELS } from './fidelity.js';
4
+ export { scanArtefactForFindings } from './validate.js';
5
+ export { runIntakePhases } from './orchestrator.js';
6
+ export { writeIntakeRecord, composeIntakeRecord, nextIntakeId } from './manifest-writer.js';
@@ -0,0 +1,100 @@
1
+ // Intake record composer + writer (spec §3.4).
2
+ //
3
+ // Composes the `intakeClassification` block and appends/replaces it on
4
+ // the manifest, then writes the manifest atomically after schema
5
+ // validation. Uses the same `.tmp → rename` pattern as the preflight
6
+ // writer.
7
+
8
+ import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
9
+ import { dirname, join } from 'node:path';
10
+
11
+ import { rcfError } from '#core/errors';
12
+ import { validateDocument } from '#core/store';
13
+
14
+ const ID_RE = /^ic-\d{4}-\d{2}-\d{2}-(\d{3})$/;
15
+
16
+ /**
17
+ * Compose the next monotonic intake id for the current date.
18
+ *
19
+ * @param {object|null} manifest
20
+ * @param {Date} [now]
21
+ * @returns {string}
22
+ */
23
+ export function nextIntakeId(manifest, now = new Date()) {
24
+ const yyyy = now.getUTCFullYear().toString().padStart(4, '0');
25
+ const mm = (now.getUTCMonth() + 1).toString().padStart(2, '0');
26
+ const dd = now.getUTCDate().toString().padStart(2, '0');
27
+ const prefix = `ic-${yyyy}-${mm}-${dd}-`;
28
+ const existingRec = manifest?.intakeClassification ?? null;
29
+ let maxN = 0;
30
+ if (existingRec && typeof existingRec.id === 'string' && existingRec.id.startsWith(prefix)) {
31
+ const m = existingRec.id.match(ID_RE);
32
+ if (m) {
33
+ const n = Number.parseInt(m[1], 10);
34
+ if (Number.isFinite(n) && n > maxN) maxN = n;
35
+ }
36
+ }
37
+ return `${prefix}${(maxN + 1).toString().padStart(3, '0')}`;
38
+ }
39
+
40
+ /**
41
+ * Compose an intakeClassification block.
42
+ *
43
+ * @param {object} args
44
+ * @param {object|null} args.manifest
45
+ * @param {string} args.fidelity
46
+ * @param {object[]} args.artefacts
47
+ * @param {object[]} args.validationFindings
48
+ * @param {object} args.elicitationScope
49
+ * @param {Date} [args.now]
50
+ * @returns {object}
51
+ */
52
+ export function composeIntakeRecord({ manifest, fidelity, artefacts, validationFindings, elicitationScope, now = new Date() }) {
53
+ const isoNow = now.toISOString();
54
+ const record = {
55
+ id: nextIntakeId(manifest, now),
56
+ createdAt: isoNow,
57
+ fidelity,
58
+ artefacts,
59
+ validationFindings,
60
+ elicitationScope,
61
+ operatorAckAt: isoNow,
62
+ };
63
+ return record;
64
+ }
65
+
66
+ /**
67
+ * Write the composed intakeClassification onto the manifest.
68
+ *
69
+ * @param {object} args
70
+ * @param {string} args.projectRoot
71
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
72
+ * @param {object} args.record
73
+ * @returns {Promise<{ record: object } | import('#core/errors').RcfError>}
74
+ */
75
+ export async function writeIntakeRecord({ projectRoot, tree, record }) {
76
+ const manifest = tree?.manifest ?? {};
77
+ const nextManifest = { ...manifest, intakeClassification: record };
78
+ const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: 'rcf/manifest.json' });
79
+ if (validation) return validation;
80
+ const abs = join(projectRoot, 'rcf', 'manifest.json');
81
+ try {
82
+ await mkdir(dirname(abs), { recursive: true });
83
+ const tmp = `${abs}.tmp`;
84
+ await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
85
+ try {
86
+ await rename(tmp, abs);
87
+ } catch (err) {
88
+ try { await unlink(tmp); } catch { /* ignore */ }
89
+ throw err;
90
+ }
91
+ } catch (err) {
92
+ return rcfError({
93
+ kind: 'ioFailure',
94
+ message: `intake: manifest write failed: ${err.message}`,
95
+ filePath: 'rcf/manifest.json',
96
+ stack: err.stack,
97
+ });
98
+ }
99
+ return { record };
100
+ }
@@ -0,0 +1,138 @@
1
+ // Intake orchestrator (spec §6.4 phases 1-3 combined).
2
+ //
3
+ // Reads the supplied artefacts, runs the fidelity classifier, applies
4
+ // the validation scans, folds in any operator-supplied non-interactive
5
+ // input file, and composes an intakeClassification record ready for the
6
+ // manifest writer. Never talks to a subagent in v1 (the intake-worker
7
+ // dispatch in spec §6.6 is out of scope for this build ship — the
8
+ // deterministic scans plus operator-declared "otherDeclared" findings
9
+ // via --input cover phase 2 without the reader dispatch).
10
+
11
+ import { readFile, stat } from 'node:fs/promises';
12
+ import { basename, isAbsolute, join } from 'node:path';
13
+ import { createHash } from 'node:crypto';
14
+
15
+ import { rcfError } from '#core/errors';
16
+
17
+ import { classifyFidelity } from './fidelity.js';
18
+ import { scanArtefactForFindings } from './validate.js';
19
+ import { composeIntakeRecord } from './manifest-writer.js';
20
+
21
+ /**
22
+ * @typedef {'napkin'|'productBrief'|'prd'|'tad'|'other'} ArtefactKind
23
+ */
24
+
25
+ const KIND_MAP = new Map([
26
+ ['napkin', 'napkin'],
27
+ ['productbrief', 'productBrief'],
28
+ ['prd', 'prd'],
29
+ ['prdplustad', 'prd'],
30
+ ['tad', 'tad'],
31
+ ['other', 'other'],
32
+ ]);
33
+
34
+ /**
35
+ * Read + classify + validate every artefact and compose a full
36
+ * intakeClassification record. Returns an RcfError on IO failure.
37
+ *
38
+ * @param {object} args
39
+ * @param {string} args.projectRoot
40
+ * @param {string[]} args.artefactPaths
41
+ * @param {string|null} [args.kindHint]
42
+ * @param {object|null} [args.input] optional non-interactive input file
43
+ * @param {Date} [args.now]
44
+ * @returns {Promise<{ record: object } | import('#core/errors').RcfError>}
45
+ */
46
+ export async function runIntakePhases({ projectRoot, artefactPaths, kindHint = null, input = null, now = new Date() }) {
47
+ const artefactMeta = [];
48
+ const allFindings = [];
49
+ let text = '';
50
+ const paths = [...new Set(artefactPaths)];
51
+ for (const raw of paths) {
52
+ const abs = isAbsolute(raw) ? raw : join(projectRoot, raw);
53
+ let body = '';
54
+ let hash = null;
55
+ let wordCount = 0;
56
+ try {
57
+ body = await readFile(abs, 'utf8');
58
+ const info = await stat(abs);
59
+ void info;
60
+ hash = `sha256:${createHash('sha256').update(body).digest('hex')}`;
61
+ wordCount = body.trim().length === 0 ? 0 : body.trim().split(/\s+/).length;
62
+ } catch (err) {
63
+ return rcfError({
64
+ kind: 'usage',
65
+ message: `intake: cannot read artefact ${raw}: ${err.message}`,
66
+ filePath: raw,
67
+ });
68
+ }
69
+ text += `\n${body}`;
70
+ const kind = resolveKind({ path: raw, kindHint });
71
+ artefactMeta.push({
72
+ path: raw,
73
+ kind,
74
+ wordCount,
75
+ hash,
76
+ operatorSourced: true,
77
+ });
78
+ for (const f of scanArtefactForFindings(body)) allFindings.push(f);
79
+ }
80
+
81
+ const { fidelity, signals } = classifyFidelity(text, { kindHint });
82
+
83
+ // Fold non-interactive input: extra findings, operator responses,
84
+ // and elicitationScope hints all land here.
85
+ const inputFindings = Array.isArray(input?.validationFindings) ? input.validationFindings : [];
86
+ const inputResponses = new Map();
87
+ for (const f of inputFindings) {
88
+ if (typeof f?.detail === 'string' && typeof f?.operatorResponse === 'string') {
89
+ inputResponses.set(f.detail, f.operatorResponse);
90
+ }
91
+ if (f?.kind === 'otherDeclared') allFindings.push(f);
92
+ }
93
+
94
+ const isoNow = now.toISOString();
95
+ const findingsWithTimestamps = allFindings.map((f) => {
96
+ const entry = { kind: f.kind, detail: f.detail, raisedAt: isoNow };
97
+ if (f.kindDescription) entry.kindDescription = f.kindDescription;
98
+ const response = inputResponses.get(f.detail) ?? input?.operatorResponse;
99
+ if (typeof response === 'string' && response.length > 0) {
100
+ entry.operatorResponse = response;
101
+ entry.resolvedAt = isoNow;
102
+ }
103
+ return entry;
104
+ });
105
+
106
+ const elicitationScope = input?.elicitationScope ?? {
107
+ prdDrafted: fidelity === 'prd' || fidelity === 'prdPlusTad' ? 'supplied' : 'drafted',
108
+ reqDraftedFromArtefact: [],
109
+ reqRequiringElicitation: [],
110
+ acsFromArtefact: 0,
111
+ acsFromElicitation: 'all',
112
+ };
113
+
114
+ const record = composeIntakeRecord({
115
+ manifest: null,
116
+ fidelity: input?.fidelity ?? fidelity,
117
+ artefacts: artefactMeta,
118
+ validationFindings: findingsWithTimestamps,
119
+ elicitationScope,
120
+ now,
121
+ });
122
+
123
+ void signals; // exposed on classifier return for future observability
124
+
125
+ return { record };
126
+ }
127
+
128
+ function resolveKind({ path, kindHint }) {
129
+ const hint = KIND_MAP.get(String(kindHint ?? '').toLowerCase());
130
+ if (hint) return hint;
131
+ const base = basename(path).toLowerCase();
132
+ if (base.includes('napkin')) return 'napkin';
133
+ if (base.includes('prd') && base.includes('tad')) return 'prd';
134
+ if (base.includes('prd')) return 'prd';
135
+ if (base.includes('tad')) return 'tad';
136
+ if (base.includes('brief')) return 'productBrief';
137
+ return 'other';
138
+ }