create-cmp-cli 0.24.0 → 0.25.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/bin/create-cmp.mjs +33 -3
- package/package.json +10 -4
- package/packages/harness/package.json +18 -8
- package/packages/harness/src/approve.mjs +10 -11
- package/packages/harness/src/console/console-data.mjs +117 -0
- package/packages/harness/src/console/console-evidence.mjs +122 -0
- package/packages/harness/src/console/console-overview.mjs +642 -0
- package/packages/harness/src/console/console-shell.mjs +1139 -0
- package/packages/harness/src/console/console-tabs.mjs +2828 -0
- package/packages/harness/src/console/contrast.mjs +74 -0
- package/packages/harness/src/console/preview-service.mjs +1384 -0
- package/packages/harness/src/framework-check.mjs +271 -37
- package/packages/harness/src/lib/a11y.mjs +1 -1
- package/packages/harness/src/lib/affected-tests.mjs +99 -44
- package/packages/harness/src/lib/approvals.mjs +177 -350
- package/packages/harness/src/lib/audit-cadence.mjs +1 -1
- package/packages/harness/src/lib/comments.mjs +18 -4
- package/packages/harness/src/lib/determinism.mjs +275 -14
- package/packages/harness/src/lib/evidence-badge.mjs +34 -2
- package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
- package/packages/harness/src/lib/evidence-level.mjs +144 -59
- package/packages/harness/src/lib/feature-brief.mjs +53 -16
- package/packages/harness/src/lib/flight-recorder.mjs +72 -8
- package/packages/harness/src/lib/framework-check.mjs +434 -53
- package/packages/harness/src/lib/harness-lock.mjs +87 -19
- package/packages/harness/src/lib/harness-manifest.mjs +208 -0
- package/packages/harness/src/lib/harness-region.mjs +106 -2
- package/packages/harness/src/lib/harness-source.mjs +142 -0
- package/packages/harness/src/lib/inputs-hash.mjs +175 -22
- package/packages/harness/src/lib/lane-markers.mjs +78 -0
- package/packages/harness/src/lib/lane-narrator.mjs +4 -2
- package/packages/harness/src/lib/lane-runner.mjs +72 -6
- package/packages/harness/src/lib/plan.mjs +44 -11
- package/packages/harness/src/lib/plant-calibration.mjs +135 -0
- package/packages/harness/src/lib/profile-loader.mjs +323 -0
- package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
- package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
- package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
- package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
- package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
- package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
- package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
- package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
- package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +229 -27
- package/packages/harness/src/lib/receipt-validate.mjs +84 -16
- package/packages/harness/src/lib/spec-coverage.mjs +179 -92
- package/packages/harness/src/lib/spec-model.mjs +262 -0
- package/packages/harness/src/lib/step-cache.mjs +11 -2
- package/packages/harness/src/lib/step-outcomes.mjs +84 -151
- package/packages/harness/src/lib/walk.mjs +1 -1
- package/packages/harness/src/preview-gallery.mjs +1 -1
- package/packages/harness/src/receipt-check.mjs +80 -24
- package/packages/harness/src/record-audit.mjs +1 -1
- package/packages/harness/src/scaffold-feature.mjs +10 -2
- package/packages/harness/src/verify.mjs +222 -142
- package/packages/harness/src/watch.mjs +146 -33
- package/packages/receipts/package.json +1 -1
- package/packages/receipts/src/index.mjs +2 -2
- package/packages/receipts/src/inputs-hash.mjs +175 -22
- package/packages/receipts/src/receipt-validate.mjs +84 -16
- package/src/commands/attach.mjs +128 -4
- package/src/commands/harden.mjs +7 -1
- package/src/commands/upgrade.mjs +7 -0
- package/src/lib/minimal.mjs +23 -1
- package/src/scaffold.mjs +8 -0
- package/template/.claude/settings.json +2 -2
- package/template/.claude/skills/add-feature/SKILL.md +7 -3
- package/template/.claude/skills/add-repository/SKILL.md +2 -2
- package/template/.claude/skills/add-screen/SKILL.md +8 -4
- package/template/.githooks/pre-push +9 -0
- package/template/CLAUDE.md +55 -186
- package/template/docs/TESTING.md +1 -1
- package/template/gitignore +3 -0
- package/template/qa/approve.mjs +10 -11
- package/template/qa/comments.json +1 -1
- package/template/qa/evidence/schema.json +258 -34
- package/template/qa/framework-check.mjs +271 -37
- package/template/qa/harness-manifest.json +10 -0
- package/template/qa/lib/a11y.mjs +1 -1
- package/template/qa/lib/affected-tests.mjs +99 -44
- package/template/qa/lib/approvals.mjs +177 -350
- package/template/qa/lib/audit-cadence.mjs +1 -1
- package/template/qa/lib/comments.mjs +18 -4
- package/template/qa/lib/determinism.mjs +275 -14
- package/template/qa/lib/evidence-badge.mjs +34 -2
- package/template/qa/lib/evidence-ladder.mjs +202 -0
- package/template/qa/lib/evidence-level.mjs +144 -59
- package/template/qa/lib/feature-brief.mjs +53 -16
- package/template/qa/lib/flight-recorder.mjs +72 -8
- package/template/qa/lib/framework-check.mjs +434 -53
- package/template/qa/lib/harness-lock.mjs +87 -19
- package/template/qa/lib/harness-manifest.mjs +208 -0
- package/template/qa/lib/harness-region.mjs +106 -2
- package/template/qa/lib/harness-source.mjs +142 -0
- package/template/qa/lib/inputs-hash.mjs +175 -22
- package/template/qa/lib/lane-markers.mjs +78 -0
- package/template/qa/lib/lane-narrator.mjs +4 -2
- package/template/qa/lib/lane-runner.mjs +72 -6
- package/template/qa/lib/plan.mjs +44 -11
- package/template/qa/lib/plant-calibration.mjs +135 -0
- package/template/qa/lib/profile-loader.mjs +323 -0
- package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
- package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
- package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
- package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
- package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/template/qa/lib/profiles/cmp/index.mjs +47 -0
- package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
- package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
- package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
- package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +229 -27
- package/template/qa/lib/receipt-validate.mjs +84 -16
- package/template/qa/lib/spec-coverage.mjs +179 -92
- package/template/qa/lib/spec-model.mjs +262 -0
- package/template/qa/lib/step-cache.mjs +11 -2
- package/template/qa/lib/step-outcomes.mjs +84 -151
- package/template/qa/lib/walk.mjs +1 -1
- package/template/qa/preview-gallery.mjs +1 -1
- package/template/qa/receipt-check.mjs +80 -24
- package/template/qa/record-audit.mjs +1 -1
- package/template/qa/scaffold-feature.mjs +10 -2
- package/template/qa/verified-surface.json +17 -0
- package/template/qa/verify.mjs +222 -142
- package/template/qa/watch.mjs +146 -33
- /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
- /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
// MOVED from inspector/mcp/src/lib/console-overview.mjs — NORTH-STAR §9, stage
|
|
2
|
+
// 0.5, "the console into the harness". Unchanged in the move; like
|
|
3
|
+
// console-shell.mjs it had no imports, so it crossed the package boundary
|
|
4
|
+
// without leaving an edge behind. See console-shell.mjs's note for why the
|
|
5
|
+
// destination is `src/console/` rather than `src/lib/`.
|
|
6
|
+
//
|
|
7
|
+
// console-overview.mjs — the front door (STUDIO-REDESIGN.md §3.7).
|
|
8
|
+
//
|
|
9
|
+
// WHY THIS SECTION EXISTS, and what it supersedes. §2 of the redesign made the
|
|
10
|
+
// dashboard function "ambient — never a separate tab": every fact renders in
|
|
11
|
+
// the section that owns it, and the rail carries the roll-up. That rule was
|
|
12
|
+
// right about ownership and wrong about ENTRY. It was patched once already —
|
|
13
|
+
// the 07-28 audit added the governance strip to answer "I should be able to see
|
|
14
|
+
// the status at all times" — and the second report of the same failure (2026-08-22,
|
|
15
|
+
// Karel: "not easy to see and use ... there is so much happening all over the
|
|
16
|
+
// place") is the evidence that a rail widget cannot carry it. Nine peer sections
|
|
17
|
+
// with no hierarchy of attention above them is not a navigation problem; it is a
|
|
18
|
+
// missing page.
|
|
19
|
+
//
|
|
20
|
+
// So the ambient rule is superseded HERE and ONLY here: exactly one section may
|
|
21
|
+
// aggregate, it is the first one, and it OWNS NO FACTS. Every rule that made the
|
|
22
|
+
// ambient design right still binds:
|
|
23
|
+
//
|
|
24
|
+
// - Composition only. This module derives nothing. It arranges what
|
|
25
|
+
// deriveHumanQueue, getApprovalAnchoredDiff, getDigestData and the receipt
|
|
26
|
+
// bridge already returned. If a number here could ever disagree with the
|
|
27
|
+
// section that owns it, this file is wrong.
|
|
28
|
+
// - Sign where you read, AMENDED 2026-08-24 (Karel: "in the overview give the
|
|
29
|
+
// option to approve as well"). Each row now carries its own signature
|
|
30
|
+
// control, because the rule's purpose — never sign what you have not read —
|
|
31
|
+
// is served by the evidence already on the row (the drift file split, the
|
|
32
|
+
// promise tally and receipt) plus "read it first" one click away, not by
|
|
33
|
+
// forcing a round trip for a decision already made. The control is NOT a
|
|
34
|
+
// second mechanism: it emits the exact markup the existing wiring speaks
|
|
35
|
+
// (.approve-btn / .feature-accept-btn), so there is still one approve path,
|
|
36
|
+
// one accept path, and refusals stay the server's.
|
|
37
|
+
// - Evidence-or-silence. An unavailable ledger says so in its own words. No
|
|
38
|
+
// block invents an empty state that reads like a clean bill of health.
|
|
39
|
+
//
|
|
40
|
+
// The page answers the returning owner's three questions, in the order they are
|
|
41
|
+
// actually asked: what needs me (queue) · what changed (digest) · is it still
|
|
42
|
+
// proven (the standing line in the header).
|
|
43
|
+
//
|
|
44
|
+
// The one import, added 2026-09-08, and it does not breach "composition only":
|
|
45
|
+
// two surfaces here show an evidence rung — the standing line and the
|
|
46
|
+
// recent-requests rows — and NORTH-STAR.md §6.5 requires both to show the pack
|
|
47
|
+
// that graded it. console-evidence.mjs is where the console keeps that one
|
|
48
|
+
// spelling, so importing it is the same rule as taking `receiptGlyph` from the
|
|
49
|
+
// caller: this file arranges a derivation it does not own. The module is a
|
|
50
|
+
// sibling inside `src/console/`, so the package boundary this file crossed
|
|
51
|
+
// cleanly is untouched.
|
|
52
|
+
|
|
53
|
+
import { rungWithPack, rungPackNote } from "./console-evidence.mjs";
|
|
54
|
+
|
|
55
|
+
const esc = (s) =>
|
|
56
|
+
String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
57
|
+
const escAttr = (s) => esc(s).replace(/"/g, """);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The section's status line — the "is it still proven" answer, in one line:
|
|
61
|
+
* the lane's own glyph + verdict + rung, then the signing tally. Both halves
|
|
62
|
+
* are read from data the rail already shows, so the two can never disagree.
|
|
63
|
+
*
|
|
64
|
+
* @param {{receipt?: object|null, statuses?: object[], receiptGlyph: Function,
|
|
65
|
+
* formatAge?: Function}} p `receiptGlyph` is injected (console-shell owns
|
|
66
|
+
* that derivation — importing it here would fork nothing, but passing it
|
|
67
|
+
* keeps the ONE-derivation rule visible at the call site).
|
|
68
|
+
*/
|
|
69
|
+
export function overviewStatusHtml({ receipt, statuses = [], receiptGlyph, formatAge } = {}) {
|
|
70
|
+
const g = receiptGlyph ? receiptGlyph(receipt) : null;
|
|
71
|
+
const lane = g
|
|
72
|
+
? `<span class="glyph ${g.cls}" title="${escAttr(g.label)}">${g.ch}</span> ${esc(g.label)}`
|
|
73
|
+
: "";
|
|
74
|
+
const age =
|
|
75
|
+
receipt && receipt.available && typeof receipt.ageMs === "number" && formatAge
|
|
76
|
+
? ` · ${esc(formatAge(receipt.ageMs))}`
|
|
77
|
+
: "";
|
|
78
|
+
// The rung carries the pack that graded it (§6.5), because this is the line a
|
|
79
|
+
// returning owner reads to answer "is it still proven" and a bare L2 does not
|
|
80
|
+
// answer it: §8.9 makes one pack's L2 and another's different claims, so the
|
|
81
|
+
// grade alone tells the reader which of two unlike things they are looking at
|
|
82
|
+
// only if they already knew. The badge holds both or neither.
|
|
83
|
+
const label =
|
|
84
|
+
receipt && receipt.available ? rungWithPack(receipt.evidenceLevel, receipt.packId ?? receipt.pack) : null;
|
|
85
|
+
const rung = label
|
|
86
|
+
? ` · <span class="badge evidence-rung" title="${escAttr(rungPackNote(receipt.evidenceLevel, receipt.packId ?? receipt.pack))}">${esc(label)}</span>`
|
|
87
|
+
: "";
|
|
88
|
+
// The tally is silent on an ungoverned project rather than printing "0 of 0
|
|
89
|
+
// signed", which reads as a finding when it is an absence.
|
|
90
|
+
const tally = statuses.length
|
|
91
|
+
? ` · ${statuses.filter((s) => s.status === "approved").length} of ${statuses.length} signed`
|
|
92
|
+
: "";
|
|
93
|
+
return `${lane}${age}${rung}${tally}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A queue item's own signature control (Karel, 2026-08-24: "in the overview
|
|
98
|
+
* give the option to approve as well").
|
|
99
|
+
*
|
|
100
|
+
* This SUPERSEDES the front door's original "names the act and jumps, never
|
|
101
|
+
* signs" rule. What that rule was protecting — never sign what you have not
|
|
102
|
+
* read — is preserved by construction rather than by making you travel: every
|
|
103
|
+
* row already carries the evidence its signature is about (a drifted
|
|
104
|
+
* artifact's changed/still-signed file split, a proven feature's promise tally
|
|
105
|
+
* and receipt), and "read it in full" stays one click away as the secondary
|
|
106
|
+
* action. What the rule was costing was a mandatory round trip for the
|
|
107
|
+
* decision you had already made.
|
|
108
|
+
*
|
|
109
|
+
* It is NOT a second mechanism: the markup is exactly the contract the
|
|
110
|
+
* existing wiring speaks (.approve-btn[data-artifact] -> POST /api/approve,
|
|
111
|
+
* .feature-accept-btn[data-name] -> POST /api/feature/accept), the panel is
|
|
112
|
+
* already in GOVERNED_PANELS so the SSE swap re-wires these buttons like any
|
|
113
|
+
* other, and refusals stay the server's to make.
|
|
114
|
+
*/
|
|
115
|
+
function itemActionHtml(item, { byArtifact, byFeature }) {
|
|
116
|
+
const record = byArtifact.get(item.artifact) || null;
|
|
117
|
+
const featureName = item.artifact.startsWith("feature-brief:") ? item.artifact.slice("feature-brief:".length) : null;
|
|
118
|
+
const feature = featureName ? byFeature.get(featureName) : null;
|
|
119
|
+
|
|
120
|
+
// Acceptance is a different verb on a different endpoint: the brief is
|
|
121
|
+
// already signed; what is being said is "the proven thing is what I wanted".
|
|
122
|
+
if (feature && feature.provenDone && record && record.status === "approved") {
|
|
123
|
+
return `<button type="button" class="feature-accept-btn fd-sign" data-name="${escAttr(featureName)}">Accept</button>`;
|
|
124
|
+
}
|
|
125
|
+
// A refused artifact never gets a button that could only fail on click —
|
|
126
|
+
// the same honesty signatureBarHtml applies at its own bar.
|
|
127
|
+
if (record && record.resolvable === false) return "";
|
|
128
|
+
const label = record && record.status === "unreviewed" ? "Approve" : "Re-approve";
|
|
129
|
+
return `<button type="button" class="approve-btn fd-sign" data-artifact="${escAttr(item.artifact)}">${label}</button>`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A queue item's supporting evidence — enough to judge urgency WITHOUT opening
|
|
134
|
+
* the owning section, and never more. A drifted artifact shows the file split
|
|
135
|
+
* (the changed/still-signed counts approval-diff already computed) and names
|
|
136
|
+
* the changed files; the DIFF ITSELF stays in the section that owns it. A
|
|
137
|
+
* proven feature shows the derivation that made it proven.
|
|
138
|
+
*/
|
|
139
|
+
function itemEvidenceHtml(item, { byArtifact, byFeature, anchoredDiffs }) {
|
|
140
|
+
const record = byArtifact.get(item.artifact) || null;
|
|
141
|
+
const anchored = anchoredDiffs ? anchoredDiffs[item.artifact] : null;
|
|
142
|
+
|
|
143
|
+
if (record && (record.status === "changed-since-approval" || record.status === "reopened")) {
|
|
144
|
+
if (!anchored || !anchored.available) {
|
|
145
|
+
// The honest failure: approval-diff could not locate the signed bytes.
|
|
146
|
+
// Saying so is the point — a guessed "roughly then" diff is the one
|
|
147
|
+
// output that would make this page untrustworthy.
|
|
148
|
+
const why = anchored && anchored.reason ? anchored.reason : "the signed bytes were not located in recent history";
|
|
149
|
+
return ` <p class="fd-evidence fd-evidence-absent">what changed vs. the signed bytes is not derivable — ${esc(why)}</p>`;
|
|
150
|
+
}
|
|
151
|
+
const files = anchored.files || { changed: [], unchanged: [] };
|
|
152
|
+
const changed = files.changed || [];
|
|
153
|
+
const unchanged = files.unchanged || [];
|
|
154
|
+
const total = changed.length + unchanged.length;
|
|
155
|
+
if (changed.length === 0) {
|
|
156
|
+
return ` <p class="fd-evidence">no file in this artifact differs from the signed bytes — the hash moved for another reason; open the section for the anchored diff</p>`;
|
|
157
|
+
}
|
|
158
|
+
const list = changed
|
|
159
|
+
.slice(0, 8)
|
|
160
|
+
.map((f) => `<li><span class="fd-status">${esc(f.status)}</span> <code>${esc(f.path)}</code></li>`)
|
|
161
|
+
.join("");
|
|
162
|
+
const more = changed.length > 8 ? `<li class="fd-more">… and ${changed.length - 8} more</li>` : "";
|
|
163
|
+
const still =
|
|
164
|
+
unchanged.length > 0
|
|
165
|
+
? ` · ${unchanged.length} of ${total} still exactly as signed`
|
|
166
|
+
: "";
|
|
167
|
+
return ` <details class="fd-evidence">
|
|
168
|
+
<summary>${changed.length} file${changed.length === 1 ? "" : "s"} changed since you signed${still}</summary>
|
|
169
|
+
<ul class="fd-files">${list}${more}</ul>
|
|
170
|
+
</details>`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A proven feature awaiting acceptance: show WHY it is proven — the same
|
|
174
|
+
// derivation the Features card states, never a re-computation of it.
|
|
175
|
+
const featureName = item.artifact.startsWith("feature-brief:") ? item.artifact.slice("feature-brief:".length) : null;
|
|
176
|
+
const feature = featureName ? byFeature.get(featureName) : null;
|
|
177
|
+
if (feature && feature.provenDone) {
|
|
178
|
+
const clauses =
|
|
179
|
+
typeof feature.covered === "number" && typeof feature.total === "number" && feature.total > 0
|
|
180
|
+
? `${feature.covered} of ${feature.total} clauses cited`
|
|
181
|
+
: null;
|
|
182
|
+
const verdict = feature.receipt && feature.receipt.present ? `receipt ${feature.receipt.verdict}` : null;
|
|
183
|
+
const attests = feature.receipt && feature.receipt.attestsTree ? "attesting this tree" : null;
|
|
184
|
+
const bits = [clauses, verdict, attests].filter(Boolean).join(" · ");
|
|
185
|
+
return bits ? ` <p class="fd-evidence">${bits}</p>` : "";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (record && record.status === "unreviewed") {
|
|
189
|
+
return ` <p class="fd-evidence">never signed — this artifact has had no human judgment yet</p>`;
|
|
190
|
+
}
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The front door body.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} p
|
|
198
|
+
* @param {Array<{artifact: string, tab: string, label: string}>} p.queue deriveHumanQueue()
|
|
199
|
+
* @param {object[]} [p.statuses] approvals statuses (for the ungoverned empty state)
|
|
200
|
+
* @param {object[]} [p.features] the feature board's features
|
|
201
|
+
* @param {object} [p.anchoredDiffs] artifact id -> getApprovalAnchoredDiff() result
|
|
202
|
+
* @param {string} [p.digestHtml] digestTabHtml(digest) — passed in, not imported,
|
|
203
|
+
* so this module stays acyclic and the digest keeps ONE renderer
|
|
204
|
+
* @param {string} [p.digestSince] the digest window, for the block's own subtitle
|
|
205
|
+
* @param {Function} [p.statusGlyph] console-shell's statusGlyph (injected, one derivation)
|
|
206
|
+
* @param {object[]} [p.journal] the signing journal's events — History, moved
|
|
207
|
+
* here from the rail strip so the strip could stop duplicating this page
|
|
208
|
+
* @param {Function} [p.formatAge] console-shell's formatAgeCoarse (injected)
|
|
209
|
+
*/
|
|
210
|
+
export function overviewBodyHtml({
|
|
211
|
+
queue = [],
|
|
212
|
+
statuses = [],
|
|
213
|
+
features = [],
|
|
214
|
+
anchoredDiffs = {},
|
|
215
|
+
digestHtml = "",
|
|
216
|
+
digestSince = null,
|
|
217
|
+
statusGlyph,
|
|
218
|
+
journal = [],
|
|
219
|
+
formatAge,
|
|
220
|
+
walks = null,
|
|
221
|
+
} = {}) {
|
|
222
|
+
const byArtifact = new Map(statuses.map((s) => [s.id, s]));
|
|
223
|
+
const byFeature = new Map(features.map((f) => [f.name, f]));
|
|
224
|
+
|
|
225
|
+
let queueHtml;
|
|
226
|
+
if (statuses.length === 0) {
|
|
227
|
+
// Not "nothing waits on you" — that is a claim. There is no ledger to read.
|
|
228
|
+
queueHtml = ` <p class="empty-inline">no approvals ledger in this project — nothing here is governed yet, so no signature can be waiting. A project gains a ledger at <code>qa/approvals.json</code>.</p>`;
|
|
229
|
+
} else if (queue.length === 0) {
|
|
230
|
+
queueHtml = ` <p class="fd-clear"><span class="glyph glyph-signed">●</span> Nothing waits on you. Every governed artifact is signed and unchanged since.</p>`;
|
|
231
|
+
} else {
|
|
232
|
+
queueHtml = ` <div class="banner sig-error" id="overview-error" hidden></div>
|
|
233
|
+
<ol class="fd-queue">
|
|
234
|
+
${queue
|
|
235
|
+
.map((item) => {
|
|
236
|
+
const record = byArtifact.get(item.artifact) || null;
|
|
237
|
+
// The glyph states the ACT, not the artifact's current status. An
|
|
238
|
+
// acceptance row's artifact is `approved` — its brief was signed long ago —
|
|
239
|
+
// and statusGlyph would correctly render that as a green ●, which on a
|
|
240
|
+
// queue of things waiting on you reads as "done". `glyph-attn` is the rail's
|
|
241
|
+
// existing vocabulary for exactly this state (accent ●: needs your
|
|
242
|
+
// acceptance), so the two surfaces stay in one language.
|
|
243
|
+
const g =
|
|
244
|
+
record && record.status === "approved"
|
|
245
|
+
? { ch: "●", cls: "glyph-attn", label: "proven — awaiting your acceptance" }
|
|
246
|
+
: statusGlyph
|
|
247
|
+
? statusGlyph(record)
|
|
248
|
+
: null;
|
|
249
|
+
const glyph = g
|
|
250
|
+
? `<span class="glyph ${g.cls}" title="${escAttr(g.label)}">${g.ch}</span>`
|
|
251
|
+
: `<span class="glyph glyph-unsigned">○</span>`;
|
|
252
|
+
// data-go-* is the strip's jump contract, delegated at document level so
|
|
253
|
+
// this page reuses the SAME navigation the governance strip uses — one
|
|
254
|
+
// handler, one behavior, no second mechanism.
|
|
255
|
+
return ` <li class="fd-item">
|
|
256
|
+
<p class="fd-act">${glyph} <span class="fd-label">${esc(item.label)}</span>
|
|
257
|
+
<span class="fd-actions">${itemActionHtml(item, { byArtifact, byFeature })}<button type="button" class="gov-jump fd-go" data-go-tab="${escAttr(item.tab)}" data-go-artifact="${escAttr(item.artifact)}" title="open the artifact and read it in full">read it first</button></span></p>
|
|
258
|
+
${itemEvidenceHtml(item, { byArtifact, byFeature, anchoredDiffs })}
|
|
259
|
+
</li>`;
|
|
260
|
+
})
|
|
261
|
+
.join("\n")}
|
|
262
|
+
</ol>`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// History, moved off the rail strip (2026-08-22) so the strip could stop being
|
|
266
|
+
// a lower-fidelity copy of this page. The journal is the SIGNING record —
|
|
267
|
+
// verb, artifact, who, and a reopen's reason — which is a different ledger
|
|
268
|
+
// from the digest's git-derived events, so it sits alongside them, not
|
|
269
|
+
// instead of them. Newest first, capped: this is a glance; `--log` is the
|
|
270
|
+
// full record.
|
|
271
|
+
const recent = [...journal].slice(-8).reverse();
|
|
272
|
+
const historyHtml =
|
|
273
|
+
recent.length === 0
|
|
274
|
+
? ""
|
|
275
|
+
: ` <h3 class="fd-h">History</h3>
|
|
276
|
+
<div class="fd-history">
|
|
277
|
+
${recent
|
|
278
|
+
.map((e) => {
|
|
279
|
+
const glyph = e.verb === "approve" ? "●" : e.verb === "reopen" ? "◐" : e.verb === "accept" ? "◆" : "·";
|
|
280
|
+
const ageMs = e.at ? Date.now() - Date.parse(e.at) : NaN;
|
|
281
|
+
const age = formatAge && !Number.isNaN(ageMs) ? formatAge(ageMs) : "";
|
|
282
|
+
const who = e.via ? ` via ${e.via}` : "";
|
|
283
|
+
return ` <p class="gov-event" title="${escAttr(e.at || "")}"><span class="gov-event-glyph">${glyph}</span> ${esc(e.verb)} ${esc(e.artifact || "")}${esc(who)}${age ? ` · ${esc(age)}` : ""}${e.reason ? `<span class="gov-event-reason">${esc(e.reason)}</span>` : ""}</p>`;
|
|
284
|
+
})
|
|
285
|
+
.join("\n")}
|
|
286
|
+
</div>`;
|
|
287
|
+
|
|
288
|
+
const since = digestSince ? ` <span class="fd-since">window: since ${esc(digestSince)}</span>` : "";
|
|
289
|
+
const changedBlock = digestHtml
|
|
290
|
+
? ` <h3 class="fd-h">What changed${since}</h3>
|
|
291
|
+
<div class="fd-digest">
|
|
292
|
+
${digestHtml}
|
|
293
|
+
</div>`
|
|
294
|
+
: "";
|
|
295
|
+
|
|
296
|
+
// Page anatomy (studio-drive-mode): Drive leads with the live chain, then
|
|
297
|
+
// what-needs-you, then the walks; the digest and history are the page's own
|
|
298
|
+
// MIRROR tail — complete, derived, and collapsed by default.
|
|
299
|
+
const fold = (label, inner) =>
|
|
300
|
+
inner ? ` <details class="fd-fold"><summary>${label}</summary>\n${inner}\n </details>` : "";
|
|
301
|
+
|
|
302
|
+
return `${driveChainHtml(walks && walks.chain ? walks.chain : null)} <p class="meta">The three questions, in the order they get asked. Every line below is arranged
|
|
303
|
+
from the section that owns it — this page derives nothing of its own, and signing happens where you read.</p>
|
|
304
|
+
<h3 class="fd-h">What needs you${queue.length ? ` <span class="fd-count">${queue.length}</span>` : ""}</h3>
|
|
305
|
+
${queueHtml}
|
|
306
|
+
${walksHtml(features, statuses, walks)}
|
|
307
|
+
${fold("What changed", changedBlock)}
|
|
308
|
+
${fold("History", historyHtml)}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── The live chain (studio-drive-mode) ───────────────────────────────────────
|
|
312
|
+
//
|
|
313
|
+
// What the agent is doing in the CURRENT REQUEST: the request itself (tier 1,
|
|
314
|
+
// recorded mechanically from the human's own prompt), the declared step chain
|
|
315
|
+
// (tier 2 — agent-declared, so its AGE is always shown), and what is actually
|
|
316
|
+
// running right now (tier 3 — the lane/render markers, derived, overriding).
|
|
317
|
+
// The chain gates nothing; the walk below stays the truth for doneness.
|
|
318
|
+
|
|
319
|
+
/** "40s ago" / "12 min ago" — mirror of the harness's formatAge, display-only. */
|
|
320
|
+
function fmtChainAge(ms) {
|
|
321
|
+
if (!(ms >= 0)) return "age unknown";
|
|
322
|
+
if (ms < 90000) return `${Math.round(ms / 1000)}s ago`;
|
|
323
|
+
if (ms < 90 * 60000) return `${Math.round(ms / 60000)} min ago`;
|
|
324
|
+
return `${Math.round(ms / 3600000)}h ago`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** "12s" / "~3 min" — mirror of the harness's formatDuration, display-only. */
|
|
328
|
+
function fmtChainDur(ms) {
|
|
329
|
+
if (!(ms >= 0)) return "";
|
|
330
|
+
if (ms < 120000) return `${Math.max(1, Math.round(ms / 1000))}s`;
|
|
331
|
+
return `~${Math.round(ms / 60000)} min`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Provenance chips (drive-narration N3) — each chain tier labeled as what it
|
|
335
|
+
// is: the request `recorded` (mechanically, from the human's own prompt), the
|
|
336
|
+
// steps `declared` (by the agent, aged), the busy line `observed` (the lane's
|
|
337
|
+
// own marker). The one distinction no self-reported progress display makes.
|
|
338
|
+
const CH_PROV = {
|
|
339
|
+
recorded: `<span class="ch-prov ch-prov-obs" title="recorded mechanically from your own prompt — not the agent's words">recorded</span>`,
|
|
340
|
+
declared: `<span class="ch-prov ch-prov-dec" title="declared by the agent — the age says how fresh the claim is">declared</span>`,
|
|
341
|
+
observed: `<span class="ch-prov ch-prov-obs" title="observed from the lane's own marker — not an agent claim">observed</span>`,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The busy phrase: the harness's own pre-rendered narration when the project's
|
|
346
|
+
* plan.mjs provides it (busyText — one voice everywhere), else the legacy
|
|
347
|
+
* truthiness phrasing for older harnesses.
|
|
348
|
+
*/
|
|
349
|
+
function chainBusyPhrase(chain) {
|
|
350
|
+
if (typeof chain.busyText === "string") return chain.busyText;
|
|
351
|
+
if (chain.busy && chain.busy.lane) return "the full check is running NOW";
|
|
352
|
+
if (chain.busy && chain.busy.render) return "a preview render is in flight";
|
|
353
|
+
return "";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** One row per closed chain (N5): request → steps → outcome, newest first. */
|
|
357
|
+
function chainHistoryHtml(history) {
|
|
358
|
+
if (!Array.isArray(history) || history.length === 0) return "";
|
|
359
|
+
const rows = history
|
|
360
|
+
.map((h) => {
|
|
361
|
+
const label = h.title || h.request || "(untitled request)";
|
|
362
|
+
const ageMs = h.at ? Date.now() - Date.parse(h.at) : NaN;
|
|
363
|
+
// The trail's own glance (qa/lib/plan.mjs receiptGlance) stores the rung
|
|
364
|
+
// AND the pack it was graded under, and this row shows both for a reason
|
|
365
|
+
// the other rung surfaces do not have: these rows OUTLIVE the run that
|
|
366
|
+
// made them, so a bare grade here can never be attributed later by
|
|
367
|
+
// anything. A row reading "PASS · L2" is unreadable the moment a second
|
|
368
|
+
// pack exists in the repo's history (§8.9).
|
|
369
|
+
const rung = h.receipt ? rungWithPack(h.receipt.rung, h.receipt.pack) : null;
|
|
370
|
+
const outcome = h.receipt && h.receipt.verdict
|
|
371
|
+
? `<span class="ch-hist-outcome ${h.receipt.verdict === "PASS" ? "ok" : "bad"}"${rung ? ` title="${escAttr(rungPackNote(h.receipt.rung, h.receipt.pack))}"` : ""}>${esc(h.receipt.verdict)}${rung ? ` · ${esc(rung)}` : ""}</span>`
|
|
372
|
+
: `<span class="ch-hist-outcome">no receipt at close</span>`;
|
|
373
|
+
const dur = typeof h.durationMs === "number" && h.durationMs > 0 ? ` · ${esc(fmtChainDur(h.durationMs))}` : "";
|
|
374
|
+
const steps = Array.isArray(h.steps) && h.steps.length ? ` · ${h.steps.length} step${h.steps.length === 1 ? "" : "s"}` : "";
|
|
375
|
+
return ` <p class="ch-hist-row" title="${escAttr(Array.isArray(h.steps) ? h.steps.join(" → ") : "")}">${esc(label)}${steps}${dur} · ${outcome}${Number.isNaN(ageMs) ? "" : ` · ${esc(fmtChainAge(ageMs))}`}</p>`;
|
|
376
|
+
})
|
|
377
|
+
.join("\n");
|
|
378
|
+
return ` <details class="ch-hist"><summary>Recent requests <span class="fd-count">${history.length}</span></summary>\n${rows}\n </details>\n`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function driveChainHtml(chain) {
|
|
382
|
+
if (!chain || (!chain.plan && !chain.request)) return "";
|
|
383
|
+
const title = chain.plan && chain.plan.title ? chain.plan.title : chain.request ? chain.request.text : "";
|
|
384
|
+
const busyPhrase = chainBusyPhrase(chain);
|
|
385
|
+
const busy = busyPhrase !== "" ? `${CH_PROV.observed} <span class="ch-busy">${esc(busyPhrase)}</span>` : "";
|
|
386
|
+
let steps = "";
|
|
387
|
+
let meta = "";
|
|
388
|
+
if (chain.plan) {
|
|
389
|
+
const cur = chain.plan.current;
|
|
390
|
+
const nowMs = Date.now();
|
|
391
|
+
steps = ` <p class="ch-steps">${chain.plan.steps
|
|
392
|
+
.map((st) => {
|
|
393
|
+
const cls = st.done ? "ch-done" : st.n === cur ? "ch-cur" : "ch-todo";
|
|
394
|
+
const glyph = st.done ? "✓" : st.n === cur ? "◉" : "○";
|
|
395
|
+
// N1: a done step wears its wall time, the current one its elapsed —
|
|
396
|
+
// derived from the declaration's own write stamps, absent pre-N1.
|
|
397
|
+
let time = "";
|
|
398
|
+
if (st.done) {
|
|
399
|
+
const a = Date.parse(st.startedAt || "");
|
|
400
|
+
const b = Date.parse(st.doneAt || "");
|
|
401
|
+
if (!Number.isNaN(a) && !Number.isNaN(b)) time = ` <span class="ch-time">(${esc(fmtChainDur(Math.max(0, b - a)))})</span>`;
|
|
402
|
+
} else if (st.n === cur) {
|
|
403
|
+
const a = Date.parse(st.startedAt || "");
|
|
404
|
+
if (!Number.isNaN(a)) time = ` <span class="ch-time">${esc(fmtChainDur(Math.max(0, nowMs - a)))} in</span>`;
|
|
405
|
+
}
|
|
406
|
+
return `<span class="ch-step ${cls}"><span class="ch-glyph">${glyph}</span> ${st.n}. ${esc(st.label)}${time}</span>`;
|
|
407
|
+
})
|
|
408
|
+
.join('<span class="ch-arrow">→</span>')}</p>`;
|
|
409
|
+
const now = chain.plan.steps.find((st) => st.n === cur) ?? null;
|
|
410
|
+
meta = ` <p class="ch-meta">${now ? `now: step ${now.n} of ${chain.plan.steps.length} — ${esc(now.label)}` : "chain complete"}${busy ? ` · ${busy}` : ""} · ${CH_PROV.declared} <span class="ch-age">updated ${esc(fmtChainAge(chain.planAgeMs))}</span></p>`;
|
|
411
|
+
} else {
|
|
412
|
+
meta = ` <p class="ch-meta">no declared step chain for this request yet${busy ? ` · ${busy}` : ""}</p>`;
|
|
413
|
+
}
|
|
414
|
+
return ` <div class="ch-strip">
|
|
415
|
+
<p class="ch-request"><span class="lbl">Request</span> ${esc(title)} ${CH_PROV.recorded}</p>
|
|
416
|
+
${steps}${meta}${chainHistoryHtml(chain.history)} </div>
|
|
417
|
+
`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── In flight — the walks (docs/features/walk-status.md) ─────────────────────
|
|
421
|
+
//
|
|
422
|
+
// The six-stage projection of the feature board: Decide · Design · Contract ·
|
|
423
|
+
// Build · Prove · Sign-off, promises (clauses) as Build's inner progress.
|
|
424
|
+
// STEP_STAGE mirrors qa/lib/walk.mjs VERBATIM — the harness owns the mapping;
|
|
425
|
+
// this is its rendering. Stage states are positional around the board's own
|
|
426
|
+
// derived nextStep, exactly like walkOfFeature.
|
|
427
|
+
|
|
428
|
+
const WK_STAGES = [
|
|
429
|
+
["decide", "Decide"], ["design", "Design"], ["contract", "Contract"],
|
|
430
|
+
["build", "Build"], ["prove", "Prove"], ["signoff", "Sign-off"],
|
|
431
|
+
];
|
|
432
|
+
const WK_STEP_STAGE = {
|
|
433
|
+
"sign-brief": "decide", "re-approve": "decide",
|
|
434
|
+
design: "design", audit: "design", "sign-design": "design",
|
|
435
|
+
contract: "contract", "sign-spec": "contract",
|
|
436
|
+
build: "build", redesign: "build", prove: "prove", accept: "signoff",
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The In-flight block: one card per OPEN walk (most-recent board order), plus
|
|
441
|
+
* arrivals — REOPENED artifacts no open walk accounts for (a harness wave's
|
|
442
|
+
* rule-change reopens). Drifted artifacts are deliberately NOT repeated here:
|
|
443
|
+
* they are already loud in the What-needs-you queue above; one fact, one place.
|
|
444
|
+
* Empty walks + no arrivals -> "" (silence, never an empty frame).
|
|
445
|
+
*
|
|
446
|
+
* Two renderings (walk-legibility L5). When the project's own walk derivation
|
|
447
|
+
* is available (`walksData` from getWalksData — qa/lib/walk.mjs), the card is
|
|
448
|
+
* the walk's PRIMARY human surface: the promise list itself with per-promise
|
|
449
|
+
* kept state, the stage gloss, Prove's measured lane cost, the signature
|
|
450
|
+
* button ON the your-turn row (the same .approve-btn/.feature-accept-btn wire
|
|
451
|
+
* every other control speaks — one approve path), and each arrival's
|
|
452
|
+
* now-or-after choice as two buttons that post a general comment for the
|
|
453
|
+
* agent to observe (the pick-btn precedent — no new decision machinery).
|
|
454
|
+
* Without it (pre-walk scaffolds), the original board-mirroring thumbnail.
|
|
455
|
+
*/
|
|
456
|
+
export function walksHtml(features = [], statuses = [], walksData = null) {
|
|
457
|
+
if (walksData && walksData.available) return walksRichHtml(walksData, statuses);
|
|
458
|
+
const open = features.filter((f) => f.phase !== "accepted");
|
|
459
|
+
const owned = new Set();
|
|
460
|
+
for (const f of open) {
|
|
461
|
+
owned.add(`feature-brief:${f.name}`);
|
|
462
|
+
owned.add(`feature-design:${f.name}`);
|
|
463
|
+
for (const n of f.specNames || [f.name]) owned.add(`feature-spec:${n}`);
|
|
464
|
+
for (const t of f.touches || []) owned.add(t.id);
|
|
465
|
+
}
|
|
466
|
+
const arrivals = statuses.filter((s) => s.status === "reopened" && !owned.has(s.id));
|
|
467
|
+
if (open.length === 0 && arrivals.length === 0) return "";
|
|
468
|
+
|
|
469
|
+
const cards = open.map((f) => {
|
|
470
|
+
const currentKey = WK_STEP_STAGE[f.nextStep && f.nextStep.key] ?? null;
|
|
471
|
+
const idx = currentKey ? WK_STAGES.findIndex(([k]) => k === currentKey) : WK_STAGES.length;
|
|
472
|
+
const dots = WK_STAGES.map(([key, label], i) => {
|
|
473
|
+
const state =
|
|
474
|
+
key === "design" && f.design === null ? "skip" : i < idx ? "done" : i === idx ? "cur" : "todo";
|
|
475
|
+
const title = state === "skip" ? `${label} — no UI surface` : label;
|
|
476
|
+
return `<span class="wk-stage wk-${state}" title="${escAttr(title)}"><span class="wk-dot"></span>${esc(label)}</span>`;
|
|
477
|
+
}).join("");
|
|
478
|
+
const promises =
|
|
479
|
+
typeof f.total === "number" && f.total > 0
|
|
480
|
+
? `<span class="wk-promises">${f.covered} of ${f.total} promises kept</span>`
|
|
481
|
+
: "";
|
|
482
|
+
const owner = f.nextStep ? f.nextStep.label : "";
|
|
483
|
+
const you =
|
|
484
|
+
f.nextStep && f.nextStep.owner === "human"
|
|
485
|
+
? `<span class="wk-turn">YOUR TURN</span> ${esc(owner)}`
|
|
486
|
+
: `<span class="wk-agent">agent</span> ${esc(owner)}`;
|
|
487
|
+
return ` <div class="wk-card" data-walk="${escAttr(f.name)}">
|
|
488
|
+
<p class="wk-name">${esc(f.name)}${promises}</p>
|
|
489
|
+
<p class="wk-stages">${dots}</p>
|
|
490
|
+
<p class="wk-you">${you}</p>
|
|
491
|
+
</div>`;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const arrived = arrivals.map(
|
|
495
|
+
(a) =>
|
|
496
|
+
` <p class="wk-arrival">▲ ARRIVED, UNPLANNED — ${esc(a.label || a.id)} · reopened outside every open walk — now, or after the current walk lands?</p>`,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
return ` <h3 class="fd-h">In flight${open.length ? ` <span class="fd-count">${open.length}</span>` : ""}</h3>
|
|
500
|
+
${cards.join("\n")}
|
|
501
|
+
${arrived.join("\n")}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The rich In-flight rendering — the walk derivation's own objects
|
|
506
|
+
* (qa/lib/walk.mjs via getWalksData), arranged. Derives nothing: stage
|
|
507
|
+
* states, promise kept-ness, the measured lane cost and whose-turn all
|
|
508
|
+
* arrive computed; this maps them to markup.
|
|
509
|
+
*/
|
|
510
|
+
function walksRichHtml(walksData, statuses = []) {
|
|
511
|
+
const { walks = [], arrivals = [], gloss = {} } = walksData;
|
|
512
|
+
if (walks.length === 0 && arrivals.length === 0) return "";
|
|
513
|
+
const byId = new Map(statuses.map((s) => [s.id, s]));
|
|
514
|
+
|
|
515
|
+
const stageState = { done: "done", current: "cur", pending: "todo", skipped: "skip" };
|
|
516
|
+
const cards = walks.map((w) => {
|
|
517
|
+
const dots = w.stages
|
|
518
|
+
.map((s) => {
|
|
519
|
+
const cls = stageState[s.state] ?? "todo";
|
|
520
|
+
const title = s.note ? `${s.label} — ${s.note}` : s.label;
|
|
521
|
+
const note = s.note && s.key === "prove" ? ` <span class="wk-note">${esc(s.note)}</span>` : "";
|
|
522
|
+
return `<span class="wk-stage wk-${cls}" title="${escAttr(title)}"><span class="wk-dot"></span>${esc(s.label)}${note}</span>`;
|
|
523
|
+
})
|
|
524
|
+
.join("");
|
|
525
|
+
|
|
526
|
+
const g = gloss[w.currentStage];
|
|
527
|
+
const stageLine = w.currentStage
|
|
528
|
+
? `<span class="wk-gloss">${esc(w.stages.find((s) => s.key === w.currentStage)?.label ?? "")}${g ? ` — ${esc(g)}` : ""}</span>`
|
|
529
|
+
: "";
|
|
530
|
+
|
|
531
|
+
// The promises THEMSELVES (L5): kept ✓ / being-kept ▸ / pending ○, in the
|
|
532
|
+
// spec's own words. The list scrolls (CSS) rather than truncates — a
|
|
533
|
+
// collapsed tally is exactly what this rendering replaces.
|
|
534
|
+
const all = (w.promises && w.promises.all) || [];
|
|
535
|
+
const currentId = w.promises && w.promises.current ? w.promises.current.id : null;
|
|
536
|
+
const promiseList =
|
|
537
|
+
all.length > 0
|
|
538
|
+
? ` <ul class="wk-plist">\n${all
|
|
539
|
+
.map((p) => {
|
|
540
|
+
const cls = p.kept ? "wk-p-kept" : p.id === currentId ? "wk-p-cur" : "wk-p-todo";
|
|
541
|
+
const glyph = p.kept ? "✓" : p.id === currentId ? "▸" : "○";
|
|
542
|
+
return ` <li class="${cls}"><span class="wk-p-glyph">${glyph}</span> <code>${esc(p.id)}</code>${p.title ? ` ${esc(p.title)}` : ""}</li>`;
|
|
543
|
+
})
|
|
544
|
+
.join("\n")}\n </ul>`
|
|
545
|
+
: "";
|
|
546
|
+
|
|
547
|
+
// The signature ON the row (L5): the walk names which signature the step
|
|
548
|
+
// waits for; the button speaks the one existing wire (.approve-btn /
|
|
549
|
+
// .feature-accept-btn — refusals stay the server's). An already-approved
|
|
550
|
+
// artifact gets no button that could only fail on click.
|
|
551
|
+
const buttons = (w.you.signable || [])
|
|
552
|
+
.map((s) => {
|
|
553
|
+
if (s.verb === "accept")
|
|
554
|
+
return `<button type="button" class="feature-accept-btn fd-sign" data-name="${escAttr(s.artifact)}">Accept</button>`;
|
|
555
|
+
const record = byId.get(s.artifact);
|
|
556
|
+
if (record && record.status === "approved") return "";
|
|
557
|
+
if (record && record.resolvable === false) return "";
|
|
558
|
+
const label = record && record.status === "unreviewed" ? "Approve" : "Re-approve";
|
|
559
|
+
return `<button type="button" class="approve-btn fd-sign" data-artifact="${escAttr(s.artifact)}">${label}</button>`;
|
|
560
|
+
})
|
|
561
|
+
.filter(Boolean)
|
|
562
|
+
.join(" ");
|
|
563
|
+
|
|
564
|
+
const you =
|
|
565
|
+
w.you.turn === "you"
|
|
566
|
+
? `<span class="wk-turn">YOUR TURN</span> ${esc(w.you.act ?? "")}${buttons ? ` ${buttons}` : ""}`
|
|
567
|
+
: w.you.turn === "agent"
|
|
568
|
+
? `<span class="wk-agent">agent</span> ${esc(w.you.act ?? "")}${
|
|
569
|
+
w.stops && w.stops.length
|
|
570
|
+
? ` <span class="wk-stops">next stop${w.stops.length > 1 ? "s" : ""} for you: ${esc(w.stops.join(", "))}</span>`
|
|
571
|
+
: ""
|
|
572
|
+
}`
|
|
573
|
+
: esc(w.doneReason ?? "closed");
|
|
574
|
+
|
|
575
|
+
const tally =
|
|
576
|
+
w.promises && typeof w.promises.total === "number" && w.promises.total > 0
|
|
577
|
+
? `<span class="wk-promises">${w.promises.kept} of ${w.promises.total} promises kept</span>`
|
|
578
|
+
: "";
|
|
579
|
+
|
|
580
|
+
return ` <div class="wk-card" data-walk="${escAttr(w.name)}">
|
|
581
|
+
<p class="wk-name">${esc(w.name)}${tally} ${stageLine}</p>
|
|
582
|
+
<p class="wk-stages">${dots}</p>
|
|
583
|
+
${promiseList ? `${promiseList}\n` : ""} <p class="wk-you">${you}</p>
|
|
584
|
+
</div>`;
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Arrivals: the now-or-after question as two buttons (L5). Each posts a
|
|
588
|
+
// general comment over the EXISTING /api/comment wire (the pick-btn
|
|
589
|
+
// precedent) — the agent observes it via review_comments; no new state.
|
|
590
|
+
const arrived = arrivals.map(
|
|
591
|
+
(a) =>
|
|
592
|
+
` <p class="wk-arrival">▲ ARRIVED, UNPLANNED — ${esc(a.label || a.id)}${
|
|
593
|
+
a.reason ? ` · ${esc(a.reason)}` : ""
|
|
594
|
+
} — when?
|
|
595
|
+
<button type="button" class="wk-arrival-btn" data-arrival="${escAttr(a.id)}" data-choice="now">Now</button>
|
|
596
|
+
<button type="button" class="wk-arrival-btn" data-arrival="${escAttr(a.id)}" data-choice="after">After the current walk</button>
|
|
597
|
+
</p>`,
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
// The card quotes what a check COSTS, so it must not quote a cache hit. A
|
|
601
|
+
// single last-run figure is dominated by Gradle's cache state; once the
|
|
602
|
+
// journal holds more than two full runs the typical and worst runs are named
|
|
603
|
+
// alongside it. Every figure is a run that happened — never an estimate.
|
|
604
|
+
const lane = walksData.lane;
|
|
605
|
+
const laneSpread =
|
|
606
|
+
lane && lane.runs > 2 && lane.medianMs > 0
|
|
607
|
+
? ` · typically ${esc(fmtLaneMs(lane.medianMs))}${lane.maxMs > lane.medianMs ? `, worst ${esc(fmtLaneMs(lane.maxMs))}` : ""}`
|
|
608
|
+
: "";
|
|
609
|
+
const laneLine =
|
|
610
|
+
lane && typeof lane.durationMs === "number"
|
|
611
|
+
? ` <p class="wk-lane">full check: ${esc(fmtLaneMs(lane.durationMs))} last run${laneSpread} (measured)</p>`
|
|
612
|
+
: "";
|
|
613
|
+
|
|
614
|
+
return ` <h3 class="fd-h">In flight${walks.length ? ` <span class="fd-count">${walks.length}</span>` : ""}</h3>
|
|
615
|
+
${cards.join("\n")}
|
|
616
|
+
${arrived.join("\n")}
|
|
617
|
+
${laneLine}`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Mirror of the harness's humanDuration formatting, display-only. */
|
|
621
|
+
function fmtLaneMs(ms) {
|
|
622
|
+
if (!(ms > 0)) return "unknown";
|
|
623
|
+
return ms < 120000 ? `${Math.round(ms / 1000)}s` : `~${Math.round(ms / 60000)} min`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** The front door's rail glyph — the queue's own state, never a fifth meaning. */
|
|
627
|
+
export function overviewGlyph(queue = [], statuses = []) {
|
|
628
|
+
if (statuses.length === 0) return null;
|
|
629
|
+
if (queue.length === 0) return { ch: "●", cls: "glyph-signed", label: "nothing waiting on you" };
|
|
630
|
+
// Drift is read from the LEDGER, never from the label's wording — matching
|
|
631
|
+
// prose to pick a colour makes the glyph silently wrong the day someone
|
|
632
|
+
// rewords deriveHumanQueue, and a rail glyph that quietly stops going red is
|
|
633
|
+
// the failure this console exists to prevent.
|
|
634
|
+
const byId = new Map(statuses.map((st) => [st.id, st]));
|
|
635
|
+
const drifted = queue.some((q) => {
|
|
636
|
+
const record = byId.get(q.artifact);
|
|
637
|
+
return record && record.status === "changed-since-approval";
|
|
638
|
+
});
|
|
639
|
+
return drifted
|
|
640
|
+
? { ch: "⚠", cls: "glyph-drift", label: `${queue.length} act(s) waiting — drift among them` }
|
|
641
|
+
: { ch: "○", cls: "glyph-unsigned", label: `${queue.length} act(s) waiting on you` };
|
|
642
|
+
}
|