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,176 @@
1
+ // Test-pointer working-tree resolution (w-2026-07-28-005).
2
+ //
3
+ // This is the test-axis twin of `cn-resolve.js`. Every Test Case declares a
4
+ // `testPointer` (`filePath::testName`). This module checks each pointer
5
+ // against the CHECKED-OUT WORKING TREE: the file must exist AND a
6
+ // declaration-anchor regex must find the named test inside it. Coverage
7
+ // (`rcf coverage`) consumes the result: a TC whose pointer does not resolve
8
+ // is never counted as covering its AC - it surfaces as `covered-unresolved`
9
+ // instead. Without this, a tree full of stub TC rows reports full coverage
10
+ // while pointing at nothing.
11
+ //
12
+ // Determinism (same posture as cn-resolve): file existence is `fs.stat`;
13
+ // test presence is a fixed set of declaration-anchor regexes per language.
14
+ // No LLM, no test execution, no parsing beyond regex. Fully reproducible
15
+ // given a working tree.
16
+ //
17
+ // Language support is an anchor TABLE, keyed by file extension: adding a
18
+ // language is one entry (id, extensions, anchorsFor). JS/TS ships now, with
19
+ // anchors derived from this repo's own corpus (1022 x `test('name', ...)`
20
+ // plus template-literal names) widened to the common `test` / `it` /
21
+ // `describe` declaration forms and their `.only` / `.skip` / `.each`
22
+ // modifier chains, across all three quote styles (', ", `).
23
+ //
24
+ // HONEST LIMITATION (inherited from cn-resolve, same register as
25
+ // docs/code-nodes.md): the anchor check proves a test DECLARATION with that
26
+ // name exists - not that the test still asserts what the AC says. A renamed
27
+ // test is caught; a gutted-but-same-named test is NOT caught (semantic drift
28
+ // is out of reach for a deterministic check). A test name built from a
29
+ // template-literal interpolation cannot be anchored statically and reports
30
+ // unresolved unless the pointer carries the literal source text.
31
+
32
+ import { readFile, stat } from 'node:fs/promises';
33
+ import { extname, join } from 'node:path';
34
+
35
+ /**
36
+ * Split a test pointer into its file part and test-name part.
37
+ * The separator is the FIRST `::`; test names may themselves contain `::`.
38
+ * @param {string} pointer e.g. "test/store/loader.test.js::loads a document"
39
+ * @returns {{ file: string, testName: string | null }}
40
+ */
41
+ export function splitTestPointer(pointer) {
42
+ const sep = pointer.indexOf('::');
43
+ if (sep < 0) return { file: pointer, testName: null };
44
+ const testName = pointer.slice(sep + 2);
45
+ return { file: pointer.slice(0, sep), testName: testName.length > 0 ? testName : null };
46
+ }
47
+
48
+ function escapeRegExp(s) {
49
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
50
+ }
51
+
52
+ // The anchor table. One entry per supported language; adding a language is
53
+ // adding an entry (nothing else changes). `anchorsFor` returns the
54
+ // deterministic declaration-anchor matchers for a test name; a test is
55
+ // considered PRESENT if any anchor matches the file text.
56
+ const ANCHOR_LANGUAGES = [
57
+ {
58
+ id: 'javascript',
59
+ extensions: ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx'],
60
+ anchorsFor(testName) {
61
+ const n = escapeRegExp(testName);
62
+ // `test('name'` / `it("name"` / `describe(`name`` plus modifier
63
+ // chains (`test.skip(`, `it.only(`, `test.each(...)(` is NOT chased -
64
+ // parameterised names are dynamic and out of deterministic reach).
65
+ const head = String.raw`\b(?:test|it|describe)(?:\.\w+)*\s*\(\s*`;
66
+ return [
67
+ new RegExp(`${head}'${n}'`),
68
+ new RegExp(`${head}"${n}"`),
69
+ new RegExp(`${head}\`${n}\``),
70
+ ];
71
+ },
72
+ },
73
+ ];
74
+
75
+ function languageForFile(file) {
76
+ const ext = extname(file).toLowerCase();
77
+ return ANCHOR_LANGUAGES.find((lang) => lang.extensions.includes(ext)) ?? null;
78
+ }
79
+
80
+ /**
81
+ * @typedef {object} TestPointerResolution
82
+ * @property {boolean} resolved
83
+ * @property {'ok'|'missing-pointer'|'malformed-pointer'|'unsupported-file-type'|'file-missing'|'test-missing'} reason
84
+ * @property {string|null} testPointer - the raw pointer (null when absent)
85
+ * @property {string} tsId
86
+ * @property {string} tcId
87
+ */
88
+
89
+ /**
90
+ * Key for the per-TC resolution map. Exported so consumers (coverage) and
91
+ * this module agree on one spelling.
92
+ * @param {string} tsId
93
+ * @param {string} tcId
94
+ * @returns {string}
95
+ */
96
+ export function testCaseKey(tsId, tcId) {
97
+ return `${tsId}::${tcId}`;
98
+ }
99
+
100
+ /**
101
+ * Resolve every Test Case's `testPointer` in the tree against the working
102
+ * tree. Returns a Map keyed by `testCaseKey(tsId, tcId)`; every TC in the
103
+ * tree gets an entry, resolved or not. A per-file read cache avoids
104
+ * re-reading a file that multiple TCs point into (same posture as
105
+ * cn-resolve's cache).
106
+ *
107
+ * @param {object} args
108
+ * @param {string} args.projectRoot - absolute path to project root
109
+ * @param {object} args.tree - walker TreeModel (carries tree.testSuites)
110
+ * @returns {Promise<Map<string, TestPointerResolution>>}
111
+ */
112
+ export async function resolveTestPointers({ projectRoot, tree }) {
113
+ /** @type {Map<string, TestPointerResolution>} */
114
+ const results = new Map();
115
+ const fileCache = new Map();
116
+
117
+ const readFileCached = async (absFile) => {
118
+ if (fileCache.has(absFile)) return fileCache.get(absFile);
119
+ let text = null;
120
+ try {
121
+ text = await readFile(absFile, 'utf8');
122
+ } catch {
123
+ text = null;
124
+ }
125
+ fileCache.set(absFile, text);
126
+ return text;
127
+ };
128
+
129
+ for (const ts of tree.testSuites ?? []) {
130
+ for (const tc of ts.testCases ?? []) {
131
+ if (!tc?.id) continue;
132
+ const key = testCaseKey(ts.id, tc.id);
133
+ const record = (resolved, reason, pointer = null) => {
134
+ results.set(key, { resolved, reason, testPointer: pointer, tsId: ts.id, tcId: tc.id });
135
+ };
136
+
137
+ const pointer = tc.testPointer;
138
+ if (typeof pointer !== 'string' || pointer.length === 0) {
139
+ record(false, 'missing-pointer');
140
+ continue;
141
+ }
142
+ const { file, testName } = splitTestPointer(pointer);
143
+ if (!testName || file.length === 0) {
144
+ record(false, 'malformed-pointer', pointer);
145
+ continue;
146
+ }
147
+ const language = languageForFile(file);
148
+ if (!language) {
149
+ record(false, 'unsupported-file-type', pointer);
150
+ continue;
151
+ }
152
+
153
+ const absFile = join(projectRoot, file);
154
+ let fileOk = false;
155
+ try {
156
+ const st = await stat(absFile);
157
+ fileOk = st.isFile();
158
+ } catch {
159
+ fileOk = false;
160
+ }
161
+ if (!fileOk) {
162
+ record(false, 'file-missing', pointer);
163
+ continue;
164
+ }
165
+
166
+ const text = await readFileCached(absFile);
167
+ const present = text != null && language.anchorsFor(testName).some((re) => re.test(text));
168
+ if (!present) {
169
+ record(false, 'test-missing', pointer);
170
+ continue;
171
+ }
172
+ record(true, 'ok', pointer);
173
+ }
174
+ }
175
+ return results;
176
+ }
@@ -0,0 +1,191 @@
1
+ // Schema validator. Registers the @stravica-ai/rcf-schemas@0.2.0 bundle once
2
+ // at start-up and exposes a single `validateDocument` entry point. Returns
3
+ // `null` on success or a structured `validation` error on failure.
4
+ //
5
+ // Validation runs on load (FBS-001 / AC-701-3): the published bundle is the
6
+ // contract, not a local copy - with ONE documented strictness overlay
7
+ // (`testPointer` required on every Test Case; see the overlay block below).
8
+ // Referential-integrity checking lives in the walker (D8); this module is
9
+ // schema-shape-only.
10
+
11
+ import Ajv2020 from 'ajv/dist/2020.js';
12
+ import addFormats from 'ajv-formats';
13
+
14
+ import commonSchema from '@stravica-ai/rcf-schemas/schemas/common.schema.json' with { type: 'json' };
15
+ import manifestSchema from '@stravica-ai/rcf-schemas/schemas/manifest.schema.json' with { type: 'json' };
16
+ import prdSchema from '@stravica-ai/rcf-schemas/schemas/prd.schema.json' with { type: 'json' };
17
+ import reqSchema from '@stravica-ai/rcf-schemas/schemas/req.schema.json' with { type: 'json' };
18
+ import userStorySchema from '@stravica-ai/rcf-schemas/schemas/user-story.schema.json' with { type: 'json' };
19
+ import tadSchema from '@stravica-ai/rcf-schemas/schemas/tad.schema.json' with { type: 'json' };
20
+ import tacSchema from '@stravica-ai/rcf-schemas/schemas/tac.schema.json' with { type: 'json' };
21
+ import adrSchema from '@stravica-ai/rcf-schemas/schemas/adr.schema.json' with { type: 'json' };
22
+ import buildSequenceSchema from '@stravica-ai/rcf-schemas/schemas/build-sequence.schema.json' with { type: 'json' };
23
+ import fbsSchema from '@stravica-ai/rcf-schemas/schemas/fbs.schema.json' with { type: 'json' };
24
+ import testSuiteSchema from '@stravica-ai/rcf-schemas/schemas/test-suite.schema.json' with { type: 'json' };
25
+ // Phase 10 (X2 CodeNode bridge): 11th document kind, delivered in
26
+ // @stravica-ai/rcf-schemas@0.3.0.
27
+ import cnSchema from '@stravica-ai/rcf-schemas/schemas/cn.schema.json' with { type: 'json' };
28
+
29
+ import { rcfError } from '../errors/index.js';
30
+
31
+ // w-2026-07-28-005: strictness overlay - `testPointer` is REQUIRED on every
32
+ // Test Case. The published bundle (0.3.1) still declares it optional; an
33
+ // optional pointer is how 76 stub TC rows can claim coverage while pointing
34
+ // at nothing, so Build Lite refuses the shape at the schema layer. This is
35
+ // the ONLY local divergence from the published bundle, it is a pure
36
+ // tightening (every document valid here is valid upstream), and it is
37
+ // registered under the bundle's own $id so every validation path (walker
38
+ // load, post-write gate, write verbs) inherits it. Durable home: making
39
+ // testPointer required in @stravica-ai/rcf-schemas itself; drop this block
40
+ // when that ships.
41
+ const testSuiteSchemaStrict = structuredClone(testSuiteSchema);
42
+ {
43
+ const testCase = testSuiteSchemaStrict.$defs.testCase;
44
+ testCase.required = [...testCase.required, 'testPointer'];
45
+ testCase.properties.testPointer = {
46
+ ...testCase.properties.testPointer,
47
+ minLength: 1,
48
+ description: 'Required pointer to the executable test, format: filePath::testName.',
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @typedef {('manifest'|'prd'|'req'|'userStory'|'tad'|'tac'|'adr'|'buildSequence'|'fbs'|'testSuite'|'codeNode')} DocKind
54
+ */
55
+
56
+ const SCHEMAS = {
57
+ manifest: manifestSchema,
58
+ prd: prdSchema,
59
+ req: reqSchema,
60
+ userStory: userStorySchema,
61
+ tad: tadSchema,
62
+ tac: tacSchema,
63
+ adr: adrSchema,
64
+ buildSequence: buildSequenceSchema,
65
+ fbs: fbsSchema,
66
+ // w-2026-07-28-005 overlay: the tightened copy, not the raw import.
67
+ testSuite: testSuiteSchemaStrict,
68
+ // Phase 10: Code Node.
69
+ codeNode: cnSchema,
70
+ };
71
+
72
+ const ID_FIELD = {
73
+ manifest: null,
74
+ prd: 'prdId',
75
+ req: 'reqId',
76
+ userStory: 'usId',
77
+ tad: 'tadId',
78
+ tac: 'tacId',
79
+ adr: 'adrId',
80
+ buildSequence: 'bsId',
81
+ fbs: 'fbsId',
82
+ // Test Suite uses the plain `id` field (see @stravica-ai/rcf-schemas@0.2.0
83
+ // test-suite.schema.json). No `tsId` field.
84
+ testSuite: 'id',
85
+ // Phase 10: Code Node.
86
+ codeNode: 'cnId',
87
+ };
88
+
89
+ let cachedAjv = null;
90
+
91
+ function getAjv() {
92
+ if (cachedAjv) return cachedAjv;
93
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
94
+ addFormats(ajv);
95
+ ajv.addSchema(commonSchema);
96
+ for (const schema of Object.values(SCHEMAS)) {
97
+ ajv.addSchema(schema);
98
+ }
99
+ cachedAjv = ajv;
100
+ return ajv;
101
+ }
102
+
103
+ /**
104
+ * Returns the set of document kinds the validator knows about.
105
+ * @returns {DocKind[]}
106
+ */
107
+ export function knownKinds() {
108
+ return /** @type {DocKind[]} */ (Object.keys(SCHEMAS));
109
+ }
110
+
111
+ /**
112
+ * Returns the property name carrying the document's id, or null for manifest.
113
+ *
114
+ * @param {DocKind} kind
115
+ * @returns {string|null}
116
+ */
117
+ export function idFieldFor(kind) {
118
+ if (!(kind in ID_FIELD)) {
119
+ throw new TypeError(`Unknown kind: ${kind}`);
120
+ }
121
+ return ID_FIELD[kind];
122
+ }
123
+
124
+ /**
125
+ * Extract the document id, or null for manifest.
126
+ *
127
+ * @param {object} doc
128
+ * @param {DocKind} kind
129
+ * @returns {string|null}
130
+ */
131
+ export function documentIdOf(doc, kind) {
132
+ const field = idFieldFor(kind);
133
+ if (!field) return null;
134
+ const value = doc?.[field];
135
+ return typeof value === 'string' ? value : null;
136
+ }
137
+
138
+ /**
139
+ * Ajv's default `enum` message is "must be equal to one of the allowed
140
+ * values" and never says which. The allowed set is in `params`, which
141
+ * we were discarding - leaving the only way to find a valid status /
142
+ * priority / testLevel / category / generationStrategy be reading the
143
+ * schema source. Append it when Ajv provides it.
144
+ *
145
+ * @param {object} e - an Ajv error object
146
+ * @returns {string} suffix to append to the error message ('' when none)
147
+ */
148
+ function detailOf(e) {
149
+ const allowed = e?.params?.allowedValues;
150
+ if (!Array.isArray(allowed) || allowed.length === 0) return '';
151
+ return ` (allowed: ${allowed.join(', ')})`;
152
+ }
153
+
154
+ /**
155
+ * Validate a document against its schema.
156
+ *
157
+ * @param {object} args
158
+ * @param {object} args.doc - parsed JSON document
159
+ * @param {DocKind} args.kind
160
+ * @param {string} [args.filePath] - optional, recorded on validation errors
161
+ * @returns {import('../errors/index.js').RcfError | null}
162
+ */
163
+ export function validateDocument({ doc, kind, filePath }) {
164
+ const schema = SCHEMAS[kind];
165
+ if (!schema) {
166
+ return rcfError({
167
+ kind: 'validation',
168
+ message: `Unknown document kind: ${kind}`,
169
+ filePath,
170
+ });
171
+ }
172
+ const ajv = getAjv();
173
+ const validate = ajv.getSchema(schema.$id) ?? ajv.compile(schema);
174
+ if (validate(doc)) return null;
175
+ const ajvErrors = validate.errors ?? [];
176
+ const first = ajvErrors[0] ?? {};
177
+ const field = first.instancePath?.replace(/^\//, '').replace(/\//g, '.') || undefined;
178
+ const rule = first.keyword || undefined;
179
+ const message = ajvErrors
180
+ .map((e) => `${e.instancePath || '/'} ${e.message ?? 'invalid'}${detailOf(e)}`)
181
+ .join('; ');
182
+ const documentId = documentIdOf(doc, kind) ?? undefined;
183
+ return rcfError({
184
+ kind: 'validation',
185
+ message,
186
+ documentId,
187
+ filePath,
188
+ field,
189
+ rule,
190
+ });
191
+ }