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,285 @@
1
+ // Build queue semantics (Phase 6 §D2). Pure projection of the FBS
2
+ // documents into a deterministic queue: per-item derived state
3
+ // (actionable / blocked / inProgress / complete / verified), cycle
4
+ // detection over the dependency graph, aggregate totals and next-item
5
+ // selection. No I/O, no wall clock, no mutation of the tree.
6
+ //
7
+ // An FBS item is ACTIONABLE when its executionStatus is `notStarted`
8
+ // AND every id in dependsOnFbsIds resolves to an FBS whose status is
9
+ // `complete` or `verified`. `notStarted` with an unsatisfied dependency
10
+ // is BLOCKED. `inProgress` is neither: it is in flight and selection
11
+ // skips it (the envelope surfaces it so a harness can notice an
12
+ // abandoned claim). Dependency cycles make every member permanently
13
+ // blocked; the queue labels them `blocked (cycle)` rather than crashing
14
+ // (`rcf validate` remains the integrity surface).
15
+ //
16
+ // Each item also carries a TIER: the length of its longest dependency
17
+ // chain (tier 0 = no dependencies). Items sharing a tier have no
18
+ // dependency path between them, so a tier is a parallel-safe group -
19
+ // its members can be built concurrently (AC-502-2). This is a port of
20
+ // the full RCF platform's `computeTiers` (rcf-common build-graph),
21
+ // adapted to the queue's data model and cycle posture: a cycle member,
22
+ // or any item whose dependency chain runs into a cycle, has no defined
23
+ // chain length, so its tier is null and it joins no group (it is
24
+ // already reported `blocked (cycle)` / blocked above).
25
+
26
+ /** Lifecycle order (common.schema.json executionStatus enum). */
27
+ export const LIFECYCLE = ['notStarted', 'inProgress', 'complete', 'verified'];
28
+
29
+ const SATISFIED = new Set(['complete', 'verified']);
30
+
31
+ /**
32
+ * @typedef {object} QueueItem
33
+ * @property {string} fbsId
34
+ * @property {number} buildOrder
35
+ * @property {string} title
36
+ * @property {string} executionStatus
37
+ * @property {string[]} dependsOnFbsIds
38
+ * @property {number|null} tier - parallel-safe group (longest dependency
39
+ * chain length; null when the chain runs into a cycle)
40
+ * @property {('actionable'|'blocked'|'inProgress'|'complete'|'verified')} state
41
+ * @property {string[]} blockedBy - dependency ids not yet complete/verified
42
+ * @property {boolean} [cycle] - present (true) on blocked cycle members
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} TierGroup
47
+ * @property {number} tier - 0-based tier number (0 = no dependencies)
48
+ * @property {string[]} fbsIds - members, in queue order; no dependency
49
+ * path exists between any two of them, so they are parallel-safe
50
+ */
51
+
52
+ /**
53
+ * @typedef {object} QueueResult
54
+ * @property {{bsId: string, title: string, generationStrategy: string}|null} bs
55
+ * @property {object} totals
56
+ * @property {string|null} nextActionable
57
+ * @property {TierGroup[]} tiers - parallel-safe groups, tier ascending
58
+ * @property {QueueItem[]} items
59
+ */
60
+
61
+ /**
62
+ * Sort comparator: buildOrder ascending, fbsId lexicographic tie-break
63
+ * (§D2 - the schema does not enforce buildOrder uniqueness, so the
64
+ * tie-break keeps the total order deterministic).
65
+ *
66
+ * @param {{buildOrder?: number, fbsId?: string}} a
67
+ * @param {{buildOrder?: number, fbsId?: string}} b
68
+ * @returns {number}
69
+ */
70
+ export function byBuildOrder(a, b) {
71
+ const orderA = a.buildOrder ?? 0;
72
+ const orderB = b.buildOrder ?? 0;
73
+ if (orderA !== orderB) return orderA - orderB;
74
+ return (a.fbsId ?? '').localeCompare(b.fbsId ?? '');
75
+ }
76
+
77
+ /**
78
+ * Find every FBS id that sits on a dependency cycle. Standard
79
+ * iterative DFS with a colour map over the dependsOnFbsIds edges;
80
+ * when a back edge closes a loop, every node on the current stack
81
+ * segment from the loop entry is a cycle member.
82
+ *
83
+ * @param {Map<string, object>} fbsById
84
+ * @returns {Set<string>}
85
+ */
86
+ function findCycleMembers(fbsById) {
87
+ const members = new Set();
88
+ const state = new Map(); // id -> 'visiting' | 'done'
89
+ const stack = [];
90
+
91
+ const visit = (startId) => {
92
+ // Iterative DFS frame stack: [id, depIndex].
93
+ const frames = [[startId, 0]];
94
+ while (frames.length > 0) {
95
+ const frame = frames[frames.length - 1];
96
+ const [id] = frame;
97
+ if (frame[1] === 0) {
98
+ state.set(id, 'visiting');
99
+ stack.push(id);
100
+ }
101
+ const deps = fbsById.get(id)?.dependsOnFbsIds ?? [];
102
+ if (frame[1] >= deps.length) {
103
+ state.set(id, 'done');
104
+ stack.pop();
105
+ frames.pop();
106
+ continue;
107
+ }
108
+ const depId = deps[frame[1]];
109
+ frame[1] += 1;
110
+ if (!fbsById.has(depId)) continue;
111
+ const depState = state.get(depId);
112
+ if (depState === 'visiting') {
113
+ // Back edge: every id on the stack from depId onward is a member.
114
+ const from = stack.indexOf(depId);
115
+ for (let i = from; i < stack.length; i += 1) members.add(stack[i]);
116
+ } else if (depState === undefined) {
117
+ frames.push([depId, 0]);
118
+ }
119
+ }
120
+ };
121
+
122
+ for (const id of fbsById.keys()) {
123
+ if (!state.has(id)) visit(id);
124
+ }
125
+ return members;
126
+ }
127
+
128
+ /**
129
+ * Compute the tier of every FBS id: the length of its longest
130
+ * dependency chain (tier 0 = no dependencies). Port of the platform's
131
+ * `computeTiers` memoised longest-chain DFS, iterative in the style of
132
+ * findCycleMembers. Cycle handling reuses the members findCycleMembers
133
+ * already detected rather than re-implementing detection: a member, or
134
+ * any item whose chain reaches one, gets tier null (no defined chain
135
+ * length) - the walk short-circuits there, so a cyclic graph can never
136
+ * loop it. A dependency id that resolves to no FBS contributes chain
137
+ * length 0 (same as the platform port; `rcf validate` is the
138
+ * broken-reference surface).
139
+ *
140
+ * @param {Map<string, object>} fbsById
141
+ * @param {Set<string>} cycleMembers
142
+ * @returns {Map<string, number|null>}
143
+ */
144
+ function computeTiers(fbsById, cycleMembers) {
145
+ const tiers = new Map(); // id -> number | null
146
+
147
+ const compute = (startId) => {
148
+ // Iterative DFS frame stack: [id, depIndex, maxDepTier]; maxDepTier
149
+ // null = poisoned by a cycle somewhere beneath this item.
150
+ const frames = [[startId, 0, -1]];
151
+ while (frames.length > 0) {
152
+ const frame = frames[frames.length - 1];
153
+ const [id] = frame;
154
+ if (cycleMembers.has(id)) {
155
+ tiers.set(id, null);
156
+ frames.pop();
157
+ continue;
158
+ }
159
+ const deps = fbsById.get(id)?.dependsOnFbsIds ?? [];
160
+ if (frame[1] >= deps.length) {
161
+ tiers.set(id, frame[2] === null ? null : frame[2] + 1);
162
+ frames.pop();
163
+ continue;
164
+ }
165
+ const depId = deps[frame[1]];
166
+ frame[1] += 1;
167
+ if (frame[2] === null) continue; // already poisoned; drain remaining deps
168
+ const depTier = fbsById.has(depId) ? tiers.get(depId) : 0;
169
+ if (depTier === undefined) {
170
+ // Unresolved dependency: compute it first, then revisit this edge.
171
+ frame[1] -= 1;
172
+ frames.push([depId, 0, -1]);
173
+ } else if (depTier === null) {
174
+ frame[2] = null;
175
+ } else if (depTier > frame[2]) {
176
+ frame[2] = depTier;
177
+ }
178
+ }
179
+ };
180
+
181
+ for (const id of fbsById.keys()) {
182
+ if (!tiers.has(id)) compute(id);
183
+ }
184
+ return tiers;
185
+ }
186
+
187
+ /**
188
+ * Group tiered items into parallel-safe TierGroups, tier ascending,
189
+ * members in queue order. Null-tier (cycle-tainted) items join no
190
+ * group.
191
+ *
192
+ * @param {QueueItem[]} items - already in queue order
193
+ * @returns {TierGroup[]}
194
+ */
195
+ function groupTiers(items) {
196
+ const byTier = new Map(); // tier -> fbsIds
197
+ for (const item of items) {
198
+ if (item.tier === null) continue;
199
+ if (!byTier.has(item.tier)) byTier.set(item.tier, []);
200
+ byTier.get(item.tier).push(item.fbsId);
201
+ }
202
+ return [...byTier.entries()]
203
+ .sort((a, b) => a[0] - b[0])
204
+ .map(([tier, fbsIds]) => ({ tier, fbsIds }));
205
+ }
206
+
207
+ /**
208
+ * Compute the queue over a walked tree. Deterministic: same tree,
209
+ * same result, always.
210
+ *
211
+ * @param {import('#core/store/walker.js').TreeModel} tree
212
+ * @returns {QueueResult}
213
+ */
214
+ export function computeQueue(tree) {
215
+ const fbsItems = [...(tree.fbsItems ?? [])].sort(byBuildOrder);
216
+ const fbsById = new Map(fbsItems.map((f) => [f.fbsId, f]));
217
+ const cycleMembers = findCycleMembers(fbsById);
218
+ const tierById = computeTiers(fbsById, cycleMembers);
219
+
220
+ const items = fbsItems.map((fbs) => {
221
+ const deps = fbs.dependsOnFbsIds ?? [];
222
+ const blockedBy = deps
223
+ .filter((depId) => !SATISFIED.has(fbsById.get(depId)?.executionStatus))
224
+ .map((depId) => fbsById.get(depId) ?? { fbsId: depId, buildOrder: 0 })
225
+ .sort(byBuildOrder)
226
+ .map((dep) => dep.fbsId);
227
+ let state;
228
+ if (fbs.executionStatus === 'notStarted') {
229
+ state = blockedBy.length === 0 ? 'actionable' : 'blocked';
230
+ } else {
231
+ state = fbs.executionStatus;
232
+ }
233
+ const item = {
234
+ fbsId: fbs.fbsId,
235
+ buildOrder: fbs.buildOrder,
236
+ tier: tierById.get(fbs.fbsId),
237
+ title: fbs.title,
238
+ executionStatus: fbs.executionStatus,
239
+ dependsOnFbsIds: deps,
240
+ state,
241
+ blockedBy,
242
+ };
243
+ if (state === 'blocked' && cycleMembers.has(fbs.fbsId)) item.cycle = true;
244
+ return item;
245
+ });
246
+
247
+ const totals = {
248
+ items: items.length,
249
+ notStarted: items.filter((i) => i.executionStatus === 'notStarted').length,
250
+ inProgress: items.filter((i) => i.executionStatus === 'inProgress').length,
251
+ complete: items.filter((i) => i.executionStatus === 'complete').length,
252
+ verified: items.filter((i) => i.executionStatus === 'verified').length,
253
+ actionable: items.filter((i) => i.state === 'actionable').length,
254
+ blocked: items.filter((i) => i.state === 'blocked').length,
255
+ };
256
+
257
+ const next = selectNextItem(items);
258
+ const bs = tree.bs
259
+ ? { bsId: tree.bs.bsId, title: tree.bs.title, generationStrategy: tree.bs.generationStrategy }
260
+ : null;
261
+
262
+ return { bs, totals, nextActionable: next ? next.fbsId : null, tiers: groupTiers(items), items };
263
+ }
264
+
265
+ /**
266
+ * Select the next actionable item from a computed queue: lowest
267
+ * buildOrder (fbsId tie-break) among actionable items. Items are
268
+ * already in total order, so the first actionable wins.
269
+ *
270
+ * @param {QueueItem[]} items
271
+ * @returns {QueueItem|null}
272
+ */
273
+ function selectNextItem(items) {
274
+ return items.find((i) => i.state === 'actionable') ?? null;
275
+ }
276
+
277
+ /**
278
+ * Select the next actionable FBS item from a QueueResult.
279
+ *
280
+ * @param {QueueResult} queue
281
+ * @returns {QueueItem|null}
282
+ */
283
+ export function selectNext(queue) {
284
+ return selectNextItem(queue.items);
285
+ }
@@ -0,0 +1,231 @@
1
+ // `rcf browser-verify <fbs-id>` handler
2
+ // (ui-design-gate-0.7.0-spec §8, §9).
3
+ //
4
+ // Two modes:
5
+ // --mode operatorSession record an operator ack (no captures)
6
+ // --mode agentScreenshotCritique run the injected browser driver,
7
+ // capture routes x themes, run
8
+ // invariants + auth smoke, write the
9
+ // browserVerification record.
10
+ //
11
+ // The agent driver is injectable via `deps.browserDriver`; the default
12
+ // stub emits zero captures and the record surfaces a warn on the
13
+ // `agentDriverWired` synthetic invariant, so the CLI is testable
14
+ // end-to-end without a live Playwright.
15
+ //
16
+ // --ack marks operatorAckAt on the latest record for the FBS (used to
17
+ // clear a warn verdict per §8.5).
18
+
19
+ import { parseArgs } from 'node:util';
20
+ import process from 'node:process';
21
+
22
+ import { walkTree } from '#core/store';
23
+ import { writeUnexpectedFailure, rcfError } from '#core/errors';
24
+
25
+ import { findProjectRoot } from '../view/index.js';
26
+ import {
27
+ composeOperatorSessionRecord,
28
+ runAgentScreenshotCritique,
29
+ stubBrowserDriver,
30
+ writeBrowserVerificationAck,
31
+ writeBrowserVerificationRecord,
32
+ } from '../browser-verify/index.js';
33
+
34
+ const OPTION_SPEC = {
35
+ mode: { type: 'string' },
36
+ url: { type: 'string' },
37
+ profile: { type: 'string' },
38
+ 'dry-run': { type: 'boolean' },
39
+ json: { type: 'boolean' },
40
+ quiet: { type: 'boolean' },
41
+ ack: { type: 'boolean' },
42
+ notes: { type: 'string' },
43
+ help: { type: 'boolean' },
44
+ };
45
+
46
+ export const HELP = `Usage: rcf browser-verify <fbs-id> [options]
47
+
48
+ Stage 5 browser-verification gate for a UI-bearing FBS. Writes a
49
+ browserVerification record on the manifest; the finalise gate reads it.
50
+
51
+ Options:
52
+ --mode <mode> operatorSession | agentScreenshotCritique
53
+ (default: agentScreenshotCritique)
54
+ --url <url> Runtime URL to drive against (default: taken
55
+ from --profile, else http://127.0.0.1:3000).
56
+ --profile <profile> deployed | ci | local-dev (default: local-dev)
57
+ --notes "..." Agent-critique rubric findings to record on
58
+ the browserVerification.notes field.
59
+ --dry-run Print the composed record; do not write.
60
+ --json Emit the composed record as JSON to stdout.
61
+ --quiet Suppress non-error confirmations.
62
+ --ack Mark operatorAckAt on the latest record for
63
+ this FBS (used to clear a warn verdict).
64
+ --help Print this help.
65
+
66
+ Exit codes:
67
+ 0 verdict pass
68
+ 1 IO / unexpected runtime failure
69
+ 2 usage error
70
+ 3 schema or tree validation failure
71
+ 4 verdict warn or block (Stage 5 refused; --ack clears a warn)
72
+ `;
73
+
74
+ /**
75
+ * @param {string[]} argv
76
+ * @param {object} [deps]
77
+ * @returns {Promise<number>}
78
+ */
79
+ export async function main(argv, deps = {}) {
80
+ const stdout = deps.stdout ?? process.stdout;
81
+ const stderr = deps.stderr ?? process.stderr;
82
+ const cwd = deps.cwd ?? process.cwd();
83
+
84
+ let parsed;
85
+ try {
86
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
87
+ } catch (err) {
88
+ stderr.write(`[error] usage ${err.message}\n`);
89
+ stderr.write(HELP);
90
+ return 2;
91
+ }
92
+ const flags = parsed.values;
93
+ if (flags.help) { stdout.write(HELP); return 0; }
94
+ if (parsed.positionals.length !== 1) {
95
+ stderr.write('[error] usage browser-verify: expected exactly one <fbs-id> positional\n');
96
+ return 2;
97
+ }
98
+ const fbsId = parsed.positionals[0];
99
+ if (!/^FBS-\d{3,}$/.test(fbsId)) {
100
+ stderr.write(`[error] usage browser-verify: expected an FBS id like FBS-011, got '${fbsId}'\n`);
101
+ return 2;
102
+ }
103
+ const mode = flags.mode ?? 'agentScreenshotCritique';
104
+ if (!['operatorSession', 'agentScreenshotCritique'].includes(mode)) {
105
+ stderr.write(`[error] usage browser-verify: unknown --mode '${mode}' (expected operatorSession | agentScreenshotCritique)\n`);
106
+ return 2;
107
+ }
108
+ const profile = flags.profile ?? 'local-dev';
109
+ if (!['deployed', 'ci', 'local-dev'].includes(profile)) {
110
+ stderr.write(`[error] usage browser-verify: unknown --profile '${profile}' (expected deployed | ci | local-dev)\n`);
111
+ return 2;
112
+ }
113
+ const runtimeUrl = flags.url ?? defaultUrlForProfile(profile);
114
+
115
+ const projectRoot = await findProjectRoot(cwd);
116
+ if (!projectRoot) {
117
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `rcf init` first.\n');
118
+ return 2;
119
+ }
120
+ const walkResult = await walkTree({ projectRoot });
121
+ if (walkResult.errors.length > 0) {
122
+ stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding\n`);
123
+ }
124
+ const { tree } = walkResult;
125
+ const fbs = tree.byId.get(fbsId);
126
+ if (!fbs || tree.kindById.get(fbsId) !== 'fbs') {
127
+ stderr.write(`[error] usage browser-verify: ${fbsId} not found or not an FBS\n`);
128
+ return 2;
129
+ }
130
+ if (fbs.uiBearing !== true) {
131
+ stderr.write(`[warn] browser-verify: ${fbsId} is not uiBearing; the browser gate is a no-op. Set uiBearing=true if this FBS renders UI.\n`);
132
+ }
133
+ const now = deps.now ? new Date(deps.now()) : new Date();
134
+
135
+ if (flags.ack) {
136
+ const result = await writeBrowserVerificationAck({
137
+ projectRoot, tree, fbsId, operatorAckAt: true, now,
138
+ });
139
+ if (result && 'kind' in result && 'message' in result) {
140
+ if (result.kind === 'ioFailure') { writeUnexpectedFailure(result, stderr); return 1; }
141
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
142
+ if (result.kind === 'usage') return 2;
143
+ if (result.kind === 'validation') return 3;
144
+ return 1;
145
+ }
146
+ if (!flags.quiet) stdout.write(`browser-verify --ack: recorded operatorAckAt on latest record for ${fbsId}.\n`);
147
+ return 0;
148
+ }
149
+
150
+ let record;
151
+ try {
152
+ if (mode === 'operatorSession') {
153
+ record = composeOperatorSessionRecord({
154
+ tree, fbs, runtimeUrl, runtimeProfile: profile,
155
+ declaredRoutes: [],
156
+ invariantTicks: [{ invariant: 'operatorAcknowledged', verdict: 'pass', severity: 'block' }],
157
+ now,
158
+ });
159
+ } else {
160
+ const browserDriver = deps.browserDriver ?? stubBrowserDriver;
161
+ const fetchFn = deps.fetch ?? (typeof fetch === 'function' ? fetch : null);
162
+ if (!fetchFn) {
163
+ stderr.write('[error] usage browser-verify: no fetch available in the runtime; supply deps.fetch\n');
164
+ return 2;
165
+ }
166
+ record = await runAgentScreenshotCritique({
167
+ tree, fbs, runtimeUrl, runtimeProfile: profile,
168
+ browserDriver, fetch: fetchFn,
169
+ artefactDir: `.rcf/artefacts/${fbsId.toLowerCase()}`,
170
+ critiqueNotes: flags.notes,
171
+ now,
172
+ });
173
+ }
174
+ } catch (err) {
175
+ writeUnexpectedFailure(
176
+ rcfError({ kind: 'ioFailure', message: `browser-verify: ${err.message}`, stack: err.stack }),
177
+ stderr,
178
+ );
179
+ return 1;
180
+ }
181
+
182
+ if (flags.json) stdout.write(`${JSON.stringify(record, null, 2)}\n`);
183
+
184
+ if (flags['dry-run']) {
185
+ if (!flags.quiet) {
186
+ renderSummary({ stdout, record });
187
+ }
188
+ return record.verdict === 'pass' ? 0 : 4;
189
+ }
190
+
191
+ const write = await writeBrowserVerificationRecord({ projectRoot, tree, record });
192
+ if (write && 'kind' in write && 'message' in write) {
193
+ if (write.kind === 'ioFailure') { writeUnexpectedFailure(write, stderr); return 1; }
194
+ stderr.write(`[error] ${write.kind} ${write.message}\n`);
195
+ if (write.kind === 'usage') return 2;
196
+ if (write.kind === 'validation') return 3;
197
+ return 1;
198
+ }
199
+ if (!flags.quiet) {
200
+ renderSummary({ stdout, record });
201
+ stdout.write(`browser-verify ${fbsId}: wrote ${record.id} to rcf/manifest.json (verdict=${record.verdict}).\n`);
202
+ }
203
+ return record.verdict === 'pass' ? 0 : 4;
204
+ }
205
+
206
+ function defaultUrlForProfile(profile) {
207
+ if (profile === 'ci') return 'http://127.0.0.1:3000';
208
+ if (profile === 'deployed') return '';
209
+ return 'http://127.0.0.1:3000';
210
+ }
211
+
212
+ function renderSummary({ stdout, record }) {
213
+ stdout.write(`\nbrowser-verify ${record.fbsId} (${record.mode}, ${record.runtimeProfile})\n`);
214
+ if (typeof record.notes === 'string' && record.notes.length > 0) {
215
+ stdout.write(' agent critique:\n');
216
+ for (const line of record.notes.split('\n')) stdout.write(` ${line}\n`);
217
+ }
218
+ stdout.write(' invariant checks:\n');
219
+ for (const c of record.invariantChecks) {
220
+ const detail = c.detail ? ` - ${c.detail}` : '';
221
+ stdout.write(` [${c.verdict}] ${c.invariant}${detail}\n`);
222
+ }
223
+ if (Array.isArray(record.authSmokeChecks) && record.authSmokeChecks.length > 0) {
224
+ stdout.write(' auth smoke checks:\n');
225
+ for (const c of record.authSmokeChecks) {
226
+ const detail = c.detail ? ` - ${c.detail}` : '';
227
+ stdout.write(` [${c.verdict}] ${c.check} (status=${c.status ?? 'n/a'})${detail}\n`);
228
+ }
229
+ }
230
+ stdout.write(` verdict: ${record.verdict}\n\n`);
231
+ }