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,500 @@
|
|
|
1
|
+
// Readiness command-page tables (TAC-4135 renderQuestionsTable,
|
|
2
|
+
// renderBlockingTable, QuestionRow, BlockingRow, FilterBar; ADR-4139;
|
|
3
|
+
// FBS-204, US-18003 AC-18003-1..5, AC-18003-7, AC-18003-8).
|
|
4
|
+
//
|
|
5
|
+
// Two pure HTML-string renderers over the ReadinessResult the viewer
|
|
6
|
+
// already has, read-only: no CLI command text, no write control, no
|
|
7
|
+
// Freeze now (AC-18003-7, ADR-4139). Every row carries the item id,
|
|
8
|
+
// the plain-English ask or finding, the chain location, a link that
|
|
9
|
+
// opens the item in full context, and what resolved looks like
|
|
10
|
+
// (ADR-4139 decision line). Composite failing ids split on the first
|
|
11
|
+
// colon so every blocking link resolves through findByDocId (AC-18003-4).
|
|
12
|
+
//
|
|
13
|
+
// Shared with FBS-205..208: the questions and blocking tables are the
|
|
14
|
+
// "linked tables over the readiness object" the ADR pins. The
|
|
15
|
+
// Copy-for-your-agent handle (AC-18003-5) uses the shared toast helper
|
|
16
|
+
// in page-init.js; wiring sits there, markup here (data-rcf-copy-for).
|
|
17
|
+
//
|
|
18
|
+
// No em-dashes anywhere in this file. The strings are plain product
|
|
19
|
+
// words; nothing routes to a CLI command.
|
|
20
|
+
|
|
21
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
22
|
+
import { phrasebookEntry } from './phrasebook.js';
|
|
23
|
+
import { splitCompositeId } from './composite-id.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} QuestionRow
|
|
27
|
+
* @property {number} number 1-based row number across the table
|
|
28
|
+
* @property {string} itemId the failing item id (brief:ledger, profile:surface, REQ-012, ...)
|
|
29
|
+
* @property {string} group the source span label used for the group-column sort key
|
|
30
|
+
* @property {string} ask plain-English sentence the owner answers
|
|
31
|
+
* @property {string} settles what shape of answer resolves this row
|
|
32
|
+
* @property {string} state 'open' | 'optional' (the chain's state for the question)
|
|
33
|
+
* @property {string} locationHref the hash: #tab=readiness&sub=questions&entity=<itemId>
|
|
34
|
+
* @property {string} locationLabel short human label naming the chain location
|
|
35
|
+
* @property {string} briefHandle one line "<itemId>: <ask>" with no command text
|
|
36
|
+
* @property {string} checkId '<stage>/<check>' for the hover title and stage-ref
|
|
37
|
+
* @property {string} stage Dn stage key
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} BlockingRow
|
|
42
|
+
* @property {string} stage Dn stage key
|
|
43
|
+
* @property {string} check check name (brief:sinceFreeze, stories:reqHasUs, ...)
|
|
44
|
+
* @property {string} rawId the failing id as it appears on the chain (may be composite)
|
|
45
|
+
* @property {string} docId `splitCompositeId(rawId).docId`
|
|
46
|
+
* @property {string} fragment `splitCompositeId(rawId).fragment`
|
|
47
|
+
* @property {string} href `#<docId>` when docId is set, else `#rcf-readiness-row-<rawId-slug>`
|
|
48
|
+
* @property {string} why gate explanation from `stages[].checks[].failing[].why`
|
|
49
|
+
* @property {string} resolved plain words naming what the row looks like when it clears
|
|
50
|
+
* @property {string} persona 'productOwner' | 'engineer'
|
|
51
|
+
* @property {string} question the check's question (surfaces as a readable tooltip)
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} TablesOpts
|
|
56
|
+
* @property {{stage?: string, persona?: string}} [filter] optional FilterBar state for the blocking table
|
|
57
|
+
* @property {object | null} [freezeRecord] freeze record on disk (for the freeze-state block)
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
// Safe DOM-id token for an itemId that may hold colons, slashes,
|
|
61
|
+
// angle brackets or spaces (profile:surface, brief:openQuestions,
|
|
62
|
+
// TAC-4122-define-gates:checkPersona). Mirrors page-init.js
|
|
63
|
+
// slugifyEntity so the router can find the row by data-rcf-entity.
|
|
64
|
+
export function entityDomId(itemId) {
|
|
65
|
+
const s = String(itemId ?? '');
|
|
66
|
+
return `rcf-readiness-row-${s.replace(/[^A-Za-z0-9-]/g, '_')}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Document-id prefixes the viewer's hash router can resolve to a
|
|
70
|
+
// rendered node via findByDocId. Non-document prefixes (profile, ADR,
|
|
71
|
+
// D1..D8, TAD.*) render as plain text in the blocking table because
|
|
72
|
+
// their anchor would be dead. Dave ruling 2026-10-08: chain ids are
|
|
73
|
+
// content, rendered as a prefix link (when the prefix is a resolvable
|
|
74
|
+
// doc id) with the full composite id in a data attribute, never as a
|
|
75
|
+
// visible shell command fragment.
|
|
76
|
+
const RESOLVABLE_DOC_PREFIX = /^(REQ|AC|US|TAC|TS|TC|FBS|ADR|TAD|CN|PRD)-/;
|
|
77
|
+
function isResolvableDocId(id) {
|
|
78
|
+
return typeof id === 'string' && RESOLVABLE_DOC_PREFIX.test(id);
|
|
79
|
+
}
|
|
80
|
+
export { isResolvableDocId };
|
|
81
|
+
|
|
82
|
+
// Document-id itemId used in question rows. Reaches the "opens the
|
|
83
|
+
// item in full context" clause of AC-18003-1: the bare hash resolves
|
|
84
|
+
// to the item's own tab (see page-init.js resolveHash fast path).
|
|
85
|
+
// Non-document itemIds (brief:ledger, profile:surface, open-decision:5)
|
|
86
|
+
// stay on the readiness tab with the questions sub-view, since that
|
|
87
|
+
// is where the row lives.
|
|
88
|
+
function pickLocationHref(itemId) {
|
|
89
|
+
if (typeof itemId !== 'string' || itemId.length === 0) {
|
|
90
|
+
return '#tab=readiness&sub=questions';
|
|
91
|
+
}
|
|
92
|
+
if (isResolvableDocId(itemId)) return `#${itemId}`;
|
|
93
|
+
return `#tab=readiness&sub=questions&entity=${encodeURIComponent(itemId)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const SETTLES_FALLBACK = 'Answer the ask or record a decision; the row clears when the chain updates.';
|
|
97
|
+
const RESOLVED_FALLBACK = 'The gate passes when the named failing items clear on the chain.';
|
|
98
|
+
|
|
99
|
+
const RESOLVED_BY_CHECK = {
|
|
100
|
+
'brief:sinceFreeze': 'A brief statement is written, or the operator records text for one.',
|
|
101
|
+
'brief:kinds': 'Every statement carries a kind (capability, constraint, actor, record, system, surface, outOfScope, question).',
|
|
102
|
+
'brief:openQuestions': 'Every open question in the brief is answered or promoted to a decision.',
|
|
103
|
+
'brief:profile': 'The profile names a review surface and a register.',
|
|
104
|
+
'brief:profile:surface': 'The profile names a review surface (viewer, runningApp or prDiff).',
|
|
105
|
+
'brief:profile:register': 'The profile names a register (productOwner or engineer).',
|
|
106
|
+
'skeleton:resolvedBy': 'The statement is placed under a requirement (new or existing) or marked outOfScope.',
|
|
107
|
+
'skeleton:reqIntent': 'The requirement carries a plain description and an area.',
|
|
108
|
+
'stories:reqHasUs': 'At least one user story is written under the requirement.',
|
|
109
|
+
'stories:closedSets': 'The story set under the requirement is closed (no open hooks).',
|
|
110
|
+
'stories:usFloors': 'The user story has the minimum acceptance criteria the chain requires.',
|
|
111
|
+
'shapes:tacHasInterface': 'Every TAC carries at least one interface.',
|
|
112
|
+
'shapes:pathsResolve': 'Every interface path resolves to a file on disk.',
|
|
113
|
+
'shapes:tacComplete': 'Every TAC carries interfaces, responsibilities and dependencies.',
|
|
114
|
+
'crosscut:securityArchitecture': 'The TAD security architecture is written.',
|
|
115
|
+
'crosscut:performanceArchitecture': 'The TAD performance architecture is written.',
|
|
116
|
+
'crosscut:observability': 'The TAD observability section is written.',
|
|
117
|
+
'crosscut:operationalConcerns': 'The operational concerns section is complete.',
|
|
118
|
+
'consistency:contradictions': 'The named contradictions in the chain are resolved.',
|
|
119
|
+
'consistency:unsatisfiable': 'The unsatisfiable statement is rewritten or retired.',
|
|
120
|
+
'decisions:wellFormed': 'Every open decision has two or more options and a named default.',
|
|
121
|
+
'decisions:resolved': 'Every open decision is resolved.',
|
|
122
|
+
'freeze:treeHash': 'The frozen tree hash matches the current tree hash.',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const SETTLES_BY_CHECK = {
|
|
126
|
+
'brief:sinceFreeze': 'A document path or a few sentences.',
|
|
127
|
+
'brief:kinds': 'Pick a kind for the statement.',
|
|
128
|
+
'brief:openQuestions': 'Give an answer or list the options and a default.',
|
|
129
|
+
'brief:profile:surface': 'Pick viewer, runningApp or prDiff.',
|
|
130
|
+
'brief:profile:register': 'Pick productOwner or engineer.',
|
|
131
|
+
'skeleton:resolvedBy': 'Say which requirement this belongs to, or mark it out of scope.',
|
|
132
|
+
'skeleton:reqIntent': 'One or two plain sentences, and the area.',
|
|
133
|
+
'stories:reqHasUs': 'One sentence: as a who, I want what, so that why.',
|
|
134
|
+
'decisions:wellFormed': 'Two or more options and a named default.',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Plain words for a stage's product name. Mirrors stage-legend titles
|
|
138
|
+
// so the row surface uses the same vocabulary readers already learned.
|
|
139
|
+
const STAGE_LABEL = {
|
|
140
|
+
D1: 'Brief',
|
|
141
|
+
D2: 'Skeleton',
|
|
142
|
+
D3: 'Shapes',
|
|
143
|
+
D4: 'Stories',
|
|
144
|
+
D5: 'Crosscut',
|
|
145
|
+
D6: 'Consistency',
|
|
146
|
+
D7: 'Decisions',
|
|
147
|
+
D8: 'Freeze',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
function stageLabel(stage) {
|
|
151
|
+
return STAGE_LABEL[stage] ?? stage;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sourceSpanLabel(span) {
|
|
155
|
+
if (typeof span !== 'string' || span.length === 0) return 'The tree';
|
|
156
|
+
if (span === 'tree') return 'The tree';
|
|
157
|
+
if (span.startsWith('brief:')) {
|
|
158
|
+
const n = span.slice('brief:'.length);
|
|
159
|
+
return /^\d+$/.test(n) ? `Brief statement ${n}` : 'Brief';
|
|
160
|
+
}
|
|
161
|
+
if (/^REQ-/.test(span)) return `Requirement ${span}`;
|
|
162
|
+
if (/^US-/.test(span)) return `User story ${span}`;
|
|
163
|
+
if (/^AC-/.test(span)) return `Acceptance criterion ${span}`;
|
|
164
|
+
if (/^TAC-/.test(span)) return `Technical area ${span}`;
|
|
165
|
+
return span;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function pickItemId(q) {
|
|
169
|
+
if (!q || typeof q !== 'object') return '';
|
|
170
|
+
if (typeof q.itemId === 'string' && q.itemId.length > 0) return q.itemId;
|
|
171
|
+
if (typeof q.id === 'string' && /^(brief:|profile:|decision:|REQ-|AC-|TAC-|US-|TS-|TC-|FBS-|ADR-|TAD-)/.test(q.id)) return q.id;
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pickGroupLabel(q) {
|
|
176
|
+
if (!q || typeof q !== 'object') return 'The tree';
|
|
177
|
+
if (typeof q.sourceSpanLabel === 'string' && q.sourceSpanLabel.length > 0) return q.sourceSpanLabel;
|
|
178
|
+
const ctx = q.context && typeof q.context === 'object' ? q.context : null;
|
|
179
|
+
const stmtSrc = ctx && typeof ctx.statement === 'object' && ctx.statement
|
|
180
|
+
? ctx.statement.source
|
|
181
|
+
: null;
|
|
182
|
+
const span = typeof q.sourceSpan === 'string' && q.sourceSpan.length > 0
|
|
183
|
+
? q.sourceSpan
|
|
184
|
+
: (typeof stmtSrc === 'string' && stmtSrc.length > 0
|
|
185
|
+
? stmtSrc
|
|
186
|
+
: (ctx && typeof ctx.sourceSpan === 'string' ? ctx.sourceSpan : ''));
|
|
187
|
+
return sourceSpanLabel(span);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function pickState(q) {
|
|
191
|
+
if (q && q.optional === true) return 'optional';
|
|
192
|
+
const blocks = typeof q?.blocks === 'string' ? q.blocks : '';
|
|
193
|
+
if (blocks === 'build') return 'open-build';
|
|
194
|
+
return 'open';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildBriefHandle(itemId, ask) {
|
|
198
|
+
const a = typeof ask === 'string' ? ask.trim() : '';
|
|
199
|
+
const id = typeof itemId === 'string' ? itemId : '';
|
|
200
|
+
if (!id) return a;
|
|
201
|
+
if (!a) return id;
|
|
202
|
+
return `${id}: ${a}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Project `readiness.questions` into QuestionRow[]. Falls back to the
|
|
207
|
+
* phrasebook when a question record is missing ask/heading (older
|
|
208
|
+
* computeQuestions outputs or synthetic test fixtures).
|
|
209
|
+
*
|
|
210
|
+
* @param {Array<object>} questions
|
|
211
|
+
* @returns {QuestionRow[]}
|
|
212
|
+
*/
|
|
213
|
+
export function buildQuestionRows(questions) {
|
|
214
|
+
if (!Array.isArray(questions)) return [];
|
|
215
|
+
/** @type {QuestionRow[]} */
|
|
216
|
+
const rows = [];
|
|
217
|
+
let n = 0;
|
|
218
|
+
for (const raw of questions) {
|
|
219
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
220
|
+
const q = /** @type {Record<string, unknown>} */ (raw);
|
|
221
|
+
n += 1;
|
|
222
|
+
const checkId = typeof q.checkId === 'string'
|
|
223
|
+
? q.checkId
|
|
224
|
+
: (typeof q.check === 'string' ? q.check : '');
|
|
225
|
+
const stage = typeof q.stage === 'string' ? q.stage : '';
|
|
226
|
+
const itemId = pickItemId(q);
|
|
227
|
+
const pb = phrasebookEntry(checkId, itemId);
|
|
228
|
+
const ask = typeof q.ask === 'string' && q.ask.length > 0 ? q.ask : pb.ask;
|
|
229
|
+
const settles = typeof q.hint === 'string' && q.hint.length > 0
|
|
230
|
+
? q.hint
|
|
231
|
+
: (SETTLES_BY_CHECK[checkId] ?? pb.hint ?? SETTLES_FALLBACK);
|
|
232
|
+
rows.push({
|
|
233
|
+
number: n,
|
|
234
|
+
itemId,
|
|
235
|
+
group: pickGroupLabel(q),
|
|
236
|
+
ask,
|
|
237
|
+
settles,
|
|
238
|
+
state: pickState(q),
|
|
239
|
+
locationHref: pickLocationHref(itemId),
|
|
240
|
+
locationLabel: itemId || `${stage}/${checkId}`,
|
|
241
|
+
briefHandle: buildBriefHandle(itemId, ask),
|
|
242
|
+
checkId: stage && checkId ? `${stage}/${checkId}` : (checkId || stage),
|
|
243
|
+
stage,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return rows;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Project every failing check across stages into BlockingRow[]. One
|
|
251
|
+
* row per failing item (never capped; AC-18003-3). Rows carry the
|
|
252
|
+
* composite split so the link resolves through findByDocId.
|
|
253
|
+
*
|
|
254
|
+
* @param {Array<object>} stages
|
|
255
|
+
* @returns {BlockingRow[]}
|
|
256
|
+
*/
|
|
257
|
+
export function buildBlockingRows(stages) {
|
|
258
|
+
if (!Array.isArray(stages)) return [];
|
|
259
|
+
/** @type {BlockingRow[]} */
|
|
260
|
+
const rows = [];
|
|
261
|
+
for (const s of stages) {
|
|
262
|
+
if (!s || typeof s !== 'object') continue;
|
|
263
|
+
const stage = typeof s.stage === 'string' ? s.stage : '';
|
|
264
|
+
const checks = Array.isArray(s.checks) ? s.checks : [];
|
|
265
|
+
for (const c of checks) {
|
|
266
|
+
if (!c || typeof c !== 'object' || c.ok) continue;
|
|
267
|
+
const check = typeof c.name === 'string' ? c.name : '';
|
|
268
|
+
const persona = typeof c.persona === 'string' ? c.persona : 'engineer';
|
|
269
|
+
const question = typeof c.question === 'string' ? c.question : '';
|
|
270
|
+
const failing = Array.isArray(c.failing) ? c.failing : [];
|
|
271
|
+
for (const f of failing) {
|
|
272
|
+
if (!f || typeof f !== 'object') continue;
|
|
273
|
+
const rawId = typeof f.id === 'string' ? f.id : '';
|
|
274
|
+
const why = typeof f.why === 'string' ? f.why : '';
|
|
275
|
+
const split = splitCompositeId(rawId);
|
|
276
|
+
const href = split.docId ? `#${split.docId}` : `#${entityDomId(rawId)}`;
|
|
277
|
+
rows.push({
|
|
278
|
+
stage,
|
|
279
|
+
check,
|
|
280
|
+
rawId,
|
|
281
|
+
docId: split.docId,
|
|
282
|
+
fragment: split.fragment,
|
|
283
|
+
href,
|
|
284
|
+
why,
|
|
285
|
+
resolved: RESOLVED_BY_CHECK[check] ?? RESOLVED_FALLBACK,
|
|
286
|
+
persona,
|
|
287
|
+
question,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return rows;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---- renderQuestionsTable ------------------------------------------
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Render the one questions table (AC-18003-1, -2, -5, -8). The caller
|
|
299
|
+
* passes the already-projected rows (via `buildQuestionRows`) or raw
|
|
300
|
+
* `readiness.questions` and `rows` wins when both are set.
|
|
301
|
+
*
|
|
302
|
+
* @param {object} args
|
|
303
|
+
* @param {Array<object>} [args.questions] raw readiness.questions[]
|
|
304
|
+
* @param {QuestionRow[]} [args.rows] pre-projected rows (test shim)
|
|
305
|
+
* @returns {string}
|
|
306
|
+
*/
|
|
307
|
+
export function renderQuestionsTable(args = {}) {
|
|
308
|
+
const rows = Array.isArray(args.rows) ? args.rows : buildQuestionRows(args.questions ?? []);
|
|
309
|
+
const total = rows.length;
|
|
310
|
+
const headerCount = `<span class="rcf-badge rcf-badge--count">${total}</span>`;
|
|
311
|
+
if (total === 0) {
|
|
312
|
+
// AC-18003-8: both tables must be present. Render the <table>
|
|
313
|
+
// skeleton with the thead so the "both tables present" check is
|
|
314
|
+
// literally true, and surface the empty-state caption in-table.
|
|
315
|
+
return `<section class="rcf-cmd-table rcf-cmd-table--questions" data-rcf-table="questions" data-rcf-empty="yes" aria-labelledby="rcf-readiness-questions-heading">
|
|
316
|
+
<header class="rcf-cmd-table__head"><h3 id="rcf-readiness-questions-heading">Questions for you</h3> ${headerCount}</header>
|
|
317
|
+
<p class="muted small">Nothing on this tree needs your answer right now; this page updates on its own as the tree changes.</p>
|
|
318
|
+
<table class="rcf-cmd-table__table" aria-describedby="rcf-readiness-questions-heading">
|
|
319
|
+
<caption class="rcf-sr-only">On me: no items</caption>
|
|
320
|
+
<thead>
|
|
321
|
+
<tr>
|
|
322
|
+
<th scope="col" data-rcf-col="number" data-rcf-sortable="yes">#</th>
|
|
323
|
+
<th scope="col" data-rcf-col="group" data-rcf-sortable="yes">Group</th>
|
|
324
|
+
<th scope="col" data-rcf-col="ask">Ask</th>
|
|
325
|
+
<th scope="col" data-rcf-col="location" data-rcf-sortable="yes">Chain location</th>
|
|
326
|
+
<th scope="col" data-rcf-col="settles">What resolves it</th>
|
|
327
|
+
<th scope="col" data-rcf-col="state" data-rcf-sortable="yes">State</th>
|
|
328
|
+
<th scope="col" data-rcf-col="trace"><span class="rcf-sr-only">Trace this id</span></th>
|
|
329
|
+
<th scope="col" data-rcf-col="copy"><span class="rcf-sr-only">Copy for your agent</span></th>
|
|
330
|
+
</tr>
|
|
331
|
+
</thead>
|
|
332
|
+
<tbody></tbody>
|
|
333
|
+
</table>
|
|
334
|
+
</section>`;
|
|
335
|
+
}
|
|
336
|
+
const body = rows.map(renderQuestionRow).join('');
|
|
337
|
+
return `<section class="rcf-cmd-table rcf-cmd-table--questions" data-rcf-table="questions" aria-labelledby="rcf-readiness-questions-heading">
|
|
338
|
+
<header class="rcf-cmd-table__head"><h3 id="rcf-readiness-questions-heading">Questions for you</h3> ${headerCount}</header>
|
|
339
|
+
<p class="muted small">Each row names an item the owner answers; the copy button hands your agent the id and the ask with no command text. The Trace action opens the matrix for the item.</p>
|
|
340
|
+
<table class="rcf-cmd-table__table" aria-describedby="rcf-readiness-questions-heading">
|
|
341
|
+
<thead>
|
|
342
|
+
<tr>
|
|
343
|
+
<th scope="col" data-rcf-col="number" data-rcf-sortable="yes">#</th>
|
|
344
|
+
<th scope="col" data-rcf-col="group" data-rcf-sortable="yes">Group</th>
|
|
345
|
+
<th scope="col" data-rcf-col="ask">Ask</th>
|
|
346
|
+
<th scope="col" data-rcf-col="location" data-rcf-sortable="yes">Chain location</th>
|
|
347
|
+
<th scope="col" data-rcf-col="settles">What resolves it</th>
|
|
348
|
+
<th scope="col" data-rcf-col="state" data-rcf-sortable="yes">State</th>
|
|
349
|
+
<th scope="col" data-rcf-col="trace"><span class="rcf-sr-only">Trace this id</span></th>
|
|
350
|
+
<th scope="col" data-rcf-col="copy"><span class="rcf-sr-only">Copy for your agent</span></th>
|
|
351
|
+
</tr>
|
|
352
|
+
</thead>
|
|
353
|
+
<tbody>${body}</tbody>
|
|
354
|
+
</table>
|
|
355
|
+
</section>`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// FBS-208 (AC-209-3): the Trace action links to the Trace sub-tab for
|
|
359
|
+
// the row's id. Non-document ids (brief:ledger, profile:surface, ...)
|
|
360
|
+
// have no chain pivot and emit a disabled span instead of a dead link.
|
|
361
|
+
function traceActionFor(id) {
|
|
362
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
363
|
+
return `<span class="rcf-trace-action rcf-trace-action--na" data-rcf-trace="no" aria-label="No trace available for this item">-</span>`;
|
|
364
|
+
}
|
|
365
|
+
if (!isResolvableDocId(id)) {
|
|
366
|
+
return `<span class="rcf-trace-action rcf-trace-action--na" data-rcf-trace="no" aria-label="No trace available for this item">-</span>`;
|
|
367
|
+
}
|
|
368
|
+
const href = `#tab=readiness&sub=trace&entity=${encodeURIComponent(id)}`;
|
|
369
|
+
return `<a class="rcf-trace-action" data-rcf-trace-for="${escapeHtml(id)}" href="${escapeHtml(href)}">Trace</a>`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderQuestionRow(r) {
|
|
373
|
+
const stageAttr = r.stage ? ` data-rcf-stage="${escapeHtml(r.stage)}"` : '';
|
|
374
|
+
const locationLink = `<a href="${escapeHtml(r.locationHref)}" data-rcf-location="${escapeHtml(r.itemId)}">${escapeHtml(r.locationLabel)}</a>`;
|
|
375
|
+
const copyLabel = 'Copy for your agent';
|
|
376
|
+
return `<tr id="${escapeHtml(entityDomId(r.itemId))}" data-rcf-entity="${escapeHtml(r.itemId)}" data-rcf-state="${escapeHtml(r.state)}" data-rcf-group="${escapeHtml(r.group)}"${stageAttr}>
|
|
377
|
+
<td data-rcf-col="number">${r.number}</td>
|
|
378
|
+
<td data-rcf-col="group">${escapeHtml(r.group)}</td>
|
|
379
|
+
<td data-rcf-col="ask">${escapeHtml(r.ask)}</td>
|
|
380
|
+
<td data-rcf-col="location">${locationLink}</td>
|
|
381
|
+
<td data-rcf-col="settles">${escapeHtml(r.settles)}</td>
|
|
382
|
+
<td data-rcf-col="state">${escapeHtml(r.state)}</td>
|
|
383
|
+
<td data-rcf-col="trace">${traceActionFor(r.itemId)}</td>
|
|
384
|
+
<td data-rcf-col="copy"><button type="button" class="rcf-cmd-copy" data-rcf-copy-for="${escapeHtml(r.itemId)}" data-rcf-copy-text="${escapeHtml(r.briefHandle)}" title="${escapeHtml(copyLabel)}">${escapeHtml(copyLabel)}</button></td>
|
|
385
|
+
</tr>`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---- renderBlockingTable -------------------------------------------
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Render the one blocking table (AC-18003-3, -4, -7, -8). FilterBar
|
|
392
|
+
* state is markup only in this file; the wiring lives in page-init.js
|
|
393
|
+
* and reads `data-rcf-stage` / `data-rcf-persona` on each row. Rows
|
|
394
|
+
* are not capped (every failing id across every failing check is a row).
|
|
395
|
+
*
|
|
396
|
+
* @param {object} args
|
|
397
|
+
* @param {Array<object>} [args.stages] readiness.stages
|
|
398
|
+
* @param {BlockingRow[]} [args.rows] pre-projected rows (test shim)
|
|
399
|
+
* @param {{stage?: string, persona?: string}} [args.filter] initial FilterBar state
|
|
400
|
+
* @returns {string}
|
|
401
|
+
*/
|
|
402
|
+
export function renderBlockingTable(args = {}) {
|
|
403
|
+
const rows = Array.isArray(args.rows) ? args.rows : buildBlockingRows(args.stages ?? []);
|
|
404
|
+
const total = rows.length;
|
|
405
|
+
const filter = args.filter ?? {};
|
|
406
|
+
const headerCount = `<span class="rcf-badge rcf-badge--count">${total}</span>`;
|
|
407
|
+
if (total === 0) {
|
|
408
|
+
// AC-18003-8: both tables must be present even when there is
|
|
409
|
+
// nothing to list. Render the <table> skeleton with the thead so
|
|
410
|
+
// the "both tables present" check is literally true.
|
|
411
|
+
return `<section class="rcf-cmd-table rcf-cmd-table--blocking" data-rcf-table="blocking" data-rcf-empty="yes" aria-labelledby="rcf-readiness-blocking-heading">
|
|
412
|
+
<header class="rcf-cmd-table__head"><h3 id="rcf-readiness-blocking-heading">Blocking items</h3> ${headerCount}</header>
|
|
413
|
+
<p class="muted small">No failing items; every check passed on the current tree.</p>
|
|
414
|
+
<table class="rcf-cmd-table__table" aria-describedby="rcf-readiness-blocking-heading">
|
|
415
|
+
<caption class="rcf-sr-only">On engineers: no items</caption>
|
|
416
|
+
<thead>
|
|
417
|
+
<tr>
|
|
418
|
+
<th scope="col" data-rcf-col="stage">Stage</th>
|
|
419
|
+
<th scope="col" data-rcf-col="check">Check</th>
|
|
420
|
+
<th scope="col" data-rcf-col="docId">Item</th>
|
|
421
|
+
<th scope="col" data-rcf-col="why">Why it is failing</th>
|
|
422
|
+
<th scope="col" data-rcf-col="resolved">What resolves it</th>
|
|
423
|
+
<th scope="col" data-rcf-col="persona">On</th>
|
|
424
|
+
<th scope="col" data-rcf-col="trace"><span class="rcf-sr-only">Trace this id</span></th>
|
|
425
|
+
</tr>
|
|
426
|
+
</thead>
|
|
427
|
+
<tbody></tbody>
|
|
428
|
+
</table>
|
|
429
|
+
</section>`;
|
|
430
|
+
}
|
|
431
|
+
const stageOptions = Array.from(new Set(rows.map((r) => r.stage).filter(Boolean))).sort();
|
|
432
|
+
const personaOptions = Array.from(new Set(rows.map((r) => r.persona).filter(Boolean))).sort();
|
|
433
|
+
const stageSel = filter.stage && stageOptions.includes(filter.stage) ? filter.stage : '';
|
|
434
|
+
const personaSel = filter.persona && personaOptions.includes(filter.persona) ? filter.persona : '';
|
|
435
|
+
const body = rows.map((r) => renderBlockingRow(r, { stageSel, personaSel })).join('');
|
|
436
|
+
return `<section class="rcf-cmd-table rcf-cmd-table--blocking" data-rcf-table="blocking" aria-labelledby="rcf-readiness-blocking-heading">
|
|
437
|
+
<header class="rcf-cmd-table__head"><h3 id="rcf-readiness-blocking-heading">Blocking items</h3> ${headerCount}</header>
|
|
438
|
+
<div class="rcf-cmd-filterbar" data-rcf-filterbar="readiness-blocking" role="search">
|
|
439
|
+
<label class="rcf-cmd-filterbar__field">
|
|
440
|
+
<span>Stage</span>
|
|
441
|
+
<select data-filter-key="stage">
|
|
442
|
+
<option value="">All</option>
|
|
443
|
+
${stageOptions.map((s) => `<option value="${escapeHtml(s)}"${s === stageSel ? ' selected' : ''}>${escapeHtml(stageLabel(s))} (${escapeHtml(s)})</option>`).join('')}
|
|
444
|
+
</select>
|
|
445
|
+
</label>
|
|
446
|
+
<label class="rcf-cmd-filterbar__field">
|
|
447
|
+
<span>Persona</span>
|
|
448
|
+
<select data-filter-key="persona">
|
|
449
|
+
<option value="">All</option>
|
|
450
|
+
${personaOptions.map((p) => `<option value="${escapeHtml(p)}"${p === personaSel ? ' selected' : ''}>${escapeHtml(p === 'productOwner' ? 'Product owner' : 'Engineer')}</option>`).join('')}
|
|
451
|
+
</select>
|
|
452
|
+
</label>
|
|
453
|
+
</div>
|
|
454
|
+
<table class="rcf-cmd-table__table" aria-describedby="rcf-readiness-blocking-heading">
|
|
455
|
+
<thead>
|
|
456
|
+
<tr>
|
|
457
|
+
<th scope="col" data-rcf-col="stage">Stage</th>
|
|
458
|
+
<th scope="col" data-rcf-col="check">Check</th>
|
|
459
|
+
<th scope="col" data-rcf-col="docId">Item</th>
|
|
460
|
+
<th scope="col" data-rcf-col="why">Why it is failing</th>
|
|
461
|
+
<th scope="col" data-rcf-col="resolved">What resolves it</th>
|
|
462
|
+
<th scope="col" data-rcf-col="persona">On</th>
|
|
463
|
+
<th scope="col" data-rcf-col="trace"><span class="rcf-sr-only">Trace this id</span></th>
|
|
464
|
+
</tr>
|
|
465
|
+
</thead>
|
|
466
|
+
<tbody>${body}</tbody>
|
|
467
|
+
</table>
|
|
468
|
+
</section>`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function renderBlockingRow(r, { stageSel, personaSel }) {
|
|
472
|
+
const hidden = (stageSel && stageSel !== r.stage) || (personaSel && personaSel !== r.persona);
|
|
473
|
+
const hiddenAttr = hidden ? ' hidden' : '';
|
|
474
|
+
const title = r.question ? ` title="${escapeHtml(r.question)}"` : '';
|
|
475
|
+
// Dave ruling 2026-10-08 (AC-18003-7 amended): no shell command text
|
|
476
|
+
// visible; chain ids render as a prefix link with a plain-English
|
|
477
|
+
// label (the row's check/why/resolved cells carry that label),
|
|
478
|
+
// and the full composite id stays in the DOM in a data attribute
|
|
479
|
+
// (data-rcf-raw-id below). Fragments like "rcf define freeze" or
|
|
480
|
+
// "surface" that live after the first colon are NOT rendered as
|
|
481
|
+
// visible text. A non-document prefix (profile, ADR, D1..D8) renders
|
|
482
|
+
// as a plain <span> rather than a dead anchor.
|
|
483
|
+
const docId = r.docId || r.rawId;
|
|
484
|
+
const link = isResolvableDocId(docId)
|
|
485
|
+
? `<a href="${escapeHtml(r.href)}" data-rcf-prefix-link="yes">${escapeHtml(docId)}</a>`
|
|
486
|
+
: `<span class="rcf-cmd-table__prefix" data-rcf-prefix-text="yes">${escapeHtml(docId)}</span>`;
|
|
487
|
+
const personaWord = r.persona === 'productOwner' ? 'On me' : 'On engineers';
|
|
488
|
+
const stageAttr = r.stage ? ` data-rcf-stage="${escapeHtml(r.stage)}"` : '';
|
|
489
|
+
const personaAttr = r.persona ? ` data-rcf-persona="${escapeHtml(r.persona)}"` : '';
|
|
490
|
+
const stageText = r.stage ? `${stageLabel(r.stage)} (${r.stage})` : '';
|
|
491
|
+
return `<tr${stageAttr}${personaAttr} data-rcf-raw-id="${escapeHtml(r.rawId)}" data-rcf-doc-id="${escapeHtml(r.docId)}" data-rcf-fragment="${escapeHtml(r.fragment)}"${hiddenAttr}${title}>
|
|
492
|
+
<td data-rcf-col="stage">${escapeHtml(stageText)}</td>
|
|
493
|
+
<td data-rcf-col="check">${escapeHtml(r.check)}</td>
|
|
494
|
+
<td data-rcf-col="docId">${link}</td>
|
|
495
|
+
<td data-rcf-col="why">${escapeHtml(r.why)}</td>
|
|
496
|
+
<td data-rcf-col="resolved">${escapeHtml(r.resolved)}</td>
|
|
497
|
+
<td data-rcf-col="persona">${escapeHtml(personaWord)}</td>
|
|
498
|
+
<td data-rcf-col="trace">${traceActionFor(r.docId || r.rawId)}</td>
|
|
499
|
+
</tr>`;
|
|
500
|
+
}
|