rcf-lite 0.32.4 → 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 +42 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/discovery/README.md +22 -0
- 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 +18 -1
- package/src/cli/finalise.js +141 -2
- package/src/cli/view.js +13 -1
- package/src/define/ledgers.js +9 -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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Composite id splitter for the Readiness blocking table (TAC-4135
|
|
2
|
+
// splitCompositeId, ADR-4139; FBS-204, US-18003 AC-18003-4).
|
|
3
|
+
//
|
|
4
|
+
// Failing items in `stages[].checks[].failing[]` and in
|
|
5
|
+
// `levels.*.blockedBy[].ids[]` sometimes carry a composite id of the
|
|
6
|
+
// shape `<docId>:<fragment>` (for example "AC-060-3:userID" or
|
|
7
|
+
// "TAC-4122-define-gates:checkPersona"). The blocking table needs the
|
|
8
|
+
// document part on its own so the row's link resolves through
|
|
9
|
+
// findByDocId; the fragment stays visible so the row still names the
|
|
10
|
+
// exact failing nib. Split on the first colon so the fragment keeps
|
|
11
|
+
// its own colons (TAD-001:security:foo splits to docId=TAD-001,
|
|
12
|
+
// fragment=security:foo).
|
|
13
|
+
//
|
|
14
|
+
// Pure, no I/O. Returns { docId, fragment } where `fragment` is '' when
|
|
15
|
+
// no colon is present. A null / non-string input yields { docId: '',
|
|
16
|
+
// fragment: '' } so callers can wrap defensively.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} SplitId
|
|
20
|
+
* @property {string} docId the resolvable document id (part before the first colon)
|
|
21
|
+
* @property {string} fragment the remainder (empty when there was no colon)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Split a composite failing id on the FIRST colon.
|
|
26
|
+
*
|
|
27
|
+
* @param {string | null | undefined} id
|
|
28
|
+
* @returns {SplitId}
|
|
29
|
+
*/
|
|
30
|
+
export function splitCompositeId(id) {
|
|
31
|
+
if (typeof id !== 'string' || id.length === 0) return { docId: '', fragment: '' };
|
|
32
|
+
const i = id.indexOf(':');
|
|
33
|
+
if (i === -1) return { docId: id, fragment: '' };
|
|
34
|
+
return { docId: id.slice(0, i), fragment: id.slice(i + 1) };
|
|
35
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Readiness coverage summary (FBS-205, US-18004 under REQ-180,
|
|
2
|
+
// TAC-4135-readiness-command-page, ADR-4139-readiness-tables-read-only-no-commands).
|
|
3
|
+
//
|
|
4
|
+
// Replaces renderCoverage. The old block read `coverage.tree.pass`
|
|
5
|
+
// and `coverage.tree.total`, which never exist on the real compute
|
|
6
|
+
// (coverage.tree carries `totals.{requirements, covered,
|
|
7
|
+
// coveredUnresolved, uncovered}` and `requirements[].coverageClass`
|
|
8
|
+
// plus `unresolvedTestPointers[]`). The result rendered as "pass /
|
|
9
|
+
// " blanks on every real tree. This module reads the fields that
|
|
10
|
+
// coverage actually produces and prints a stat line, one stacked
|
|
11
|
+
// bar, a family tiles row and (when non-empty) an unresolved-
|
|
12
|
+
// pointers table. Every value renders as either a number or the
|
|
13
|
+
// words "coverage unavailable"; a blank never leaves the page.
|
|
14
|
+
//
|
|
15
|
+
// Pure HTML-string render. No scripts, no commands, no write
|
|
16
|
+
// controls. Tokens stay on --sv-* (contrast-aa.test.js) and no new
|
|
17
|
+
// token is introduced.
|
|
18
|
+
|
|
19
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
20
|
+
|
|
21
|
+
const UNAVAILABLE = 'coverage unavailable';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} CoverageTreeTotals
|
|
25
|
+
* @property {number} requirements
|
|
26
|
+
* @property {number} covered
|
|
27
|
+
* @property {number} coveredUnresolved
|
|
28
|
+
* @property {number} uncovered
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} CoverageSummary
|
|
33
|
+
* @property {object} totals - the four totals and percentCovered as strings or UNAVAILABLE
|
|
34
|
+
* @property {object[]} families - per-family tile data
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shape the family counts stat tiles from the walker's tree model.
|
|
39
|
+
* The spec names six families: requirements, stories, criteria,
|
|
40
|
+
* suites, cases, build specs. Each count is derived without a
|
|
41
|
+
* separate compute - the walker already carries each list.
|
|
42
|
+
*
|
|
43
|
+
* @param {object | null | undefined} tree
|
|
44
|
+
* @returns {Array<{key: string, label: string, count: number|null}>}
|
|
45
|
+
*/
|
|
46
|
+
export function familyCounts(tree) {
|
|
47
|
+
if (!tree || typeof tree !== 'object') {
|
|
48
|
+
return SIX_FAMILIES.map((f) => ({ key: f.key, label: f.label, count: null }));
|
|
49
|
+
}
|
|
50
|
+
const reqs = Array.isArray(tree.requirements) ? tree.requirements.length : null;
|
|
51
|
+
const stories = Array.isArray(tree.userStories) ? tree.userStories.length : null;
|
|
52
|
+
let criteria = 0;
|
|
53
|
+
if (Array.isArray(tree.userStories)) {
|
|
54
|
+
for (const us of tree.userStories) {
|
|
55
|
+
if (Array.isArray(us && us.acceptanceCriteria)) criteria += us.acceptanceCriteria.length;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
criteria = null;
|
|
59
|
+
}
|
|
60
|
+
const suites = Array.isArray(tree.testSuites) ? tree.testSuites.length : null;
|
|
61
|
+
let cases = 0;
|
|
62
|
+
if (Array.isArray(tree.testSuites)) {
|
|
63
|
+
for (const ts of tree.testSuites) {
|
|
64
|
+
if (Array.isArray(ts && ts.testCases)) cases += ts.testCases.length;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
cases = null;
|
|
68
|
+
}
|
|
69
|
+
const fbsItems = Array.isArray(tree.fbsItems) ? tree.fbsItems.length : null;
|
|
70
|
+
return [
|
|
71
|
+
{ key: 'requirements', label: 'Requirements', count: reqs },
|
|
72
|
+
{ key: 'stories', label: 'Stories', count: stories },
|
|
73
|
+
{ key: 'criteria', label: 'Criteria', count: criteria },
|
|
74
|
+
{ key: 'suites', label: 'Suites', count: suites },
|
|
75
|
+
{ key: 'cases', label: 'Cases', count: cases },
|
|
76
|
+
{ key: 'buildSpecs', label: 'Build specs', count: fbsItems },
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const SIX_FAMILIES = [
|
|
81
|
+
{ key: 'requirements', label: 'Requirements' },
|
|
82
|
+
{ key: 'stories', label: 'Stories' },
|
|
83
|
+
{ key: 'criteria', label: 'Criteria' },
|
|
84
|
+
{ key: 'suites', label: 'Suites' },
|
|
85
|
+
{ key: 'cases', label: 'Cases' },
|
|
86
|
+
{ key: 'buildSpecs', label: 'Build specs' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compute the four numeric slots + percent as either numbers or the
|
|
91
|
+
* UNAVAILABLE sentinel. AC-18004-6 and AC-18004-7 bind: no slot ever
|
|
92
|
+
* renders as a blank; a missing total renders the words
|
|
93
|
+
* "coverage unavailable".
|
|
94
|
+
*/
|
|
95
|
+
export function computeCoverageSummary(coverageTree) {
|
|
96
|
+
const t = coverageTree && typeof coverageTree === 'object' ? coverageTree.totals : null;
|
|
97
|
+
if (!t || typeof t !== 'object') {
|
|
98
|
+
return {
|
|
99
|
+
totals: {
|
|
100
|
+
requirements: UNAVAILABLE,
|
|
101
|
+
covered: UNAVAILABLE,
|
|
102
|
+
coveredUnresolved: UNAVAILABLE,
|
|
103
|
+
uncovered: UNAVAILABLE,
|
|
104
|
+
},
|
|
105
|
+
percentCovered: UNAVAILABLE,
|
|
106
|
+
available: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const reqs = numOrUnavail(t.requirements);
|
|
110
|
+
const covered = numOrUnavail(t.covered);
|
|
111
|
+
const coveredUnresolved = numOrUnavail(t.coveredUnresolved);
|
|
112
|
+
const uncovered = numOrUnavail(t.uncovered);
|
|
113
|
+
const reqsN = typeof reqs === 'number' ? reqs : null;
|
|
114
|
+
const coveredN = typeof covered === 'number' ? covered : null;
|
|
115
|
+
const percent = (reqsN === null || coveredN === null || reqsN === 0)
|
|
116
|
+
? (reqsN === 0 ? '0%' : UNAVAILABLE)
|
|
117
|
+
: `${Math.round((coveredN / reqsN) * 100)}%`;
|
|
118
|
+
const available = reqs !== UNAVAILABLE && covered !== UNAVAILABLE
|
|
119
|
+
&& coveredUnresolved !== UNAVAILABLE && uncovered !== UNAVAILABLE;
|
|
120
|
+
return {
|
|
121
|
+
totals: {
|
|
122
|
+
requirements: reqs,
|
|
123
|
+
covered,
|
|
124
|
+
coveredUnresolved,
|
|
125
|
+
uncovered,
|
|
126
|
+
},
|
|
127
|
+
percentCovered: percent,
|
|
128
|
+
available,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function numOrUnavail(v) {
|
|
133
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) return UNAVAILABLE;
|
|
134
|
+
return v;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Render the coverage sub-view: a stat line, a stacked bar on
|
|
139
|
+
* --sv-success / --sv-warning / --sv-danger over --sv-surface, a
|
|
140
|
+
* family tiles row, and (when non-empty) an unresolved-pointers
|
|
141
|
+
* table. AC-18004-1/-2/-3/-6/-7.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} args
|
|
144
|
+
* @param {object | null | undefined} args.coverage - readiness.coverage
|
|
145
|
+
* @param {object | null | undefined} args.tree - the walker tree model
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function renderCoverageSummary(args) {
|
|
149
|
+
const coverage = args && args.coverage ? args.coverage : null;
|
|
150
|
+
const tree = args && args.tree ? args.tree : null;
|
|
151
|
+
const treeCov = coverage ? coverage.tree : null;
|
|
152
|
+
const summary = computeCoverageSummary(treeCov);
|
|
153
|
+
const tiles = familyCounts(tree);
|
|
154
|
+
const totalsText = renderTotals(summary);
|
|
155
|
+
const bar = renderBar(summary, treeCov);
|
|
156
|
+
const tilesHtml = renderTiles(tiles);
|
|
157
|
+
const unresolved = renderUnresolvedPointers(treeCov);
|
|
158
|
+
const dataAvailable = summary.available ? 'yes' : 'no';
|
|
159
|
+
return `<section class="rcf-cov-summary" data-rcf-coverage-available="${dataAvailable}" aria-labelledby="rcf-readiness-coverage-heading">
|
|
160
|
+
<header class="rcf-cov-summary__head"><h3 id="rcf-readiness-coverage-heading">Coverage</h3></header>
|
|
161
|
+
${totalsText}
|
|
162
|
+
${bar}
|
|
163
|
+
${tilesHtml}
|
|
164
|
+
${unresolved}
|
|
165
|
+
</section>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderTotals(summary) {
|
|
169
|
+
// AC-18004-7: a value the coverage result does not carry renders as
|
|
170
|
+
// "coverage unavailable", not as a blank. We always render the four
|
|
171
|
+
// slots; each one shows either its number or the unavailable word.
|
|
172
|
+
const t = summary.totals;
|
|
173
|
+
const slot = (name, label) => {
|
|
174
|
+
const v = t[name];
|
|
175
|
+
const text = typeof v === 'number' ? String(v) : UNAVAILABLE;
|
|
176
|
+
const data = typeof v === 'number' ? 'no' : 'yes';
|
|
177
|
+
return `<span class="rcf-cov-summary__slot" data-rcf-slot="${name}" data-rcf-unavailable="${data}">${escapeHtml(label)}: <strong>${escapeHtml(text)}</strong></span>`;
|
|
178
|
+
};
|
|
179
|
+
const unavailAll = summary.available ? '' : ' rcf-cov-summary__totals--unavailable';
|
|
180
|
+
return `<p class="rcf-cov-summary__totals${unavailAll}">
|
|
181
|
+
${slot('requirements', 'Requirements')}
|
|
182
|
+
${slot('covered', 'Covered')}
|
|
183
|
+
${slot('coveredUnresolved', 'Covered-unresolved')}
|
|
184
|
+
${slot('uncovered', 'Uncovered')}
|
|
185
|
+
<span class="rcf-cov-summary__slot" data-rcf-slot="percentCovered" data-rcf-unavailable="${summary.percentCovered === UNAVAILABLE ? 'yes' : 'no'}">${escapeHtml(summary.percentCovered)}</span>
|
|
186
|
+
</p>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderBar(summary, treeCov) {
|
|
190
|
+
if (!summary.available) {
|
|
191
|
+
return `<div class="rcf-cov-summary__bar" data-rcf-bar-empty="yes" role="img" aria-label="${escapeHtml(UNAVAILABLE)}"></div>`;
|
|
192
|
+
}
|
|
193
|
+
const t = treeCov && treeCov.totals ? treeCov.totals : null;
|
|
194
|
+
const total = t ? (Number(t.requirements) || 0) : 0;
|
|
195
|
+
const covered = t ? (Number(t.covered) || 0) : 0;
|
|
196
|
+
const coveredUnresolved = t ? (Number(t.coveredUnresolved) || 0) : 0;
|
|
197
|
+
const uncovered = t ? (Number(t.uncovered) || 0) : 0;
|
|
198
|
+
// Three widths. Over --sv-surface. --sv-success / --sv-warning / --sv-danger.
|
|
199
|
+
const safeTotal = total > 0 ? total : (covered + coveredUnresolved + uncovered) || 1;
|
|
200
|
+
const w1 = pct(covered, safeTotal);
|
|
201
|
+
const w2 = pct(coveredUnresolved, safeTotal);
|
|
202
|
+
const w3 = pct(uncovered, safeTotal);
|
|
203
|
+
const label = `covered ${covered}, covered-unresolved ${coveredUnresolved}, uncovered ${uncovered} out of ${total}`;
|
|
204
|
+
return `<div class="rcf-cov-summary__bar" role="img" aria-label="${escapeHtml(label)}">
|
|
205
|
+
<span class="rcf-cov-summary__bar-seg rcf-cov-summary__bar-seg--covered" data-rcf-seg="covered" style="width: ${w1};" title="Covered: ${escapeHtml(String(covered))}"></span>
|
|
206
|
+
<span class="rcf-cov-summary__bar-seg rcf-cov-summary__bar-seg--unresolved" data-rcf-seg="unresolved" style="width: ${w2};" title="Covered-unresolved: ${escapeHtml(String(coveredUnresolved))}"></span>
|
|
207
|
+
<span class="rcf-cov-summary__bar-seg rcf-cov-summary__bar-seg--uncovered" data-rcf-seg="uncovered" style="width: ${w3};" title="Uncovered: ${escapeHtml(String(uncovered))}"></span>
|
|
208
|
+
</div>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function pct(part, total) {
|
|
212
|
+
if (!total) return '0%';
|
|
213
|
+
const n = Math.max(0, Math.min(100, (part / total) * 100));
|
|
214
|
+
return `${n.toFixed(2)}%`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderTiles(tiles) {
|
|
218
|
+
const cells = tiles.map((f) => {
|
|
219
|
+
const value = typeof f.count === 'number' ? String(f.count) : UNAVAILABLE;
|
|
220
|
+
const unavail = typeof f.count !== 'number';
|
|
221
|
+
return `<li class="rcf-cov-summary__tile" data-rcf-family="${escapeHtml(f.key)}" data-rcf-unavailable="${unavail ? 'yes' : 'no'}">
|
|
222
|
+
<span class="rcf-cov-summary__tile-count">${escapeHtml(value)}</span>
|
|
223
|
+
<span class="rcf-cov-summary__tile-label">${escapeHtml(f.label)}</span>
|
|
224
|
+
</li>`;
|
|
225
|
+
}).join('');
|
|
226
|
+
return `<ul class="rcf-cov-summary__tiles" aria-label="Document counts per family">${cells}</ul>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Each reason word the compute emits for an unresolved pointer has a
|
|
230
|
+
// plain-English resolution: what has to change in the test tree to
|
|
231
|
+
// clear the row. digest-review rule: rows carry id + plain-
|
|
232
|
+
// English ask + chain location + link + what resolved looks like.
|
|
233
|
+
const UNRESOLVED_RESOLVED = {
|
|
234
|
+
'test-missing': 'resolved by: add a test case in the pointer file whose name matches the testPointer slot',
|
|
235
|
+
'file-missing': 'resolved by: create the test file at the pointer path and add the matching test case',
|
|
236
|
+
'pointer-malformed': 'resolved by: fix the testPointer on the TC so it names a file and a case name',
|
|
237
|
+
'ambiguous': 'resolved by: narrow the testPointer so one and only one test case matches it',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function resolvedForUnresolved(reason) {
|
|
241
|
+
if (!reason) return 'resolved by: add a resolving test case for this pointer';
|
|
242
|
+
if (UNRESOLVED_RESOLVED[reason]) return UNRESOLVED_RESOLVED[reason];
|
|
243
|
+
return `resolved by: fix the pointer so the "${reason}" condition clears`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function renderUnresolvedPointers(treeCov) {
|
|
247
|
+
const items = treeCov && Array.isArray(treeCov.unresolvedTestPointers) ? treeCov.unresolvedTestPointers : [];
|
|
248
|
+
if (items.length === 0) {
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
const rows = items.map((p) => {
|
|
252
|
+
const tsId = typeof p.tsId === 'string' ? p.tsId : '';
|
|
253
|
+
const tcId = typeof p.tcId === 'string' ? p.tcId : '';
|
|
254
|
+
const pointer = typeof p.testPointer === 'string' && p.testPointer.length > 0 ? p.testPointer : UNAVAILABLE;
|
|
255
|
+
const reason = typeof p.reason === 'string' && p.reason.length > 0 ? p.reason : UNAVAILABLE;
|
|
256
|
+
const resolved = resolvedForUnresolved(typeof p.reason === 'string' ? p.reason : '');
|
|
257
|
+
// `#entity=<TS-id>` without a tab key resolves through the viewer's
|
|
258
|
+
// bare-entity path (page-init.js resolveHash): it finds the TS node,
|
|
259
|
+
// activates its owning tab (Architecture) and scrolls to it. The
|
|
260
|
+
// former `#tab=testing&entity=...` href pointed at a tab that does
|
|
261
|
+
// not exist (TABS: readiness, overview, requirements, architecture,
|
|
262
|
+
// build, product-map) and fell through to Readiness, so clicking a
|
|
263
|
+
// row did not land on its suite.
|
|
264
|
+
const tsCell = tsId
|
|
265
|
+
? `<a href="#entity=${escapeHtml(tsId)}" data-rcf-ts="${escapeHtml(tsId)}">${escapeHtml(tsId)}</a>`
|
|
266
|
+
: `<em>${escapeHtml(UNAVAILABLE)}</em>`;
|
|
267
|
+
const tcCell = tcId ? `<code>${escapeHtml(tcId)}</code>` : `<em>${escapeHtml(UNAVAILABLE)}</em>`;
|
|
268
|
+
// FBS-208 (AC-209-3): the suite id in the row carries a Trace
|
|
269
|
+
// action that lands on the Trace sub-tab for the id.
|
|
270
|
+
const traceCell = tsId
|
|
271
|
+
? `<a class="rcf-trace-action" data-rcf-trace-for="${escapeHtml(tsId)}" href="#tab=readiness&sub=trace&entity=${encodeURIComponent(tsId)}">Trace</a>`
|
|
272
|
+
: `<span class="rcf-trace-action rcf-trace-action--na" data-rcf-trace="no" aria-label="No trace available for this item">-</span>`;
|
|
273
|
+
return `<tr>
|
|
274
|
+
<td>${tsCell}</td>
|
|
275
|
+
<td>${tcCell}</td>
|
|
276
|
+
<td><code>${escapeHtml(pointer)}</code></td>
|
|
277
|
+
<td>${escapeHtml(reason)}</td>
|
|
278
|
+
<td class="rcf-cov-summary__unresolved-resolved">${escapeHtml(resolved)}</td>
|
|
279
|
+
<td>${traceCell}</td>
|
|
280
|
+
</tr>`;
|
|
281
|
+
}).join('');
|
|
282
|
+
return `<section class="rcf-cov-summary__unresolved" aria-labelledby="rcf-readiness-coverage-unresolved-heading">
|
|
283
|
+
<h4 id="rcf-readiness-coverage-unresolved-heading">Unresolved test pointers <span class="rcf-badge rcf-badge--count">${items.length}</span></h4>
|
|
284
|
+
<table class="rcf-cov-summary__unresolved-table">
|
|
285
|
+
<thead><tr><th>Suite</th><th>Case</th><th>Pointer</th><th>Reason</th><th>What resolves it</th><th><span class="rcf-sr-only">Trace this id</span></th></tr></thead>
|
|
286
|
+
<tbody>${rows}</tbody>
|
|
287
|
+
</table>
|
|
288
|
+
</section>`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const UNRESOLVED_POINTER_RESOLVED = UNRESOLVED_RESOLVED;
|
|
292
|
+
|
|
293
|
+
export const COVERAGE_UNAVAILABLE_TEXT = UNAVAILABLE;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Readiness delta counts (FBS-206, TAC-4135-readiness-command-page,
|
|
2
|
+
// ADR-4139). Pure HTML string renderer: emits six count numbers
|
|
3
|
+
// (changed, added, removed, briefSince, impacted, impactedFbs) and
|
|
4
|
+
// hides the per-document list and its diff widgets inside a
|
|
5
|
+
// collapsed <details> so a thousand-document delta no longer buries
|
|
6
|
+
// the failing ids.
|
|
7
|
+
//
|
|
8
|
+
// AC-18005-3 pins the expanded contract: when the reader opens the
|
|
9
|
+
// <details>, the per-document list renders (changed with the diff
|
|
10
|
+
// component, added, removed, impacted ACs and FBS as links); when
|
|
11
|
+
// closed, only the six counts are visible.
|
|
12
|
+
//
|
|
13
|
+
// No command text, no write affordance.
|
|
14
|
+
|
|
15
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
16
|
+
import { diff } from '../components/diff.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} DeltaCounts
|
|
20
|
+
* @property {number} changed
|
|
21
|
+
* @property {number} added
|
|
22
|
+
* @property {number} removed
|
|
23
|
+
* @property {number} briefSince
|
|
24
|
+
* @property {number} impacted
|
|
25
|
+
* @property {number} impactedFbs
|
|
26
|
+
* @property {boolean} expanded renderer-side flag mirroring the <details open> state
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fold the six counts from a ReadinessDelta.
|
|
31
|
+
*
|
|
32
|
+
* @param {import('../../query/readiness.js').ReadinessDelta | null | undefined} d
|
|
33
|
+
* @returns {Omit<DeltaCounts, 'expanded'>}
|
|
34
|
+
*/
|
|
35
|
+
export function foldCounts(d) {
|
|
36
|
+
if (!d || typeof d !== 'object') {
|
|
37
|
+
return { changed: 0, added: 0, removed: 0, briefSince: 0, impacted: 0, impactedFbs: 0 };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
changed: Array.isArray(d.changed) ? d.changed.length : 0,
|
|
41
|
+
added: Array.isArray(d.added) ? d.added.length : 0,
|
|
42
|
+
removed: Array.isArray(d.removed) ? d.removed.length : 0,
|
|
43
|
+
briefSince: Array.isArray(d.briefSince) ? d.briefSince.length : 0,
|
|
44
|
+
impacted: Array.isArray(d.impacted) ? d.impacted.length : 0,
|
|
45
|
+
impactedFbs: Array.isArray(d.impactedFbs) ? d.impactedFbs.length : 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the delta-counts block (overview sub-view). The six count
|
|
51
|
+
* tiles are always visible; the per-document list sits inside a
|
|
52
|
+
* collapsed <details>.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} args
|
|
55
|
+
* @param {import('../../query/readiness.js').ReadinessDelta | null | undefined} args.delta
|
|
56
|
+
* @param {object | null | undefined} [args.freezeRecord]
|
|
57
|
+
* @param {boolean} [args.expanded] default false
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function renderDeltaCounts({ delta, freezeRecord, expanded }) {
|
|
61
|
+
const counts = foldCounts(delta);
|
|
62
|
+
const totalTouched = counts.changed + counts.added + counts.removed;
|
|
63
|
+
const empty = totalTouched === 0 && counts.briefSince === 0 && counts.impacted === 0 && counts.impactedFbs === 0;
|
|
64
|
+
if (empty) {
|
|
65
|
+
return `<section class="rcf-readiness-delta-counts" data-rcf-expanded="${expanded ? 'yes' : 'no'}">`
|
|
66
|
+
+ `<h3>Delta since freeze</h3>`
|
|
67
|
+
+ `<p><em>No delta since the last freeze.</em></p>`
|
|
68
|
+
+ `</section>`;
|
|
69
|
+
}
|
|
70
|
+
const tiles = [
|
|
71
|
+
tile('changed', counts.changed, 'Documents changed'),
|
|
72
|
+
tile('added', counts.added, 'Documents added'),
|
|
73
|
+
tile('removed', counts.removed, 'Documents removed'),
|
|
74
|
+
tile('briefSince', counts.briefSince, 'Brief statements since freeze'),
|
|
75
|
+
tile('impacted', counts.impacted, 'ACs to re-verify'),
|
|
76
|
+
tile('impactedFbs', counts.impactedFbs, 'FBS to re-execute'),
|
|
77
|
+
].join('');
|
|
78
|
+
const open = expanded ? ' open' : '';
|
|
79
|
+
const docsList = renderDocsList(delta, freezeRecord);
|
|
80
|
+
return `<section class="rcf-readiness-delta-counts" data-rcf-expanded="${expanded ? 'yes' : 'no'}">`
|
|
81
|
+
+ `<h3>Delta since freeze</h3>`
|
|
82
|
+
+ `<div class="rcf-readiness-delta-counts__tiles" role="group" aria-label="Delta counts">${tiles}</div>`
|
|
83
|
+
+ `<details class="rcf-readiness-delta-counts__details"${open}>`
|
|
84
|
+
+ `<summary>Show per-document changes</summary>`
|
|
85
|
+
+ docsList
|
|
86
|
+
+ `</details>`
|
|
87
|
+
+ `</section>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function tile(key, n, label) {
|
|
91
|
+
const empty = n === 0 ? ' rcf-readiness-delta-counts__tile--empty' : '';
|
|
92
|
+
return `<div class="rcf-readiness-delta-counts__tile${empty}" data-rcf-count="${escapeHtml(key)}" data-rcf-count-value="${n}">`
|
|
93
|
+
+ `<span class="rcf-readiness-delta-counts__n">${n}</span>`
|
|
94
|
+
+ `<span class="rcf-readiness-delta-counts__label">${escapeHtml(label)}</span>`
|
|
95
|
+
+ `</div>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderDocsList(delta, freezeRecord) {
|
|
99
|
+
const d = delta && typeof delta === 'object' ? delta : null;
|
|
100
|
+
if (!d) return '';
|
|
101
|
+
const frozenHashes = (freezeRecord && typeof freezeRecord === 'object' && freezeRecord.docHashes && typeof freezeRecord.docHashes === 'object')
|
|
102
|
+
? freezeRecord.docHashes
|
|
103
|
+
: {};
|
|
104
|
+
const currentHashes = (d && typeof d.currentDocHashes === 'object' && d.currentDocHashes !== null)
|
|
105
|
+
? d.currentDocHashes
|
|
106
|
+
: {};
|
|
107
|
+
const added = Array.isArray(d.added) ? d.added : [];
|
|
108
|
+
const changed = Array.isArray(d.changed) ? d.changed : [];
|
|
109
|
+
const removed = Array.isArray(d.removed) ? d.removed : [];
|
|
110
|
+
const brief = Array.isArray(d.briefSince) ? d.briefSince : [];
|
|
111
|
+
const impactedAc = Array.isArray(d.impacted) ? d.impacted : [];
|
|
112
|
+
const impactedFbs = Array.isArray(d.impactedFbs) ? d.impactedFbs : [];
|
|
113
|
+
const docsBlock = (added.length + changed.length + removed.length) > 0
|
|
114
|
+
? `<section class="rcf-readiness-delta-counts__docs">`
|
|
115
|
+
+ `<h4>Documents (${added.length + changed.length + removed.length})</h4>`
|
|
116
|
+
+ `<ul>`
|
|
117
|
+
+ added.map((id) => `<li><a href="#${escapeHtml(String(id))}">${escapeHtml(String(id))}</a> <em>added</em>${renderDocDiff(id, frozenHashes, currentHashes, 'added')}</li>`).join('')
|
|
118
|
+
+ changed.map((id) => `<li><a href="#${escapeHtml(String(id))}">${escapeHtml(String(id))}</a> <em>changed</em>${renderDocDiff(id, frozenHashes, currentHashes, 'changed')}</li>`).join('')
|
|
119
|
+
+ removed.map((id) => `<li>${escapeHtml(String(id))} <em>removed</em>${renderDocDiff(id, frozenHashes, currentHashes, 'removed')}</li>`).join('')
|
|
120
|
+
+ `</ul>`
|
|
121
|
+
+ `</section>`
|
|
122
|
+
: '';
|
|
123
|
+
const briefBlock = brief.length > 0
|
|
124
|
+
? `<section class="rcf-readiness-delta-counts__brief">`
|
|
125
|
+
+ `<h4>Brief statements since freeze (${brief.length})</h4>`
|
|
126
|
+
+ `<ul>${brief.map((n) => `<li>#${escapeHtml(String(n))}</li>`).join('')}</ul>`
|
|
127
|
+
+ `</section>`
|
|
128
|
+
: '';
|
|
129
|
+
const impactedBlock = (impactedAc.length + impactedFbs.length) > 0
|
|
130
|
+
? `<section class="rcf-readiness-delta-counts__impacted" data-rcf-impacted-ac="${impactedAc.length}" data-rcf-impacted-fbs="${impactedFbs.length}">`
|
|
131
|
+
+ `<h4>Impacted by delta (${impactedAc.length} AC, ${impactedFbs.length} FBS)</h4>`
|
|
132
|
+
+ (impactedAc.length > 0
|
|
133
|
+
? `<p class="muted small">To re-verify (${impactedAc.length}):</p>`
|
|
134
|
+
+ `<ul class="rcf-readiness-delta-counts__impacted-ac">${impactedAc.map((n) => `<li><a href="#${escapeHtml(String(n))}">${escapeHtml(String(n))}</a></li>`).join('')}</ul>`
|
|
135
|
+
: '')
|
|
136
|
+
+ (impactedFbs.length > 0
|
|
137
|
+
? `<p class="muted small">To re-execute (${impactedFbs.length}):</p>`
|
|
138
|
+
+ `<ul class="rcf-readiness-delta-counts__impacted-fbs">${impactedFbs.map((id) => `<li><a href="#${escapeHtml(String(id))}">${escapeHtml(String(id))}</a></li>`).join('')}</ul>`
|
|
139
|
+
: '')
|
|
140
|
+
+ `</section>`
|
|
141
|
+
: '';
|
|
142
|
+
return docsBlock + briefBlock + impactedBlock;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderDocDiff(id, frozenHashes, currentHashes, kind) {
|
|
146
|
+
const frozenHash = typeof frozenHashes?.[id] === 'string' ? frozenHashes[id] : null;
|
|
147
|
+
const currentHash = typeof currentHashes?.[id] === 'string' ? currentHashes[id] : null;
|
|
148
|
+
const beforeNote = 'frozen body not captured in freeze record';
|
|
149
|
+
const before = kind === 'added'
|
|
150
|
+
? { note: 'not in the frozen tree' }
|
|
151
|
+
: { hash: frozenHash ?? '', note: beforeNote };
|
|
152
|
+
const after = kind === 'removed'
|
|
153
|
+
? { note: 'removed from the live tree' }
|
|
154
|
+
: { hash: currentHash ?? '', note: 'current body available on the document tab' };
|
|
155
|
+
return ` ${diff(before, after)}`;
|
|
156
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Readiness stage grid (FBS-206, TAC-4135-readiness-command-page,
|
|
2
|
+
// ADR-4139). Pure HTML string renderer: emits a two-column table
|
|
3
|
+
// (tree, delta) with one row per stage. Each VerdictGridCell carries
|
|
4
|
+
// pass/total/state and a failing-count link into the blocking table
|
|
5
|
+
// filtered to that stage.
|
|
6
|
+
//
|
|
7
|
+
// Viewer read-only (Dave constraint F7, ADR-4139): no command text,
|
|
8
|
+
// no write affordance. The failing count is a hash link
|
|
9
|
+
// (#tab=readiness&sub=blocking&stage=<D>) picked up by the blocking
|
|
10
|
+
// FilterBar wire (page-init.js).
|
|
11
|
+
//
|
|
12
|
+
// Acknowledgement source (AC-18005-6): stages[].state is the single
|
|
13
|
+
// source for the cell state ("acknowledged" when the pass-fold set
|
|
14
|
+
// it via a D8 freeze match). The reason reads from stages[].reason
|
|
15
|
+
// (notApplicable) or, for acknowledgements, from freezeRecord.gates
|
|
16
|
+
// via one helper (reasonForCell) so the grid never reads
|
|
17
|
+
// freezeRecord.gates directly from its row renderer.
|
|
18
|
+
//
|
|
19
|
+
// Node 24 ESM; no dependencies.
|
|
20
|
+
|
|
21
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
22
|
+
import { stageTitle } from './stage-legend.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} VerdictGridCell
|
|
26
|
+
* @property {number} pass
|
|
27
|
+
* @property {number} total
|
|
28
|
+
* @property {'passed'|'failing'|'acknowledged'|'notApplicable'} state
|
|
29
|
+
* @property {number} failing count of failing ids in that stage x scope
|
|
30
|
+
* @property {string} [reason] hover title (notApplicable or acknowledged)
|
|
31
|
+
* @property {string} [href] link target when failing>0
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the cell shape for one (stage, scope) pair.
|
|
36
|
+
*
|
|
37
|
+
* @param {import('../../query/readiness.js').StageResult} s
|
|
38
|
+
* @param {'tree'|'delta'} scope
|
|
39
|
+
* @param {object | null | undefined} freezeRecord
|
|
40
|
+
* @returns {VerdictGridCell}
|
|
41
|
+
*/
|
|
42
|
+
export function buildCell(s, scope, freezeRecord) {
|
|
43
|
+
const checks = Array.isArray(s.checks) ? s.checks.filter((c) => c && c.over === scope) : [];
|
|
44
|
+
let pass = 0;
|
|
45
|
+
let total = 0;
|
|
46
|
+
let failing = 0;
|
|
47
|
+
for (const c of checks) {
|
|
48
|
+
pass += Number.isFinite(c.pass) ? c.pass : 0;
|
|
49
|
+
total += Number.isFinite(c.total) ? c.total : 0;
|
|
50
|
+
if (!c.ok && Array.isArray(c.failing)) failing += c.failing.length;
|
|
51
|
+
}
|
|
52
|
+
const state = cellStateFromStage(s, checks);
|
|
53
|
+
const reason = reasonForCell(s, state, freezeRecord);
|
|
54
|
+
const cell = { pass, total, state, failing };
|
|
55
|
+
if (reason) cell.reason = reason;
|
|
56
|
+
if (failing > 0) {
|
|
57
|
+
cell.href = `#tab=readiness&sub=blocking&stage=${encodeURIComponent(s.stage)}`;
|
|
58
|
+
}
|
|
59
|
+
return cell;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fold the per-scope cell state from stages[].state and the scope's
|
|
64
|
+
* own checks. The stage-level state is authoritative for passed /
|
|
65
|
+
* acknowledged / notApplicable (D8 and the acknowledge-sweep set it);
|
|
66
|
+
* if the scope has no checks at all the cell is notApplicable; a
|
|
67
|
+
* failing check in the scope flips the cell to failing even when the
|
|
68
|
+
* other scope passed (so delta failures surface on their own column).
|
|
69
|
+
*
|
|
70
|
+
* @param {import('../../query/readiness.js').StageResult} s
|
|
71
|
+
* @param {import('../../query/readiness.js').CheckResult[]} scopeChecks
|
|
72
|
+
*/
|
|
73
|
+
function cellStateFromStage(s, scopeChecks) {
|
|
74
|
+
if (scopeChecks.length === 0) return 'notApplicable';
|
|
75
|
+
// Stage-level acknowledgement wins over scope-specific failures:
|
|
76
|
+
// foldState only sets state='acknowledged' when there is a failing
|
|
77
|
+
// check and the gate is acked at the current tree hash, so the
|
|
78
|
+
// acknowledged envelope is the authoritative verdict for both the
|
|
79
|
+
// tree and delta columns.
|
|
80
|
+
if (s.state === 'acknowledged') return 'acknowledged';
|
|
81
|
+
if (scopeChecks.some((c) => !c.ok)) return 'failing';
|
|
82
|
+
if (s.state === 'notApplicable') return 'notApplicable';
|
|
83
|
+
return 'passed';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reason-helper: the hover title for a cell. notApplicable rows read
|
|
88
|
+
* from stages[].reason; acknowledged rows read the ack-reason from
|
|
89
|
+
* freezeRecord.gates[gate] (via this one function, not from the row
|
|
90
|
+
* renderer); failing / passed rows return undefined (the stage-title
|
|
91
|
+
* tooltip still fires at the <th> level).
|
|
92
|
+
*
|
|
93
|
+
* @param {import('../../query/readiness.js').StageResult} s
|
|
94
|
+
* @param {VerdictGridCell['state']} state
|
|
95
|
+
* @param {object | null | undefined} freezeRecord
|
|
96
|
+
* @returns {string | undefined}
|
|
97
|
+
*/
|
|
98
|
+
export function reasonForCell(s, state, freezeRecord) {
|
|
99
|
+
if (state === 'notApplicable') {
|
|
100
|
+
return typeof s.reason === 'string' && s.reason.length > 0 ? s.reason : undefined;
|
|
101
|
+
}
|
|
102
|
+
if (state === 'acknowledged') {
|
|
103
|
+
const gates = freezeRecord && typeof freezeRecord === 'object' ? freezeRecord.gates : null;
|
|
104
|
+
const ack = gates && typeof gates === 'object' ? gates[s.gate] : null;
|
|
105
|
+
// The freeze record writes the reason at gates[g].at.reason (see
|
|
106
|
+
// src/cli/freeze.js buildGatesEntry / mergeAckedGates); the legacy
|
|
107
|
+
// flat gates[g].reason is accepted as a fallback for older files.
|
|
108
|
+
const at = ack && typeof ack === 'object' ? ack.at : null;
|
|
109
|
+
if (at && typeof at === 'object' && typeof at.reason === 'string' && at.reason.length > 0) {
|
|
110
|
+
return at.reason;
|
|
111
|
+
}
|
|
112
|
+
if (ack && typeof ack === 'object' && typeof ack.reason === 'string' && ack.reason.length > 0) {
|
|
113
|
+
return ack.reason;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build the full stage-grid HTML (overview sub-view block).
|
|
122
|
+
*
|
|
123
|
+
* @param {object} args
|
|
124
|
+
* @param {import('../../query/readiness.js').StageResult[]} args.stages
|
|
125
|
+
* @param {object | null | undefined} [args.freezeRecord]
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function renderVerdictGrid({ stages, freezeRecord }) {
|
|
129
|
+
const rows = Array.isArray(stages) ? stages : [];
|
|
130
|
+
if (rows.length === 0) {
|
|
131
|
+
return `<section class="rcf-readiness-grid" data-rcf-grid="verdict">`
|
|
132
|
+
+ `<p><em>No stages to render.</em></p>`
|
|
133
|
+
+ `</section>`;
|
|
134
|
+
}
|
|
135
|
+
const header = `<thead><tr>`
|
|
136
|
+
+ `<th scope="col">Stage</th>`
|
|
137
|
+
+ `<th scope="col" data-scope="tree">Tree</th>`
|
|
138
|
+
+ `<th scope="col" data-scope="delta">Delta</th>`
|
|
139
|
+
+ `</tr></thead>`;
|
|
140
|
+
const body = rows.map((s) => {
|
|
141
|
+
const title = stageTitle(s.stage) || s.stage;
|
|
142
|
+
const tree = buildCell(s, 'tree', freezeRecord);
|
|
143
|
+
const delta = buildCell(s, 'delta', freezeRecord);
|
|
144
|
+
const stageCell = `<th scope="row" data-rcf-stage-ref="${escapeHtml(s.stage)}" title="${escapeHtml(title)}">`
|
|
145
|
+
+ `<span class="rcf-readiness-grid__stage-id">${escapeHtml(s.stage)}</span> `
|
|
146
|
+
+ `<code class="muted">${escapeHtml(s.gate)}</code>`
|
|
147
|
+
+ `</th>`;
|
|
148
|
+
return `<tr data-rcf-stage="${escapeHtml(s.stage)}">`
|
|
149
|
+
+ stageCell
|
|
150
|
+
+ renderCellTd(s, 'tree', tree)
|
|
151
|
+
+ renderCellTd(s, 'delta', delta)
|
|
152
|
+
+ `</tr>`;
|
|
153
|
+
}).join('\n');
|
|
154
|
+
return `<section class="rcf-readiness-grid" data-rcf-grid="verdict">`
|
|
155
|
+
+ `<table class="rcf-readiness-grid__table rcf-cmd-table" data-rcf-table="verdict-grid">`
|
|
156
|
+
+ header
|
|
157
|
+
+ `<tbody>${body}</tbody>`
|
|
158
|
+
+ `</table>`
|
|
159
|
+
+ `</section>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderCellTd(s, scope, cell) {
|
|
163
|
+
const stateClass = `rcf-readiness-grid__cell--${escapeHtml(cell.state)}`;
|
|
164
|
+
const title = cell.reason ? ` title="${escapeHtml(cell.reason)}"` : '';
|
|
165
|
+
const countsText = `${cell.pass}/${cell.total}`;
|
|
166
|
+
const failingText = cell.failing > 0
|
|
167
|
+
? (cell.href
|
|
168
|
+
? ` <a class="rcf-readiness-grid__failing" href="${escapeHtml(cell.href)}" data-rcf-link="blocking-filtered" data-rcf-filter-stage="${escapeHtml(s.stage)}">${cell.failing} failing</a>`
|
|
169
|
+
: ` <span class="rcf-readiness-grid__failing">${cell.failing} failing</span>`)
|
|
170
|
+
: '';
|
|
171
|
+
return `<td class="rcf-readiness-grid__cell ${stateClass}" data-rcf-cell-state="${escapeHtml(cell.state)}" data-rcf-cell-scope="${escapeHtml(scope)}" data-rcf-cell-pass="${cell.pass}" data-rcf-cell-total="${cell.total}" data-rcf-cell-failing="${cell.failing}"${title}>`
|
|
172
|
+
+ `<span class="rcf-readiness-grid__counts">${countsText}</span>`
|
|
173
|
+
+ failingText
|
|
174
|
+
+ `</td>`;
|
|
175
|
+
}
|