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,345 @@
1
+ // Pure trace compute. Given a walker-produced TreeModel + a pivot id,
2
+ // walk the graph in one or both directions.
3
+ //
4
+ // Phase 5 §D8 trace-back: cross-links are NOT traversed by --back.
5
+ // From an AC, back walks the parent-child edge to US (not the
6
+ // `fbsByAcId` cross-link). This keeps --back unambiguous and
7
+ // terminating; cross-link fan-out is what `impact` is for.
8
+ //
9
+ // Phase 5 §D9 --both output shape: {pivot, ancestors, descendants} -
10
+ // two arrays around a single pivot id; not one merged graph. The
11
+ // pivot appears in neither array (it's the anchor).
12
+ //
13
+ // Phase 5 §4.2 test surface documents the expected node/edge shape:
14
+ // - Forward from PRD returns full descendant tree down to TCs.
15
+ // - Forward from AC follows all cross-links (tsByAcId, tcsByAcId,
16
+ // fbsByAcId) - but a delivering FBS is a cross-link LEAF: trace
17
+ // never traverses through the FBS layer (AC-402-3). The FBS
18
+ // dependency fan-out is impact's business (expandFbsDependents).
19
+ // - Back from TC walks TC -> TS -> AC -> US -> REQ -> PRD (AC noted
20
+ // via testCases[].acId; back-walk continues through parentByChild).
21
+
22
+ /**
23
+ * @typedef {import('#core/store/walker.js').TreeModel} TreeModel
24
+ */
25
+
26
+ /**
27
+ * @typedef {'forward' | 'back' | 'both'} TraceDirection
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} TraceNode
32
+ * @property {string} id
33
+ * @property {string} kind
34
+ * @property {number} depth - 0 for pivot; positive for descendants; negative for ancestors
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} TraceEdge
39
+ * @property {string} from
40
+ * @property {string} to
41
+ * @property {'parentChild' | 'crossLink'} kind
42
+ */
43
+
44
+ /**
45
+ * @typedef {object} TraceResult
46
+ * @property {string} pivot - the id queried
47
+ * @property {TraceDirection} direction
48
+ * @property {boolean} found - false when the pivot id is unknown
49
+ * @property {TraceNode[]} [nodes] - direction forward | back
50
+ * @property {TraceEdge[]} [edges] - direction forward | back
51
+ * @property {TraceNode[]} [ancestors] - direction both
52
+ * @property {TraceNode[]} [descendants] - direction both
53
+ */
54
+
55
+ const INLINE_AC_RE = /^AC-/;
56
+ const INLINE_TC_RE = /^TC-/;
57
+
58
+ /**
59
+ * Return the effective kind of an id. `tree.kindById` covers every
60
+ * standalone doc; inline AC / TC ids are inferred from the id prefix.
61
+ *
62
+ * @param {TreeModel} tree
63
+ * @param {string} id
64
+ * @returns {string | null}
65
+ */
66
+ export function kindOf(tree, id) {
67
+ const k = tree.kindById.get(id);
68
+ if (k) return k;
69
+ if (INLINE_AC_RE.test(id)) return tree.parentByChild.has(id) ? 'ac' : null;
70
+ if (INLINE_TC_RE.test(id)) return tree.parentByChild.has(id) ? 'tc' : null;
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Compute a trace from `id` in the requested direction. Unknown pivot
76
+ * returns `{found: false}`; the handler layer converts this to exit 2.
77
+ *
78
+ * @param {TreeModel} tree
79
+ * @param {object} opts
80
+ * @param {string} opts.id
81
+ * @param {TraceDirection} [opts.direction]
82
+ * @param {boolean} [opts.includeCode] - Phase 10: extend the forward fan-out
83
+ * into Code Nodes (AC -> implementing CN -> transitive dependents). Opt-in
84
+ * so spec-only queries stay byte-identical (spec D9).
85
+ * @param {boolean} [opts.expandFbsDependents] - opt-in (impact only): expand
86
+ * an FBS node's dependency fan-out (dependentsByFbsId). Default OFF: trace
87
+ * is the intent-chain walk, and per AC-402-3 a delivering FBS appears as a
88
+ * cross-link LEAF - trace never traverses through the FBS layer. Impact is
89
+ * the blast-radius question, so `computeImpact` opts in (D7: a dependent of
90
+ * an affected FBS needs re-execution).
91
+ * @returns {TraceResult}
92
+ */
93
+ export function computeTrace(tree, {
94
+ id, direction = 'forward', includeCode = false, expandFbsDependents = false,
95
+ }) {
96
+ const kind = kindOf(tree, id);
97
+ if (!kind) {
98
+ return { pivot: id, direction, found: false };
99
+ }
100
+ if (direction === 'forward') {
101
+ const { nodes, edges } = walkForward(tree, id, kind, includeCode, expandFbsDependents);
102
+ return { pivot: id, direction, found: true, nodes, edges };
103
+ }
104
+ if (direction === 'back') {
105
+ const { nodes, edges } = walkBack(tree, id, kind);
106
+ return { pivot: id, direction, found: true, nodes, edges };
107
+ }
108
+ // both
109
+ const back = walkBack(tree, id, kind);
110
+ const fwd = walkForward(tree, id, kind, includeCode, expandFbsDependents);
111
+ // Both arrays exclude the pivot; pivot is the anchor between them
112
+ // (spec §D9: two arrays around a single pivot id).
113
+ return {
114
+ pivot: id,
115
+ direction: 'both',
116
+ found: true,
117
+ ancestors: back.nodes.filter((n) => n.id !== id),
118
+ descendants: fwd.nodes.filter((n) => n.id !== id),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Forward walk from pivot. BFS over structural children + cross-link
124
+ * children. Each visited id becomes a node; each traversal step
125
+ * becomes an edge (parentChild or crossLink).
126
+ *
127
+ * @param {TreeModel} tree
128
+ * @param {string} pivot
129
+ * @param {string} pivotKind
130
+ * @param {boolean} [includeCode] - Phase 10 opt-in for the code layer.
131
+ * @param {boolean} [expandFbsDependents] - impact-only opt-in; see
132
+ * `computeTrace`. When off, FBS nodes are leaves (AC-402-3).
133
+ * @returns {{nodes: TraceNode[], edges: TraceEdge[]}}
134
+ */
135
+ function walkForward(tree, pivot, pivotKind, includeCode = false, expandFbsDependents = false) {
136
+ /** @type {TraceNode[]} */
137
+ const nodes = [{ id: pivot, kind: pivotKind, depth: 0 }];
138
+ /** @type {TraceEdge[]} */
139
+ const edges = [];
140
+ const seen = new Set([pivot]);
141
+ const depthById = new Map([[pivot, 0]]);
142
+ /** @type {string[]} */
143
+ const queue = [pivot];
144
+
145
+ while (queue.length > 0) {
146
+ const cur = queue.shift();
147
+ if (!cur) break;
148
+ const curDepth = depthById.get(cur) ?? 0;
149
+ const children = forwardChildrenOf(tree, cur, includeCode, expandFbsDependents);
150
+ for (const child of children) {
151
+ // Always emit the edge (even if child already visited via a
152
+ // different parent) so the graph is complete; only enqueue and
153
+ // add-as-node the first time.
154
+ edges.push({ from: cur, to: child.id, kind: child.edgeKind });
155
+ if (seen.has(child.id)) continue;
156
+ seen.add(child.id);
157
+ const childKind = kindOf(tree, child.id);
158
+ if (!childKind) continue;
159
+ const nextDepth = curDepth + 1;
160
+ depthById.set(child.id, nextDepth);
161
+ nodes.push({ id: child.id, kind: childKind, depth: nextDepth });
162
+ queue.push(child.id);
163
+ }
164
+ }
165
+ return { nodes, edges };
166
+ }
167
+
168
+ /**
169
+ * Back walk from pivot. Follows `parentByChild` up to the root.
170
+ * D8: cross-links are NOT traversed. Special case for TC: also
171
+ * include the referenced AC (from tc.acId) between TS and US so the
172
+ * back-trace surfaces the AC the TC exercises (§4.2 test surface).
173
+ *
174
+ * @param {TreeModel} tree
175
+ * @param {string} pivot
176
+ * @param {string} pivotKind
177
+ * @returns {{nodes: TraceNode[], edges: TraceEdge[]}}
178
+ */
179
+ function walkBack(tree, pivot, pivotKind) {
180
+ /** @type {TraceNode[]} */
181
+ const nodes = [{ id: pivot, kind: pivotKind, depth: 0 }];
182
+ /** @type {TraceEdge[]} */
183
+ const edges = [];
184
+ const seen = new Set([pivot]);
185
+
186
+ const addStep = (fromId, toId, edgeKind, depth) => {
187
+ if (!toId || seen.has(toId)) return;
188
+ const k = kindOf(tree, toId);
189
+ if (!k) return;
190
+ seen.add(toId);
191
+ nodes.push({ id: toId, kind: k, depth });
192
+ edges.push({ from: fromId, to: toId, kind: edgeKind });
193
+ };
194
+
195
+ // TC special-case: insert AC (via testCases[].acId cross-link) between
196
+ // TS and US in the back trace. Every other segment follows parentByChild.
197
+ if (pivotKind === 'tc') {
198
+ const tsId = tree.parentByChild.get(pivot);
199
+ if (tsId) {
200
+ addStep(pivot, tsId, 'parentChild', -1);
201
+ const ts = tree.byId.get(tsId);
202
+ const tc = ts?.testCases?.find((t) => t.id === pivot);
203
+ const acId = tc?.acId;
204
+ if (acId && !seen.has(acId)) {
205
+ addStep(tsId, acId, 'crossLink', -2);
206
+ // Continue from AC upward via parentByChild.
207
+ let cur = acId;
208
+ let depth = -3;
209
+ while (cur) {
210
+ const parent = tree.parentByChild.get(cur);
211
+ if (!parent) break;
212
+ addStep(cur, parent, 'parentChild', depth);
213
+ cur = parent;
214
+ depth -= 1;
215
+ }
216
+ return { nodes, edges };
217
+ }
218
+ // Fall through to plain TS -> US -> REQ -> PRD walk.
219
+ let cur = tsId;
220
+ let depth = -2;
221
+ while (cur) {
222
+ const parent = tree.parentByChild.get(cur);
223
+ if (!parent) break;
224
+ addStep(cur, parent, 'parentChild', depth);
225
+ cur = parent;
226
+ depth -= 1;
227
+ }
228
+ return { nodes, edges };
229
+ }
230
+ }
231
+
232
+ // Phase 10 (X2 CodeNode bridge): CN special-case. A code node's parents
233
+ // are the ACs it implements (a cross-link, not a parent-child edge).
234
+ // Insert each AC below its US, then continue up via parentByChild
235
+ // (AC -> US -> REQ -> PRD). This is exactly the `rcf trace <path>`
236
+ // backward query: from a source location up to every requirement that
237
+ // location serves.
238
+ if (pivotKind === 'codeNode') {
239
+ const cn = tree.byId.get(pivot);
240
+ let depth = -1;
241
+ for (const acId of cn?.implementsAcIds ?? []) {
242
+ if (seen.has(acId)) continue;
243
+ addStep(pivot, acId, 'crossLink', depth);
244
+ let cur = acId;
245
+ let up = depth - 1;
246
+ while (cur) {
247
+ const parent = tree.parentByChild.get(cur);
248
+ if (!parent || seen.has(parent)) break;
249
+ addStep(cur, parent, 'parentChild', up);
250
+ cur = parent;
251
+ up -= 1;
252
+ }
253
+ }
254
+ return { nodes, edges };
255
+ }
256
+
257
+ let cur = pivot;
258
+ let depth = -1;
259
+ while (cur) {
260
+ const parent = tree.parentByChild.get(cur);
261
+ if (!parent) break;
262
+ addStep(cur, parent, 'parentChild', depth);
263
+ cur = parent;
264
+ depth -= 1;
265
+ }
266
+ return { nodes, edges };
267
+ }
268
+
269
+ /**
270
+ * Enumerate forward children of `id`. Combines:
271
+ * - parent-child edges via `childrenByParent`
272
+ * - inline AC ids for US pivots
273
+ * - inline TC ids for TS pivots
274
+ * - cross-links: tsByAcId + fbsByAcId + tcsByAcId for AC pivots
275
+ * - cross-links: usByTacId + FBS-context-refs for TAC pivots
276
+ * - cross-links: FBS-context-refs for ADR pivots
277
+ * - cross-links: dependentsByFbsId for FBS pivots
278
+ *
279
+ * @param {TreeModel} tree
280
+ * @param {string} id
281
+ * @param {boolean} [includeCode] - Phase 10 opt-in for the code layer.
282
+ * @param {boolean} [expandFbsDependents] - impact-only opt-in; see
283
+ * `computeTrace`. When off (trace), an FBS enumerates no children:
284
+ * it is a cross-link leaf per AC-402-3.
285
+ * @returns {{id: string, edgeKind: 'parentChild' | 'crossLink'}[]}
286
+ */
287
+ function forwardChildrenOf(tree, id, includeCode = false, expandFbsDependents = false) {
288
+ const kind = kindOf(tree, id);
289
+ const doc = tree.byId.get(id);
290
+ /** @type {{id: string, edgeKind: 'parentChild' | 'crossLink'}[]} */
291
+ const out = [];
292
+ const seen = new Set();
293
+ const push = (childId, edgeKind) => {
294
+ if (!childId || seen.has(childId)) return;
295
+ seen.add(childId);
296
+ out.push({ id: childId, edgeKind });
297
+ };
298
+
299
+ // Parent-child edges (standalone kids).
300
+ for (const c of tree.childrenByParent.get(id) ?? []) push(c, 'parentChild');
301
+
302
+ // Inline children.
303
+ if (kind === 'userStory' && doc) {
304
+ for (const ac of doc.acceptanceCriteria ?? []) push(ac?.id, 'parentChild');
305
+ }
306
+ if (kind === 'testSuite' && doc) {
307
+ for (const tc of doc.testCases ?? []) push(tc?.id, 'parentChild');
308
+ }
309
+
310
+ // Cross-links.
311
+ if (kind === 'ac') {
312
+ for (const tsId of tree.tsByAcId.get(id) ?? []) push(tsId, 'crossLink');
313
+ for (const entry of tree.tcsByAcId.get(id) ?? []) push(entry.tcId, 'crossLink');
314
+ for (const fbsId of tree.fbsByAcId.get(id) ?? []) push(fbsId, 'crossLink');
315
+ // Phase 10 (X2 CodeNode bridge): AC -> implementing Code Nodes. This is
316
+ // the seam that carries a forward trace/impact from spec into source.
317
+ // Opt-in (includeCode) so spec-only queries stay byte-identical.
318
+ if (includeCode) {
319
+ for (const cnId of tree.cnByAcId?.get(id) ?? []) push(cnId, 'crossLink');
320
+ }
321
+ }
322
+ // Phase 10: CN -> dependent Code Nodes. Forward from a code node is the
323
+ // blast radius: everything that declares this node in its dependencies[].
324
+ if (kind === 'codeNode' && includeCode) {
325
+ for (const depId of tree.dependentsByCnId?.get(id) ?? []) push(depId, 'crossLink');
326
+ }
327
+ if (kind === 'tac') {
328
+ for (const usId of tree.usByTacId.get(id) ?? []) push(usId, 'crossLink');
329
+ for (const fbs of tree.fbsItems) {
330
+ if ((fbs.contextRequirements?.tacIds ?? []).includes(id)) push(fbs.fbsId, 'crossLink');
331
+ }
332
+ }
333
+ if (kind === 'adr') {
334
+ for (const fbs of tree.fbsItems) {
335
+ if ((fbs.contextRequirements?.adrIds ?? []).includes(id)) push(fbs.fbsId, 'crossLink');
336
+ }
337
+ }
338
+ // AC-402-3: in a trace, the FBS layer is never traversed through - a
339
+ // delivering FBS is a cross-link leaf. Impact opts in to the dependency
340
+ // fan-out (D7: dependents of an affected FBS need re-execution).
341
+ if (kind === 'fbs' && expandFbsDependents) {
342
+ for (const depId of tree.dependentsByFbsId.get(id) ?? []) push(depId, 'crossLink');
343
+ }
344
+ return out;
345
+ }
@@ -0,0 +1,87 @@
1
+ // Fixture-pack loader for the register canary (spec §7.3).
2
+ //
3
+ // The fixture pack ships in `#core/fixtures/register-canary`.
4
+ // This loader reads each file by id, verifies the shape the runner
5
+ // consumes, and returns the pack in canonical order. Operators can
6
+ // extend the pack via a local `fixtures/register-canary/*.json` in
7
+ // their own project; the loader accepts an override directory for that.
8
+
9
+ import { readFile, readdir } from 'node:fs/promises';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+
15
+ /** Canonical fixture ids that ship in the core pack (spec §7.3). */
16
+ export const DEFAULT_FIXTURE_IDS = Object.freeze([
17
+ 'canary-prompt-01',
18
+ 'canary-prompt-02',
19
+ 'canary-prompt-03',
20
+ ]);
21
+
22
+ /**
23
+ * Locate the core fixture directory. Post-0.7.1 consolidation the core src
24
+ * lives inline under `src/core/`, so the loader (which sits at
25
+ * `src/register-canary/`) can reach the fixture pack via a single relative
26
+ * walk. `RCF_CANARY_FIXTURE_DIR` still overrides for tests and operators.
27
+ *
28
+ * @returns {string}
29
+ */
30
+ export function defaultFixtureDir() {
31
+ // src/register-canary/ -> up one -> src/ -> core/fixtures/register-canary.
32
+ // Same relative shape in dev and inside the published tarball because the
33
+ // umbrella ships src/ verbatim (no build step). See package.json `files`.
34
+ const fixtureCandidate = join(here, '..', 'core', 'fixtures', 'register-canary');
35
+ return process.env.RCF_CANARY_FIXTURE_DIR ?? fixtureCandidate;
36
+ }
37
+
38
+ /**
39
+ * @typedef {object} CanaryFixture
40
+ * @property {string} id
41
+ * @property {string} operatorPrompt
42
+ * @property {Array<{ path: string, content: string }>} [supportingArtefacts]
43
+ * @property {string[]} grantedPermissions
44
+ * @property {number} wordCountBudget
45
+ * @property {string} [notes]
46
+ */
47
+
48
+ /**
49
+ * Load a fixture pack from a directory. Reads every *.json file in
50
+ * canonical order; the ids must be unique. Overrides via env
51
+ * `RCF_CANARY_FIXTURE_DIR` (used by tests).
52
+ *
53
+ * @param {string} [dir]
54
+ * @returns {Promise<CanaryFixture[]>}
55
+ */
56
+ export async function loadFixturePack(dir = defaultFixtureDir()) {
57
+ const files = (await readdir(dir))
58
+ .filter((n) => n.endsWith('.json'))
59
+ .sort();
60
+ const fixtures = [];
61
+ const ids = new Set();
62
+ for (const name of files) {
63
+ const abs = join(dir, name);
64
+ const raw = await readFile(abs, 'utf8');
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(raw);
68
+ } catch (err) {
69
+ throw new Error(`register-canary fixture ${name}: JSON parse failed: ${err.message}`);
70
+ }
71
+ if (typeof parsed?.id !== 'string' || parsed.id.length === 0) {
72
+ throw new Error(`register-canary fixture ${name}: missing id`);
73
+ }
74
+ if (ids.has(parsed.id)) {
75
+ throw new Error(`register-canary fixture ${name}: duplicate id ${parsed.id}`);
76
+ }
77
+ ids.add(parsed.id);
78
+ if (typeof parsed?.operatorPrompt !== 'string' || parsed.operatorPrompt.length === 0) {
79
+ throw new Error(`register-canary fixture ${parsed.id}: missing operatorPrompt`);
80
+ }
81
+ if (!Array.isArray(parsed?.grantedPermissions)) {
82
+ throw new Error(`register-canary fixture ${parsed.id}: grantedPermissions must be an array`);
83
+ }
84
+ fixtures.push(parsed);
85
+ }
86
+ return fixtures;
87
+ }
@@ -0,0 +1,10 @@
1
+ // Public surface for the register-canary runner (spec §7).
2
+
3
+ export { loadFixturePack, DEFAULT_FIXTURE_IDS } from './fixture-loader.js';
4
+ export {
5
+ composeCanaryRecord,
6
+ writeCanaryManifest,
7
+ readCanaryManifest,
8
+ DEFAULT_CANARY_MANIFEST_PATH,
9
+ } from './record-writer.js';
10
+ export { runCanaryAgainstFixture, runCanaryPack, MOCK_SUBAGENT } from './runner.js';
@@ -0,0 +1,132 @@
1
+ // Canary record composer + fixture-manifest writer (spec §3.5, §7.4).
2
+ //
3
+ // Records land on a fixture manifest (not a real chain manifest) at
4
+ // packages/rcf-lite/fixtures/canary-manifest.json. Consumers grep the top
5
+ // record's verdict to decide whether to gate a release; a
6
+ // ship-despite-fail record carries `shipDespiteFailReason` so the
7
+ // operator's ruling is durable and greppable.
8
+
9
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
10
+ import { dirname, join, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+
15
+ /** Location of the fixture manifest that carries the canary history. */
16
+ export const DEFAULT_CANARY_MANIFEST_PATH = resolve(here, '..', '..', 'fixtures', 'canary-manifest.json');
17
+
18
+ const ID_RE = /^rc-\d{4}-\d{2}-\d{2}-(\d{3})$/;
19
+
20
+ function nextCanaryId(records, now = new Date()) {
21
+ const yyyy = now.getUTCFullYear().toString().padStart(4, '0');
22
+ const mm = (now.getUTCMonth() + 1).toString().padStart(2, '0');
23
+ const dd = now.getUTCDate().toString().padStart(2, '0');
24
+ const prefix = `rc-${yyyy}-${mm}-${dd}-`;
25
+ let maxN = 0;
26
+ for (const r of records) {
27
+ if (typeof r?.id !== 'string' || !r.id.startsWith(prefix)) continue;
28
+ const m = r.id.match(ID_RE);
29
+ if (m) {
30
+ const n = Number.parseInt(m[1], 10);
31
+ if (Number.isFinite(n) && n > maxN) maxN = n;
32
+ }
33
+ }
34
+ return `${prefix}${(maxN + 1).toString().padStart(3, '0')}`;
35
+ }
36
+
37
+ /**
38
+ * Compose a registerCanary[] record for one fixture run.
39
+ *
40
+ * @param {object} args
41
+ * @param {object[]} args.existingRecords
42
+ * @param {string} args.buildVersion
43
+ * @param {string} args.fixturePromptId
44
+ * @param {string} args.responseBody
45
+ * @param {{ verdict: 'pass'|'fail', grades: object }} args.grade
46
+ * @param {string} [args.shipDespiteFailReason]
47
+ * @param {Date} [args.now]
48
+ * @returns {object}
49
+ */
50
+ export function composeCanaryRecord({ existingRecords, buildVersion, fixturePromptId, responseBody, grade, shipDespiteFailReason, now = new Date() }) {
51
+ const isoNow = now.toISOString();
52
+ const record = {
53
+ id: nextCanaryId(existingRecords, now),
54
+ createdAt: isoNow,
55
+ buildVersion,
56
+ fixturePromptId,
57
+ responseWordCount: countWords(responseBody),
58
+ grades: grade.grades,
59
+ verdict: grade.verdict,
60
+ };
61
+ if (typeof shipDespiteFailReason === 'string' && shipDespiteFailReason.length > 0) {
62
+ record.shipDespiteFailReason = shipDespiteFailReason;
63
+ }
64
+ return record;
65
+ }
66
+
67
+ function countWords(text) {
68
+ const stripped = String(text ?? '')
69
+ .replace(/```[\s\S]*?```/g, ' ')
70
+ .replace(/`[^`\n]*`/g, ' ')
71
+ .trim();
72
+ if (stripped.length === 0) return 0;
73
+ return stripped.split(/\s+/).length;
74
+ }
75
+
76
+ /**
77
+ * Read the fixture manifest (or return an empty seed when missing).
78
+ *
79
+ * @param {string} [path]
80
+ * @returns {Promise<{ registerCanary: object[] }>}
81
+ */
82
+ export async function readCanaryManifest(path = DEFAULT_CANARY_MANIFEST_PATH) {
83
+ try {
84
+ const raw = await readFile(path, 'utf8');
85
+ const parsed = JSON.parse(raw);
86
+ if (!Array.isArray(parsed?.registerCanary)) {
87
+ return { ...parsed, registerCanary: [] };
88
+ }
89
+ return parsed;
90
+ } catch (err) {
91
+ if (err.code === 'ENOENT') return { registerCanary: [] };
92
+ throw err;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Write the fixture manifest atomically. `records` is the full array
98
+ * (append-only usage: read, append the new record, write the result).
99
+ *
100
+ * @param {object} args
101
+ * @param {object[]} args.records
102
+ * @param {string} [args.path]
103
+ * @returns {Promise<void>}
104
+ */
105
+ export async function writeCanaryManifest({ records, path = DEFAULT_CANARY_MANIFEST_PATH }) {
106
+ const abs = path;
107
+ await mkdir(dirname(abs), { recursive: true });
108
+ const tmp = `${abs}.tmp`;
109
+ const body = { registerCanary: records };
110
+ await writeFile(tmp, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
111
+ try {
112
+ await rename(tmp, abs);
113
+ } catch (err) {
114
+ try { await unlink(tmp); } catch { /* ignore */ }
115
+ throw err;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Convenience: append one record to the fixture manifest and write back.
121
+ *
122
+ * @param {object} args
123
+ * @param {object} args.record
124
+ * @param {string} [args.path]
125
+ * @returns {Promise<{ records: object[] }>}
126
+ */
127
+ export async function appendCanaryRecord({ record, path = DEFAULT_CANARY_MANIFEST_PATH }) {
128
+ const manifest = await readCanaryManifest(path);
129
+ const records = [...(manifest.registerCanary ?? []), record];
130
+ await writeCanaryManifest({ records, path });
131
+ return { records };
132
+ }