rcf-lite 0.0.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/CHANGELOG.md +344 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +71 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/admissibility/enforce.js +142 -0
  177. package/src/admissibility/index.js +8 -0
  178. package/src/admissibility/markers.js +104 -0
  179. package/src/admissibility/scope-lint.js +163 -0
  180. package/src/browser-verify/auth-smoke.js +109 -0
  181. package/src/browser-verify/index.js +29 -0
  182. package/src/browser-verify/invariants.js +336 -0
  183. package/src/browser-verify/manifest-writer.js +189 -0
  184. package/src/browser-verify/runner.js +171 -0
  185. package/src/build/bundle.js +198 -0
  186. package/src/build/formatters/json.js +18 -0
  187. package/src/build/formatters/markdown.js +406 -0
  188. package/src/build/index.js +14 -0
  189. package/src/build/mark.js +177 -0
  190. package/src/build/queue.js +285 -0
  191. package/src/cli/browser-verify.js +231 -0
  192. package/src/cli/build.js +584 -0
  193. package/src/cli/coverage.js +219 -0
  194. package/src/cli/create.js +358 -0
  195. package/src/cli/delete.js +127 -0
  196. package/src/cli/design.js +353 -0
  197. package/src/cli/doctor.js +548 -0
  198. package/src/cli/fbs.js +171 -0
  199. package/src/cli/finalise.js +377 -0
  200. package/src/cli/guidance.js +206 -0
  201. package/src/cli/help.js +156 -0
  202. package/src/cli/impact.js +119 -0
  203. package/src/cli/init.js +282 -0
  204. package/src/cli/intake.js +153 -0
  205. package/src/cli/link.js +128 -0
  206. package/src/cli/mcp.js +160 -0
  207. package/src/cli/preflight.js +220 -0
  208. package/src/cli/read.js +168 -0
  209. package/src/cli/req-baseline.js +269 -0
  210. package/src/cli/req-classify.js +135 -0
  211. package/src/cli/review.js +295 -0
  212. package/src/cli/test-suite.js +221 -0
  213. package/src/cli/trace.js +172 -0
  214. package/src/cli/ui-baseline.js +292 -0
  215. package/src/cli/ui-classify.js +108 -0
  216. package/src/cli/update.js +197 -0
  217. package/src/cli/validate.js +168 -0
  218. package/src/cli/view.js +452 -0
  219. package/src/core/baseline-catalog/data/auth.json +42 -0
  220. package/src/core/baseline-catalog/data/http-api.json +42 -0
  221. package/src/core/baseline-catalog/data/notifications.json +33 -0
  222. package/src/core/baseline-catalog/data/persistence.json +33 -0
  223. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  224. package/src/core/baseline-catalog/index.js +121 -0
  225. package/src/core/errors/index.js +167 -0
  226. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  227. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  228. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  229. package/src/core/isolation/index.js +60 -0
  230. package/src/core/mcp/framing.js +103 -0
  231. package/src/core/mcp/index.js +8 -0
  232. package/src/core/mcp/server.js +228 -0
  233. package/src/core/patterns/register-canary.js +209 -0
  234. package/src/core/patterns/req-shapes.js +158 -0
  235. package/src/core/patterns/services.js +358 -0
  236. package/src/core/patterns/ui-shapes.js +166 -0
  237. package/src/core/store/cn-resolve.js +134 -0
  238. package/src/core/store/derive-deps.js +93 -0
  239. package/src/core/store/ids.js +78 -0
  240. package/src/core/store/index.js +20 -0
  241. package/src/core/store/init.js +255 -0
  242. package/src/core/store/loader.js +214 -0
  243. package/src/core/store/tp-resolve.js +176 -0
  244. package/src/core/store/validator.js +191 -0
  245. package/src/core/store/walker.js +944 -0
  246. package/src/core/store/writer.js +1879 -0
  247. package/src/design/index.js +11 -0
  248. package/src/design/writer.js +271 -0
  249. package/src/finalise/detect.js +151 -0
  250. package/src/finalise/index.js +31 -0
  251. package/src/finalise/ingest.js +160 -0
  252. package/src/finalise/install.js +119 -0
  253. package/src/finalise/ship-without-verified.js +131 -0
  254. package/src/finalise/spawn.js +84 -0
  255. package/src/intake/fidelity.js +105 -0
  256. package/src/intake/index.js +6 -0
  257. package/src/intake/manifest-writer.js +100 -0
  258. package/src/intake/orchestrator.js +138 -0
  259. package/src/intake/validate.js +80 -0
  260. package/src/mcp/map-errors.js +131 -0
  261. package/src/mcp/prompts.js +49 -0
  262. package/src/mcp/resources.js +244 -0
  263. package/src/mcp/tools.js +1212 -0
  264. package/src/preflight/design-shapes.js +185 -0
  265. package/src/preflight/index.js +90 -0
  266. package/src/preflight/manifest-writer.js +264 -0
  267. package/src/preflight/scanner.js +206 -0
  268. package/src/preflight/secrets.js +134 -0
  269. package/src/preflight/session.js +246 -0
  270. package/src/query/attestation.js +285 -0
  271. package/src/query/coverage.js +308 -0
  272. package/src/query/formatters/json.js +21 -0
  273. package/src/query/formatters/mermaid.js +209 -0
  274. package/src/query/formatters/table.js +203 -0
  275. package/src/query/impact.js +173 -0
  276. package/src/query/index.js +13 -0
  277. package/src/query/refuse-on-admissibility.js +73 -0
  278. package/src/query/trace.js +345 -0
  279. package/src/register-canary/fixture-loader.js +87 -0
  280. package/src/register-canary/index.js +10 -0
  281. package/src/register-canary/record-writer.js +132 -0
  282. package/src/register-canary/runner.js +156 -0
  283. package/src/req-baseline/gate.js +86 -0
  284. package/src/req-baseline/index.js +27 -0
  285. package/src/req-baseline/open-candidates.js +143 -0
  286. package/src/req-baseline/opt-out.js +195 -0
  287. package/src/req-baseline/sweep.js +230 -0
  288. package/src/req-detection/classifier.js +181 -0
  289. package/src/req-detection/index.js +9 -0
  290. package/src/req-detection/persist.js +55 -0
  291. package/src/review/index.js +325 -0
  292. package/src/review/mutation.js +117 -0
  293. package/src/review/ui-baseline-drift.js +138 -0
  294. package/src/ruleset/index.js +140 -0
  295. package/src/ruleset/ruleset.json +146 -0
  296. package/src/server/index.js +178 -0
  297. package/src/server/routes.js +110 -0
  298. package/src/server/sse.js +118 -0
  299. package/src/setup/agent-setup.js +362 -0
  300. package/src/setup/identity-seed.js +104 -0
  301. package/src/setup/knowledge-seed.js +123 -0
  302. package/src/setup/managed-block.js +193 -0
  303. package/src/setup/managed-gitignore.js +166 -0
  304. package/src/setup/managed-markers.js +49 -0
  305. package/src/ui-baseline/defaults.js +119 -0
  306. package/src/ui-baseline/index.js +25 -0
  307. package/src/ui-baseline/manifest-writer.js +282 -0
  308. package/src/ui-baseline/session.js +178 -0
  309. package/src/ui-detection/classifier.js +192 -0
  310. package/src/verify/chain/index.js +221 -0
  311. package/src/verify/cli/cleanup.js +61 -0
  312. package/src/verify/cli/help.js +56 -0
  313. package/src/verify/cli/mcp.js +98 -0
  314. package/src/verify/cli/provision.js +71 -0
  315. package/src/verify/cli/report.js +71 -0
  316. package/src/verify/cli/run.js +155 -0
  317. package/src/verify/engine/brief.js +87 -0
  318. package/src/verify/engine/index.js +177 -0
  319. package/src/verify/engine/launcher.js +307 -0
  320. package/src/verify/mcp/tools.js +107 -0
  321. package/src/verify/profile/index.js +146 -0
  322. package/src/verify/provision/index.js +256 -0
  323. package/src/verify/report/index.js +139 -0
  324. package/src/verify/report/renderer.js +118 -0
  325. package/src/verify/verdict/index.js +313 -0
  326. package/src/view/doc-renderers/adr.js +44 -0
  327. package/src/view/doc-renderers/build-sequence.js +40 -0
  328. package/src/view/doc-renderers/fbs.js +128 -0
  329. package/src/view/doc-renderers/helpers.js +159 -0
  330. package/src/view/doc-renderers/index.js +12 -0
  331. package/src/view/doc-renderers/prd.js +45 -0
  332. package/src/view/doc-renderers/req.js +43 -0
  333. package/src/view/doc-renderers/tac.js +38 -0
  334. package/src/view/doc-renderers/tad.js +74 -0
  335. package/src/view/doc-renderers/test-suite.js +45 -0
  336. package/src/view/doc-renderers/user-story.js +63 -0
  337. package/src/view/html-page.js +462 -0
  338. package/src/view/index.js +63 -0
  339. package/src/view/live-client.js +338 -0
  340. package/src/view/mermaid-diagram.js +178 -0
  341. package/src/view/style.css +735 -0
  342. package/src/view/tree-model.js +152 -0
  343. package/src/view/vendored/mermaid.min.js +2607 -0
  344. package/src/view-supervisor/index.js +26 -0
  345. package/src/view-supervisor/logs.js +32 -0
  346. package/src/view-supervisor/manifest-writer.js +178 -0
  347. package/src/view-supervisor/persist-until.js +85 -0
  348. package/src/view-supervisor/supervisor.js +276 -0
  349. package/src/watch/index.js +152 -0
