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,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
+ }
@@ -0,0 +1,80 @@
1
+ // Intake-validation scans (spec §6.4 phase 2).
2
+ //
3
+ // Deterministic scans applied over the raw artefact text; each finding
4
+ // carries a `kind` from the schema enum
5
+ // impliedButNotStated | contradiction | missingLoadBearingConstraint |
6
+ // otherDeclared
7
+ // The "otherDeclared" bucket carries a free-text `kindDescription`; the
8
+ // deterministic scans in this module never emit "otherDeclared" — that
9
+ // bucket exists for the intake-worker subagent reader (spec §6.6),
10
+ // which is out of the v1 build scope for this ship.
11
+ //
12
+ // The scans are BROAD by design; the operator resolves each finding at
13
+ // phase 2 before phase 3 hands off to elicitation.
14
+
15
+ /**
16
+ * @typedef {'impliedButNotStated'|'contradiction'|'missingLoadBearingConstraint'|'otherDeclared'} FindingKind
17
+ */
18
+
19
+ /**
20
+ * @typedef {object} IntakeFinding
21
+ * @property {FindingKind} kind
22
+ * @property {string} detail
23
+ * @property {string} [kindDescription]
24
+ */
25
+
26
+ const SERVICE_HINTS = [
27
+ { name: 'Resend', envHint: /RESEND_?API_?KEY/i },
28
+ { name: 'Twilio', envHint: /TWILIO_(?:AUTH_)?TOKEN|TWILIO_ACCOUNT_SID/i },
29
+ { name: 'SendGrid', envHint: /SENDGRID_?API_?KEY/i },
30
+ { name: 'Stripe', envHint: /STRIPE_(?:SECRET|API)_KEY/i },
31
+ { name: 'Slack', envHint: /SLACK_(?:BOT_)?TOKEN|SLACK_WEBHOOK/i },
32
+ ];
33
+
34
+ const NEGATED_LOGIN_PHRASES = /\bno (?:login|sign[-\s]?in|auth|authenticat(?:ion|ed))\s+(?:is|required|needed)?/i;
35
+ const HAS_ADMIN_UI = /\b(admin(?:istrator)?|dashboard|admin panel|admin route)\b/i;
36
+ const HAS_WEB_UI = /\b(browser|html|web page|dashboard|admin)\b/i;
37
+ const HAS_BROWSER_SIGNIN = /\b(browser sign[-\s]?in|sign[-\s]?in page|login page|html login)\b/i;
38
+ const HAS_API_ONLY = /\b(api[-\s]?only|programmatic (?:client|access)|sdk[-\s]?driven|no browser)\b/i;
39
+
40
+ /**
41
+ * Scan one artefact and return the deterministic findings.
42
+ *
43
+ * @param {string} text
44
+ * @returns {IntakeFinding[]}
45
+ */
46
+ export function scanArtefactForFindings(text) {
47
+ const t = typeof text === 'string' ? text : '';
48
+ const findings = [];
49
+
50
+ // impliedButNotStated: brief mentions a web UI but never names the
51
+ // sign-in surface (HTML login page vs API-only).
52
+ if (HAS_WEB_UI.test(t) && !HAS_BROWSER_SIGNIN.test(t) && !HAS_API_ONLY.test(t)) {
53
+ findings.push({
54
+ kind: 'impliedButNotStated',
55
+ detail: 'artefact names a web UI (browser / HTML / dashboard) but does not name whether a browser sign-in page is in scope. The current wording could ship as API-only or with an HTML login; both are consistent with the brief. Resolve before REQ drafting so downstream ACs land on the right shape.',
56
+ });
57
+ }
58
+
59
+ // contradiction: brief says "no login required" alongside an admin UI.
60
+ if (NEGATED_LOGIN_PHRASES.test(t) && HAS_ADMIN_UI.test(t)) {
61
+ findings.push({
62
+ kind: 'contradiction',
63
+ detail: 'artefact says "no login required" in one section and names an admin dashboard elsewhere. An unauthenticated admin surface is either a mistake in the brief or a deliberate posture that changes every auth AC downstream.',
64
+ });
65
+ }
66
+
67
+ // missingLoadBearingConstraint: brief names a third-party service but
68
+ // does not name the credential env var.
69
+ for (const svc of SERVICE_HINTS) {
70
+ const nameRe = new RegExp(`\\b${svc.name}\\b`, 'i');
71
+ if (nameRe.test(t) && !svc.envHint.test(t)) {
72
+ findings.push({
73
+ kind: 'missingLoadBearingConstraint',
74
+ detail: `artefact names ${svc.name} as an integration but does not name the credential env var. Track A preflight will refuse to accept the service without a declared credential; capture the name now so the preflight session runs against a concrete key.`,
75
+ });
76
+ }
77
+ }
78
+
79
+ return findings;
80
+ }