rcf-lite 0.28.1 → 0.28.4

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 (219) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/bin/rcf.js +4 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/package.json +1 -1
  5. package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
  6. package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
  7. package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
  8. package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
  9. package/rcf/code-nodes/cn-602.json +3 -2
  10. package/rcf/code-nodes/cn-700.json +23 -0
  11. package/rcf/code-nodes/cn-701.json +14 -0
  12. package/rcf/code-nodes/cn-702.json +14 -0
  13. package/rcf/code-nodes/cn-703.json +16 -0
  14. package/rcf/code-nodes/cn-720.json +15 -0
  15. package/rcf/code-nodes/cn-721.json +15 -0
  16. package/rcf/code-nodes/cn-722.json +15 -0
  17. package/rcf/code-nodes/cn-723.json +18 -0
  18. package/rcf/code-nodes/cn-724.json +14 -0
  19. package/rcf/code-nodes/cn-725.json +21 -0
  20. package/rcf/code-nodes/cn-726.json +17 -0
  21. package/rcf/code-nodes/cn-727.json +14 -0
  22. package/rcf/code-nodes/cn-728.json +21 -0
  23. package/rcf/code-nodes/cn-729.json +18 -0
  24. package/rcf/code-nodes/cn-730.json +14 -0
  25. package/rcf/fbs/fbs-181.json +3 -2
  26. package/rcf/fbs/fbs-190.json +37 -0
  27. package/rcf/fbs/fbs-191.json +34 -0
  28. package/rcf/fbs/fbs-192.json +17 -0
  29. package/rcf/fbs/fbs-193.json +37 -0
  30. package/rcf/fbs/fbs-194.json +35 -0
  31. package/rcf/fbs/fbs-195.json +37 -0
  32. package/rcf/fbs/fbs-196.json +37 -0
  33. package/rcf/fbs/fbs-197.json +41 -0
  34. package/rcf/requirements/req-001.json +2 -1
  35. package/rcf/requirements/req-002.json +2 -1
  36. package/rcf/requirements/req-003.json +2 -1
  37. package/rcf/requirements/req-004.json +2 -1
  38. package/rcf/requirements/req-005.json +2 -1
  39. package/rcf/requirements/req-006.json +2 -1
  40. package/rcf/requirements/req-007.json +2 -1
  41. package/rcf/requirements/req-008.json +2 -1
  42. package/rcf/requirements/req-009.json +2 -1
  43. package/rcf/requirements/req-010.json +5 -2
  44. package/rcf/requirements/req-011.json +2 -1
  45. package/rcf/requirements/req-012.json +2 -1
  46. package/rcf/requirements/req-013.json +2 -1
  47. package/rcf/requirements/req-014.json +2 -1
  48. package/rcf/requirements/req-015.json +2 -1
  49. package/rcf/requirements/req-016.json +5 -2
  50. package/rcf/requirements/req-017.json +6 -2
  51. package/rcf/requirements/req-018.json +5 -2
  52. package/rcf/requirements/req-019.json +5 -2
  53. package/rcf/requirements/req-020.json +5 -2
  54. package/rcf/requirements/req-021.json +5 -2
  55. package/rcf/requirements/req-022.json +5 -2
  56. package/rcf/requirements/req-023.json +6 -4
  57. package/rcf/requirements/req-024.json +5 -2
  58. package/rcf/requirements/req-025.json +5 -2
  59. package/rcf/requirements/req-026.json +5 -2
  60. package/rcf/requirements/req-027.json +5 -2
  61. package/rcf/requirements/req-028.json +5 -2
  62. package/rcf/requirements/req-029.json +5 -2
  63. package/rcf/requirements/req-030.json +5 -2
  64. package/rcf/requirements/req-040.json +5 -2
  65. package/rcf/requirements/req-041.json +5 -2
  66. package/rcf/requirements/req-042.json +5 -2
  67. package/rcf/requirements/req-043.json +5 -2
  68. package/rcf/requirements/req-050.json +5 -2
  69. package/rcf/requirements/req-051.json +5 -2
  70. package/rcf/requirements/req-052.json +5 -2
  71. package/rcf/requirements/req-053.json +5 -2
  72. package/rcf/requirements/req-054.json +5 -2
  73. package/rcf/requirements/req-060.json +5 -2
  74. package/rcf/requirements/req-061.json +5 -2
  75. package/rcf/requirements/req-062.json +5 -2
  76. package/rcf/requirements/req-063.json +5 -2
  77. package/rcf/requirements/req-070.json +5 -2
  78. package/rcf/requirements/req-071.json +5 -2
  79. package/rcf/requirements/req-072.json +5 -2
  80. package/rcf/requirements/req-073.json +5 -2
  81. package/rcf/requirements/req-074.json +5 -2
  82. package/rcf/requirements/req-075.json +5 -2
  83. package/rcf/requirements/req-076.json +5 -2
  84. package/rcf/requirements/req-077.json +5 -2
  85. package/rcf/requirements/req-080.json +5 -2
  86. package/rcf/requirements/req-081.json +5 -2
  87. package/rcf/requirements/req-082.json +5 -2
  88. package/rcf/requirements/req-083.json +5 -2
  89. package/rcf/requirements/req-084.json +5 -2
  90. package/rcf/requirements/req-085.json +5 -2
  91. package/rcf/requirements/req-086.json +5 -2
  92. package/rcf/requirements/req-090.json +5 -2
  93. package/rcf/requirements/req-091.json +5 -2
  94. package/rcf/requirements/req-092.json +5 -2
  95. package/rcf/requirements/req-093.json +5 -2
  96. package/rcf/requirements/req-094.json +5 -2
  97. package/rcf/requirements/req-100.json +5 -2
  98. package/rcf/requirements/req-101.json +5 -2
  99. package/rcf/requirements/req-102.json +5 -2
  100. package/rcf/requirements/req-103.json +5 -2
  101. package/rcf/requirements/req-104.json +5 -2
  102. package/rcf/requirements/req-110.json +5 -2
  103. package/rcf/requirements/req-111.json +5 -2
  104. package/rcf/requirements/req-112.json +5 -2
  105. package/rcf/requirements/req-113.json +5 -2
  106. package/rcf/requirements/req-114.json +5 -2
  107. package/rcf/requirements/req-115.json +5 -2
  108. package/rcf/requirements/req-120.json +5 -2
  109. package/rcf/requirements/req-121.json +5 -2
  110. package/rcf/requirements/req-122.json +5 -2
  111. package/rcf/requirements/req-123.json +5 -2
  112. package/rcf/requirements/req-124.json +5 -2
  113. package/rcf/requirements/req-125.json +5 -2
  114. package/rcf/requirements/req-130.json +6 -2
  115. package/rcf/requirements/req-131.json +6 -2
  116. package/rcf/requirements/req-132.json +6 -2
  117. package/rcf/requirements/req-133.json +6 -2
  118. package/rcf/requirements/req-134.json +6 -2
  119. package/rcf/requirements/req-140.json +5 -2
  120. package/rcf/requirements/req-145.json +5 -2
  121. package/rcf/requirements/req-150.json +6 -2
  122. package/rcf/requirements/req-151.json +6 -2
  123. package/rcf/requirements/req-152.json +6 -2
  124. package/rcf/requirements/req-153.json +6 -2
  125. package/rcf/requirements/req-154.json +5 -2
  126. package/rcf/requirements/req-155.json +4 -1
  127. package/rcf/requirements/req-156.json +4 -1
  128. package/rcf/requirements/req-157.json +4 -1
  129. package/rcf/requirements/req-158.json +4 -1
  130. package/rcf/requirements/req-159.json +4 -1
  131. package/rcf/requirements/req-160.json +4 -1
  132. package/rcf/requirements/req-161.json +4 -1
  133. package/rcf/requirements/req-162.json +4 -1
  134. package/rcf/requirements/req-163.json +5 -2
  135. package/rcf/requirements/req-164.json +5 -2
  136. package/rcf/requirements/req-165.json +5 -2
  137. package/rcf/requirements/req-170.json +33 -0
  138. package/rcf/requirements/req-171.json +44 -0
  139. package/rcf/requirements/req-172.json +25 -0
  140. package/rcf/requirements/req-173.json +25 -0
  141. package/rcf/requirements/req-174.json +25 -0
  142. package/rcf/requirements/req-175.json +25 -0
  143. package/rcf/tacs/tac-4103-feedback-redact.json +2 -2
  144. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  145. package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
  146. package/rcf/tacs/tac-4122-define-gates.json +25 -0
  147. package/rcf/tacs/tac-4123-define-readiness.json +25 -0
  148. package/rcf/test-suites/ts-188.json +10 -2
  149. package/rcf/test-suites/ts-191.json +19 -5
  150. package/rcf/test-suites/ts-204.json +30 -0
  151. package/rcf/test-suites/ts-205.json +22 -0
  152. package/rcf/test-suites/ts-206.json +22 -0
  153. package/rcf/test-suites/ts-207.json +22 -0
  154. package/rcf/test-suites/ts-208.json +22 -0
  155. package/rcf/test-suites/ts-209.json +22 -0
  156. package/rcf/test-suites/ts-210.json +22 -0
  157. package/rcf/test-suites/ts-211.json +22 -0
  158. package/rcf/test-suites/ts-212.json +22 -0
  159. package/rcf/test-suites/ts-213.json +50 -0
  160. package/rcf/test-suites/ts-214.json +74 -0
  161. package/rcf/test-suites/ts-215.json +67 -0
  162. package/rcf/test-suites/ts-216.json +78 -0
  163. package/rcf/test-suites/ts-217.json +62 -0
  164. package/rcf/user-stories/us-15601.json +10 -1
  165. package/rcf/user-stories/us-15701.json +4 -4
  166. package/rcf/user-stories/us-17001.json +27 -0
  167. package/rcf/user-stories/us-17002.json +22 -0
  168. package/rcf/user-stories/us-17003.json +22 -0
  169. package/rcf/user-stories/us-17004.json +22 -0
  170. package/rcf/user-stories/us-17005.json +22 -0
  171. package/rcf/user-stories/us-17006.json +21 -0
  172. package/rcf/user-stories/us-17007.json +21 -0
  173. package/rcf/user-stories/us-17008.json +22 -0
  174. package/rcf/user-stories/us-17101.json +22 -0
  175. package/rcf/user-stories/us-17102.json +22 -0
  176. package/rcf/user-stories/us-17201.json +38 -0
  177. package/rcf/user-stories/us-17301.json +38 -0
  178. package/rcf/user-stories/us-17401.json +58 -0
  179. package/rcf/user-stories/us-17501.json +48 -0
  180. package/releases/releases.yaml +31 -1
  181. package/src/browser-verify/manifest-writer.js +9 -5
  182. package/src/cli/browser-verify.js +9 -1
  183. package/src/cli/feedback.js +316 -32
  184. package/src/cli/help.js +11 -0
  185. package/src/cli/intake.js +25 -4
  186. package/src/cli/ledger.js +399 -0
  187. package/src/cli/preflight.js +24 -3
  188. package/src/cli/readiness.js +280 -0
  189. package/src/cli/review.js +9 -1
  190. package/src/cli/ui-baseline.js +14 -1
  191. package/src/core/store/index.js +5 -0
  192. package/src/core/store/validate-composed-record.js +95 -0
  193. package/src/core/store/writer.js +86 -6
  194. package/src/define/freeze-record.js +232 -0
  195. package/src/define/ledgers.js +513 -0
  196. package/src/feedback/gh.js +27 -1
  197. package/src/feedback/redact-allowlist.json +7 -0
  198. package/src/feedback/redact.js +126 -11
  199. package/src/feedback/render.js +34 -12
  200. package/src/intake/manifest-writer.js +6 -3
  201. package/src/intake/orchestrator.js +44 -7
  202. package/src/preflight/manifest-writer.js +11 -11
  203. package/src/query/delta.js +276 -0
  204. package/src/query/gates.js +832 -0
  205. package/src/query/index.js +36 -0
  206. package/src/query/readiness.js +333 -0
  207. package/src/req-baseline/sweep.js +34 -5
  208. package/src/server/index.js +37 -0
  209. package/src/server/routes.js +65 -3
  210. package/src/ui-baseline/manifest-writer.js +11 -10
  211. package/src/view/doc-renderers/helpers.js +39 -2
  212. package/src/view/doc-renderers/req.js +10 -2
  213. package/src/view/doc-renderers/tad.js +67 -6
  214. package/src/view/doc-renderers/user-story.js +10 -5
  215. package/src/view/html-page.js +347 -28
  216. package/src/view/index.js +14 -1
  217. package/src/view/live-client.js +22 -2
  218. package/src/view/product-map.js +751 -0
  219. package/src/view/style.css +383 -0
