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
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
// Readiness tab PO layer (viewer UI refresh PR 8, TAC-4133, ADR-4135
|
|
1
|
+
// Readiness tab PO layer (viewer UI refresh PR 8, TAC-4133, ADR-4135;
|
|
2
|
+
// FBS-204 reshape and FBS-206 retire of the For-engineers DocRow).
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// What remains here after FBS-206:
|
|
5
|
+
// - renderVerdictCards: the two equal-height cards (Ready for
|
|
6
|
+
// engineers / Ready to build) with count-links into the Questions
|
|
7
|
+
// and Blocking sub-tabs. readiness.js imports and composes it
|
|
8
|
+
// into the Overview sub-view.
|
|
9
|
+
// - renderReadinessPO: a thin composition kept for the existing
|
|
10
|
+
// AC-18002-1/-2 tests and the components fixture page; it emits
|
|
11
|
+
// verdict cards + the two tables (same shape FBS-204 shipped).
|
|
12
|
+
// The For-engineers DocRow is gone (AC-18005-4: no DocRow
|
|
13
|
+
// wrapper; the sub-view structure in readiness.js owns the
|
|
14
|
+
// engineer surface now).
|
|
8
15
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// `.rcf-po-verdicts`), not scripted measurement (ADR-4135).
|
|
16
|
+
// Viewer read-only (ADR-4139): no CLI command text, no write
|
|
17
|
+
// affordance. Dave constraints F2 and F4 keep operator names out of
|
|
18
|
+
// src/tests/fixtures.
|
|
13
19
|
|
|
14
20
|
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
15
21
|
import { formatVerdictLines } from '../../query/readiness.js';
|
|
16
|
-
import { renderDocRow } from '../components/doc-row.js';
|
|
17
22
|
import { toQuestions, preferReadinessQuestions } from './question-adapter.js';
|
|
18
|
-
import {
|
|
23
|
+
import { renderQuestionsTable, renderBlockingTable, buildQuestionRows, buildBlockingRows } from './tables.js';
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* @typedef {import('../../query/readiness.js').ReadinessResult} ReadinessResult
|
|
@@ -23,12 +28,15 @@ import { stageTitle } from './stage-legend.js';
|
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
|
-
*
|
|
31
|
+
* Thin composition kept for legacy tests and the components fixture:
|
|
32
|
+
* verdict cards + the questions and blocking tables. The For-engineers
|
|
33
|
+
* DocRow retired in FBS-206 (AC-18005-4) so this function no longer
|
|
34
|
+
* carries an engineer wrapper; readiness.js owns the sub-view layout
|
|
35
|
+
* now.
|
|
27
36
|
*
|
|
28
37
|
* @param {ReadinessResult | null} readiness
|
|
29
38
|
* @param {object} [opts]
|
|
30
39
|
* @param {'productOwner'|'engineer'|'unstated'} [opts.persona='productOwner']
|
|
31
|
-
* @param {string} opts.engineerBody - rendered engineer-level HTML (the former renderReadinessPanel body)
|
|
32
40
|
* @returns {string}
|
|
33
41
|
*/
|
|
34
42
|
export function renderReadinessPO(readiness, opts) {
|
|
@@ -36,23 +44,75 @@ export function renderReadinessPO(readiness, opts) {
|
|
|
36
44
|
return '<p><em>Readiness could not be computed for this tree.</em></p>';
|
|
37
45
|
}
|
|
38
46
|
const persona = opts?.persona ?? 'productOwner';
|
|
39
|
-
const engineerBody = typeof opts?.engineerBody === 'string' ? opts.engineerBody : '';
|
|
40
47
|
const verdictLines = formatVerdictLines(readiness);
|
|
41
48
|
const questionsSource = preferReadinessQuestions(readiness) ? 'readiness.questions' : 'blockers';
|
|
42
49
|
const q = toQuestions(readiness);
|
|
43
|
-
|
|
50
|
+
const questionRows = buildQuestionRowsForTable(q);
|
|
51
|
+
const blockingRows = buildBlockingRows(Array.isArray(readiness.stages) ? readiness.stages : []);
|
|
52
|
+
const questionsTable = renderQuestionsTable({ rows: questionRows });
|
|
53
|
+
const blockingTable = renderBlockingTable({ rows: blockingRows });
|
|
54
|
+
const paintOrder = persona === 'engineer'
|
|
55
|
+
? [blockingTable, questionsTable]
|
|
56
|
+
: [questionsTable, blockingTable];
|
|
57
|
+
const tablesSection = `<div class="rcf-cmd-tables" data-rcf-register="${escapeHtml(persona)}" data-rcf-source="${escapeHtml(questionsSource)}">${paintOrder.join('\n')}</div>`;
|
|
44
58
|
return [
|
|
45
|
-
renderVerdictCards(readiness, verdictLines),
|
|
46
|
-
|
|
47
|
-
renderNextStep(readiness, q),
|
|
48
|
-
renderReqWorkTable(readiness),
|
|
49
|
-
renderForEngineers(readiness, engineerBody, persona),
|
|
59
|
+
renderVerdictCards(readiness, verdictLines, q),
|
|
60
|
+
tablesSection,
|
|
50
61
|
].join('\n');
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
function buildQuestionRowsForTable(adapterResult) {
|
|
65
|
+
const groups = Array.isArray(adapterResult?.groups) ? adapterResult.groups : [];
|
|
66
|
+
const optional = adapterResult?.optional ?? null;
|
|
67
|
+
const synthetic = [];
|
|
68
|
+
for (const g of groups) {
|
|
69
|
+
const items = Array.isArray(g?.items) ? g.items : [];
|
|
70
|
+
for (const it of items) {
|
|
71
|
+
synthetic.push({
|
|
72
|
+
itemId: it.itemId ?? it.id ?? '',
|
|
73
|
+
ask: it.ask,
|
|
74
|
+
hint: it.hint,
|
|
75
|
+
sourceSpanLabel: g.label,
|
|
76
|
+
checkId: it.checkId,
|
|
77
|
+
optional: Boolean(it.optional),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (optional && Array.isArray(optional.items)) {
|
|
82
|
+
for (const it of optional.items) {
|
|
83
|
+
synthetic.push({
|
|
84
|
+
itemId: it.itemId ?? '',
|
|
85
|
+
ask: it.ask,
|
|
86
|
+
hint: it.hint,
|
|
87
|
+
sourceSpanLabel: optional.label,
|
|
88
|
+
checkId: it.checkId,
|
|
89
|
+
optional: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return buildQuestionRows(synthetic);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- VerdictCards (equal height, chain term muted) ---------------------
|
|
97
|
+
//
|
|
98
|
+
// FBS-205 (AC-18004-5): the count on each card is a link to the
|
|
99
|
+
// matching sub-tab (intent -> questions, build -> blocking); the
|
|
100
|
+
// "What happens next" sentence folds into the card from the same
|
|
101
|
+
// inputs. FBS-206 wires the sub-tab router so the links actually
|
|
102
|
+
// activate the target sub-view.
|
|
103
|
+
|
|
104
|
+
const QUESTIONS_HREF = '#tab=readiness&sub=questions';
|
|
105
|
+
const BLOCKING_HREF = '#tab=readiness&sub=blocking';
|
|
54
106
|
|
|
55
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Render the two VerdictCards.
|
|
109
|
+
*
|
|
110
|
+
* @param {ReadinessResult} readiness
|
|
111
|
+
* @param {{ intentComplete: string, readyToBuild: string }} verdictLines
|
|
112
|
+
* @param {ReturnType<typeof toQuestions>} q
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function renderVerdictCards(readiness, verdictLines, q) {
|
|
56
116
|
const l = readiness.levels;
|
|
57
117
|
const intentOk = l.intentComplete.ok;
|
|
58
118
|
const buildOk = l.readyToBuild.ok;
|
|
@@ -64,225 +124,55 @@ function renderVerdictCards(readiness, verdictLines) {
|
|
|
64
124
|
|
|
65
125
|
const intentValueLabel = intentOk ? 'Yes' : 'Not yet';
|
|
66
126
|
const buildValueLabel = buildOk ? 'Yes' : 'Not yet';
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
127
|
+
|
|
128
|
+
const questionGroups = Array.isArray(q && q.groups) ? q.groups : [];
|
|
129
|
+
const questionTotal = questionGroups.reduce((sum, g) => sum + ((g && g.items && g.items.length) || 0), 0);
|
|
130
|
+
const askTotal = Number.isFinite(questionTotal) ? questionTotal : intentCount;
|
|
131
|
+
|
|
132
|
+
const intentCountLine = askTotal === 0
|
|
133
|
+
? `<a class="rcf-po-verdict__count-link" href="${escapeHtml(QUESTIONS_HREF)}" data-rcf-link="questions">0 questions waiting for you</a>`
|
|
134
|
+
: `<a class="rcf-po-verdict__count-link" href="${escapeHtml(QUESTIONS_HREF)}" data-rcf-link="questions"><strong>${askTotal} question${askTotal === 1 ? '' : 's'} waiting for you</strong></a>`;
|
|
70
135
|
const buildCountLine = buildTotal === 0
|
|
71
|
-
?
|
|
72
|
-
: `<strong>${buildEng} item${buildEng === 1 ? '' : 's'} for engineers</strong>, ${buildPo === 0 ? 'none' : buildPo} for you
|
|
136
|
+
? `<a class="rcf-po-verdict__count-link" href="${escapeHtml(BLOCKING_HREF)}" data-rcf-link="blocking">nothing waiting for engineers</a>`
|
|
137
|
+
: `<a class="rcf-po-verdict__count-link" href="${escapeHtml(BLOCKING_HREF)}" data-rcf-link="blocking"><strong>${buildEng} item${buildEng === 1 ? '' : 's'} for engineers</strong>, ${buildPo === 0 ? 'none' : buildPo} for you</a>`;
|
|
138
|
+
|
|
139
|
+
const intentNext = intentNextSentence(intentOk, askTotal);
|
|
140
|
+
const buildNext = buildNextSentence(buildOk, buildEng);
|
|
73
141
|
|
|
74
142
|
return `<section class="rcf-po-verdicts" data-rcf-intent-ok="${intentOk ? 'yes' : 'no'}" data-rcf-build-ok="${buildOk ? 'yes' : 'no'}">
|
|
75
143
|
<article class="rcf-po-verdict rcf-po-verdict--intent">
|
|
76
144
|
<p class="rcf-po-verdict__eyebrow muted">Your requirements set</p>
|
|
77
145
|
<h2 class="rcf-po-verdict__heading">Ready for engineers: <span class="rcf-pill rcf-pill--${intentOk ? 'ok' : 'warn'}">${escapeHtml(intentValueLabel)}</span> <span class="rcf-chain-term" title="What the chain calls this rung">intent-complete</span></h2>
|
|
78
146
|
<p class="rcf-po-verdict__count">${intentCountLine}</p>
|
|
147
|
+
<p class="rcf-po-verdict__next" data-rcf-next="intent">${intentNext}</p>
|
|
79
148
|
<p class="rcf-po-verdict__verdict muted"><code>${escapeHtml(verdictLines.intentComplete)}</code></p>
|
|
80
149
|
</article>
|
|
81
150
|
<article class="rcf-po-verdict rcf-po-verdict--build">
|
|
82
151
|
<p class="rcf-po-verdict__eyebrow muted">The build</p>
|
|
83
152
|
<h2 class="rcf-po-verdict__heading">Ready to build: <span class="rcf-pill rcf-pill--${buildOk ? 'ok' : 'warn'}">${escapeHtml(buildValueLabel)}</span> <span class="rcf-chain-term" title="What the chain calls this rung">ready-to-build</span></h2>
|
|
84
153
|
<p class="rcf-po-verdict__count">${buildCountLine}</p>
|
|
154
|
+
<p class="rcf-po-verdict__next" data-rcf-next="build">${buildNext}</p>
|
|
85
155
|
<p class="rcf-po-verdict__verdict muted"><code>${escapeHtml(verdictLines.readyToBuild)}</code></p>
|
|
86
156
|
</article>
|
|
87
157
|
</section>`;
|
|
88
158
|
}
|
|
89
159
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const l = readiness.levels;
|
|
94
|
-
const groups = Array.isArray(q.groups) ? q.groups : [];
|
|
95
|
-
const total = groups.reduce((sum, g) => sum + (g.items?.length ?? 0), 0);
|
|
96
|
-
|
|
97
|
-
if (total === 0) {
|
|
98
|
-
const verdictLines = formatVerdictLines(readiness);
|
|
99
|
-
// AC-18002-2: when the question set is empty, the block renders
|
|
100
|
-
// the intent-complete verdict line (verbatim from the readiness
|
|
101
|
-
// compute). Honours the defensive !ok branch by still rendering
|
|
102
|
-
// the live verdict line so engineers and PO see the same words
|
|
103
|
-
// the CLI prints; a stub fallback would drift from `rcf define
|
|
104
|
-
// readiness --json`.
|
|
105
|
-
const line = verdictLines.intentComplete;
|
|
106
|
-
const optionalHtml = q.optional
|
|
107
|
-
? renderOptionalGroup(q.optional)
|
|
108
|
-
: '';
|
|
109
|
-
return `<section class="rcf-po-questions" data-rcf-source="${escapeHtml(source)}" data-rcf-empty="yes">
|
|
110
|
-
<header class="rcf-po-questions__head"><h2>Questions for you</h2> <span class="rcf-badge rcf-badge--count">0</span></header>
|
|
111
|
-
<p class="muted small">Nothing on this tree needs your answer right now; this page updates on its own as the tree changes.</p>
|
|
112
|
-
<p class="rcf-po-questions__verdict"><code>${escapeHtml(line)}</code></p>
|
|
113
|
-
${optionalHtml}
|
|
114
|
-
</section>`;
|
|
160
|
+
function intentNextSentence(ok, askTotal) {
|
|
161
|
+
if (ok && askTotal === 0) {
|
|
162
|
+
return 'Your requirements set is ready for engineers; this page updates as they move the build along.';
|
|
115
163
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const groupHtml = groups.map((g) => {
|
|
119
|
-
const items = (g.items ?? []).map((it) => {
|
|
120
|
-
n += 1;
|
|
121
|
-
return renderQuestionItem(it, n);
|
|
122
|
-
}).join('');
|
|
123
|
-
return `<article class="rcf-po-question-group" data-rcf-group-label="${escapeHtml(g.label)}">
|
|
124
|
-
<header class="rcf-po-question-group__head"><h3>${escapeHtml(g.label)}</h3></header>
|
|
125
|
-
${items}
|
|
126
|
-
</article>`;
|
|
127
|
-
}).join('\n');
|
|
128
|
-
|
|
129
|
-
const optionalHtml = q.optional
|
|
130
|
-
? renderOptionalGroup(q.optional)
|
|
131
|
-
: '';
|
|
132
|
-
|
|
133
|
-
return `<section class="rcf-po-questions" data-rcf-source="${escapeHtml(source)}">
|
|
134
|
-
<header class="rcf-po-questions__head"><h2>Questions for you</h2> <span class="rcf-badge rcf-badge--count">${total}</span></header>
|
|
135
|
-
<p class="muted small">Answer these in your agent session; this page updates on its own. Nothing here needs an id or a command.</p>
|
|
136
|
-
${groupHtml}
|
|
137
|
-
${optionalHtml}
|
|
138
|
-
</section>`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function renderQuestionItem(it, n) {
|
|
142
|
-
const detailAttrs = buildStageAttrs(it.checkId);
|
|
143
|
-
return `<div class="rcf-po-question" data-rcf-check="${escapeHtml(it.checkId)}">
|
|
144
|
-
<div class="rcf-po-question__head"><span class="rcf-badge rcf-badge--q">Q${n}</span><h4 class="rcf-po-question__heading">${escapeHtml(it.heading)}</h4></div>
|
|
145
|
-
<p class="rcf-po-question__ask">${escapeHtml(it.ask)}</p>
|
|
146
|
-
<p class="rcf-po-question__hint muted small">${escapeHtml(it.hint)}</p>
|
|
147
|
-
<details class="rcf-po-question__detail"><summary class="small muted">Show the detail (engineer view)</summary><p class="mono small"${detailAttrs}>${escapeHtml(it.detail)}</p></details>
|
|
148
|
-
</div>`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function renderOptionalGroup(group) {
|
|
152
|
-
const items = (group.items ?? []).map((it) => {
|
|
153
|
-
return `<div class="rcf-po-question rcf-po-question--optional" data-rcf-check="${escapeHtml(it.checkId)}">
|
|
154
|
-
<h4 class="rcf-po-question__heading">${escapeHtml(it.heading)}</h4>
|
|
155
|
-
<p class="rcf-po-question__ask">${escapeHtml(it.ask)}</p>
|
|
156
|
-
<p class="rcf-po-question__hint muted small">${escapeHtml(it.hint)}</p>
|
|
157
|
-
</div>`;
|
|
158
|
-
}).join('');
|
|
159
|
-
return `<article class="rcf-po-question-group rcf-po-question-group--optional">
|
|
160
|
-
<header class="rcf-po-question-group__head"><h3>${escapeHtml(group.label)}</h3></header>
|
|
161
|
-
${items}
|
|
162
|
-
</article>`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Build the `data-rcf-stage-attrs` and `title` attribute pair for a
|
|
167
|
-
* check id of the form `D1/brief:sinceFreeze` so the D-ref hover
|
|
168
|
-
* title and the open-on-click wiring (page-init.js wireStageLegend)
|
|
169
|
-
* both work.
|
|
170
|
-
*/
|
|
171
|
-
function buildStageAttrs(checkId) {
|
|
172
|
-
const match = /^D([1-8])/.exec(checkId ?? '');
|
|
173
|
-
if (!match) return '';
|
|
174
|
-
const stage = `D${match[1]}`;
|
|
175
|
-
return ` data-rcf-stage-ref="${stage}" title="${escapeHtml(stageTitle(stage))}"`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ---- block 3: What happens next ---------------------------------------
|
|
179
|
-
|
|
180
|
-
function renderNextStep(readiness, q) {
|
|
181
|
-
const l = readiness.levels;
|
|
182
|
-
const groups = Array.isArray(q.groups) ? q.groups : [];
|
|
183
|
-
const total = groups.reduce((sum, g) => sum + (g.items?.length ?? 0), 0);
|
|
184
|
-
const buildBlockers = Array.isArray(l.readyToBuild.blockedBy) ? l.readyToBuild.blockedBy : [];
|
|
185
|
-
const engCount = buildBlockers.filter((b) => b && b.persona === 'engineer').length;
|
|
186
|
-
const verdictLines = formatVerdictLines(readiness);
|
|
187
|
-
|
|
188
|
-
const lines = [];
|
|
189
|
-
if (total === 0 && l.intentComplete.ok) {
|
|
190
|
-
lines.push(`<p><strong>Your requirements set is ready for engineers.</strong> This page will update as the engineer-level work moves along.</p>`);
|
|
191
|
-
} else if (total === 0) {
|
|
192
|
-
lines.push(`<p>Nothing on this tree needs your answer right now.</p>`);
|
|
193
|
-
} else {
|
|
194
|
-
lines.push(`<p><strong>Answer the ${total} question${total === 1 ? '' : 's'} above</strong> and your requirements set is ready for engineers.</p>`);
|
|
164
|
+
if (askTotal === 0) {
|
|
165
|
+
return 'Nothing on this tree needs your answer right now.';
|
|
195
166
|
}
|
|
196
|
-
|
|
197
|
-
lines.push(`<p>Engineers then have <strong>${engCount} item${engCount === 1 ? '' : 's'}</strong> to settle before the build can start. You do not need to do anything for those.</p>`);
|
|
198
|
-
} else if (l.readyToBuild.ok) {
|
|
199
|
-
lines.push(`<p>Engineers have nothing waiting; the tree is ready to freeze.</p>`);
|
|
200
|
-
}
|
|
201
|
-
lines.push(`<p class="small muted">Verdict as the CLI prints it: <code>${escapeHtml(verdictLines.intentComplete)}</code></p>`);
|
|
202
|
-
|
|
203
|
-
return `<section class="rcf-po-next">
|
|
204
|
-
<h2>What happens next</h2>
|
|
205
|
-
<div class="rcf-po-next__card">${lines.join('\n ')}</div>
|
|
206
|
-
</section>`;
|
|
167
|
+
return `Answer the ${askTotal} question${askTotal === 1 ? '' : 's'} in the questions table and your requirements set is ready for engineers.`;
|
|
207
168
|
}
|
|
208
169
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const rows = buildReqWorkRows(readiness);
|
|
213
|
-
if (rows.length === 0) {
|
|
214
|
-
return `<section class="rcf-po-reqwork" data-rcf-empty="yes">
|
|
215
|
-
<h2>Requirements that still need work <span class="rcf-badge rcf-badge--count">0</span></h2>
|
|
216
|
-
<p class="muted small">Each row says what is missing in plain words. Open a requirement to read it in the Requirements tab.</p>
|
|
217
|
-
<p class="rcf-po-reqwork__empty">All requirements have a plain description, a home, and at least one story. Nothing is waiting on you here.</p>
|
|
218
|
-
</section>`;
|
|
170
|
+
function buildNextSentence(ok, engCount) {
|
|
171
|
+
if (ok) {
|
|
172
|
+
return 'Engineers have nothing waiting; the tree is ready to freeze.';
|
|
219
173
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<td>${escapeHtml(r.reason)}</td>
|
|
223
|
-
<td><a href="#tab=requirements&entity=${escapeHtml(r.reqId)}">Open</a></td>
|
|
224
|
-
</tr>`).join('');
|
|
225
|
-
return `<section class="rcf-po-reqwork">
|
|
226
|
-
<h2>Requirements that still need work <span class="rcf-badge rcf-badge--count">${rows.length}</span></h2>
|
|
227
|
-
<p class="muted small">Each row says what is missing in plain words. Open a requirement to read it in the Requirements tab.</p>
|
|
228
|
-
<table class="rcf-po-reqwork__table">
|
|
229
|
-
<thead><tr><th>Requirement</th><th>What is missing</th><th></th></tr></thead>
|
|
230
|
-
<tbody>${body}</tbody>
|
|
231
|
-
</table>
|
|
232
|
-
</section>`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* The PO checks that name a REQ (`skeleton:reqIntent`,
|
|
237
|
-
* `stories:reqHasUs`, `skeleton:resolvedBy` with REQ-shaped
|
|
238
|
-
* suggestions). The Needs-work filter on the Requirements tab reads
|
|
239
|
-
* from the same set.
|
|
240
|
-
*/
|
|
241
|
-
function buildReqWorkRows(readiness) {
|
|
242
|
-
const stages = Array.isArray(readiness.stages) ? readiness.stages : [];
|
|
243
|
-
/** @type {Map<string, string>} */
|
|
244
|
-
const reasonById = new Map();
|
|
245
|
-
const REASON = {
|
|
246
|
-
'stories:reqHasUs': 'No story yet: who uses this, and what do they do with it?',
|
|
247
|
-
'skeleton:reqIntent': 'Needs a plain description: what must the product do here, and which part does it belong to?',
|
|
248
|
-
'skeleton:resolvedBy': 'A statement from your document may belong here; confirm in the questions above.',
|
|
249
|
-
};
|
|
250
|
-
for (const s of stages) {
|
|
251
|
-
if (!s || !Array.isArray(s.checks)) continue;
|
|
252
|
-
for (const c of s.checks) {
|
|
253
|
-
if (!c || c.ok) continue;
|
|
254
|
-
const reason = REASON[c.name];
|
|
255
|
-
if (!reason) continue;
|
|
256
|
-
const failing = Array.isArray(c.failing) ? c.failing : [];
|
|
257
|
-
for (const f of failing) {
|
|
258
|
-
if (!f || typeof f !== 'object') continue;
|
|
259
|
-
const id = typeof f.id === 'string' ? f.id : '';
|
|
260
|
-
if (!/^REQ-\d+/.test(id)) continue;
|
|
261
|
-
const reqId = id.split(':')[0];
|
|
262
|
-
if (!reasonById.has(reqId)) reasonById.set(reqId, reason);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
174
|
+
if (engCount === 0) {
|
|
175
|
+
return 'No engineer items are blocking the build right now.';
|
|
265
176
|
}
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ---- block 5: For engineers DocRow ------------------------------------
|
|
270
|
-
|
|
271
|
-
function renderForEngineers(readiness, engineerBody, persona) {
|
|
272
|
-
const l = readiness.levels;
|
|
273
|
-
const buildBlockers = Array.isArray(l.readyToBuild.blockedBy) ? l.readyToBuild.blockedBy : [];
|
|
274
|
-
const engCount = buildBlockers.filter((b) => b && b.persona === 'engineer').length;
|
|
275
|
-
const open = persona === 'engineer';
|
|
276
|
-
const metaBits = [
|
|
277
|
-
`<span class="rcf-badge rcf-badge--count">${engCount} item${engCount === 1 ? '' : 's'}</span>`,
|
|
278
|
-
].join(' ');
|
|
279
|
-
return `<section class="rcf-po-engineer-section">${renderDocRow({
|
|
280
|
-
id: 'For engineers',
|
|
281
|
-
title: 'stage gates (with a D1 to D8 guide), findings by check, delta, coverage, decisions, freeze',
|
|
282
|
-
body: engineerBody,
|
|
283
|
-
className: 'rcf-po-engineer',
|
|
284
|
-
open,
|
|
285
|
-
dataDocId: 'rcf-readiness-engineer',
|
|
286
|
-
meta: metaBits,
|
|
287
|
-
})}</section>`;
|
|
177
|
+
return `Engineers have ${engCount} item${engCount === 1 ? '' : 's'} to settle before the build can start; you do not need to do anything for those.`;
|
|
288
178
|
}
|