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,323 @@
1
+ // Viewer UI refresh PR 7 (TAC-4132, ADR-4134): the ID lookup index
2
+ // built once per rewalk from BuiltTreeModel. One row per document id
3
+ // plus one row per AC under each US, with the minimum metadata the
4
+ // LookupModal needs to rank and route: `{ id, kind, title, snippet,
5
+ // parent, tab }`. The server serves this via `GET /index.json` so
6
+ // wespa's reverse proxy sees it as a new relative route under the
7
+ // mount point (release-noted; ADR-4134 names the alternatives rejected).
8
+ //
9
+ // Pure projection: reads nothing beyond the model and returns a plain
10
+ // JSON-safe object. The viewer client fetches it lazily on first open
11
+ // and refreshes it when the SSE tree-update broadcast lands.
12
+
13
+ const SNIPPET_LIMIT = 110;
14
+
15
+ const TAB_BY_KIND = {
16
+ prd: 'overview',
17
+ req: 'requirements',
18
+ us: 'requirements',
19
+ ac: 'requirements',
20
+ tad: 'architecture',
21
+ tac: 'architecture',
22
+ adr: 'architecture',
23
+ bs: 'build',
24
+ fbs: 'build',
25
+ ts: 'requirements',
26
+ };
27
+
28
+ /**
29
+ * @typedef {object} IndexRow
30
+ * @property {string} id canonical document / AC id
31
+ * @property {'prd'|'req'|'us'|'ac'|'tad'|'tac'|'adr'|'bs'|'fbs'|'ts'} kind
32
+ * @property {string} title display title (empty string on a doc with no title)
33
+ * @property {string} snippet first SNIPPET_LIMIT chars of the kind's prose field with whitespace collapsed
34
+ * @property {string} parent parent document id (empty string when none; e.g. the PRD has no parent)
35
+ * @property {'overview'|'requirements'|'architecture'|'build'} tab
36
+ */
37
+
38
+ /**
39
+ * Collapse an arbitrary string to the first SNIPPET_LIMIT characters
40
+ * with every whitespace run normalised to a single space, so the
41
+ * modal rows render consistently and the JSON payload stays small.
42
+ *
43
+ * @param {unknown} raw
44
+ * @returns {string}
45
+ */
46
+ function snippetOf(raw) {
47
+ if (typeof raw !== 'string' || raw.length === 0) return '';
48
+ const flat = raw.replace(/\s+/g, ' ').trim();
49
+ if (flat.length <= SNIPPET_LIMIT) return flat;
50
+ return flat.slice(0, SNIPPET_LIMIT);
51
+ }
52
+
53
+ /**
54
+ * Build the ID lookup index from a BuiltTreeModel. One row per
55
+ * document id (PRD, REQ, US, TAD, TAC, ADR, BS, FBS, TS) plus one row
56
+ * per AC on each US. Rows carry `{ id, kind, title, snippet, parent,
57
+ * tab }` and nothing else; the client renders every surface detail
58
+ * (count badge, kind pill) from this core shape.
59
+ *
60
+ * @param {import('./tree-model.js').BuiltTreeModel} model
61
+ * @returns {{ rows: IndexRow[] }}
62
+ */
63
+ export function buildViewIndex(model) {
64
+ const rows = [];
65
+
66
+ const prd = model?.prd;
67
+ if (prd?.prdId) {
68
+ rows.push({
69
+ id: prd.prdId,
70
+ kind: 'prd',
71
+ title: typeof prd.productName === 'string' ? prd.productName : '',
72
+ snippet: snippetOf(prd.executiveSummary ?? prd.problemStatement ?? ''),
73
+ parent: '',
74
+ tab: TAB_BY_KIND.prd,
75
+ });
76
+ }
77
+
78
+ for (const r of model?.requirements ?? []) {
79
+ if (!r?.reqId) continue;
80
+ rows.push({
81
+ id: r.reqId,
82
+ kind: 'req',
83
+ title: typeof r.title === 'string' ? r.title : '',
84
+ snippet: snippetOf(r.description ?? r.rationale ?? ''),
85
+ parent: typeof r.prdId === 'string' ? r.prdId : '',
86
+ tab: TAB_BY_KIND.req,
87
+ });
88
+ }
89
+
90
+ for (const us of model?.userStories ?? []) {
91
+ if (!us?.usId) continue;
92
+ rows.push({
93
+ id: us.usId,
94
+ kind: 'us',
95
+ title: typeof us.title === 'string' ? us.title : '',
96
+ snippet: snippetOf(us.description ?? us.iWant ?? ''),
97
+ parent: typeof us.reqId === 'string' ? us.reqId : '',
98
+ tab: TAB_BY_KIND.us,
99
+ });
100
+ for (const ac of us.acceptanceCriteria ?? []) {
101
+ if (!ac?.id) continue;
102
+ rows.push({
103
+ id: ac.id,
104
+ kind: 'ac',
105
+ title: '',
106
+ snippet: snippetOf(ac.description ?? ''),
107
+ parent: us.usId,
108
+ tab: TAB_BY_KIND.ac,
109
+ });
110
+ }
111
+ }
112
+
113
+ const tad = model?.tad;
114
+ if (tad?.tadId) {
115
+ rows.push({
116
+ id: tad.tadId,
117
+ kind: 'tad',
118
+ title: typeof tad.title === 'string' ? tad.title : '',
119
+ snippet: snippetOf(tad.systemOverview ?? ''),
120
+ parent: typeof tad.prdId === 'string' ? tad.prdId : '',
121
+ tab: TAB_BY_KIND.tad,
122
+ });
123
+ }
124
+
125
+ for (const t of model?.tacs ?? []) {
126
+ if (!t?.tacId) continue;
127
+ rows.push({
128
+ id: t.tacId,
129
+ kind: 'tac',
130
+ title: typeof t.name === 'string' ? t.name : '',
131
+ snippet: snippetOf(t.purpose ?? ''),
132
+ parent: typeof t.tadId === 'string' ? t.tadId : '',
133
+ tab: TAB_BY_KIND.tac,
134
+ });
135
+ }
136
+
137
+ for (const a of model?.adrs ?? []) {
138
+ if (!a?.adrId) continue;
139
+ rows.push({
140
+ id: a.adrId,
141
+ kind: 'adr',
142
+ title: typeof a.title === 'string' ? a.title : '',
143
+ snippet: snippetOf(a.decision ?? a.context ?? ''),
144
+ parent: typeof a.tadId === 'string' ? a.tadId : '',
145
+ tab: TAB_BY_KIND.adr,
146
+ });
147
+ }
148
+
149
+ const bs = model?.bs;
150
+ if (bs?.bsId) {
151
+ rows.push({
152
+ id: bs.bsId,
153
+ kind: 'bs',
154
+ title: typeof bs.title === 'string' ? bs.title : '',
155
+ snippet: snippetOf(bs.buildPhilosophy ?? ''),
156
+ parent: typeof bs.prdId === 'string' ? bs.prdId : '',
157
+ tab: TAB_BY_KIND.bs,
158
+ });
159
+ }
160
+
161
+ for (const f of model?.fbsItems ?? []) {
162
+ if (!f?.fbsId) continue;
163
+ rows.push({
164
+ id: f.fbsId,
165
+ kind: 'fbs',
166
+ title: typeof f.title === 'string' ? f.title : '',
167
+ snippet: snippetOf(f.summary ?? ''),
168
+ parent: typeof f.bsId === 'string' ? f.bsId : '',
169
+ tab: TAB_BY_KIND.fbs,
170
+ });
171
+ }
172
+
173
+ for (const ts of model?.testSuites ?? []) {
174
+ const id = ts?.id ?? ts?.tsId;
175
+ if (!id) continue;
176
+ rows.push({
177
+ id,
178
+ kind: 'ts',
179
+ title: typeof ts.title === 'string' ? ts.title : '',
180
+ snippet: snippetOf(ts.purpose ?? ''),
181
+ parent: typeof ts.usId === 'string' ? ts.usId : '',
182
+ tab: TAB_BY_KIND.ts,
183
+ });
184
+ }
185
+
186
+ return { rows };
187
+ }
188
+
189
+ /**
190
+ * Build the JSON string the server returns on `GET /index.json`. Kept
191
+ * next to buildViewIndex so the Content-Length can be set from the
192
+ * byte length of the exact string the client receives.
193
+ *
194
+ * @param {import('./tree-model.js').BuiltTreeModel} model
195
+ * @returns {string}
196
+ */
197
+ export function serialiseViewIndex(model) {
198
+ return JSON.stringify(buildViewIndex(model));
199
+ }
200
+
201
+ /**
202
+ * Pure ranking helper mirrored into page-init.js's wireLookup. Kept
203
+ * here so the server-side tests can exercise the exact ranking rules
204
+ * the browser applies. Design doc section 6 rules:
205
+ * - A single-character query searches ids only; the title / snippet
206
+ * word-match pass is skipped.
207
+ * - Otherwise ids rank first (exact > prefix > contains).
208
+ * - Then every query word must match title or snippet, title above
209
+ * snippet.
210
+ * - 20 results max; stable by id on ties.
211
+ *
212
+ * @param {string} query
213
+ * @param {IndexRow[]} rows
214
+ * @returns {Array<{ row: IndexRow, score: number, needles: string[] }>}
215
+ */
216
+ export function rankLookupRows(query, rows) {
217
+ if (!query || !Array.isArray(rows)) return [];
218
+ const q = String(query).trim().toLowerCase();
219
+ if (!q) return [];
220
+ const scored = [];
221
+ if (q.length === 1) {
222
+ for (const r of rows) {
223
+ const idLow = (r.id || '').toLowerCase();
224
+ if (idLow.indexOf(q) === -1) continue;
225
+ let score = 100;
226
+ if (idLow === q) score = 1000;
227
+ else if (idLow.indexOf(q) === 0) score = 500;
228
+ scored.push({ row: r, score, needles: [q] });
229
+ }
230
+ } else {
231
+ const words = q.split(/\s+/).filter(Boolean);
232
+ for (const r of rows) {
233
+ const idLow = (r.id || '').toLowerCase();
234
+ const titleLow = (r.title || '').toLowerCase();
235
+ const snipLow = (r.snippet || '').toLowerCase();
236
+ let score = 0;
237
+ if (idLow === q) score = 10000;
238
+ else if (idLow.indexOf(q) === 0) score = 5000;
239
+ else if (idLow.indexOf(q) !== -1) score = 2500;
240
+ else {
241
+ let titleHits = 0;
242
+ let snipHits = 0;
243
+ let allHit = true;
244
+ for (const w of words) {
245
+ const inTitle = titleLow.indexOf(w) !== -1;
246
+ const inSnip = snipLow.indexOf(w) !== -1;
247
+ if (inTitle) titleHits += 1;
248
+ else if (inSnip) snipHits += 1;
249
+ else { allHit = false; break; }
250
+ }
251
+ if (!allHit) continue;
252
+ score = 1000 + titleHits * 50 + snipHits * 10;
253
+ }
254
+ if (score > 0) scored.push({ row: r, score, needles: words });
255
+ }
256
+ }
257
+ scored.sort((a, b) => {
258
+ if (b.score !== a.score) return b.score - a.score;
259
+ return (a.row.id || '').localeCompare(b.row.id || '');
260
+ });
261
+ return scored.slice(0, 20);
262
+ }
263
+
264
+ /**
265
+ * The LookupModal shell HTML. Rendered once at the top of the shell
266
+ * (outside #rcf-live-content so the SSE innerHTML swap cannot blow it
267
+ * away). The modal is hidden by default; wireLookup toggles it. The
268
+ * results list is rendered client-side after the index fetch resolves,
269
+ * so this emits only the input, the status / empty-state slot, the
270
+ * reload affordance and the live-region hooks screen readers expect.
271
+ *
272
+ * No `<div>` tags are used: the toast container uses `<output>` for
273
+ * exactly this reason - the Phase 3.8 D13a shell invariant checked by
274
+ * `test/view/html-page.test.js` reads the last `</div>` to find the
275
+ * live-content wrapper close, so the modal uses semantic elements
276
+ * (`<aside>` / `<section>` / `<header>` / `<ul>` / `<p>`) throughout.
277
+ *
278
+ * @returns {string}
279
+ */
280
+ export function renderLookupModal() {
281
+ return `<aside class="rcf-lookup" data-rcf-lookup hidden role="dialog" aria-modal="true" aria-labelledby="rcf-lookup-title">
282
+ <button type="button" class="rcf-lookup-backdrop" data-rcf-lookup-backdrop aria-label="Close id lookup backdrop" tabindex="-1"></button>
283
+ <section class="rcf-lookup-panel" role="document">
284
+ <header class="rcf-lookup-head">
285
+ <h2 id="rcf-lookup-title" class="rcf-lookup-heading">Find any document or AC</h2>
286
+ <button type="button" class="rcf-lookup-close" data-rcf-lookup-close aria-label="Close id lookup">Close</button>
287
+ </header>
288
+ <label class="rcf-lookup-field">
289
+ <span class="rcf-lookup-label">Type an id or title</span>
290
+ <input type="text" class="rcf-lookup-input" data-rcf-lookup-input
291
+ autocomplete="off" spellcheck="false" aria-controls="rcf-lookup-results"
292
+ aria-describedby="rcf-lookup-status" placeholder="REQ-002, US-304, LookupModal, ...">
293
+ </label>
294
+ <p class="rcf-lookup-status" id="rcf-lookup-status" data-rcf-lookup-status role="status" aria-live="polite"></p>
295
+ <ul class="rcf-lookup-results" id="rcf-lookup-results" data-rcf-lookup-results role="listbox" aria-label="Lookup results"></ul>
296
+ <section class="rcf-lookup-empty" data-rcf-lookup-empty hidden>
297
+ <p class="rcf-lookup-empty-msg" data-rcf-lookup-empty-msg>No matches.</p>
298
+ <button type="button" class="rcf-lookup-reload" data-rcf-lookup-reload hidden>Reload index</button>
299
+ </section>
300
+ </section>
301
+ </aside>`;
302
+ }
303
+
304
+ /**
305
+ * The SearchButton for the shell's `<div class="tools">`. One
306
+ * <button> with a search mark glyph; opens the LookupModal on click.
307
+ * Carries the Cmd/Ctrl+F mnemonic in the title/aria-label so a
308
+ * keyboard-first reader knows the shortcut.
309
+ *
310
+ * @returns {string}
311
+ */
312
+ export function renderSearchButton() {
313
+ return `<button type="button" class="rcf-search-btn" data-rcf-search
314
+ aria-label="Find any document or AC (Cmd/Ctrl+F)"
315
+ title="Find any document or AC (Cmd/Ctrl+F)">
316
+ <svg class="rcf-search-icon" aria-hidden="true" viewBox="0 0 20 20" width="16" height="16">
317
+ <circle cx="9" cy="9" r="6" fill="none" stroke="currentColor" stroke-width="1.8"/>
318
+ <path d="M13.5 13.5l4 4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
319
+ </svg>
320
+ <span class="rcf-search-label">Find</span>
321
+ <span class="rcf-search-kbd" aria-hidden="true"><kbd>Ctrl</kbd>+<kbd>F</kbd></span>
322
+ </button>`;
323
+ }