@@ -77,6 +77,43 @@ export function fieldList(label, items) {
77
77
  return `<section class="field-list"><h4>${escapeHtml(label)}</h4><ul>${li}</ul></section>`;
78
78
  }
79
79
 
80
+ /**
81
+ * 0.28.2 (issue #235): render an array-of-object field as a HTML
82
+ * table with the schema-known columns. Empty columns are suppressed so
83
+ * schema-optional fields do not surface as blank columns. Every cell
84
+ * is escaped. Used by the TAD renderer for the three structured
85
+ * fields the TAD schema names (dataStores, coreEntities,
86
+ * externalSystems) so a review-surface reader sees a legible table
87
+ * instead of a single-line `JSON.stringify` dump.
88
+ *
89
+ * @param {string} label section heading
90
+ * @param {object[]} rows array of row objects (dropped when empty)
91
+ * @param {Array<{ key: string, label?: string }>} columns
92
+ * @returns {string}
93
+ */
94
+ export function fieldObjectTable(label, rows, columns) {
95
+ if (!Array.isArray(rows) || rows.length === 0) return '';
96
+ if (!Array.isArray(columns) || columns.length === 0) return '';
97
+ // Drop columns that are empty across every row so a schema-optional
98
+ // field (`purpose`, `description`, `protocol`) does not surface as
99
+ // a blank column.
100
+ const usedColumns = columns.filter((col) => rows.some((r) => {
101
+ const v = r?.[col.key];
102
+ return v !== undefined && v !== null && String(v).length > 0;
103
+ }));
104
+ if (usedColumns.length === 0) return '';
105
+ const thead = usedColumns.map((c) => `<th>${escapeHtml(c.label ?? c.key)}</th>`).join('');
106
+ const tbody = rows.map((r) => {
107
+ const cells = usedColumns.map((c) => {
108
+ const v = r?.[c.key];
109
+ const text = v === undefined || v === null ? '' : String(v);
110
+ return `<td>${escapeHtml(text)}</td>`;
111
+ }).join('');
112
+ return `<tr>${cells}</tr>`;
113
+ }).join('');
114
+ return `<section class="field-list"><h4>${escapeHtml(label)}</h4><table class="field-table"><thead><tr>${thead}</tr></thead><tbody>${tbody}</tbody></table></section>`;
115
+ }
116
+
80
117
  /**
81
118
  * Render the "Show raw JSON" disclosure block for a document.
82
119
  *
@@ -91,9 +128,9 @@ export function fieldList(label, items) {
91
128
  * @param {string} parentDocId - the enclosing doc's display id (e.g. "REQ-002")
92
129
  * @returns {string}
93
130
  */
