rcf-lite 0.28.4 → 0.29.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 +107 -1
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +89 -0
- package/guidance/define-intake.md +97 -0
- package/guidance/define-intent.md +107 -0
- package/guidance/discover-prototype.md +67 -0
- package/guidance/discovery-prototypes.md +69 -0
- package/guidance/harness-template.md +34 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +34 -0
- package/guidance/manifest.json +9 -2
- package/package.json +1 -1
- package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
- package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
- package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
- package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
- package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
- package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
- package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
- package/rcf/adrs/adr-4134-index-json-route.json +35 -0
- package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
- package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
- package/rcf/code-nodes/cn-725.json +18 -2
- package/rcf/code-nodes/cn-726.json +10 -2
- package/rcf/code-nodes/cn-727.json +13 -4
- package/rcf/code-nodes/cn-728.json +18 -2
- package/rcf/code-nodes/cn-729.json +10 -2
- package/rcf/code-nodes/cn-730.json +13 -3
- package/rcf/code-nodes/cn-731.json +20 -0
- package/rcf/code-nodes/cn-732.json +14 -0
- package/rcf/code-nodes/cn-733.json +14 -0
- package/rcf/code-nodes/cn-734.json +14 -0
- package/rcf/code-nodes/cn-735.json +20 -0
- package/rcf/code-nodes/cn-736.json +14 -0
- package/rcf/code-nodes/cn-737.json +14 -0
- package/rcf/code-nodes/cn-738.json +14 -0
- package/rcf/code-nodes/cn-739.json +18 -0
- package/rcf/code-nodes/cn-740.json +19 -0
- package/rcf/code-nodes/cn-741.json +16 -0
- package/rcf/code-nodes/cn-742.json +16 -0
- package/rcf/code-nodes/cn-743.json +15 -0
- package/rcf/code-nodes/cn-744.json +22 -0
- package/rcf/code-nodes/cn-745.json +16 -0
- package/rcf/code-nodes/cn-746.json +14 -0
- package/rcf/code-nodes/cn-747.json +18 -0
- package/rcf/code-nodes/cn-748.json +16 -0
- package/rcf/code-nodes/cn-749.json +16 -0
- package/rcf/code-nodes/cn-750.json +15 -0
- package/rcf/code-nodes/cn-751.json +18 -0
- package/rcf/code-nodes/cn-752.json +14 -0
- package/rcf/fbs/fbs-196.json +13 -5
- package/rcf/fbs/fbs-198.json +42 -0
- package/rcf/fbs/fbs-199.json +59 -0
- package/rcf/fbs/fbs-200.json +46 -0
- package/rcf/fbs/fbs-201.json +49 -0
- package/rcf/fbs/fbs-202.json +110 -0
- package/rcf/fbs/fbs-203.json +100 -0
- package/rcf/requirements/req-002.json +2 -2
- package/rcf/requirements/req-173.json +2 -2
- package/rcf/requirements/req-174.json +10 -4
- package/rcf/requirements/req-175.json +10 -4
- package/rcf/requirements/req-176.json +25 -0
- package/rcf/requirements/req-180.json +31 -0
- package/rcf/requirements/req-181.json +31 -0
- package/rcf/requirements/req-182.json +25 -0
- package/rcf/requirements/req-186.json +25 -0
- package/rcf/requirements/req-187.json +31 -0
- package/rcf/requirements/req-188.json +25 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +15 -3
- package/rcf/tacs/tac-4122-define-gates.json +40 -4
- package/rcf/tacs/tac-4123-define-readiness.json +39 -3
- package/rcf/tacs/tac-4124-define-freeze.json +28 -0
- package/rcf/tacs/tac-4127-readiness-view.json +49 -0
- package/rcf/tacs/tac-4128-define-guidance.json +53 -0
- package/rcf/tacs/tac-4129-define-questions.json +45 -0
- package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
- package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
- package/rcf/tacs/tac-4132-id-lookup.json +42 -0
- package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
- package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
- package/rcf/test-suites/ts-016.json +3 -3
- package/rcf/test-suites/ts-215.json +148 -4
- package/rcf/test-suites/ts-216.json +19 -3
- package/rcf/test-suites/ts-218.json +78 -0
- package/rcf/test-suites/ts-219.json +70 -0
- package/rcf/test-suites/ts-220.json +78 -0
- package/rcf/test-suites/ts-221.json +78 -0
- package/rcf/test-suites/ts-222.json +100 -0
- package/rcf/test-suites/ts-223.json +94 -0
- package/rcf/test-suites/ts-224.json +70 -0
- package/rcf/test-suites/ts-225.json +78 -0
- package/rcf/test-suites/ts-226.json +158 -0
- package/rcf/test-suites/ts-227.json +38 -0
- package/rcf/test-suites/ts-228.json +86 -0
- package/rcf/test-suites/ts-229.json +83 -0
- package/rcf/user-stories/us-17302.json +50 -0
- package/rcf/user-stories/us-17401.json +14 -2
- package/rcf/user-stories/us-17402.json +55 -0
- package/rcf/user-stories/us-17403.json +60 -0
- package/rcf/user-stories/us-17502.json +60 -0
- package/rcf/user-stories/us-17503.json +65 -0
- package/rcf/user-stories/us-17601.json +58 -0
- package/rcf/user-stories/us-18001.json +55 -0
- package/rcf/user-stories/us-18002.json +35 -0
- package/rcf/user-stories/us-18101.json +35 -0
- package/rcf/user-stories/us-18202.json +40 -0
- package/rcf/user-stories/us-18601.json +55 -0
- package/rcf/user-stories/us-18602.json +50 -0
- package/rcf/user-stories/us-18603.json +45 -0
- package/rcf/user-stories/us-18701.json +50 -0
- package/rcf/user-stories/us-18702.json +50 -0
- package/rcf/user-stories/us-18801.json +45 -0
- package/rcf/user-stories/us-204.json +59 -0
- package/rcf/user-stories/us-205.json +64 -0
- package/rcf/user-stories/us-206.json +54 -0
- package/releases/releases.yaml +11 -1
- package/src/cli/freeze.js +730 -0
- package/src/cli/help.js +12 -0
- package/src/cli/ledger.js +228 -12
- package/src/cli/questions.js +288 -0
- package/src/cli/readiness.js +267 -49
- package/src/cli/view.js +29 -0
- package/src/define/freeze-record.js +30 -15
- package/src/define/ledgers.js +210 -11
- package/src/intake/index.js +1 -1
- package/src/intake/orchestrator.js +117 -0
- package/src/mcp/tools.js +354 -0
- package/src/query/delta.js +6 -0
- package/src/query/gates.js +314 -39
- package/src/query/index.js +16 -2
- package/src/query/questions.js +438 -0
- package/src/query/readiness.js +248 -7
- package/src/server/index.js +24 -2
- package/src/server/routes.js +68 -1
- package/src/setup/identity-seed.js +17 -0
- package/src/view/build-dag.js +563 -0
- package/src/view/components/badge.js +41 -0
- package/src/view/components/diff.js +72 -0
- package/src/view/components/doc-row.js +62 -0
- package/src/view/components/empty-state.js +35 -0
- package/src/view/components/entity-selector.js +137 -0
- package/src/view/components/filter-bar.js +97 -0
- package/src/view/components/findings-list.js +49 -0
- package/src/view/components/index.js +12 -0
- package/src/view/components/pill.js +46 -0
- package/src/view/components/sub-tab-strip.js +43 -0
- package/src/view/components/toast.js +25 -0
- package/src/view/components-fixture.js +265 -0
- package/src/view/doc-renderers/architecture.js +304 -0
- package/src/view/doc-renderers/build.js +160 -0
- package/src/view/doc-renderers/index.js +2 -0
- package/src/view/doc-renderers/prd.js +35 -6
- package/src/view/html-page.js +700 -602
- package/src/view/index.js +96 -3
- package/src/view/live-client.js +39 -6
- package/src/view/needs-work.js +79 -0
- package/src/view/page-init.js +2193 -0
- package/src/view/readiness/phrasebook.js +115 -0
- package/src/view/readiness/po-layer.js +288 -0
- package/src/view/readiness/question-adapter.js +329 -0
- package/src/view/readiness/stage-legend.js +71 -0
- package/src/view/readiness.js +505 -0
- package/src/view/style.css +2017 -382
- package/src/view/test-host/embed-client.js +121 -0
- package/src/view/test-host/fixture.js +183 -0
- package/src/view/test-host/lints.js +220 -0
- package/src/view/view-index.js +323 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Readiness PO phrasebook (viewer UI refresh PR 8, TAC-4133).
|
|
2
|
+
//
|
|
3
|
+
// One entry per row of the viewer UI refresh design doc section 8's PO
|
|
4
|
+
// question table, keyed by check id. Every entry carries the plain
|
|
5
|
+
// product-owner wording: `heading` (the PO's question), `ask` (the
|
|
6
|
+
// exact ask), `hint` (what kind of answer settles it) and an optional
|
|
7
|
+
// `optional` flag for decisions-with-a-default (they do not hold up
|
|
8
|
+
// the hand-over).
|
|
9
|
+
//
|
|
10
|
+
// The phrasebook ships now so the Remi pilot does not wait on DEFINE
|
|
11
|
+
// step 3 PR 2's `computeQuestions`. When that shape lands, the
|
|
12
|
+
// adapter in `question-adapter.js` switches source via a feature
|
|
13
|
+
// detect; the phrasebook stays as the fallback for any blocker whose
|
|
14
|
+
// check does not carry a precomputed question.
|
|
15
|
+
//
|
|
16
|
+
// Strings never contain em-dashes (per the brief and the register
|
|
17
|
+
// scan AC-18202-3 the chain pack reserves). Keep this file as plain
|
|
18
|
+
// data; no logic.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} PhrasebookEntry
|
|
22
|
+
* @property {string} heading plain question for the PO
|
|
23
|
+
* @property {string} ask the exact sentence to answer
|
|
24
|
+
* @property {string} hint what shape of answer settles it
|
|
25
|
+
* @property {boolean} [optional] decisions-with-a-default rows do not hold up the hand-over
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** @type {Record<string, PhrasebookEntry>} */
|
|
29
|
+
export const PHRASEBOOK = {
|
|
30
|
+
'brief:sinceFreeze': {
|
|
31
|
+
heading: 'What is this project for?',
|
|
32
|
+
ask: 'The brief is empty. Hand me your document, or tell me in a few sentences what this is for.',
|
|
33
|
+
hint: 'Answer with a document or a few sentences.',
|
|
34
|
+
},
|
|
35
|
+
'brief:profile': {
|
|
36
|
+
heading: 'How you want to review and who is writing this?',
|
|
37
|
+
ask: 'Where will you look at what we produce (a web page, the running app, or a code review diff), and are you the product owner or an engineer?',
|
|
38
|
+
hint: 'Pick a review surface and a register.',
|
|
39
|
+
},
|
|
40
|
+
'brief:profile:surface': {
|
|
41
|
+
heading: 'Where will you look at what we produce?',
|
|
42
|
+
ask: 'A web page, the running app, or a code review diff?',
|
|
43
|
+
hint: 'Pick one.',
|
|
44
|
+
},
|
|
45
|
+
'brief:profile:register': {
|
|
46
|
+
heading: 'Who is writing this?',
|
|
47
|
+
ask: 'Are you the product owner or an engineer?',
|
|
48
|
+
hint: 'Pick one.',
|
|
49
|
+
},
|
|
50
|
+
'brief:kinds': {
|
|
51
|
+
heading: 'What kind of statement is this?',
|
|
52
|
+
ask: 'Something the product must do, a rule, a person, a thing it tracks, another system, a screen, out of scope, or a question?',
|
|
53
|
+
hint: 'Pick a kind for the statement your document opens with.',
|
|
54
|
+
},
|
|
55
|
+
'brief:openQuestions': {
|
|
56
|
+
heading: 'Your document asks a question.',
|
|
57
|
+
ask: 'Answer now, or record a decision with options and a default?',
|
|
58
|
+
hint: 'Give an answer, or list the options.',
|
|
59
|
+
},
|
|
60
|
+
'skeleton:resolvedBy': {
|
|
61
|
+
heading: 'Does this statement become a requirement?',
|
|
62
|
+
ask: 'Your document carries a capability statement with no home yet. Is this its own requirement, part of an existing one, or something else (a person, a record, another system, a screen, or out of scope)?',
|
|
63
|
+
hint: 'Its own requirement, part of an existing one, something else, or out of scope.',
|
|
64
|
+
},
|
|
65
|
+
'skeleton:reqIntent': {
|
|
66
|
+
heading: 'A requirement needs a plain description.',
|
|
67
|
+
ask: 'What must the product do here, and which part does it belong to?',
|
|
68
|
+
hint: 'One or two plain sentences, and the area.',
|
|
69
|
+
},
|
|
70
|
+
'stories:reqHasUs': {
|
|
71
|
+
heading: 'Who uses this, and what do they do with it?',
|
|
72
|
+
ask: 'Say it as: as a who, I want what, so that why.',
|
|
73
|
+
hint: 'One sentence is enough; the first acceptance criterion is drafted for you.',
|
|
74
|
+
},
|
|
75
|
+
'decisions:wellFormed': {
|
|
76
|
+
heading: 'A decision has no options or default yet.',
|
|
77
|
+
ask: 'What are the choices, and which would you pick?',
|
|
78
|
+
hint: 'List two or three options and name a default.',
|
|
79
|
+
optional: true,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Look up a phrasebook entry for a check id. Prefers a sub-check keyed
|
|
85
|
+
* by `checkId:itemId` (so `brief:profile` + item `profile:surface`
|
|
86
|
+
* resolves to the surface-specific entry) and falls back to the plain
|
|
87
|
+
* check id. Returns a conservative fallback that names the check id
|
|
88
|
+
* when nothing is on file, so a new check added since the design doc
|
|
89
|
+
* still renders a question (the viewer never omits a blocker from the
|
|
90
|
+
* DOM).
|
|
91
|
+
*
|
|
92
|
+
* @param {string} checkId
|
|
93
|
+
* @param {string} [itemId] id of the failing item (for sub-check keys and the fallback)
|
|
94
|
+
* @returns {PhrasebookEntry}
|
|
95
|
+
*/
|
|
96
|
+
export function phrasebookEntry(checkId, itemId) {
|
|
97
|
+
if (typeof itemId === 'string' && itemId.length > 0) {
|
|
98
|
+
const subKey = `${checkId}:${itemId}`;
|
|
99
|
+
if (PHRASEBOOK[subKey]) return PHRASEBOOK[subKey];
|
|
100
|
+
// Also try the itemId as its own key (brief:profile has items
|
|
101
|
+
// `profile:surface` and `profile:register`; the phrasebook stores
|
|
102
|
+
// them as `brief:profile:surface` and `brief:profile:register`,
|
|
103
|
+
// which is a superset of the item id itself).
|
|
104
|
+
const compoundKey = `${checkId}:${itemId.split(':').pop()}`;
|
|
105
|
+
if (PHRASEBOOK[compoundKey]) return PHRASEBOOK[compoundKey];
|
|
106
|
+
}
|
|
107
|
+
const entry = PHRASEBOOK[checkId];
|
|
108
|
+
if (entry) return entry;
|
|
109
|
+
const subject = itemId ? ` for ${itemId}` : '';
|
|
110
|
+
return {
|
|
111
|
+
heading: `Something needs an answer${subject}.`,
|
|
112
|
+
ask: `Your tree carries an item from the check ${checkId} that is waiting on you${subject}. Tell me what the right next step is in a sentence.`,
|
|
113
|
+
hint: `No phrasebook entry for ${checkId} yet; answer in plain words and the next session will refine it.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// Readiness tab PO layer (viewer UI refresh PR 8, TAC-4133, ADR-4135).
|
|
2
|
+
//
|
|
3
|
+
// Four blocks for the product owner (two VerdictCards at equal
|
|
4
|
+
// height, QuestionCards grouped by source span, a what-happens-next
|
|
5
|
+
// paragraph and a Requirements that still need work table) plus one
|
|
6
|
+
// collapsed "For engineers" DocRow at the foot that carries the full
|
|
7
|
+
// engineer rendering unchanged (never removed from the DOM; AC-18002-3).
|
|
8
|
+
//
|
|
9
|
+
// The PO layer is a server-rendered HTML string: no inline script
|
|
10
|
+
// goes to the page. Equal-height cards come from a CSS grid with
|
|
11
|
+
// `align-items: stretch` on the parent (see src/view/style.css,
|
|
12
|
+
// `.rcf-po-verdicts`), not scripted measurement (ADR-4135).
|
|
13
|
+
|
|
14
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
15
|
+
import { formatVerdictLines } from '../../query/readiness.js';
|
|
16
|
+
import { renderDocRow } from '../components/doc-row.js';
|
|
17
|
+
import { toQuestions, preferReadinessQuestions } from './question-adapter.js';
|
|
18
|
+
import { stageTitle } from './stage-legend.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {import('../../query/readiness.js').ReadinessResult} ReadinessResult
|
|
22
|
+
* @typedef {import('../../query/readiness.js').Blocker} Blocker
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render the four PO blocks plus the For-engineers DocRow.
|
|
27
|
+
*
|
|
28
|
+
* @param {ReadinessResult | null} readiness
|
|
29
|
+
* @param {object} [opts]
|
|
30
|
+
* @param {'productOwner'|'engineer'|'unstated'} [opts.persona='productOwner']
|
|
31
|
+
* @param {string} opts.engineerBody - rendered engineer-level HTML (the former renderReadinessPanel body)
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function renderReadinessPO(readiness, opts) {
|
|
35
|
+
if (!readiness) {
|
|
36
|
+
return '<p><em>Readiness could not be computed for this tree.</em></p>';
|
|
37
|
+
}
|
|
38
|
+
const persona = opts?.persona ?? 'productOwner';
|
|
39
|
+
const engineerBody = typeof opts?.engineerBody === 'string' ? opts.engineerBody : '';
|
|
40
|
+
const verdictLines = formatVerdictLines(readiness);
|
|
41
|
+
const questionsSource = preferReadinessQuestions(readiness) ? 'readiness.questions' : 'blockers';
|
|
42
|
+
const q = toQuestions(readiness);
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
renderVerdictCards(readiness, verdictLines),
|
|
46
|
+
renderQuestionCards(readiness, q, questionsSource),
|
|
47
|
+
renderNextStep(readiness, q),
|
|
48
|
+
renderReqWorkTable(readiness),
|
|
49
|
+
renderForEngineers(readiness, engineerBody, persona),
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---- block 1: two VerdictCards (equal height, chain term muted) --------
|
|
54
|
+
|
|
55
|
+
function renderVerdictCards(readiness, verdictLines) {
|
|
56
|
+
const l = readiness.levels;
|
|
57
|
+
const intentOk = l.intentComplete.ok;
|
|
58
|
+
const buildOk = l.readyToBuild.ok;
|
|
59
|
+
const intentCount = Array.isArray(l.intentComplete.blockedBy) ? l.intentComplete.blockedBy.length : 0;
|
|
60
|
+
const buildBlockers = Array.isArray(l.readyToBuild.blockedBy) ? l.readyToBuild.blockedBy : [];
|
|
61
|
+
const buildTotal = buildBlockers.length;
|
|
62
|
+
const buildPo = buildBlockers.filter((b) => b && b.persona === 'productOwner').length;
|
|
63
|
+
const buildEng = buildBlockers.filter((b) => b && b.persona === 'engineer').length;
|
|
64
|
+
|
|
65
|
+
const intentValueLabel = intentOk ? 'Yes' : 'Not yet';
|
|
66
|
+
const buildValueLabel = buildOk ? 'Yes' : 'Not yet';
|
|
67
|
+
const intentCountLine = intentCount === 0
|
|
68
|
+
? '0 questions waiting for you'
|
|
69
|
+
: `<strong>${intentCount} question${intentCount === 1 ? '' : 's'} waiting for you</strong>`;
|
|
70
|
+
const buildCountLine = buildTotal === 0
|
|
71
|
+
? 'nothing waiting for engineers'
|
|
72
|
+
: `<strong>${buildEng} item${buildEng === 1 ? '' : 's'} for engineers</strong>, ${buildPo === 0 ? 'none' : buildPo} for you`;
|
|
73
|
+
|
|
74
|
+
return `<section class="rcf-po-verdicts" data-rcf-intent-ok="${intentOk ? 'yes' : 'no'}" data-rcf-build-ok="${buildOk ? 'yes' : 'no'}">
|
|
75
|
+
<article class="rcf-po-verdict rcf-po-verdict--intent">
|
|
76
|
+
<p class="rcf-po-verdict__eyebrow muted">Your requirements set</p>
|
|
77
|
+
<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
|
+
<p class="rcf-po-verdict__count">${intentCountLine}</p>
|
|
79
|
+
<p class="rcf-po-verdict__verdict muted"><code>${escapeHtml(verdictLines.intentComplete)}</code></p>
|
|
80
|
+
</article>
|
|
81
|
+
<article class="rcf-po-verdict rcf-po-verdict--build">
|
|
82
|
+
<p class="rcf-po-verdict__eyebrow muted">The build</p>
|
|
83
|
+
<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
|
+
<p class="rcf-po-verdict__count">${buildCountLine}</p>
|
|
85
|
+
<p class="rcf-po-verdict__verdict muted"><code>${escapeHtml(verdictLines.readyToBuild)}</code></p>
|
|
86
|
+
</article>
|
|
87
|
+
</section>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- block 2: QuestionCards grouped by source span --------------------
|
|
91
|
+
|
|
92
|
+
function renderQuestionCards(readiness, q, source) {
|
|
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>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let n = 0;
|
|
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>`);
|
|
195
|
+
}
|
|
196
|
+
if (engCount > 0) {
|
|
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>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---- block 4: Requirements that still need work -----------------------
|
|
210
|
+
|
|
211
|
+
function renderReqWorkTable(readiness) {
|
|
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>`;
|
|
219
|
+
}
|
|
220
|
+
const body = rows.map((r) => `<tr>
|
|
221
|
+
<td><code class="rcf-po-reqwork__id">${escapeHtml(r.reqId)}</code></td>
|
|
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
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return Array.from(reasonById.entries()).map(([reqId, reason]) => ({ reqId, reason }));
|
|
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>`;
|
|
288
|
+
}
|