rcf-lite 0.28.2 → 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 (196) hide show
  1. package/CHANGELOG.md +211 -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-700.json +23 -0
  10. package/rcf/code-nodes/cn-701.json +14 -0
  11. package/rcf/code-nodes/cn-702.json +14 -0
  12. package/rcf/code-nodes/cn-703.json +16 -0
  13. package/rcf/code-nodes/cn-720.json +15 -0
  14. package/rcf/code-nodes/cn-721.json +15 -0
  15. package/rcf/code-nodes/cn-722.json +15 -0
  16. package/rcf/code-nodes/cn-723.json +18 -0
  17. package/rcf/code-nodes/cn-724.json +14 -0
  18. package/rcf/code-nodes/cn-725.json +21 -0
  19. package/rcf/code-nodes/cn-726.json +17 -0
  20. package/rcf/code-nodes/cn-727.json +14 -0
  21. package/rcf/code-nodes/cn-728.json +21 -0
  22. package/rcf/code-nodes/cn-729.json +18 -0
  23. package/rcf/code-nodes/cn-730.json +14 -0
  24. package/rcf/fbs/fbs-190.json +37 -0
  25. package/rcf/fbs/fbs-191.json +34 -0
  26. package/rcf/fbs/fbs-192.json +17 -0
  27. package/rcf/fbs/fbs-193.json +37 -0
  28. package/rcf/fbs/fbs-194.json +35 -0
  29. package/rcf/fbs/fbs-195.json +37 -0
  30. package/rcf/fbs/fbs-196.json +37 -0
  31. package/rcf/fbs/fbs-197.json +41 -0
  32. package/rcf/requirements/req-001.json +2 -1
  33. package/rcf/requirements/req-002.json +2 -1
  34. package/rcf/requirements/req-003.json +2 -1
  35. package/rcf/requirements/req-004.json +2 -1
  36. package/rcf/requirements/req-005.json +2 -1
  37. package/rcf/requirements/req-006.json +2 -1
  38. package/rcf/requirements/req-007.json +2 -1
  39. package/rcf/requirements/req-008.json +2 -1
  40. package/rcf/requirements/req-009.json +2 -1
  41. package/rcf/requirements/req-010.json +5 -2
  42. package/rcf/requirements/req-011.json +2 -1
  43. package/rcf/requirements/req-012.json +2 -1
  44. package/rcf/requirements/req-013.json +2 -1
  45. package/rcf/requirements/req-014.json +2 -1
  46. package/rcf/requirements/req-015.json +2 -1
  47. package/rcf/requirements/req-016.json +5 -2
  48. package/rcf/requirements/req-017.json +6 -2
  49. package/rcf/requirements/req-018.json +5 -2
  50. package/rcf/requirements/req-019.json +5 -2
  51. package/rcf/requirements/req-020.json +5 -2
  52. package/rcf/requirements/req-021.json +5 -2
  53. package/rcf/requirements/req-022.json +5 -2
  54. package/rcf/requirements/req-023.json +6 -4
  55. package/rcf/requirements/req-024.json +5 -2
  56. package/rcf/requirements/req-025.json +5 -2
  57. package/rcf/requirements/req-026.json +5 -2
  58. package/rcf/requirements/req-027.json +5 -2
  59. package/rcf/requirements/req-028.json +5 -2
  60. package/rcf/requirements/req-029.json +5 -2
  61. package/rcf/requirements/req-030.json +5 -2
  62. package/rcf/requirements/req-040.json +5 -2
  63. package/rcf/requirements/req-041.json +5 -2
  64. package/rcf/requirements/req-042.json +5 -2
  65. package/rcf/requirements/req-043.json +5 -2
  66. package/rcf/requirements/req-050.json +5 -2
  67. package/rcf/requirements/req-051.json +5 -2
  68. package/rcf/requirements/req-052.json +5 -2
  69. package/rcf/requirements/req-053.json +5 -2
  70. package/rcf/requirements/req-054.json +5 -2
  71. package/rcf/requirements/req-060.json +5 -2
  72. package/rcf/requirements/req-061.json +5 -2
  73. package/rcf/requirements/req-062.json +5 -2
  74. package/rcf/requirements/req-063.json +5 -2
  75. package/rcf/requirements/req-070.json +5 -2
  76. package/rcf/requirements/req-071.json +5 -2
  77. package/rcf/requirements/req-072.json +5 -2
  78. package/rcf/requirements/req-073.json +5 -2
  79. package/rcf/requirements/req-074.json +5 -2
  80. package/rcf/requirements/req-075.json +5 -2
  81. package/rcf/requirements/req-076.json +5 -2
  82. package/rcf/requirements/req-077.json +5 -2
  83. package/rcf/requirements/req-080.json +5 -2
  84. package/rcf/requirements/req-081.json +5 -2
  85. package/rcf/requirements/req-082.json +5 -2
  86. package/rcf/requirements/req-083.json +5 -2
  87. package/rcf/requirements/req-084.json +5 -2
  88. package/rcf/requirements/req-085.json +5 -2
  89. package/rcf/requirements/req-086.json +5 -2
  90. package/rcf/requirements/req-090.json +5 -2
  91. package/rcf/requirements/req-091.json +5 -2
  92. package/rcf/requirements/req-092.json +5 -2
  93. package/rcf/requirements/req-093.json +5 -2
  94. package/rcf/requirements/req-094.json +5 -2
  95. package/rcf/requirements/req-100.json +5 -2
  96. package/rcf/requirements/req-101.json +5 -2
  97. package/rcf/requirements/req-102.json +5 -2
  98. package/rcf/requirements/req-103.json +5 -2
  99. package/rcf/requirements/req-104.json +5 -2
  100. package/rcf/requirements/req-110.json +5 -2
  101. package/rcf/requirements/req-111.json +5 -2
  102. package/rcf/requirements/req-112.json +5 -2
  103. package/rcf/requirements/req-113.json +5 -2
  104. package/rcf/requirements/req-114.json +5 -2
  105. package/rcf/requirements/req-115.json +5 -2
  106. package/rcf/requirements/req-120.json +5 -2
  107. package/rcf/requirements/req-121.json +5 -2
  108. package/rcf/requirements/req-122.json +5 -2
  109. package/rcf/requirements/req-123.json +5 -2
  110. package/rcf/requirements/req-124.json +5 -2
  111. package/rcf/requirements/req-125.json +5 -2
  112. package/rcf/requirements/req-130.json +6 -2
  113. package/rcf/requirements/req-131.json +6 -2
  114. package/rcf/requirements/req-132.json +6 -2
  115. package/rcf/requirements/req-133.json +6 -2
  116. package/rcf/requirements/req-134.json +6 -2
  117. package/rcf/requirements/req-140.json +5 -2
  118. package/rcf/requirements/req-145.json +5 -2
  119. package/rcf/requirements/req-150.json +6 -2
  120. package/rcf/requirements/req-151.json +6 -2
  121. package/rcf/requirements/req-152.json +6 -2
  122. package/rcf/requirements/req-153.json +6 -2
  123. package/rcf/requirements/req-154.json +5 -2
  124. package/rcf/requirements/req-155.json +4 -1
  125. package/rcf/requirements/req-156.json +4 -1
  126. package/rcf/requirements/req-157.json +4 -1
  127. package/rcf/requirements/req-158.json +4 -1
  128. package/rcf/requirements/req-159.json +4 -1
  129. package/rcf/requirements/req-160.json +4 -1
  130. package/rcf/requirements/req-161.json +4 -1
  131. package/rcf/requirements/req-162.json +4 -1
  132. package/rcf/requirements/req-163.json +5 -2
  133. package/rcf/requirements/req-164.json +5 -2
  134. package/rcf/requirements/req-165.json +5 -2
  135. package/rcf/requirements/req-170.json +33 -0
  136. package/rcf/requirements/req-171.json +44 -0
  137. package/rcf/requirements/req-172.json +25 -0
  138. package/rcf/requirements/req-173.json +25 -0
  139. package/rcf/requirements/req-174.json +25 -0
  140. package/rcf/requirements/req-175.json +25 -0
  141. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  142. package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
  143. package/rcf/tacs/tac-4122-define-gates.json +25 -0
  144. package/rcf/tacs/tac-4123-define-readiness.json +25 -0
  145. package/rcf/test-suites/ts-204.json +30 -0
  146. package/rcf/test-suites/ts-205.json +22 -0
  147. package/rcf/test-suites/ts-206.json +22 -0
  148. package/rcf/test-suites/ts-207.json +22 -0
  149. package/rcf/test-suites/ts-208.json +22 -0
  150. package/rcf/test-suites/ts-209.json +22 -0
  151. package/rcf/test-suites/ts-210.json +22 -0
  152. package/rcf/test-suites/ts-211.json +22 -0
  153. package/rcf/test-suites/ts-212.json +22 -0
  154. package/rcf/test-suites/ts-213.json +50 -0
  155. package/rcf/test-suites/ts-214.json +74 -0
  156. package/rcf/test-suites/ts-215.json +67 -0
  157. package/rcf/test-suites/ts-216.json +78 -0
  158. package/rcf/test-suites/ts-217.json +62 -0
  159. package/rcf/user-stories/us-15601.json +4 -4
  160. package/rcf/user-stories/us-17001.json +27 -0
  161. package/rcf/user-stories/us-17002.json +22 -0
  162. package/rcf/user-stories/us-17003.json +22 -0
  163. package/rcf/user-stories/us-17004.json +22 -0
  164. package/rcf/user-stories/us-17005.json +22 -0
  165. package/rcf/user-stories/us-17006.json +21 -0
  166. package/rcf/user-stories/us-17007.json +21 -0
  167. package/rcf/user-stories/us-17008.json +22 -0
  168. package/rcf/user-stories/us-17101.json +22 -0
  169. package/rcf/user-stories/us-17102.json +22 -0
  170. package/rcf/user-stories/us-17201.json +38 -0
  171. package/rcf/user-stories/us-17301.json +38 -0
  172. package/rcf/user-stories/us-17401.json +58 -0
  173. package/rcf/user-stories/us-17501.json +48 -0
  174. package/releases/releases.yaml +21 -1
  175. package/src/cli/feedback.js +126 -10
  176. package/src/cli/help.js +11 -0
  177. package/src/cli/ledger.js +399 -0
  178. package/src/cli/readiness.js +280 -0
  179. package/src/core/store/writer.js +86 -6
  180. package/src/define/freeze-record.js +232 -0
  181. package/src/define/ledgers.js +513 -0
  182. package/src/feedback/redact.js +105 -19
  183. package/src/query/delta.js +276 -0
  184. package/src/query/gates.js +832 -0
  185. package/src/query/index.js +36 -0
  186. package/src/query/readiness.js +333 -0
  187. package/src/server/index.js +37 -0
  188. package/src/server/routes.js +65 -3
  189. package/src/view/doc-renderers/helpers.js +2 -2
  190. package/src/view/doc-renderers/req.js +10 -2
  191. package/src/view/doc-renderers/user-story.js +10 -5
  192. package/src/view/html-page.js +347 -28
  193. package/src/view/index.js +14 -1
  194. package/src/view/live-client.js +22 -2
  195. package/src/view/product-map.js +751 -0
  196. package/src/view/style.css +378 -0
