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,262 @@
|
|
|
1
|
+
// spec-model.mjs — the scanner's view of a stack: what a profile's `layout`
|
|
2
|
+
// and `tiers` declarations become once the core has checked them.
|
|
3
|
+
//
|
|
4
|
+
// qa/lib/spec-coverage.mjs owns the MECHANIC — the clause grammar, the
|
|
5
|
+
// citation binding window, coverage in both directions, tier-must-observe.
|
|
6
|
+
// The profile owns the MODEL — where specs and sources live, what a citation
|
|
7
|
+
// file looks like, which tiers exist and which can observe which promise
|
|
8
|
+
// (docs/NORTH-STAR.md §6; AGNOSTIC-HARNESS-ARCHITECTURE.md §4.2 #1 and #4).
|
|
9
|
+
// This module joins them: it builds one validated `model` object from a
|
|
10
|
+
// profile, and it resolves that model from a project root for the callers
|
|
11
|
+
// that only have a root — the console's Specs bridge, feature-brief's derived
|
|
12
|
+
// doneness, the framework-check runner.
|
|
13
|
+
//
|
|
14
|
+
// Resolution is SYNCHRONOUS on purpose. Every root-only caller is a sync
|
|
15
|
+
// function deep in a sync chain (approvals → feature-brief → scanCitations),
|
|
16
|
+
// and the profile is plain ESM with no top-level await, so it is loaded with
|
|
17
|
+
// `require()` — supported for ESM since Node 20.19 / 22.12 and sharing the
|
|
18
|
+
// module cache with `import()`, so the lane and a sync reader see the same
|
|
19
|
+
// instance. Node 18 and 20.18 are end-of-life; the engines floor says so.
|
|
20
|
+
//
|
|
21
|
+
// Two override rules, both deliberate:
|
|
22
|
+
// - the manifest's layout fields (`specs`, `citationRoots`) override the
|
|
23
|
+
// profile's, field by field — the same semantics the console applies, so
|
|
24
|
+
// an attached foreign repo that told `attach` where its tests are gets the
|
|
25
|
+
// same scan from both readers. A stamped app's manifest carries the
|
|
26
|
+
// profile's own values, so the override is the identity there.
|
|
27
|
+
// - there is NO fallback model. A profile that declares no `layout` or no
|
|
28
|
+
// `tiers` is refused by name at load; a root with no manifest is refused
|
|
29
|
+
// with the command that writes one. A scanner that guessed a layout would
|
|
30
|
+
// report honest-looking absences about files ten characters away.
|
|
31
|
+
//
|
|
32
|
+
// SINGLE SOURCE OF TRUTH: packages/harness/src/lib/spec-model.mjs in the
|
|
33
|
+
// create-cmp repo. Vendored byte-identical into qa/lib/ — edit the package
|
|
34
|
+
// source, then run `node scripts/sync-harness.mjs`.
|
|
35
|
+
|
|
36
|
+
import { resolveHarnessManifest } from "./harness-manifest.mjs";
|
|
37
|
+
import { loadProfileSync } from "./profile-loader.mjs";
|
|
38
|
+
|
|
39
|
+
/** A clause's declared requirement name: `[tier: device]` → "device". One path segment of letters/digits/dashes. */
|
|
40
|
+
export const TIER_NAME_RE = /^[a-z][a-z0-9-]*$/i;
|
|
41
|
+
|
|
42
|
+
function isStringList(v) {
|
|
43
|
+
return Array.isArray(v) && v.length > 0 && v.every((s) => typeof s === "string" && s.trim() !== "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function relPathProblem(field, value) {
|
|
47
|
+
if (typeof value !== "string" || value.trim() === "") return `${field} must be a non-empty string`;
|
|
48
|
+
if (value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value)) return `${field} must be relative to the project root (got "${value}")`;
|
|
49
|
+
if (value.includes("\\")) return `${field} must use "/" separators (got "${value}")`;
|
|
50
|
+
if (value.split("/").some((seg) => seg === "..")) return `${field} may not escape the project root (got "${value}")`;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Every contract violation in a profile's `layout` and `tiers`, or [] when
|
|
56
|
+
* they are usable. Never throws; a caller prints all defects at once.
|
|
57
|
+
* @param {{layout?: unknown, tiers?: unknown}} profile
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* The core's fallback grammar: Kotlin/JVM and JavaScript. It is a FALLBACK and
|
|
62
|
+
* not a default in the approving sense — `SpecModel.grammar.isDefault` records
|
|
63
|
+
* that a profile declared none of it, and the coverage scan says so out loud
|
|
64
|
+
* when nothing binds. A stack whose tests look like anything else declares its
|
|
65
|
+
* own `grammar` export; `create-cmp harness init` seeds one from the language
|
|
66
|
+
* it detects.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* THE GRAMMAR IS DECLARED, NEVER DEFAULTED. Until 2026-09-08 this constant held
|
|
70
|
+
* Kotlin's and JavaScript's test-declaration regexes as the core's FALLBACK, and
|
|
71
|
+
* `cmp` itself never declared a grammar — so every profile that forgot the
|
|
72
|
+
* export was graded with Kotlin's, silently. PATTERN: declaration over
|
|
73
|
+
* inference, the shape tree-sitter uses (one query per language, named
|
|
74
|
+
* captures). WHY IT WORKS: a required field cannot be silently wrong for the
|
|
75
|
+
* author who forgot it — the lane refuses by name. HOW IT FAILS: a copied regex
|
|
76
|
+
* from another language binds nothing or the wrong lines. WHAT WE DO: the Rule
|
|
77
|
+
* 0 instrument plants an unbound citation in THIS language and watches the
|
|
78
|
+
* grammar fail it by name; the coverage diagnostic prints "N markers seen, 0
|
|
79
|
+
* bound". The two fields below are language-neutral and may default.
|
|
80
|
+
*/
|
|
81
|
+
export const GRAMMAR_REQUIRED = Object.freeze(["citationMarker", "testDeclaration", "lineComment"]);
|
|
82
|
+
export const GRAMMAR_DEFAULTS = Object.freeze({ blockComment: null, bindingWindow: 5 });
|
|
83
|
+
|
|
84
|
+
/** A profile may declare a pattern as a RegExp or as a source string. */
|
|
85
|
+
function regexOr(value, fallback) {
|
|
86
|
+
if (value instanceof RegExp) return value;
|
|
87
|
+
if (typeof value === "string" && value.trim()) {
|
|
88
|
+
try {
|
|
89
|
+
return new RegExp(value);
|
|
90
|
+
} catch {
|
|
91
|
+
return fallback;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function specDeclarationProblems(profile) {
|
|
98
|
+
const out = [];
|
|
99
|
+
const layout = profile?.layout;
|
|
100
|
+
const tiers = profile?.tiers;
|
|
101
|
+
if (!layout || typeof layout !== "object") out.push("layout must be an object");
|
|
102
|
+
else {
|
|
103
|
+
const p = relPathProblem("layout.specs", layout.specs);
|
|
104
|
+
if (p) out.push(p);
|
|
105
|
+
if (!isStringList(layout.citationRoots)) out.push("layout.citationRoots must be a non-empty list of project-relative paths");
|
|
106
|
+
else for (const r of layout.citationRoots) { const q = relPathProblem("layout.citationRoots[]", r); if (q) out.push(q); }
|
|
107
|
+
if (!isStringList(layout.citationExts) || !layout.citationExts.every((e) => e.startsWith("."))) out.push('layout.citationExts must be a non-empty list of file extensions, each starting with "."');
|
|
108
|
+
if (layout.sourceRoots != null) {
|
|
109
|
+
if (!isStringList(layout.sourceRoots)) out.push("layout.sourceRoots must be a non-empty list of project-relative paths");
|
|
110
|
+
else for (const r of layout.sourceRoots) { const q = relPathProblem("layout.sourceRoots[]", r); if (q) out.push(q); }
|
|
111
|
+
}
|
|
112
|
+
if (layout.buildDir != null) {
|
|
113
|
+
const q = relPathProblem("layout.buildDir", layout.buildDir);
|
|
114
|
+
if (q) out.push(q);
|
|
115
|
+
}
|
|
116
|
+
if (layout.flows != null) {
|
|
117
|
+
if (typeof layout.flows !== "object") out.push("layout.flows must be null or {dir, exts}");
|
|
118
|
+
else {
|
|
119
|
+
const q = relPathProblem("layout.flows.dir", layout.flows.dir);
|
|
120
|
+
if (q) out.push(q);
|
|
121
|
+
if (!isStringList(layout.flows.exts) || !layout.flows.exts.every((e) => e.startsWith("."))) out.push('layout.flows.exts must be a non-empty list of file extensions, each starting with "."');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!tiers || typeof tiers !== "object") out.push("tiers must be an object");
|
|
126
|
+
else {
|
|
127
|
+
if (!isStringList(tiers.names)) out.push("tiers.names must be a non-empty list of tier names");
|
|
128
|
+
const names = new Set(isStringList(tiers.names) ? tiers.names : []);
|
|
129
|
+
if (!Array.isArray(tiers.hostOnly) || !tiers.hostOnly.every((t) => names.has(t))) out.push("tiers.hostOnly must list tiers from tiers.names");
|
|
130
|
+
if (!tiers.satisfying || typeof tiers.satisfying !== "object") out.push("tiers.satisfying must map a requirement name to the tiers that satisfy it");
|
|
131
|
+
else {
|
|
132
|
+
for (const [req, list] of Object.entries(tiers.satisfying)) {
|
|
133
|
+
if (!TIER_NAME_RE.test(req)) out.push(`tiers.satisfying has an invalid requirement name "${req}"`);
|
|
134
|
+
if (!isStringList(list) || !list.every((t) => names.has(t))) out.push(`tiers.satisfying.${req} must list tiers from tiers.names`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (tiers.journey != null && !names.has(tiers.journey)) out.push("tiers.journey must be one of tiers.names (or null when this stack has no journey tier)");
|
|
138
|
+
if (typeof tiers.forFile !== "function") out.push("tiers.forFile(rel) must be a function returning the citing file's tier");
|
|
139
|
+
}
|
|
140
|
+
const grammar = profile?.grammar;
|
|
141
|
+
if (!grammar || typeof grammar !== "object") {
|
|
142
|
+
out.push("grammar is required — declare citationMarker, testDeclaration and lineComment for this stack's language; the core has no fallback grammar (`create-cmp harness init` seeds one per language)");
|
|
143
|
+
} else {
|
|
144
|
+
for (const f of GRAMMAR_REQUIRED) {
|
|
145
|
+
const v = grammar[f];
|
|
146
|
+
if (!(v instanceof RegExp) && !(typeof v === "string" && v.trim())) out.push(`grammar.${f} is required — a RegExp or a pattern string for this language`);
|
|
147
|
+
else if (typeof v === "string") {
|
|
148
|
+
// A pattern that does not compile used to fall back to Kotlin's. It is a
|
|
149
|
+
// declaration problem, named here, so the lane refuses before it grades.
|
|
150
|
+
try { new RegExp(v); } catch (err) { out.push(`grammar.${f} is not a valid pattern: ${err.message}`); }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the scanner's model from a profile's declarations, applying manifest
|
|
159
|
+
* layout overrides field by field. Pure.
|
|
160
|
+
* @param {{id?: string, layout: object, tiers: object}} profile
|
|
161
|
+
* @param {{specs?: string, citationRoots?: string[]}} [overrides] manifest layout fields
|
|
162
|
+
* @returns {{ok: true, model: SpecModel} | {ok: false, reason: string}}
|
|
163
|
+
*/
|
|
164
|
+
export function specModelFrom(profile, overrides = {}) {
|
|
165
|
+
const problems = specDeclarationProblems(profile);
|
|
166
|
+
if (problems.length) {
|
|
167
|
+
return { ok: false, reason: `profile ${JSON.stringify(profile?.id ?? "?")} declares an unusable layout/tiers: ${problems.join("; ")}` };
|
|
168
|
+
}
|
|
169
|
+
const { layout, tiers } = profile;
|
|
170
|
+
const specsDir = typeof overrides.specs === "string" && overrides.specs.trim() ? overrides.specs : layout.specs;
|
|
171
|
+
const citationRoots = isStringList(overrides.citationRoots) ? [...overrides.citationRoots] : [...layout.citationRoots];
|
|
172
|
+
const flows = layout.flows ? { dir: layout.flows.dir, exts: [...layout.flows.exts] } : null;
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
model: Object.freeze({
|
|
176
|
+
profileId: typeof profile.id === "string" ? profile.id : null,
|
|
177
|
+
specsDir,
|
|
178
|
+
citationRoots: Object.freeze(citationRoots),
|
|
179
|
+
citationExts: Object.freeze([...layout.citationExts]),
|
|
180
|
+
// Absent means "the citation roots are the source roots" — a profile that
|
|
181
|
+
// never distinguished them keeps working, and nothing is invented.
|
|
182
|
+
sourceRoots: Object.freeze(isStringList(layout.sourceRoots) ? [...layout.sourceRoots] : [...citationRoots]),
|
|
183
|
+
flows: flows ? Object.freeze(flows) : null,
|
|
184
|
+
buildDir: typeof layout.buildDir === "string" ? layout.buildDir : null,
|
|
185
|
+
// THE GRAMMAR — what a citation and a test declaration LOOK LIKE in this
|
|
186
|
+
// stack's language. Stage 0 moved names, paths and tier names into the
|
|
187
|
+
// profile and left this behind, which was the more dangerous half: a
|
|
188
|
+
// directory name that is wrong produces a refusal, and a grammar that is
|
|
189
|
+
// wrong produces a WRONG VERDICT. The core's fallbacks match Kotlin/JVM
|
|
190
|
+
// and JavaScript only, so a Python or Go project scanned with them finds
|
|
191
|
+
// every marker and binds none — every clause reads as uncited and the
|
|
192
|
+
// message points at the spec file, which is not the problem. Measured on
|
|
193
|
+
// a real Python adoption, 2026-09-05. Field-by-field override: a profile
|
|
194
|
+
// that declares only `testDeclaration` keeps the rest.
|
|
195
|
+
grammar: Object.freeze({
|
|
196
|
+
citationMarker: regexOr(profile.grammar?.citationMarker, null),
|
|
197
|
+
testDeclaration: regexOr(profile.grammar?.testDeclaration, null),
|
|
198
|
+
typeDeclaration: regexOr(profile.grammar?.typeDeclaration, null),
|
|
199
|
+
lineComment: regexOr(profile.grammar?.lineComment, null),
|
|
200
|
+
blockComment: Object.freeze(
|
|
201
|
+
profile.grammar?.blockComment && typeof profile.grammar.blockComment.open === "string" && typeof profile.grammar.blockComment.close === "string"
|
|
202
|
+
? { open: profile.grammar.blockComment.open, close: profile.grammar.blockComment.close }
|
|
203
|
+
: GRAMMAR_DEFAULTS.blockComment,
|
|
204
|
+
),
|
|
205
|
+
bindingWindow:
|
|
206
|
+
Number.isInteger(profile.grammar?.bindingWindow) && profile.grammar.bindingWindow > 0
|
|
207
|
+
? profile.grammar.bindingWindow
|
|
208
|
+
: GRAMMAR_DEFAULTS.bindingWindow,
|
|
209
|
+
// Always false since 2026-09-08: there is no fallback to be running on.
|
|
210
|
+
// Kept as a field because two readers print it.
|
|
211
|
+
isDefault: false,
|
|
212
|
+
}),
|
|
213
|
+
tiers: Object.freeze({
|
|
214
|
+
names: Object.freeze([...tiers.names]),
|
|
215
|
+
hostOnly: Object.freeze([...tiers.hostOnly]),
|
|
216
|
+
satisfying: Object.freeze(Object.fromEntries(Object.entries(tiers.satisfying).map(([k, v]) => [k, Object.freeze([...v])]))),
|
|
217
|
+
journey: tiers.journey ?? null,
|
|
218
|
+
forFile: (rel) => {
|
|
219
|
+
const t = tiers.forFile(rel);
|
|
220
|
+
return typeof t === "string" && t ? t : "other";
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The model for a project root: manifest → profile (sync) → declarations,
|
|
229
|
+
* with the manifest's layout fields overriding the profile's.
|
|
230
|
+
* @param {string} root
|
|
231
|
+
* @returns {{ok: true, model: SpecModel} | {ok: false, reason: string}}
|
|
232
|
+
*/
|
|
233
|
+
export function resolveSpecModel(root) {
|
|
234
|
+
const manifest = resolveHarnessManifest(root);
|
|
235
|
+
if (!manifest.ok) return { ok: false, reason: manifest.reason };
|
|
236
|
+
const loaded = loadProfileSync(root, manifest.manifest.profile);
|
|
237
|
+
if (!loaded.ok) return { ok: false, reason: loaded.reason };
|
|
238
|
+
return specModelFrom(loaded.profile, { specs: manifest.manifest.specs, citationRoots: manifest.manifest.citationRoots });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* resolveSpecModel for callers with no refusal channel: throws the reason.
|
|
243
|
+
* @param {string} root
|
|
244
|
+
* @returns {SpecModel}
|
|
245
|
+
*/
|
|
246
|
+
export function requireSpecModel(root) {
|
|
247
|
+
const r = resolveSpecModel(root);
|
|
248
|
+
if (!r.ok) throw new Error(r.reason);
|
|
249
|
+
return r.model;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @typedef {object} SpecModel
|
|
254
|
+
* @property {string|null} profileId
|
|
255
|
+
* @property {string} specsDir
|
|
256
|
+
* @property {readonly string[]} citationRoots
|
|
257
|
+
* @property {readonly string[]} citationExts
|
|
258
|
+
* @property {readonly string[]} sourceRoots
|
|
259
|
+
* @property {{dir: string, exts: readonly string[]}|null} flows
|
|
260
|
+
* @property {string|null} buildDir
|
|
261
|
+
* @property {{names: readonly string[], hostOnly: readonly string[], satisfying: Record<string, readonly string[]>, journey: string|null, forFile: (rel: string) => string}} tiers
|
|
262
|
+
*/
|
|
@@ -38,7 +38,16 @@ import fs from "node:fs";
|
|
|
38
38
|
import path from "node:path";
|
|
39
39
|
|
|
40
40
|
export const STEP_CACHE_REL_PATH = "composeApp/build/.cmp-step-cache.json";
|
|
41
|
-
export const STEP_CACHE_SCHEMA = "
|
|
41
|
+
export const STEP_CACHE_SCHEMA = "prooflane-step-cache/1";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Schema names a cache file may carry. Unlike the comments ledger, nothing here
|
|
45
|
+
* is at stake but time: an unrecognised schema makes `loadStepCache` return an
|
|
46
|
+
* EMPTY cache, which is correct — a cache you cannot read is a cache you do not
|
|
47
|
+
* have. Accepting the old name only spares every existing adopter one needless
|
|
48
|
+
* cold rebuild on the run after they upgrade.
|
|
49
|
+
*/
|
|
50
|
+
export const STEP_CACHE_SCHEMAS = Object.freeze(["cmp-step-cache/1", STEP_CACHE_SCHEMA]);
|
|
42
51
|
|
|
43
52
|
function toPosix(p) {
|
|
44
53
|
return p.split(path.sep).join("/");
|
|
@@ -116,7 +125,7 @@ export function computeStepInputsHash(root, inputs) {
|
|
|
116
125
|
export function loadStepCache(root) {
|
|
117
126
|
try {
|
|
118
127
|
const parsed = JSON.parse(fs.readFileSync(path.join(root, STEP_CACHE_REL_PATH), "utf8"));
|
|
119
|
-
if (!parsed || parsed.schema
|
|
128
|
+
if (!parsed || !STEP_CACHE_SCHEMAS.includes(parsed.schema) || typeof parsed.steps !== "object" || parsed.steps === null || Array.isArray(parsed.steps)) {
|
|
120
129
|
return { schema: STEP_CACHE_SCHEMA, steps: {} };
|
|
121
130
|
}
|
|
122
131
|
return { schema: STEP_CACHE_SCHEMA, steps: parsed.steps };
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// the step — a gate that misattributes its own failures corrodes the gates that
|
|
11
11
|
// are right.
|
|
12
12
|
//
|
|
13
|
-
// Pure, so the wording and the rule are testable without
|
|
13
|
+
// Pure, so the wording and the rule are testable without a build tool or a device.
|
|
14
14
|
// (docs/proposals/evidence-economics.md C3, S4.)
|
|
15
15
|
//
|
|
16
16
|
// FOUR VERDICTS. PASS / FAIL / SKIP had no way to say "I could not run": a
|
|
@@ -24,47 +24,6 @@
|
|
|
24
24
|
// TIMEOUT vs FAILED, pytest's error vs failed — the distinction every mature
|
|
25
25
|
// runner makes and this one did not.
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* The androidChecks outcome from Gradle's exit and the JUnit summary.
|
|
29
|
-
*
|
|
30
|
-
* @param {{ok: boolean, out: string}} res the Gradle invocation
|
|
31
|
-
* @param {{tests: number, failures: number, errors: number}|null} summary parsed JUnit
|
|
32
|
-
* results, or null when none were written
|
|
33
|
-
* @param {{gradlew?: string}} [opts]
|
|
34
|
-
* @returns {{verdict: "PASS"|"FAIL"|"ERROR", executed: boolean, reason?: string}}
|
|
35
|
-
*/
|
|
36
|
-
export function androidChecksOutcome(res, summary, { gradlew = "./gradlew" } = {}) {
|
|
37
|
-
const executed = Boolean(summary && summary.tests > 0);
|
|
38
|
-
if (res.ok) return { verdict: "PASS", executed };
|
|
39
|
-
const tail = String(res.out ?? "")
|
|
40
|
-
.split("\n")
|
|
41
|
-
.filter((l) => /FAILED|error:|failed/i.test(l))
|
|
42
|
-
.slice(0, 12)
|
|
43
|
-
.join("\n");
|
|
44
|
-
if (executed) {
|
|
45
|
-
return {
|
|
46
|
-
verdict: "FAIL",
|
|
47
|
-
executed,
|
|
48
|
-
reason:
|
|
49
|
-
`connectedDebugAndroidTest failed (${summary.failures + summary.errors} of ${summary.tests} tests) — ` +
|
|
50
|
-
`an on-device behavior claim is broken. Fix the behavior, not the test:\n${tail}`,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
// ERROR, not FAIL: the step could not execute. A device tier that could not
|
|
54
|
-
// run is not evidence (the lane still FAILs), and going green would be the
|
|
55
|
-
// worse lie — but "your behaviour is broken" is withdrawn, and the receipt
|
|
56
|
-
// can tell a red that measured something from a red that measured nothing.
|
|
57
|
-
return {
|
|
58
|
-
verdict: "ERROR",
|
|
59
|
-
executed,
|
|
60
|
-
reason:
|
|
61
|
-
"connectedDebugAndroidTest DID NOT EXECUTE — the run reported no tests at all, so this step has observed " +
|
|
62
|
-
"nothing about your change and is not accusing it. Usual cause: another adb/Gradle session touching the same " +
|
|
63
|
-
"device (a manual `adb` command, a second lane, a running preview), or an install that never landed. " +
|
|
64
|
-
`Re-run this step alone with nothing else on the device before suspecting the code:\n ${gradlew} :composeApp:connectedDebugAndroidTest --rerun\n${tail}`,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
27
|
/** Thrown by the lane's subprocess helper when a step's deadline passes. */
|
|
69
28
|
export class StepTimeout extends Error {
|
|
70
29
|
constructor(cmd, deadlineMs) {
|
|
@@ -88,15 +47,69 @@ export function spawnTimedOut(res) {
|
|
|
88
47
|
return res.signal === "SIGTERM" && (res.status === null || res.status === undefined);
|
|
89
48
|
}
|
|
90
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The bounds a pack gets when it declares none.
|
|
52
|
+
*
|
|
53
|
+
* These were the rule. "Never under five minutes, never over thirty" is a
|
|
54
|
+
* judgement about how long a build can honestly take before it is wedged, and
|
|
55
|
+
* it was made once, for one toolchain: a cold `xcodebuild`, a cold `cargo test`
|
|
56
|
+
* against an empty target dir, or a Gradle daemon starting on a cold CI runner
|
|
57
|
+
* can each exceed thirty minutes honestly — and the lane killed the step and
|
|
58
|
+
* wrote an ERROR row, which is a wrong verdict about a healthy build.
|
|
59
|
+
*
|
|
60
|
+
* `stepDeadlineMs` always accepted overrides. Nothing supplied them: there was
|
|
61
|
+
* no channel from the profile to the call, so the numbers were unreachable from
|
|
62
|
+
* the only place that knows the stack. That was the defect, not the constants —
|
|
63
|
+
* which is why they survive here as a fallback rather than being replaced by a
|
|
64
|
+
* different guess.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_STEP_DEADLINES = Object.freeze({ floorMs: 5 * 60_000, ceilingMs: 30 * 60_000 });
|
|
67
|
+
|
|
68
|
+
/** A positive, finite number of milliseconds, or null. */
|
|
69
|
+
function msOrNull(v) {
|
|
70
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a pack's declared deadline bounds against the fallback.
|
|
75
|
+
*
|
|
76
|
+
* A malformed declaration is refused into the fallback rather than trusted: a
|
|
77
|
+
* deadline is the one number where believing a bad value either kills a healthy
|
|
78
|
+
* build or lets a wedged one run forever. This mirrors how the spine treats
|
|
79
|
+
* every other pack field — duck-typed and degrading — except that here the
|
|
80
|
+
* degradation is TOLD to the adopter, because `isDefault` reaches the ERROR row.
|
|
81
|
+
*
|
|
82
|
+
* `isDefault` is true only when NOTHING usable was declared. A pack that named
|
|
83
|
+
* one bound has made a choice and is not lectured about the other.
|
|
84
|
+
*
|
|
85
|
+
* @param {{floorMs?: number, ceilingMs?: number}|null|undefined} declared
|
|
86
|
+
* @returns {{floorMs: number, ceilingMs: number, isDefault: boolean}}
|
|
87
|
+
*/
|
|
88
|
+
export function resolveStepDeadlines(declared) {
|
|
89
|
+
const floor = declared && typeof declared === "object" ? msOrNull(declared.floorMs) : null;
|
|
90
|
+
const ceiling = declared && typeof declared === "object" ? msOrNull(declared.ceilingMs) : null;
|
|
91
|
+
return {
|
|
92
|
+
floorMs: floor ?? DEFAULT_STEP_DEADLINES.floorMs,
|
|
93
|
+
ceilingMs: ceiling ?? DEFAULT_STEP_DEADLINES.ceilingMs,
|
|
94
|
+
isDefault: floor === null && ceiling === null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
/**
|
|
92
99
|
* A step's own deadline, from the journal's last measured duration for it:
|
|
93
|
-
* three times what it usually takes, never under
|
|
94
|
-
* daemon is slow, not wedged), never over
|
|
100
|
+
* three times what it usually takes, never under the pack's floor (a cold build
|
|
101
|
+
* daemon is slow, not wedged), never over its ceiling (past that it IS wedged).
|
|
95
102
|
* Unknown steps get the ceiling — a first run is never cut short.
|
|
103
|
+
*
|
|
104
|
+
* A floor above the ceiling is incoherent and the CEILING wins, because `min`
|
|
105
|
+
* is the outer operation: cutting a healthy step short is the failure this
|
|
106
|
+
* mechanism exists to avoid, so an incoherent declaration errs toward waiting.
|
|
107
|
+
*
|
|
96
108
|
* @param {number|null|undefined} expectedMs
|
|
109
|
+
* @param {{floorMs?: number, ceilingMs?: number}} [bounds] the pack's, via resolveStepDeadlines
|
|
97
110
|
* @returns {number}
|
|
98
111
|
*/
|
|
99
|
-
export function stepDeadlineMs(expectedMs, { floorMs =
|
|
112
|
+
export function stepDeadlineMs(expectedMs, { floorMs = DEFAULT_STEP_DEADLINES.floorMs, ceilingMs = DEFAULT_STEP_DEADLINES.ceilingMs } = {}) {
|
|
100
113
|
if (!(expectedMs > 0)) return ceilingMs;
|
|
101
114
|
return Math.min(ceilingMs, Math.max(floorMs, Math.round(expectedMs * 3)));
|
|
102
115
|
}
|
|
@@ -106,122 +119,42 @@ export function stepDeadlineMs(expectedMs, { floorMs = 5 * 60_000, ceilingMs = 3
|
|
|
106
119
|
* out of the step's own body (which used to crash the whole lane; now it is
|
|
107
120
|
* one ERROR row and the lane keeps going, because the other steps' verdicts
|
|
108
121
|
* are still worth having).
|
|
122
|
+
* WHERE TO LOOK is the pack's to say, never the spine's: this used to end with
|
|
123
|
+
* "check `./gradlew --status` and `adb devices`", which is confident wrong
|
|
124
|
+
* advice in a repo that has neither. A pack marks a step with `fn.timeoutHint`
|
|
125
|
+
* (the same mechanism as `fn.layer`) and the runner passes it through; with no
|
|
126
|
+
* hint the message says what it honestly knows and stops.
|
|
127
|
+
*
|
|
109
128
|
* @param {string} name the step's display name
|
|
110
129
|
* @param {unknown} err
|
|
111
130
|
* @param {number} durationMs
|
|
131
|
+
* HOW LONG TO WAIT is the pack's too, and when it has not said, the row says
|
|
132
|
+
* so. A killed step used to give an adopter no way to learn the bound existed —
|
|
133
|
+
* let alone that it was another stack's number — so a cold `xcodebuild` looked
|
|
134
|
+
* like a wedged build rather than a knob nobody had turned.
|
|
135
|
+
*
|
|
136
|
+
* @param {{hint?: string, deadlineWasDefault?: boolean}} [opts] `hint` — the pack's own
|
|
137
|
+
* where-to-look sentence; `deadlineWasDefault` — the bound was inherited, so name the knob
|
|
112
138
|
* @returns {{name: string, verdict: "ERROR", reason: string, durationMs: number, details: {executed: false, kind: string}}}
|
|
113
139
|
*/
|
|
114
|
-
export function stepErrorResult(name, err, durationMs) {
|
|
140
|
+
export function stepErrorResult(name, err, durationMs, { hint, deadlineWasDefault = false } = {}) {
|
|
115
141
|
const timeout = err instanceof StepTimeout;
|
|
142
|
+
const where = typeof hint === "string" && hint.trim() ? ` ${hint.trim()}` : "";
|
|
143
|
+
const knob = deadlineWasDefault
|
|
144
|
+
? ` This deadline is the spine's fallback, not a number this stack chose — if the step was healthy and simply slow, declare \`stepDeadlines: { ceilingMs }\` on the step pack.`
|
|
145
|
+
: "";
|
|
116
146
|
const reason = timeout
|
|
117
|
-
? `DID NOT COMPLETE — no result within its deadline (${Math.round(err.deadlineMs / 60000)} min). This step has observed nothing about your change and is not accusing it
|
|
118
|
-
`
|
|
147
|
+
? `DID NOT COMPLETE — no result within its deadline (${Math.round(err.deadlineMs / 60000)} min). This step has observed nothing about your change and is not accusing it.${where}${knob} ` +
|
|
148
|
+
`Re-run the step alone before suspecting the code.
|
|
119
149
|
${err.cmd}`
|
|
120
150
|
: `DID NOT RUN — the step threw before producing a verdict: ${err && err.message ? err.message : String(err)}. ` +
|
|
121
151
|
`Nothing here is a claim about your change.`;
|
|
122
152
|
return { name, verdict: "ERROR", reason, durationMs, details: { executed: false, kind: timeout ? "deadline" : "threw" } };
|
|
123
153
|
}
|
|
124
154
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
* @param {string|null} xml
|
|
132
|
-
* @returns {Array<{flow: string, ok: boolean, message?: string}>|null}
|
|
133
|
-
*/
|
|
134
|
-
export function parseMaestroJunit(xml) {
|
|
135
|
-
if (typeof xml !== "string" || !/<testcase\b/.test(xml)) return null;
|
|
136
|
-
const rows = [];
|
|
137
|
-
const caseRe = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
|
|
138
|
-
let m;
|
|
139
|
-
while ((m = caseRe.exec(xml))) {
|
|
140
|
-
const attrs = m[1] || "";
|
|
141
|
-
const body = m[2] || "";
|
|
142
|
-
const name = (attrs.match(/\bname="([^"]*)"/) || [])[1] ?? (attrs.match(/\bid="([^"]*)"/) || [])[1] ?? "?";
|
|
143
|
-
const status = (attrs.match(/\bstatus="([^"]*)"/) || [])[1];
|
|
144
|
-
const failed = /<(failure|error)\b/.test(body) || (status && !/^(SUCCESS|PASSED?|OK)$/i.test(status));
|
|
145
|
-
const message = failed ? ((body.match(/<(?:failure|error)\b[^>]*message="([^"]*)"/) || [])[1] ?? body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 300)) : undefined;
|
|
146
|
-
rows.push(failed ? { flow: name, ok: false, message } : { flow: name, ok: true });
|
|
147
|
-
}
|
|
148
|
-
return rows.length ? rows : null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* The e2e step's verdict from the Maestro run: exit code + per-flow report +
|
|
153
|
-
* the list of flows the directory held. FAIL names every failing flow; a run
|
|
154
|
-
* whose report lists fewer flows than the directory holds is ERROR — the lane
|
|
155
|
-
* cannot claim flows it has no row for.
|
|
156
|
-
* @param {{ok: boolean, out: string}} res
|
|
157
|
-
* @param {Array<{flow: string, ok: boolean, message?: string}>|null} perFlow
|
|
158
|
-
* @param {string[]} flows root-relative flow files the directory holds
|
|
159
|
-
* @returns {{verdict: "PASS"|"FAIL"|"ERROR", reason?: string, details: object}}
|
|
160
|
-
*/
|
|
161
|
-
export function maestroOutcome(res, perFlow, flows) {
|
|
162
|
-
const details = { flows, results: perFlow ?? undefined };
|
|
163
|
-
if (perFlow) {
|
|
164
|
-
const failed = perFlow.filter((r) => !r.ok);
|
|
165
|
-
if (failed.length) {
|
|
166
|
-
return {
|
|
167
|
-
verdict: "FAIL",
|
|
168
|
-
reason: `Maestro: ${failed.length} of ${perFlow.length} flow${perFlow.length === 1 ? "" : "s"} failed — ${failed.map((r) => `${r.flow}${r.message ? ` (${r.message.split("\n")[0].slice(0, 120)})` : ""}`).join("; ")}`,
|
|
169
|
-
details,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
if (perFlow.length < flows.length) {
|
|
173
|
-
return {
|
|
174
|
-
verdict: "ERROR",
|
|
175
|
-
reason: `Maestro reported ${perFlow.length} flow${perFlow.length === 1 ? "" : "s"} but ${E2E_DIR_LABEL} holds ${flows.length} — the run did not cover every flow, so no verdict can be claimed for the rest`,
|
|
176
|
-
details,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
if (!res.ok) {
|
|
180
|
-
return { verdict: "FAIL", reason: `Maestro exited non-zero with every flow reported green — treat as a run failure:\n${String(res.out).split("\n").slice(-10).join("\n")}`, details };
|
|
181
|
-
}
|
|
182
|
-
return { verdict: "PASS", details };
|
|
183
|
-
}
|
|
184
|
-
if (!res.ok) {
|
|
185
|
-
return { verdict: "FAIL", reason: `Maestro failed (no per-flow report was written):\n${String(res.out).split("\n").slice(-15).join("\n")}`, details };
|
|
186
|
-
}
|
|
187
|
-
return { verdict: "PASS", reason: "Maestro exited 0 but wrote no per-flow report — verdict from the exit code only", details };
|
|
188
|
-
}
|
|
189
|
-
const E2E_DIR_LABEL = "qa/e2e";
|
|
190
|
-
|
|
191
|
-
// ── Device-log incidents, scoped to the app under test ──────────────────────
|
|
192
|
-
/**
|
|
193
|
-
* ANR / fatal-exception lines from `adb logcat -d -b system,crash,main` that
|
|
194
|
-
* belong to one of `appIds` (an app's process may be `pkg` or `pkg:remote`).
|
|
195
|
-
* An emulator carries other apps — on 2026-09-03 the first self-booted lane
|
|
196
|
-
* went red on `ANR in com.karel.bratometer` while driving com.fleet.check —
|
|
197
|
-
* so an incident in another package is NOT this lane's failure. With no
|
|
198
|
-
* appIds known the sweep stays unscoped (every incident counts) and says so.
|
|
199
|
-
* @param {string} log
|
|
200
|
-
* @param {string[]} appIds
|
|
201
|
-
* @returns {{lines: string[], scoped: boolean}}
|
|
202
|
-
*/
|
|
203
|
-
export function deviceLogIncidents(log, appIds = []) {
|
|
204
|
-
const lines = String(log ?? "").split("\n");
|
|
205
|
-
const ids = appIds.filter((id) => typeof id === "string" && id.trim() && id !== "__PACKAGE__");
|
|
206
|
-
const scoped = ids.length > 0;
|
|
207
|
-
const ours = (proc) => !scoped || ids.some((id) => proc === id || proc.startsWith(`${id}:`));
|
|
208
|
-
const out = [];
|
|
209
|
-
lines.forEach((line, i) => {
|
|
210
|
-
const anr = line.match(/ANR in (\S+?)(?:\s|,|$)/);
|
|
211
|
-
if (anr) {
|
|
212
|
-
if (ours(anr[1].replace(/[,)]$/, ""))) out.push(line);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (/FATAL EXCEPTION/i.test(line)) {
|
|
216
|
-
if (!scoped) {
|
|
217
|
-
out.push(line);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
// The crash buffer names the process on one of the next few lines.
|
|
221
|
-
const window = lines.slice(i, i + 8).join("\n");
|
|
222
|
-
const proc = window.match(/Process:\s*(\S+?)(?:,|\s|$)/);
|
|
223
|
-
if (proc && ours(proc[1])) out.push(line);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
return { lines: out, scoped };
|
|
227
|
-
}
|
|
155
|
+
// The Maestro per-flow verdict and the device-log sweep moved to
|
|
156
|
+
// qa/lib/profiles/cmp/maestro.mjs (Stage 0 PR 3); androidChecksOutcome moved to
|
|
157
|
+
// qa/lib/profiles/cmp/android-checks.mjs (Stage 0 PR 6b.2). Facts about an
|
|
158
|
+
// Android device, a Gradle task and an APK are a mobile profile's, not the
|
|
159
|
+
// spine's. What is left here is neutral: deadlines, timeouts, and turning a
|
|
160
|
+
// throw into one ERROR row.
|
package/template/qa/lib/walk.mjs
CHANGED
|
@@ -512,7 +512,7 @@ export function renderInject(data) {
|
|
|
512
512
|
for (const a of arrivals)
|
|
513
513
|
parts.push(`▲ ARRIVED, UNPLANNED — ${a.label} (${a.status}): ${a.reason ?? "no recorded reason"}. Offer: handle now, or after the current walk lands (recommended: after).`);
|
|
514
514
|
parts.push(
|
|
515
|
-
"Protocol: open every reply with the chat header line above, verbatim. Speak stages as Decide·Design·Contract·Build·Prove·Sign-off — with their plain-words gloss on first mention — and clauses as promises. Declare the chain at kickoff and advance it as you go; if the studio line above says DOWN or not running, restore it (preview tool) or surface it before proceeding. Quiet between headers (one line per stage transition). At any human gate, render the full stop card: stage, what it is in plain words, then the easiest act first (the studio console when it is up; the CLI as fallback), then what comes after. Quote the lane's cost only from the measured figure in the card — never estimate it. Never open a second walk silently. Principles that bind THIS turn (docs/PRINCIPLES.md): derived, never claimed — name the command behind any claim; the layer you changed cannot certify itself — run its consumers, stamp a fresh app for a template or harness change; never wait on nothing — every wait is bounded, and if you are blocked, say on what and stop.",
|
|
515
|
+
"Protocol: open every reply with the chat header line above, verbatim. Speak stages as Decide·Design·Contract·Build·Prove·Sign-off — with their plain-words gloss on first mention — and clauses as promises. Declare the chain at kickoff and advance it as you go; if the studio line above says DOWN or not running, restore it (preview tool) or surface it before proceeding. Quiet between headers (one line per stage transition). At any human gate, render the full stop card: stage, what it is in plain words, then the easiest act first (the studio console when it is up; the CLI as fallback), then what comes after. Quote the lane's cost only from the measured figure in the card — never estimate it. Never open a second walk silently. Principles that bind THIS turn (create-cmp's docs/PRINCIPLES.md): derived, never claimed — name the command behind any claim; the layer you changed cannot certify itself — run its consumers, stamp a fresh app for a template or harness change; never wait on nothing — every wait is bounded, and if you are blocked, say on what and stop.",
|
|
516
516
|
);
|
|
517
517
|
return parts.join("\n\n");
|
|
518
518
|
}
|
|
@@ -20,7 +20,7 @@ const previewsDir = resolve(
|
|
|
20
20
|
process.argv[2] || join(HERE, "..", "composeApp", "build", "previews"),
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const { renderTreeSvg } = await import(new URL("./lib/render.mjs", import.meta.url));
|
|
23
|
+
const { renderTreeSvg } = await import(new URL("./lib/profiles/cmp/render.mjs", import.meta.url));
|
|
24
24
|
const { auditA11y } = await import(new URL("./lib/a11y.mjs", import.meta.url));
|
|
25
25
|
|
|
26
26
|
// The app's display name is read at RUNTIME from create-cmp.json, never stamped in.
|