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.
Files changed (168) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/bin/rcf.js +4 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/define-draft-shapes.md +89 -0
  5. package/guidance/define-intake.md +97 -0
  6. package/guidance/define-intent.md +107 -0
  7. package/guidance/discover-prototype.md +67 -0
  8. package/guidance/discovery-prototypes.md +69 -0
  9. package/guidance/harness-template.md +34 -0
  10. package/guidance/managed/agent-instructions-block.hash +1 -1
  11. package/guidance/managed/agent-instructions-block.md +34 -0
  12. package/guidance/manifest.json +9 -2
  13. package/package.json +1 -1
  14. package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
  15. package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
  16. package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
  17. package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
  18. package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
  19. package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
  20. package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
  21. package/rcf/adrs/adr-4134-index-json-route.json +35 -0
  22. package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
  23. package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
  24. package/rcf/code-nodes/cn-725.json +18 -2
  25. package/rcf/code-nodes/cn-726.json +10 -2
  26. package/rcf/code-nodes/cn-727.json +13 -4
  27. package/rcf/code-nodes/cn-728.json +18 -2
  28. package/rcf/code-nodes/cn-729.json +10 -2
  29. package/rcf/code-nodes/cn-730.json +13 -3
  30. package/rcf/code-nodes/cn-731.json +20 -0
  31. package/rcf/code-nodes/cn-732.json +14 -0
  32. package/rcf/code-nodes/cn-733.json +14 -0
  33. package/rcf/code-nodes/cn-734.json +14 -0
  34. package/rcf/code-nodes/cn-735.json +20 -0
  35. package/rcf/code-nodes/cn-736.json +14 -0
  36. package/rcf/code-nodes/cn-737.json +14 -0
  37. package/rcf/code-nodes/cn-738.json +14 -0
  38. package/rcf/code-nodes/cn-739.json +18 -0
  39. package/rcf/code-nodes/cn-740.json +19 -0
  40. package/rcf/code-nodes/cn-741.json +16 -0
  41. package/rcf/code-nodes/cn-742.json +16 -0
  42. package/rcf/code-nodes/cn-743.json +15 -0
  43. package/rcf/code-nodes/cn-744.json +22 -0
  44. package/rcf/code-nodes/cn-745.json +16 -0
  45. package/rcf/code-nodes/cn-746.json +14 -0
  46. package/rcf/code-nodes/cn-747.json +18 -0
  47. package/rcf/code-nodes/cn-748.json +16 -0
  48. package/rcf/code-nodes/cn-749.json +16 -0
  49. package/rcf/code-nodes/cn-750.json +15 -0
  50. package/rcf/code-nodes/cn-751.json +18 -0
  51. package/rcf/code-nodes/cn-752.json +14 -0
  52. package/rcf/fbs/fbs-196.json +13 -5
  53. package/rcf/fbs/fbs-198.json +42 -0
  54. package/rcf/fbs/fbs-199.json +59 -0
  55. package/rcf/fbs/fbs-200.json +46 -0
  56. package/rcf/fbs/fbs-201.json +49 -0
  57. package/rcf/fbs/fbs-202.json +110 -0
  58. package/rcf/fbs/fbs-203.json +100 -0
  59. package/rcf/requirements/req-002.json +2 -2
  60. package/rcf/requirements/req-173.json +2 -2
  61. package/rcf/requirements/req-174.json +10 -4
  62. package/rcf/requirements/req-175.json +10 -4
  63. package/rcf/requirements/req-176.json +25 -0
  64. package/rcf/requirements/req-180.json +31 -0
  65. package/rcf/requirements/req-181.json +31 -0
  66. package/rcf/requirements/req-182.json +25 -0
  67. package/rcf/requirements/req-186.json +25 -0
  68. package/rcf/requirements/req-187.json +31 -0
  69. package/rcf/requirements/req-188.json +25 -0
  70. package/rcf/tacs/tac-4121-define-ledgers.json +15 -3
  71. package/rcf/tacs/tac-4122-define-gates.json +40 -4
  72. package/rcf/tacs/tac-4123-define-readiness.json +39 -3
  73. package/rcf/tacs/tac-4124-define-freeze.json +28 -0
  74. package/rcf/tacs/tac-4127-readiness-view.json +49 -0
  75. package/rcf/tacs/tac-4128-define-guidance.json +53 -0
  76. package/rcf/tacs/tac-4129-define-questions.json +45 -0
  77. package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
  78. package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
  79. package/rcf/tacs/tac-4132-id-lookup.json +42 -0
  80. package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
  81. package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
  82. package/rcf/test-suites/ts-016.json +3 -3
  83. package/rcf/test-suites/ts-215.json +148 -4
  84. package/rcf/test-suites/ts-216.json +19 -3
  85. package/rcf/test-suites/ts-218.json +78 -0
  86. package/rcf/test-suites/ts-219.json +70 -0
  87. package/rcf/test-suites/ts-220.json +78 -0
  88. package/rcf/test-suites/ts-221.json +78 -0
  89. package/rcf/test-suites/ts-222.json +100 -0
  90. package/rcf/test-suites/ts-223.json +94 -0
  91. package/rcf/test-suites/ts-224.json +70 -0
  92. package/rcf/test-suites/ts-225.json +78 -0
  93. package/rcf/test-suites/ts-226.json +158 -0
  94. package/rcf/test-suites/ts-227.json +38 -0
  95. package/rcf/test-suites/ts-228.json +86 -0
  96. package/rcf/test-suites/ts-229.json +83 -0
  97. package/rcf/user-stories/us-17302.json +50 -0
  98. package/rcf/user-stories/us-17401.json +14 -2
  99. package/rcf/user-stories/us-17402.json +55 -0
  100. package/rcf/user-stories/us-17403.json +60 -0
  101. package/rcf/user-stories/us-17502.json +60 -0
  102. package/rcf/user-stories/us-17503.json +65 -0
  103. package/rcf/user-stories/us-17601.json +58 -0
  104. package/rcf/user-stories/us-18001.json +55 -0
  105. package/rcf/user-stories/us-18002.json +35 -0
  106. package/rcf/user-stories/us-18101.json +35 -0
  107. package/rcf/user-stories/us-18202.json +40 -0
  108. package/rcf/user-stories/us-18601.json +55 -0
  109. package/rcf/user-stories/us-18602.json +50 -0
  110. package/rcf/user-stories/us-18603.json +45 -0
  111. package/rcf/user-stories/us-18701.json +50 -0
  112. package/rcf/user-stories/us-18702.json +50 -0
  113. package/rcf/user-stories/us-18801.json +45 -0
  114. package/rcf/user-stories/us-204.json +59 -0
  115. package/rcf/user-stories/us-205.json +64 -0
  116. package/rcf/user-stories/us-206.json +54 -0
  117. package/releases/releases.yaml +11 -1
  118. package/src/cli/freeze.js +730 -0
  119. package/src/cli/help.js +12 -0
  120. package/src/cli/ledger.js +228 -12
  121. package/src/cli/questions.js +288 -0
  122. package/src/cli/readiness.js +267 -49
  123. package/src/cli/view.js +29 -0
  124. package/src/define/freeze-record.js +30 -15
  125. package/src/define/ledgers.js +210 -11
  126. package/src/intake/index.js +1 -1
  127. package/src/intake/orchestrator.js +117 -0
  128. package/src/mcp/tools.js +354 -0
  129. package/src/query/delta.js +6 -0
  130. package/src/query/gates.js +314 -39
  131. package/src/query/index.js +16 -2
  132. package/src/query/questions.js +438 -0
  133. package/src/query/readiness.js +248 -7
  134. package/src/server/index.js +24 -2
  135. package/src/server/routes.js +68 -1
  136. package/src/setup/identity-seed.js +17 -0
  137. package/src/view/build-dag.js +563 -0
  138. package/src/view/components/badge.js +41 -0
  139. package/src/view/components/diff.js +72 -0
  140. package/src/view/components/doc-row.js +62 -0
  141. package/src/view/components/empty-state.js +35 -0
  142. package/src/view/components/entity-selector.js +137 -0
  143. package/src/view/components/filter-bar.js +97 -0
  144. package/src/view/components/findings-list.js +49 -0
  145. package/src/view/components/index.js +12 -0
  146. package/src/view/components/pill.js +46 -0
  147. package/src/view/components/sub-tab-strip.js +43 -0
  148. package/src/view/components/toast.js +25 -0
  149. package/src/view/components-fixture.js +265 -0
  150. package/src/view/doc-renderers/architecture.js +304 -0
  151. package/src/view/doc-renderers/build.js +160 -0
  152. package/src/view/doc-renderers/index.js +2 -0
  153. package/src/view/doc-renderers/prd.js +35 -6
  154. package/src/view/html-page.js +700 -602
  155. package/src/view/index.js +96 -3
  156. package/src/view/live-client.js +39 -6
  157. package/src/view/needs-work.js +79 -0
  158. package/src/view/page-init.js +2193 -0
  159. package/src/view/readiness/phrasebook.js +115 -0
  160. package/src/view/readiness/po-layer.js +288 -0
  161. package/src/view/readiness/question-adapter.js +329 -0
  162. package/src/view/readiness/stage-legend.js +71 -0
  163. package/src/view/readiness.js +505 -0
  164. package/src/view/style.css +2017 -382
  165. package/src/view/test-host/embed-client.js +121 -0
  166. package/src/view/test-host/fixture.js +183 -0
  167. package/src/view/test-host/lints.js +220 -0
  168. 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&amp;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
+ }