rcf-lite 0.32.5 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/discovery/discovery-viewer-command-page-1-layout.html +191 -0
- package/guidance/discovery/discovery-viewer-command-page-2-coverage.html +193 -0
- package/guidance/discovery/discovery-viewer-command-page-3-trace-matrix.html +195 -0
- package/guidance/discovery/discovery-viewer-command-page-4-tables.html +196 -0
- package/guidance/discovery/discovery-viewer-command-page-5-subtabs.html +224 -0
- package/guidance/discovery/discovery-viewer-command-page-6-one-page.html +280 -0
- package/guidance/discovery/discovery-viewer-command-page.md +301 -0
- package/package.json +1 -1
- package/rcf/adrs/adr-4139-readiness-tables-read-only-no-commands.json +30 -0
- package/rcf/adrs/adr-4140-view-query-routes-per-pivot-matrix-no-mermaid.json +30 -0
- package/rcf/adrs/adr-4141-verify-modes.json +25 -0
- package/rcf/code-nodes/cn-771.json +3 -2
- package/rcf/code-nodes/cn-779.json +19 -0
- package/rcf/code-nodes/cn-780.json +14 -0
- package/rcf/code-nodes/cn-781.json +14 -0
- package/rcf/code-nodes/cn-782.json +15 -0
- package/rcf/code-nodes/cn-783.json +17 -0
- package/rcf/code-nodes/cn-784.json +20 -0
- package/rcf/code-nodes/cn-785.json +14 -0
- package/rcf/code-nodes/cn-786.json +14 -0
- package/rcf/code-nodes/cn-787.json +18 -0
- package/rcf/code-nodes/cn-788.json +14 -0
- package/rcf/code-nodes/cn-789.json +14 -0
- package/rcf/code-nodes/cn-790.json +15 -0
- package/rcf/code-nodes/cn-791.json +14 -0
- package/rcf/code-nodes/cn-792.json +17 -0
- package/rcf/code-nodes/cn-793.json +17 -0
- package/rcf/code-nodes/cn-794.json +18 -0
- package/rcf/code-nodes/cn-795.json +15 -0
- package/rcf/code-nodes/cn-796.json +14 -0
- package/rcf/code-nodes/cn-797.json +14 -0
- package/rcf/code-nodes/cn-798.json +14 -0
- package/rcf/define/brief-ledger.json +47 -0
- package/rcf/define/decisions-ledger.json +113 -0
- package/rcf/fbs/fbs-204.json +46 -0
- package/rcf/fbs/fbs-205.json +46 -0
- package/rcf/fbs/fbs-206.json +49 -0
- package/rcf/fbs/fbs-207.json +43 -0
- package/rcf/fbs/fbs-208.json +47 -0
- package/rcf/fbs/fbs-209.json +22 -0
- package/rcf/requirements/req-002.json +5 -2
- package/rcf/requirements/req-180.json +6 -3
- package/rcf/tacs/tac-4127-readiness-view.json +2 -2
- package/rcf/tacs/tac-4133-readiness-po-layer.json +2 -2
- package/rcf/tacs/tac-4135-readiness-command-page.json +94 -0
- package/rcf/tacs/tac-4136-view-query-routes.json +68 -0
- package/rcf/tacs/tac-4141-verify-modes.json +25 -0
- package/rcf/test-suites/ts-223.json +7 -7
- package/rcf/test-suites/ts-237.json +78 -0
- package/rcf/test-suites/ts-238.json +70 -0
- package/rcf/test-suites/ts-239.json +78 -0
- package/rcf/test-suites/ts-240.json +70 -0
- package/rcf/test-suites/ts-241.json +70 -0
- package/rcf/test-suites/ts-242.json +46 -0
- package/rcf/user-stories/us-16503.json +51 -0
- package/rcf/user-stories/us-18001.json +4 -4
- package/rcf/user-stories/us-18003.json +79 -0
- package/rcf/user-stories/us-18004.json +70 -0
- package/rcf/user-stories/us-18005.json +79 -0
- package/rcf/user-stories/us-208.json +74 -0
- package/rcf/user-stories/us-209.json +70 -0
- package/releases/releases.yaml +10 -1
- package/src/cli/finalise.js +141 -2
- package/src/cli/view.js +13 -1
- package/src/finalise/spawn.js +2 -0
- package/src/server/index.js +104 -39
- package/src/server/routes.js +169 -0
- package/src/verify/cli/run.js +40 -0
- package/src/verify/engine/deterministic.js +150 -0
- package/src/verify/engine/index.js +27 -4
- package/src/view/doc-renderers/req.js +11 -9
- package/src/view/html-page.js +15 -11
- package/src/view/index.js +7 -1
- package/src/view/live-client.js +4 -0
- package/src/view/page-init.js +842 -1
- package/src/view/readiness/composite-id.js +35 -0
- package/src/view/readiness/coverage-summary.js +293 -0
- package/src/view/readiness/delta-counts.js +156 -0
- package/src/view/readiness/grid.js +175 -0
- package/src/view/readiness/po-layer.js +110 -220
- package/src/view/readiness/tables.js +500 -0
- package/src/view/readiness/thin-reqs.js +237 -0
- package/src/view/readiness/trace-matrix.js +392 -0
- package/src/view/readiness.js +359 -370
- package/src/view/style.css +594 -0
- package/src/watch/index.js +75 -17
package/src/view/page-init.js
CHANGED
|
@@ -349,6 +349,20 @@
|
|
|
349
349
|
return document.querySelector('[data-doc-id="' + id.replace(/"/g, '\\\\"') + '"]');
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
// FBS-204 (TAC-4135, AC-18003-2): the Readiness command page carries
|
|
353
|
+
// rows whose itemId is not a document id (brief:ledger,
|
|
354
|
+
// profile:surface, open-decision:5). findByDocId cannot resolve
|
|
355
|
+
// these; findByReadinessEntity reads the row's [data-rcf-entity]
|
|
356
|
+
// attribute which the tables renderer stamps on every row. Called
|
|
357
|
+
// from the hash router when #tab=readiness carries an &entity= whose
|
|
358
|
+
// value does not land on a doc.
|
|
359
|
+
function findByReadinessEntity(itemId) {
|
|
360
|
+
if (!itemId) return null;
|
|
361
|
+
try {
|
|
362
|
+
return document.querySelector('[data-rcf-entity="' + String(itemId).replace(/"/g, '\\"') + '"]');
|
|
363
|
+
} catch (e) { return null; }
|
|
364
|
+
}
|
|
365
|
+
|
|
352
366
|
// ---- product map ------------------------------------------------------
|
|
353
367
|
|
|
354
368
|
function pmGroupPanel(name) {
|
|
@@ -638,7 +652,7 @@
|
|
|
638
652
|
if (raw.indexOf('=') !== -1 && raw.indexOf('tab=') === -1) {
|
|
639
653
|
var kv = parseHashParams(raw);
|
|
640
654
|
if (kv.entity) {
|
|
641
|
-
var entTarget = findByDocId(kv.entity);
|
|
655
|
+
var entTarget = findByDocId(kv.entity) || findByReadinessEntity(kv.entity);
|
|
642
656
|
if (entTarget) {
|
|
643
657
|
var entTab = tabForNode(entTarget);
|
|
644
658
|
if (entTab) activateTab(entTab);
|
|
@@ -681,10 +695,31 @@
|
|
|
681
695
|
if (tab === 'build') {
|
|
682
696
|
applyBuildHash(params);
|
|
683
697
|
}
|
|
698
|
+
// Readiness tab (FBS-206, AC-18005-2 and -8): activate the right
|
|
699
|
+
// sub-tab, drop an unknown sub= from the hash, and apply any
|
|
700
|
+
// blocking FilterBar params the Overview verdict grid links
|
|
701
|
+
// carry (stage=, persona=).
|
|
702
|
+
if (tab === 'readiness' && readinessSubTabStrip()) {
|
|
703
|
+
applyReadinessHash(params);
|
|
704
|
+
}
|
|
684
705
|
// #tab=requirements&entity=REQ-002 opens the entity in place.
|
|
706
|
+
// FBS-204 (AC-18003-2): #tab=readiness&sub=questions&entity=<itemId>
|
|
707
|
+
// lands on a readiness-row element when the itemId is not a
|
|
708
|
+
// document (brief:ledger, profile:surface, open-decision:5).
|
|
685
709
|
if (params.entity) {
|
|
686
710
|
var ent = findByDocId(params.entity);
|
|
711
|
+
if (!ent && tab === 'readiness') ent = findByReadinessEntity(params.entity);
|
|
687
712
|
if (ent) {
|
|
713
|
+
// FBS-204 (AC-18003-1): "opens the item in full context".
|
|
714
|
+
// When the entity resolves to a document that lives on a
|
|
715
|
+
// different tab from the one in the hash (for example
|
|
716
|
+
// #tab=readiness&sub=questions&entity=REQ-012 and REQ-012
|
|
717
|
+
// sits in the Requirements tab), switch to that tab so the
|
|
718
|
+
// target becomes visible rather than opening hidden.
|
|
719
|
+
var entTabName = tabForNode(ent);
|
|
720
|
+
if (entTabName && entTabName !== tab && TABS.indexOf(entTabName) !== -1) {
|
|
721
|
+
activateTab(entTabName);
|
|
722
|
+
}
|
|
688
723
|
openAncestorDetails(ent);
|
|
689
724
|
if (ent.tagName && ent.tagName.toLowerCase() === 'details') ent.open = true;
|
|
690
725
|
try { ent.scrollIntoView({ block: 'start' }); } catch (e) { ent.scrollIntoView(); }
|
|
@@ -722,6 +757,12 @@
|
|
|
722
757
|
writeHash(buildHashFragment(), true);
|
|
723
758
|
return;
|
|
724
759
|
}
|
|
760
|
+
// Readiness tab (FBS-206): mirror the sub-tab state into the hash
|
|
761
|
+
// so a tab click from elsewhere lands on the current sub.
|
|
762
|
+
if (name === 'readiness' && readinessSubTabStrip()) {
|
|
763
|
+
writeHash(readinessHashFragment(), true);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
725
766
|
// Hash write preserves ?embed=/?theme=/... (Dex contract, PR 1).
|
|
726
767
|
writeHash('#tab=' + name, true);
|
|
727
768
|
}
|
|
@@ -2124,6 +2165,17 @@
|
|
|
2124
2165
|
var parentId = row.getAttribute('data-rcf-lookup-parent');
|
|
2125
2166
|
if (kind === 'ts' && parentId) { id = parentId; tab = 'requirements'; }
|
|
2126
2167
|
lookupClose();
|
|
2168
|
+
// FBS-208 (AC-209-3): a lookup hit while the Readiness tab is
|
|
2169
|
+
// active lands on the Trace sub-tab for the chosen id (preserves
|
|
2170
|
+
// embed/theme via the writeHash contract). The chain pivots the
|
|
2171
|
+
// Trace matrix supports are PRD/REQ/US/AC/TS/TC/FBS/CN/TAC/ADR.
|
|
2172
|
+
var activeTabBtn = document.querySelector('.tabs [role="tab"][aria-selected="true"]');
|
|
2173
|
+
var active = activeTabBtn ? activeTabBtn.getAttribute('data-tab') : '';
|
|
2174
|
+
if (active === 'readiness' && /^(PRD|REQ|US|AC|TS|TC|FBS|CN|TAC|ADR)-/.test(id)) {
|
|
2175
|
+
writeHash('#tab=readiness&sub=trace&entity=' + encodeURIComponent(id), false);
|
|
2176
|
+
resolveHash(window.location.hash);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2127
2179
|
// writeHash preserves ?embed=/?theme=/... (Dex contract, PR 1); the
|
|
2128
2180
|
// Router's hashchange listener then activates the tab and opens
|
|
2129
2181
|
// every ancestor DocRow (resolveHash).
|
|
@@ -2272,6 +2324,622 @@
|
|
|
2272
2324
|
|
|
2273
2325
|
var hashchangeWired = false;
|
|
2274
2326
|
|
|
2327
|
+
// ---- Readiness SubTabStrip (FBS-206, TAC-4135, AC-18005-2 and -8) ----
|
|
2328
|
+
//
|
|
2329
|
+
// The Readiness tab carries a SubTabStrip with five sub-views
|
|
2330
|
+
// (overview, questions, blocking, coverage, trace). The strip and
|
|
2331
|
+
// the sub-panels live inside #tab-readiness, so they survive an
|
|
2332
|
+
// SSE innerHTML swap of #rcf-live-content (the swap re-runs
|
|
2333
|
+
// onReady, which calls wireReadinessSubTabStrip and resolveHash;
|
|
2334
|
+
// the latter calls applyReadinessHash to restore sub= from the URL
|
|
2335
|
+
// when the hash has it). Unknown sub= drops to overview and is
|
|
2336
|
+
// removed from the hash.
|
|
2337
|
+
|
|
2338
|
+
var READINESS_SUBS = ['overview', 'questions', 'blocking', 'coverage', 'trace'];
|
|
2339
|
+
|
|
2340
|
+
function readinessSubTabStrip() {
|
|
2341
|
+
return document.querySelector('[data-rcf-subtabstrip="readiness"]');
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
function readinessSubPanel(sub) {
|
|
2345
|
+
return document.querySelector('[data-rcf-subpanel="' + sub + '"]');
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
function activateReadinessSub(sub) {
|
|
2349
|
+
if (READINESS_SUBS.indexOf(sub) === -1) sub = 'overview';
|
|
2350
|
+
var strip = readinessSubTabStrip();
|
|
2351
|
+
if (strip) {
|
|
2352
|
+
var btns = strip.querySelectorAll('[role="tab"]');
|
|
2353
|
+
for (var i = 0; i < btns.length; i += 1) {
|
|
2354
|
+
var isTarget = btns[i].getAttribute('data-sub') === sub;
|
|
2355
|
+
btns[i].setAttribute('aria-selected', isTarget ? 'true' : 'false');
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
for (var j = 0; j < READINESS_SUBS.length; j += 1) {
|
|
2359
|
+
var p = readinessSubPanel(READINESS_SUBS[j]);
|
|
2360
|
+
if (!p) continue;
|
|
2361
|
+
if (READINESS_SUBS[j] === sub) p.removeAttribute('hidden');
|
|
2362
|
+
else p.setAttribute('hidden', '');
|
|
2363
|
+
}
|
|
2364
|
+
return sub;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function currentReadinessSub() {
|
|
2368
|
+
var strip = readinessSubTabStrip();
|
|
2369
|
+
if (!strip) return 'overview';
|
|
2370
|
+
var active = strip.querySelector('[role="tab"][aria-selected="true"]');
|
|
2371
|
+
return (active && active.getAttribute('data-sub')) || 'overview';
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function readinessHashFragment() {
|
|
2375
|
+
var parts = ['tab=readiness'];
|
|
2376
|
+
var sub = currentReadinessSub();
|
|
2377
|
+
if (sub && sub !== 'overview') parts.push('sub=' + encodeURIComponent(sub));
|
|
2378
|
+
return '#' + parts.join('&');
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function writeReadinessHash() {
|
|
2382
|
+
writeHash(readinessHashFragment(), true);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
function applyReadinessHash(params) {
|
|
2386
|
+
// decodeURIComponent throws URIError on malformed input such as a
|
|
2387
|
+
// bare '%' or an incomplete escape. Treat that as unknown so the
|
|
2388
|
+
// overview fallback and hash-drop still fire (AC-18005-8).
|
|
2389
|
+
var raw = 'overview';
|
|
2390
|
+
var decodeFailed = false;
|
|
2391
|
+
if (params && params.sub) {
|
|
2392
|
+
try { raw = decodeURIComponent(params.sub); } catch (e) { decodeFailed = true; raw = params.sub; }
|
|
2393
|
+
}
|
|
2394
|
+
var known = !decodeFailed && READINESS_SUBS.indexOf(raw) !== -1;
|
|
2395
|
+
var sub = known ? raw : 'overview';
|
|
2396
|
+
activateReadinessSub(sub);
|
|
2397
|
+
// AC-18005-8: unknown sub= is dropped from the hash and the overview
|
|
2398
|
+
// is activated. Rewrite the hash in place without pushing a new entry.
|
|
2399
|
+
if (!known && params && params.sub) {
|
|
2400
|
+
try {
|
|
2401
|
+
if (window.history && typeof window.history.replaceState === 'function') {
|
|
2402
|
+
window.history.replaceState(null, '', window.location.pathname + window.location.search + readinessHashFragment());
|
|
2403
|
+
} else {
|
|
2404
|
+
window.location.hash = readinessHashFragment().slice(1);
|
|
2405
|
+
}
|
|
2406
|
+
} catch (err) { /* best effort */ }
|
|
2407
|
+
}
|
|
2408
|
+
// Trace sub-tab (FBS-208, AC-209-1/-3/-6): the active entity is the
|
|
2409
|
+
// pivot; fetch + render when the sub activates.
|
|
2410
|
+
if (sub === 'trace') {
|
|
2411
|
+
activateTraceSubView();
|
|
2412
|
+
}
|
|
2413
|
+
// Blocking sub-tab filter: carry the stage= param into the FilterBar
|
|
2414
|
+
// and apply so a verdict-grid failing-count link lands on a filtered
|
|
2415
|
+
// view. The persona= param is accepted on the same shape.
|
|
2416
|
+
if (sub === 'blocking') {
|
|
2417
|
+
var stage = '';
|
|
2418
|
+
var persona = '';
|
|
2419
|
+
try { if (params && params.stage) stage = decodeURIComponent(params.stage); } catch (e) { stage = ''; }
|
|
2420
|
+
try { if (params && params.persona) persona = decodeURIComponent(params.persona); } catch (e) { persona = ''; }
|
|
2421
|
+
var bar = document.querySelector('[data-rcf-filterbar="readiness-blocking"]');
|
|
2422
|
+
if (bar) {
|
|
2423
|
+
var stageSel = bar.querySelector('[data-filter-key="stage"]');
|
|
2424
|
+
var personaSel = bar.querySelector('[data-filter-key="persona"]');
|
|
2425
|
+
if (stageSel) stageSel.value = stage;
|
|
2426
|
+
if (personaSel) personaSel.value = persona;
|
|
2427
|
+
applyReadinessBlockingFilter(bar.parentNode);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
function wireReadinessSubTabStrip() {
|
|
2433
|
+
var strip = readinessSubTabStrip();
|
|
2434
|
+
if (!strip || strip.__rcfReadinessSubWired) return;
|
|
2435
|
+
strip.__rcfReadinessSubWired = true;
|
|
2436
|
+
var btns = strip.querySelectorAll('[role="tab"]');
|
|
2437
|
+
for (var i = 0; i < btns.length; i += 1) {
|
|
2438
|
+
btns[i].addEventListener('click', function (ev) {
|
|
2439
|
+
ev.preventDefault && ev.preventDefault();
|
|
2440
|
+
var sub = ev.currentTarget.getAttribute('data-sub');
|
|
2441
|
+
if (!sub) return;
|
|
2442
|
+
activateReadinessSub(sub);
|
|
2443
|
+
writeReadinessHash();
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
// ---- Trace matrix (FBS-208, TAC-4136, ADR-4140, AC-209-1..7) --------
|
|
2450
|
+
//
|
|
2451
|
+
// The Readiness Trace sub-view fetches `./trace.json?id=<pivot>` and
|
|
2452
|
+
// `./coverage.json?scope=<reqId>` and renders the matrix into
|
|
2453
|
+
// `[data-rcf-trace-root]`. Results are kept in a page-lifetime Map
|
|
2454
|
+
// keyed by `${version}:${pivot}` (AC-209-4): a tree-update swap
|
|
2455
|
+
// invalidates nothing in the cache but marks the current render as
|
|
2456
|
+
// stale until the next fetch completes. The empty state offers the
|
|
2457
|
+
// lookup modal (AC-209-6). On >200 rows a client-side row filter
|
|
2458
|
+
// appears (AC-209-7); the full row set is retained in memory.
|
|
2459
|
+
|
|
2460
|
+
var TRACE_CACHE = Object.create(null);
|
|
2461
|
+
var TRACE_INFLIGHT = Object.create(null);
|
|
2462
|
+
var TRACE_RENDERED_PIVOT = null;
|
|
2463
|
+
var TRACE_LAST_RENDER_VERSION = 0;
|
|
2464
|
+
var TRACE_STALE = false;
|
|
2465
|
+
|
|
2466
|
+
function traceRoot() {
|
|
2467
|
+
return document.querySelector('[data-rcf-trace-root]');
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function traceVersion() {
|
|
2471
|
+
// live-client publishes the current version on window.__rcfTreeVersion
|
|
2472
|
+
// after every tree-update; before the first SSE event the first-paint
|
|
2473
|
+
// version is treated as 1.
|
|
2474
|
+
var v = window.__rcfTreeVersion;
|
|
2475
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0) return v;
|
|
2476
|
+
return 1;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
function traceCacheKey(version, pivot) {
|
|
2480
|
+
return version + ':' + pivot;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
function traceEntityFromHash() {
|
|
2484
|
+
try {
|
|
2485
|
+
var h = (window.location.hash || '').replace(/^#/, '');
|
|
2486
|
+
var params = parseHashParams(h);
|
|
2487
|
+
if (!params) return '';
|
|
2488
|
+
if (params.tab !== 'readiness' || params.sub !== 'trace') return '';
|
|
2489
|
+
return params.entity ? decodeURIComponent(params.entity) : '';
|
|
2490
|
+
} catch (e) { return ''; }
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
function parseHashParams(raw) {
|
|
2494
|
+
if (!raw) return null;
|
|
2495
|
+
var out = {};
|
|
2496
|
+
var parts = raw.split('&');
|
|
2497
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
2498
|
+
var p = parts[i];
|
|
2499
|
+
var eq = p.indexOf('=');
|
|
2500
|
+
if (eq === -1) { out[p] = ''; continue; }
|
|
2501
|
+
out[p.slice(0, eq)] = p.slice(eq + 1);
|
|
2502
|
+
}
|
|
2503
|
+
return out;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
function fetchJson(url, cb) {
|
|
2507
|
+
var xhr = new XMLHttpRequest();
|
|
2508
|
+
xhr.open('GET', url, true);
|
|
2509
|
+
xhr.onreadystatechange = function () {
|
|
2510
|
+
if (xhr.readyState !== 4) return;
|
|
2511
|
+
var ok = xhr.status >= 200 && xhr.status < 300;
|
|
2512
|
+
var parsed = null;
|
|
2513
|
+
if (ok) {
|
|
2514
|
+
try { parsed = JSON.parse(xhr.responseText); } catch (e) { parsed = null; }
|
|
2515
|
+
}
|
|
2516
|
+
cb(ok, parsed, xhr.status);
|
|
2517
|
+
};
|
|
2518
|
+
try { xhr.send(); } catch (e) { cb(false, null, 0); }
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function reqOwnerOf(pivot) {
|
|
2522
|
+
// The Coverage route takes an optional scope that must be a PRD /
|
|
2523
|
+
// REQ / US. For an AC pivot the row's REQ is the valid narrow
|
|
2524
|
+
// scope; for a US pivot the US is valid. For a REQ pivot the REQ
|
|
2525
|
+
// itself. For other kinds (TS/TC/FBS/CN/TAC/ADR) we omit scope
|
|
2526
|
+
// (tree-wide coverage). Only the AC+US+REQ prefix check is run
|
|
2527
|
+
// here, which keeps the compute small on a REQ-scoped pivot.
|
|
2528
|
+
if (typeof pivot !== 'string') return '';
|
|
2529
|
+
if (/^REQ-/.test(pivot)) return pivot;
|
|
2530
|
+
if (/^US-/.test(pivot) || /^AC-/.test(pivot)) return '';
|
|
2531
|
+
return '';
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// FBS-208 FINALISE (P2-5): the matrix pivots on upstream nodes
|
|
2535
|
+
// (REQ/US/AC). A downstream pivot (TS/TC/FBS/CN/TAC/ADR) returns
|
|
2536
|
+
// zero rows and would show a silent empty table; render an
|
|
2537
|
+
// explanatory state and offer the lookup instead.
|
|
2538
|
+
function isDownstreamPivot(pivot) {
|
|
2539
|
+
if (typeof pivot !== 'string') return false;
|
|
2540
|
+
return /^(TS-|TC-|FBS-|CN-|TAC-|ADR-)/.test(pivot);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// FBS-208 FINALISE (P1-1): on a tree-version swap the new cache key
|
|
2544
|
+
// misses; look up the most recent prior-version entry for the same
|
|
2545
|
+
// pivot so the previous render stays visible with the stale banner
|
|
2546
|
+
// while the fetch runs. The cache keys are `${version}:${pivot}`.
|
|
2547
|
+
function findPriorCachedEntry(pivot, currentVersion) {
|
|
2548
|
+
var best = null;
|
|
2549
|
+
var bestVersion = -1;
|
|
2550
|
+
for (var k in TRACE_CACHE) {
|
|
2551
|
+
if (!Object.prototype.hasOwnProperty.call(TRACE_CACHE, k)) continue;
|
|
2552
|
+
var idx = k.indexOf(':');
|
|
2553
|
+
if (idx === -1) continue;
|
|
2554
|
+
var ver = Number(k.slice(0, idx));
|
|
2555
|
+
var piv = k.slice(idx + 1);
|
|
2556
|
+
if (piv !== pivot) continue;
|
|
2557
|
+
if (!Number.isFinite(ver)) continue;
|
|
2558
|
+
if (ver >= currentVersion) continue;
|
|
2559
|
+
if (ver > bestVersion) { bestVersion = ver; best = TRACE_CACHE[k]; }
|
|
2560
|
+
}
|
|
2561
|
+
return best;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function renderTraceRoot(pivot, trace, coverage, opts) {
|
|
2565
|
+
var root = traceRoot();
|
|
2566
|
+
if (!root) return;
|
|
2567
|
+
opts = opts || {};
|
|
2568
|
+
var kind;
|
|
2569
|
+
if (opts.unavailable) kind = 'unavailable';
|
|
2570
|
+
else if (trace && trace.found === false) kind = 'unknown';
|
|
2571
|
+
else if (trace) kind = 'ok';
|
|
2572
|
+
else kind = 'shell';
|
|
2573
|
+
var staleAttr = opts.stale ? ' data-rcf-trace-stale="yes"' : '';
|
|
2574
|
+
var pivotLabel = pivot ? escapeAttr(pivot) : '';
|
|
2575
|
+
if (!pivot) {
|
|
2576
|
+
root.innerHTML = emptyStateHtml({ reason: 'no-pivot' });
|
|
2577
|
+
TRACE_RENDERED_PIVOT = '';
|
|
2578
|
+
TRACE_STALE = false;
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (kind === 'unknown') {
|
|
2582
|
+
root.innerHTML = emptyStateHtml({ reason: 'unknown-pivot', pivot: pivotLabel });
|
|
2583
|
+
TRACE_RENDERED_PIVOT = pivot;
|
|
2584
|
+
TRACE_STALE = false;
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (kind === 'unavailable') {
|
|
2588
|
+
// FBS-208 FINALISE (P2-6): a 5xx or network error is NOT the
|
|
2589
|
+
// same as 'unknown-pivot'; the chain may still carry the id.
|
|
2590
|
+
// Render a trace-unavailable state and keep the pivot visible.
|
|
2591
|
+
root.innerHTML = '<section class="rcf-trace-matrix rcf-trace-matrix--empty" data-rcf-trace-matrix="unavailable" data-rcf-trace-reason="fetch-error" data-rcf-pivot="' + pivotLabel + '" aria-labelledby="rcf-readiness-trace-heading"><header class="rcf-trace-matrix__head"><h3 id="rcf-readiness-trace-heading">Trace unavailable for ' + pivotLabel + '</h3></header><p>The trace service did not respond. Try again or pick another id.</p><p><button type="button" class="rcf-trace-matrix__lookup" data-rcf-trace-open-lookup="yes">Open the lookup</button></p></section>';
|
|
2592
|
+
TRACE_RENDERED_PIVOT = pivot;
|
|
2593
|
+
TRACE_STALE = false;
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
if (kind === 'shell') {
|
|
2597
|
+
root.innerHTML = '<section class="rcf-trace-matrix" data-rcf-trace-matrix="shell" data-rcf-pivot="' + pivotLabel + '"' + staleAttr + ' aria-labelledby="rcf-readiness-trace-heading"><header class="rcf-trace-matrix__head"><h3 id="rcf-readiness-trace-heading">Trace matrix for ' + pivotLabel + '</h3></header><p class="muted">Loading trace for ' + pivotLabel + '...</p></section>';
|
|
2598
|
+
TRACE_RENDERED_PIVOT = pivot;
|
|
2599
|
+
TRACE_LAST_RENDER_VERSION = traceVersion();
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
// FBS-208 FINALISE (P2-5): a TS/TC/FBS/CN/TAC/ADR pivot has no
|
|
2603
|
+
// upstream US/AC rows, so the matrix would be silently empty.
|
|
2604
|
+
// Render an explanatory state instead and offer the lookup.
|
|
2605
|
+
if (isDownstreamPivot(pivot)) {
|
|
2606
|
+
root.innerHTML = '<section class="rcf-trace-matrix rcf-trace-matrix--empty" data-rcf-trace-matrix="downstream" data-rcf-trace-reason="downstream-pivot" data-rcf-pivot="' + pivotLabel + '" aria-labelledby="rcf-readiness-trace-heading"><header class="rcf-trace-matrix__head"><h3 id="rcf-readiness-trace-heading">No upstream rows for ' + pivotLabel + '</h3></header><p>The matrix pivots on a requirement, story or criterion. Pick an upstream id that reaches <code>' + pivotLabel + '</code>.</p><p><button type="button" class="rcf-trace-matrix__lookup" data-rcf-trace-open-lookup="yes">Open the lookup</button></p></section>';
|
|
2607
|
+
TRACE_RENDERED_PIVOT = pivot;
|
|
2608
|
+
TRACE_LAST_RENDER_VERSION = traceVersion();
|
|
2609
|
+
TRACE_STALE = false;
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
// Full render from the client build helper.
|
|
2613
|
+
try {
|
|
2614
|
+
var rows = buildMatrixRowsFromPayload(trace, coverage);
|
|
2615
|
+
root.innerHTML = renderMatrixHtml(pivot, rows, opts.stale);
|
|
2616
|
+
TRACE_RENDERED_PIVOT = pivot;
|
|
2617
|
+
TRACE_LAST_RENDER_VERSION = traceVersion();
|
|
2618
|
+
TRACE_STALE = Boolean(opts.stale);
|
|
2619
|
+
} catch (e) {
|
|
2620
|
+
root.innerHTML = '<section class="rcf-trace-matrix rcf-trace-matrix--empty" data-rcf-trace-matrix="error" aria-labelledby="rcf-readiness-trace-heading"><header class="rcf-trace-matrix__head"><h3 id="rcf-readiness-trace-heading">Trace unavailable</h3></header><p>The trace response could not be rendered. Try again or pick another id.</p></section>';
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
function emptyStateHtml(args) {
|
|
2625
|
+
var reason = args.reason;
|
|
2626
|
+
var pivot = args.pivot || '';
|
|
2627
|
+
var heading = reason === 'unknown-pivot' && pivot
|
|
2628
|
+
? 'No trace for ' + pivot
|
|
2629
|
+
: 'Pick an id to trace';
|
|
2630
|
+
var body = reason === 'unknown-pivot'
|
|
2631
|
+
? '<p>The id <code>' + pivot + '</code> is not in the current tree.</p>'
|
|
2632
|
+
+ '<p>Pick another id below.</p>'
|
|
2633
|
+
+ '<p><button type="button" class="rcf-trace-matrix__lookup" data-rcf-trace-open-lookup="yes">Open the lookup</button></p>'
|
|
2634
|
+
: '<p>Pick a requirement, story or criterion through the Trace action on any readiness row, or open the lookup to search by id.</p>'
|
|
2635
|
+
+ '<p><button type="button" class="rcf-trace-matrix__lookup" data-rcf-trace-open-lookup="yes">Open the lookup</button></p>';
|
|
2636
|
+
var pivotAttr = pivot ? ' data-rcf-pivot="' + pivot + '"' : '';
|
|
2637
|
+
return '<section class="rcf-trace-matrix rcf-trace-matrix--empty" data-rcf-trace-matrix="empty" data-rcf-trace-reason="' + reason + '"' + pivotAttr + ' aria-labelledby="rcf-readiness-trace-heading"><header class="rcf-trace-matrix__head"><h3 id="rcf-readiness-trace-heading">' + heading + '</h3></header>' + body + '</section>';
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
var TRACE_COL_LABEL = { ts: 'Suites', tc: 'Cases', fbs: 'Build specs', cn: 'Components' };
|
|
2641
|
+
|
|
2642
|
+
function buildMatrixRowsFromPayload(trace, coverage) {
|
|
2643
|
+
if (!trace || !trace.found || !Array.isArray(trace.nodes) || !Array.isArray(trace.edges)) return [];
|
|
2644
|
+
var nodes = trace.nodes;
|
|
2645
|
+
var edges = trace.edges;
|
|
2646
|
+
var byId = {};
|
|
2647
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
2648
|
+
var n = nodes[i];
|
|
2649
|
+
if (n && typeof n.id === 'string' && typeof n.kind === 'string') byId[n.id] = n;
|
|
2650
|
+
}
|
|
2651
|
+
var outFromId = {};
|
|
2652
|
+
for (var j = 0; j < edges.length; j += 1) {
|
|
2653
|
+
var e = edges[j];
|
|
2654
|
+
if (!e || typeof e.from !== 'string' || typeof e.to !== 'string') continue;
|
|
2655
|
+
if (!outFromId[e.from]) outFromId[e.from] = [];
|
|
2656
|
+
outFromId[e.from].push(e.to);
|
|
2657
|
+
}
|
|
2658
|
+
function reachFrom(id) {
|
|
2659
|
+
// FBS-208 FINALISE (P2-4): an AC's Cases column must list only
|
|
2660
|
+
// the AC's OWN test cases, not every sibling TC the AC's TS also
|
|
2661
|
+
// contains. Stop traversal at a TS boundary: TS is recorded in
|
|
2662
|
+
// the bucket but we do not descend into its children. TC still
|
|
2663
|
+
// reaches via the AC's direct testPointer edges (AC->TC).
|
|
2664
|
+
var bucket = { ts: [], tc: [], fbs: [], cn: [] };
|
|
2665
|
+
var seen = {}; seen[id] = true;
|
|
2666
|
+
var stack = [id];
|
|
2667
|
+
while (stack.length > 0) {
|
|
2668
|
+
var cur = stack.pop();
|
|
2669
|
+
var curKind = byId[cur] && byId[cur].kind;
|
|
2670
|
+
if (curKind === 'ts' || curKind === 'testSuite') continue;
|
|
2671
|
+
var kids = outFromId[cur] || [];
|
|
2672
|
+
for (var k = 0; k < kids.length; k += 1) {
|
|
2673
|
+
var to = kids[k];
|
|
2674
|
+
if (seen[to]) continue; seen[to] = true;
|
|
2675
|
+
var kind = byId[to] && byId[to].kind;
|
|
2676
|
+
if (kind === 'ts' || kind === 'testSuite') bucket.ts.push(to);
|
|
2677
|
+
else if (kind === 'tc' || kind === 'testCase') bucket.tc.push(to);
|
|
2678
|
+
else if (kind === 'fbs') bucket.fbs.push(to);
|
|
2679
|
+
else if (kind === 'cn' || kind === 'codeNode') bucket.cn.push(to);
|
|
2680
|
+
stack.push(to);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
for (var kk in bucket) {
|
|
2684
|
+
if (!Object.prototype.hasOwnProperty.call(bucket, kk)) continue;
|
|
2685
|
+
bucket[kk] = Array.from(new Set(bucket[kk])).sort();
|
|
2686
|
+
}
|
|
2687
|
+
return bucket;
|
|
2688
|
+
}
|
|
2689
|
+
var acCov = {};
|
|
2690
|
+
if (coverage && Array.isArray(coverage.requirements)) {
|
|
2691
|
+
for (var ri = 0; ri < coverage.requirements.length; ri += 1) {
|
|
2692
|
+
var req = coverage.requirements[ri];
|
|
2693
|
+
var acs = (req && req.acs) || [];
|
|
2694
|
+
for (var ai = 0; ai < acs.length; ai += 1) {
|
|
2695
|
+
var ac = acs[ai];
|
|
2696
|
+
if (ac && typeof ac.id === 'string') acCov[ac.id] = ac;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
var usRows = [];
|
|
2701
|
+
var acsByUs = {};
|
|
2702
|
+
var standaloneAcs = [];
|
|
2703
|
+
for (var ni = 0; ni < nodes.length; ni += 1) {
|
|
2704
|
+
var nn = nodes[ni];
|
|
2705
|
+
if (nn.kind === 'userStory' || nn.kind === 'us') { usRows.push(nn); acsByUs[nn.id] = []; }
|
|
2706
|
+
}
|
|
2707
|
+
for (var ni2 = 0; ni2 < nodes.length; ni2 += 1) {
|
|
2708
|
+
var mm = nodes[ni2]; if (mm.kind !== 'ac') continue;
|
|
2709
|
+
var parentUs = null;
|
|
2710
|
+
for (var ei = 0; ei < edges.length; ei += 1) {
|
|
2711
|
+
var ee = edges[ei];
|
|
2712
|
+
if (ee.to === mm.id && ee.kind === 'parentChild') {
|
|
2713
|
+
var pp = byId[ee.from];
|
|
2714
|
+
if (pp && (pp.kind === 'userStory' || pp.kind === 'us')) { parentUs = pp.id; break; }
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (parentUs && acsByUs[parentUs]) acsByUs[parentUs].push(mm);
|
|
2718
|
+
else standaloneAcs.push(mm);
|
|
2719
|
+
}
|
|
2720
|
+
function resolutionFor(acId, kindKey, ids) {
|
|
2721
|
+
if (!ids || ids.length === 0) return 'none';
|
|
2722
|
+
if (kindKey !== 'tc') return 'resolving';
|
|
2723
|
+
var cov = acCov[acId];
|
|
2724
|
+
if (!cov) return 'unresolved';
|
|
2725
|
+
var resolvedSet = {}; (cov.testCases || []).forEach(function (x) { resolvedSet[x] = true; });
|
|
2726
|
+
var unresolvedSet = {}; (cov.unresolvedTestCases || []).forEach(function (x) { unresolvedSet[x] = true; });
|
|
2727
|
+
var hasUnres = false, hasRes = false;
|
|
2728
|
+
for (var ii = 0; ii < ids.length; ii += 1) {
|
|
2729
|
+
var id = ids[ii];
|
|
2730
|
+
if (unresolvedSet[id]) hasUnres = true;
|
|
2731
|
+
else if (resolvedSet[id]) hasRes = true;
|
|
2732
|
+
}
|
|
2733
|
+
if (hasUnres) return 'unresolved';
|
|
2734
|
+
if (hasRes) return 'resolving';
|
|
2735
|
+
return 'none';
|
|
2736
|
+
}
|
|
2737
|
+
function acRow(ac, parentUsId) {
|
|
2738
|
+
var r = reachFrom(ac.id);
|
|
2739
|
+
return {
|
|
2740
|
+
id: ac.id, kind: 'ac', title: ac.title || '', parent: parentUsId || '',
|
|
2741
|
+
cells: ['ts', 'tc', 'fbs', 'cn'].map(function (k) {
|
|
2742
|
+
return { kind: k, reached: r[k].length > 0, resolution: resolutionFor(ac.id, k, r[k]), ids: r[k] };
|
|
2743
|
+
}),
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
function usRow(us, acsUnder) {
|
|
2747
|
+
var u = { ts: {}, tc: {}, fbs: {}, cn: {} };
|
|
2748
|
+
acsUnder.forEach(function (a) {
|
|
2749
|
+
var r = reachFrom(a.id);
|
|
2750
|
+
['ts', 'tc', 'fbs', 'cn'].forEach(function (k) { r[k].forEach(function (v) { u[k][v] = true; }); });
|
|
2751
|
+
});
|
|
2752
|
+
var bucket = { ts: Object.keys(u.ts).sort(), tc: Object.keys(u.tc).sort(), fbs: Object.keys(u.fbs).sort(), cn: Object.keys(u.cn).sort() };
|
|
2753
|
+
var anyAcUnres = acsUnder.some(function (a) { var c = acCov[a.id]; return c ? c.covered !== true : bucket.tc.length > 0; });
|
|
2754
|
+
return {
|
|
2755
|
+
id: us.id, kind: 'us', title: us.title || '', parent: '',
|
|
2756
|
+
cells: ['ts', 'tc', 'fbs', 'cn'].map(function (k) {
|
|
2757
|
+
var res = k === 'tc' ? (bucket.tc.length === 0 ? 'none' : (anyAcUnres ? 'unresolved' : 'resolving')) : (bucket[k].length === 0 ? 'none' : 'resolving');
|
|
2758
|
+
return { kind: k, reached: bucket[k].length > 0, resolution: res, ids: bucket[k] };
|
|
2759
|
+
}),
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
var rows = [];
|
|
2763
|
+
for (var ui = 0; ui < usRows.length; ui += 1) {
|
|
2764
|
+
var u2 = usRows[ui]; var ks = acsByUs[u2.id] || [];
|
|
2765
|
+
rows.push(usRow(u2, ks));
|
|
2766
|
+
for (var k2 = 0; k2 < ks.length; k2 += 1) rows.push(acRow(ks[k2], u2.id));
|
|
2767
|
+
}
|
|
2768
|
+
for (var s = 0; s < standaloneAcs.length; s += 1) rows.push(acRow(standaloneAcs[s], ''));
|
|
2769
|
+
return rows;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
function escapeAttr(s) {
|
|
2773
|
+
return String(s == null ? '' : s)
|
|
2774
|
+
.replace(/&/g, '&')
|
|
2775
|
+
.replace(/</g, '<')
|
|
2776
|
+
.replace(/>/g, '>')
|
|
2777
|
+
.replace(/"/g, '"')
|
|
2778
|
+
.replace(/'/g, ''');
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
function renderMatrixHtml(pivot, rows, stale) {
|
|
2782
|
+
var filterBar = rows.length > 200
|
|
2783
|
+
? '<div class="rcf-trace-matrix__filter" data-rcf-filterbar="trace-matrix" role="search"><label><span>Filter rows (story id or criterion id)</span><input type="search" data-rcf-filter-key="rowId" placeholder="e.g. US-209 or AC-209-3"></label></div>'
|
|
2784
|
+
: '';
|
|
2785
|
+
var staleBanner = stale
|
|
2786
|
+
? '<p class="rcf-trace-matrix__stale muted small" data-rcf-trace-stale="yes">Tree changed. The matrix below reflects the previous version; a fresh trace is being fetched.</p>'
|
|
2787
|
+
: '';
|
|
2788
|
+
var head = '<thead><tr><th scope="col" data-rcf-col="row">Story / criterion</th><th scope="col" data-rcf-col="ts" data-rcf-trace-col="ts">Suites</th><th scope="col" data-rcf-col="tc" data-rcf-trace-col="tc">Cases</th><th scope="col" data-rcf-col="fbs" data-rcf-trace-col="fbs">Build specs</th><th scope="col" data-rcf-col="cn" data-rcf-trace-col="cn">Components</th></tr></thead>';
|
|
2789
|
+
var body = rows.map(function (r) {
|
|
2790
|
+
var kindAttr = r.kind === 'us' ? 'us' : 'ac';
|
|
2791
|
+
var parentAttr = r.parent ? ' data-rcf-parent="' + escapeAttr(r.parent) + '"' : '';
|
|
2792
|
+
var titleLabel = r.title ? ' <span class="rcf-trace-matrix__row-title muted small">' + escapeAttr(r.title) + '</span>' : '';
|
|
2793
|
+
var idLink = '<a href="#' + escapeAttr(r.id) + '" data-rcf-trace-row-id="' + escapeAttr(r.id) + '">' + escapeAttr(r.id) + '</a>';
|
|
2794
|
+
var cells = r.cells.map(function (c) {
|
|
2795
|
+
var label = c.ids.length === 0
|
|
2796
|
+
? '<span class="rcf-sr-only">' + (c.reached ? 'reached' : 'not reached') + '</span>'
|
|
2797
|
+
: '<span class="rcf-trace-matrix__cell-ids" data-rcf-cell-ids="' + escapeAttr(c.ids.join(',')) + '">' + escapeAttr(c.ids.length === 1 ? c.ids[0] : c.ids.length + ' ids') + '</span>';
|
|
2798
|
+
return '<td data-rcf-col="' + c.kind + '" data-rcf-trace-col="' + c.kind + '" data-rcf-cell-reach="' + (c.reached ? 'yes' : 'no') + '" data-rcf-cell-resolution="' + c.resolution + '" title="' + escapeAttr((TRACE_COL_LABEL[c.kind] || c.kind) + ': ' + (c.reached ? 'reached' : 'not reached') + ', resolution ' + c.resolution) + '">' + label + '</td>';
|
|
2799
|
+
}).join('');
|
|
2800
|
+
return '<tr data-rcf-row-id="' + escapeAttr(r.id) + '" data-rcf-row-kind="' + kindAttr + '"' + parentAttr + '><th scope="row" data-rcf-col="row">' + idLink + titleLabel + '</th>' + cells + '</tr>';
|
|
2801
|
+
}).join('');
|
|
2802
|
+
return '<section class="rcf-trace-matrix" data-rcf-trace-matrix="yes" data-rcf-pivot="' + escapeAttr(pivot) + '" aria-labelledby="rcf-readiness-trace-heading"><header class="rcf-trace-matrix__head"><h3 id="rcf-readiness-trace-heading">Trace matrix for ' + escapeAttr(pivot) + '</h3> <span class="rcf-badge rcf-badge--count">' + rows.length + '</span></header>' + staleBanner + filterBar + '<table class="rcf-trace-matrix__table" aria-describedby="rcf-readiness-trace-heading">' + head + '<tbody>' + body + '</tbody></table></section>';
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function fetchAndRenderTrace(pivot) {
|
|
2806
|
+
if (!pivot) { renderTraceRoot('', null, null, {}); return; }
|
|
2807
|
+
var v = traceVersion();
|
|
2808
|
+
var key = traceCacheKey(v, pivot);
|
|
2809
|
+
var cached = TRACE_CACHE[key];
|
|
2810
|
+
if (cached && cached.trace) {
|
|
2811
|
+
renderTraceRoot(pivot, cached.trace, cached.coverage || null, { stale: false });
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
if (TRACE_INFLIGHT[key]) return;
|
|
2815
|
+
TRACE_INFLIGHT[key] = true;
|
|
2816
|
+
// FBS-208 FINALISE (P1-1): on a tree-version swap the cache for
|
|
2817
|
+
// the new version misses. Look up a prior-version entry for the
|
|
2818
|
+
// same pivot and render it as stale while we fetch; the user sees
|
|
2819
|
+
// the previous matrix with a stale banner, not an empty shell.
|
|
2820
|
+
var prior = findPriorCachedEntry(pivot, v);
|
|
2821
|
+
if (prior && prior.trace) {
|
|
2822
|
+
renderTraceRoot(pivot, prior.trace, prior.coverage || null, { stale: true });
|
|
2823
|
+
} else {
|
|
2824
|
+
renderTraceRoot(pivot, null, null, { stale: TRACE_STALE });
|
|
2825
|
+
}
|
|
2826
|
+
var traceUrl = './trace.json?id=' + encodeURIComponent(pivot) + '&direction=forward&includeCode=1';
|
|
2827
|
+
fetchJson(traceUrl, function (ok, parsed, status) {
|
|
2828
|
+
// FBS-208 FINALISE (P2-6, stale-response guard): a hash change
|
|
2829
|
+
// since the fetch started means another pivot owns the view;
|
|
2830
|
+
// drop this callback's render.
|
|
2831
|
+
if (traceEntityFromHash() !== pivot) {
|
|
2832
|
+
TRACE_INFLIGHT[key] = false;
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (!ok || !parsed) {
|
|
2836
|
+
TRACE_INFLIGHT[key] = false;
|
|
2837
|
+
// FBS-208 FINALISE (P2-6): distinguish a 404 (unknown-pivot,
|
|
2838
|
+
// the id is not in the tree) from any other failure (service
|
|
2839
|
+
// unavailable, parse error, network). Only a 404 renders the
|
|
2840
|
+
// 'id not in current tree' empty-state.
|
|
2841
|
+
if (status === 404) {
|
|
2842
|
+
renderTraceRoot(pivot, { found: false, pivot: pivot }, null, {});
|
|
2843
|
+
} else {
|
|
2844
|
+
renderTraceRoot(pivot, null, null, { unavailable: true });
|
|
2845
|
+
}
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
if (parsed.found === false) {
|
|
2849
|
+
TRACE_INFLIGHT[key] = false;
|
|
2850
|
+
TRACE_CACHE[key] = { trace: parsed, coverage: null };
|
|
2851
|
+
renderTraceRoot(pivot, parsed, null, {});
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
var scope = reqOwnerOf(pivot);
|
|
2855
|
+
var covUrl = './coverage.json' + (scope ? '?scope=' + encodeURIComponent(scope) : '');
|
|
2856
|
+
fetchJson(covUrl, function (okCov, parsedCov) {
|
|
2857
|
+
TRACE_INFLIGHT[key] = false;
|
|
2858
|
+
// Stale-response guard again on the inner fetch.
|
|
2859
|
+
if (traceEntityFromHash() !== pivot) return;
|
|
2860
|
+
TRACE_CACHE[key] = { trace: parsed, coverage: okCov ? parsedCov : null };
|
|
2861
|
+
renderTraceRoot(pivot, parsed, okCov ? parsedCov : null, { stale: false });
|
|
2862
|
+
});
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// Called from applyReadinessHash when sub=trace activates, and after an
|
|
2867
|
+
// SSE swap when the Trace sub-tab is already active.
|
|
2868
|
+
function activateTraceSubView() {
|
|
2869
|
+
var pivot = traceEntityFromHash();
|
|
2870
|
+
fetchAndRenderTrace(pivot);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// AC-209-4: on an SSE swap `rcfPage.init()` is invoked. We mark the
|
|
2874
|
+
// current render stale when the version changed and refetch; the
|
|
2875
|
+
// previous render stays visible with the stale banner until the new
|
|
2876
|
+
// bytes arrive.
|
|
2877
|
+
function maybeRefetchTraceAfterSse() {
|
|
2878
|
+
if (TRACE_RENDERED_PIVOT === null) return;
|
|
2879
|
+
var v = traceVersion();
|
|
2880
|
+
if (v === TRACE_LAST_RENDER_VERSION) return;
|
|
2881
|
+
TRACE_STALE = true;
|
|
2882
|
+
var root = traceRoot();
|
|
2883
|
+
if (root) {
|
|
2884
|
+
// Prepend the stale banner to the existing render.
|
|
2885
|
+
var head = root.querySelector('.rcf-trace-matrix__head');
|
|
2886
|
+
if (head && !root.querySelector('[data-rcf-trace-stale]')) {
|
|
2887
|
+
var p = document.createElement('p');
|
|
2888
|
+
p.className = 'rcf-trace-matrix__stale muted small';
|
|
2889
|
+
p.setAttribute('data-rcf-trace-stale', 'yes');
|
|
2890
|
+
p.textContent = 'Tree changed. The matrix below reflects the previous version; a fresh trace is being fetched.';
|
|
2891
|
+
head.parentNode.insertBefore(p, head.nextSibling);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
var pivot = TRACE_RENDERED_PIVOT;
|
|
2895
|
+
if (pivot) fetchAndRenderTrace(pivot);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
function wireTraceMatrix() {
|
|
2899
|
+
var root = traceRoot();
|
|
2900
|
+
if (!root) return;
|
|
2901
|
+
if (root.__rcfTraceWired) {
|
|
2902
|
+
// Still re-check: an SSE swap re-invokes onReady, which should
|
|
2903
|
+
// re-fetch when the version has changed (AC-209-4).
|
|
2904
|
+
maybeRefetchTraceAfterSse();
|
|
2905
|
+
// Also render if the sub-tab is currently active and the hash
|
|
2906
|
+
// changed to a new entity while we were already wired.
|
|
2907
|
+
var panel = readinessSubPanel('trace');
|
|
2908
|
+
if (panel && !panel.hasAttribute('hidden')) activateTraceSubView();
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
root.__rcfTraceWired = true;
|
|
2912
|
+
// Click delegation for "Open the lookup" buttons in the empty state.
|
|
2913
|
+
root.addEventListener('click', function (ev) {
|
|
2914
|
+
var el = ev.target;
|
|
2915
|
+
while (el && el !== root) {
|
|
2916
|
+
if (el.getAttribute && el.getAttribute('data-rcf-trace-open-lookup') === 'yes') {
|
|
2917
|
+
ev.preventDefault && ev.preventDefault();
|
|
2918
|
+
try { lookupOpen(); } catch (e) { /* soft */ }
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
el = el.parentNode;
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
// Row filter (AC-209-7).
|
|
2925
|
+
root.addEventListener('input', function (ev) {
|
|
2926
|
+
var input = ev.target;
|
|
2927
|
+
if (!input || !input.getAttribute) return;
|
|
2928
|
+
if (input.getAttribute('data-rcf-filter-key') !== 'rowId') return;
|
|
2929
|
+
var q = (input.value || '').trim().toLowerCase();
|
|
2930
|
+
var rows = root.querySelectorAll('tr[data-rcf-row-id]');
|
|
2931
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
2932
|
+
var r = rows[i];
|
|
2933
|
+
var id = (r.getAttribute('data-rcf-row-id') || '').toLowerCase();
|
|
2934
|
+
var parent = (r.getAttribute('data-rcf-parent') || '').toLowerCase();
|
|
2935
|
+
var match = q === '' || id.indexOf(q) !== -1 || parent.indexOf(q) !== -1;
|
|
2936
|
+
if (match) r.removeAttribute('hidden');
|
|
2937
|
+
else r.setAttribute('hidden', '');
|
|
2938
|
+
}
|
|
2939
|
+
});
|
|
2940
|
+
activateTraceSubView();
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2275
2943
|
// AC-205-5: a mount URL of `?tab=requirements&entity=US-304` lands on the
|
|
2276
2944
|
// same position at first paint. The hash-router owns tab + entity, so we
|
|
2277
2945
|
// promote the tab/sub/entity query params into the hash fragment at boot
|
|
@@ -2294,6 +2962,120 @@
|
|
|
2294
2962
|
} catch (err) { /* best effort; resolveHash will no-op on empty hash */ }
|
|
2295
2963
|
}
|
|
2296
2964
|
|
|
2965
|
+
// FBS-204 (TAC-4135 AC-18003-5): Copy for your agent handles.
|
|
2966
|
+
//
|
|
2967
|
+
// Each questions-table row carries one `[data-rcf-copy-for]` button
|
|
2968
|
+
// and a `data-rcf-copy-text` attribute holding one line: `<itemId>:
|
|
2969
|
+
// <ask>` (no command text; AC-18003-7). The button copies that line
|
|
2970
|
+
// to the clipboard and fires the shared toast helper. The handler is
|
|
2971
|
+
// idempotent across SSE swaps via the __rcfCopyWired sentinel.
|
|
2972
|
+
function wireReadinessCopyHandles() {
|
|
2973
|
+
var buttons = document.querySelectorAll('[data-rcf-copy-for]');
|
|
2974
|
+
for (var i = 0; i < buttons.length; i += 1) {
|
|
2975
|
+
var btn = buttons[i];
|
|
2976
|
+
if (btn.__rcfCopyWired) continue;
|
|
2977
|
+
btn.__rcfCopyWired = true;
|
|
2978
|
+
btn.addEventListener('click', function (ev) {
|
|
2979
|
+
var target = ev.currentTarget;
|
|
2980
|
+
var text = target.getAttribute('data-rcf-copy-text') || '';
|
|
2981
|
+
copyToClipboardOrFallback(text, function (ok) {
|
|
2982
|
+
var msg = ok ? 'Copied for your agent.' : 'Could not copy; the text stays on the page.';
|
|
2983
|
+
try { showToast(msg); } catch (e) { /* no toast node */ }
|
|
2984
|
+
});
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
// FBS-204 (TAC-4135 AC-18003-1): sortable columns on the questions
|
|
2990
|
+
// table. Every header with `data-rcf-sortable="yes"` becomes a
|
|
2991
|
+
// keyboard-reachable button that sorts the table's tbody rows by
|
|
2992
|
+
// the matching `data-rcf-col` cell. Toggles ascending / descending;
|
|
2993
|
+
// clears the other headers' aria-sort. Idempotent across SSE swaps
|
|
2994
|
+
// via a __rcfSortWired sentinel. No external deps, no CSS beyond
|
|
2995
|
+
// aria-sort which the stylesheet already respects.
|
|
2996
|
+
function wireReadinessSortableHeaders() {
|
|
2997
|
+
var headers = document.querySelectorAll('table.rcf-cmd-table__table th[data-rcf-sortable="yes"]');
|
|
2998
|
+
for (var i = 0; i < headers.length; i += 1) {
|
|
2999
|
+
var th = headers[i];
|
|
3000
|
+
if (th.__rcfSortWired) continue;
|
|
3001
|
+
th.__rcfSortWired = true;
|
|
3002
|
+
th.setAttribute('role', 'button');
|
|
3003
|
+
if (!th.hasAttribute('tabindex')) th.setAttribute('tabindex', '0');
|
|
3004
|
+
th.addEventListener('click', function (ev) { sortTableByHeader(ev.currentTarget); });
|
|
3005
|
+
th.addEventListener('keydown', function (ev) {
|
|
3006
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
3007
|
+
ev.preventDefault();
|
|
3008
|
+
sortTableByHeader(ev.currentTarget);
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
function sortTableByHeader(th) {
|
|
3015
|
+
var table = th && th.closest ? th.closest('table.rcf-cmd-table__table') : null;
|
|
3016
|
+
if (!table) return;
|
|
3017
|
+
var col = th.getAttribute('data-rcf-col');
|
|
3018
|
+
if (!col) return;
|
|
3019
|
+
var current = th.getAttribute('aria-sort');
|
|
3020
|
+
var dir = current === 'ascending' ? 'descending' : 'ascending';
|
|
3021
|
+
var peers = table.querySelectorAll('th[data-rcf-sortable="yes"]');
|
|
3022
|
+
for (var i = 0; i < peers.length; i += 1) peers[i].removeAttribute('aria-sort');
|
|
3023
|
+
th.setAttribute('aria-sort', dir);
|
|
3024
|
+
var tbody = table.querySelector('tbody');
|
|
3025
|
+
if (!tbody) return;
|
|
3026
|
+
var rows = [];
|
|
3027
|
+
for (var r = 0; r < tbody.children.length; r += 1) {
|
|
3028
|
+
if (tbody.children[r].tagName && tbody.children[r].tagName.toLowerCase() === 'tr') {
|
|
3029
|
+
rows.push(tbody.children[r]);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
var isNumeric = col === 'number';
|
|
3033
|
+
rows.sort(function (a, b) {
|
|
3034
|
+
var ac = a.querySelector('td[data-rcf-col="' + col + '"]');
|
|
3035
|
+
var bc = b.querySelector('td[data-rcf-col="' + col + '"]');
|
|
3036
|
+
var av = ac ? (ac.textContent || '').trim() : '';
|
|
3037
|
+
var bv = bc ? (bc.textContent || '').trim() : '';
|
|
3038
|
+
if (isNumeric) {
|
|
3039
|
+
var an = Number(av); var bn = Number(bv);
|
|
3040
|
+
return dir === 'ascending' ? (an - bn) : (bn - an);
|
|
3041
|
+
}
|
|
3042
|
+
return dir === 'ascending' ? av.localeCompare(bv) : bv.localeCompare(av);
|
|
3043
|
+
});
|
|
3044
|
+
for (var j = 0; j < rows.length; j += 1) tbody.appendChild(rows[j]);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
// Copy helper that prefers navigator.clipboard and falls back to a
|
|
3048
|
+
// transient textarea + document.execCommand for older browsers and
|
|
3049
|
+
// embedded contexts without the Permissions API grant.
|
|
3050
|
+
function copyToClipboardOrFallback(text, cb) {
|
|
3051
|
+
var done = typeof cb === 'function' ? cb : function () {};
|
|
3052
|
+
var val = text == null ? '' : String(text);
|
|
3053
|
+
try {
|
|
3054
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
3055
|
+
navigator.clipboard.writeText(val).then(function () { done(true); }, function () { done(fallbackCopy(val)); });
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
} catch (e) { /* fall through to the textarea path */ }
|
|
3059
|
+
done(fallbackCopy(val));
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function fallbackCopy(val) {
|
|
3063
|
+
try {
|
|
3064
|
+
var ta = document.createElement('textarea');
|
|
3065
|
+
ta.value = val;
|
|
3066
|
+
ta.setAttribute('readonly', '');
|
|
3067
|
+
ta.style.position = 'fixed';
|
|
3068
|
+
ta.style.top = '-1000px';
|
|
3069
|
+
ta.style.left = '-1000px';
|
|
3070
|
+
document.body.appendChild(ta);
|
|
3071
|
+
ta.select();
|
|
3072
|
+
var ok = false;
|
|
3073
|
+
try { ok = document.execCommand && document.execCommand('copy'); } catch (e) { ok = false; }
|
|
3074
|
+
document.body.removeChild(ta);
|
|
3075
|
+
return Boolean(ok);
|
|
3076
|
+
} catch (e) { return false; }
|
|
3077
|
+
}
|
|
3078
|
+
|
|
2297
3079
|
function onReady() {
|
|
2298
3080
|
initShellFromQuery();
|
|
2299
3081
|
bootHashFromQuery();
|
|
@@ -2308,6 +3090,11 @@
|
|
|
2308
3090
|
wireFixturePage();
|
|
2309
3091
|
wireLookup();
|
|
2310
3092
|
wireStageLegend();
|
|
3093
|
+
wireReadinessCopyHandles();
|
|
3094
|
+
wireReadinessSortableHeaders();
|
|
3095
|
+
wireReadinessBlockingFilterBar();
|
|
3096
|
+
wireTraceMatrix();
|
|
3097
|
+
wireReadinessSubTabStrip();
|
|
2311
3098
|
resolveHash(window.location.hash);
|
|
2312
3099
|
if (!hashchangeWired) {
|
|
2313
3100
|
hashchangeWired = true;
|
|
@@ -2317,6 +3104,46 @@
|
|
|
2317
3104
|
}
|
|
2318
3105
|
}
|
|
2319
3106
|
|
|
3107
|
+
// FBS-204 (TAC-4135 AC-18003-3): the blocking table's FilterBar
|
|
3108
|
+
// (`data-rcf-filterbar="readiness-blocking"`) carries two <select>s
|
|
3109
|
+
// (stage, persona). Changing either hides rows that do not match.
|
|
3110
|
+
// Rows carry [data-rcf-stage] and [data-rcf-persona]; the renderer
|
|
3111
|
+
// persists the initial selection as a `selected` option so the
|
|
3112
|
+
// hash-driven first paint survives.
|
|
3113
|
+
function wireReadinessBlockingFilterBar() {
|
|
3114
|
+
var bars = document.querySelectorAll('[data-rcf-filterbar="readiness-blocking"]');
|
|
3115
|
+
for (var i = 0; i < bars.length; i += 1) {
|
|
3116
|
+
var bar = bars[i];
|
|
3117
|
+
if (bar.__rcfBlockingBarWired) continue;
|
|
3118
|
+
bar.__rcfBlockingBarWired = true;
|
|
3119
|
+
bar.addEventListener('change', function (ev) {
|
|
3120
|
+
var b = ev.currentTarget;
|
|
3121
|
+
var table = b.parentNode;
|
|
3122
|
+
if (!table) return;
|
|
3123
|
+
applyReadinessBlockingFilter(table);
|
|
3124
|
+
});
|
|
3125
|
+
applyReadinessBlockingFilter(bar.parentNode);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
function applyReadinessBlockingFilter(table) {
|
|
3130
|
+
if (!table) return;
|
|
3131
|
+
var bar = table.querySelector('[data-rcf-filterbar="readiness-blocking"]');
|
|
3132
|
+
if (!bar) return;
|
|
3133
|
+
var stageSel = bar.querySelector('[data-filter-key="stage"]');
|
|
3134
|
+
var personaSel = bar.querySelector('[data-filter-key="persona"]');
|
|
3135
|
+
var stage = stageSel ? stageSel.value : '';
|
|
3136
|
+
var persona = personaSel ? personaSel.value : '';
|
|
3137
|
+
var rows = table.querySelectorAll('tbody tr[data-rcf-stage], tbody tr[data-rcf-persona]');
|
|
3138
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
3139
|
+
var r = rows[i];
|
|
3140
|
+
var okStage = !stage || r.getAttribute('data-rcf-stage') === stage;
|
|
3141
|
+
var okPersona = !persona || r.getAttribute('data-rcf-persona') === persona;
|
|
3142
|
+
if (okStage && okPersona) r.removeAttribute('hidden');
|
|
3143
|
+
else r.setAttribute('hidden', '');
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
|
|
2320
3147
|
window.rcfPage = window.rcfPage || {};
|
|
2321
3148
|
window.rcfPage.init = onReady;
|
|
2322
3149
|
// Shared helpers available for later tabs to call. The toast helper
|
|
@@ -2325,6 +3152,20 @@
|
|
|
2325
3152
|
// acknowledgement.
|
|
2326
3153
|
window.rcfView = window.rcfView || {};
|
|
2327
3154
|
window.rcfView.showToast = showToast;
|
|
3155
|
+
// FBS-208 FINALISE: client-path internals exposed for behavioural
|
|
3156
|
+
// tests. The server-side `src/view/readiness/trace-matrix.js`
|
|
3157
|
+
// module is a spec-by-example; the shipped behaviour is this IIFE's
|
|
3158
|
+
// inline implementation. Tests use these to prove the two paths
|
|
3159
|
+
// match (parity) and to probe cache / downstream-pivot behaviour.
|
|
3160
|
+
// No production code paths read these; they are a test seam only.
|
|
3161
|
+
window.rcfTraceInternals = {
|
|
3162
|
+
buildMatrixRowsFromPayload: buildMatrixRowsFromPayload,
|
|
3163
|
+
renderMatrixHtml: renderMatrixHtml,
|
|
3164
|
+
isDownstreamPivot: isDownstreamPivot,
|
|
3165
|
+
findPriorCachedEntry: findPriorCachedEntry,
|
|
3166
|
+
traceCacheKey: traceCacheKey,
|
|
3167
|
+
cache: TRACE_CACHE,
|
|
3168
|
+
};
|
|
2328
3169
|
|
|
2329
3170
|
if (document.readyState === 'loading') {
|
|
2330
3171
|
document.addEventListener('DOMContentLoaded', onReady);
|