rcf-lite 0.28.2 → 0.29.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 (327) hide show
  1. package/CHANGELOG.md +317 -0
  2. package/bin/rcf.js +8 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/define-draft-shapes.md +89 -0
  5. package/guidance/define-intake.md +97 -0
  6. package/guidance/define-intent.md +107 -0
  7. package/guidance/discover-prototype.md +67 -0
  8. package/guidance/discovery-prototypes.md +69 -0
  9. package/guidance/harness-template.md +34 -0
  10. package/guidance/managed/agent-instructions-block.hash +1 -1
  11. package/guidance/managed/agent-instructions-block.md +34 -0
  12. package/guidance/manifest.json +9 -2
  13. package/package.json +1 -1
  14. package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
  15. package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
  16. package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
  17. package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
  18. package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
  19. package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
  20. package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
  21. package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
  22. package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
  23. package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
  24. package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
  25. package/rcf/adrs/adr-4134-index-json-route.json +35 -0
  26. package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
  27. package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
  28. package/rcf/code-nodes/cn-700.json +23 -0
  29. package/rcf/code-nodes/cn-701.json +14 -0
  30. package/rcf/code-nodes/cn-702.json +14 -0
  31. package/rcf/code-nodes/cn-703.json +16 -0
  32. package/rcf/code-nodes/cn-720.json +15 -0
  33. package/rcf/code-nodes/cn-721.json +15 -0
  34. package/rcf/code-nodes/cn-722.json +15 -0
  35. package/rcf/code-nodes/cn-723.json +18 -0
  36. package/rcf/code-nodes/cn-724.json +14 -0
  37. package/rcf/code-nodes/cn-725.json +37 -0
  38. package/rcf/code-nodes/cn-726.json +25 -0
  39. package/rcf/code-nodes/cn-727.json +23 -0
  40. package/rcf/code-nodes/cn-728.json +37 -0
  41. package/rcf/code-nodes/cn-729.json +26 -0
  42. package/rcf/code-nodes/cn-730.json +24 -0
  43. package/rcf/code-nodes/cn-731.json +20 -0
  44. package/rcf/code-nodes/cn-732.json +14 -0
  45. package/rcf/code-nodes/cn-733.json +14 -0
  46. package/rcf/code-nodes/cn-734.json +14 -0
  47. package/rcf/code-nodes/cn-735.json +20 -0
  48. package/rcf/code-nodes/cn-736.json +14 -0
  49. package/rcf/code-nodes/cn-737.json +14 -0
  50. package/rcf/code-nodes/cn-738.json +14 -0
  51. package/rcf/code-nodes/cn-739.json +18 -0
  52. package/rcf/code-nodes/cn-740.json +19 -0
  53. package/rcf/code-nodes/cn-741.json +16 -0
  54. package/rcf/code-nodes/cn-742.json +16 -0
  55. package/rcf/code-nodes/cn-743.json +15 -0
  56. package/rcf/code-nodes/cn-744.json +22 -0
  57. package/rcf/code-nodes/cn-745.json +16 -0
  58. package/rcf/code-nodes/cn-746.json +14 -0
  59. package/rcf/code-nodes/cn-747.json +18 -0
  60. package/rcf/code-nodes/cn-748.json +16 -0
  61. package/rcf/code-nodes/cn-749.json +16 -0
  62. package/rcf/code-nodes/cn-750.json +15 -0
  63. package/rcf/code-nodes/cn-751.json +18 -0
  64. package/rcf/code-nodes/cn-752.json +14 -0
  65. package/rcf/fbs/fbs-190.json +37 -0
  66. package/rcf/fbs/fbs-191.json +34 -0
  67. package/rcf/fbs/fbs-192.json +17 -0
  68. package/rcf/fbs/fbs-193.json +37 -0
  69. package/rcf/fbs/fbs-194.json +35 -0
  70. package/rcf/fbs/fbs-195.json +37 -0
  71. package/rcf/fbs/fbs-196.json +45 -0
  72. package/rcf/fbs/fbs-197.json +41 -0
  73. package/rcf/fbs/fbs-198.json +42 -0
  74. package/rcf/fbs/fbs-199.json +59 -0
  75. package/rcf/fbs/fbs-200.json +46 -0
  76. package/rcf/fbs/fbs-201.json +49 -0
  77. package/rcf/fbs/fbs-202.json +110 -0
  78. package/rcf/fbs/fbs-203.json +100 -0
  79. package/rcf/requirements/req-001.json +2 -1
  80. package/rcf/requirements/req-002.json +3 -2
  81. package/rcf/requirements/req-003.json +2 -1
  82. package/rcf/requirements/req-004.json +2 -1
  83. package/rcf/requirements/req-005.json +2 -1
  84. package/rcf/requirements/req-006.json +2 -1
  85. package/rcf/requirements/req-007.json +2 -1
  86. package/rcf/requirements/req-008.json +2 -1
  87. package/rcf/requirements/req-009.json +2 -1
  88. package/rcf/requirements/req-010.json +5 -2
  89. package/rcf/requirements/req-011.json +2 -1
  90. package/rcf/requirements/req-012.json +2 -1
  91. package/rcf/requirements/req-013.json +2 -1
  92. package/rcf/requirements/req-014.json +2 -1
  93. package/rcf/requirements/req-015.json +2 -1
  94. package/rcf/requirements/req-016.json +5 -2
  95. package/rcf/requirements/req-017.json +6 -2
  96. package/rcf/requirements/req-018.json +5 -2
  97. package/rcf/requirements/req-019.json +5 -2
  98. package/rcf/requirements/req-020.json +5 -2
  99. package/rcf/requirements/req-021.json +5 -2
  100. package/rcf/requirements/req-022.json +5 -2
  101. package/rcf/requirements/req-023.json +6 -4
  102. package/rcf/requirements/req-024.json +5 -2
  103. package/rcf/requirements/req-025.json +5 -2
  104. package/rcf/requirements/req-026.json +5 -2
  105. package/rcf/requirements/req-027.json +5 -2
  106. package/rcf/requirements/req-028.json +5 -2
  107. package/rcf/requirements/req-029.json +5 -2
  108. package/rcf/requirements/req-030.json +5 -2
  109. package/rcf/requirements/req-040.json +5 -2
  110. package/rcf/requirements/req-041.json +5 -2
  111. package/rcf/requirements/req-042.json +5 -2
  112. package/rcf/requirements/req-043.json +5 -2
  113. package/rcf/requirements/req-050.json +5 -2
  114. package/rcf/requirements/req-051.json +5 -2
  115. package/rcf/requirements/req-052.json +5 -2
  116. package/rcf/requirements/req-053.json +5 -2
  117. package/rcf/requirements/req-054.json +5 -2
  118. package/rcf/requirements/req-060.json +5 -2
  119. package/rcf/requirements/req-061.json +5 -2
  120. package/rcf/requirements/req-062.json +5 -2
  121. package/rcf/requirements/req-063.json +5 -2
  122. package/rcf/requirements/req-070.json +5 -2
  123. package/rcf/requirements/req-071.json +5 -2
  124. package/rcf/requirements/req-072.json +5 -2
  125. package/rcf/requirements/req-073.json +5 -2
  126. package/rcf/requirements/req-074.json +5 -2
  127. package/rcf/requirements/req-075.json +5 -2
  128. package/rcf/requirements/req-076.json +5 -2
  129. package/rcf/requirements/req-077.json +5 -2
  130. package/rcf/requirements/req-080.json +5 -2
  131. package/rcf/requirements/req-081.json +5 -2
  132. package/rcf/requirements/req-082.json +5 -2
  133. package/rcf/requirements/req-083.json +5 -2
  134. package/rcf/requirements/req-084.json +5 -2
  135. package/rcf/requirements/req-085.json +5 -2
  136. package/rcf/requirements/req-086.json +5 -2
  137. package/rcf/requirements/req-090.json +5 -2
  138. package/rcf/requirements/req-091.json +5 -2
  139. package/rcf/requirements/req-092.json +5 -2
  140. package/rcf/requirements/req-093.json +5 -2
  141. package/rcf/requirements/req-094.json +5 -2
  142. package/rcf/requirements/req-100.json +5 -2
  143. package/rcf/requirements/req-101.json +5 -2
  144. package/rcf/requirements/req-102.json +5 -2
  145. package/rcf/requirements/req-103.json +5 -2
  146. package/rcf/requirements/req-104.json +5 -2
  147. package/rcf/requirements/req-110.json +5 -2
  148. package/rcf/requirements/req-111.json +5 -2
  149. package/rcf/requirements/req-112.json +5 -2
  150. package/rcf/requirements/req-113.json +5 -2
  151. package/rcf/requirements/req-114.json +5 -2
  152. package/rcf/requirements/req-115.json +5 -2
  153. package/rcf/requirements/req-120.json +5 -2
  154. package/rcf/requirements/req-121.json +5 -2
  155. package/rcf/requirements/req-122.json +5 -2
  156. package/rcf/requirements/req-123.json +5 -2
  157. package/rcf/requirements/req-124.json +5 -2
  158. package/rcf/requirements/req-125.json +5 -2
  159. package/rcf/requirements/req-130.json +6 -2
  160. package/rcf/requirements/req-131.json +6 -2
  161. package/rcf/requirements/req-132.json +6 -2
  162. package/rcf/requirements/req-133.json +6 -2
  163. package/rcf/requirements/req-134.json +6 -2
  164. package/rcf/requirements/req-140.json +5 -2
  165. package/rcf/requirements/req-145.json +5 -2
  166. package/rcf/requirements/req-150.json +6 -2
  167. package/rcf/requirements/req-151.json +6 -2
  168. package/rcf/requirements/req-152.json +6 -2
  169. package/rcf/requirements/req-153.json +6 -2
  170. package/rcf/requirements/req-154.json +5 -2
  171. package/rcf/requirements/req-155.json +4 -1
  172. package/rcf/requirements/req-156.json +4 -1
  173. package/rcf/requirements/req-157.json +4 -1
  174. package/rcf/requirements/req-158.json +4 -1
  175. package/rcf/requirements/req-159.json +4 -1
  176. package/rcf/requirements/req-160.json +4 -1
  177. package/rcf/requirements/req-161.json +4 -1
  178. package/rcf/requirements/req-162.json +4 -1
  179. package/rcf/requirements/req-163.json +5 -2
  180. package/rcf/requirements/req-164.json +5 -2
  181. package/rcf/requirements/req-165.json +5 -2
  182. package/rcf/requirements/req-170.json +33 -0
  183. package/rcf/requirements/req-171.json +44 -0
  184. package/rcf/requirements/req-172.json +25 -0
  185. package/rcf/requirements/req-173.json +25 -0
  186. package/rcf/requirements/req-174.json +31 -0
  187. package/rcf/requirements/req-175.json +31 -0
  188. package/rcf/requirements/req-176.json +25 -0
  189. package/rcf/requirements/req-180.json +31 -0
  190. package/rcf/requirements/req-181.json +31 -0
  191. package/rcf/requirements/req-182.json +25 -0
  192. package/rcf/requirements/req-186.json +25 -0
  193. package/rcf/requirements/req-187.json +31 -0
  194. package/rcf/requirements/req-188.json +25 -0
  195. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  196. package/rcf/tacs/tac-4121-define-ledgers.json +35 -0
  197. package/rcf/tacs/tac-4122-define-gates.json +61 -0
  198. package/rcf/tacs/tac-4123-define-readiness.json +61 -0
  199. package/rcf/tacs/tac-4124-define-freeze.json +28 -0
  200. package/rcf/tacs/tac-4127-readiness-view.json +49 -0
  201. package/rcf/tacs/tac-4128-define-guidance.json +53 -0
  202. package/rcf/tacs/tac-4129-define-questions.json +45 -0
  203. package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
  204. package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
  205. package/rcf/tacs/tac-4132-id-lookup.json +42 -0
  206. package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
  207. package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
  208. package/rcf/test-suites/ts-016.json +3 -3
  209. package/rcf/test-suites/ts-204.json +30 -0
  210. package/rcf/test-suites/ts-205.json +22 -0
  211. package/rcf/test-suites/ts-206.json +22 -0
  212. package/rcf/test-suites/ts-207.json +22 -0
  213. package/rcf/test-suites/ts-208.json +22 -0
  214. package/rcf/test-suites/ts-209.json +22 -0
  215. package/rcf/test-suites/ts-210.json +22 -0
  216. package/rcf/test-suites/ts-211.json +22 -0
  217. package/rcf/test-suites/ts-212.json +22 -0
  218. package/rcf/test-suites/ts-213.json +50 -0
  219. package/rcf/test-suites/ts-214.json +74 -0
  220. package/rcf/test-suites/ts-215.json +211 -0
  221. package/rcf/test-suites/ts-216.json +94 -0
  222. package/rcf/test-suites/ts-217.json +62 -0
  223. package/rcf/test-suites/ts-218.json +78 -0
  224. package/rcf/test-suites/ts-219.json +70 -0
  225. package/rcf/test-suites/ts-220.json +78 -0
  226. package/rcf/test-suites/ts-221.json +78 -0
  227. package/rcf/test-suites/ts-222.json +100 -0
  228. package/rcf/test-suites/ts-223.json +94 -0
  229. package/rcf/test-suites/ts-224.json +70 -0
  230. package/rcf/test-suites/ts-225.json +78 -0
  231. package/rcf/test-suites/ts-226.json +158 -0
  232. package/rcf/test-suites/ts-227.json +38 -0
  233. package/rcf/test-suites/ts-228.json +86 -0
  234. package/rcf/test-suites/ts-229.json +83 -0
  235. package/rcf/user-stories/us-15601.json +4 -4
  236. package/rcf/user-stories/us-17001.json +27 -0
  237. package/rcf/user-stories/us-17002.json +22 -0
  238. package/rcf/user-stories/us-17003.json +22 -0
  239. package/rcf/user-stories/us-17004.json +22 -0
  240. package/rcf/user-stories/us-17005.json +22 -0
  241. package/rcf/user-stories/us-17006.json +21 -0
  242. package/rcf/user-stories/us-17007.json +21 -0
  243. package/rcf/user-stories/us-17008.json +22 -0
  244. package/rcf/user-stories/us-17101.json +22 -0
  245. package/rcf/user-stories/us-17102.json +22 -0
  246. package/rcf/user-stories/us-17201.json +38 -0
  247. package/rcf/user-stories/us-17301.json +38 -0
  248. package/rcf/user-stories/us-17302.json +50 -0
  249. package/rcf/user-stories/us-17401.json +70 -0
  250. package/rcf/user-stories/us-17402.json +55 -0
  251. package/rcf/user-stories/us-17403.json +60 -0
  252. package/rcf/user-stories/us-17501.json +48 -0
  253. package/rcf/user-stories/us-17502.json +60 -0
  254. package/rcf/user-stories/us-17503.json +65 -0
  255. package/rcf/user-stories/us-17601.json +58 -0
  256. package/rcf/user-stories/us-18001.json +55 -0
  257. package/rcf/user-stories/us-18002.json +35 -0
  258. package/rcf/user-stories/us-18101.json +35 -0
  259. package/rcf/user-stories/us-18202.json +40 -0
  260. package/rcf/user-stories/us-18601.json +55 -0
  261. package/rcf/user-stories/us-18602.json +50 -0
  262. package/rcf/user-stories/us-18603.json +45 -0
  263. package/rcf/user-stories/us-18701.json +50 -0
  264. package/rcf/user-stories/us-18702.json +50 -0
  265. package/rcf/user-stories/us-18801.json +45 -0
  266. package/rcf/user-stories/us-204.json +59 -0
  267. package/rcf/user-stories/us-205.json +64 -0
  268. package/rcf/user-stories/us-206.json +54 -0
  269. package/releases/releases.yaml +31 -1
  270. package/src/cli/feedback.js +126 -10
  271. package/src/cli/freeze.js +730 -0
  272. package/src/cli/help.js +23 -0
  273. package/src/cli/ledger.js +615 -0
  274. package/src/cli/questions.js +288 -0
  275. package/src/cli/readiness.js +498 -0
  276. package/src/cli/view.js +29 -0
  277. package/src/core/store/writer.js +86 -6
  278. package/src/define/freeze-record.js +247 -0
  279. package/src/define/ledgers.js +712 -0
  280. package/src/feedback/redact.js +105 -19
  281. package/src/intake/index.js +1 -1
  282. package/src/intake/orchestrator.js +117 -0
  283. package/src/mcp/tools.js +354 -0
  284. package/src/query/delta.js +282 -0
  285. package/src/query/gates.js +1107 -0
  286. package/src/query/index.js +50 -0
  287. package/src/query/questions.js +438 -0
  288. package/src/query/readiness.js +574 -0
  289. package/src/server/index.js +60 -1
  290. package/src/server/routes.js +133 -4
  291. package/src/setup/identity-seed.js +17 -0
  292. package/src/view/build-dag.js +563 -0
  293. package/src/view/components/badge.js +41 -0
  294. package/src/view/components/diff.js +72 -0
  295. package/src/view/components/doc-row.js +62 -0
  296. package/src/view/components/empty-state.js +35 -0
  297. package/src/view/components/entity-selector.js +137 -0
  298. package/src/view/components/filter-bar.js +97 -0
  299. package/src/view/components/findings-list.js +49 -0
  300. package/src/view/components/index.js +12 -0
  301. package/src/view/components/pill.js +46 -0
  302. package/src/view/components/sub-tab-strip.js +43 -0
  303. package/src/view/components/toast.js +25 -0
  304. package/src/view/components-fixture.js +265 -0
  305. package/src/view/doc-renderers/architecture.js +304 -0
  306. package/src/view/doc-renderers/build.js +160 -0
  307. package/src/view/doc-renderers/helpers.js +2 -2
  308. package/src/view/doc-renderers/index.js +2 -0
  309. package/src/view/doc-renderers/prd.js +35 -6
  310. package/src/view/doc-renderers/req.js +10 -2
  311. package/src/view/doc-renderers/user-story.js +10 -5
  312. package/src/view/html-page.js +717 -300
  313. package/src/view/index.js +109 -3
  314. package/src/view/live-client.js +61 -8
  315. package/src/view/needs-work.js +79 -0
  316. package/src/view/page-init.js +2193 -0
  317. package/src/view/product-map.js +751 -0
  318. package/src/view/readiness/phrasebook.js +115 -0
  319. package/src/view/readiness/po-layer.js +288 -0
  320. package/src/view/readiness/question-adapter.js +329 -0
  321. package/src/view/readiness/stage-legend.js +71 -0
  322. package/src/view/readiness.js +505 -0
  323. package/src/view/style.css +2279 -266
  324. package/src/view/test-host/embed-client.js +121 -0
  325. package/src/view/test-host/fixture.js +183 -0
  326. package/src/view/test-host/lints.js +220 -0
  327. package/src/view/view-index.js +323 -0