94
- export function rawJsonDisclosure(raw, doc, parentDocId) {
131
+ export function rawJsonDisclosure(raw, doc, parentDocId, idPrefix) {
95
132
  const json = raw ?? JSON.stringify(doc, null, 2);
96
- const rawId = `${parentDocId ?? 'doc'}::raw`;
133
+ const rawId = `${idPrefix ?? ''}${parentDocId ?? 'doc'}::raw`;
97
134
  return `<details class="raw-json" data-doc-id="${escapeHtml(rawId)}"><summary>Show raw JSON</summary><pre>${escapeHtml(json)}</pre></details>`;
98
135
  }
99
136
 
@@ -18,11 +18,19 @@ import {
18
18
  * @param {string|undefined} ctx.raw
19
19
  * @param {import('#core/errors').RcfError[]} [ctx.errors]
20
20
  * @param {string|undefined} [ctx.subdiagram]
21
+ * @param {string|undefined} [ctx.idPrefix] - prefix prepended to every emitted
22
+ * id/anchor so the same REQ can be rendered under multiple Product Map
23
+ * buckets without duplicating ids. Empty when rendered from the
24
+ * Requirements tab so its anchors stay canonical.
25
+ * @param {boolean} [ctx.suppressRawJson] - when true, the raw-JSON disclosure
26
+ * is not emitted (Product Map compact cards; the disclosure stays on
27
+ * the Requirements tab). AC-17007-1 payload weight.
21
28
  * @returns {string}
22
29
  */
23
30
  export function renderReq(req, ctx) {
24
31
  if (!req) return '';
25
- const anchor = anchorIdFor(req.reqId ?? 'REQ');
32
+ const prefix = ctx.idPrefix ?? '';
33
+ const anchor = `${prefix}${anchorIdFor(req.reqId ?? 'REQ')}`;
26
34
  const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
27
35
  const subdiagram = ctx.subdiagram
28
36
  ? `<section class="subdiagram"><h4>Slice diagram</h4><pre class="mermaid">${escapeHtml(ctx.subdiagram)}</pre></section>`
@@ -38,6 +46,6 @@ export function renderReq(req, ctx) {
38
46
  ${fieldPara('Rationale', req.rationale)}
39
47
  ${fieldList('Tags', req.tags)}
40
48
  ${subdiagram}
41
- ${rawJsonDisclosure(ctx.raw, req, req.reqId)}
49
+ ${ctx.suppressRawJson ? '' : rawJsonDisclosure(ctx.raw, req, req.reqId, prefix)}
42
50
  </article>`.trim();
43
51
  }
@@ -9,10 +9,34 @@ import {
9
9
  docLinkList,
10
10
  escapeHtml,
11
11
  fieldList,
12
+ fieldObjectTable,
12
13
  fieldPara,
13
14
  rawJsonDisclosure,
14
15
  } from './helpers.js';
15
16
 
17
+ // 0.28.2 (issue #235): structured fields the TAD schema explicitly
18
+ // types as arrays of objects. Each entry maps a field-key to the
19
+ // schema-documented column layout so `renderOptionalSections` can
20
+ // pick a table renderer over the raw-JSON dump the pre-fix code
21
+ // emitted. Columns follow @stravica-ai/rcf-schemas@0.6.3
22
+ // schemas/tad.schema.json.
23
+ const TAD_STRUCTURED_FIELDS = {
24
+ dataStores: [
25
+ { key: 'name', label: 'Name' },
26
+ { key: 'kind', label: 'Kind' },
27
+ { key: 'purpose', label: 'Purpose' },
28
+ ],
29
+ coreEntities: [
30
+ { key: 'name', label: 'Name' },
31
+ { key: 'description', label: 'Description' },
32
+ ],
33
+ externalSystems: [
34
+ { key: 'name', label: 'Name' },
35
+ { key: 'purpose', label: 'Purpose' },
36
+ { key: 'protocol', label: 'Protocol' },
37
+ ],
38
+ };
39
+
16
40
  /**
17
41
  * @param {object} tad
18
42
  * @param {object} ctx
@@ -63,12 +87,49 @@ function renderOptionalSections(tad) {
63
87
  for (const [key, label] of Object.entries(named)) {
64
88
  const section = tad[key];
65
89
  if (!section || typeof section !== 'object') continue;
66
- const rows = Object.entries(section).map(([k, v]) => {
67
- if (typeof v === 'string') return `<dt>${escapeHtml(k)}</dt><dd>${escapeHtml(v)}</dd>`;
68
- if (Array.isArray(v)) return `<dt>${escapeHtml(k)}</dt><dd>${v.map((x) => `<code>${escapeHtml(typeof x === 'object' ? JSON.stringify(x) : x)}</code>`).join(', ')}</dd>`;
69
- return `<dt>${escapeHtml(k)}</dt><dd><pre>${escapeHtml(JSON.stringify(v, null, 2))}</pre></dd>`;
70
- }).join('\n');
71
- out.push(`<section class="field-list"><h4>${escapeHtml(label)}</h4><dl>${rows}</dl></section>`);
90
+ // 0.28.2 (issue #235): pull the schema-known structured fields
91
+ // (dataStores, coreEntities, externalSystems) out of the generic
92
+ // dl loop and render them as tables. The remaining scalar and
93
+ // array-of-string fields keep their dl treatment.
94
+ const structuredBlocks = [];
95
+ const dlEntries = [];
96
+ for (const [k, v] of Object.entries(section)) {
97
+ if (TAD_STRUCTURED_FIELDS[k] && Array.isArray(v) && v.length > 0 && v.every((x) => x && typeof x === 'object')) {
98
+ const table = fieldObjectTable(prettyStructuredLabel(k), v, TAD_STRUCTURED_FIELDS[k]);
99
+ if (table) structuredBlocks.push(table);
100
+ continue;
101
+ }
102
+ if (typeof v === 'string') {
103
+ dlEntries.push(`<dt>${escapeHtml(k)}</dt><dd>${escapeHtml(v)}</dd>`);
104
+ continue;
105
+ }
106
+ if (Array.isArray(v)) {
107
+ // Array-of-string keeps the existing inline shape. An
108
+ // unknown array-of-object surface falls through to a nested
109
+ // disclosure so it stays legible even when the schema grows
110
+ // a new field before this renderer knows about it.
111
+ if (v.every((x) => typeof x !== 'object' || x === null)) {
112
+ dlEntries.push(`<dt>${escapeHtml(k)}</dt><dd>${v.map((x) => `<code>${escapeHtml(typeof x === 'object' ? JSON.stringify(x) : x)}</code>`).join(', ')}</dd>`);
113
+ } else {
114
+ const items = v.map((x) => `<li><pre>${escapeHtml(JSON.stringify(x, null, 2))}</pre></li>`).join('');
115
+ dlEntries.push(`<dt>${escapeHtml(k)}</dt><dd><ul>${items}</ul></dd>`);
116
+ }
117
+ continue;
118
+ }
119
+ dlEntries.push(`<dt>${escapeHtml(k)}</dt><dd><pre>${escapeHtml(JSON.stringify(v, null, 2))}</pre></dd>`);
120
+ }
121
+ const dl = dlEntries.length > 0 ? `<dl>${dlEntries.join('\n')}</dl>` : '';
122
+ const structured = structuredBlocks.join('\n');
123
+ out.push(`<section class="field-list"><h4>${escapeHtml(label)}</h4>${dl}${structured}</section>`);
72
124
  }
73
125
  return out.join('\n');
74
126
  }
127
+
128
+ function prettyStructuredLabel(key) {
129
+ const map = {
130
+ dataStores: 'Data stores',
131
+ coreEntities: 'Core entities',
132
+ externalSystems: 'External systems',
133
+ };
134
+ return map[key] ?? key;
135
+ }
@@ -18,13 +18,18 @@ import {
18
18
  * @param {string|undefined} ctx.raw
19
19
  * @param {import('#core/errors').RcfError[]} [ctx.errors]
20
20
  * @param {Map<string, object[]>} [ctx.fbsByAcId]
21
+ * @param {string|undefined} [ctx.idPrefix] - prefix prepended to every emitted
22
+ * id/anchor (article, AC li items, raw-JSON disclosure) so a US rendered
23
+ * under multiple Product Map buckets does not collide with the canonical
24
+ * Requirements-tab anchors. Empty from the Requirements tab.
21
25
  * @returns {string}
22
26
  */
23
27
  export function renderUserStory(us, ctx) {
24
28
  if (!us) return '';
25
- const anchor = anchorIdFor(us.usId ?? 'US');
29
+ const prefix = ctx.idPrefix ?? '';
30
+ const anchor = `${prefix}${anchorIdFor(us.usId ?? 'US')}`;
26
31
  const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
27
- const acItems = (us.acceptanceCriteria ?? []).map((ac) => renderAcItem(ac, ctx)).join('\n');
32
+ const acItems = (us.acceptanceCriteria ?? []).map((ac) => renderAcItem(ac, ctx, prefix)).join('\n');
28
33
  return `
29
34
  <article id="${anchor}" class="doc doc-us">
30
35
  <h3>${escapeHtml(us.usId ?? 'US')} - ${escapeHtml(us.title ?? '')}</h3>
@@ -40,13 +45,13 @@ export function renderUserStory(us, ctx) {
40
45
  ${acItems}
41
46
  </ul>
42
47
  </section>
43
- ${rawJsonDisclosure(ctx.raw, us, us.usId)}
48
+ ${ctx.suppressRawJson ? '' : rawJsonDisclosure(ctx.raw, us, us.usId, prefix)}
44
49
  </article>`.trim();
45
50
  }
46
51
 
47
- function renderAcItem(ac, ctx) {
52
+ function renderAcItem(ac, ctx, prefix) {
48
53
  const acId = ac.id ?? 'AC';
49
- const acAnchor = anchorIdFor(acId);
54
+ const acAnchor = `${prefix ?? ''}${anchorIdFor(acId)}`;
50
55
  const fbsList = ctx.fbsByAcId?.get(acId) ?? [];
51
56
  const coveredBy = fbsList.length === 0
52
57
  ? '<em>not yet delivered by any FBS</em>'
@@ -27,6 +27,7 @@ import {
27
27
  } from './doc-renderers/index.js';
28
28
  import { detailsWrap, escapeHtml } from './doc-renderers/helpers.js';
29
29
  import { allRequirementSubdiagrams } from './mermaid-diagram.js';
30
+ import { renderProductMapPanel } from './product-map.js';
30
31
 
31
32
  // Inline SVG favicon: the Stravica kit mark (deep-navy tile with the
32
33
  // ribbon symbol in Stravica blue), taken from the estate brand kit at
@@ -74,6 +75,7 @@ export function renderPage(model) {
74
75
  <p class="subtitle">RCF review surface</p>
75
76
  <nav class="tabs" role="tablist" aria-label="Document sections">
76
77
  <button type="button" role="tab" data-tab="overview" aria-selected="true" aria-controls="tab-overview">Overview</button>
78
+ <button type="button" role="tab" data-tab="product-map" aria-selected="false" aria-controls="tab-product-map">Product Map</button>
77
79
  <button type="button" role="tab" data-tab="requirements" aria-selected="false" aria-controls="tab-requirements">Requirements</button>
78
80
  <button type="button" role="tab" data-tab="architecture" aria-selected="false" aria-controls="tab-architecture">Architecture</button>
79
81
  <button type="button" role="tab" data-tab="build" aria-selected="false" aria-controls="tab-build">Build sequence</button>
@@ -119,6 +121,7 @@ export function renderContent(model) {
119
121
  const requirementsPanel = renderRequirementsPanel(model, subdiagrams);
120
122
  const architecturePanel = renderArchitecturePanel(model);
121
123
  const buildPanel = renderBuildPanel(model);
124
+ const productMapPanel = renderProductMapPanel(model);
122
125
 
123
126
  const errorBanner = renderErrorBanner(model.errors ?? []);
124
127
 
@@ -129,6 +132,10 @@ export function renderContent(model) {
129
132
  ${prdSection}
130
133
  </div>
131
134
  </section>
135
+ <section id="tab-product-map" role="tabpanel" hidden>
136
+ <h2 class="tab-heading">Product Map</h2>
137
+ ${productMapPanel}
138
+ </section>
132
139
  <section id="tab-requirements" role="tabpanel" hidden>
133
140
  <h2 class="tab-heading">Requirements</h2>
134
141
  ${requirementsPanel}
@@ -320,14 +327,19 @@ function renderErrorBanner(errors) {
320
327
  function inlineScript() {
321
328
  return `
322
329
  (function () {
323
- var TABS = ['overview', 'requirements', 'architecture', 'build'];
330
+ var TABS = ['overview', 'requirements', 'architecture', 'build', 'product-map'];
331
+ var PM_GROUPS = ['shape', 'component', 'trace', 'capability', 'blueprint'];
332
+ var PM_STATUSES = ['all', 'draft', 'review', 'needsRevision', 'approved', 'superseded'];
333
+ // Partial-render cache: grouping -> HTML string. First fetch fills it,
334
+ // subsequent switches use the cache.
335
+ var pmPartialCache = {};
336
+ // Track whether an SSE swap has invalidated the cache since the last
337
+ // successful hydration - the live-client sets window.__rcfPmDirty
338
+ // after each swap, and we clear the cache on the next activation.
339
+ function pmCacheKey(group) { return group; }
324
340
 
325
341
  function initMermaid() {
326
342
  if (typeof window.mermaid !== 'undefined') {
327
- // startOnLoad: false. Mermaid can't lay out diagrams inside a
328
- // display:none tabpanel (getBoundingClientRect is 0x0 -> NaN
329
- // transforms). We run each tab's diagrams on activation instead;
330
- // runMermaidIn is idempotent via the data-processed attribute.
331
343
  window.mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
332
344
  }
333
345
  }
@@ -338,13 +350,11 @@ function inlineScript() {
338
350
  if (pending.length === 0) return;
339
351
  try {
340
352
  window.mermaid.run({ nodes: Array.prototype.slice.call(pending) });
341
- } catch (e) {
342
- // Mermaid.run may throw on init errors; do not block the UI.
343
- }
353
+ } catch (e) { /* swallow */ }
344
354
  }
345
355
 
346
356
  function tabButtons() {
347
- return Array.prototype.slice.call(document.querySelectorAll('[role="tab"]'));
357
+ return Array.prototype.slice.call(document.querySelectorAll('nav.tabs [role="tab"]'));
348
358
  }
349
359
 
350
360
  function panelFor(name) {
@@ -399,32 +409,326 @@ function inlineScript() {
399
409
  return document.querySelector('[data-doc-id="' + id.replace(/"/g, '\\\\"') + '"]');
400
410
  }
401
411
 
402
- function resolveHash(hash) {
403
- if (!hash) {
404
- activateTab('overview');
412
+ function parseHashParams(raw) {
413
+ var out = {};
414
+ if (!raw) return out;
415
+ var parts = raw.split('&');
416
+ for (var i = 0; i < parts.length; i += 1) {
417
+ var eq = parts[i].indexOf('=');
418
+ if (eq === -1) continue;
419
+ var k = parts[i].slice(0, eq);
420
+ var v = parts[i].slice(eq + 1);
421
+ if (k) out[k] = v;
422
+ }
423
+ return out;
424
+ }
425
+
426
+ function pmGroupPanel(name) {
427
+ return document.getElementById('pm-group-' + name);
428
+ }
429
+
430
+ function hydrateLazyGroup(name, cb) {
431
+ var panel = pmGroupPanel(name);
432
+ if (!panel) { if (cb) cb(false); return; }
433
+ if (panel.getAttribute('data-pm-lazy') !== 'true' && !pmPartialCache[pmCacheKey(name)]) {
434
+ if (cb) cb(true);
435
+ return;
436
+ }
437
+ if (window.__rcfPmDirty) {
438
+ pmPartialCache = {};
439
+ window.__rcfPmDirty = false;
440
+ }
441
+ var cached = pmPartialCache[pmCacheKey(name)];
442
+ var heading = panel.querySelector('.pm-group-heading');
443
+ var headingHtml = heading ? heading.outerHTML : '';
444
+ if (cached) {
445
+ panel.innerHTML = headingHtml + cached;
446
+ panel.removeAttribute('data-pm-lazy');
447
+ wireProductMapWithin(panel);
448
+ if (cb) cb(true);
405
449
  return;
406
450
  }
451
+ // Show a lightweight loading state.
452
+ panel.setAttribute('data-pm-loading', 'true');
453
+ var xhr = new XMLHttpRequest();
454
+ xhr.open('GET', '/product-map/' + name, true);
455
+ xhr.onreadystatechange = function () {
456
+ if (xhr.readyState !== 4) return;
457
+ panel.removeAttribute('data-pm-loading');
458
+ if (xhr.status >= 200 && xhr.status < 300) {
459
+ pmPartialCache[pmCacheKey(name)] = xhr.responseText;
460
+ panel.innerHTML = headingHtml + xhr.responseText;
461
+ panel.removeAttribute('data-pm-lazy');
462
+ wireProductMapWithin(panel);
463
+ if (cb) cb(true);
464
+ } else {
465
+ panel.setAttribute('data-pm-lazy', 'true');
466
+ if (cb) cb(false);
467
+ }
468
+ };
469
+ xhr.send();
470
+ }
471
+
472
+ function activatePmGroup(name, opts) {
473
+ if (PM_GROUPS.indexOf(name) === -1) return false;
474
+ var buttons = document.querySelectorAll('.pm-group-btn');
475
+ for (var i = 0; i < buttons.length; i += 1) {
476
+ var btn = buttons[i];
477
+ var isTarget = btn.getAttribute('data-pm-group') === name;
478
+ btn.setAttribute('aria-selected', isTarget ? 'true' : 'false');
479
+ }
480
+ var panels = document.querySelectorAll('.pm-group');
481
+ for (var j = 0; j < panels.length; j += 1) {
482
+ var p = panels[j];
483
+ if (p.getAttribute('data-pm-group') === name) {
484
+ p.removeAttribute('hidden');
485
+ } else {
486
+ p.setAttribute('hidden', '');
487
+ }
488
+ }
489
+ var target = pmGroupPanel(name);
490
+ if (target && target.getAttribute('data-pm-lazy') === 'true') {
491
+ hydrateLazyGroup(name, function () {
492
+ if (opts && opts.after) opts.after();
493
+ applyPmStatusFilter(currentStatus());
494
+ });
495
+ } else {
496
+ if (opts && opts.after) opts.after();
497
+ applyPmStatusFilter(currentStatus());
498
+ }
499
+ return true;
500
+ }
501
+
502
+ function currentGroup() {
503
+ var sel = document.querySelector('.pm-group-btn[aria-selected="true"]');
504
+ return sel ? sel.getAttribute('data-pm-group') : 'shape';
505
+ }
506
+
507
+ function currentStatus() {
508
+ var s = document.querySelector('.pm-status-select');
509
+ return s ? s.value : 'all';
510
+ }
511
+
512
+ function currentOpenBuckets() {
513
+ var group = currentGroup();
514
+ var panel = pmGroupPanel(group);
515
+ if (!panel) return [];
516
+ var nodes = panel.querySelectorAll('details.pm-bucket[open]');
517
+ var out = [];
518
+ for (var i = 0; i < nodes.length; i += 1) {
519
+ var id = nodes[i].getAttribute('data-pm-bucket-id');
520
+ if (id) out.push(id);
521
+ }
522
+ return out;
523
+ }
524
+
525
+ function applyPmStatusFilter(status) {
526
+ if (PM_STATUSES.indexOf(status) === -1) return false;
527
+ var select = document.querySelector('.pm-status-select');
528
+ if (select) select.value = status;
529
+ var reqCards = document.querySelectorAll('#tab-product-map [data-req-status]');
530
+ for (var i = 0; i < reqCards.length; i += 1) {
531
+ var card = reqCards[i];
532
+ var s = card.getAttribute('data-req-status') || '';
533
+ if (status === 'all' || s === status) {
534
+ card.removeAttribute('hidden');
535
+ } else {
536
+ card.setAttribute('hidden', '');
537
+ }
538
+ }
539
+ // Per AC-17002-1: empty buckets under the filter are HIDDEN by
540
+ // default with a "N empty" summary line to reveal them. Also keep
541
+ // the legacy in-bucket note for tests / no-JS accessibility.
542
+ var buckets = document.querySelectorAll('#tab-product-map .pm-group:not([hidden]) details.pm-bucket');
543
+ var emptyCount = 0;
544
+ for (var k = 0; k < buckets.length; k += 1) {
545
+ var bk = buckets[k];
546
+ var visible = bk.querySelectorAll('[data-req-status]:not([hidden])');
547
+ var bodyEmpty = bk.querySelector('.pm-bucket-body') && bk.querySelector('.pm-bucket-body').children.length === 0;
548
+ var isEmpty = visible.length === 0 && !bodyEmpty; // lazy-empty buckets don't count
549
+ var body = bk.querySelector('.pm-bucket-body');
550
+ var emptyNote = body ? body.querySelector('.pm-bucket-empty-filter') : null;
551
+ if (isEmpty && status !== 'all') {
552
+ if (body && !emptyNote) {
553
+ emptyNote = document.createElement('p');
554
+ emptyNote.className = 'pm-bucket-empty pm-bucket-empty-filter';
555
+ emptyNote.innerHTML = '<em>0 requirements match this filter.</em>';
556
+ body.appendChild(emptyNote);
557
+ }
558
+ bk.setAttribute('data-pm-empty-under-filter', 'true');
559
+ emptyCount += 1;
560
+ } else {
561
+ if (emptyNote) emptyNote.parentNode.removeChild(emptyNote);
562
+ bk.removeAttribute('data-pm-empty-under-filter');
563
+ }
564
+ }
565
+ // Update the rollup summary.
566
+ var activePanel = document.querySelector('#tab-product-map .pm-group:not([hidden])');
567
+ if (activePanel) {
568
+ var roll = activePanel.querySelector('.pm-empty-roll');
569
+ if (roll) {
570
+ var showBtn = roll.querySelector('.pm-empty-count');
571
+ if (emptyCount > 0 && status !== 'all') {
572
+ if (showBtn) showBtn.textContent = String(emptyCount);
573
+ roll.removeAttribute('hidden');
574
+ activePanel.classList.add('pm-hide-empty');
575
+ } else {
576
+ roll.setAttribute('hidden', '');
577
+ activePanel.classList.remove('pm-hide-empty');
578
+ }
579
+ }
580
+ }
581
+ return true;
582
+ }
583
+
584
+ function updatePmHash() {
585
+ if (!window.history || typeof window.history.pushState !== 'function') return;
586
+ var group = currentGroup();
587
+ var status = currentStatus();
588
+ var open = currentOpenBuckets();
589
+ var next = '#tab=product-map&group=' + group + '&status=' + status;
590
+ if (open.length > 0) next += '&open=' + encodeURIComponent(open.join(','));
591
+ if (window.location.hash === next) return;
592
+ window.history.pushState(null, '', next);
593
+ }
594
+
595
+ function openPmBuckets(ids) {
596
+ if (!ids || ids.length === 0) return;
597
+ var group = currentGroup();
598
+ var panel = pmGroupPanel(group);
599
+ if (!panel) return;
600
+ for (var i = 0; i < ids.length; i += 1) {
601
+ var raw = ids[i];
602
+ if (!raw) continue;
603
+ var node = panel.querySelector('details.pm-bucket[data-pm-bucket-id="' + raw.replace(/"/g, '\\"') + '"]');
604
+ if (node) node.open = true;
605
+ }
606
+ }
607
+
608
+ function wireProductMapWithin(root) {
609
+ root = root || document;
610
+ // Bucket toggle -> push hash on user toggle. The 'toggle' event
611
+ // fires for both open and close, from user action and from our own
612
+ // scripting; guard against feedback via a dirty flag.
613
+ var buckets = root.querySelectorAll('details.pm-bucket');
614
+ for (var i = 0; i < buckets.length; i += 1) {
615
+ var bk = buckets[i];
616
+ if (bk.__rcfPmBucketWired) continue;
617
+ bk.__rcfPmBucketWired = true;
618
+ bk.addEventListener('toggle', function () {
619
+ if (window.__rcfPmSuppress) return;
620
+ updatePmHash();
621
+ });
622
+ }
623
+ // Jump chip -> open bucket + scroll.
624
+ var chips = root.querySelectorAll('.pm-jump-chip');
625
+ for (var j = 0; j < chips.length; j += 1) {
626
+ var chip = chips[j];
627
+ if (chip.__rcfPmChipWired) continue;
628
+ chip.__rcfPmChipWired = true;
629
+ chip.addEventListener('click', function (ev) {
630
+ var target = ev.currentTarget.getAttribute('data-pm-jump');
631
+ var groupAttr = ev.currentTarget.closest('.pm-jump-nav').getAttribute('data-pm-jump-group');
632
+ var panel = pmGroupPanel(groupAttr);
633
+ if (!panel) return;
634
+ var node = panel.querySelector('details.pm-bucket[data-pm-bucket-id="' + target.replace(/"/g, '\\"') + '"]');
635
+ if (!node) return;
636
+ node.open = true;
637
+ try { node.scrollIntoView({ block: 'start' }); } catch (e) { node.scrollIntoView(); }
638
+ });
639
+ }
640
+ // Bulk expand/collapse.
641
+ var bulk = root.querySelectorAll('.pm-bulk-btn');
642
+ for (var b = 0; b < bulk.length; b += 1) {
643
+ var btn = bulk[b];
644
+ if (btn.__rcfPmBulkWired) continue;
645
+ btn.__rcfPmBulkWired = true;
646
+ btn.addEventListener('click', function (ev) {
647
+ var action = ev.currentTarget.getAttribute('data-pm-bulk');
648
+ var panel = document.querySelector('#tab-product-map .pm-group:not([hidden])');
649
+ if (!panel) return;
650
+ var all = panel.querySelectorAll('details.pm-bucket');
651
+ window.__rcfPmSuppress = true;
652
+ for (var i = 0; i < all.length; i += 1) all[i].open = (action === 'expand');
653
+ window.__rcfPmSuppress = false;
654
+ updatePmHash();
655
+ });
656
+ }
657
+ // Empty-roll toggle: reveals hidden empty buckets under the filter.
658
+ var rolls = root.querySelectorAll('.pm-empty-toggle');
659
+ for (var r = 0; r < rolls.length; r += 1) {
660
+ var rt = rolls[r];
661
+ if (rt.__rcfPmEmptyWired) continue;
662
+ rt.__rcfPmEmptyWired = true;
663
+ rt.addEventListener('click', function (ev) {
664
+ var panel = ev.currentTarget.closest('.pm-group');
665
+ if (!panel) return;
666
+ if (panel.classList.contains('pm-hide-empty')) {
667
+ panel.classList.remove('pm-hide-empty');
668
+ ev.currentTarget.textContent = 'Hide empty buckets';
669
+ } else {
670
+ panel.classList.add('pm-hide-empty');
671
+ var span = document.createElement('span');
672
+ span.className = 'pm-empty-count';
673
+ span.textContent = String(panel.querySelectorAll('details.pm-bucket[data-pm-empty-under-filter="true"]').length);
674
+ ev.currentTarget.textContent = '';
675
+ ev.currentTarget.appendChild(document.createTextNode('Show '));
676
+ ev.currentTarget.appendChild(span);
677
+ ev.currentTarget.appendChild(document.createTextNode(' empty buckets'));
678
+ }
679
+ });
680
+ }
681
+ }
682
+
683
+ function wireProductMap() {
684
+ var buttons = document.querySelectorAll('.pm-group-btn');
685
+ for (var i = 0; i < buttons.length; i += 1) {
686
+ if (buttons[i].__rcfPmWired) continue;
687
+ buttons[i].__rcfPmWired = true;
688
+ buttons[i].addEventListener('click', function (ev) {
689
+ var name = ev.currentTarget.getAttribute('data-pm-group');
690
+ if (activatePmGroup(name)) updatePmHash();
691
+ });
692
+ }
693
+ var select = document.querySelector('.pm-status-select');
694
+ if (select && !select.__rcfPmWired) {
695
+ select.__rcfPmWired = true;
696
+ select.addEventListener('change', function () {
697
+ applyPmStatusFilter(select.value);
698
+ updatePmHash();
699
+ });
700
+ }
701
+ wireProductMapWithin(document);
702
+ }
703
+
704
+ function resolveHash(hash) {
705
+ if (!hash) { activateTab('overview'); return; }
407
706
  var raw = hash.charAt(0) === '#' ? hash.slice(1) : hash;
408
707
  if (raw.indexOf('tab=') === 0) {
409
- activateTab(raw.slice(4));
708
+ var afterTab = raw.slice(4);
709
+ var amp = afterTab.indexOf('&');
710
+ var tab = amp === -1 ? afterTab : afterTab.slice(0, amp);
711
+ var rest = amp === -1 ? '' : afterTab.slice(amp + 1);
712
+ activateTab(tab);
713
+ if (tab === 'product-map') {
714
+ var params = parseHashParams(rest);
715
+ var openIds = params.open ? decodeURIComponent(params.open).split(',').filter(Boolean) : [];
716
+ activatePmGroup(params.group || 'shape', {
717
+ after: function () {
718
+ if (params.status) applyPmStatusFilter(params.status);
719
+ openPmBuckets(openIds);
720
+ },
721
+ });
722
+ }
410
723
  return;
411
724
  }
412
725
  var target = findByDocId(raw);
413
- if (!target) {
414
- activateTab('overview');
415
- return;
416
- }
726
+ if (!target) { activateTab('overview'); return; }
417
727
  var tab = tabForNode(target);
418
728
  if (tab) activateTab(tab);
419
729
  openAncestorDetails(target);
420
- if (target.tagName && target.tagName.toLowerCase() === 'details') {
421
- target.open = true;
422
- }
423
- try {
424
- target.scrollIntoView({ block: 'start' });
425
- } catch (e) {
426
- target.scrollIntoView();
427
- }
730
+ if (target.tagName && target.tagName.toLowerCase() === 'details') target.open = true;
731
+ try { target.scrollIntoView({ block: 'start' }); } catch (e) { target.scrollIntoView(); }
428
732
  }
429
733
 
430
734
  function onTabClick(ev) {
@@ -432,6 +736,11 @@ function inlineScript() {
432
736
  var name = btn.getAttribute('data-tab');
433
737
  if (!name) return;
434
738
  activateTab(name);
739
+ if (name === 'product-map') {
740
+ // Hydrate whichever grouping was selected last (default: shape).
741
+ // No-op when the grouping is already inline.
742
+ activatePmGroup(currentGroup());
743
+ }
435
744
  if (window.history && typeof window.history.replaceState === 'function') {
436
745
  window.history.replaceState(null, '', '#tab=' + name);
437
746
  }
@@ -439,19 +748,29 @@ function inlineScript() {
439
748
 
440
749
  function wireTabs() {
441
750
  tabButtons().forEach(function (btn) {
751
+ if (btn.__rcfTabWired) return;
752
+ btn.__rcfTabWired = true;
442
753
  btn.addEventListener('click', onTabClick);
443
754
  });
444
755
  }
445
756
 
757
+ var hashchangeWired = false;
446
758
  function onReady() {
447
759
  initMermaid();
448
760
  wireTabs();
761
+ wireProductMap();
449
762
  resolveHash(window.location.hash);
450
- window.addEventListener('hashchange', function () {
451
- resolveHash(window.location.hash);
452
- });
763
+ if (!hashchangeWired) {
764
+ hashchangeWired = true;
765
+ window.addEventListener('hashchange', function () {
766
+ resolveHash(window.location.hash);
767
+ });
768
+ }
453
769
  }
454
770
 
771
+ window.rcfPage = window.rcfPage || {};
772
+ window.rcfPage.init = onReady;
773
+
455
774
  if (document.readyState === 'loading') {
456
775
  document.addEventListener('DOMContentLoaded', onReady);
457
776
  } else {