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.
- package/CHANGELOG.md +211 -0
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +1 -1
- package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
- package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
- package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
- package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
- package/rcf/code-nodes/cn-700.json +23 -0
- package/rcf/code-nodes/cn-701.json +14 -0
- package/rcf/code-nodes/cn-702.json +14 -0
- package/rcf/code-nodes/cn-703.json +16 -0
- package/rcf/code-nodes/cn-720.json +15 -0
- package/rcf/code-nodes/cn-721.json +15 -0
- package/rcf/code-nodes/cn-722.json +15 -0
- package/rcf/code-nodes/cn-723.json +18 -0
- package/rcf/code-nodes/cn-724.json +14 -0
- package/rcf/code-nodes/cn-725.json +21 -0
- package/rcf/code-nodes/cn-726.json +17 -0
- package/rcf/code-nodes/cn-727.json +14 -0
- package/rcf/code-nodes/cn-728.json +21 -0
- package/rcf/code-nodes/cn-729.json +18 -0
- package/rcf/code-nodes/cn-730.json +14 -0
- package/rcf/fbs/fbs-190.json +37 -0
- package/rcf/fbs/fbs-191.json +34 -0
- package/rcf/fbs/fbs-192.json +17 -0
- package/rcf/fbs/fbs-193.json +37 -0
- package/rcf/fbs/fbs-194.json +35 -0
- package/rcf/fbs/fbs-195.json +37 -0
- package/rcf/fbs/fbs-196.json +37 -0
- package/rcf/fbs/fbs-197.json +41 -0
- package/rcf/requirements/req-001.json +2 -1
- package/rcf/requirements/req-002.json +2 -1
- package/rcf/requirements/req-003.json +2 -1
- package/rcf/requirements/req-004.json +2 -1
- package/rcf/requirements/req-005.json +2 -1
- package/rcf/requirements/req-006.json +2 -1
- package/rcf/requirements/req-007.json +2 -1
- package/rcf/requirements/req-008.json +2 -1
- package/rcf/requirements/req-009.json +2 -1
- package/rcf/requirements/req-010.json +5 -2
- package/rcf/requirements/req-011.json +2 -1
- package/rcf/requirements/req-012.json +2 -1
- package/rcf/requirements/req-013.json +2 -1
- package/rcf/requirements/req-014.json +2 -1
- package/rcf/requirements/req-015.json +2 -1
- package/rcf/requirements/req-016.json +5 -2
- package/rcf/requirements/req-017.json +6 -2
- package/rcf/requirements/req-018.json +5 -2
- package/rcf/requirements/req-019.json +5 -2
- package/rcf/requirements/req-020.json +5 -2
- package/rcf/requirements/req-021.json +5 -2
- package/rcf/requirements/req-022.json +5 -2
- package/rcf/requirements/req-023.json +6 -4
- package/rcf/requirements/req-024.json +5 -2
- package/rcf/requirements/req-025.json +5 -2
- package/rcf/requirements/req-026.json +5 -2
- package/rcf/requirements/req-027.json +5 -2
- package/rcf/requirements/req-028.json +5 -2
- package/rcf/requirements/req-029.json +5 -2
- package/rcf/requirements/req-030.json +5 -2
- package/rcf/requirements/req-040.json +5 -2
- package/rcf/requirements/req-041.json +5 -2
- package/rcf/requirements/req-042.json +5 -2
- package/rcf/requirements/req-043.json +5 -2
- package/rcf/requirements/req-050.json +5 -2
- package/rcf/requirements/req-051.json +5 -2
- package/rcf/requirements/req-052.json +5 -2
- package/rcf/requirements/req-053.json +5 -2
- package/rcf/requirements/req-054.json +5 -2
- package/rcf/requirements/req-060.json +5 -2
- package/rcf/requirements/req-061.json +5 -2
- package/rcf/requirements/req-062.json +5 -2
- package/rcf/requirements/req-063.json +5 -2
- package/rcf/requirements/req-070.json +5 -2
- package/rcf/requirements/req-071.json +5 -2
- package/rcf/requirements/req-072.json +5 -2
- package/rcf/requirements/req-073.json +5 -2
- package/rcf/requirements/req-074.json +5 -2
- package/rcf/requirements/req-075.json +5 -2
- package/rcf/requirements/req-076.json +5 -2
- package/rcf/requirements/req-077.json +5 -2
- package/rcf/requirements/req-080.json +5 -2
- package/rcf/requirements/req-081.json +5 -2
- package/rcf/requirements/req-082.json +5 -2
- package/rcf/requirements/req-083.json +5 -2
- package/rcf/requirements/req-084.json +5 -2
- package/rcf/requirements/req-085.json +5 -2
- package/rcf/requirements/req-086.json +5 -2
- package/rcf/requirements/req-090.json +5 -2
- package/rcf/requirements/req-091.json +5 -2
- package/rcf/requirements/req-092.json +5 -2
- package/rcf/requirements/req-093.json +5 -2
- package/rcf/requirements/req-094.json +5 -2
- package/rcf/requirements/req-100.json +5 -2
- package/rcf/requirements/req-101.json +5 -2
- package/rcf/requirements/req-102.json +5 -2
- package/rcf/requirements/req-103.json +5 -2
- package/rcf/requirements/req-104.json +5 -2
- package/rcf/requirements/req-110.json +5 -2
- package/rcf/requirements/req-111.json +5 -2
- package/rcf/requirements/req-112.json +5 -2
- package/rcf/requirements/req-113.json +5 -2
- package/rcf/requirements/req-114.json +5 -2
- package/rcf/requirements/req-115.json +5 -2
- package/rcf/requirements/req-120.json +5 -2
- package/rcf/requirements/req-121.json +5 -2
- package/rcf/requirements/req-122.json +5 -2
- package/rcf/requirements/req-123.json +5 -2
- package/rcf/requirements/req-124.json +5 -2
- package/rcf/requirements/req-125.json +5 -2
- package/rcf/requirements/req-130.json +6 -2
- package/rcf/requirements/req-131.json +6 -2
- package/rcf/requirements/req-132.json +6 -2
- package/rcf/requirements/req-133.json +6 -2
- package/rcf/requirements/req-134.json +6 -2
- package/rcf/requirements/req-140.json +5 -2
- package/rcf/requirements/req-145.json +5 -2
- package/rcf/requirements/req-150.json +6 -2
- package/rcf/requirements/req-151.json +6 -2
- package/rcf/requirements/req-152.json +6 -2
- package/rcf/requirements/req-153.json +6 -2
- package/rcf/requirements/req-154.json +5 -2
- package/rcf/requirements/req-155.json +4 -1
- package/rcf/requirements/req-156.json +4 -1
- package/rcf/requirements/req-157.json +4 -1
- package/rcf/requirements/req-158.json +4 -1
- package/rcf/requirements/req-159.json +4 -1
- package/rcf/requirements/req-160.json +4 -1
- package/rcf/requirements/req-161.json +4 -1
- package/rcf/requirements/req-162.json +4 -1
- package/rcf/requirements/req-163.json +5 -2
- package/rcf/requirements/req-164.json +5 -2
- package/rcf/requirements/req-165.json +5 -2
- package/rcf/requirements/req-170.json +33 -0
- package/rcf/requirements/req-171.json +44 -0
- package/rcf/requirements/req-172.json +25 -0
- package/rcf/requirements/req-173.json +25 -0
- package/rcf/requirements/req-174.json +25 -0
- package/rcf/requirements/req-175.json +25 -0
- package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
- package/rcf/tacs/tac-4122-define-gates.json +25 -0
- package/rcf/tacs/tac-4123-define-readiness.json +25 -0
- package/rcf/test-suites/ts-204.json +30 -0
- package/rcf/test-suites/ts-205.json +22 -0
- package/rcf/test-suites/ts-206.json +22 -0
- package/rcf/test-suites/ts-207.json +22 -0
- package/rcf/test-suites/ts-208.json +22 -0
- package/rcf/test-suites/ts-209.json +22 -0
- package/rcf/test-suites/ts-210.json +22 -0
- package/rcf/test-suites/ts-211.json +22 -0
- package/rcf/test-suites/ts-212.json +22 -0
- package/rcf/test-suites/ts-213.json +50 -0
- package/rcf/test-suites/ts-214.json +74 -0
- package/rcf/test-suites/ts-215.json +67 -0
- package/rcf/test-suites/ts-216.json +78 -0
- package/rcf/test-suites/ts-217.json +62 -0
- package/rcf/user-stories/us-15601.json +4 -4
- package/rcf/user-stories/us-17001.json +27 -0
- package/rcf/user-stories/us-17002.json +22 -0
- package/rcf/user-stories/us-17003.json +22 -0
- package/rcf/user-stories/us-17004.json +22 -0
- package/rcf/user-stories/us-17005.json +22 -0
- package/rcf/user-stories/us-17006.json +21 -0
- package/rcf/user-stories/us-17007.json +21 -0
- package/rcf/user-stories/us-17008.json +22 -0
- package/rcf/user-stories/us-17101.json +22 -0
- package/rcf/user-stories/us-17102.json +22 -0
- package/rcf/user-stories/us-17201.json +38 -0
- package/rcf/user-stories/us-17301.json +38 -0
- package/rcf/user-stories/us-17401.json +58 -0
- package/rcf/user-stories/us-17501.json +48 -0
- package/releases/releases.yaml +21 -1
- package/src/cli/feedback.js +126 -10
- package/src/cli/help.js +11 -0
- package/src/cli/ledger.js +399 -0
- package/src/cli/readiness.js +280 -0
- package/src/core/store/writer.js +86 -6
- package/src/define/freeze-record.js +232 -0
- package/src/define/ledgers.js +513 -0
- package/src/feedback/redact.js +105 -19
- package/src/query/delta.js +276 -0
- package/src/query/gates.js +832 -0
- package/src/query/index.js +36 -0
- package/src/query/readiness.js +333 -0
- package/src/server/index.js +37 -0
- package/src/server/routes.js +65 -3
- package/src/view/doc-renderers/helpers.js +2 -2
- package/src/view/doc-renderers/req.js +10 -2
- package/src/view/doc-renderers/user-story.js +10 -5
- package/src/view/html-page.js +347 -28
- package/src/view/index.js +14 -1
- package/src/view/live-client.js +22 -2
- package/src/view/product-map.js +751 -0
- package/src/view/style.css +378 -0
package/src/view/html-page.js
CHANGED
|
@@ -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
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/view/live-client.js
CHANGED
|
@@ -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
|
-
|
|
488
|
-
|
|
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) {
|