@@ -0,0 +1,574 @@
1
+ // Readiness compute (REQ-175; proposal 2026-09-22 §2.4, §6 v3).
2
+ //
3
+ // One pure composer over the DEFINE detection primitives:
4
+ // 1. `computeDelta` (slice 1) -> changed / added / removed / briefSince
5
+ // 2. `computeImpact` per id in changed union added -> fan-out
6
+ // 3. Eight gate check functions from `./gates.js` -> stage[]
7
+ // 4. `computeCoverage` tree-wide plus per REQ ancestor of the delta
8
+ // 5. Open decisions from the decisions ledger
9
+ // 6. Fold to `freezeable` (every stage passed / acknowledged /
10
+ // notApplicable) and `nextAction` (first failing stage + check)
11
+ //
12
+ // Return shape mirrors proposal §2.4 exactly. The CLI (rcf define
13
+ // readiness) and the viewer's Readiness tab (slice 5) render the same
14
+ // object; the freeze verb (slice 3) refuses on any failing stage.
15
+
16
+ import { computeQueue } from '../build/queue.js';
17
+ import { computeCoverage } from './coverage.js';
18
+ import { computeDelta } from './delta.js';
19
+ import { computeImpact } from './impact.js';
20
+ import {
21
+ STAGE_GATES,
22
+ STAGE_ORDER,
23
+ STAGE_SHORT_NAMES,
24
+ checkD1Brief,
25
+ checkD2Skeleton,
26
+ checkD3Shapes,
27
+ checkD4Stories,
28
+ checkD5Crosscut,
29
+ checkD6Consistency,
30
+ checkD7Decisions,
31
+ checkD8Freeze,
32
+ } from './gates.js';
33
+
34
+ /**
35
+ * @typedef {object} ReadinessTree
36
+ * @property {boolean} frozen
37
+ * @property {string | null} frozenAt
38
+ * @property {string | null} treeHash
39
+ * @property {string} currentTreeHash
40
+ * @property {string | null} buildAt fbsId at the queue head, or null
41
+ * @property {number} fbsTotal
42
+ */
43
+
44
+ /**
45
+ * @typedef {object} ReadinessDelta
46
+ * @property {string[]} changed
47
+ * @property {string[]} added
48
+ * @property {string[]} removed
49
+ * @property {number[]} briefSince
50
+ * @property {import('./impact.js').ImpactNode[]} impacted
51
+ * @property {string[]} impactedFbs
52
+ * @property {Record<string, string>} currentDocHashes
53
+ * Live per-document hashes (ledgers keyed as `ledger:<name>`).
54
+ * Threaded from `computeDelta` so the Readiness tab can render a
55
+ * document-level hash diff for each changed / added id (spec §5
56
+ * block 6; the frozen body is not persisted in the freeze record
57
+ * in 0.29.0, so the diff is hash-only for now).
58
+ */
59
+
60
+ /**
61
+ * @typedef {object} CheckResult
62
+ * @property {string} name
63
+ * @property {boolean} ok
64
+ * @property {'delta'|'tree'} over
65
+ * @property {number} pass
66
+ * @property {number} total
67
+ * @property {Array<{ id: string, why: string }>} failing
68
+ * @property {'productOwner'|'engineer'} persona
69
+ * @property {string} question
70
+ */
71
+
72
+ /**
73
+ * @typedef {object} StageResult
74
+ * @property {string} stage
75
+ * @property {string} gate
76
+ * @property {'passed'|'failing'|'acknowledged'|'notApplicable'} state
77
+ * @property {CheckResult[]} checks
78
+ * @property {string} [reason]
79
+ */
80
+
81
+ /**
82
+ * @typedef {object} Blocker
83
+ * @property {string} stage
84
+ * @property {string} gate
85
+ * @property {string} check
86
+ * @property {'productOwner'|'engineer'} persona
87
+ * @property {'delta'|'tree'} over
88
+ * @property {number} failingCount
89
+ * @property {string[]} ids
90
+ * @property {string} question
91
+ */
92
+
93
+ /**
94
+ * @typedef {object} LevelVerdict
95
+ * @property {boolean} ok
96
+ * @property {Blocker[]} blockedBy
97
+ * @property {NextAction | null} nextAction
98
+ */
99
+
100
+ /**
101
+ * @typedef {object} ReadinessLevels
102
+ * @property {LevelVerdict} intentComplete
103
+ * @property {LevelVerdict} readyToBuild
104
+ */
105
+
106
+ /**
107
+ * @typedef {object} PersonaGroup
108
+ * @property {Blocker[]} blockers
109
+ * @property {NextAction | null} nextAction
110
+ */
111
+
112
+ /**
113
+ * @typedef {object} ReadinessPersonas
114
+ * @property {PersonaGroup} productOwner
115
+ * @property {PersonaGroup} engineer
116
+ */
117
+
118
+ /**
119
+ * @typedef {object} NextAction
120
+ * @property {string} stage
121
+ * @property {string} check
122
+ * @property {string[]} ids
123
+ * @property {string} command
124
+ */
125
+
126
+ /**
127
+ * @typedef {object} ReadinessResult
128
+ * @property {ReadinessTree} tree
129
+ * @property {ReadinessDelta} delta
130
+ * @property {StageResult[]} stages
131
+ * @property {NextAction | null} nextAction
132
+ * @property {{ tree: import('./coverage.js').CoverageResult, delta: import('./coverage.js').CoverageResult[] }} coverage
133
+ * @property {Array<Record<string, unknown>>} decisions
134
+ * @property {boolean} freezeable
135
+ * @property {ReadinessLevels} levels
136
+ * @property {ReadinessPersonas} personas
137
+ */
138
+
139
+ /**
140
+ * Compute the readiness object.
141
+ *
142
+ * Pure over its inputs. Callers supply the ledgers bundle, freeze
143
+ * record, testPointers map (resolveTestPointers output) and optional
144
+ * profile switches; nothing else touches the filesystem.
145
+ *
146
+ * @param {import('#core/store/walker.js').TreeModel} tree
147
+ * @param {object} args
148
+ * @param {import('./delta.js').FreezeRecord | null | undefined} [args.freeze]
149
+ * @param {import('./delta.js').LedgerBundle | undefined} [args.ledgers]
150
+ * @param {{ skipReviewFor?: string } | undefined} [args.profile]
151
+ * @param {string | null | undefined} [args.profileText] rcf/.identity/profile.md contents
152
+ * @param {Map<string, unknown> | undefined} [args.testPointers]
153
+ * @param {Array<unknown> | undefined} [args.validateErrors] tree-wide validate + walker errors
154
+ * @returns {ReadinessResult}
155
+ */
156
+ export function computeReadiness(tree, args = {}) {
157
+ const {
158
+ freeze = null,
159
+ ledgers = {},
160
+ profile = undefined,
161
+ profileText = null,
162
+ testPointers = undefined,
163
+ validateErrors = [],
164
+ } = args;
165
+
166
+ // 1. Delta.
167
+ const delta = computeDelta(tree, freeze, ledgers);
168
+
169
+ // 2. Fan-out via computeImpact per id in changed union added.
170
+ // Standalone-document ids only; ledger:<name> entries have no fan-out.
171
+ const pivotIds = [
172
+ ...delta.changed.filter((id) => !id.startsWith('ledger:')),
173
+ ...delta.added.filter((id) => !id.startsWith('ledger:')),
174
+ ];
175
+ const pivotSet = new Set(pivotIds);
176
+ /** @type {import('./impact.js').ImpactNode[]} */
177
+ const impactedNodes = [];
178
+ const impactedSeen = new Set();
179
+ const impactedFbsSeen = new Set();
180
+ // Skip fan-out when the tree is unfrozen: the delta IS the whole tree
181
+ // (proposal §2.2 "before the first freeze the delta is the whole
182
+ // tree"), so a per-id fan-out is quadratic for no signal.
183
+ const runFanOut = delta.frozen === true;
184
+ if (runFanOut) {
185
+ for (const id of pivotIds) {
186
+ const impact = computeImpact(tree, { id });
187
+ if (!impact.found || !Array.isArray(impact.nodes)) continue;
188
+ for (const node of impact.nodes) {
189
+ if (node.role === 'pivot') continue;
190
+ if (pivotSet.has(node.id)) continue;
191
+ if (!impactedSeen.has(node.id)) {
192
+ impactedSeen.add(node.id);
193
+ impactedNodes.push(node);
194
+ }
195
+ if (node.kind === 'fbs' && node.actionNeeded === 're-execute') {
196
+ impactedFbsSeen.add(node.id);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ const impactedFbs = [...impactedFbsSeen].sort();
202
+
203
+ // 3. Compose scope = changed union added union impacted (dedup, sort).
204
+ const scopeSet = new Set([
205
+ ...delta.changed.filter((id) => !id.startsWith('ledger:')),
206
+ ...delta.added.filter((id) => !id.startsWith('ledger:')),
207
+ ...impactedSeen,
208
+ ]);
209
+
210
+ // 4. Coverage: tree-wide plus per REQ ancestor of the delta.
211
+ const coverageTree = computeCoverage(tree, { testPointers, strict: true });
212
+ const deltaReqIds = collectDeltaReqAncestors(tree, pivotIds);
213
+ const coverageDelta = [];
214
+ for (const reqId of deltaReqIds) {
215
+ coverageDelta.push(computeCoverage(tree, { testPointers, strict: true, scopeId: reqId }));
216
+ }
217
+
218
+ // 5. Run the eight gate check functions in order. D8 reads the
219
+ // prior stage states, so compose sequentially.
220
+ /** @type {StageResult[]} */
221
+ const stages = [];
222
+ const currentTreeHash = delta.currentTreeHash;
223
+ /** @type {import('./gates.js').StageContext} */
224
+ const ctx = {
225
+ tree,
226
+ ledgers,
227
+ delta,
228
+ freeze,
229
+ scope: scopeSet,
230
+ validateErrors,
231
+ // Prefer the caller-supplied probe count when it is passed; falls
232
+ // back to the ledger scan inside checkD6Consistency.
233
+ probeOpenCount: undefined,
234
+ coverageTree,
235
+ profileText,
236
+ profile,
237
+ currentTreeHash,
238
+ priorStages: undefined,
239
+ };
240
+ stages.push(checkD1Brief(ctx));
241
+ stages.push(checkD2Skeleton(ctx));
242
+ stages.push(checkD3Shapes(ctx));
243
+ stages.push(checkD4Stories(ctx));
244
+ stages.push(checkD5Crosscut(ctx));
245
+ stages.push(checkD6Consistency(ctx));
246
+ stages.push(checkD7Decisions(ctx));
247
+ stages.push(checkD8Freeze({ ...ctx, priorStages: stages.map((s) => ({ stage: s.stage, state: s.state })) }));
248
+
249
+ // 6. Open decisions in the #241 format for the surface consumer.
250
+ const decisions = /** @type {any} */ (ledgers?.decisions);
251
+ const decisionEntries = Array.isArray(decisions?.decisions) ? decisions.decisions : [];
252
+ const openDecisions = decisionEntries.filter((d) => d?.status === 'open');
253
+
254
+ // 7. Fold freezeable + nextAction.
255
+ const freezeable = stages.every((s) => s.state === 'passed' || s.state === 'acknowledged' || s.state === 'notApplicable');
256
+ const nextAction = freezeable ? null : deriveNextAction(stages);
257
+
258
+ // 7b. Fold `levels` and `personas` from stages[].checks[] alone
259
+ // (ADR-4126, proposal section 2.1). Nothing is written. L1
260
+ // intent-complete is "every productOwner check ok across every
261
+ // stage"; L2 ready-to-build equals freezeable with blockers over
262
+ // failing stages only.
263
+ const levels = deriveLevels(stages, freezeable, nextAction);
264
+ const personas = derivePersonas(stages, levels);
265
+
266
+ // 8. Build the tree summary line inputs.
267
+ const queue = computeQueue(tree);
268
+ const fbsTotal = (tree.fbsItems ?? []).length;
269
+
270
+ /** @type {ReadinessResult} */
271
+ return {
272
+ tree: {
273
+ frozen: delta.frozen,
274
+ frozenAt: delta.frozenAt,
275
+ treeHash: delta.treeHash,
276
+ currentTreeHash: delta.currentTreeHash,
277
+ buildAt: queue.nextActionable ?? null,
278
+ fbsTotal,
279
+ },
280
+ delta: {
281
+ changed: delta.changed,
282
+ added: delta.added,
283
+ removed: delta.removed,
284
+ briefSince: delta.briefSince,
285
+ impacted: impactedNodes,
286
+ impactedFbs,
287
+ currentDocHashes: delta.currentDocHashes ?? {},
288
+ },
289
+ stages,
290
+ nextAction,
291
+ coverage: { tree: coverageTree, delta: coverageDelta },
292
+ decisions: openDecisions,
293
+ freezeable,
294
+ levels,
295
+ personas,
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Walk back from every delta pivot to its REQ ancestor and collect
301
+ * the unique REQ ids. The walker records parent-child edges under
302
+ * `parentByChild`; the walk terminates at the first REQ hit.
303
+ *
304
+ * @param {import('#core/store/walker.js').TreeModel} tree
305
+ * @param {string[]} pivotIds
306
+ * @returns {string[]}
307
+ */
308
+ function collectDeltaReqAncestors(tree, pivotIds) {
309
+ const seen = new Set();
310
+ for (const id of pivotIds) {
311
+ let cursor = id;
312
+ let hops = 0;
313
+ while (cursor && hops < 32) {
314
+ hops += 1;
315
+ const kind = tree.kindById?.get(cursor);
316
+ if (kind === 'req') {
317
+ seen.add(cursor);
318
+ break;
319
+ }
320
+ // Some kinds carry an explicit reqId; use that when the parent
321
+ // chain skips the REQ (e.g. a TC leaf whose parent is a TS).
322
+ const doc = tree.byId?.get(cursor);
323
+ if (doc && typeof doc === 'object' && typeof /** @type {any} */ (doc).reqId === 'string') {
324
+ seen.add(/** @type {any} */ (doc).reqId);
325
+ break;
326
+ }
327
+ const parent = tree.parentByChild?.get(cursor);
328
+ if (!parent) break;
329
+ cursor = parent;
330
+ }
331
+ }
332
+ return [...seen].sort();
333
+ }
334
+
335
+ /**
336
+ * Pick the first failing stage in D1..D8 order and the first failing
337
+ * check inside it, then compose a `NextAction`. With no `persona`
338
+ * passed, behaves exactly as 0.28.4 did (first failing stage, first
339
+ * failing check, largest failing list). With a `persona` ('productOwner'
340
+ * | 'engineer'), picks the first failing stage that holds a failing
341
+ * check of that persona, then the largest failing-list check among
342
+ * them; the command carries `--level intent` on the productOwner
343
+ * variant and no `--level` flag otherwise (proposal section 2.1).
344
+ *
345
+ * The productOwner next action is defined over every stage holding a
346
+ * failing PO check (not just stages with state `failing`), because
347
+ * `levels.intentComplete.blockedBy` ignores stage state. In practice
348
+ * PO checks live on blocking stages (D1, D2, D4, D7) so any failing
349
+ * PO check also makes its stage `failing`, but the compute does not
350
+ * rely on that coincidence.
351
+ *
352
+ * @param {StageResult[]} stages
353
+ * @param {'productOwner' | 'engineer'} [persona]
354
+ * @returns {NextAction | null}
355
+ */
356
+ export function deriveNextAction(stages, persona) {
357
+ const byStage = new Map(stages.map((s) => [s.stage, s]));
358
+ for (const stageId of STAGE_ORDER) {
359
+ const s = byStage.get(stageId);
360
+ if (!s) continue;
361
+ // Without a persona filter: today's rule (stage must be
362
+ // `failing`; this skips `acknowledged` warn-with-ack stages).
363
+ // With a persona filter: scan any stage that holds a failing
364
+ // check of that persona. PO checks only live on blocking stages
365
+ // so this coincides with "stage failing" in practice, but it
366
+ // keeps the compute honest if that ever changes.
367
+ if (!persona && s.state !== 'failing') continue;
368
+ const failing = s.checks.filter((c) => !c.ok && (persona ? c.persona === persona : true));
369
+ if (failing.length === 0) continue;
370
+ // First failing check; tie-break by the largest failing list so
371
+ // the operator sees the most impactful gap first.
372
+ const check = failing.reduce((a, b) => (b.failing.length > a.failing.length ? b : a), failing[0]);
373
+ const ids = [...new Set(check.failing.map((f) => f.id))].slice(0, 20);
374
+ const shortName = STAGE_SHORT_NAMES[stageId] ?? stageId;
375
+ const command = persona === 'productOwner'
376
+ ? `rcf define readiness --level intent --check ${shortName}`
377
+ : `rcf define readiness --check ${shortName}`;
378
+ return {
379
+ stage: stageId,
380
+ check: check.name,
381
+ ids,
382
+ command,
383
+ };
384
+ }
385
+ return null;
386
+ }
387
+
388
+ /**
389
+ * Build a `Blocker` row from a stage + check pair. `ids` are
390
+ * deduplicated and capped at 20 to match the display convention
391
+ * `nextAction.ids` uses (proposal section 2.1).
392
+ *
393
+ * @param {StageResult} stage
394
+ * @param {CheckResult} check
395
+ * @returns {Blocker}
396
+ */
397
+ function makeBlocker(stage, check) {
398
+ return {
399
+ stage: stage.stage,
400
+ gate: stage.gate,
401
+ check: check.name,
402
+ persona: check.persona,
403
+ over: check.over,
404
+ failingCount: check.failing.length,
405
+ ids: [...new Set(check.failing.map((f) => f.id))].slice(0, 20),
406
+ question: check.question,
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Fold the two levels from `stages[].checks[]` (ADR-4126, proposal
412
+ * section 2.1). L1 ignores stage state and lists every failing PO
413
+ * check; L2 lists every failing check in a stage whose state is
414
+ * `failing` (checks inside an `acknowledged` stage are not blockers).
415
+ *
416
+ * @param {StageResult[]} stages
417
+ * @param {boolean} freezeable
418
+ * @param {NextAction | null} nextAction
419
+ * @returns {ReadinessLevels}
420
+ */
421
+ export function deriveLevels(stages, freezeable, nextAction) {
422
+ /** @type {Blocker[]} */
423
+ const poBlockers = [];
424
+ /** @type {Blocker[]} */
425
+ const buildBlockers = [];
426
+ for (const stage of stages) {
427
+ for (const check of stage.checks) {
428
+ if (check.ok) continue;
429
+ if (check.persona === 'productOwner') {
430
+ poBlockers.push(makeBlocker(stage, check));
431
+ }
432
+ if (stage.state === 'failing') {
433
+ buildBlockers.push(makeBlocker(stage, check));
434
+ }
435
+ }
436
+ }
437
+ const intentOk = poBlockers.length === 0;
438
+ return {
439
+ intentComplete: {
440
+ ok: intentOk,
441
+ blockedBy: poBlockers,
442
+ nextAction: intentOk ? null : deriveNextAction(stages, 'productOwner'),
443
+ },
444
+ readyToBuild: {
445
+ ok: freezeable,
446
+ blockedBy: buildBlockers,
447
+ nextAction,
448
+ },
449
+ };
450
+ }
451
+
452
+ /**
453
+ * Fold the `personas` block (ADR-4126, proposal section 3.3).
454
+ * `productOwner.blockers` deep-equals `levels.intentComplete.blockedBy`;
455
+ * `engineer.blockers` is the failing engineer checks in failing stages
456
+ * (the L2 blockedBy minus the PO entries).
457
+ *
458
+ * @param {StageResult[]} stages
459
+ * @param {ReadinessLevels} levels
460
+ * @returns {ReadinessPersonas}
461
+ */
462
+ export function derivePersonas(stages, levels) {
463
+ const engineerBlockers = levels.readyToBuild.blockedBy.filter((b) => b.persona === 'engineer');
464
+ return {
465
+ productOwner: {
466
+ blockers: levels.intentComplete.blockedBy,
467
+ nextAction: levels.intentComplete.nextAction,
468
+ },
469
+ engineer: {
470
+ blockers: engineerBlockers,
471
+ nextAction: deriveNextAction(stages, 'engineer'),
472
+ },
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Validate a `--level` flag value against the closed set
478
+ * `intent | build`. Returns the value, `null` when the flag was
479
+ * absent, or throws a usage error whose message is formatted exactly
480
+ * like the `--check` usage line so the CLI can forward it verbatim.
481
+ *
482
+ * @param {string | undefined} raw
483
+ * @returns {'intent' | 'build' | null}
484
+ */
485
+ export function parseLevelFlag(raw) {
486
+ if (raw === undefined || raw === null) return null;
487
+ if (raw === 'intent' || raw === 'build') return raw;
488
+ throw new Error(`unknown --level ${raw} (expected intent | build)`);
489
+ }
490
+
491
+ /**
492
+ * Validate a `--persona` flag value against the closed set
493
+ * `productOwner | engineer`. Returns the value, `null` when the flag
494
+ * was absent, or throws a usage error with the same shape.
495
+ *
496
+ * @param {string | undefined} raw
497
+ * @returns {'productOwner' | 'engineer' | null}
498
+ */
499
+ export function parsePersonaFlag(raw) {
500
+ if (raw === undefined || raw === null) return null;
501
+ if (raw === 'productOwner' || raw === 'engineer') return raw;
502
+ throw new Error(`unknown --persona ${raw} (expected productOwner | engineer)`);
503
+ }
504
+
505
+ /**
506
+ * Format the two verdict lines shared by the CLI text report and
507
+ * the Readiness tab (proposal section 2.3). The strings are built
508
+ * from `levels` alone so the two surfaces cannot disagree.
509
+ *
510
+ * @param {ReadinessResult} result
511
+ * @returns {{ intentComplete: string, readyToBuild: string }}
512
+ */
513
+ export function formatVerdictLines(result) {
514
+ const l = result.levels;
515
+ const intentLine = l.intentComplete.ok
516
+ ? 'Intent-complete: yes. Every product-owner question is answered.'
517
+ : (() => {
518
+ const n = l.intentComplete.blockedBy.length;
519
+ const detail = l.intentComplete.blockedBy
520
+ .map((b) => `${b.stage}/${b.check}`)
521
+ .join(', ');
522
+ return `Intent-complete: no; ${n} question${n === 1 ? '' : 's'} for the product owner (${detail}).`;
523
+ })();
524
+ const buildLine = l.readyToBuild.ok
525
+ ? `Ready-to-build: yes. Freezeable at ${shortHash(result.tree.currentTreeHash)}.`
526
+ : (() => {
527
+ const blockers = l.readyToBuild.blockedBy;
528
+ const stages = [...new Set(blockers.map((b) => b.stage))].sort();
529
+ const total = blockers.length;
530
+ const p = blockers.filter((b) => b.persona === 'productOwner').length;
531
+ const e = blockers.filter((b) => b.persona === 'engineer').length;
532
+ return `Ready-to-build: no; blocked on ${stages.join(', ')} (${total} check${total === 1 ? '' : 's'}: ${p} product owner, ${e} engineer).`;
533
+ })();
534
+ return { intentComplete: intentLine, readyToBuild: buildLine };
535
+ }
536
+
537
+ /**
538
+ * Short 8-character prefix of a `sha256:<hex>` string, or '(none)' when
539
+ * absent. Kept exported so text formatters share one convention.
540
+ *
541
+ * @param {string | null | undefined} hash
542
+ * @returns {string}
543
+ */
544
+ export function shortHash(hash) {
545
+ if (typeof hash !== 'string' || !hash.startsWith('sha256:')) return '(none)';
546
+ return hash.slice('sha256:'.length, 'sha256:'.length + 8);
547
+ }
548
+
549
+ /**
550
+ * One-line tree summary matching proposal §6.2:
551
+ * "Frozen at 9f3a2c on 2026-09-20. 4 documents changed since, 11
552
+ * impacted. Build at FBS-031 of 41; 2 FBS impacted."
553
+ * "Unfrozen. 30 documents; the whole tree is the delta."
554
+ *
555
+ * @param {ReadinessResult} result
556
+ * @returns {string}
557
+ */
558
+ export function formatTreeLine(result) {
559
+ const t = result.tree;
560
+ const d = result.delta;
561
+ if (!t.frozen) {
562
+ const total = d.added.length + d.changed.length;
563
+ return `Unfrozen. ${total} document${total === 1 ? '' : 's'}; the whole tree is the delta.`;
564
+ }
565
+ const changedCount = d.changed.length + d.added.length + d.removed.length;
566
+ const impactedCount = d.impacted.length;
567
+ const buildAt = t.buildAt ?? '(none)';
568
+ const impactedFbs = d.impactedFbs.length;
569
+ const at = t.frozenAt ? t.frozenAt.split('T')[0] : '(no timestamp)';
570
+ return `Frozen at ${shortHash(t.treeHash)} on ${at}. ${changedCount} document${changedCount === 1 ? '' : 's'} changed since, ${impactedCount} impacted. Build at ${buildAt} of ${t.fbsTotal}; ${impactedFbs} FBS impacted.`;
571
+ }
572
+
573
+ // Re-export for CLI convenience.
574
+ export { STAGE_GATES, STAGE_ORDER, STAGE_SHORT_NAMES };
@@ -11,9 +11,10 @@
11
11
  // on top; the server itself does not `process.exit`.
12
12
 
13
13
  import { createServer } from 'node:http';
14
+ import { readFile } from 'node:fs/promises';
14
15
  import { join } from 'node:path';
15
16
 
16
- import { LIVE_CLIENT_PATH, STYLE_CSS_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
17
+ import { LIVE_CLIENT_PATH, PAGE_INIT_PATH, STYLE_CSS_PATH, TEST_HOST_CLIENT_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
17
18
  import { watch as defaultWatch } from '../watch/index.js';
18
19
  import { createRouter } from './routes.js';
19
20
  import { createScopeHandler } from './scope-endpoint.js';
@@ -28,6 +29,10 @@ import { createSseHub } from './sse.js';
28
29
  * @property {number} [debounceMs=50]
29
30
  * @property {(line: string) => void} [log] - stderr sink
30
31
  * @property {typeof defaultWatch} [watchImpl] - injectable watch primitive for tests
32
+ * @property {boolean} [testHost=false] - viewer UI refresh PR 9 (TAC-4134,
33
+ * ADR-4136): when true, mount the wespa host fixture at /test-host.html
34
+ * plus its embed-client script at /test-host.js. Off by default so
35
+ * wespa's proxy allow-list is unaffected.
31
36
  */
32
37
 
33
38
  /**
@@ -42,6 +47,25 @@ import { createSseHub } from './sse.js';
42
47
  * rewalk: () => Promise<void>,
43
48
  * }>}
44
49
  */
50
+
51
+ /**
52
+ * Issue #248 (0.28.3): read a shipped static asset once and return
53
+ * `{ buffer, size, contentType }`. Any read failure surfaces here so
54
+ * `startServer` can fail fast with an ioFailure before it binds.
55
+ *
56
+ * @param {string} path
57
+ * @param {string} contentType
58
+ */
59
+ async function loadStaticAsset(path, contentType) {
60
+ // Codex P2-1: derive size from the actual captured buffer, not from
61
+ // a parallel stat that could snapshot a different revision if the
62
+ // file is being replaced concurrently. A wrong content-length header
63
+ // would cause truncation or mis-alignment for the entire server
64
+ // lifetime because we cache both values together.
65
+ const buffer = await readFile(path);
66
+ return { buffer, size: buffer.length, contentType };
67
+ }
68
+
45
69
  export async function startServer(args) {
46
70
  const projectRoot = args.projectRoot;
47
71
  const port = typeof args.port === 'number' ? args.port : 4373;
@@ -50,6 +74,7 @@ export async function startServer(args) {
50
74
  const debounceMs = typeof args.debounceMs === 'number' ? args.debounceMs : 50;
51
75
  const log = typeof args.log === 'function' ? args.log : () => {};
52
76
  const watchImpl = typeof args.watchImpl === 'function' ? args.watchImpl : defaultWatch;
77
+ const testHost = args.testHost === true;
53
78
 
54
79
  /** @type {{ version: number, fullPageHtml: string, contentHtml: string, errors: import('#core/errors').RcfError[] } | null} */
55
80
  let state = null;
@@ -76,6 +101,12 @@ export async function startServer(args) {
76
101
  fullPageHtml: result.fullPageHtml,
77
102
  contentHtml: result.contentHtml,
78
103
  errors: result.errors,
104
+ pmPartials: result.pmPartials,
105
+ // Viewer UI refresh PR 7 (TAC-4132, ADR-4134): the ID lookup
106
+ // index is built once per rewalk and snapshotted on `state`
107
+ // so `GET /index.json` serves from memory and the SSE
108
+ // `tree-update` version lets the client invalidate its cache.
109
+ indexJson: result.indexJson,
79
110
  };
80
111
  sse.broadcast('tree-update', { version, contentHtml: result.contentHtml });
81
112
  if (result.errors && result.errors.length > 0) {
@@ -97,14 +128,42 @@ export async function startServer(args) {
97
128
  // and the first SSE connect gets a real payload.
98
129
  await rewalk();
99
130
 
131
+ // Issue #248 (0.28.3): snapshot the shipped static assets at startup
132
+ // so a checkout branch switch during the server's lifetime cannot
133
+ // change what gets served. The tree walker is the only path that
134
+ // re-reads disk on rcf/ change; every other asset is fixed at boot.
135
+ const [styleAsset, mermaidAsset, liveClientAsset, pageInitAsset, testHostClientAsset] = await Promise.all([
136
+ loadStaticAsset(STYLE_CSS_PATH, 'text/css; charset=utf-8'),
137
+ loadStaticAsset(VENDORED_MERMAID_PATH, 'application/javascript; charset=utf-8'),
138
+ loadStaticAsset(LIVE_CLIENT_PATH, 'application/javascript; charset=utf-8'),
139
+ loadStaticAsset(PAGE_INIT_PATH, 'application/javascript; charset=utf-8'),
140
+ // Viewer UI refresh PR 9 (TAC-4134): the wespa host fixture client
141
+ // only loads when testHost is on. Keeping the read behind the gate
142
+ // means a startup under default options never touches the file.
143
+ testHost
144
+ ? loadStaticAsset(TEST_HOST_CLIENT_PATH, 'application/javascript; charset=utf-8')
145
+ : Promise.resolve(null),
146
+ ]);
147
+
100
148
  const scopeHandler = createScopeHandler({ projectRoot });
101
149
  const router = createRouter({
102
150
  currentState: () => state,
103
151
  sse,
152
+ // Snapshotted assets take precedence; the legacy path props are
153
+ // still passed so tests that mount the router directly with a
154
+ // path (no asset load) keep working.
155
+ styleAsset,
156
+ mermaidAsset,
157
+ liveClientAsset,
158
+ pageInitAsset,
104
159
  stylePath: STYLE_CSS_PATH,
105
160
  mermaidPath: VENDORED_MERMAID_PATH,
106
161
  liveClientPath: LIVE_CLIENT_PATH,
162
+ pageInitPath: PAGE_INIT_PATH,
107
163
  scope: scopeHandler,
164
+ testHost,
165
+ testHostClientAsset,
166
+ testHostClientPath: testHost ? TEST_HOST_CLIENT_PATH : undefined,
108
167
  });
109
168
 
110
169
  const server = createServer(router);