@@ -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 {
package/src/view/index.js CHANGED
@@ -10,6 +10,7 @@ import { stat } from 'node:fs/promises';
10
10
 
11
11
  import { walkTree } from '#core/store';
12
12
  import { renderContent, renderPage } from './html-page.js';
13
+ import { renderProductMapGrouping } from './product-map.js';
13
14
  import { buildTreeModel } from './tree-model.js';
14
15
 
15
16
  const here = dirname(fileURLToPath(import.meta.url));
@@ -59,5 +60,17 @@ export async function renderModelToPage({ projectRoot }) {
59
60
  const model = buildTreeModel({ tree, errors });
60
61
  const fullPageHtml = renderPage(model);
61
62
  const contentHtml = renderContent(model);
62
- return { fullPageHtml, contentHtml, errors, tree };
63
+ // Pre-rendered partials for the /product-map/<group> endpoint
64
+ // (AC-17007-1). The full page ships only the shape grouping's REQ
65
+ // cards; the client fetches the other three on demand from these
66
+ // strings. Regenerated on every rewalk so the cache stays consistent
67
+ // with the tree on disk.
68
+ const pmPartials = {
69
+ shape: renderProductMapGrouping(model, 'shape'),
70
+ component: renderProductMapGrouping(model, 'component'),
71
+ trace: renderProductMapGrouping(model, 'trace'),
72
+ capability: renderProductMapGrouping(model, 'capability'),
73
+ blueprint: renderProductMapGrouping(model, 'blueprint'),
74
+ };
75
+ return { fullPageHtml, contentHtml, errors, tree, pmPartials };
63
76
  }
@@ -434,6 +434,12 @@
434
434
  host.innerHTML = payload.contentHtml;
435
435
  }