@@ -0,0 +1,230 @@
1
+ // Baseline-AC sweep planner + applier (spec §5.3, §5.4).
2
+ //
3
+ // The "open sweep candidate" property is DERIVED from the tree, not
4
+ // persisted: a candidate is open exactly when the parent REQ carries a
5
+ // shape, the baselineKey is not present as an AC on the US, and no
6
+ // baselineAcOptOuts record covers it. See open-candidates.js. This module
7
+ // walks the tree via that derivation, presents the candidates for
8
+ // operator resolution, and materialises the two durable outcomes:
9
+ // accepted keys become ACs (authoredBy: baseline), opted-out keys become
10
+ // baselineAcOptOuts entries. Skipping (or being silent) simply leaves
11
+ // the derived-open state as-is; the Stage-1 gate (gate.js) refuses build
12
+ // on any FBS binding an AC on a US that still derives an open key.
13
+ //
14
+ // Moment-4 non-interactive create (spec §5.3 moment 4): a US created
15
+ // under a classified REQ does not need any explicit auto-enqueue write
16
+ // because the derivation catches unresolved keys directly. The Stage-1
17
+ // gate remains the single unbypassable refusal surface.
18
+
19
+ import { updateDocument } from '#core/store';
20
+ import {
21
+ BASELINE_SHAPE_KEYS,
22
+ getBaselineSet,
23
+ } from '#core/baseline-catalog';
24
+
25
+ import { writeOptOut } from './opt-out.js';
26
+ import { openCandidatesForUs } from './open-candidates.js';
27
+
28
+ /**
29
+ * Plan the baseline sweep across the tree.
30
+ *
31
+ * @param {import('#core/store/walker.js').TreeModel} tree
32
+ * @param {object} opts
33
+ * @param {string|null} [opts.reqId]
34
+ * @param {boolean} [opts.all]
35
+ * @returns {{ candidates: Array<{ usId: string, reqId: string, baselineKey: string, reqShape: string, canonicalText: string }> }}
36
+ */
37
+ export function planSweep(tree, opts = {}) {
38
+ const { reqId = null, all = false } = opts;
39
+
40
+ /** @type {Array<{ usId: string, reqId: string, baselineKey: string, reqShape: string, canonicalText: string }>} */
41
+ const candidates = [];
42
+
43
+ for (const [id, kind] of tree.kindById) {
44
+ if (kind !== 'userStory') continue;
45
+ const us = tree.byId.get(id);
46
+ if (!us) continue;
47
+ if (!all && reqId && us.reqId !== reqId) continue;
48
+ if (!all && !reqId) continue;
49
+ for (const c of openCandidatesForUs(tree, us)) {
50
+ candidates.push(c);
51
+ }
52
+ }
53
+ return { candidates };
54
+ }
55
+
56
+ /**
57
+ * Apply a batch of decisions.
58
+ *
59
+ * @param {object} args
60
+ * @param {string} args.projectRoot
61
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
62
+ * @param {Array<{ candidate: object, action: 'accept'|'opt-out'|'skip', reason?: string }>} args.decisions
63
+ * @param {Date} [args.now]
64
+ * @returns {Promise<{ accepted: number, optedOut: number, left: number, optOutIds: string[], writtenAcIds: string[] } | import('#core/errors').RcfError>}
65
+ */
66
+ export async function applySweepDecisions({ projectRoot, tree, decisions, now = new Date() }) {
67
+ let accepted = 0;
68
+ let optedOut = 0;
69
+ let left = 0;
70
+ /** @type {string[]} */
71
+ const optOutIds = [];
72
+ /** @type {string[]} */
73
+ const writtenAcIds = [];
74
+
75
+ // Group by US so each US gets one write.
76
+ const byUs = new Map();
77
+ for (const d of decisions) {
78
+ const usId = d.candidate.usId;
79
+ if (!byUs.has(usId)) byUs.set(usId, []);
80
+ byUs.get(usId).push(d);
81
+ }
82
+
83
+ /** @type {Array<{ reqId: string, baselineKey: string, reason: string }>} */
84
+ const pendingOptOuts = [];
85
+
86
+ for (const [usId, list] of byUs) {
87
+ const usDoc = tree.byId.get(usId);
88
+ if (!usDoc) continue;
89
+
90
+ const acsToAppend = [];
91
+ for (const d of list) {
92
+ if (d.action === 'accept') {
93
+ const ac = composeBaselineAc({ usDoc, candidate: d.candidate, now });
94
+ acsToAppend.push(ac);
95
+ writtenAcIds.push(ac.id);
96
+ accepted += 1;
97
+ } else if (d.action === 'opt-out') {
98
+ pendingOptOuts.push({ reqId: d.candidate.reqId, baselineKey: d.candidate.baselineKey, reason: d.reason ?? '' });
99
+ optedOut += 1;
100
+ } else {
101
+ left += 1;
102
+ }
103
+ }
104
+
105
+ if (acsToAppend.length === 0) continue;
106
+
107
+ const nextAcs = [
108
+ ...(Array.isArray(usDoc.acceptanceCriteria) ? usDoc.acceptanceCriteria : []),
109
+ ...acsToAppend,
110
+ ];
111
+
112
+ const result = await updateDocument({
113
+ projectRoot,
114
+ tree,
115
+ id: usId,
116
+ patch: { acceptanceCriteria: nextAcs },
117
+ sets: [],
118
+ options: {},
119
+ walkErrors: [],
120
+ });
121
+ if (result && result.kind && typeof result.message === 'string') return result;
122
+ }
123
+
124
+ for (const p of pendingOptOuts) {
125
+ const out = await writeOptOut({
126
+ projectRoot,
127
+ tree,
128
+ reqId: p.reqId,
129
+ baselineKey: p.baselineKey,
130
+ reason: p.reason,
131
+ scope: 'req',
132
+ now,
133
+ });
134
+ if (out && out.kind && typeof out.message === 'string') return out;
135
+ optOutIds.push(out.id);
136
+ }
137
+
138
+ return { accepted, optedOut, left, optOutIds, writtenAcIds };
139
+ }
140
+
141
+ /**
142
+ * Moment-4 auto-enqueue is a no-op today: the openness of a candidate is
143
+ * derived by open-candidates.js from the tree state (shape-classified
144
+ * REQ, no matching AC, no matching opt-out). A fresh US under a
145
+ * classified REQ therefore surfaces every unresolved baseline key as
146
+ * OPEN automatically, without a separate queue-write. The Stage-1 gate
147
+ * (gate.js) reads the same derivation and refuses build accordingly.
148
+ *
149
+ * Kept exported so the create/us hook has a stable seam name and can
150
+ * grow a queue-write in a future schemas release when a persistent
151
+ * open-queue field lands on user-story.
152
+ *
153
+ * @param {object} args
154
+ * @param {string} args.projectRoot
155
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
156
+ * @param {string} args.usId
157
+ * @returns {Promise<{ enqueued: string[] }>}
158
+ */
159
+ export async function applyPendingBaselinesForUs({ tree, usId }) {
160
+ const usDoc = tree.byId.get(usId);
161
+ if (!usDoc) return { enqueued: [] };
162
+ const open = openCandidatesForUs(tree, usDoc);
163
+ return { enqueued: open.map((c) => c.baselineKey) };
164
+ }
165
+
166
+ /**
167
+ * Compose an AC to inject for a baseline candidate.
168
+ *
169
+ * @param {object} args
170
+ * @param {object} args.usDoc
171
+ * @param {{ baselineKey: string, reqShape: string, canonicalText: string, given?: string, when?: string, then?: string }} args.candidate
172
+ * @param {Date} args.now
173
+ * @returns {object}
174
+ */
175
+ export function composeBaselineAc({ usDoc, candidate, now }) {
176
+ const isoNow = now.toISOString();
177
+ const id = nextAcId(usDoc);
178
+ const canonicalText = candidate.canonicalText;
179
+ const gwt = candidate.given || candidate.when || candidate.then
180
+ ? { given: candidate.given ?? '', when: candidate.when ?? '', then: candidate.then ?? '' }
181
+ : extractGwt(canonicalText);
182
+ const ac = {
183
+ id,
184
+ description: canonicalText,
185
+ testable: true,
186
+ provenance: {
187
+ authoredBy: 'baseline',
188
+ baselineKey: candidate.baselineKey,
189
+ injectedAt: isoNow,
190
+ sourceReqShape: candidate.reqShape,
191
+ acceptedByOperatorAt: isoNow,
192
+ },
193
+ };
194
+ if (gwt.given) ac.given = gwt.given;
195
+ if (gwt.when) ac.when = gwt.when;
196
+ if (gwt.then) ac.then = gwt.then;
197
+ return ac;
198
+ }
199
+
200
+ const AC_HIER_ID_RE = /^AC-(\d{3,})-(\d+)$/;
201
+
202
+ function nextAcId(usDoc) {
203
+ const acs = Array.isArray(usDoc.acceptanceCriteria) ? usDoc.acceptanceCriteria : [];
204
+ // Prefer hierarchical form matching the US id, e.g. US-201 → AC-201-1.
205
+ const usNum = (usDoc.usId ?? '').match(/^US-(\d+)$/)?.[1];
206
+ let maxN = 0;
207
+ for (const ac of acs) {
208
+ const m = (ac?.id ?? '').match(AC_HIER_ID_RE);
209
+ if (!m) continue;
210
+ const [, hier, tail] = m;
211
+ if (usNum && hier !== usNum) continue;
212
+ const n = Number.parseInt(tail, 10);
213
+ if (Number.isFinite(n) && n > maxN) maxN = n;
214
+ }
215
+ return usNum ? `AC-${usNum}-${maxN + 1}` : `AC-${(Math.floor(Math.random() * 999)).toString().padStart(3, '0')}-1`;
216
+ }
217
+
218
+ function extractGwt(text) {
219
+ const gwt = { given: '', when: '', then: '' };
220
+ const g = text.match(/given\s+(.+?),\s*when/i);
221
+ const w = text.match(/,\s*when\s+(.+?),\s*then/i);
222
+ const t = text.match(/,\s*then\s+(.+)$/i);
223
+ if (g) gwt.given = g[1].trim();
224
+ if (w) gwt.when = w[1].trim();
225
+ if (t) gwt.then = t[1].trim().replace(/\.\s*$/, '');
226
+ return gwt;
227
+ }
228
+
229
+ // Kept-exported catalog constants for downstream discoverability.
230
+ void BASELINE_SHAPE_KEYS; void getBaselineSet;
@@ -0,0 +1,181 @@
1
+ // REQ-shape classifier (Track C+D spec §4.4).
2
+ //
3
+ // Deterministic keyword-scan over an already-loaded REQ document. Reads
4
+ // `title`, `description`, `rationale`, folds the parent PRD's `intent`
5
+ // and `problem` as fallback context, and returns a
6
+ // `shapeClassification` block per the requirement schema (§3.1). Never
7
+ // reads USes or ACs (they may not exist yet and would circularly
8
+ // re-trigger the classifier).
9
+ //
10
+ // Signals come from `matchReqShapeSignals` in core (single source of
11
+ // truth; Track B's UI patterns flow through the same file). Multi-shape
12
+ // verdicts are legitimate; the classifier records each match tagged with
13
+ // the source field it came from.
14
+ //
15
+ // Content-pending case: a REQ whose `description` is empty at write time
16
+ // records `shapes: []`, `reason: content-pending`; the classifier reruns
17
+ // on the first `rcf update req --description ...` that adds real content.
18
+
19
+ import { matchReqShapeSignals, SHAPE_KEYS } from '#core/patterns/req-shapes';
20
+
21
+ /**
22
+ * @typedef {'webUi'|'httpApi'|'auth'|'persistence'|'notifications'} ReqShape
23
+ * @typedef {'webUi'|'httpApi'|'auth'|'persistence'|'notifications'|'none'} ReqShapeWithNone
24
+ * @typedef {'keyword-scan'|'content-pending'|'inheritedFromParent'|'operatorOverride'} ClassifierReason
25
+ */
26
+
27
+ /**
28
+ * @typedef {object} ReqShapeSignal
29
+ * @property {'title'|'description'|'rationale'} source
30
+ * @property {string} match
31
+ * @property {ReqShape} shape
32
+ */
33
+
34
+ /**
35
+ * @typedef {object} ReqShapeClassification
36
+ * @property {ReqShapeWithNone[]} shapes
37
+ * @property {ClassifierReason} reason
38
+ * @property {ReqShapeSignal[]} [signals]
39
+ * @property {string} classifiedAt
40
+ * @property {object} [operatorOverride]
41
+ */
42
+
43
+ const ALLOWED_SOURCES = Object.freeze(['title', 'description', 'rationale']);
44
+
45
+ /**
46
+ * Compute the shape classification for a REQ. Pure function; does not
47
+ * write to the document. Callers merge the returned block onto the REQ
48
+ * before persisting.
49
+ *
50
+ * @param {object} reqDoc the REQ document body
51
+ * @param {object} [opts]
52
+ * @param {object} [opts.parentPrd] parent PRD body for fallback context
53
+ * @param {Date} [opts.now] clock injection for tests
54
+ * @returns {ReqShapeClassification}
55
+ */
56
+ export function classifyReq(reqDoc, opts = {}) {
57
+ const now = opts.now ?? new Date();
58
+ const isoNow = now.toISOString();
59
+
60
+ const title = typeof reqDoc?.title === 'string' ? reqDoc.title : '';
61
+ const description = typeof reqDoc?.description === 'string' ? reqDoc.description : '';
62
+ const rationale = typeof reqDoc?.rationale === 'string' ? reqDoc.rationale : '';
63
+
64
+ // Content-pending: the REQ has no real description body yet. Placeholder
65
+ // scaffolds land as "TODO", so treat descriptions that are empty or
66
+ // exactly-a-todo as pending. The classifier re-runs on the next update
67
+ // once the operator or an intake pass fills in real prose.
68
+ const contentPending = description.length === 0 || /^\s*TODO\b/i.test(description);
69
+ if (contentPending) {
70
+ return {
71
+ shapes: [],
72
+ reason: 'content-pending',
73
+ classifiedAt: isoNow,
74
+ };
75
+ }
76
+
77
+ const signals = [];
78
+
79
+ const primarySources = [
80
+ { source: 'title', text: title },
81
+ { source: 'description', text: description },
82
+ { source: 'rationale', text: rationale },
83
+ ];
84
+
85
+ for (const { source, text } of primarySources) {
86
+ if (!ALLOWED_SOURCES.includes(source) || text.length === 0) continue;
87
+ for (const m of matchReqShapeSignals(text)) {
88
+ signals.push({ source, shape: m.shape, match: m.match });
89
+ }
90
+ }
91
+
92
+ // Parent PRD fallback: only used to seed additional shapes when the REQ
93
+ // itself did not surface one. Signals are still attributed to the REQ's
94
+ // own fields per the schema enum (title/description/rationale) so the
95
+ // signals array shape stays stable; a shape inherited from the PRD lands
96
+ // in `shapes[]` with no signal row and the classifier reason stays
97
+ // keyword-scan (the signals are still what the REQ said; the parent
98
+ // widens the shape list only when the REQ's own text was silent). This
99
+ // matches spec §4.4 "fallback context" without mixing scopes.
100
+ const parentPrd = opts.parentPrd ?? null;
101
+ const parentText = parentPrd
102
+ ? [parentPrd.intent, parentPrd.problem].filter((s) => typeof s === 'string' && s.length > 0).join(' ')
103
+ : '';
104
+ const parentSignals = parentText.length > 0 ? matchReqShapeSignals(parentText) : [];
105
+
106
+ // Merge shapes: any shape the REQ's own text matched, plus any shape
107
+ // the parent PRD adds. Preserved in canonical order.
108
+ const shapesSet = new Set();
109
+ for (const s of signals) shapesSet.add(s.shape);
110
+ for (const m of parentSignals) shapesSet.add(m.shape);
111
+
112
+ const shapes = SHAPE_KEYS.filter((s) => shapesSet.has(s));
113
+ const finalShapes = shapes.length === 0 ? ['none'] : shapes;
114
+
115
+ const block = {
116
+ shapes: finalShapes,
117
+ reason: 'keyword-scan',
118
+ classifiedAt: isoNow,
119
+ };
120
+ if (signals.length > 0) block.signals = signals;
121
+ return block;
122
+ }
123
+
124
+ /**
125
+ * Merge a fresh classification onto an existing REQ document. Preserves
126
+ * a previous `operatorOverride` block when present, and preserves
127
+ * signals from earlier runs when the new run comes back
128
+ * `content-pending` (never clear real signals with a pending re-run;
129
+ * pending is a "no verdict yet" state, not a delete).
130
+ *
131
+ * @param {object} reqDoc REQ document to merge onto (not mutated)
132
+ * @param {ReqShapeClassification} block classification from `classifyReq`
133
+ * @returns {object} new REQ doc with the merged block
134
+ */
135
+ export function mergeClassificationOntoReq(reqDoc, block) {
136
+ const existing = reqDoc?.shapeClassification;
137
+ // On content-pending: only apply if there is no previous verdict.
138
+ if (block.reason === 'content-pending' && existing && existing.reason !== 'content-pending') {
139
+ return reqDoc;
140
+ }
141
+ const next = { ...reqDoc, shapeClassification: { ...block } };
142
+ if (existing?.operatorOverride) {
143
+ next.shapeClassification.operatorOverride = existing.operatorOverride;
144
+ // An operator override wins on the shapes field.
145
+ if (Array.isArray(existing.operatorOverride.newShapes) && existing.operatorOverride.newShapes.length > 0) {
146
+ next.shapeClassification.shapes = [...existing.operatorOverride.newShapes];
147
+ }
148
+ }
149
+ return next;
150
+ }
151
+
152
+ /**
153
+ * Compose an operator override block per spec §4.5. Records both the
154
+ * original classifier shapes and the newly-ruled shapes so the override
155
+ * history is visible to reviewers. Does not mutate the input; returns a
156
+ * new `shapeClassification` block ready to write.
157
+ *
158
+ * @param {object} args
159
+ * @param {ReqShapeWithNone[]} args.originalShapes
160
+ * @param {ReqShapeWithNone[]} args.newShapes
161
+ * @param {string} args.reason
162
+ * @param {ReqShapeSignal[]} [args.signals]
163
+ * @param {Date} [args.now]
164
+ * @returns {ReqShapeClassification}
165
+ */
166
+ export function composeOperatorOverride({ originalShapes, newShapes, reason, signals, now = new Date() }) {
167
+ const isoNow = now.toISOString();
168
+ const block = {
169
+ shapes: [...newShapes],
170
+ reason: 'operatorOverride',
171
+ classifiedAt: isoNow,
172
+ operatorOverride: {
173
+ originalShapes: [...originalShapes],
174
+ newShapes: [...newShapes],
175
+ reason,
176
+ ackAt: isoNow,
177
+ },
178
+ };
179
+ if (Array.isArray(signals) && signals.length > 0) block.signals = signals;
180
+ return block;
181
+ }
@@ -0,0 +1,9 @@
1
+ // Public surface for the REQ-shape classifier module (Track C+D §4).
2
+
3
+ export {
4
+ classifyReq,
5
+ mergeClassificationOntoReq,
6
+ composeOperatorOverride,
7
+ } from './classifier.js';
8
+
9
+ export { classifyAndPersistReq } from './persist.js';
@@ -0,0 +1,55 @@
1
+ // Persist a REQ-shape classification onto a REQ document (Track C+D §4.4).
2
+ //
3
+ // Reads the REQ from the in-memory tree, computes classification via
4
+ // classifyReq (folding parent PRD context when the PRD is loaded), merges
5
+ // per mergeClassificationOntoReq (which respects operator overrides and
6
+ // refuses to overwrite a real verdict with content-pending), and writes
7
+ // the merged shapeClassification block back via updateDocument.
8
+ //
9
+ // Never fails the calling verb: a classification write error is warned
10
+ // on stderr but returns success (the write we care about is the REQ's
11
+ // own body write; classification is provenance layered on top).
12
+
13
+ import { updateDocument } from '#core/store';
14
+ import { classifyReq, mergeClassificationOntoReq } from './classifier.js';
15
+
16
+ /**
17
+ * @param {object} args
18
+ * @param {string} args.projectRoot
19
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
20
+ * @param {string} args.reqId the id of the REQ to classify
21
+ * @param {Date} [args.now]
22
+ * @returns {Promise<{ ok: true, block: object, changed: boolean } | { ok: false, message: string }>}
23
+ */
24
+ export async function classifyAndPersistReq({ projectRoot, tree, reqId, now = new Date() }) {
25
+ const reqDoc = tree?.byId?.get(reqId);
26
+ if (!reqDoc || reqDoc.reqId !== reqId) {
27
+ return { ok: false, message: `req-classify: ${reqId} not found in tree` };
28
+ }
29
+ const prdId = reqDoc.prdId;
30
+ const parentPrd = prdId ? tree.byId.get(prdId) : null;
31
+
32
+ const block = classifyReq(reqDoc, { parentPrd: parentPrd ?? undefined, now });
33
+ const merged = mergeClassificationOntoReq(reqDoc, block);
34
+
35
+ // Nothing to write when the merge decided to keep the existing block.
36
+ if (merged === reqDoc) return { ok: true, block: reqDoc.shapeClassification, changed: false };
37
+
38
+ const nextBlock = merged.shapeClassification;
39
+ // Write back through the store's updateDocument path so validation
40
+ // fires and updatedAt bumps.
41
+ const result = await updateDocument({
42
+ projectRoot,
43
+ tree,
44
+ id: reqId,
45
+ patch: { shapeClassification: nextBlock },
46
+ sets: [],
47
+ options: {},
48
+ walkErrors: [],
49
+ });
50
+ if (result && result.kind && typeof result.message === 'string') {
51
+ // RcfError shape
52
+ return { ok: false, message: `req-classify: ${result.message}` };
53
+ }
54
+ return { ok: true, block: nextBlock, changed: true };
55
+ }