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,185 @@
1
+ // Design-shape questions catalogue for `rcf preflight`
2
+ // (verification-integrity-cluster-spec ADDENDUM §A).
3
+ //
4
+ // The v1 catalogue carries exactly ONE question — `auth.htmlLoginPage` —
5
+ // per the plan-committed Baz-approved scope. Additional questions land
6
+ // in v2 only on the evidence-gated triggers named in §A.3 (three
7
+ // separate projects that would have benefited, OR a case-study defect
8
+ // the existing surface cannot express). Broadening speculatively
9
+ // re-introduces the elicitation surface bloat this addendum exists to
10
+ // avoid.
11
+ //
12
+ // Two exports:
13
+ // - CATALOGUE_V1: the versioned question set, one entry per question.
14
+ // - selectApplicableQuestions(reqs): given a list of REQs, decide which
15
+ // questions to pose. Deterministic; the interactive session drives it,
16
+ // the non-interactive path reads the same catalogue when validating a
17
+ // pre-filled input file.
18
+ //
19
+ // The catalogue is data, not logic. Downstream consumers own the
20
+ // side-effects (writing designShapeAnswers into the preFlightConfig
21
+ // record; the Track B / Track C+D field composition documented in
22
+ // ADDENDUM §A.2 and §A.5).
23
+
24
+ import { matchReqShapeSignals } from '#core/patterns/req-shapes';
25
+
26
+ /**
27
+ * @typedef {'reqScoped'} DesignShapeQuestionScope
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} DesignShapeAnswerChoice
32
+ * @property {string} value the answer id written into designShapeAnswers[]
33
+ * @property {string} display operator-facing label
34
+ * @property {boolean} triggersOptOut whether choosing this writes into baselineAcOptOuts[]
35
+ * @property {string} [uiBaselineWritePath] dot path into uiBaseline.defaults when known
36
+ * @property {boolean|string} [uiBaselineWriteValue] value to write at that path
37
+ */
38
+
39
+ /**
40
+ * @typedef {object} DesignShapeQuestion
41
+ * @property {string} id catalogue id (for example, auth.htmlLoginPage)
42
+ * @property {DesignShapeQuestionScope} scope
43
+ * @property {(reqDoc: object) => boolean} trigger returns true when the REQ should be asked this question
44
+ * @property {string} prompt canonical prompt text
45
+ * @property {DesignShapeAnswerChoice[]} choices
46
+ * @property {number} reasonMinLength minimum reason length when the answer triggers a ledger write; matches Track C+D floor
47
+ * @property {string} baselineKey Track C+D baseline key this question backs, for opt-out ledger writes
48
+ */
49
+
50
+ /**
51
+ * True when a REQ carries the `auth` shape. Reads
52
+ * `req.shapeClassification.shapes[]` when the classifier has run (Track
53
+ * C+D populates this); falls back to a live `matchReqShapeSignals` pass
54
+ * over the REQ's title, description and rationale when the field is
55
+ * absent (Track C+D classifier not yet run on this repo).
56
+ *
57
+ * @param {object} reqDoc
58
+ * @returns {boolean}
59
+ */
60
+ function reqCarriesAuthShape(reqDoc) {
61
+ const classified = reqDoc?.shapeClassification?.shapes;
62
+ if (Array.isArray(classified) && classified.length > 0) {
63
+ return classified.includes('auth');
64
+ }
65
+ const parts = [reqDoc?.title, reqDoc?.description, reqDoc?.rationale]
66
+ .filter((s) => typeof s === 'string' && s.length > 0);
67
+ for (const text of parts) {
68
+ for (const m of matchReqShapeSignals(text)) {
69
+ if (m.shape === 'auth') return true;
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * Deep-freeze helper. Object.freeze is shallow, so a caller could mutate
77
+ * a nested field even on an outwardly frozen object. Review N-6
78
+ * (non-blocking): the catalogue is a spec-committed data surface and
79
+ * must not drift in memory. This helper recurses through plain objects
80
+ * and arrays, freezing every reachable value, and returns the input
81
+ * for chaining. Functions (used as `trigger`) are frozen at the field
82
+ * level (Object.freeze on the containing object) but not descended
83
+ * into.
84
+ *
85
+ * @template T
86
+ * @param {T} value
87
+ * @returns {Readonly<T>}
88
+ */
89
+ function deepFreeze(value) {
90
+ if (value === null || typeof value !== 'object') return value;
91
+ if (Object.isFrozen(value)) return value;
92
+ Object.freeze(value);
93
+ for (const key of Reflect.ownKeys(value)) {
94
+ const nested = /** @type {any} */ (value)[key];
95
+ if (nested !== null && typeof nested === 'object' && !Object.isFrozen(nested)) {
96
+ deepFreeze(nested);
97
+ }
98
+ }
99
+ return value;
100
+ }
101
+
102
+ /**
103
+ * Ratified catalogue. Editing this array in-place is a spec change;
104
+ * adding a question requires evidence per §A.3. Deep-frozen so the
105
+ * shape cannot drift in memory even at nested field level (review
106
+ * N-6).
107
+ *
108
+ * @type {Readonly<DesignShapeQuestion[]>}
109
+ */
110
+ export const CATALOGUE_V1 = deepFreeze([
111
+ {
112
+ id: 'auth.htmlLoginPage',
113
+ scope: 'reqScoped',
114
+ trigger: reqCarriesAuthShape,
115
+ prompt: 'HTML login page or API-only?',
116
+ choices: [
117
+ {
118
+ value: 'htmlLoginPage',
119
+ display: 'HTML login page',
120
+ triggersOptOut: false,
121
+ uiBaselineWritePath: 'defaults.authFlow.htmlLoginPageRequired',
122
+ uiBaselineWriteValue: true,
123
+ },
124
+ {
125
+ value: 'apiOnly',
126
+ display: 'API-only',
127
+ triggersOptOut: true,
128
+ uiBaselineWritePath: 'defaults.authFlow.htmlLoginPageRequired',
129
+ uiBaselineWriteValue: false,
130
+ },
131
+ ],
132
+ reasonMinLength: 20,
133
+ baselineKey: 'auth.htmlLoginPage',
134
+ },
135
+ ]);
136
+
137
+ /**
138
+ * Given the REQs in scope, decide which questions to pose against each
139
+ * REQ. Returns one entry per (question, req) pair whose trigger fires;
140
+ * the interactive session iterates in the returned order.
141
+ *
142
+ * @param {object[]} reqs
143
+ * @param {Readonly<DesignShapeQuestion[]>} [catalogue]
144
+ * @returns {Array<{ question: DesignShapeQuestion, reqId: string }>}
145
+ */
146
+ export function selectApplicableQuestions(reqs, catalogue = CATALOGUE_V1) {
147
+ /** @type {Array<{ question: DesignShapeQuestion, reqId: string }>} */
148
+ const out = [];
149
+ for (const req of reqs ?? []) {
150
+ if (!req?.reqId) continue;
151
+ for (const question of catalogue) {
152
+ if (question.scope !== 'reqScoped') continue;
153
+ if (question.trigger(req)) {
154
+ out.push({ question, reqId: req.reqId });
155
+ }
156
+ }
157
+ }
158
+ return out;
159
+ }
160
+
161
+ /**
162
+ * Validate a design-shape answer against the catalogue. Returns null on
163
+ * a valid answer, or a string message describing the problem.
164
+ *
165
+ * @param {object} args
166
+ * @param {string} args.questionId
167
+ * @param {string} args.answer
168
+ * @param {string} [args.reason]
169
+ * @returns {string|null}
170
+ */
171
+ export function validateDesignShapeAnswer({ questionId, answer, reason }) {
172
+ const question = CATALOGUE_V1.find((q) => q.id === questionId);
173
+ if (!question) return `unknown design-shape question '${questionId}'`;
174
+ const choice = question.choices.find((c) => c.value === answer);
175
+ if (!choice) {
176
+ const values = question.choices.map((c) => c.value).join(' | ');
177
+ return `unknown answer '${answer}' for question '${questionId}' (expected ${values})`;
178
+ }
179
+ if (choice.triggersOptOut) {
180
+ if (typeof reason !== 'string' || reason.length < question.reasonMinLength) {
181
+ return `answer '${answer}' for question '${questionId}' writes a baseline opt-out; reason must be at least ${question.reasonMinLength} characters`;
182
+ }
183
+ }
184
+ return null;
185
+ }
@@ -0,0 +1,90 @@
1
+ // Barrel: pre-flight surface for verification-integrity-cluster-spec §4.
2
+ //
3
+ // Consumers reach the pre-flight surface through this barrel so the
4
+ // internal file layout (secrets side-file, scanner, session, design
5
+ // shapes, manifest writer) can evolve without churning callers. The
6
+ // gitignore aggregator seam is the sole exception: `managed-gitignore.js`
7
+ // imports `preflightEntry` directly from `./secrets.js` to keep the
8
+ // aggregator's own import list flat, one entry per module (0.6.0 spec
9
+ // §4.1 D-4 pattern).
10
+
11
+ import {
12
+ CATALOGUE_V1,
13
+ } from './design-shapes.js';
14
+ import {
15
+ composeOptOutRecord,
16
+ } from './manifest-writer.js';
17
+
18
+ export {
19
+ preflightEntry,
20
+ preflightSecretsPath,
21
+ loadPreflightSecretsFile,
22
+ recordPreflightSecret,
23
+ } from './secrets.js';
24
+
25
+ export {
26
+ scanForServiceCandidates,
27
+ CATEGORY_DISPLAY_NAMES,
28
+ } from './scanner.js';
29
+
30
+ export {
31
+ CATALOGUE_V1,
32
+ selectApplicableQuestions,
33
+ validateDesignShapeAnswer,
34
+ } from './design-shapes.js';
35
+
36
+ export {
37
+ composePreflightRecord,
38
+ composeOptOutRecord,
39
+ nextPreflightId,
40
+ nextOptOutId,
41
+ writePreflightRecord,
42
+ } from './manifest-writer.js';
43
+
44
+ export {
45
+ runInteractiveSession,
46
+ normaliseNonInteractiveInput,
47
+ } from './session.js';
48
+
49
+ /**
50
+ * Compose the baselineAcOptOuts entries triggered by design-shape
51
+ * answers on a preflight record. Called by the CLI handler after
52
+ * `composePreflightRecord` so the `linkedPreFlightConfigRef` can point
53
+ * at the composed record's id.
54
+ *
55
+ * @param {object} args
56
+ * @param {object|null} args.manifest
57
+ * @param {object} args.preflightRecord the composed record (id already allocated)
58
+ * @param {import('./manifest-writer.js').PreflightDesignShapeAnswer[]} args.designShapeAnswers
59
+ * @param {Date} [args.now]
60
+ * @returns {object[]}
61
+ */
62
+ export function composeDesignShapeOptOuts({ manifest, preflightRecord, designShapeAnswers, now = new Date() }) {
63
+ const optOuts = [];
64
+ let dateCursor = now;
65
+ for (const answer of designShapeAnswers) {
66
+ const question = CATALOGUE_V1.find((q) => q.id === answer.questionId);
67
+ if (!question) continue;
68
+ const choice = question.choices.find((c) => c.value === answer.answer);
69
+ if (!choice || !choice.triggersOptOut) continue;
70
+ // Compose one opt-out at a time so the id monotonic counter walks
71
+ // forward correctly on a single-session batch. We synthesise a
72
+ // provisional manifest snapshot per compose so `nextOptOutId` sees
73
+ // any records the previous compose calls in this loop added.
74
+ const provisional = {
75
+ ...(manifest ?? {}),
76
+ baselineAcOptOuts: [...(manifest?.baselineAcOptOuts ?? []), ...optOuts],
77
+ };
78
+ const linkedPreFlightConfigRef = `${preflightRecord.id}#designShapeAnswers.${answer.questionId}`;
79
+ const record = composeOptOutRecord({
80
+ manifest: provisional,
81
+ baselineKey: question.baselineKey,
82
+ reason: answer.reason ?? '',
83
+ reqId: answer.reqId,
84
+ linkedPreFlightConfigRef,
85
+ now: dateCursor,
86
+ });
87
+ optOuts.push(record);
88
+ }
89
+ return optOuts;
90
+ }
@@ -0,0 +1,264 @@
1
+ // Pre-flight manifest writer
2
+ // (verification-integrity-cluster-spec §3.3, ADDENDUM §A.2).
3
+ //
4
+ // Composes and persists `preFlightConfig[]` records on the project
5
+ // manifest. The record shape is defined by the 0.4.0 manifest schema
6
+ // (`$defs/preFlightConfigRecord`); this module owns id allocation,
7
+ // timestamping, per-service entry composition, design-shape answer
8
+ // composition, and the `baselineAcOptOuts[]` fold that happens when a
9
+ // design-shape answer triggers a ledger write per §A.2.
10
+ //
11
+ // The writer takes an already-loaded walker tree and a session summary,
12
+ // runs the composition, and calls the core store's `updateDocument`
13
+ // against the manifest. All secrets discipline is upstream: this module
14
+ // never sees a credential value; the session's per-service records carry
15
+ // only booleans (`credentialSupplied`, `sandboxProvisioned`) and the
16
+ // env-var NAME (side-file, not chain).
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} PreflightServiceRuling
26
+ * @property {string} id
27
+ * @property {string} displayName
28
+ * @property {string[]} sourceRefs
29
+ * @property {'live'|'sandboxed'|'mocked'|'declaredMockOnly'|'notShipped'} attestationMode
30
+ * @property {boolean} credentialSupplied
31
+ * @property {boolean} sandboxProvisioned
32
+ * @property {string} [operatorReason]
33
+ * @property {string[]} [affectedFbsIds]
34
+ */
35
+
36
+ /**
37
+ * @typedef {object} PreflightDesignShapeAnswer
38
+ * @property {string} questionId
39
+ * @property {string} [reqId]
40
+ * @property {string} answer
41
+ * @property {string} [reason]
42
+ */
43
+
44
+ /**
45
+ * @typedef {object} BaselineOptOutWrite
46
+ * @property {string} baselineKey
47
+ * @property {'req'|'project'} scope
48
+ * @property {string} reason
49
+ * @property {string} [reqId]
50
+ * @property {string} linkedPreFlightConfigRef
51
+ */
52
+
53
+ /**
54
+ * Compute the next preFlightConfig id for today: monotonic
55
+ * `pfc-YYYY-MM-DD-NNN`. Reads the manifest's existing preFlightConfig
56
+ * to find the highest suffix for the given date.
57
+ *
58
+ * @param {object|null} manifest
59
+ * @param {Date} now
60
+ * @returns {string}
61
+ */
62
+ export function nextPreflightId(manifest, now) {
63
+ const y = now.getUTCFullYear().toString().padStart(4, '0');
64
+ const m = (now.getUTCMonth() + 1).toString().padStart(2, '0');
65
+ const d = now.getUTCDate().toString().padStart(2, '0');
66
+ const prefix = `pfc-${y}-${m}-${d}-`;
67
+ const existing = Array.isArray(manifest?.preFlightConfig) ? manifest.preFlightConfig : [];
68
+ let maxN = 0;
69
+ for (const rec of existing) {
70
+ if (typeof rec?.id !== 'string' || !rec.id.startsWith(prefix)) continue;
71
+ const suffix = rec.id.slice(prefix.length);
72
+ const n = Number.parseInt(suffix, 10);
73
+ if (Number.isFinite(n) && n > maxN) maxN = n;
74
+ }
75
+ const next = (maxN + 1).toString().padStart(3, '0');
76
+ return `${prefix}${next}`;
77
+ }
78
+
79
+ /**
80
+ * Compute the next baselineAcOptOuts id: monotonic `boo-YYYY-MM-DD-NNN`.
81
+ *
82
+ * @param {object|null} manifest
83
+ * @param {Date} now
84
+ * @returns {string}
85
+ */
86
+ export function nextOptOutId(manifest, now) {
87
+ const y = now.getUTCFullYear().toString().padStart(4, '0');
88
+ const m = (now.getUTCMonth() + 1).toString().padStart(2, '0');
89
+ const d = now.getUTCDate().toString().padStart(2, '0');
90
+ const prefix = `boo-${y}-${m}-${d}-`;
91
+ const existing = Array.isArray(manifest?.baselineAcOptOuts) ? manifest.baselineAcOptOuts : [];
92
+ let maxN = 0;
93
+ for (const rec of existing) {
94
+ if (typeof rec?.id !== 'string' || !rec.id.startsWith(prefix)) continue;
95
+ const n = Number.parseInt(rec.id.slice(prefix.length), 10);
96
+ if (Number.isFinite(n) && n > maxN) maxN = n;
97
+ }
98
+ return `${prefix}${(maxN + 1).toString().padStart(3, '0')}`;
99
+ }
100
+
101
+ /**
102
+ * Compose one preFlightConfig record from a session. The record's
103
+ * `id` is monotonic per date; `createdAt` and `operatorAckAt` are the
104
+ * same instant for a completed session.
105
+ *
106
+ * @param {object} args
107
+ * @param {object|null} args.manifest
108
+ * @param {string} args.prdId
109
+ * @param {PreflightServiceRuling[]} args.services
110
+ * @param {PreflightDesignShapeAnswer[]} [args.designShapeAnswers]
111
+ * @param {Date} [args.now]
112
+ * @returns {object}
113
+ */
114
+ export function composePreflightRecord({
115
+ manifest, prdId, services, designShapeAnswers = [], now = new Date(),
116
+ }) {
117
+ const isoNow = now.toISOString();
118
+ const id = nextPreflightId(manifest, now);
119
+ const servicesInScope = services.map((s) => {
120
+ const entry = {
121
+ id: s.id,
122
+ displayName: s.displayName,
123
+ sourceRefs: Array.isArray(s.sourceRefs) ? [...s.sourceRefs] : [],
124
+ attestationMode: s.attestationMode,
125
+ credentialSupplied: Boolean(s.credentialSupplied),
126
+ sandboxProvisioned: Boolean(s.sandboxProvisioned),
127
+ };
128
+ if (typeof s.operatorReason === 'string' && s.operatorReason.length > 0) {
129
+ entry.operatorReason = s.operatorReason;
130
+ }
131
+ if (Array.isArray(s.affectedFbsIds) && s.affectedFbsIds.length > 0) {
132
+ entry.affectedFbsIds = [...s.affectedFbsIds];
133
+ }
134
+ return entry;
135
+ });
136
+ const record = {
137
+ id,
138
+ createdAt: isoNow,
139
+ prdId,
140
+ servicesInScope,
141
+ operatorAckAt: isoNow,
142
+ };
143
+ if (Array.isArray(designShapeAnswers) && designShapeAnswers.length > 0) {
144
+ record.designShapeAnswers = designShapeAnswers.map((a) => {
145
+ const entry = { questionId: a.questionId, answer: a.answer, answeredAt: isoNow };
146
+ if (typeof a.reqId === 'string' && a.reqId.length > 0) entry.reqId = a.reqId;
147
+ if (typeof a.reason === 'string' && a.reason.length > 0) entry.reason = a.reason;
148
+ return entry;
149
+ });
150
+ }
151
+ return record;
152
+ }
153
+
154
+ /**
155
+ * Compose a baselineAcOptOuts entry for a design-shape answer that
156
+ * triggers a ledger write (ADDENDUM §A.2). The record is REQ-scoped
157
+ * when the answer is REQ-scoped; project scope requires an explicit
158
+ * upstream ruling.
159
+ *
160
+ * @param {object} args
161
+ * @param {object|null} args.manifest
162
+ * @param {string} args.baselineKey
163
+ * @param {string} args.reason
164
+ * @param {string} [args.reqId]
165
+ * @param {string} args.linkedPreFlightConfigRef
166
+ * @param {Date} [args.now]
167
+ * @returns {object}
168
+ */
169
+ export function composeOptOutRecord({
170
+ manifest, baselineKey, reason, reqId, linkedPreFlightConfigRef, now = new Date(),
171
+ }) {
172
+ const isoNow = now.toISOString();
173
+ const id = nextOptOutId(manifest, now);
174
+ const record = {
175
+ id,
176
+ createdAt: isoNow,
177
+ baselineKey,
178
+ scope: reqId ? 'req' : 'project',
179
+ reason,
180
+ operatorAckAt: isoNow,
181
+ linkedPreFlightConfigRef,
182
+ };
183
+ if (reqId) record.reqId = reqId;
184
+ return record;
185
+ }
186
+
187
+ /**
188
+ * Persist a preflight session: append the composed record onto the
189
+ * manifest's `preFlightConfig[]` and any triggered opt-out records onto
190
+ * `baselineAcOptOuts[]`. Validates the composed manifest against the
191
+ * schema BEFORE writing; on validation failure the on-disk manifest is
192
+ * untouched. Atomic write via a `.tmp` rename so a partial write cannot
193
+ * leave the manifest half-composed.
194
+ *
195
+ * The store's `updateDocument` verb accepts child-doc and root-doc ids
196
+ * (PRD / TAD / BS) via `pathForId`, but the manifest sits outside that
197
+ * id space; every other manifest mutation the codebase performs also
198
+ * goes direct-to-disk (see `initProject` in `core/store/init.js`), which
199
+ * this helper mirrors.
200
+ *
201
+ * @param {object} args
202
+ * @param {string} args.projectRoot
203
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
204
+ * @param {object} args.record the composed preFlightConfig record
205
+ * @param {object[]} [args.optOuts] composed baselineAcOptOuts entries
206
+ * @param {object} [args.uiBaselineWrites] optional deep-set writes for uiBaseline.defaults (Track B fenced-TODO)
207
+ * @param {object} [args.options]
208
+ * @returns {Promise<{ record: object, optOutIds: string[], skippedUiBaseline: boolean } | import('#core/errors').RcfError>}
209
+ */
210
+ export async function writePreflightRecord({
211
+ projectRoot, tree, record, optOuts = [], options = {},
212
+ }) {
213
+ const manifest = tree.manifest ?? {};
214
+ const nextManifest = { ...manifest };
215
+
216
+ const existingPfc = Array.isArray(nextManifest.preFlightConfig) ? nextManifest.preFlightConfig : [];
217
+ nextManifest.preFlightConfig = [...existingPfc, record];
218
+
219
+ if (optOuts.length > 0) {
220
+ const existingOptOuts = Array.isArray(nextManifest.baselineAcOptOuts) ? nextManifest.baselineAcOptOuts : [];
221
+ nextManifest.baselineAcOptOuts = [...existingOptOuts, ...optOuts];
222
+ }
223
+
224
+ const relPath = 'rcf/manifest.json';
225
+ const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: relPath });
226
+ if (validation) return validation;
227
+
228
+ const absPath = join(projectRoot, 'rcf', 'manifest.json');
229
+ if (options.dryRun) {
230
+ return { record, optOutIds: optOuts.map((o) => o.id), skippedUiBaseline: true, dryRun: true };
231
+ }
232
+ try {
233
+ await mkdir(dirname(absPath), { recursive: true });
234
+ const tmp = `${absPath}.tmp`;
235
+ await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
236
+ try {
237
+ await rename(tmp, absPath);
238
+ } catch (err) {
239
+ try { await unlink(tmp); } catch { /* ignore */ }
240
+ throw err;
241
+ }
242
+ } catch (err) {
243
+ return rcfError({
244
+ kind: 'ioFailure',
245
+ message: `preflight: manifest write failed: ${err.message}`,
246
+ filePath: relPath,
247
+ stack: err.stack,
248
+ });
249
+ }
250
+ return {
251
+ record,
252
+ optOutIds: optOuts.map((o) => o.id),
253
+ // Track A does NOT build the uiBaseline init machinery (that's
254
+ // Track B). When a design-shape answer would write into
255
+ // `uiBaseline.defaults.authFlow.htmlLoginPageRequired` and no
256
+ // `uiBaseline` record exists yet, the answer stays in
257
+ // designShapeAnswers[] and (when the answer is API-only) in the
258
+ // baselineAcOptOuts[] ledger, exactly as ADDENDUM §A.2 specifies.
259
+ // The uiBaseline write itself is fenced pending Track B — the
260
+ // linkedPreFlightConfigRef on the opt-out record is the seam
261
+ // Track B reads.
262
+ skippedUiBaseline: true,
263
+ };
264
+ }