436
436
  currentVersion = payload.version;
437
+ // The Product Map panel keeps a client-side partial cache
438
+ // (window.__rcfPmPartialCache is opaque to us). A tree-update
439
+ // invalidates every partial: the next activatePmGroup on a lazy
440
+ // grouping refetches instead of using the stale cache. See
441
+ // hydrateLazyGroup in the inline script.
442
+ win.__rcfPmDirty = true;
437
443
  // Tab buttons live in the header (outside the swap wrapper) so
438
444
  // their click handlers survive. But the swap serves every panel
439
445
  // with the same `hidden` state Phase 3.6 renders (Overview
@@ -442,6 +448,13 @@
442
448
  // Mermaid inside the selected panel because .mermaid nodes in
443
449
  // the payload have no `data-processed` yet.
444
450
  resyncTabsAfterSwap(doc, win);
451
+ // Re-invoke the page init so pm-group-btn / pm-status-select
452
+ // listeners (which lived inside the swapped wrapper) are attached
453
+ // to the fresh DOM. onReady is idempotent for the header-scoped
454
+ // bindings that survive the swap.
455
+ if (win.rcfPage && typeof win.rcfPage.init === 'function') {
456
+ try { win.rcfPage.init(); } catch (e) { /* soft */ }
457
+ }
445
458
  restoreState(deps);
