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
|
@@ -38,6 +38,17 @@
|
|
|
38
38
|
// byte-identical at scaffold time — edit the package source, then run
|
|
39
39
|
// `node scripts/sync-harness.mjs`.
|
|
40
40
|
|
|
41
|
+
// The core's fallback grammar, for the ONE thing this file reads out of a flow:
|
|
42
|
+
// the citation marker. Importing it rather than re-declaring the pattern is the
|
|
43
|
+
// point — a second copy of "what a citation looks like" is how the selector and
|
|
44
|
+
// the scanner came to disagree about the same file (see `flowCitation`). The
|
|
45
|
+
// import performs no IO; every function below still takes data and returns data.
|
|
46
|
+
// The badge floor's two halves, imported for the same reason as the grammar
|
|
47
|
+
// above: the plant below calibrates THE grader the lane runs and THE definition
|
|
48
|
+
// of plant material the runner plants from, not a re-statement of either.
|
|
49
|
+
import { evidenceLevel } from "./evidence-level.mjs";
|
|
50
|
+
import { plantCalibration } from "./plant-calibration.mjs";
|
|
51
|
+
|
|
41
52
|
/**
|
|
42
53
|
* Per-direction bound. Rule 0's whole claim is about SPEED of refusal, so the
|
|
43
54
|
* default is small on purpose: the smoke profile is every pure-Node gate and no
|
|
@@ -62,18 +73,133 @@ export const PLANT_KINDS = Object.freeze({
|
|
|
62
73
|
});
|
|
63
74
|
|
|
64
75
|
/**
|
|
65
|
-
* The plants that need nothing but a lane.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
76
|
+
* The plants that need nothing but a lane. The step that reads the machine-owned
|
|
77
|
+
* region exists in every project that has a lane at all — so these two are the
|
|
78
|
+
* floor. If even these cannot run, the tree has no harness to check and the
|
|
79
|
+
* instrument must say so rather than report a vacuous PASS.
|
|
69
80
|
*/
|
|
70
81
|
export const FLOOR_KINDS = Object.freeze([PLANT_KINDS.NARROWED_SURFACE, PLANT_KINDS.EDITED_LANE]);
|
|
71
82
|
|
|
83
|
+
/**
|
|
84
|
+
* How the two floor plants say which receipt row they are about.
|
|
85
|
+
*
|
|
86
|
+
* THE ROW THAT VOUCHES IS THE ROW CARRYING THE VOUCHING DATA, not the row with
|
|
87
|
+
* a particular name — the principle qa/lib/receipt-validate.mjs settled in
|
|
88
|
+
* `checkLaneVouching` one layer out, mirrored here rather than re-invented.
|
|
89
|
+
* These two plants used to declare `step: "harnessIntegrity"` as a literal.
|
|
90
|
+
* That is a name the cmp pack chose for its own step; REQUIRED_EXPORTS never
|
|
91
|
+
* mentions it and a profile author has no way to discover it. On a pack that
|
|
92
|
+
* spells its self-check `harness_integrity`, both floor plants looked for a row
|
|
93
|
+
* that does not exist and the instrument reported "the guard did not FAIL BY
|
|
94
|
+
* NAME" — about a guard that had failed, by name, on the row immediately beside
|
|
95
|
+
* it. An adopter's Rule 0 check therefore fails on a working lane, and the
|
|
96
|
+
* message sends them into the harness instead of into the spelling.
|
|
97
|
+
*
|
|
98
|
+
* `vouching` says: find the row whose `harness` object carries the integrity
|
|
99
|
+
* check's own findings (the field the receipt schema documents, written by
|
|
100
|
+
* every row that performs it). `step` stays as the FALLBACK name, for receipts
|
|
101
|
+
* written before rows carried one — the same two-step lookup checkLaneVouching
|
|
102
|
+
* uses, and the same reason.
|
|
103
|
+
*
|
|
104
|
+
* `hookPattern` is matched against the Stop hook's refusal when the plant's
|
|
105
|
+
* receipt is forged to PASS. It named `harnessIntegrity` too, and the hook's
|
|
106
|
+
* refusal quotes the failing ROW'S name — so on that same pack the hook refused
|
|
107
|
+
* correctly and the instrument called the correct refusal a framework defect.
|
|
108
|
+
* The alternatives here are the core's own two wordings for this class of
|
|
109
|
+
* refusal (qa/lib/receipt-validate.mjs `checkLaneVouching`: the failing-rows
|
|
110
|
+
* branch and the did-not-vouch branch), neither of which is any pack's to spell.
|
|
111
|
+
*/
|
|
112
|
+
const VOUCHING_STEP = Object.freeze({
|
|
113
|
+
step: "harnessIntegrity",
|
|
114
|
+
vouching: true,
|
|
115
|
+
hookPattern: "vouch|the row is the more specific truth",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Which of THIS pack's steps observes the violation a plant makes.
|
|
120
|
+
*
|
|
121
|
+
* THE SECOND HALF OF THE SAME LESSON, and the reason a fix applied to instances
|
|
122
|
+
* comes back. `VOUCHING_STEP` above took the literal `harnessIntegrity` out of
|
|
123
|
+
* the two floor plants because it is a name the cmp pack chose (NORTH-STAR
|
|
124
|
+
* §9.1). The other five plants kept theirs — `specCoverage` on the three spec
|
|
125
|
+
* plants, `e2eCoverage` on the two flow ones — and those are the same string in
|
|
126
|
+
* the same position, doing the same damage: a name the profile protocol never
|
|
127
|
+
* mentions, that REQUIRED_EXPORTS does not ask for and a profile author has no
|
|
128
|
+
* way to discover, asserted by the core against every profile there will ever
|
|
129
|
+
* be.
|
|
130
|
+
*
|
|
131
|
+
* Measured rather than reasoned about, because §9.1's lesson is that a stack
|
|
132
|
+
* assumption naming no stack is found only by running. A `harness init`
|
|
133
|
+
* skeleton with its two steps renamed to snake_case and NOTHING else changed:
|
|
134
|
+
* the lane FAILs, `spec_coverage` names the orphaned citation exactly as it
|
|
135
|
+
* should, and this instrument stops at the first plant with
|
|
136
|
+
*
|
|
137
|
+
* planted "orphaned citation" and the lane said FAIL (specCoverage: no row)
|
|
138
|
+
* — the guard did not FAIL BY NAME
|
|
139
|
+
*
|
|
140
|
+
* That is worse than the floor-plant case rather than equal to it, because the
|
|
141
|
+
* spec plants run FIRST and a plant failure aborts the run: on such a tree the
|
|
142
|
+
* two plants that were already fixed never execute at all, and the adopter's
|
|
143
|
+
* Rule 0 check accuses their working lane while pointing at the harness.
|
|
144
|
+
*
|
|
145
|
+
* Three ways to fix it; only one is honest here.
|
|
146
|
+
*
|
|
147
|
+
* THE DATA SELECTOR `vouching` USES IS NOT AVAILABLE. It works because the CORE
|
|
148
|
+
* computes the integrity findings and the row carries them — `harness`, the one
|
|
149
|
+
* part of that row the receipt schema documents — so every pack that performs
|
|
150
|
+
* the check answers to it whether it declares anything or not. There is no
|
|
151
|
+
* equivalent for spec or journey coverage. What cmp's coverage row carries is
|
|
152
|
+
* `details` ({clauses, withdrawn, tags, files}), written by cmp's own step
|
|
153
|
+
* function; the generated skeleton writes a different shape ({clauses,
|
|
154
|
+
* citations}) from the same core scanner; and nothing documents either as a
|
|
155
|
+
* contract. Inventing a field now would put an undocumented requirement on
|
|
156
|
+
* every future pack, silently unmet by any pack that omits it — the same
|
|
157
|
+
* undiscoverable literal wearing different clothes.
|
|
158
|
+
*
|
|
159
|
+
* WIDENING TO "ANY ROW THAT WENT RED" IS NOT IT EITHER. The kept plant in
|
|
160
|
+
* test/framework-check-agnostic.test.mjs builds the receipt that refutes it: the
|
|
161
|
+
* integrity row FAILs quoting the planted clause id while the coverage gate sits
|
|
162
|
+
* PASS beside it. A lookup that accepts that reads a broken lock as a working
|
|
163
|
+
* spec gate, which is the mix-up this whole instrument exists to catch.
|
|
164
|
+
*
|
|
165
|
+
* SO THE PACK SAYS — through the declaration that already exists for this
|
|
166
|
+
* instrument. `plants` is NORTH-STAR §6's eighth declaration, "the Rule 0/1
|
|
167
|
+
* violations the instrument runs forever", and its endpoint in
|
|
168
|
+
* AGNOSTIC-HARNESS-ARCHITECTURE.md §5.1 is a `plants(tree)` that returns the
|
|
169
|
+
* plants themselves. Which step asserts a plant is part of that plant, so it
|
|
170
|
+
* belongs there and no tenth declaration is invented to hold it. The KEY is a
|
|
171
|
+
* plant kind — PLANT_KINDS, the core's vocabulary — and the VALUE is the pack's
|
|
172
|
+
* spelling, so neither side has to learn the other's words.
|
|
173
|
+
*
|
|
174
|
+
* AND THE DEFAULT IS NOT cmp's SPELLING. That is the distinction §9.1 drew for
|
|
175
|
+
* `compileStepName`: "the distinction is the KEY's presence, not its value — a
|
|
176
|
+
* pack that declares none short-circuits on NOTHING rather than inheriting
|
|
177
|
+
* another stack's step name." Here a pack that declares no step for a kind gets
|
|
178
|
+
* null, and `plantRow` then holds the LANE instead of a row nobody named: the
|
|
179
|
+
* assertion widens honestly, still refuses a lane that stayed green or went red
|
|
180
|
+
* without naming what was planted, and reports the row it actually read.
|
|
181
|
+
* Declaring narrows it back to exactly one row; declaring nothing never inherits
|
|
182
|
+
* the wrong one.
|
|
183
|
+
*
|
|
184
|
+
* @param {{observedBy?: Record<string, string>}} tree
|
|
185
|
+
* @param {string} kind one of PLANT_KINDS
|
|
186
|
+
* @returns {string|null} this pack's name for the step, or null if it named none
|
|
187
|
+
*/
|
|
188
|
+
function observingStep(tree, kind) {
|
|
189
|
+
const declared = tree?.observedBy?.[kind];
|
|
190
|
+
return typeof declared === "string" && declared.length > 0 ? declared : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
72
193
|
/** A clause id at the head of a spec list item: `- **HOME-02** — …`. */
|
|
73
194
|
const CLAUSE_RE = /^-\s+\*\*([A-Z][A-Z0-9]*-\d{2,})\*\*/m;
|
|
74
195
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
196
|
+
/**
|
|
197
|
+
* The ids on a citation line, once the MARKER has already matched it:
|
|
198
|
+
* `# SPEC: HOME-02, HOME-03 — …`. The id grammar is the core's (it is the same
|
|
199
|
+
* one scanCitations uses); the marker in front of it is the profile's.
|
|
200
|
+
*/
|
|
201
|
+
const CITATION_IDS_RE = /SPEC:\s*([A-Z0-9,\s-]+)/;
|
|
202
|
+
const CLAUSE_ID_RE = /^[A-Z][A-Z0-9]*-\d{2,}$/;
|
|
77
203
|
|
|
78
204
|
/**
|
|
79
205
|
* The first clause id in a spec, or null. Used to pick something real to plant
|
|
@@ -90,13 +216,43 @@ export function firstClauseId(text) {
|
|
|
90
216
|
|
|
91
217
|
/**
|
|
92
218
|
* The clause a flow cites, or null.
|
|
219
|
+
*
|
|
220
|
+
* THE MARKER IS THE PROFILE'S. This matched `^#\s*SPEC:` and nothing else,
|
|
221
|
+
* which is true of Maestro YAML and false of a stack whose journeys are .ts,
|
|
222
|
+
* .kt, .rb or anything C-family. The cost was not a refusal: a project whose
|
|
223
|
+
* journeys cite with `//` was told `no "# SPEC:" citation in N flow file(s)`,
|
|
224
|
+
* both e2eCoverage plants stood down, and the check printed PASS with the
|
|
225
|
+
* reason folded into an ⓘ line — a gate reported as calibrated that had never
|
|
226
|
+
* been read. And the disagreement was internal: the core's own fallback marker
|
|
227
|
+
* (spec-model.mjs `DEFAULT_GRAMMAR.citationMarker`) has always accepted `//`,
|
|
228
|
+
* so qa/lib/spec-coverage.mjs `scanCitations` was counting the very citation
|
|
229
|
+
* this function could not see, in the same file, in the same tree.
|
|
230
|
+
*
|
|
231
|
+
* Same shape as scanCitations: test the profile's marker against the trimmed
|
|
232
|
+
* line, then read the ids after it. A profile that declares no `grammar` gets
|
|
233
|
+
* the same fallback the scan uses, so the two answers cannot drift apart again.
|
|
234
|
+
*
|
|
93
235
|
* @param {string} text
|
|
236
|
+
* @param {{citationMarker?: RegExp}} [grammar] the SpecModel's grammar
|
|
94
237
|
* @returns {string|null}
|
|
95
238
|
*/
|
|
96
|
-
export function flowCitation(text) {
|
|
239
|
+
export function flowCitation(text, grammar) {
|
|
97
240
|
if (typeof text !== "string") return null;
|
|
98
|
-
|
|
99
|
-
|
|
241
|
+
// The marker is the profile's; with none declared nothing can be a citation — and the lane refuses that profile before this runs.
|
|
242
|
+
const MARKER = grammar?.citationMarker;
|
|
243
|
+
if (!(MARKER instanceof RegExp)) return null;
|
|
244
|
+
for (const line of text.split("\n")) {
|
|
245
|
+
const trimmed = line.trim();
|
|
246
|
+
if (!MARKER.test(trimmed)) continue;
|
|
247
|
+
const m = trimmed.match(CITATION_IDS_RE);
|
|
248
|
+
if (!m) continue;
|
|
249
|
+
const id = m[1]
|
|
250
|
+
.split(/[,\s]+/)
|
|
251
|
+
.map((s) => s.trim())
|
|
252
|
+
.find((s) => CLAUSE_ID_RE.test(s));
|
|
253
|
+
if (id) return id;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
100
256
|
}
|
|
101
257
|
|
|
102
258
|
/**
|
|
@@ -115,11 +271,26 @@ export function clauseFamily(clause) {
|
|
|
115
271
|
* Decide which plants this tree can support, and say WHY each unavailable one
|
|
116
272
|
* is unavailable.
|
|
117
273
|
*
|
|
274
|
+
* `grammar` is the SpecModel's (qa/lib/spec-model.mjs): the caller passes the
|
|
275
|
+
* profile's, and a caller that passes none gets the same fallback the coverage
|
|
276
|
+
* scan uses. It decides one thing here — what a citation in a journey looks
|
|
277
|
+
* like — and getting it from the profile is what stops this instrument from
|
|
278
|
+
* disagreeing with the gate it is calibrating (see `flowCitation`).
|
|
279
|
+
*
|
|
280
|
+
* `observedBy` is the profile's `plants.observedBy` — plant kind → the name
|
|
281
|
+
* THIS pack gives the step that catches that kind of violation. A kind it does
|
|
282
|
+
* not name gets a null `step`, which is an assertion over the lane rather than
|
|
283
|
+
* over one row, never cmp's spelling by default (see `observingStep`).
|
|
284
|
+
*
|
|
118
285
|
* @param {{specs?: Array<{rel: string, text: string}>,
|
|
119
286
|
* flows?: Array<{rel: string, text: string}>,
|
|
120
287
|
* harnessLib?: string[],
|
|
121
|
-
* testDir?: string|null
|
|
122
|
-
*
|
|
288
|
+
* testDir?: string|null,
|
|
289
|
+
* plantsDeclared?: boolean,
|
|
290
|
+
* unmeetableTier?: string,
|
|
291
|
+
* observedBy?: Record<string, string>,
|
|
292
|
+
* grammar?: {citationMarker?: RegExp}}} tree
|
|
293
|
+
* @returns {{plants: Array<{kind: string, label: string, step: string|null,
|
|
123
294
|
* names: string[], target: object}>,
|
|
124
295
|
* unavailable: Array<{kind: string, reason: string}>}}
|
|
125
296
|
*/
|
|
@@ -128,6 +299,14 @@ export function selectPlants(tree) {
|
|
|
128
299
|
const flows = Array.isArray(tree?.flows) ? tree.flows : [];
|
|
129
300
|
const harnessLib = Array.isArray(tree?.harnessLib) ? tree.harnessLib : [];
|
|
130
301
|
const testDir = tree?.testDir ?? null;
|
|
302
|
+
// Where this stack keeps the flow-shaped citation files the lane executes
|
|
303
|
+
// (the profile's `layout.flows.dir`). The nested-flow plant must land INSIDE
|
|
304
|
+
// it — a subdirectory the runner walks past — so the directory cannot be a
|
|
305
|
+
// constant here. Derived from the flows themselves when the caller does not
|
|
306
|
+
// say, so a caller with flows always gets the plant.
|
|
307
|
+
const flowsDir = typeof tree?.flowsDir === "string" && tree.flowsDir
|
|
308
|
+
? tree.flowsDir
|
|
309
|
+
: (flows.find((f) => typeof f?.rel === "string" && f.rel.includes("/"))?.rel.replace(/\/[^/]*$/, "") ?? null);
|
|
131
310
|
|
|
132
311
|
const plants = [];
|
|
133
312
|
const unavailable = [];
|
|
@@ -143,12 +322,13 @@ export function selectPlants(tree) {
|
|
|
143
322
|
: "no spec files — nothing declares behavior to plant against";
|
|
144
323
|
for (const kind of [PLANT_KINDS.ORPHANED_CITATION, PLANT_KINDS.UNBOUND_CITATION, PLANT_KINDS.TIER_UNMET]) skip(kind, why);
|
|
145
324
|
} else {
|
|
146
|
-
// Renaming a live clause orphans every citation of it:
|
|
147
|
-
// name the id it can no longer find.
|
|
325
|
+
// Renaming a live clause orphans every citation of it: whichever step reads
|
|
326
|
+
// clause↔test citations must name the id it can no longer find. WHICH step
|
|
327
|
+
// that is is the pack's to spell and never this file's — see observingStep.
|
|
148
328
|
plants.push({
|
|
149
329
|
kind: PLANT_KINDS.ORPHANED_CITATION,
|
|
150
330
|
label: "orphaned citation",
|
|
151
|
-
step:
|
|
331
|
+
step: observingStep(tree, PLANT_KINDS.ORPHANED_CITATION),
|
|
152
332
|
names: [clause],
|
|
153
333
|
target: { spec: spec.rel, clause },
|
|
154
334
|
});
|
|
@@ -158,39 +338,71 @@ export function selectPlants(tree) {
|
|
|
158
338
|
// exists, the tag exists, and nothing runs. The two remaining spec plants
|
|
159
339
|
// need somewhere to put that Kotlin, so they hang on a test source dir.
|
|
160
340
|
if (!testDir) {
|
|
161
|
-
|
|
341
|
+
// TWO different causes reached this branch and the message named only
|
|
342
|
+
// one of them, wrongly: a profile that declares no `plants` never even
|
|
343
|
+
// looks for a test directory (framework-check.mjs passes testDir: null),
|
|
344
|
+
// so an adopter with two perfectly good test directories was told they
|
|
345
|
+
// had none. Worse, the plant this silently skips is `tier-unmet` — the
|
|
346
|
+
// one that calibrates the gate the whole instrument exists to prove —
|
|
347
|
+
// and the run still printed PASS. Saying which cause it is turns an
|
|
348
|
+
// invisible gap into a one-line fix. (And the core names no language.)
|
|
349
|
+
const why = tree?.plantsDeclared === false
|
|
350
|
+
? "this profile declares no plants — add a `plants` export { testFileBasename, unboundCitationSource, tierUnmetCitationSource, unmeetableTier }"
|
|
351
|
+
: "no test source directory found — a planted citation has nowhere to live";
|
|
162
352
|
skip(PLANT_KINDS.UNBOUND_CITATION, why);
|
|
163
353
|
skip(PLANT_KINDS.TIER_UNMET, why);
|
|
164
354
|
} else {
|
|
165
355
|
plants.push({
|
|
166
356
|
kind: PLANT_KINDS.UNBOUND_CITATION,
|
|
167
357
|
label: "unbound citation",
|
|
168
|
-
step:
|
|
358
|
+
step: observingStep(tree, PLANT_KINDS.UNBOUND_CITATION),
|
|
169
359
|
names: [`${family}-99`],
|
|
170
360
|
target: { spec: spec.rel, clause: `${family}-99`, testDir },
|
|
171
361
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
362
|
+
// The tier this plant declares must be the PROFILE'S. The core used to
|
|
363
|
+
// fall back to `?? "e2e"` at the write site — a CMP tier name, asserted
|
|
364
|
+
// by the spine into any stack's spec file. A profile that ships plants
|
|
365
|
+
// but names no unmeetable tier would then plant a clause tagged with a
|
|
366
|
+
// tier it does not define, and the resulting FAIL would be right for
|
|
367
|
+
// entirely the wrong reason. There is no default; there is a skip.
|
|
368
|
+
if (!tree?.unmeetableTier) {
|
|
369
|
+
skip(
|
|
370
|
+
PLANT_KINDS.TIER_UNMET,
|
|
371
|
+
"this profile's plants declare no `unmeetableTier` — name a tier in `tiers.satisfying` that a host-tier test cannot satisfy",
|
|
372
|
+
);
|
|
373
|
+
} else {
|
|
374
|
+
plants.push({
|
|
375
|
+
kind: PLANT_KINDS.TIER_UNMET,
|
|
376
|
+
label: "tier unmet",
|
|
377
|
+
step: observingStep(tree, PLANT_KINDS.TIER_UNMET),
|
|
378
|
+
names: [`${family}-98`],
|
|
379
|
+
target: { spec: spec.rel, clause: `${family}-98`, testDir, unmeetableTier: tree.unmeetableTier },
|
|
380
|
+
});
|
|
381
|
+
}
|
|
179
382
|
}
|
|
180
383
|
}
|
|
181
384
|
|
|
182
385
|
// ── Flow-derived plants ──────────────────────────────────────────────────
|
|
183
386
|
// Both strip EVERY citation from every flow, not just one line. e2eCoverage
|
|
184
387
|
// asks whether a screen feature has any device journey at all, so removing a
|
|
185
|
-
// single
|
|
388
|
+
// single citation from a flow that carries several leaves the feature
|
|
186
389
|
// covered and the gate — correctly — green. A plant that does not actually
|
|
187
390
|
// produce the violation is worse than no plant: it reads as a calibrated
|
|
188
391
|
// gate while proving nothing.
|
|
189
|
-
|
|
392
|
+
//
|
|
393
|
+
// WHICH flows cite is decided by the profile's citation marker, never by `#`:
|
|
394
|
+
// see `flowCitation`. The skip below prints the marker it used, because "no
|
|
395
|
+
// citation in 3 flow files" over three flows that all carry one is a sentence
|
|
396
|
+
// that sends the reader to the flows instead of to the grammar.
|
|
397
|
+
const grammar = tree?.grammar ?? null;
|
|
398
|
+
const marker = grammar?.citationMarker instanceof RegExp ? grammar.citationMarker : null;
|
|
399
|
+
const citingFlows = marker ? flows.filter((f) => flowCitation(f?.text, grammar)) : [];
|
|
190
400
|
if (!citingFlows.length) {
|
|
191
|
-
const why =
|
|
192
|
-
?
|
|
193
|
-
:
|
|
401
|
+
const why = !marker
|
|
402
|
+
? "the profile declares no grammar.citationMarker — the plant selector cannot read a citation (the lane refuses this profile for the same reason)"
|
|
403
|
+
: flows.length
|
|
404
|
+
? `no citation matching /${marker.source}/ in ${flows.length} flow file(s) — the journey-coverage gate has nothing to lose`
|
|
405
|
+
: `no flows${flowsDir ? ` under ${flowsDir}` : ""} — this project declares no journeys`;
|
|
194
406
|
skip(PLANT_KINDS.FEATURE_WITHOUT_FLOW, why);
|
|
195
407
|
skip(PLANT_KINDS.NESTED_FLOW, why);
|
|
196
408
|
} else {
|
|
@@ -199,36 +411,44 @@ export function selectPlants(tree) {
|
|
|
199
411
|
plants.push({
|
|
200
412
|
kind: PLANT_KINDS.FEATURE_WITHOUT_FLOW,
|
|
201
413
|
label: "feature without a flow",
|
|
202
|
-
step:
|
|
414
|
+
step: observingStep(tree, PLANT_KINDS.FEATURE_WITHOUT_FLOW),
|
|
203
415
|
names: [],
|
|
204
416
|
// FAIL BY NAME, without knowing this project's feature names: the gate
|
|
205
417
|
// must name the feature it caught, in the [brackets] its reason uses.
|
|
206
418
|
reasonPattern: String.raw`\[[^\]\s]+\]`,
|
|
207
419
|
target: { flows: rels },
|
|
208
420
|
});
|
|
209
|
-
// The citations move into a subdirectory
|
|
210
|
-
//
|
|
211
|
-
// must read exactly like having no
|
|
421
|
+
// The citations move into a subdirectory of the flows directory that the
|
|
422
|
+
// lane's own directory run does not descend into. The tags exist, the flow
|
|
423
|
+
// is real, and nothing executes it — which must read exactly like having no
|
|
424
|
+
// journey at all.
|
|
212
425
|
plants.push({
|
|
213
426
|
kind: PLANT_KINDS.NESTED_FLOW,
|
|
214
427
|
label: "flow the lane never runs",
|
|
215
|
-
step:
|
|
428
|
+
step: observingStep(tree, PLANT_KINDS.NESTED_FLOW),
|
|
216
429
|
names: [],
|
|
217
430
|
reasonPattern: String.raw`\[[^\]\s]+\]`,
|
|
218
|
-
target: { flows: rels, nestInto:
|
|
431
|
+
target: { flows: rels, nestInto: `${flowsDir}/wip` },
|
|
219
432
|
});
|
|
220
433
|
}
|
|
221
434
|
|
|
222
435
|
// ── Region plants — the floor ────────────────────────────────────────────
|
|
223
436
|
// A narrowed declaration un-attests a whole layer while every checker stays
|
|
224
437
|
// intact (payment-blueprint's planted proof); an edited lane cannot vouch for
|
|
225
|
-
// its own verdict. Both are read by
|
|
438
|
+
// its own verdict. Both are read by the step that vouches for the lane, which
|
|
439
|
+
// needs only a lane — see `vouching` below for how that row is found.
|
|
226
440
|
plants.push({
|
|
227
441
|
kind: PLANT_KINDS.NARROWED_SURFACE,
|
|
228
442
|
label: "narrowed surface declaration",
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
443
|
+
...VOUCHING_STEP,
|
|
444
|
+
// Assert the FILE, not an internal state word. "unrecorded" only holds when
|
|
445
|
+
// the declaration is absent from the lock — true for a freshly stamped app,
|
|
446
|
+
// false for a repo whose lock was taken after `harness init` wrote the
|
|
447
|
+
// surface, where the identical edit reads as "modified". Both are the same
|
|
448
|
+
// correct refusal; pinning one of them made the instrument fail on a lane
|
|
449
|
+
// that was working. What the plant actually cares about is that the refusal
|
|
450
|
+
// NAMES what it refused over, which is now true in either state.
|
|
451
|
+
names: ["qa/verified-surface.json"],
|
|
232
452
|
target: { declaration: "qa/verified-surface.json" },
|
|
233
453
|
});
|
|
234
454
|
|
|
@@ -239,9 +459,11 @@ export function selectPlants(tree) {
|
|
|
239
459
|
plants.push({
|
|
240
460
|
kind: PLANT_KINDS.EDITED_LANE,
|
|
241
461
|
label: "edited lane cannot vouch",
|
|
242
|
-
|
|
462
|
+
...VOUCHING_STEP,
|
|
463
|
+
// "modified" is the integrity check's own status word (qa/lib/harness-
|
|
464
|
+
// region.mjs), not a step name and not a stack's — every pack's row
|
|
465
|
+
// reports it, because the core computes it.
|
|
243
466
|
names: ["modified"],
|
|
244
|
-
hookPattern: "harnessIntegrity|vouch",
|
|
245
467
|
target: { file: spine },
|
|
246
468
|
});
|
|
247
469
|
}
|
|
@@ -276,6 +498,148 @@ export function assessCoverage(plants) {
|
|
|
276
498
|
return { ok: true };
|
|
277
499
|
}
|
|
278
500
|
|
|
501
|
+
/**
|
|
502
|
+
* THE BADGE FLOOR'S KEPT PLANT — "a profile with no calibrated plants earns no
|
|
503
|
+
* rung" (NORTH-STAR.md §8.9), watched failing rather than assumed to work.
|
|
504
|
+
*
|
|
505
|
+
* WHY IT IS HERE AND NOT ONLY IN THE HARNESS'S OWN SUITE. GATE-RULES Rule 1 is
|
|
506
|
+
* explicit about where a plant goes — "Add the plant to `qa/framework-check.mjs`
|
|
507
|
+
* and run that" — and the reason is this gate's own subject: the floor exists to
|
|
508
|
+
* protect a claim made on EVERY adopter's receipt, so the plant that proves it
|
|
509
|
+
* still bites has to run in every adopter's tree, forever, not once in ours.
|
|
510
|
+
* A gate proved only in the repository that wrote it is a gate proved on the one
|
|
511
|
+
* profile it was written against, which is how every wrong verdict in
|
|
512
|
+
* NORTH-STAR.md §9.1 survived a green suite.
|
|
513
|
+
*
|
|
514
|
+
* THE VIOLATION PLANTED is the twin `scripts/stage2-gate.mjs` built: this
|
|
515
|
+
* profile's own ladder and a lane whose rows all PASS, graded with the `plants`
|
|
516
|
+
* declaration REMOVED. Before 2026-09-08 that earned `L1` — measured on two
|
|
517
|
+
* scratch adopters differing in exactly one export. It must now earn nothing,
|
|
518
|
+
* and the same rows WITH the declaration must still earn their rung, or the
|
|
519
|
+
* plant would go green on a grader that had simply stopped grading anyone.
|
|
520
|
+
* Those two halves are the same pair criteria F and G of the stage-2 gate make,
|
|
521
|
+
* and they are made here in microseconds instead of two adopted trees.
|
|
522
|
+
*
|
|
523
|
+
* NO TREE IS TOUCHED AND NO LANE IS RUN. The rows are synthesised from the
|
|
524
|
+
* ladder's own step names, so this asserts over the GRADER — which is the only
|
|
525
|
+
* thing the floor lives in. That is also its limit, and it is stated in the
|
|
526
|
+
* runner's own output rather than hidden here: it proves the rung a lane WOULD
|
|
527
|
+
* be given, not that a lane was run.
|
|
528
|
+
*
|
|
529
|
+
* @param {{ladder: object|null|undefined, plants: object|null|undefined}} profile
|
|
530
|
+
* the profile's resolved ladder (qa/lib/evidence-ladder.mjs) and its `plants`
|
|
531
|
+
* declaration — both exactly as the lane runner reads them
|
|
532
|
+
* @returns {{ok: true, rung: string}
|
|
533
|
+
* |{ok: false, reason: string}
|
|
534
|
+
* |{available: false, reason: string}} `available: false` is neither a pass
|
|
535
|
+
* nor a failure: it is a plant this tree cannot make, reported with its cause
|
|
536
|
+
* the same way every unavailable plant is.
|
|
537
|
+
*/
|
|
538
|
+
export function assessBadgeFloor({ ladder, plants } = {}) {
|
|
539
|
+
const calibrated = plantCalibration(plants);
|
|
540
|
+
if (!calibrated.ok) {
|
|
541
|
+
return { available: false, reason: `${calibrated.reason} — there is no declaration to strip, and no rung to lose` };
|
|
542
|
+
}
|
|
543
|
+
if (!ladder || typeof ladder !== "object") {
|
|
544
|
+
return {
|
|
545
|
+
available: false,
|
|
546
|
+
reason:
|
|
547
|
+
"this profile declares no `ladder` a reader can see without starting a lane, so no rung exists either way " +
|
|
548
|
+
"(a ladder declared only on the object `steps(ctx)` returns is invisible here — qa/lib/evidence-ladder.mjs)",
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const names = [...new Set([...(ladder.l0Required ?? []), ...(ladder.l1Required ?? [])])];
|
|
552
|
+
const rows = names.map((name) => ({ name, verdict: "PASS" }));
|
|
553
|
+
const earned = evidenceLevel(rows, null, { mode: "full", ladder, plants });
|
|
554
|
+
if (!earned) {
|
|
555
|
+
return {
|
|
556
|
+
available: false,
|
|
557
|
+
reason:
|
|
558
|
+
`this profile's ladder grants no rung even with ${names.length ? names.join(", ") : "every declared step"} PASSing, ` +
|
|
559
|
+
"so there is no rung for the floor to withhold",
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
// THE PLANT: the same rows, the same ladder, and no plant material.
|
|
563
|
+
const stripped = evidenceLevel(rows, null, { mode: "full", ladder, plants: undefined });
|
|
564
|
+
if (stripped) {
|
|
565
|
+
return {
|
|
566
|
+
ok: false,
|
|
567
|
+
reason:
|
|
568
|
+
`PLANTED the profile's own ladder with its \`plants\` declaration removed and the lane still earned ` +
|
|
569
|
+
`${stripped.rung} · ${stripped.name} — the badge floor is not being applied. NORTH-STAR.md §8.9: "a profile ` +
|
|
570
|
+
`with no calibrated plants earns no rung"; §6.7: a profile ships plants this instrument can run, or it ships ` +
|
|
571
|
+
`without a badge. The grader is qa/lib/evidence-level.mjs and the floor it asks is qa/lib/plant-calibration.mjs`,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return { ok: true, rung: earned.rung };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* What "FAIL BY NAME" means for one plant, in ONE place.
|
|
579
|
+
*
|
|
580
|
+
* Two callers need this answer — `plantRow`, to pick the row a plant with no
|
|
581
|
+
* declared step is about, and `assessPlantRun`, to say which part of the
|
|
582
|
+
* naming is missing — and a second copy of "what counts as naming" is exactly
|
|
583
|
+
* how `flowCitation` and `scanCitations` came to disagree about the same file
|
|
584
|
+
* in the same tree. So there is one, and the caller that needs a sentence gets
|
|
585
|
+
* the offending part back rather than re-deriving it.
|
|
586
|
+
*
|
|
587
|
+
* @param {{reason?: string}} row
|
|
588
|
+
* @param {{names?: string[], reasonPattern?: string}} plant
|
|
589
|
+
* @returns {{kind: "name"|"pattern", want: string, reason: string}|null} null when the row names everything asked of it
|
|
590
|
+
*/
|
|
591
|
+
function missingName(row, plant) {
|
|
592
|
+
const reason = String(row?.reason ?? "");
|
|
593
|
+
for (const name of plant?.names ?? []) {
|
|
594
|
+
if (!reason.includes(name)) return { kind: "name", want: name, reason };
|
|
595
|
+
}
|
|
596
|
+
if (plant?.reasonPattern && !new RegExp(plant.reasonPattern).test(reason)) {
|
|
597
|
+
return { kind: "pattern", want: plant.reasonPattern, reason };
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* The receipt row a plant's assertion is about — found three ways, in the order
|
|
604
|
+
* of how much each one can be trusted about a pack nobody here has met.
|
|
605
|
+
*
|
|
606
|
+
* BY ITS DATA, for the two floor plants. Their assertion is about the step that
|
|
607
|
+
* VOUCHES FOR THE LANE, whatever the pack calls it, and the core computes the
|
|
608
|
+
* findings that row carries — so `harness` identifies it on every pack, with
|
|
609
|
+
* the name only as the pre-`harness` fallback. See VOUCHING_STEP.
|
|
610
|
+
*
|
|
611
|
+
* BY THE NAME THE PACK GAVE IT, when the pack declared one for this plant kind
|
|
612
|
+
* (`plants.observedBy`). This is the sharp case and the one to prefer: a
|
|
613
|
+
* different row failing instead is the mix-up the instrument exists to catch,
|
|
614
|
+
* and only a named row can catch it.
|
|
615
|
+
*
|
|
616
|
+
* BY THE LANE, when the pack declared nothing. There is no honest third source
|
|
617
|
+
* for another pack's step name (see `observingStep`), so the assertion becomes
|
|
618
|
+
* "some row went red naming what was planted" — the row carrying the plant's own
|
|
619
|
+
* fingerprint. Only FAIL rows can satisfy it: an ERROR is a step that fell over,
|
|
620
|
+
* not a gate that read something and refused, and treating the two alike is how
|
|
621
|
+
* a crash comes to read as a calibrated gate. When no FAIL row names the plant,
|
|
622
|
+
* a red row is returned anyway rather than null, because `assessPlantRun` then
|
|
623
|
+
* prints that row's reason — "this is what your lane actually said" is a far
|
|
624
|
+
* better sentence to hand an adopter than "no row" — and the FAIL rows are
|
|
625
|
+
* offered ahead of the ERROR ones, or a step that blew up alongside a gate that
|
|
626
|
+
* fired correctly would be reported as the finding. This branch is weaker than
|
|
627
|
+
* a declared name and is meant to be: it is what a pack gets for not saying,
|
|
628
|
+
* and it still bites.
|
|
629
|
+
*
|
|
630
|
+
* @param {Array<object>} steps
|
|
631
|
+
* @param {{step?: string|null, vouching?: boolean, names?: string[], reasonPattern?: string}} plant
|
|
632
|
+
* @returns {object|null}
|
|
633
|
+
*/
|
|
634
|
+
export function plantRow(steps, plant) {
|
|
635
|
+
const rows = Array.isArray(steps) ? steps : [];
|
|
636
|
+
const byName = plant?.step ? (rows.find((s) => s && s.name === plant.step) ?? null) : null;
|
|
637
|
+
if (plant?.vouching) return rows.find((s) => s && s.harness && typeof s.harness === "object") ?? byName;
|
|
638
|
+
if (plant?.step) return byName;
|
|
639
|
+
const failed = rows.filter((s) => s && s.verdict === "FAIL");
|
|
640
|
+
return failed.find((s) => !missingName(s, plant)) ?? failed[0] ?? rows.find((s) => s && s.verdict === "ERROR") ?? null;
|
|
641
|
+
}
|
|
642
|
+
|
|
279
643
|
/**
|
|
280
644
|
* Judge one planted run. Every branch here is a distinct framework defect and
|
|
281
645
|
* says which one it is: a hang, a lane that produced no receipt, a guard that
|
|
@@ -286,10 +650,15 @@ export function assessCoverage(plants) {
|
|
|
286
650
|
* told apart from a gate failing for an unrelated reason, which is how a
|
|
287
651
|
* calibration passes on a gate that was never actually read.
|
|
288
652
|
*
|
|
653
|
+
* Every message below names the row it ACTUALLY READ, not the row the plant
|
|
654
|
+
* asked for: on a pack whose steps are spelled differently those are two
|
|
655
|
+
* different strings, and reporting the request instead of the finding is how a
|
|
656
|
+
* lookup miss reads as a gate defect.
|
|
657
|
+
*
|
|
289
658
|
* @param {{hung?: boolean, ms?: number, exit?: number|null,
|
|
290
659
|
* receipt?: {verdict?: string, steps?: Array<object>}|null,
|
|
291
660
|
* stderr?: string}} run
|
|
292
|
-
* @param {{label: string, step
|
|
661
|
+
* @param {{label: string, kind?: string, step?: string|null, names?: string[], vouching?: boolean}} plant
|
|
293
662
|
* @param {number} boundMs
|
|
294
663
|
* @returns {{ok: true}|{ok: false, reason: string}}
|
|
295
664
|
*/
|
|
@@ -303,24 +672,36 @@ export function assessPlantRun(run, plant, boundMs) {
|
|
|
303
672
|
const tail = String(run?.stderr ?? "").slice(-600);
|
|
304
673
|
return { ok: false, reason: `"${label}" returned no receipt (exit ${run?.exit ?? "?"})${tail ? `:\n${tail}` : ""}` };
|
|
305
674
|
}
|
|
306
|
-
const row = (receipt.steps
|
|
675
|
+
const row = plantRow(receipt.steps, plant);
|
|
676
|
+
const rowName = row?.name ?? plant.step ?? "the lane";
|
|
307
677
|
if (receipt.verdict !== "FAIL" || !row || row.verdict !== "FAIL") {
|
|
678
|
+
// A missing row says what was looked for. "harnessIntegrity: no row" over a
|
|
679
|
+
// receipt whose vouching row is called something else is a true sentence
|
|
680
|
+
// that points at the wrong thing. The third branch is the pack that named
|
|
681
|
+
// no step for this kind: "no row" would be the same wrong sentence again,
|
|
682
|
+
// so it says what the lookup actually was AND names the declaration that
|
|
683
|
+
// would make it sharp — an adopter cannot fix a requirement nobody states.
|
|
684
|
+
const found = row
|
|
685
|
+
? `${rowName}: ${row.verdict}`
|
|
686
|
+
: plant.vouching
|
|
687
|
+
? `no row carries a \`harness\` object and none is named ${plant.step}`
|
|
688
|
+
: plant.step
|
|
689
|
+
? `${plant.step}: no row`
|
|
690
|
+
: `no row on this receipt went red at all, and this profile's \`plants.observedBy\` names no step for ${plant.kind ?? "this plant"}`;
|
|
308
691
|
return {
|
|
309
692
|
ok: false,
|
|
310
|
-
reason: `planted "${label}" and the lane said ${receipt.verdict} (${
|
|
693
|
+
reason: `planted "${label}" and the lane said ${receipt.verdict} (${found}) — the guard did not FAIL BY NAME`,
|
|
311
694
|
};
|
|
312
695
|
}
|
|
313
|
-
const reason = String(row.reason ?? "");
|
|
314
|
-
for (const name of plant.names ?? []) {
|
|
315
|
-
if (!reason.includes(name)) {
|
|
316
|
-
return { ok: false, reason: `${plant.step} FAILed on "${label}" but did not NAME ${name}:\n${reason}` };
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
696
|
// Some gates name something the selector cannot know in advance — a feature
|
|
320
|
-
// this project happens to have.
|
|
321
|
-
// NAME instead of settling for "it went red"
|
|
322
|
-
|
|
323
|
-
|
|
697
|
+
// this project happens to have. `reasonPattern` is how those still assert
|
|
698
|
+
// FAIL BY NAME instead of settling for "it went red"; `missingName` holds
|
|
699
|
+
// both halves so the row lookup above cannot drift from the judgement here.
|
|
700
|
+
const missing = missingName(row, plant);
|
|
701
|
+
if (missing) {
|
|
702
|
+
return missing.kind === "name"
|
|
703
|
+
? { ok: false, reason: `${rowName} FAILed on "${label}" but did not NAME ${missing.want}:\n${missing.reason}` }
|
|
704
|
+
: { ok: false, reason: `${rowName} FAILed on "${label}" but named nothing matching /${missing.want}/:\n${missing.reason}` };
|
|
324
705
|
}
|
|
325
706
|
return { ok: true };
|
|
326
707
|
}
|