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,406 @@
1
+ // Markdown emitter for the build verb (Phase 6 §D4). Two shapes: the
2
+ // seven-section spec-bundle document (§D3) and the queue-overview
3
+ // table. Zero-dep string assembly, byte-stable given the same result
4
+ // (§D10): no wall clock, no randomness, fixed ordering throughout.
5
+ // British English, ASCII hyphens only (§D17).
6
+ //
7
+ // The bundle's consumer is an implementing agent-harness reading a
8
+ // spec: headings and tables carry the structure without a parser, and
9
+ // the same artefact is human-reviewable as-is (OQ-P6-5).
10
+
11
+ const SECTION_NUMBERS = {
12
+ header: 1,
13
+ queueContext: 2,
14
+ work: 3,
15
+ acceptanceCriteria: 4,
16
+ architecturalContext: 5,
17
+ testSurface: 6,
18
+ runbook: 7,
19
+ };
20
+
21
+ /**
22
+ * Format a build result as markdown.
23
+ *
24
+ * @param {object} result - QueueResult | BundleResult | next-mode empty envelope
25
+ * @param {('queue'|'bundle'|'next')} mode
26
+ * @returns {string}
27
+ */
28
+ export function formatMarkdown(result, mode) {
29
+ if (mode === 'queue') return formatQueueTable(result);
30
+ if (mode === 'next' && !result.fbs) return formatNextEmpty(result);
31
+ return formatBundle(result);
32
+ }
33
+
34
+ /**
35
+ * Queue overview as a markdown table.
36
+ *
37
+ * @param {object} queue - QueueResult
38
+ * @returns {string}
39
+ */
40
+ function formatQueueTable(queue) {
41
+ const lines = [];
42
+ const heading = queue.bs
43
+ ? `# Build queue: ${queue.bs.bsId} - ${queue.bs.title}`
44
+ : '# Build queue';
45
+ lines.push(heading, '');
46
+ if (queue.bs?.generationStrategy) {
47
+ lines.push(`Generation strategy: ${queue.bs.generationStrategy}`, '');
48
+ }
49
+ lines.push('| order | tier | id | title | status | state | blocked by |');
50
+ lines.push('|---|---|---|---|---|---|---|');
51
+ for (const item of queue.items) {
52
+ const state = item.cycle ? 'blocked (cycle)' : item.state;
53
+ const blockedBy = item.blockedBy.join(', ');
54
+ const tier = item.tier ?? '-';
55
+ lines.push(
56
+ `| ${item.buildOrder} | ${tier} | ${item.fbsId} | ${item.title} | ${item.executionStatus} | ${state} | ${blockedBy} |`,
57
+ );
58
+ }
59
+ const t = queue.totals;
60
+ lines.push('');
61
+ lines.push(
62
+ `Totals: items ${t.items} | notStarted ${t.notStarted} | inProgress ${t.inProgress} `
63
+ + `| complete ${t.complete} | verified ${t.verified} | actionable ${t.actionable} `
64
+ + `| blocked ${t.blocked}`,
65
+ );
66
+ // Parallel-safe tiers (AC-502-2): items sharing a tier have no
67
+ // dependency path between them and can be built concurrently.
68
+ // Cycle-tainted items have no tier and appear in no group.
69
+ const tiers = queue.tiers ?? [];
70
+ lines.push('');
71
+ if (tiers.length > 0) {
72
+ lines.push('Parallel-safe tiers (items in the same tier have no dependency between them and can build in parallel):');
73
+ for (const group of tiers) {
74
+ lines.push(`- tier ${group.tier}: ${group.fbsIds.join(', ')}`);
75
+ }
76
+ } else {
77
+ lines.push('Parallel-safe tiers: none.');
78
+ }
79
+ lines.push('');
80
+ lines.push(`Next actionable: ${queue.nextActionable ?? 'none'}`);
81
+ return `${lines.join('\n')}\n`;
82
+ }
83
+
84
+ /**
85
+ * `--next` with nothing actionable (§D2 / OQ-P6-2): an explicit
86
+ * envelope distinguishing "done" from "stuck", not an error.
87
+ *
88
+ * @param {object} envelope
89
+ * @returns {string}
90
+ */
91
+ function formatNextEmpty(envelope) {
92
+ const lines = ['# Build queue: nothing actionable', ''];
93
+ if (envelope.queueEmpty) {
94
+ lines.push('Queue complete: every item is complete or verified.');
95
+ } else {
96
+ const blocked = envelope.blocked.length > 0 ? envelope.blocked.join(', ') : 'none';
97
+ const inProgress = envelope.inProgress.length > 0 ? envelope.inProgress.join(', ') : 'none';
98
+ lines.push(
99
+ 'Queue not complete but nothing is actionable.',
100
+ '',
101
+ `- Blocked: ${blocked}`,
102
+ `- In progress: ${inProgress}`,
103
+ );
104
+ }
105
+ return `${lines.join('\n')}\n`;
106
+ }
107
+
108
+ /**
109
+ * The seven-section spec bundle (§D3). Section numbering is fixed;
110
+ * section 5 is omitted entirely when the FBS carries no
111
+ * contextRequirements (§D11) without renumbering the rest.
112
+ *
113
+ * @param {object} bundle - BundleResult
114
+ * @returns {string}
115
+ */
116
+ function formatBundle(bundle) {
117
+ const lines = [];
118
+ const { fbs } = bundle;
119
+ lines.push(`# Spec bundle: ${fbs.fbsId} - ${fbs.title}`, '');
120
+ pushHeader(lines, bundle);
121
+ pushQueueContext(lines, bundle);
122
+ pushWork(lines, bundle);
123
+ pushAcceptanceCriteria(lines, bundle);
124
+ if (bundle.context) pushArchitecturalContext(lines, bundle);
125
+ pushTestSurface(lines, bundle);
126
+ pushRunbook(lines, bundle);
127
+ return `${lines.join('\n')}\n`;
128
+ }
129
+
130
+ function pushHeader(lines, bundle) {
131
+ const { fbs, queue, bs, prd } = bundle;
132
+ lines.push(`## ${SECTION_NUMBERS.header}. Header`, '');
133
+ lines.push(`- Item: ${fbs.fbsId} - ${fbs.title}`);
134
+ lines.push(`- Queue: order ${fbs.buildOrder}, item ${queue.position} of ${queue.total}`);
135
+ lines.push(`- Execution status: ${fbs.executionStatus}`);
136
+ if (fbs.estimatedSize !== undefined) lines.push(`- Estimated size: ${fbs.estimatedSize}`);
137
+ if (fbs.estimatedHours !== undefined) lines.push(`- Estimated hours: ${fbs.estimatedHours}`);
138
+ if (fbs.riskLevel !== undefined) lines.push(`- Risk level: ${fbs.riskLevel}`);
139
+ if (fbs.domain !== undefined) lines.push(`- Domain: ${fbs.domain}`);
140
+ const chain = [];
141
+ if (bs) chain.push(bs.bsId);
142
+ if (prd) chain.push(prd.productName ? `${prd.prdId} (${prd.productName})` : prd.prdId);
143
+ if (chain.length > 0) lines.push(`- Parent chain: ${chain.join(' -> ')}`);
144
+ if (fbs.updatedAt !== undefined) lines.push(`- Spec last touched: ${fbs.updatedAt}`);
145
+ lines.push('');
146
+ }
147
+
148
+ function pushQueueContext(lines, bundle) {
149
+ const { bs, blockedBy, dependencies, dependents } = bundle;
150
+ lines.push(`## ${SECTION_NUMBERS.queueContext}. Queue and dependency context`, '');
151
+ if (blockedBy.length > 0) {
152
+ const blocking = dependencies
153
+ .filter((d) => blockedBy.includes(d.fbsId))
154
+ .map((d) => `${d.fbsId} (${d.executionStatus ?? 'unknown'})`)
155
+ .join(', ');
156
+ lines.push(`> **BLOCKED**: unsatisfied dependencies - ${blocking}.`);
157
+ lines.push('> This bundle is a read-ahead. Do not start implementation until every');
158
+ lines.push('> dependency is complete or verified; `--strict` refuses this bundle.');
159
+ lines.push('');
160
+ }
161
+ if (bs) {
162
+ lines.push(`- Build sequence: ${bs.bsId} - ${bs.title}`);
163
+ if (bs.generationStrategy !== undefined) lines.push(`- Generation strategy: ${bs.generationStrategy}`);
164
+ if (bs.buildPhilosophy !== undefined) lines.push(`- Build philosophy: ${bs.buildPhilosophy}`);
165
+ lines.push('');
166
+ }
167
+ if (dependencies.length > 0) {
168
+ lines.push('Dependencies:', '');
169
+ lines.push('| id | title | status |');
170
+ lines.push('|---|---|---|');
171
+ for (const dep of dependencies) {
172
+ lines.push(`| ${dep.fbsId} | ${dep.title ?? ''} | ${dep.executionStatus ?? ''} |`);
173
+ }
174
+ lines.push('');
175
+ } else {
176
+ lines.push('Dependencies: none.', '');
177
+ }
178
+ lines.push(
179
+ dependents.length > 0
180
+ ? `Dependents waiting on this item: ${dependents.join(', ')}`
181
+ : 'Dependents waiting on this item: none.',
182
+ );
183
+ lines.push('');
184
+ }
185
+
186
+ function pushWork(lines, bundle) {
187
+ const { fbs } = bundle;
188
+ lines.push(`## ${SECTION_NUMBERS.work}. The work`, '');
189
+ lines.push(fbs.summary, '');
190
+ if (fbs.approach !== undefined) {
191
+ lines.push('Approach:', '', fbs.approach, '');
192
+ }
193
+ if (fbs.deliverables !== undefined && fbs.deliverables.length > 0) {
194
+ lines.push('Deliverables:', '');
195
+ for (const d of fbs.deliverables) lines.push(`- ${d}`);
196
+ lines.push('');
197
+ }
198
+ if (fbs.notes !== undefined) {
199
+ lines.push('Notes:', '', fbs.notes, '');
200
+ }
201
+ }
202
+
203
+ function pushAcceptanceCriteria(lines, bundle) {
204
+ lines.push(`## ${SECTION_NUMBERS.acceptanceCriteria}. Acceptance criteria`, '');
205
+ for (const us of bundle.userStories) {
206
+ lines.push(`### ${us.usId}: ${us.title}${us.status !== undefined ? ` (status: ${us.status})` : ''}`, '');
207
+ if (us.asA !== undefined && us.iWant !== undefined && us.soThat !== undefined) {
208
+ lines.push(`As a ${us.asA}, I want ${us.iWant}, so that ${us.soThat}.`, '');
209
+ }
210
+ const acs = bundle.acceptanceCriteria.filter((ac) => ac.usId === us.usId);
211
+ const req = bundle.requirements.find((r) => r.reqId === acs[0]?.reqId);
212
+ if (req) {
213
+ const meta = [req.category, req.priority !== undefined ? `priority: ${req.priority}` : null]
214
+ .filter((v) => v !== null && v !== undefined)
215
+ .join(', ');
216
+ lines.push(`Parent requirement ${req.reqId}: ${req.title}${meta ? ` (${meta})` : ''}`, '');
217
+ if (req.description !== undefined) lines.push(`> ${req.description}`);
218
+ if (req.rationale !== undefined) {
219
+ if (req.description !== undefined) lines.push('>');
220
+ lines.push(`> Rationale: ${req.rationale}`);
221
+ }
222
+ if (req.description !== undefined || req.rationale !== undefined) lines.push('');
223
+ }
224
+ for (const ac of acs) {
225
+ lines.push(`#### ${ac.id}: ${ac.description}`, '');
226
+ if (ac.given !== undefined) lines.push(`- Given: ${ac.given}`);
227
+ if (ac.when !== undefined) lines.push(`- When: ${ac.when}`);
228
+ if (ac.then !== undefined) lines.push(`- Then: ${ac.then}`);
229
+ lines.push(`- Testable: ${ac.testable ? 'yes' : 'no'}`);
230
+ lines.push('');
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Render a resolved TAD / PRD section value. String content renders as
237
+ * prose; structured content (the TAD's sections are objects / arrays)
238
+ * renders as a fenced JSON block - still byte-stable, still readable.
239
+ */
240
+ function pushSectionValue(lines, value) {
241
+ if (typeof value === 'string') {
242
+ lines.push(value, '');
243
+ } else {
244
+ lines.push('```json');
245
+ lines.push(JSON.stringify(value, null, 2));
246
+ lines.push('```', '');
247
+ }
248
+ }
249
+
250
+ function pushArchitecturalContext(lines, bundle) {
251
+ const ctx = bundle.context;
252
+ lines.push(`## ${SECTION_NUMBERS.architecturalContext}. Architectural context`, '');
253
+ for (const tac of ctx.tacs) {
254
+ lines.push(`### TAC ${tac.tacId}: ${tac.name}`, '');
255
+ if (tac.purpose !== undefined) lines.push(`Purpose: ${tac.purpose}`, '');
256
+ if (tac.responsibilities !== undefined && tac.responsibilities.length > 0) {
257
+ lines.push('Responsibilities:', '');
258
+ for (const r of tac.responsibilities) lines.push(`- ${r}`);
259
+ lines.push('');
260
+ }
261
+ if (tac.interfaces !== undefined && tac.interfaces.length > 0) {
262
+ lines.push('Interfaces:', '');
263
+ for (const i of tac.interfaces) {
264
+ const kind = i.kind !== undefined ? ` (${i.kind})` : '';
265
+ const desc = i.description !== undefined ? `: ${i.description}` : '';
266
+ lines.push(`- ${i.name}${kind}${desc}`);
267
+ }
268
+ lines.push('');
269
+ }
270
+ if (tac.dependencies !== undefined && tac.dependencies.length > 0) {
271
+ lines.push('Dependencies:', '');
272
+ for (const d of tac.dependencies) {
273
+ const ref = d.kind === 'tac' && d.tacId !== undefined ? `${d.kind} ${d.tacId}` : d.kind;
274
+ const desc = d.description !== undefined ? `: ${d.description}` : '';
275
+ lines.push(`- ${d.name} (${ref})${desc}`);
276
+ }
277
+ lines.push('');
278
+ }
279
+ if (tac.tradeoffs !== undefined) lines.push(`Tradeoffs: ${tac.tradeoffs}`, '');
280
+ if (tac.notes !== undefined) lines.push(`Notes: ${tac.notes}`, '');
281
+ }
282
+ for (const adr of ctx.adrs) {
283
+ lines.push(`### ADR ${adr.adrId}: ${adr.title}${adr.status !== undefined ? ` (${adr.status})` : ''}`, '');
284
+ if (adr.context !== undefined) lines.push(`Context: ${adr.context}`, '');
285
+ if (adr.decision !== undefined) lines.push(`Decision: ${adr.decision}`, '');
286
+ if (adr.consequences !== undefined) lines.push(`Consequences: ${adr.consequences}`, '');
287
+ if (adr.alternativesConsidered !== undefined && adr.alternativesConsidered.length > 0) {
288
+ lines.push('Alternatives considered:', '');
289
+ for (const alt of adr.alternativesConsidered) {
290
+ const summary = alt.summary !== undefined ? `: ${alt.summary}` : '';
291
+ const reason = alt.reasonNotChosen !== undefined ? ` (not chosen: ${alt.reasonNotChosen})` : '';
292
+ lines.push(`- ${alt.name}${summary}${reason}`);
293
+ }
294
+ lines.push('');
295
+ }
296
+ }
297
+ for (const [name, value] of Object.entries(ctx.tadSections)) {
298
+ lines.push(`### TAD section: ${name}`, '');
299
+ pushSectionValue(lines, value);
300
+ }
301
+ for (const [name, value] of Object.entries(ctx.prdSections)) {
302
+ lines.push(`### PRD section: ${name}`, '');
303
+ pushSectionValue(lines, value);
304
+ }
305
+ if (ctx.unresolvedSections.length > 0) {
306
+ lines.push(
307
+ `Warning - unresolved sections (names that did not match a top-level TAD / PRD property): ${ctx.unresolvedSections.join(', ')}`,
308
+ '',
309
+ );
310
+ }
311
+ const pt = ctx.passThrough;
312
+ const ptRows = [
313
+ ['Existing modules', pt.existingModules],
314
+ ['Schemas', pt.schemas],
315
+ ['External docs', pt.externalDocs],
316
+ ['Other', pt.other],
317
+ ].filter(([, list]) => list.length > 0);
318
+ if (ptRows.length > 0) {
319
+ lines.push('Referenced material (pass-through, verbatim):', '');
320
+ for (const [label, list] of ptRows) {
321
+ lines.push(`- ${label}: ${list.join(', ')}`);
322
+ }
323
+ lines.push('');
324
+ }
325
+ }
326
+
327
+ function pushTestSurface(lines, bundle) {
328
+ lines.push(`## ${SECTION_NUMBERS.testSurface}. Existing test surface`, '');
329
+ lines.push('Presence reporting off the tree, not a coverage verdict (`rcf coverage` is the coverage surface).', '');
330
+ for (const entry of bundle.tests) {
331
+ if (!entry.covered && entry.suites.length === 0) {
332
+ lines.push(`- ${entry.acId}: no existing tests - test suite to be written for this AC`);
333
+ continue;
334
+ }
335
+ const suites = entry.suites.length > 0 ? entry.suites.join(', ') : 'none';
336
+ lines.push(`- ${entry.acId}: suites: ${suites}`);
337
+ for (const c of entry.cases) {
338
+ const pointer = c.testPointer !== undefined ? ` [${c.testPointer}]` : '';
339
+ const status = c.status !== undefined ? ` (${c.status})` : '';
340
+ lines.push(` - ${c.tcId}${status}: ${c.description ?? ''}${pointer}`);
341
+ }
342
+ }
343
+ lines.push('');
344
+ }
345
+
346
+ /**
347
+ * Section 7 - the full RCF five-stage build-cycle runbook (§D3.7,
348
+ * amendment D3-A). Static text parameterised only by fbsId and the
349
+ * in-scope AC ids; the each-stage-commits discipline is part of the
350
+ * printed contract. The tool assembles and referees; the harness
351
+ * executes the cycle.
352
+ */
353
+ function pushRunbook(lines, bundle) {
354
+ const fbsId = bundle.fbs.fbsId;
355
+ const acIds = bundle.acceptanceCriteria.map((ac) => ac.id).join(', ');
356
+ lines.push(`## ${SECTION_NUMBERS.runbook}. Build-cycle runbook`, '');
357
+ lines.push('This bundle is the work order for one pass of the RCF five-stage build');
358
+ lines.push('cycle: Define -> Build -> Review -> Test -> Finalise. The tool assembles');
359
+ lines.push('and referees; the harness executes. Every stage ends in a commit.', '');
360
+ // Deep-layer pointer (Phase 7.5 §D5.4, OQ-P7.5-3): the runbook is the
361
+ // terse just-in-time layer; the playbook is the deep layer.
362
+ lines.push('Deep guidance: rcf://docs/build-cycle and the rcf_execute_build_cycle prompt, or `rcf guidance build-cycle-playbook` on the CLI.', '');
363
+
364
+ lines.push('### Stage 1 - Define', '');
365
+ lines.push('Satisfied by this bundle: the FBS, acceptance criteria, ancestry and');
366
+ lines.push('architectural context above ARE the definition. Confirm your plan against');
367
+ lines.push(`every in-scope acceptance criterion (${acIds}) in section 4 before writing`);
368
+ lines.push('code, then mark pickup:', '');
369
+ lines.push(` rcf build ${fbsId} --mark inProgress`, '');
370
+ lines.push('Commit any plan artefacts the driving workflow requires.', '');
371
+
372
+ lines.push('### Stage 2 - Build', '');
373
+ lines.push('Implement to the acceptance criteria in section 4 using the architectural');
374
+ lines.push('context in section 5. The bundle is the spec: deviation is escalation to');
375
+ lines.push('the operator of the loop, not improvisation. As you implement, author or');
376
+ lines.push('update Code Nodes for the source you write:', '');
377
+ lines.push(' rcf create cn --path <file>[#symbol] --acs <ac-ids>', '');
378
+ lines.push('Do this now, not as an afterthought: comprehension of which symbols serve');
379
+ lines.push('which acceptance criteria is cheapest to capture while you are writing the');
380
+ lines.push('code, and Stage 5 refuses completion without it. Commit at stage end.', '');
381
+
382
+ lines.push('### Stage 3 - Review', '');
383
+ lines.push('Mechanical referee pass:', '');
384
+ lines.push(' rcf validate', '');
385
+ lines.push('must come back clean; then re-read the diff against every in-scope');
386
+ lines.push('acceptance criterion and document any deviations. Commit.', '');
387
+
388
+ lines.push('### Stage 4 - Test', '');
389
+ lines.push('Exercise every in-scope acceptance criterion: write or extend the TS / TC');
390
+ lines.push('documents (section 6 lists the existing surface and the flagged gaps) and');
391
+ lines.push('the tests they point to, until:', '');
392
+ lines.push(' rcf coverage --strict', '');
393
+ lines.push('covers the in-scope acceptance criteria. Commit.', '');
394
+
395
+ lines.push('### Stage 5 - Finalise', '');
396
+ lines.push('CI green; PR raised and merged per the driving workflow\'s convention.');
397
+ lines.push('After the merge:', '');
398
+ lines.push(` rcf build ${fbsId} --mark complete`, '');
399
+ lines.push('This refuses (exit 3, missingCodeNodes) if any in-scope acceptance');
400
+ lines.push('criterion still carries no Code Node - go back to Stage 2 and author it,');
401
+ lines.push('or, for a genuinely no-code spec (docs-only, config-only), declare:', '');
402
+ lines.push(` rcf build ${fbsId} --mark complete --no-code-nodes`, '');
403
+ lines.push('Then ship-gate the deployed app - an independent rcf-verify run that');
404
+ lines.push('passes with ship authority promotes complete -> verified:', '');
405
+ lines.push(` rcf finalise ${fbsId} --url <deploy-url>`);
406
+ }
@@ -0,0 +1,14 @@
1
+ // Public surface for the build module (Phase 6 §D8). Pure logic +
2
+ // formatters; the CLI handler lives at `src/cli/build.js`.
3
+
4
+ export { computeQueue, selectNext, byBuildOrder, LIFECYCLE } from './queue.js';
5
+ export { assembleBundle } from './bundle.js';
6
+ export {
7
+ checkCodeNodeGate,
8
+ checkContrastBeforePaletteGate,
9
+ checkDesignGate,
10
+ hasNoCodeNodesDeclaration,
11
+ planMark,
12
+ } from './mark.js';
13
+ export { formatMarkdown } from './formatters/markdown.js';
14
+ export { formatJson } from './formatters/json.js';
@@ -0,0 +1,177 @@
1
+ // Lifecycle marking (Phase 6 §D5). Pure transition planning for
2
+ // `rcf build <fbs-id> --mark <status>`: enum validation, forward-only
3
+ // transition-table validation, idempotent no-op detection. Returns a
4
+ // plan (or a structured error / refusal); the CLI handler executes the
5
+ // plan via the Phase 4 `updateDocument` write path. Phase 6 adds no
6
+ // new write primitive.
7
+ //
8
+ // The lifecycle is the schema enum, in order:
9
+ // notStarted -> inProgress -> complete -> verified
10
+ // Forward jumps are legal (notStarted -> complete for trivially-shipped
11
+ // items) but the mark ladder caps at `complete`: `--mark verified` is
12
+ // refused (exit 4) and points to the finalise gate (`rcf finalise`), which
13
+ // is the only path that writes `verified` after an independent verify run.
14
+ // Backward transitions are refused (exit 4) and the message names the
15
+ // deliberate-correction escape hatch (`rcf update`).
16
+
17
+ import { rcfError } from '#core/errors';
18
+ import { LIFECYCLE } from './queue.js';
19
+
20
+ // Phase 10 (X2 CodeNode bridge, D17, operator ruling 2026-07-10): the
21
+ // mark-complete CN gate. The no-code-nodes declaration is a dedicated
22
+ // `noCodeNodes: boolean` field on the FBS schema (rcf-schemas@0.3.1) - NOT
23
+ // a free-form convention string. An unvalidated magic value would silently
24
+ // fail on a typo, which is exactly the failure mode this feature exists to
25
+ // make visible (operator ruling 2026-07-10).
26
+ /**
27
+ * True when the FBS already carries the `--no-code-nodes` declaration.
28
+ * @param {object} fbs
29
+ * @returns {boolean}
30
+ */
31
+ export function hasNoCodeNodesDeclaration(fbs) {
32
+ return fbs?.noCodeNodes === true;
33
+ }
34
+
35
+ /**
36
+ * @typedef {object} MarkPlan
37
+ * @property {string} fbsId
38
+ * @property {string} from
39
+ * @property {string} to
40
+ * @property {boolean} noOp - true when the item is already at `to`
41
+ * @property {boolean} [refused] - true on a backward transition
42
+ * @property {string} [message] - refusal message (refused plans only)
43
+ */
44
+
45
+ /**
46
+ * Plan a lifecycle transition. Returns an RcfError (usage) for a bad
47
+ * status value or a non-FBS id, a refused plan for a backward
48
+ * transition, otherwise an executable plan.
49
+ *
50
+ * @param {import('#core/store/walker.js').TreeModel} tree
51
+ * @param {object} opts
52
+ * @param {string} opts.fbsId
53
+ * @param {string} opts.status - target executionStatus
54
+ * @returns {MarkPlan | import('#core/errors').RcfError}
55
+ */
56
+ export function planMark(tree, { fbsId, status }) {
57
+ if (!LIFECYCLE.includes(status)) {
58
+ return rcfError({
59
+ kind: 'usage',
60
+ message: `build: unknown --mark value '${status}' (expected ${LIFECYCLE.join(' | ')})`,
61
+ });
62
+ }
63
+ const fbs = tree.byId.get(fbsId);
64
+ if (!fbs || tree.kindById.get(fbsId) !== 'fbs') {
65
+ return rcfError({
66
+ kind: 'usage',
67
+ message: `build: id ${fbsId} not found or not an FBS`,
68
+ documentId: fbsId,
69
+ });
70
+ }
71
+ const from = fbs.executionStatus;
72
+ // The mark ladder caps at `complete` (w-2026-07-22-004). `verified` means
73
+ // "independently verified against the running deploy" - it is written only by
74
+ // the finalise gate (`rcf finalise`), which spawns a fresh rcf-verify run.
75
+ // Letting `--mark verified` promote with no verify run would let the builder
76
+ // sidestep that gate with one flag, defeating the spec-9 independence
77
+ // guarantee. Refused as part of the mark-refusal family (exit 4); the message
78
+ // names the finalise gate and keeps `rcf update` as the explicit manual
79
+ // override (unchanged - a deliberate, logged operator decision).
80
+ if (status === 'verified') {
81
+ return {
82
+ fbsId,
83
+ from,
84
+ to: status,
85
+ noOp: false,
86
+ refused: true,
87
+ message: `build: refusing --mark verified on ${fbsId}; 'verified' is written only by the `
88
+ + `independent verify gate, not by --mark. Promote it with: rcf finalise ${fbsId} --url <deploy-url>. `
89
+ + `For a deliberate manual override (no verify run) use: rcf update ${fbsId} --set executionStatus=verified`,
90
+ };
91
+ }
92
+ const fromIndex = LIFECYCLE.indexOf(from);
93
+ const toIndex = LIFECYCLE.indexOf(status);
94
+ if (toIndex === fromIndex) {
95
+ return { fbsId, from, to: status, noOp: true };
96
+ }
97
+ if (toIndex < fromIndex) {
98
+ return {
99
+ fbsId,
100
+ from,
101
+ to: status,
102
+ noOp: false,
103
+ refused: true,
104
+ message: `build: refusing backward transition ${from} -> ${status} on ${fbsId}; `
105
+ + `for a deliberate correction use: rcf update ${fbsId} --set executionStatus=${status}`,
106
+ };
107
+ }
108
+ return { fbsId, from, to: status, noOp: false };
109
+ }
110
+
111
+ /**
112
+ * The mark-complete CN gate (D17). Deterministic edge counting: an AC is
113
+ * "covered" when `tree.cnByAcId` carries at least one implementing CN.
114
+ * Called only when the target status is `complete`; the caller (cli/build)
115
+ * skips this entirely when the FBS already carries the no-code-nodes
116
+ * declaration or the invocation supplies `--no-code-nodes`.
117
+ *
118
+ * @param {import('#core/store/walker.js').TreeModel} tree
119
+ * @param {object} fbs - the FBS document being marked complete
120
+ * @returns {{ ok: true } | { ok: false, missingAcIds: string[] }}
121
+ */
122
+ export function checkCodeNodeGate(tree, fbs) {
123
+ const missingAcIds = (fbs.acIds ?? []).filter((acId) => (tree.cnByAcId?.get(acId) ?? []).length === 0);
124
+ if (missingAcIds.length === 0) return { ok: true };
125
+ return { ok: false, missingAcIds: [...missingAcIds].sort() };
126
+ }
127
+
128
+ // Track B (ui-design-gate-0.7.0-spec §5.2, §5.5): the `--mark complete`
129
+ // design gate. Refuses when the FBS is uiBearing=true and
130
+ // designStageComplete is not true on the FBS record. The gate is the
131
+ // boolean primitive per §12 O-3 (Decided); the executionStatus enum is
132
+ // unchanged.
133
+ /**
134
+ * @param {object} fbs
135
+ * @returns {{ ok: true } | { ok: false, reason: 'designStageIncomplete' }}
136
+ */
137
+ export function checkDesignGate(fbs) {
138
+ if (fbs?.uiBearing !== true) return { ok: true };
139
+ if (fbs?.designStageComplete === true) return { ok: true };
140
+ return { ok: false, reason: 'designStageIncomplete' };
141
+ }
142
+
143
+ // Track B (ui-design-gate-0.7.0-spec §7 mandate 10). Refuses when the
144
+ // operator's own attestation says the wrong order was authored, and
145
+ // (optionally) when a git-history probe unambiguously contradicts the
146
+ // operator's attestation. The git probe is out of scope for the pure
147
+ // mark.js module; the caller (cli/build) supplies the probe result.
148
+ /**
149
+ * @param {object} fbs
150
+ * @param {object} [historyProbe] optional git-history corroboration result
151
+ * @param {boolean} [historyProbe.inconclusive] true when git says nothing
152
+ * @param {boolean} [historyProbe.tokensCreatedFirst] true when history says tokens preceded contrast test
153
+ * @returns {{ ok: true } | { ok: false, reason: string, message: string }}
154
+ */
155
+ export function checkContrastBeforePaletteGate(fbs, historyProbe = { inconclusive: true }) {
156
+ if (fbs?.uiBearing !== true) return { ok: true };
157
+ const stage = fbs?.designStage;
158
+ if (!stage?.themeAndA11y) return { ok: true }; // gate deferred to design-mark-complete surface
159
+ const boolean = stage.themeAndA11y.contrastTestAuthoredBeforePalette;
160
+ if (boolean === false) {
161
+ return {
162
+ ok: false,
163
+ reason: 'contrastTestAfterPalette',
164
+ message: `build --mark complete: refused - ${fbs.fbsId} designStage.themeAndA11y.contrastTestAuthoredBeforePalette is false. `
165
+ + 'Author the contrast test before the palette, or record the reversal by ratifying the ordering explicitly.',
166
+ };
167
+ }
168
+ if (boolean === true && historyProbe && historyProbe.inconclusive !== true && historyProbe.tokensCreatedFirst === true) {
169
+ return {
170
+ ok: false,
171
+ reason: 'contrastGitHistoryConflict',
172
+ message: `build --mark complete: refused - ${fbs.fbsId} attests contrastTestAuthoredBeforePalette=true, but git history shows the tokens module (${stage.themeAndA11y.themeTokensModule}) was created before the contrast test (${stage.themeAndA11y.contrastTestPath}). `
173
+ + 'Reconcile the attestation with the history: rewrite the test-authoring order in the log, or set contrastTestAuthoredBeforePalette=false and accept the ruling.',
174
+ };
175
+ }
176
+ return { ok: true };
177
+ }