446
459
  applyScope();
447
460
  setState('connected');
@@ -484,8 +497,15 @@
484
497
  // the panels back in line with the aria-selected button and re-run
485
498
  // Mermaid in the visible one.
486
499
  function resyncTabsAfterSwap(doc, win) {
487
- var TABS = ['overview', 'requirements', 'architecture', 'build'];
488
- var selectedBtn = doc.querySelector('[role="tab"][aria-selected="true"]');
500
+ // Phase 3.8 tabs; Phase 3.9 added product-map. Product Map MUST be
501
+ // in this list so an SSE swap while it is the active tab keeps the
502
+ // panel visible; without it, resolveActiveTab falls back to
503
+ // overview and the freshly-swapped pm listeners have already been
504
+ // rewired by the rcfPage.init() call below.
505
+ var TABS = ['overview', 'requirements', 'architecture', 'build', 'product-map'];
506
+ // Scope to the top-level nav.tabs to avoid matching pm-group-btn
507
+ // nodes (they also carry role="tab").
508
+ var selectedBtn = doc.querySelector('nav.tabs [role="tab"][aria-selected="true"]');
489
509
  var selected = selectedBtn && selectedBtn.getAttribute ? selectedBtn.getAttribute('data-tab') : null;
490
510
  if (!selected || TABS.indexOf(selected) === -1) selected = 'overview';
491
511
  for (var i = 0; i < TABS.length; i += 1) {