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
|
@@ -33,12 +33,13 @@
|
|
|
33
33
|
// changes, because a crash mid-plant must never be able to lose your work.
|
|
34
34
|
//
|
|
35
35
|
// Which plants are possible is DERIVED from the tree (qa/lib/framework-check.mjs).
|
|
36
|
-
// A project with no specs, no flows, or no
|
|
36
|
+
// A project with no specs, no flows, or no test sources still gets the region
|
|
37
37
|
// plants; each unavailable plant is reported WITH ITS REASON. Nothing here ever
|
|
38
38
|
// prints PASS because it found nothing to do.
|
|
39
39
|
|
|
40
40
|
import { spawnSync } from "node:child_process";
|
|
41
41
|
import fs from "node:fs";
|
|
42
|
+
import os from "node:os";
|
|
42
43
|
import path from "node:path";
|
|
43
44
|
import { fileURLToPath } from "node:url";
|
|
44
45
|
|
|
@@ -51,15 +52,22 @@ import {
|
|
|
51
52
|
assessPlantRun,
|
|
52
53
|
assessGreenRun,
|
|
53
54
|
assessCalibrationCost,
|
|
55
|
+
assessBadgeFloor,
|
|
56
|
+
plantRow,
|
|
54
57
|
} from "./lib/framework-check.mjs";
|
|
58
|
+
import { plantCalibration } from "./lib/plant-calibration.mjs";
|
|
59
|
+
import { evidenceLadderFor } from "./lib/evidence-ladder.mjs";
|
|
55
60
|
import { listHarnessFiles } from "./lib/harness-region.mjs";
|
|
56
|
-
import {
|
|
61
|
+
import { checkHarnessIntegrity, describeIntegrity, writeHarnessLock } from "./lib/harness-lock.mjs";
|
|
62
|
+
import { listFlowFiles, scanCitations, walkFiles } from "./lib/spec-coverage.mjs";
|
|
63
|
+
import { resolveSpecModel } from "./lib/spec-model.mjs";
|
|
64
|
+
import { resolveHarnessManifest } from "./lib/harness-manifest.mjs";
|
|
65
|
+
import { loadProfileSync } from "./lib/profile-loader.mjs";
|
|
57
66
|
|
|
58
67
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
59
68
|
const RECEIPT_REL = "qa/evidence/latest.json";
|
|
60
69
|
const SURFACE_REL = "qa/verified-surface.json";
|
|
61
70
|
const README_REL = "README.md";
|
|
62
|
-
const PLANTED_TEST_BASENAME = "CmpFrameworkCheckPlanted.kt";
|
|
63
71
|
|
|
64
72
|
const argv = process.argv.slice(2);
|
|
65
73
|
const flag = (n) => argv.includes(n);
|
|
@@ -188,45 +196,129 @@ function hookRefuses() {
|
|
|
188
196
|
// ── Read the tree, decide what can be planted ───────────────────────────────
|
|
189
197
|
|
|
190
198
|
function readSpecs() {
|
|
191
|
-
|
|
199
|
+
if (!SPEC_MODEL) return [];
|
|
200
|
+
const dir = path.join(ROOT, ...SPEC_MODEL.specsDir.split("/"));
|
|
192
201
|
if (!fs.existsSync(dir)) return [];
|
|
193
202
|
return fs
|
|
194
203
|
.readdirSync(dir)
|
|
195
204
|
.filter((n) => n.endsWith(".spec.md"))
|
|
196
205
|
.sort()
|
|
197
|
-
.map((n) => ({ rel:
|
|
206
|
+
.map((n) => ({ rel: `${SPEC_MODEL.specsDir}/${n}`, text: fs.readFileSync(path.join(dir, n), "utf8") }));
|
|
198
207
|
}
|
|
199
208
|
|
|
209
|
+
// The scanner's model — the profile's layout and tiers. This runner plants
|
|
210
|
+
// into specs, flows and test sources, so it has to know where they are; it
|
|
211
|
+
// asks the profile, never a constant. A tree with no usable profile has no
|
|
212
|
+
// spec plants to make, and says so per plant rather than guessing.
|
|
213
|
+
const SPEC_MODEL_RESULT = resolveSpecModel(ROOT);
|
|
214
|
+
// REFUSE FIRST. With no manifest there is no layout, so every plant that reads
|
|
215
|
+
// the tree reported an honest-looking absence of files that exist: a project
|
|
216
|
+
// with two specs and nine clauses was told "no spec files — nothing declares
|
|
217
|
+
// behavior to plant against", four lines before the manifest refusal explained
|
|
218
|
+
// the real problem. That is the exact failure harness-manifest.mjs names in its
|
|
219
|
+
// own header as the reason there is no default profile, reproduced inside the
|
|
220
|
+
// instrument built to prove the lane returns. A reader who cannot know the
|
|
221
|
+
// layout says so and stops, rather than describing a tree it cannot see.
|
|
222
|
+
if (!SPEC_MODEL_RESULT.ok) {
|
|
223
|
+
process.stderr.write(`\n${SPEC_MODEL_RESULT.reason}\n`);
|
|
224
|
+
process.exit(2);
|
|
225
|
+
}
|
|
226
|
+
const SPEC_MODEL = SPEC_MODEL_RESULT.model;
|
|
227
|
+
|
|
228
|
+
// The SOURCE this profile's planted citations live in. A citation must sit on
|
|
229
|
+
// a test, so two of the plants have to WRITE a test — in this stack's
|
|
230
|
+
// language, with its test-declaration syntax, at a path it compiles. Everything
|
|
231
|
+
// else about a plant is stack-free: the clause grammar and the citation marker
|
|
232
|
+
// are the core scanner's, and which plants a tree can support is derived from
|
|
233
|
+
// the tree. A profile that declares none simply ships without those two, said
|
|
234
|
+
// out loud per plant (§5.2: no plants, no badge).
|
|
235
|
+
const PROFILE_MODULE = (() => {
|
|
236
|
+
const manifest = resolveHarnessManifest(ROOT);
|
|
237
|
+
if (!manifest.ok) return null;
|
|
238
|
+
const loaded = loadProfileSync(ROOT, manifest.manifest.profile);
|
|
239
|
+
return loaded.ok ? loaded.profile : null;
|
|
240
|
+
})();
|
|
241
|
+
const PLANT_DECL = PROFILE_MODULE?.plants ?? null;
|
|
242
|
+
|
|
243
|
+
// The SOURCE half of that declaration, which the two citation plants cannot do
|
|
244
|
+
// without. Kept separate from the declaration itself because the other half —
|
|
245
|
+
// which step observes which plant (`observedBy`) — costs a pack nothing to give
|
|
246
|
+
// and must not be withheld by the absence of a Kotlin/Python/Go snippet it has
|
|
247
|
+
// not written yet. Requiring all of it or none of it would mean a pack that
|
|
248
|
+
// names its steps but ships no plant source still had cmp's spellings asserted
|
|
249
|
+
// against it, which is the defect this reads the declaration to avoid.
|
|
250
|
+
//
|
|
251
|
+
// THE TEST FOR "ENOUGH TO PLANT WITH" USED TO BE THREE `typeof`s WRITTEN HERE.
|
|
252
|
+
// It moved to qa/lib/plant-calibration.mjs the day the RUNG came to depend on
|
|
253
|
+
// the same answer (NORTH-STAR §8.9 — a profile with no calibrated plants earns
|
|
254
|
+
// no rung): the grader and the instrument asking that question separately is
|
|
255
|
+
// how the same word ends up meaning two things in one tree, which is the
|
|
256
|
+
// `flowCitation`/`scanCitations` episode this file already carries.
|
|
257
|
+
const PROFILE_PLANTS = plantCalibration(PLANT_DECL).ok ? PLANT_DECL : null;
|
|
258
|
+
const PLANTED_TEST_REL = (testDir) => `${testDir}/${PROFILE_PLANTS.testFileBasename}`;
|
|
259
|
+
|
|
200
260
|
function readFlows() {
|
|
201
|
-
|
|
261
|
+
if (!SPEC_MODEL) return [];
|
|
262
|
+
return listFlowFiles(ROOT, SPEC_MODEL).map((rel) => ({ rel, text: fs.readFileSync(abs(rel), "utf8") }));
|
|
202
263
|
}
|
|
203
264
|
|
|
204
265
|
/**
|
|
205
|
-
* A directory that already holds a
|
|
206
|
-
* own citations rather than guessed from a package convention — an
|
|
207
|
-
* project's source layout is not ours to assume.
|
|
266
|
+
* A directory that already holds a host-only-tier test, derived from the
|
|
267
|
+
* tree's own citations rather than guessed from a package convention — an
|
|
268
|
+
* adopted project's source layout is not ours to assume.
|
|
208
269
|
*/
|
|
209
270
|
function findTestDir() {
|
|
271
|
+
if (!SPEC_MODEL) return null;
|
|
210
272
|
let tags = [];
|
|
211
273
|
try {
|
|
212
|
-
tags = scanCitations(ROOT);
|
|
274
|
+
tags = scanCitations(ROOT, SPEC_MODEL);
|
|
213
275
|
} catch {
|
|
214
276
|
return null;
|
|
215
277
|
}
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
278
|
+
const hostOnly = SPEC_MODEL.tiers.hostOnly;
|
|
279
|
+
const cited = tags.find((tag) => hostOnly.includes(tag.tier));
|
|
280
|
+
if (cited) return path.dirname(cited.file);
|
|
281
|
+
// No citations yet: fall back to any source under a citation root that the
|
|
282
|
+
// profile's own tiering places on a host-only tier.
|
|
283
|
+
for (const rootRel of SPEC_MODEL.citationRoots) {
|
|
284
|
+
const hit = walkFiles(path.join(ROOT, ...rootRel.split("/")), SPEC_MODEL.citationExts).find((f) =>
|
|
285
|
+
hostOnly.includes(SPEC_MODEL.tiers.forFile(path.relative(ROOT, f))),
|
|
286
|
+
);
|
|
287
|
+
if (hit) return path.dirname(path.relative(ROOT, hit));
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
221
290
|
}
|
|
222
291
|
|
|
223
|
-
|
|
292
|
+
// `smoke` is the lane's cheapest profile: the steps that need no build tool, no
|
|
293
|
+
// device and no network. Naming the build tool here ("no Gradle") was one
|
|
294
|
+
// stack's word in the instrument that exists to prove this file has none — a
|
|
295
|
+
// small lie, but in the one place that can least afford it.
|
|
296
|
+
out(`framework check: bound=${BOUND_MS}ms per direction, profile=smoke (host-only steps: no build tool, no device, no network)`);
|
|
224
297
|
|
|
298
|
+
// With no plant material the instrument cannot write a test, so the two plants
|
|
299
|
+
// that need one are unavailable — reported by name, never quietly dropped.
|
|
225
300
|
const tree = {
|
|
226
301
|
specs: readSpecs(),
|
|
227
302
|
flows: readFlows(),
|
|
228
303
|
harnessLib: listHarnessFiles(ROOT).filter((rel) => rel.startsWith("qa/lib/")),
|
|
229
|
-
testDir: findTestDir(),
|
|
304
|
+
testDir: PROFILE_PLANTS ? findTestDir() : null,
|
|
305
|
+
// So the skip can name the REAL cause instead of blaming the tree.
|
|
306
|
+
plantsDeclared: Boolean(PROFILE_PLANTS),
|
|
307
|
+
// No `?? "e2e"` anywhere: the tier a plant declares is the profile's or absent.
|
|
308
|
+
unmeetableTier: PROFILE_PLANTS?.unmeetableTier ?? null,
|
|
309
|
+
// WHICH STEP CATCHES WHICH PLANT is the pack's word, exactly as the tier
|
|
310
|
+
// above is. There is no `?? "specCoverage"` here for the same reason there is
|
|
311
|
+
// no `?? "e2e"` there: a default is another stack's name asserted against
|
|
312
|
+
// this one, and lib/framework-check.mjs `observingStep` records what that
|
|
313
|
+
// cost. A pack that names none gets an assertion over the lane.
|
|
314
|
+
observedBy: PLANT_DECL?.observedBy ?? null,
|
|
315
|
+
flowsDir: SPEC_MODEL && SPEC_MODEL.flows ? SPEC_MODEL.flows.dir : null,
|
|
316
|
+
// What a citation LOOKS like is the profile's, never `#`. Without this the
|
|
317
|
+
// selector falls back to DEFAULT_GRAMMAR, which accepts `//` and `#` and so
|
|
318
|
+
// happens to serve C-family and YAML journeys — and silently refuses to
|
|
319
|
+
// select either flow plant for a stack whose comments start `--`, `;` or `%`.
|
|
320
|
+
// A plant that is never selected is a gate nobody is calibrating.
|
|
321
|
+
grammar: SPEC_MODEL ? SPEC_MODEL.grammar : null,
|
|
230
322
|
};
|
|
231
323
|
|
|
232
324
|
const { plants, unavailable } = selectPlants(tree);
|
|
@@ -235,6 +327,50 @@ if (!coverage.ok) die(coverage.reason);
|
|
|
235
327
|
|
|
236
328
|
for (const u of unavailable) out(` ⓘ ${u.kind.padEnd(24)} not planted — ${u.reason}`);
|
|
237
329
|
|
|
330
|
+
// A plant whose step this pack has not named is neither unavailable nor broken:
|
|
331
|
+
// it asserts over the LANE instead of over one row, which is a real gate and a
|
|
332
|
+
// blunter one (qa/lib/framework-check.mjs `observingStep`). It is said out loud
|
|
333
|
+
// for the same reason every unavailable plant says why — and for one more. The
|
|
334
|
+
// whole episode behind `observedBy` is that a requirement nobody can discover is
|
|
335
|
+
// a requirement nobody meets: `harnessIntegrity` was undiscoverable and cost an
|
|
336
|
+
// adopter a lane that could never mint a valid receipt (NORTH-STAR §9.1). A
|
|
337
|
+
// declaration that only appears in a refusal repeats that, one level quieter. So
|
|
338
|
+
// the instrument names it while everything is GREEN, which is the only moment
|
|
339
|
+
// the reader is not already debugging something else.
|
|
340
|
+
for (const p of plants) {
|
|
341
|
+
if (p.step || p.vouching) continue;
|
|
342
|
+
out(
|
|
343
|
+
` ⓘ ${p.kind.padEnd(24)} asserted over the whole lane — this profile's \`plants.observedBy\` names no step ` +
|
|
344
|
+
`for it; naming one narrows the assertion to that row`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── The badge floor, planted (NORTH-STAR §8.9, §6.7; GATE-RULES Rule 1) ─────
|
|
349
|
+
// "A profile with no calibrated plants earns no rung" is a guarantee, and until
|
|
350
|
+
// 2026-09-08 nothing checked it: two scratch adopters differing in exactly one
|
|
351
|
+
// export — one shipping `plants`, one shipping none — both earned L1. The floor
|
|
352
|
+
// now lives in the grader (qa/lib/evidence-level.mjs), and a floor nobody
|
|
353
|
+
// watches fail is a floor that quietly stops being there, so this plants the
|
|
354
|
+
// violation it exists to catch and asserts the refusal.
|
|
355
|
+
//
|
|
356
|
+
// It runs BEFORE the dirty-tree check because it touches nothing: no file, no
|
|
357
|
+
// lane, no receipt. That is also its limit and the line below says so — it
|
|
358
|
+
// proves what the grader WOULD give this ladder, not that a lane ran.
|
|
359
|
+
const badgeFloor = (() => {
|
|
360
|
+
const started = t();
|
|
361
|
+
const resolved = evidenceLadderFor(PROFILE_MODULE);
|
|
362
|
+
const assessed = resolved.ok
|
|
363
|
+
? assessBadgeFloor({ ladder: resolved.ladder, plants: PLANT_DECL })
|
|
364
|
+
: { available: false, reason: resolved.reason };
|
|
365
|
+
return { ...assessed, ms: t() - started };
|
|
366
|
+
})();
|
|
367
|
+
if (badgeFloor.ok === false) die(badgeFloor.reason);
|
|
368
|
+
out(
|
|
369
|
+
badgeFloor.available === false
|
|
370
|
+
? ` ⓘ ${"badge floor".padEnd(24)} not planted — ${badgeFloor.reason}`
|
|
371
|
+
: ` badge floor ${String(badgeFloor.ms).padStart(5)}ms ✓ this profile's ladder grades ${badgeFloor.rung} with its plants and NOTHING without them (grader only — no lane run)`,
|
|
372
|
+
);
|
|
373
|
+
|
|
238
374
|
// ── Refuse to start on a dirty tree ─────────────────────────────────────────
|
|
239
375
|
// Everything this may write, named up front. A file with uncommitted changes is
|
|
240
376
|
// a file whose bytes we would be gambling with if the process died mid-plant.
|
|
@@ -245,7 +381,7 @@ for (const p of plants) {
|
|
|
245
381
|
if (p.target.flow) touched.add(p.target.flow);
|
|
246
382
|
if (p.target.file) touched.add(p.target.file);
|
|
247
383
|
if (p.target.declaration) touched.add(p.target.declaration);
|
|
248
|
-
if (p.target.testDir) touched.add(
|
|
384
|
+
if (p.target.testDir) touched.add(PLANTED_TEST_REL(p.target.testDir));
|
|
249
385
|
}
|
|
250
386
|
|
|
251
387
|
{
|
|
@@ -289,7 +425,7 @@ function plantFiles(plant) {
|
|
|
289
425
|
if (plant.target.flows) rels.push(...plant.target.flows);
|
|
290
426
|
if (plant.target.file) rels.push(plant.target.file);
|
|
291
427
|
if (plant.target.declaration) rels.push(plant.target.declaration);
|
|
292
|
-
if (plant.target.testDir) rels.push(
|
|
428
|
+
if (plant.target.testDir) rels.push(PLANTED_TEST_REL(plant.target.testDir));
|
|
293
429
|
if (plant.target.nestInto) {
|
|
294
430
|
for (const rel of plant.target.flows ?? []) rels.push(`${plant.target.nestInto}/${path.basename(rel)}`);
|
|
295
431
|
}
|
|
@@ -300,9 +436,31 @@ function read(rel) {
|
|
|
300
436
|
return fs.readFileSync(abs(rel), "utf8");
|
|
301
437
|
}
|
|
302
438
|
|
|
303
|
-
/**
|
|
304
|
-
|
|
305
|
-
|
|
439
|
+
/**
|
|
440
|
+
* Every citation line REMOVED — in whatever comment syntax this stack writes.
|
|
441
|
+
*
|
|
442
|
+
* This replaced a line that neutralised `^#\s*SPEC:` and wrote a `#` comment in
|
|
443
|
+
* its place, which is one stack's syntax twice over. It was survivable only
|
|
444
|
+
* while the plant selector was ALSO keyed to `#`: a `//`-citing project had
|
|
445
|
+
* both flow plants skipped, silently, and the two failures cancelled. Now that
|
|
446
|
+
* the selector reads the profile's declared marker, this one would be reached
|
|
447
|
+
* with a marker it cannot match — the citation would stay, the plant would not
|
|
448
|
+
* bite, and the instrument would report a framework defect that is its own.
|
|
449
|
+
*
|
|
450
|
+
* Removing the whole line rather than rewriting it is what makes this
|
|
451
|
+
* language-free: deleting a comment is safe in YAML, in every C-family flow, in
|
|
452
|
+
* Python and in Lua, whereas writing a replacement comment requires knowing the
|
|
453
|
+
* syntax. The marker is matched on the TRIMMED line, agreeing with
|
|
454
|
+
* `scanCitations` and with the selector about indented citations.
|
|
455
|
+
*/
|
|
456
|
+
function stripCitations(text, marker = SPEC_MODEL?.grammar?.citationMarker ?? /\bSPEC:/) {
|
|
457
|
+
// A profile could declare its marker with /g, which makes `.test()` stateful
|
|
458
|
+
// across calls. Rebuild without flags rather than trust the declaration.
|
|
459
|
+
const probe = new RegExp(marker.source);
|
|
460
|
+
return text
|
|
461
|
+
.split("\n")
|
|
462
|
+
.filter((line) => !probe.test(line.trim()))
|
|
463
|
+
.join("\n");
|
|
306
464
|
}
|
|
307
465
|
|
|
308
466
|
function applyPlant(plant) {
|
|
@@ -320,10 +478,7 @@ function applyPlant(plant) {
|
|
|
320
478
|
// the clause exists, the tag exists, and nothing runs.
|
|
321
479
|
const text = read(target.spec);
|
|
322
480
|
write(target.spec, `${text.trimEnd()}\n- **${target.clause}** — Given a planted clause, Then a class-level tag must not count.\n`);
|
|
323
|
-
write(
|
|
324
|
-
`${target.testDir}/${PLANTED_TEST_BASENAME}`,
|
|
325
|
-
`// Planted by qa/framework-check.mjs — reverted automatically.\n\n// SPEC: ${target.clause}\nclass CmpFrameworkCheckPlanted {\n val a = 1\n val b = 2\n val c = 3\n val d = 4\n val e = 5\n val f = 6\n fun helper() {}\n}\n`,
|
|
326
|
-
);
|
|
481
|
+
write(PLANTED_TEST_REL(target.testDir), PROFILE_PLANTS.unboundCitationSource(target.clause));
|
|
327
482
|
break;
|
|
328
483
|
}
|
|
329
484
|
case PLANT_KINDS.TIER_UNMET: {
|
|
@@ -331,12 +486,9 @@ function applyPlant(plant) {
|
|
|
331
486
|
const text = read(target.spec);
|
|
332
487
|
write(
|
|
333
488
|
target.spec,
|
|
334
|
-
`${text.trimEnd()}\n- **${target.clause}** [tier:
|
|
335
|
-
);
|
|
336
|
-
write(
|
|
337
|
-
`${target.testDir}/${PLANTED_TEST_BASENAME}`,
|
|
338
|
-
`// Planted by qa/framework-check.mjs — reverted automatically.\n\nimport kotlin.test.Test\n\nclass CmpFrameworkCheckPlanted {\n // SPEC: ${target.clause}\n @Test\n fun planted() {}\n}\n`,
|
|
489
|
+
`${text.trimEnd()}\n- **${target.clause}** [tier: ${target.unmeetableTier}] — Given a planted clause only the target can observe, Then a host-tier citation cannot satisfy it.\n`,
|
|
339
490
|
);
|
|
491
|
+
write(PLANTED_TEST_REL(target.testDir), PROFILE_PLANTS.tierUnmetCitationSource(target.clause));
|
|
340
492
|
break;
|
|
341
493
|
}
|
|
342
494
|
case PLANT_KINDS.FEATURE_WITHOUT_FLOW: {
|
|
@@ -429,8 +581,15 @@ try {
|
|
|
429
581
|
}
|
|
430
582
|
}
|
|
431
583
|
|
|
584
|
+
// NAME THE ROW THAT ACTUALLY WENT RED, not the one the plant asked for.
|
|
585
|
+
// For a plant whose step this pack did not declare those are different
|
|
586
|
+
// strings — the plant asked for nothing — and "undefined FAIL naming
|
|
587
|
+
// CART-01" is a line that makes a working run look broken. It is also the
|
|
588
|
+
// more useful line in every case: on a pack the reader does not know, this
|
|
589
|
+
// is where they learn which of their own gates is calibrated.
|
|
432
590
|
const named = plant.names.length ? ` naming ${plant.names.join(", ")}` : "";
|
|
433
|
-
|
|
591
|
+
const observed = plantRow(run.receipt.steps, plant)?.name ?? plant.step ?? "the lane";
|
|
592
|
+
out(` FAIL: ${plant.label.padEnd(28)} ${String(run.ms).padStart(5)}ms ✓ ${observed} FAIL${named}`);
|
|
434
593
|
revertPlant(plant);
|
|
435
594
|
}
|
|
436
595
|
|
|
@@ -439,9 +598,21 @@ try {
|
|
|
439
598
|
const first = plants[0];
|
|
440
599
|
applyPlant(first);
|
|
441
600
|
const r = runSmoke();
|
|
442
|
-
revertPlant(first);
|
|
443
601
|
if (!r.receipt || r.receipt.verdict !== "FAIL") die("could not produce a FAIL receipt for the hook check");
|
|
602
|
+
// ASK THE HOOK BEFORE REVERTING. `revertPlant` restores RECEIPT_REL from
|
|
603
|
+
// what was on disk when this run started, so reverting first meant the hook
|
|
604
|
+
// was never shown the FAIL receipt the lane had just written. On a tree with
|
|
605
|
+
// no prior receipt the revert DELETED it and the hook refused because there
|
|
606
|
+
// was no receipt at all — green, for a reason that has nothing to do with
|
|
607
|
+
// this assertion. On a tree where the lane had already run, the revert put
|
|
608
|
+
// the earlier PASS back and the check failed on a working lane. Neither
|
|
609
|
+
// outcome tested "the Stop hook refuses a FAIL receipt". A check that cannot
|
|
610
|
+
// fail for its own reason is the exact defect this instrument exists to
|
|
611
|
+
// catch, which is why it is worth the four lines to say so. Found by the
|
|
612
|
+
// first non-Compose adoption, 2026-09-04. (`die` is safe here: every touched
|
|
613
|
+
// file is restored in the outer finally, plant reverted or not.)
|
|
444
614
|
if (!hookRefuses().refused) die("the Stop hook did not refuse a FAIL receipt");
|
|
615
|
+
revertPlant(first);
|
|
445
616
|
out(` Stop hook refuses a FAIL receipt ✓`);
|
|
446
617
|
}
|
|
447
618
|
|
|
@@ -449,7 +620,19 @@ try {
|
|
|
449
620
|
// synthetic row over this tree's own valid hash, so only the skip is new.
|
|
450
621
|
{
|
|
451
622
|
const green = runSmoke();
|
|
452
|
-
if (!green.receipt || green.receipt.verdict !== "PASS")
|
|
623
|
+
if (!green.receipt || green.receipt.verdict !== "PASS") {
|
|
624
|
+
// NAME THE STEPS. "could not produce a PASS receipt" told a reader that
|
|
625
|
+
// something was wrong and nothing about what, in the one place where the
|
|
626
|
+
// instrument has the whole receipt in hand. Evidence-or-silence applies to
|
|
627
|
+
// the instrument's own refusals too.
|
|
628
|
+
const bad = (green.receipt?.steps ?? [])
|
|
629
|
+
.filter((s) => s.verdict === "FAIL" || s.verdict === "ERROR")
|
|
630
|
+
.map((s) => `${s.name}: ${s.reason ?? s.verdict}`);
|
|
631
|
+
die(
|
|
632
|
+
`could not produce a PASS receipt for the device-tier hook check` +
|
|
633
|
+
(bad.length ? ` — ${bad.join("; ")}` : green.receipt ? ` (verdict ${green.receipt.verdict}, no failing step)` : " (no receipt at all)"),
|
|
634
|
+
);
|
|
635
|
+
}
|
|
453
636
|
const planted = {
|
|
454
637
|
...green.receipt,
|
|
455
638
|
profile: "local",
|
|
@@ -461,10 +644,57 @@ try {
|
|
|
461
644
|
};
|
|
462
645
|
write(RECEIPT_REL, JSON.stringify(planted, null, 2));
|
|
463
646
|
const hook = hookRefuses();
|
|
464
|
-
|
|
465
|
-
|
|
647
|
+
// Stage 0 PR 6c: the refusal is stack-free — ANY step that skipped for an
|
|
648
|
+
// environmental reason blocks done, whatever it is called.
|
|
649
|
+
if (!hook.refused || !/a tier did not run/.test(hook.stderr)) {
|
|
650
|
+
die(`the Stop hook did not refuse a receipt whose tier was skipped for an environmental reason:\n${hook.stderr.slice(-400)}`);
|
|
651
|
+
}
|
|
652
|
+
out(` Stop hook refuses a skipped tier ✓`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// 4b. THE VACUITY FLOOR (ADR-0010) — the one plant that cannot be made in
|
|
656
|
+
// this tree.
|
|
657
|
+
//
|
|
658
|
+
// Its violation is the ABSENCE of the engine, so planting it here would
|
|
659
|
+
// delete the files this instrument is running from: the lane would fail to
|
|
660
|
+
// START rather than fail by name, and a plant that proves the tree is broken
|
|
661
|
+
// proves nothing about the gate. So the region is built in a scratch
|
|
662
|
+
// directory, locked, and the refusal is watched there.
|
|
663
|
+
//
|
|
664
|
+
// The exception is granted for one reason and holds only while that reason
|
|
665
|
+
// does: this is the only gate whose violation removes the instrument that
|
|
666
|
+
// would observe it. A future gate wanting an out-of-tree plant must show the
|
|
667
|
+
// same property, not cite this one.
|
|
668
|
+
{
|
|
669
|
+
const started = Date.now();
|
|
670
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "vacuity-floor-"));
|
|
671
|
+
try {
|
|
672
|
+
fs.mkdirSync(path.join(scratch, "qa"), { recursive: true });
|
|
673
|
+
// A region of declarations and nothing else — ADR-0008's probe, exactly.
|
|
674
|
+
fs.writeFileSync(path.join(scratch, "qa", "verified-surface.json"), '{"surface":["src"]}\n');
|
|
675
|
+
fs.writeFileSync(path.join(scratch, "qa", "harness-manifest.json"), '{"schema":"harness-manifest/1"}\n');
|
|
676
|
+
writeHarnessLock(scratch, { version: "0.0.0-plant" });
|
|
677
|
+
const seen = checkHarnessIntegrity(scratch);
|
|
678
|
+
if (!seen.vacuous || seen.engineFiles !== 0) {
|
|
679
|
+
die(`the vacuity floor did not see an engine-free region: engineFiles=${seen.engineFiles}, vacuous=${seen.vacuous}`);
|
|
680
|
+
}
|
|
681
|
+
if (seen.status !== "intact") {
|
|
682
|
+
die(`the plant is only meaningful while the region reads intact — it read "${seen.status}", so the floor is being proved against the wrong state`);
|
|
683
|
+
}
|
|
684
|
+
// FAILING BY NAME is the requirement (§8.8), and the name has to reach a
|
|
685
|
+
// human. `describeIntegrity` is the shared voice every caller renders, so
|
|
686
|
+
// the plant watches THAT refuse — a flag nobody prints would be a gate
|
|
687
|
+
// nobody sees.
|
|
688
|
+
const said = describeIntegrity(seen);
|
|
689
|
+
if (!/not a lane/.test(said) || /files verified/.test(said)) {
|
|
690
|
+
die(`the vacuity floor is invisible to every caller that renders an integrity result: describeIntegrity said "${said}"`);
|
|
691
|
+
}
|
|
692
|
+
const ms = Date.now() - started;
|
|
693
|
+
cycles.push({ label: "vacuity floor", ms });
|
|
694
|
+
out(` FAIL: region holds no engine ${String(ms).padStart(5)}ms ✓ intact but vacuous, and it SAYS so — ${seen.fileCount} file(s), 0 engine`);
|
|
695
|
+
} finally {
|
|
696
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
466
697
|
}
|
|
467
|
-
out(` Stop hook refuses a skipped device tier ✓`);
|
|
468
698
|
}
|
|
469
699
|
|
|
470
700
|
// 5. Everything reverted, and it passes again — the plants were the only cause.
|
|
@@ -492,6 +722,10 @@ if (asJson) {
|
|
|
492
722
|
verdict: "PASS",
|
|
493
723
|
plants: plants.map((p) => ({ kind: p.kind, label: p.label, step: p.step })),
|
|
494
724
|
unavailable,
|
|
725
|
+
// Reported beside the tree plants, never counted as one: it plants into
|
|
726
|
+
// the GRADER rather than into the tree, and a reader comparing plant
|
|
727
|
+
// counts across runs must not see this one move the number.
|
|
728
|
+
badgeFloor,
|
|
495
729
|
cycles,
|
|
496
730
|
totalMs: cost.totalMs,
|
|
497
731
|
budgetMs: BUDGET_MS,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "harness-manifest/2",
|
|
3
|
+
"profile": { "id": "cmp" },
|
|
4
|
+
"receipt": "qa/evidence/latest.json",
|
|
5
|
+
"architectureDoc": "docs/ARCHITECTURE.md",
|
|
6
|
+
"specs": "specs",
|
|
7
|
+
"citationRoots": ["composeApp/src", "qa/e2e"],
|
|
8
|
+
"approvals": "qa/approvals.json",
|
|
9
|
+
"packs": ["cmp"]
|
|
10
|
+
}
|
package/template/qa/lib/a11y.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// Trees produced before the role/clickable/disabled contract extension are handled
|
|
22
22
|
// gracefully: nodes without `clickable` are simply skipped, never crashed on.
|
|
23
23
|
|
|
24
|
-
import { walk } from "./tree.mjs";
|
|
24
|
+
import { walk } from "./profiles/cmp/tree.mjs";
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Audit a tree for accessibility faults.
|