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,156 @@
1
+ // Register-canary runner (spec §7.4).
2
+ //
3
+ // Runs one fixture (or the whole pack) against a subagent driver,
4
+ // grades the response with core's REGISTER_CANARY_DIMENSIONS_V1, and
5
+ // composes the record shape for the fixture manifest.
6
+ //
7
+ // Subagent driver: injected. The default is a mock that returns a
8
+ // clean "in hand, next step is X" message, but the runner FORCES a
9
+ // mock run to be distinguishable from a real subagent run in the
10
+ // durable record — a mock never produces a pass verdict indistinguishable
11
+ // from a live one. Concretely:
12
+ // - the runner takes a `driverMode` argument ('mock' | 'live');
13
+ // - a mock run suffixes `buildVersion` with `-mockdriver` so the
14
+ // mode marker travels with the record into any chain manifest a
15
+ // downstream consumer could copy the record into (§7.4 fixture
16
+ // manifest is not chain-validated, but the record shape must stay
17
+ // honest across surfaces);
18
+ // - a mock run OVERRIDES the top-level verdict to `fail` regardless
19
+ // of grade, and populates `shipDespiteFailReason` with a plain
20
+ // "mock canary driver — infrastructure test only, no real subagent
21
+ // dispatched" line, so the record is durably marked as machinery
22
+ // verification rather than a release gate.
23
+ // This mirrors Track A N-2 (unwired mutation runner returning PASS is
24
+ // theatre) and Track B flagged-call-2 (stub browser driver forced to
25
+ // warn-never-pass): unwired drivers must not produce clean-pass records.
26
+ // The production driver (Anthropic Agent SDK, Opus 4.7 per estate
27
+ // ladder) invokes with `driverMode: 'live'` and the overlay is skipped
28
+ // entirely — the record is a genuine grader output.
29
+
30
+ import { gradeResponse } from '#core/patterns/register-canary';
31
+
32
+ import { composeCanaryRecord } from './record-writer.js';
33
+
34
+ /**
35
+ * Marker text applied to a mock-driver record's `shipDespiteFailReason`.
36
+ * Deliberately long enough to satisfy the schema `minLength: 1` and
37
+ * explicit enough that a reviewer greps the record and sees the mode.
38
+ */
39
+ export const MOCK_DRIVER_MARKER = 'mock canary driver used; no real subagent was dispatched. This record verifies canary infrastructure only, not the register itself.';
40
+
41
+ /**
42
+ * Suffix appended to `buildVersion` when the driver mode is mock. Ports
43
+ * across surfaces (the fixture manifest is a private greppable log
44
+ * today; a future ship into a chain manifest would carry the marker
45
+ * intact through the schema-legal buildVersion string).
46
+ */
47
+ export const MOCK_DRIVER_BUILD_VERSION_SUFFIX = '-mockdriver';
48
+
49
+ /**
50
+ * @typedef {object} CanaryDriverInput
51
+ * @property {string} operatorPrompt
52
+ * @property {Array<{ path: string, content: string }>} supportingArtefacts
53
+ * @property {string[]} grantedPermissions
54
+ * @property {string} guidance the shipping playbook content injected as system context
55
+ * @property {string} fixtureId
56
+ */
57
+
58
+ /**
59
+ * @typedef {object} CanaryDriverOutput
60
+ * @property {string} responseBody the first-response body text (system prompts / tool-use excluded)
61
+ */
62
+
63
+ /**
64
+ * @typedef {(input: CanaryDriverInput) => Promise<CanaryDriverOutput>} CanaryDriver
65
+ */
66
+
67
+ /**
68
+ * A clean-register mock driver. Produces a short, on-register reply
69
+ * that passes every graded dimension. Used by tests and by the CLI
70
+ * when no live driver is wired.
71
+ *
72
+ * @type {CanaryDriver}
73
+ */
74
+ export async function MOCK_SUBAGENT() {
75
+ return {
76
+ responseBody: "Have it in hand. The brief points at a single-operator uptime tool with an admin surface and email recovery pings. Two open decisions before any code: which host you want to deploy against, and the credential store for Resend. Once you name those I will scaffold the tree and hand you the first build item.",
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Run one fixture against a driver and produce a record.
82
+ *
83
+ * @param {object} args
84
+ * @param {object} args.fixture
85
+ * @param {CanaryDriver} args.driver
86
+ * @param {'mock'|'live'} args.driverMode mock runs are overridden to fail; see file header
87
+ * @param {string} args.buildVersion
88
+ * @param {string} args.guidance
89
+ * @param {object[]} args.existingRecords
90
+ * @param {Date} [args.now]
91
+ * @returns {Promise<{ record: object, responseBody: string }>}
92
+ */
93
+ export async function runCanaryAgainstFixture({ fixture, driver, driverMode, buildVersion, guidance, existingRecords, now }) {
94
+ if (driverMode !== 'mock' && driverMode !== 'live') {
95
+ throw new Error(`runCanaryAgainstFixture: driverMode must be 'mock' or 'live' (got ${JSON.stringify(driverMode)})`);
96
+ }
97
+ const output = await driver({
98
+ operatorPrompt: fixture.operatorPrompt,
99
+ supportingArtefacts: fixture.supportingArtefacts ?? [],
100
+ grantedPermissions: fixture.grantedPermissions ?? [],
101
+ guidance,
102
+ fixtureId: fixture.id,
103
+ });
104
+ const grade = gradeResponse({
105
+ responseBody: output.responseBody,
106
+ grantedPermissions: fixture.grantedPermissions,
107
+ wordCountBudget: fixture.wordCountBudget,
108
+ });
109
+ // A mock driver never produces a clean-pass record: suffix the
110
+ // buildVersion, force the top-level verdict to `fail`, and stamp
111
+ // shipDespiteFailReason with the mock-marker string. The `grades`
112
+ // block still reflects what the mock actually said, so a reader can
113
+ // see the pattern-level detail — the aggregate is what is forced.
114
+ const effectiveBuildVersion = driverMode === 'mock' ? `${buildVersion}${MOCK_DRIVER_BUILD_VERSION_SUFFIX}` : buildVersion;
115
+ const effectiveGrade = driverMode === 'mock' ? { verdict: 'fail', grades: grade.grades } : grade;
116
+ const record = composeCanaryRecord({
117
+ existingRecords,
118
+ buildVersion: effectiveBuildVersion,
119
+ fixturePromptId: fixture.id,
120
+ responseBody: output.responseBody,
121
+ grade: effectiveGrade,
122
+ shipDespiteFailReason: driverMode === 'mock' ? MOCK_DRIVER_MARKER : undefined,
123
+ now,
124
+ });
125
+ return { record, responseBody: output.responseBody };
126
+ }
127
+
128
+ /**
129
+ * Run every fixture in the pack. Returns the records in run order plus
130
+ * an aggregate verdict (any fail forces aggregate fail).
131
+ *
132
+ * @param {object} args
133
+ * @param {object[]} args.fixtures
134
+ * @param {CanaryDriver} args.driver
135
+ * @param {'mock'|'live'} args.driverMode
136
+ * @param {string} args.buildVersion
137
+ * @param {string} args.guidance
138
+ * @param {object[]} args.existingRecords
139
+ * @param {Date} [args.now]
140
+ * @returns {Promise<{ records: object[], verdict: 'pass'|'fail', driverMode: 'mock'|'live' }>}
141
+ */
142
+ export async function runCanaryPack({ fixtures, driver, driverMode, buildVersion, guidance, existingRecords, now }) {
143
+ const records = [];
144
+ let aggregate = 'pass';
145
+ let existing = [...existingRecords];
146
+ for (const fixture of fixtures) {
147
+ // eslint-disable-next-line no-await-in-loop
148
+ const { record } = await runCanaryAgainstFixture({
149
+ fixture, driver, driverMode, buildVersion, guidance, existingRecords: existing, now,
150
+ });
151
+ records.push(record);
152
+ existing = [...existing, record];
153
+ if (record.verdict === 'fail') aggregate = 'fail';
154
+ }
155
+ return { records, verdict: aggregate, driverMode };
156
+ }
@@ -0,0 +1,86 @@
1
+ // Stage-1 refusal gate (spec §5.4).
2
+ //
3
+ // `rcf build --next` refuses to enter Build for any FBS binding an AC
4
+ // on a US that still has open baseline sweep candidates. The refusal
5
+ // message follows the canonical shape in spec §5.4.
6
+ //
7
+ // The gate itself is a pure function against the tree; the CLI layer
8
+ // calls it after selecting the next actionable FBS (see build.js).
9
+
10
+ import { openCandidatesForUs } from './open-candidates.js';
11
+
12
+ /**
13
+ * @typedef {object} Stage1Refusal
14
+ * @property {string} fbsId
15
+ * @property {string} usId
16
+ * @property {Array<{ baselineKey: string, canonicalText: string, reqShape: string }>} openCandidates
17
+ * @property {string} reqId
18
+ * @property {string} message pre-formatted refusal, spec §5.4 shape
19
+ */
20
+
21
+ /**
22
+ * Inspect an FBS for a refusal condition. Walks the FBS's bound ACs to
23
+ * their owning USes; any US with open baseline candidates fails the
24
+ * gate. Returns null when the FBS is clear.
25
+ *
26
+ * @param {import('#core/store/walker.js').TreeModel} tree
27
+ * @param {string} fbsId
28
+ * @returns {Stage1Refusal|null}
29
+ */
30
+ export function fbsRefusalForOpenSweep(tree, fbsId) {
31
+ const fbs = tree?.byId?.get(fbsId);
32
+ if (!fbs) return null;
33
+ const acIds = Array.isArray(fbs.acIds) ? fbs.acIds : [];
34
+ const seenUsIds = new Set();
35
+ for (const acId of acIds) {
36
+ // AC ids are inline on the parent US; `walkTree` records the
37
+ // AC-to-US mapping under `parentByChild` for exactly this lookup
38
+ // (the `usByAcId` map on the render-side tree model is derived
39
+ // from the same source).
40
+ const usId = tree.parentByChild?.get(acId);
41
+ if (!usId || seenUsIds.has(usId)) continue;
42
+ seenUsIds.add(usId);
43
+ const us = tree.byId.get(usId);
44
+ if (!us) continue;
45
+ const open = openCandidatesForUs(tree, us);
46
+ if (open.length === 0) continue;
47
+ return {
48
+ fbsId,
49
+ usId,
50
+ reqId: us.reqId,
51
+ openCandidates: open,
52
+ message: formatStage1RefusalMessage({ fbsId, usId, reqId: us.reqId, openCandidates: open }),
53
+ };
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Format the spec §5.4 refusal line for an operator.
60
+ *
61
+ * @param {object} args
62
+ * @param {string} args.fbsId
63
+ * @param {string} args.usId
64
+ * @param {string} args.reqId
65
+ * @param {Array<{ baselineKey: string, canonicalText: string }>} args.openCandidates
66
+ * @returns {string}
67
+ */
68
+ export function formatStage1RefusalMessage({ fbsId, usId, reqId, openCandidates }) {
69
+ const lines = [];
70
+ lines.push(`refused build: ${fbsId} binds ACs on ${usId}, but ${usId} has ${openCandidates.length} open baseline`);
71
+ lines.push(` candidate${openCandidates.length === 1 ? '' : 's'} awaiting a decision:`);
72
+ for (const c of openCandidates) {
73
+ const short = shortDescription(c.canonicalText);
74
+ lines.push(` ${c.baselineKey}: ${short}`);
75
+ }
76
+ lines.push(` Resolve: rcf req-baseline sweep --req ${reqId}`);
77
+ return lines.join('\n');
78
+ }
79
+
80
+ function shortDescription(text) {
81
+ // Trim to a leading "given ... then <clause>" summary; cap at 100
82
+ // chars with an ellipsis. Never rewrites the sentence.
83
+ const trimmed = typeof text === 'string' ? text : '';
84
+ if (trimmed.length <= 100) return trimmed;
85
+ return `${trimmed.slice(0, 97)}...`;
86
+ }
@@ -0,0 +1,27 @@
1
+ // Public surface for the baseline-AC injection module
2
+ // (elicitation-and-playbook-hardening-0.7.0-spec §5).
3
+
4
+ export {
5
+ planSweep,
6
+ applySweepDecisions,
7
+ applyPendingBaselinesForUs,
8
+ } from './sweep.js';
9
+
10
+ export {
11
+ writeOptOut,
12
+ removeOptOut,
13
+ nextOptOutId,
14
+ isOptedOut,
15
+ optOutMap,
16
+ } from './opt-out.js';
17
+
18
+ export {
19
+ listOpenCandidates,
20
+ usHasOpenCandidates,
21
+ openCandidatesForUs,
22
+ } from './open-candidates.js';
23
+
24
+ export {
25
+ fbsRefusalForOpenSweep,
26
+ formatStage1RefusalMessage,
27
+ } from './gate.js';
@@ -0,0 +1,143 @@
1
+ // Open-sweep candidate tracker (spec §5.3 moment 4, §5.4 gate).
2
+ //
3
+ // The moment-4 posture: a non-interactive `rcf create us` under a
4
+ // classified REQ surfaces baseline keys that are neither present as
5
+ // ACs nor covered by an opt-out. This module answers "which USes have
6
+ // baselineKeys that are still OPEN?" without ever writing to disk.
7
+ //
8
+ // There is NO persisted sweep queue: `@stravica-ai/rcf-schemas`
9
+ // user-story.schema.json is `additionalProperties: false` and carries
10
+ // no `baselineSweepQueue` property, so writing one would fail schema
11
+ // validation. Openness is DERIVED from three facts read live per call:
12
+ // the parent REQ's `shapeClassification`, the US's own AC keys, and
13
+ // the manifest's `baselineAcOptOuts[]`. Spec §18 2026-07-31 fold
14
+ // ratifies the derivation as normative (replaces the earlier
15
+ // "auto-enqueue" wording).
16
+
17
+ import { getBaselineSet, getBaselineEntry, BASELINE_SHAPE_KEYS } from '#core/baseline-catalog';
18
+ import { optOutMap } from './opt-out.js';
19
+
20
+ /**
21
+ * @typedef {object} OpenCandidate
22
+ * @property {string} usId
23
+ * @property {string} reqId
24
+ * @property {string} baselineKey
25
+ * @property {string} reqShape
26
+ * @property {string} canonicalText
27
+ */
28
+
29
+ /**
30
+ * Enumerate open sweep candidates across the tree.
31
+ *
32
+ * A candidate is open when:
33
+ * - the parent REQ carries a `shapeClassification` with a non-empty
34
+ * shape list (excluding `none`);
35
+ * - a baseline key from any of those shapes is not present as an AC
36
+ * on the US (neither authoredBy: baseline nor operatorEdited with
37
+ * the same key);
38
+ * - no baselineAcOptOuts entry covers the key (req-scoped for this
39
+ * REQ, or project-scoped).
40
+ *
41
+ * Openness is derived per call from these three signals; there is no
42
+ * persisted queue by ruling (spec §18 2026-07-31 fold; schema fence at
43
+ * user-story additionalProperties:false).
44
+ *
45
+ * Note the caller may pass reqId to narrow to one REQ.
46
+ *
47
+ * @param {import('#core/store/walker.js').TreeModel} tree
48
+ * @param {object} [opts]
49
+ * @param {string|null} [opts.reqId]
50
+ * @returns {Array<{ usId: string, reqId: string, baselineKeys: string[] }>}
51
+ */
52
+ export function listOpenCandidates(tree, opts = {}) {
53
+ const filterReq = opts.reqId ?? null;
54
+ const results = [];
55
+ for (const [id, kind] of tree.kindById) {
56
+ if (kind !== 'userStory') continue;
57
+ const us = tree.byId.get(id);
58
+ if (!us) continue;
59
+ if (filterReq && us.reqId !== filterReq) continue;
60
+ const open = openCandidatesForUs(tree, us);
61
+ if (open.length > 0) {
62
+ results.push({ usId: id, reqId: us.reqId, baselineKeys: open.map((c) => c.baselineKey) });
63
+ }
64
+ }
65
+ results.sort((a, b) => a.usId.localeCompare(b.usId));
66
+ return results;
67
+ }
68
+
69
+ /**
70
+ * Enumerate open candidates for one loaded US.
71
+ *
72
+ * @param {import('#core/store/walker.js').TreeModel} tree
73
+ * @param {object} usDoc
74
+ * @returns {OpenCandidate[]}
75
+ */
76
+ export function openCandidatesForUs(tree, usDoc) {
77
+ const reqDoc = tree?.byId?.get(usDoc?.reqId);
78
+ const shapes = shapesOfReq(reqDoc);
79
+ if (shapes.length === 0) return [];
80
+
81
+ const acKeys = new Set();
82
+ for (const ac of usDoc.acceptanceCriteria ?? []) {
83
+ if (ac?.provenance?.baselineKey) acKeys.add(ac.provenance.baselineKey);
84
+ }
85
+
86
+ const optedOut = optOutMap(tree?.manifest ?? null, usDoc.reqId);
87
+
88
+ const proposeAll = new Set();
89
+ for (const shape of shapes) {
90
+ const set = getBaselineSet(shape);
91
+ if (!set) continue;
92
+ for (const entry of set.entries) proposeAll.add(entry.baselineKey);
93
+ }
94
+
95
+ const results = [];
96
+ for (const key of proposeAll) {
97
+ if (acKeys.has(key)) continue;
98
+ if (optedOut.has(key)) continue;
99
+ const entry = getBaselineEntry(key);
100
+ if (!entry) continue;
101
+ // shape derived from entry's set (spec order: webUi > httpApi > auth
102
+ // > persistence > notifications).
103
+ const reqShape = shapeOfEntry(key);
104
+ results.push({
105
+ usId: usDoc.usId,
106
+ reqId: usDoc.reqId,
107
+ baselineKey: key,
108
+ reqShape,
109
+ canonicalText: entry.canonicalText,
110
+ });
111
+ }
112
+ // Stable order by shape then by canonical order within the set.
113
+ results.sort((a, b) => a.baselineKey.localeCompare(b.baselineKey));
114
+ return results;
115
+ }
116
+
117
+ /**
118
+ * True when the US has one or more open sweep candidates.
119
+ *
120
+ * @param {import('#core/store/walker.js').TreeModel} tree
121
+ * @param {object} usDoc
122
+ * @returns {boolean}
123
+ */
124
+ export function usHasOpenCandidates(tree, usDoc) {
125
+ return openCandidatesForUs(tree, usDoc).length > 0;
126
+ }
127
+
128
+ function shapesOfReq(reqDoc) {
129
+ const shapes = reqDoc?.shapeClassification?.shapes;
130
+ if (!Array.isArray(shapes)) return [];
131
+ return shapes.filter((s) => s !== 'none');
132
+ }
133
+
134
+ function shapeOfEntry(baselineKey) {
135
+ for (const shape of BASELINE_SHAPE_KEYS) {
136
+ const set = getBaselineSet(shape);
137
+ if (!set) continue;
138
+ for (const entry of set.entries) {
139
+ if (entry.baselineKey === baselineKey) return shape;
140
+ }
141
+ }
142
+ return 'unknown';
143
+ }
@@ -0,0 +1,195 @@
1
+ // Baseline-AC opt-out ledger (spec §5.4).
2
+ //
3
+ // Writes and removes entries on manifest.baselineAcOptOuts[]. Composes
4
+ // with the design-shape answer opt-out surface that Track A owns
5
+ // (preflight/manifest-writer.js already carries composeOptOutRecord for
6
+ // the preflight-driven case); this module is the C+D-native writer for
7
+ // operator rulings recorded outside a preflight session.
8
+ //
9
+ // Both surfaces write to the same array; both use the same
10
+ // boo-YYYY-MM-DD-NNN monotonic id scheme so id collisions across
11
+ // surfaces are impossible.
12
+
13
+ import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
14
+ import { dirname, join } from 'node:path';
15
+
16
+ import { rcfError, isRcfError } from '#core/errors';
17
+ import { validateDocument } from '#core/store';
18
+
19
+ const OPT_OUT_ID_RE = /^boo-\d{4}-\d{2}-\d{2}-(\d{3})$/;
20
+
21
+ /**
22
+ * Compose the next monotonic opt-out id for the current date. Reads the
23
+ * existing manifest to find the max NNN suffix for the same YYYY-MM-DD.
24
+ *
25
+ * @param {object|null} manifest
26
+ * @param {Date} [now]
27
+ * @returns {string}
28
+ */
29
+ export function nextOptOutId(manifest, now = new Date()) {
30
+ const yyyy = now.getUTCFullYear().toString().padStart(4, '0');
31
+ const mm = (now.getUTCMonth() + 1).toString().padStart(2, '0');
32
+ const dd = now.getUTCDate().toString().padStart(2, '0');
33
+ const prefix = `boo-${yyyy}-${mm}-${dd}-`;
34
+ const existing = Array.isArray(manifest?.baselineAcOptOuts) ? manifest.baselineAcOptOuts : [];
35
+ let maxN = 0;
36
+ for (const rec of existing) {
37
+ if (typeof rec?.id !== 'string') continue;
38
+ const m = rec.id.match(OPT_OUT_ID_RE);
39
+ if (!m) continue;
40
+ if (!rec.id.startsWith(prefix)) continue;
41
+ const n = Number.parseInt(m[1], 10);
42
+ if (Number.isFinite(n) && n > maxN) maxN = n;
43
+ }
44
+ return `${prefix}${(maxN + 1).toString().padStart(3, '0')}`;
45
+ }
46
+
47
+ /**
48
+ * Compose a C+D-native opt-out record (no preflight linkage).
49
+ *
50
+ * @param {object} args
51
+ * @param {object|null} args.manifest
52
+ * @param {string} args.reqId
53
+ * @param {string} args.baselineKey
54
+ * @param {string} args.reason minLength 20 (schema-enforced)
55
+ * @param {'req'|'project'} [args.scope] defaults to req
56
+ * @param {Date} [args.now]
57
+ * @returns {object}
58
+ */
59
+ export function composeOptOutRecord({ manifest, reqId, baselineKey, reason, scope = 'req', now = new Date() }) {
60
+ const isoNow = now.toISOString();
61
+ const id = nextOptOutId(manifest, now);
62
+ const record = {
63
+ id,
64
+ createdAt: isoNow,
65
+ baselineKey,
66
+ scope,
67
+ reason,
68
+ operatorAckAt: isoNow,
69
+ };
70
+ if (scope === 'req') record.reqId = reqId;
71
+ return record;
72
+ }
73
+
74
+ /**
75
+ * Write a new opt-out record atomically.
76
+ *
77
+ * @param {object} args
78
+ * @param {string} args.projectRoot
79
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
80
+ * @param {string} args.reqId
81
+ * @param {string} args.baselineKey
82
+ * @param {string} args.reason
83
+ * @param {'req'|'project'} [args.scope]
84
+ * @param {Date} [args.now]
85
+ * @returns {Promise<{ id: string, record: object } | import('#core/errors').RcfError>}
86
+ */
87
+ export async function writeOptOut({ projectRoot, tree, reqId, baselineKey, reason, scope = 'req', now = new Date() }) {
88
+ const manifest = tree?.manifest ?? {};
89
+ const record = composeOptOutRecord({ manifest, reqId, baselineKey, reason, scope, now });
90
+ const nextManifest = {
91
+ ...manifest,
92
+ baselineAcOptOuts: [
93
+ ...(Array.isArray(manifest.baselineAcOptOuts) ? manifest.baselineAcOptOuts : []),
94
+ record,
95
+ ],
96
+ };
97
+ const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: 'rcf/manifest.json' });
98
+ if (validation) return validation;
99
+ const abs = join(projectRoot, 'rcf', 'manifest.json');
100
+ try {
101
+ await mkdir(dirname(abs), { recursive: true });
102
+ const tmp = `${abs}.tmp`;
103
+ await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
104
+ try {
105
+ await rename(tmp, abs);
106
+ } catch (err) {
107
+ try { await unlink(tmp); } catch { /* ignore */ }
108
+ throw err;
109
+ }
110
+ } catch (err) {
111
+ return rcfError({
112
+ kind: 'ioFailure',
113
+ message: `req-baseline opt-out: manifest write failed: ${err.message}`,
114
+ filePath: 'rcf/manifest.json',
115
+ stack: err.stack,
116
+ });
117
+ }
118
+ return { id: record.id, record };
119
+ }
120
+
121
+ /**
122
+ * Remove any existing opt-out matching the reqId/baselineKey. Deletes
123
+ * both req-scoped and project-scoped matches for the same baselineKey
124
+ * so a stale opt-out cannot linger.
125
+ *
126
+ * @param {object} args
127
+ * @param {string} args.projectRoot
128
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
129
+ * @param {string} args.reqId
130
+ * @param {string} args.baselineKey
131
+ * @returns {Promise<{ removed: number } | import('#core/errors').RcfError>}
132
+ */
133
+ export async function removeOptOut({ projectRoot, tree, reqId, baselineKey }) {
134
+ const manifest = tree?.manifest ?? {};
135
+ const existing = Array.isArray(manifest.baselineAcOptOuts) ? manifest.baselineAcOptOuts : [];
136
+ const remaining = existing.filter((r) => !(r?.baselineKey === baselineKey && (r?.scope === 'project' || r?.reqId === reqId)));
137
+ const removed = existing.length - remaining.length;
138
+ if (removed === 0) return { removed: 0 };
139
+ const nextManifest = { ...manifest, baselineAcOptOuts: remaining };
140
+ const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: 'rcf/manifest.json' });
141
+ if (validation) return validation;
142
+ const abs = join(projectRoot, 'rcf', 'manifest.json');
143
+ try {
144
+ const tmp = `${abs}.tmp`;
145
+ await writeFile(tmp, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
146
+ try {
147
+ await rename(tmp, abs);
148
+ } catch (err) {
149
+ try { await unlink(tmp); } catch { /* ignore */ }
150
+ throw err;
151
+ }
152
+ } catch (err) {
153
+ return rcfError({
154
+ kind: 'ioFailure',
155
+ message: `req-baseline opt-out --remove: manifest write failed: ${err.message}`,
156
+ filePath: 'rcf/manifest.json',
157
+ stack: err.stack,
158
+ });
159
+ }
160
+ return { removed };
161
+ }
162
+
163
+ /**
164
+ * Build a Set of baselineKeys opted out for a given REQ.
165
+ *
166
+ * @param {object|null} manifest
167
+ * @param {string} reqId
168
+ * @returns {Set<string>}
169
+ */
170
+ export function optOutMap(manifest, reqId) {
171
+ const entries = Array.isArray(manifest?.baselineAcOptOuts) ? manifest.baselineAcOptOuts : [];
172
+ const out = new Set();
173
+ for (const r of entries) {
174
+ if (!r || typeof r.baselineKey !== 'string') continue;
175
+ if (r.scope === 'project') out.add(r.baselineKey);
176
+ else if (r.scope === 'req' && r.reqId === reqId) out.add(r.baselineKey);
177
+ }
178
+ return out;
179
+ }
180
+
181
+ /**
182
+ * Convenience predicate.
183
+ *
184
+ * @param {object|null} manifest
185
+ * @param {string} reqId
186
+ * @param {string} baselineKey
187
+ * @returns {boolean}
188
+ */
189
+ export function isOptedOut(manifest, reqId, baselineKey) {
190
+ return optOutMap(manifest, reqId).has(baselineKey);
191
+ }
192
+
193
+ // Kept exported for consumers that need the RcfError predicate alongside
194
+ // the writer helpers.
195
+ void isRcfError;