create-cmp-cli 0.23.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 +46 -18
- 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 +747 -0
- package/packages/harness/src/lib/a11y.mjs +1 -1
- package/packages/harness/src/lib/affected-tests.mjs +99 -44
- package/packages/harness/src/lib/agent-hold.mjs +234 -0
- 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 +38 -6
- 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 +778 -0
- 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 +180 -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} +230 -28
- 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/plan.mjs +30 -1
- package/packages/harness/src/preview-gallery.mjs +1 -1
- package/packages/harness/src/receipt-check.mjs +95 -26
- 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 +180 -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 +4 -0
- package/template/qa/approve.mjs +46 -18
- package/template/qa/comments.json +1 -1
- package/template/qa/evidence/schema.json +258 -34
- package/template/qa/framework-check.mjs +747 -0
- 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/agent-hold.mjs +234 -0
- 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 +38 -6
- 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 +778 -0
- 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 +180 -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} +230 -28
- 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/plan.mjs +30 -1
- package/template/qa/preview-gallery.mjs +1 -1
- package/template/qa/receipt-check.mjs +95 -26
- 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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// tarball rather than the working tree.
|
|
7
7
|
//
|
|
8
8
|
// SINGLE SOURCE OF TRUTH: packages/receipts/src/receipt-validate.mjs in the
|
|
9
|
-
// create-cmp repo (the
|
|
9
|
+
// create-cmp repo (the `prooflane-receipts` package). The copy in a generated
|
|
10
10
|
// project's qa/lib/ is vendored byte-identical at scaffold time and pinned by
|
|
11
11
|
// test/receipts-parity.test.mjs — edit the package source, then run
|
|
12
12
|
// `node scripts/sync-harness.mjs`.
|
|
@@ -74,20 +74,31 @@ export function checkLaneVouching(receipt) {
|
|
|
74
74
|
detail: `the receipt's verdict is PASS but ${failed.length} step(s) did not pass: ${names} — the row is the more specific truth`,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
// THE ROW THAT VOUCHES IS THE ROW CARRYING THE VOUCHING DATA, not the row with
|
|
78
|
+
// a particular name. This found a step named exactly `harnessIntegrity` — a
|
|
79
|
+
// name the cmp pack chose, that `REQUIRED_EXPORTS` never mentions, and that a
|
|
80
|
+
// profile author has no way to discover. A green lane whose self-vouching step
|
|
81
|
+
// was spelled `harness_integrity` minted receipts that were invalid FOREVER,
|
|
82
|
+
// in every reader, and the refusal accused the lane of not vouching for
|
|
83
|
+
// itself. The `harness` object on a step row is what the schema already
|
|
84
|
+
// documents as the integrity check's own findings, so it is the honest key.
|
|
85
|
+
// The name is kept as a fallback for receipts written before rows carried it.
|
|
86
|
+
const integrity = steps.find((s) => s && s.harness && typeof s.harness === "object") ?? steps.find((s) => s && s.name === "harnessIntegrity");
|
|
78
87
|
if (!integrity) {
|
|
79
88
|
return {
|
|
80
89
|
ok: false,
|
|
81
|
-
detail:
|
|
90
|
+
detail:
|
|
91
|
+
"no step on this receipt vouches for the lane — no row carries a `harness` object and none is named harnessIntegrity, " +
|
|
92
|
+
"so nothing attests that the lane's own code is the code that ran",
|
|
82
93
|
};
|
|
83
94
|
}
|
|
84
95
|
if (integrity.verdict !== "PASS") {
|
|
85
96
|
return {
|
|
86
97
|
ok: false,
|
|
87
|
-
detail:
|
|
98
|
+
detail: `${integrity.name ?? "the integrity step"} is ${integrity.verdict}, not PASS — the lane did not vouch for itself, so its PASS over the tree cannot be trusted`,
|
|
88
99
|
};
|
|
89
100
|
}
|
|
90
|
-
return { ok: true, detail:
|
|
101
|
+
return { ok: true, detail: `lane vouched for itself (${integrity.name ?? "integrity step"} PASS, no failing rows)` };
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
export function evaluateReceipt(receipt, recompute) {
|
|
@@ -101,6 +112,33 @@ export function evaluateReceipt(receipt, recompute) {
|
|
|
101
112
|
};
|
|
102
113
|
}
|
|
103
114
|
|
|
115
|
+
// WHICH PACK GRADED THIS — ADR-0011. §8.9's comparability rule rests entirely
|
|
116
|
+
// on this field ("a `cmp` L2 and any other pack's L2 are different claims"),
|
|
117
|
+
// and until now no predicate read it: remove `pack` and a receipt kept its
|
|
118
|
+
// rung while losing the only thing that says what the rung is a rung OF.
|
|
119
|
+
//
|
|
120
|
+
// REFUSED, not flagged, and the cost is known. A receipt written before the
|
|
121
|
+
// field existed (2026-09-04) is refused too, because nothing in a receipt can
|
|
122
|
+
// tell "never had one" from "had one, and it was removed" — and of those two
|
|
123
|
+
// errors, accepting tampering is the one a predicate exists to prevent. The
|
|
124
|
+
// remedy is the one the binding check above already offers for the same class
|
|
125
|
+
// of staleness, and costs the same: re-run the lane.
|
|
126
|
+
//
|
|
127
|
+
// This is deliberately NOT the ADR-0007 case. There a label moved and no
|
|
128
|
+
// assertion changed, so invalidating old receipts would have been pure loss.
|
|
129
|
+
// Here the receipt is genuinely missing the field that makes its rung mean
|
|
130
|
+
// something — it is not being punished for a name, it is being asked for a
|
|
131
|
+
// claim it never made.
|
|
132
|
+
if (!receipt.pack || typeof receipt.pack.id !== "string" || receipt.pack.id.length === 0) {
|
|
133
|
+
return {
|
|
134
|
+
valid: false,
|
|
135
|
+
reason:
|
|
136
|
+
`receipt names no step pack — re-run the lane (attesting profile: ${profile ?? "unknown"}). ` +
|
|
137
|
+
`A rung is comparable only within its pack, so a receipt that does not name one cannot be compared to any other`,
|
|
138
|
+
profile,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
104
142
|
if (receipt.verdict === "FAIL") {
|
|
105
143
|
return {
|
|
106
144
|
valid: false,
|
|
@@ -146,12 +184,33 @@ export const DEFAULT_POLICY = {
|
|
|
146
184
|
/** A receipt older than this no longer counts as fresh (hosted check only). */
|
|
147
185
|
maxAgeMs: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
148
186
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
187
|
+
* An absolute wall-time floor for executed (non-SKIP) gates. `null` — OFF by
|
|
188
|
+
* default, and that is a decision rather than an omission.
|
|
189
|
+
*
|
|
190
|
+
* This was 5000, with the reasoning that a PASS receipt summing to less
|
|
191
|
+
* cannot attest a real lane run: the tell for a replayed/cached green or a
|
|
192
|
+
* hand-written verdict. That reasoning holds for a Gradle lane and is FALSE
|
|
193
|
+
* for a Go service, a Rust crate, a Python package or a TypeScript library,
|
|
194
|
+
* whose lanes honestly finish in hundreds of milliseconds. Those adopters
|
|
195
|
+
* were told their evidence was fabricated — the one accusation this product
|
|
196
|
+
* cannot afford to make wrongly.
|
|
197
|
+
*
|
|
198
|
+
* The number was not the defect. ONE receipt carries nothing that could
|
|
199
|
+
* justify any number: no start time, no top-level duration, no baseline —
|
|
200
|
+
* `generatedAt` is a timestamp, not an interval — so nothing on it can be
|
|
201
|
+
* cross-checked against anything else on it. A floor is therefore a fact
|
|
202
|
+
* about the STACK, and this module does not know the stack. It is the
|
|
203
|
+
* notary's to set, from data the notary has and the receipt does not: a
|
|
204
|
+
* lane that has taken thirty seconds every day for a month and today claims
|
|
205
|
+
* forty-two milliseconds is a real finding, and it is a finding about a
|
|
206
|
+
* HISTORY, not about a receipt.
|
|
207
|
+
*
|
|
208
|
+
* What is lost, said plainly: a hand-written receipt claiming small
|
|
209
|
+
* durations is no longer refused here. It was never much of a defence — a
|
|
210
|
+
* forger types a larger number — and every stack-independent check that
|
|
211
|
+
* does catch fabrication is untouched below.
|
|
153
212
|
*/
|
|
154
|
-
minExecutedMs:
|
|
213
|
+
minExecutedMs: null,
|
|
155
214
|
};
|
|
156
215
|
|
|
157
216
|
/**
|
|
@@ -176,9 +235,15 @@ export function checkFreshness(receipt, { now = Date.now(), maxAgeMs = DEFAULT_P
|
|
|
176
235
|
}
|
|
177
236
|
|
|
178
237
|
/**
|
|
179
|
-
* Execution plausibility:
|
|
180
|
-
* real
|
|
181
|
-
*
|
|
238
|
+
* Execution plausibility: did this lane execute anything, and are its numbers
|
|
239
|
+
* real numbers?
|
|
240
|
+
*
|
|
241
|
+
* Three refusals, all stack-independent and all about the SHAPE of the
|
|
242
|
+
* evidence rather than its size: a receipt with no steps, a receipt whose every
|
|
243
|
+
* step is a SKIP or an ERROR (neither measured anything), and a step whose
|
|
244
|
+
* duration is not a finite non-negative number. An absolute wall-time floor is
|
|
245
|
+
* a fourth check and is OFF unless a caller sets `minExecutedMs` — see
|
|
246
|
+
* DEFAULT_POLICY for why a default one is a claim about the stack.
|
|
182
247
|
* @returns {{ok: boolean, detail: string, executedMs?: number, executedSteps?: number}}
|
|
183
248
|
*/
|
|
184
249
|
export function checkExecutionPlausibility(receipt, { minExecutedMs = DEFAULT_POLICY.minExecutedMs } = {}) {
|
|
@@ -200,10 +265,13 @@ export function checkExecutionPlausibility(receipt, { minExecutedMs = DEFAULT_PO
|
|
|
200
265
|
}
|
|
201
266
|
total += step.durationMs;
|
|
202
267
|
}
|
|
203
|
-
|
|
268
|
+
// Applied only when a caller supplies one. The message attributes the floor
|
|
269
|
+
// to whoever set it and states the measurement, rather than asserting that a
|
|
270
|
+
// fast receipt cannot be real — which this module has no way to know.
|
|
271
|
+
if (typeof minExecutedMs === "number" && minExecutedMs > 0 && total < minExecutedMs) {
|
|
204
272
|
return {
|
|
205
273
|
ok: false,
|
|
206
|
-
detail: `
|
|
274
|
+
detail: `executed gates report ${total}ms total, below this validator's configured ${minExecutedMs}ms floor — for a fast stack that may be honest, so treat it as a finding to explain rather than proof of fabrication`,
|
|
207
275
|
executedMs: total,
|
|
208
276
|
executedSteps: executed.length,
|
|
209
277
|
};
|
|
@@ -247,7 +315,7 @@ export function validateReceiptForTree({ root, now = Date.now(), policy = {} } =
|
|
|
247
315
|
if (receipt === null) {
|
|
248
316
|
return {
|
|
249
317
|
status: "missing",
|
|
250
|
-
reason: `no receipt at ${RECEIPT_REL_PATH} — this repo does not carry the
|
|
318
|
+
reason: `no receipt at ${RECEIPT_REL_PATH} — this repo does not carry the prooflane evidence harness (that is not a failure)`,
|
|
251
319
|
checks: [{ id: "receipt-present", ok: false, detail: `no parsable receipt at ${RECEIPT_REL_PATH}` }],
|
|
252
320
|
skips: [],
|
|
253
321
|
};
|
package/src/commands/attach.mjs
CHANGED
|
@@ -19,6 +19,12 @@ import { colors, ok, warn, fail } from "../lib/log.mjs";
|
|
|
19
19
|
import { consent } from "../bootstrap/exec.mjs";
|
|
20
20
|
import { sessionStartCommand } from "../lib/hooks.mjs";
|
|
21
21
|
import { SIDECAR_SUFFIX } from "../lib/harness-upgrade.mjs";
|
|
22
|
+
import {
|
|
23
|
+
MANIFEST_REL_PATH,
|
|
24
|
+
PROFILE_ID_RE,
|
|
25
|
+
manifestFor,
|
|
26
|
+
manifestProblems,
|
|
27
|
+
} from "../../packages/harness/src/lib/harness-manifest.mjs";
|
|
22
28
|
|
|
23
29
|
const COMPOSE_SIGNALS = [
|
|
24
30
|
"org.jetbrains.compose",
|
|
@@ -155,7 +161,7 @@ export function classifyTarget(projectDir) {
|
|
|
155
161
|
* @param {boolean} [params.apply=false]
|
|
156
162
|
* @returns {{units: Array<{relPath:string, action:string}>, notWired: string[]}}
|
|
157
163
|
*/
|
|
158
|
-
export function attachProject({ projectDir, apply = false }) {
|
|
164
|
+
export function attachProject({ projectDir, apply = false, manifest = null }) {
|
|
159
165
|
const target = classifyTarget(projectDir);
|
|
160
166
|
if (!target.ok) throw new Error(target.reason);
|
|
161
167
|
|
|
@@ -163,6 +169,17 @@ export function attachProject({ projectDir, apply = false }) {
|
|
|
163
169
|
{ relPath: "AGENTS.md", content: attachAgentsMd() },
|
|
164
170
|
{ relPath: ".claude/settings.json", content: attachSettings() },
|
|
165
171
|
];
|
|
172
|
+
// The harness manifest — WHICH STACK PROFILE this repo uses, and where its
|
|
173
|
+
// specs, sources and receipt live. The lane refuses to run without one and
|
|
174
|
+
// there is no default (decision 3, 2026-09-04), so for a repo create-cmp
|
|
175
|
+
// never stamped this is the interview's output, landed as one more unit.
|
|
176
|
+
// Only ever offered when the caller resolved one: a manifest already on disk
|
|
177
|
+
// is the repo's own declaration and is never touched here.
|
|
178
|
+
if (manifest) {
|
|
179
|
+
const problems = manifestProblems(manifest);
|
|
180
|
+
if (problems.length) throw new Error(`refusing to write an invalid manifest: ${problems.join("; ")}`);
|
|
181
|
+
files.push({ relPath: MANIFEST_REL_PATH, content: `${JSON.stringify(manifest, null, 2)}\n` });
|
|
182
|
+
}
|
|
166
183
|
const units = [];
|
|
167
184
|
for (const { relPath, content } of files) {
|
|
168
185
|
const abs = path.join(projectDir, relPath);
|
|
@@ -191,11 +208,109 @@ export function attachProject({ projectDir, apply = false }) {
|
|
|
191
208
|
notWired: [
|
|
192
209
|
"headless screen previews (PreviewRegistry + renderScreens) — staged M0b, needs a per-repo Gradle wiring design",
|
|
193
210
|
"live on-device inspector — ships with scaffolded apps; not injectable into a foreign build yet",
|
|
194
|
-
"verify lane / evidence receipts / enforcement — the lane
|
|
211
|
+
"verify lane / evidence receipts / enforcement — the manifest above tells the lane which profile and where things live; vendoring the lane itself into a foreign repo is the next M0b step",
|
|
195
212
|
],
|
|
196
213
|
};
|
|
197
214
|
}
|
|
198
215
|
|
|
216
|
+
/**
|
|
217
|
+
* A manifest from flags alone — the non-interactive path (`--yes`). Pure:
|
|
218
|
+
* returns null when no `--profile` was given, otherwise the manifest with its
|
|
219
|
+
* problems (empty when valid) so the caller decides how to refuse.
|
|
220
|
+
* @param {object} flags
|
|
221
|
+
* @returns {{manifest: object, problems: string[]} | null}
|
|
222
|
+
*/
|
|
223
|
+
export function manifestFromFlags(flags) {
|
|
224
|
+
if (typeof flags.profile !== "string") return null;
|
|
225
|
+
const layout = {};
|
|
226
|
+
if (typeof flags.specs === "string") layout.specs = flags.specs;
|
|
227
|
+
if (typeof flags.receipt === "string") layout.receipt = flags.receipt;
|
|
228
|
+
if (typeof flags["citation-roots"] === "string") {
|
|
229
|
+
const roots = flags["citation-roots"]
|
|
230
|
+
.split(",")
|
|
231
|
+
.map((s) => s.trim())
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
// An empty value is "no override", never an empty list — the lane would
|
|
234
|
+
// refuse `citationRoots: []` and the user would have to decode why.
|
|
235
|
+
if (roots.length) layout.citationRoots = roots;
|
|
236
|
+
}
|
|
237
|
+
const manifest = manifestFor(flags.profile, layout);
|
|
238
|
+
return { manifest, problems: manifestProblems(manifest) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The manifest interview — the questions a foreign repo has to answer before
|
|
243
|
+
* the lane can know what a step or a test root is here. Four questions, each
|
|
244
|
+
* with a default a Compose/KMP repo would accept unchanged; a backend types
|
|
245
|
+
* its own. Validated to the same contract the lane applies before it is
|
|
246
|
+
* written, so the interview can never produce a manifest the lane refuses.
|
|
247
|
+
*/
|
|
248
|
+
async function interviewManifest() {
|
|
249
|
+
let prompts;
|
|
250
|
+
try {
|
|
251
|
+
prompts = (await import("prompts")).default;
|
|
252
|
+
} catch {
|
|
253
|
+
process.stderr.write(
|
|
254
|
+
"The manifest interview needs the 'prompts' package. Re-run with --yes --profile <id> [--specs <dir>] [--citation-roots a,b] [--receipt <path>] for non-interactive use.\n"
|
|
255
|
+
);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
const onCancel = () => {
|
|
259
|
+
process.stdout.write("Cancelled — no manifest written.\n");
|
|
260
|
+
process.exit(1);
|
|
261
|
+
};
|
|
262
|
+
const a = await prompts(
|
|
263
|
+
[
|
|
264
|
+
{
|
|
265
|
+
type: "text",
|
|
266
|
+
name: "profile",
|
|
267
|
+
message: "Stack profile id (a directory under qa/lib/profiles/)",
|
|
268
|
+
initial: "cmp",
|
|
269
|
+
validate: (v) => (PROFILE_ID_RE.test(String(v).trim()) ? true : `must match ${PROFILE_ID_RE}`),
|
|
270
|
+
},
|
|
271
|
+
{ type: "text", name: "specs", message: "Where do specs live?", initial: "specs" },
|
|
272
|
+
{ type: "text", name: "citationRoots", message: "Source roots the lane scans for SPEC: citations (comma-separated)", initial: "composeApp/src, qa/e2e" },
|
|
273
|
+
{ type: "text", name: "receipt", message: "Where should the receipt be written?", initial: "qa/evidence/latest.json" },
|
|
274
|
+
],
|
|
275
|
+
{ onCancel }
|
|
276
|
+
);
|
|
277
|
+
return manifestFor(String(a.profile).trim(), {
|
|
278
|
+
specs: String(a.specs).trim(),
|
|
279
|
+
citationRoots: String(a.citationRoots)
|
|
280
|
+
.split(",")
|
|
281
|
+
.map((s) => s.trim())
|
|
282
|
+
.filter(Boolean),
|
|
283
|
+
receipt: String(a.receipt).trim(),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Resolve the manifest `attach` should land, if any: none when one is already
|
|
289
|
+
* on disk (never overwritten here — valid or not, it is the repo's own
|
|
290
|
+
* declaration; a malformed one is the lane's refusal to explain); from flags
|
|
291
|
+
* when `--profile` is given; from the interview otherwise; and a refusal when
|
|
292
|
+
* `--yes` forbids asking and no flags answer.
|
|
293
|
+
*/
|
|
294
|
+
async function manifestForAttach(projectDir, flags) {
|
|
295
|
+
if (fs.existsSync(path.join(projectDir, ...MANIFEST_REL_PATH.split("/")))) return null;
|
|
296
|
+
const fromFlags = manifestFromFlags(flags);
|
|
297
|
+
if (fromFlags) {
|
|
298
|
+
if (fromFlags.problems.length) {
|
|
299
|
+
fail(`the manifest from your flags is invalid: ${fromFlags.problems.join("; ")}`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
return fromFlags.manifest;
|
|
303
|
+
}
|
|
304
|
+
if (flags.yes === true) {
|
|
305
|
+
fail(
|
|
306
|
+
`${MANIFEST_REL_PATH} is missing and --yes forbids asking. Pass --profile <id> ` +
|
|
307
|
+
`[--specs <dir>] [--citation-roots a,b] [--receipt <path>], or run without --yes to be asked.`
|
|
308
|
+
);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
return interviewManifest();
|
|
312
|
+
}
|
|
313
|
+
|
|
199
314
|
/**
|
|
200
315
|
* `create-cmp attach [dir] [--dry-run] [--yes]`
|
|
201
316
|
*/
|
|
@@ -209,9 +324,18 @@ export async function runAttach(flags, positional) {
|
|
|
209
324
|
` project: ${colors.cyan(projectDir)}\n\n`
|
|
210
325
|
);
|
|
211
326
|
|
|
327
|
+
// Classify BEFORE asking anything: a stamped app or a non-Gradle tree is
|
|
328
|
+
// refused with its reason, not interviewed and then refused.
|
|
329
|
+
const target = classifyTarget(projectDir);
|
|
330
|
+
if (!target.ok) {
|
|
331
|
+
fail(target.reason);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
const manifest = await manifestForAttach(projectDir, flags);
|
|
335
|
+
|
|
212
336
|
let plan;
|
|
213
337
|
try {
|
|
214
|
-
plan = attachProject({ projectDir, apply: false });
|
|
338
|
+
plan = attachProject({ projectDir, apply: false, manifest });
|
|
215
339
|
} catch (e) {
|
|
216
340
|
fail(e.message);
|
|
217
341
|
process.exit(1);
|
|
@@ -241,7 +365,7 @@ export async function runAttach(flags, positional) {
|
|
|
241
365
|
process.exit(0);
|
|
242
366
|
}
|
|
243
367
|
|
|
244
|
-
const applied = attachProject({ projectDir, apply: true });
|
|
368
|
+
const applied = attachProject({ projectDir, apply: true, manifest });
|
|
245
369
|
for (const u of applied.units) {
|
|
246
370
|
if (u.action === "written") ok(`wrote ${u.relPath}`);
|
|
247
371
|
if (u.action === "sidecar") warn(`wrote ${u.relPath}${SIDECAR_SUFFIX} (yours untouched)`);
|
package/src/commands/harden.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
checkHarnessIntegrity,
|
|
53
53
|
describeIntegrity,
|
|
54
54
|
} from "../../packages/harness/src/lib/harness-lock.mjs";
|
|
55
|
+
import { writeHarnessSource, HARNESS_PKG_NAME } from "../../packages/harness/src/lib/harness-source.mjs";
|
|
55
56
|
|
|
56
57
|
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
57
58
|
|
|
@@ -152,7 +153,12 @@ export async function hardenProject({ projectDir, templateDir, apply = false, lo
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
const harnessVersion = shippedHarnessVersion();
|
|
155
|
-
if (harnessVersion)
|
|
156
|
+
if (harnessVersion) {
|
|
157
|
+
// Same ordering as the stamp: the record is inside the region the lock
|
|
158
|
+
// hashes, so it exists first (ADR-0008).
|
|
159
|
+
writeHarnessSource(projectDir, { name: HARNESS_PKG_NAME, version: harnessVersion, source: "local" });
|
|
160
|
+
writeHarnessLock(projectDir, { version: harnessVersion });
|
|
161
|
+
}
|
|
156
162
|
|
|
157
163
|
const updated = { ...record, harness: true, engineVersion: currentEngineVersion() };
|
|
158
164
|
fs.writeFileSync(specPath, JSON.stringify(updated, null, 2) + "\n");
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import { consent } from "../bootstrap/exec.mjs";
|
|
|
43
43
|
import { loadRegistry, latestSet, getSet } from "../lib/registry.mjs";
|
|
44
44
|
import { planUpgrade, BACKUP_SUFFIX, sidecarDroppedLines, staleBackupPaths } from "../lib/upgrade.mjs";
|
|
45
45
|
import { writeHarnessLock, checkHarnessIntegrity, describeIntegrity } from "../../packages/harness/src/lib/harness-lock.mjs";
|
|
46
|
+
import { writeHarnessSource, HARNESS_PKG_NAME } from "../../packages/harness/src/lib/harness-source.mjs";
|
|
46
47
|
import { LOCAL_PATCH_PATH, stampBaseWith } from "../lib/harness-upgrade.mjs";
|
|
47
48
|
import { buildTokenMap } from "../lib/tokens.mjs";
|
|
48
49
|
import {
|
|
@@ -409,6 +410,12 @@ async function harnessPlanAndApply({ flags, record, projectDir, targetDir, tmpRo
|
|
|
409
410
|
// version numbers buy. So the lock is rewritten on its own schedule.
|
|
410
411
|
const harnessVersion = shippedHarnessVersion();
|
|
411
412
|
if (harnessVersion) {
|
|
413
|
+
// The provenance record first: it is inside the region the lock hashes, and
|
|
414
|
+
// rewriting it is what makes a version-only upgrade visible in the TREE
|
|
415
|
+
// rather than only in a number (ADR-0008 / Stage 1 criterion D). Without
|
|
416
|
+
// this a stamped app upgraded here moved its lock's `version` and not one
|
|
417
|
+
// digest — the shape the criterion forbids.
|
|
418
|
+
writeHarnessSource(projectDir, { name: HARNESS_PKG_NAME, version: harnessVersion, source: "local" });
|
|
412
419
|
writeHarnessLock(projectDir, { version: harnessVersion });
|
|
413
420
|
ok(`lane locked at ${colors.bold(harnessVersion)} — ${describeIntegrity(checkHarnessIntegrity(projectDir))}`);
|
|
414
421
|
}
|
package/src/lib/minimal.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// additive, idempotent, never clobbering. See src/commands/harden.mjs.
|
|
26
26
|
|
|
27
27
|
import fs from "node:fs";
|
|
28
|
+
import { HARNESS_DECLARATIONS, HARNESS_GENERATED } from "../../packages/harness/src/lib/harness-region.mjs";
|
|
28
29
|
import path from "node:path";
|
|
29
30
|
|
|
30
31
|
import { listHarnessFiles } from "../../packages/harness/src/lib/harness-region.mjs";
|
|
@@ -59,12 +60,33 @@ const IMPORT_SPECIFIER_RE = /(?:from\s+|new URL\(\s*)["'](\.\.?\/[^"']+\.mjs)["'
|
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* The machine-owned files a minimal scaffold keeps: the entry points plus
|
|
62
|
-
* their transitive ./-relative imports, resolved against the tree as stamped
|
|
63
|
+
* their transitive ./-relative imports, resolved against the tree as stamped —
|
|
64
|
+
* PLUS the region members that are not modules and therefore cannot be reached
|
|
65
|
+
* by any import closure.
|
|
66
|
+
*
|
|
67
|
+
* That last clause is not a special case, it is the shape of the region: it
|
|
68
|
+
* holds code AND the files the lane reads about itself (the two declarations)
|
|
69
|
+
* and the record of which harness it carries (`qa/harness-source.json`,
|
|
70
|
+
* ADR-0008). A closure walk can only ever see the code half, so deriving the
|
|
71
|
+
* keep-set from imports alone quietly proposes deleting the other half — and
|
|
72
|
+
* the file it would delete is the one that says what this lane IS.
|
|
73
|
+
*
|
|
63
74
|
* @param {string} projectDir
|
|
64
75
|
* @returns {Set<string>} project-relative posix paths
|
|
65
76
|
*/
|
|
66
77
|
export function laneKeepSet(projectDir) {
|
|
67
78
|
const keep = new Set();
|
|
79
|
+
// The declarations are gated on presence; the generated record is NOT, and
|
|
80
|
+
// the asymmetry is the point. `qa/harness-source.json` is written at STAMP
|
|
81
|
+
// time, so it is absent from the template this set is usually derived against
|
|
82
|
+
// and present in every stamped tree — gating it on presence would produce a
|
|
83
|
+
// keep-set that is right about the template and wrong about the app, which is
|
|
84
|
+
// the only tree the answer is ever used on. A name kept but absent costs
|
|
85
|
+
// nothing: the set decides what NOT to delete.
|
|
86
|
+
for (const rel of HARNESS_DECLARATIONS) {
|
|
87
|
+
if (fs.existsSync(path.join(projectDir, ...rel.split("/")))) keep.add(rel);
|
|
88
|
+
}
|
|
89
|
+
for (const rel of HARNESS_GENERATED) keep.add(rel);
|
|
68
90
|
const queue = MINIMAL_LANE_ENTRY_POINTS.filter((rel) =>
|
|
69
91
|
fs.existsSync(path.join(projectDir, rel))
|
|
70
92
|
);
|
package/src/scaffold.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { validatePackageName } from "./lib/package-name.mjs";
|
|
|
22
22
|
import { buildTokenMap, replaceTokens, replacePathTokens, isBinaryPath, slugifyAppName } from "./lib/tokens.mjs";
|
|
23
23
|
import { isHarnessFile, listHarnessFiles } from "../packages/harness/src/lib/harness-region.mjs";
|
|
24
24
|
import { writeHarnessLock } from "../packages/harness/src/lib/harness-lock.mjs";
|
|
25
|
+
import { writeHarnessSource, HARNESS_PKG_NAME } from "../packages/harness/src/lib/harness-source.mjs";
|
|
25
26
|
import { renamePackageDirs } from "./lib/rename.mjs";
|
|
26
27
|
import {
|
|
27
28
|
stripFeatureBlocks,
|
|
@@ -305,6 +306,13 @@ function writeLaneLock(projectDir) {
|
|
|
305
306
|
// it would attest nothing while creating the very qa/ directory the strip
|
|
306
307
|
// just removed.
|
|
307
308
|
if (listHarnessFiles(projectDir).length === 0) return;
|
|
309
|
+
// PROVENANCE BEFORE THE LOCK (ADR-0008). The record is inside the region, so
|
|
310
|
+
// it must exist before the region is hashed. A stamped app's lane bytes were
|
|
311
|
+
// COPIED FROM A PATH ON THIS MACHINE — the template inside whichever
|
|
312
|
+
// create-cmp is installed here — which is `local` exactly as the enum defines
|
|
313
|
+
// it, and is true however create-cmp itself arrived. Calling it `registry`
|
|
314
|
+
// because the scaffolder came from npx would name the wrong artifact.
|
|
315
|
+
writeHarnessSource(projectDir, { name: HARNESS_PKG_NAME, version, source: "local" });
|
|
308
316
|
writeHarnessLock(projectDir, { version });
|
|
309
317
|
}
|
|
310
318
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"This app is governed by its delivery contract (CLAUDE.md): behavior starts in specs/, done is `node qa/verify.mjs` with a
|
|
9
|
+
"command": "printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"This app is governed by its delivery contract (CLAUDE.md): behavior starts in specs/, done is `node qa/verify.mjs` with a receipt that attests this tree (commit it with the change), approvals gate signed artifacts. The cmp-inspector MCP tools (preview loop, live tier) are the expected eyes \u2014 if they are absent from this session, that is a fault to diagnose (plugin disabled, session predates plugin enablement, or stale plugin copy; see cmp-doctor), not a cue to fall back to screenshots or blind adb.\"}}'"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"type": "command",
|
|
24
|
-
"command": "grep -qE 'connected[A-Za-z]*AndroidTest|maestro test|adb (-s [^ ]+ )?(install|uninstall)' && printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"permissionDecisionReason\":\"Reminder: device evidence is lane-owned and batched. node qa/verify.mjs sequences the device steps once, last, under a machine-global per-serial lease (qa/lib/device-lease.mjs) \u2014 the one device is scarce, slow, and fragile, so device proof is a checkpoint, never an inner loop. Driving it by hand mid-task risks colliding with a running lane (wedged adbd, device offline, false reds, crossed app state). Ad-hoc debugging stays allowed; batch the evidence into the lane.\"}}' || true"
|
|
24
|
+
"command": "grep -qE 'connected[A-Za-z]*AndroidTest|maestro test|adb (-s [^ ]+ )?(install|uninstall)|emulator (-avd|@)|:composeApp:installDebug' && printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"permissionDecisionReason\":\"Reminder: device evidence is lane-owned and batched. node qa/verify.mjs sequences the device steps once, last, under a machine-global per-serial lease (qa/lib/profiles/cmp/device-lease.mjs) \u2014 the one device is scarce, slow, and fragile, so device proof is a checkpoint, never an inner loop. Driving it by hand mid-task risks colliding with a running lane (wedged adbd, device offline, false reds, crossed app state). Ad-hoc debugging stays allowed; batch the evidence into the lane.\"}}' || true"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"type": "command",
|
|
@@ -15,7 +15,7 @@ description: >-
|
|
|
15
15
|
|
|
16
16
|
> Spec-first, deterministic-stamp, gate-proven. The script (`qa/scaffold-feature.mjs`) does the
|
|
17
17
|
> mechanical work — copy the exemplar file set, whole-word identifier rename, anchor
|
|
18
|
-
> injection into the three shared files. You (the AI) only refine spec wording and adapt the
|
|
18
|
+
> injection into the three shared files (four when the inspector shipped — the preview registry too). You (the AI) only refine spec wording and adapt the
|
|
19
19
|
> feature to its real shape. You are not done until `node qa/verify.mjs` PASSes and the receipt
|
|
20
20
|
> is committed — see this project's `CLAUDE.md`.
|
|
21
21
|
|
|
@@ -126,7 +126,7 @@ Then adapt the generated code to match:
|
|
|
126
126
|
registry vocabulary** (`ScreenColumn`/`AppHeader`/`ContentStateContainer`/`ListItemCard`,
|
|
127
127
|
`presentation/components/*.kt`) — adapt the content shape inside `ContentStateContainer`'s
|
|
128
128
|
trailing slot, don't hand-roll a new header/loading state/list row on top of it. If the
|
|
129
|
-
feature's data genuinely needs a component the
|
|
129
|
+
feature's data genuinely needs a component the registry doesn't cover, propose the addition to the
|
|
130
130
|
human explicitly (a new file is a registry change — it invalidates the `components` approval).
|
|
131
131
|
- Update the copied tests (`<Feature>ViewModelTest.kt`, `<Feature>ScreenTest.kt`) to match
|
|
132
132
|
whatever you changed. The gate (step 6) will tell you exactly what you missed — a compile
|
|
@@ -152,12 +152,16 @@ copy-paste artifact. Commit it alongside the feature.
|
|
|
152
152
|
|
|
153
153
|
### 6. Gate
|
|
154
154
|
|
|
155
|
+
**Before the lane — the device journey.** The stamper wrote `qa/e2e/<feature>.yaml` as a placeholder and
|
|
156
|
+
said so; the lane's `e2eCoverage` gate FAILs until that flow is a real journey citing a `<FEATURE>-NN`
|
|
157
|
+
clause it proves. That red is the gate working, not a bug — make the flow real first.
|
|
158
|
+
|
|
155
159
|
```
|
|
156
160
|
node qa/verify.mjs
|
|
157
161
|
```
|
|
158
162
|
|
|
159
163
|
This must PASS. It proves: the spec's seven clauses are all bound to a citing test
|
|
160
|
-
(`specCoverage`), the build compiles, unit tests pass (ViewModel + UseCase + Repository +
|
|
164
|
+
(`specCoverage`), the device journey cites a clause it proves (`e2eCoverage`), the build compiles, unit tests pass (ViewModel + UseCase + Repository +
|
|
161
165
|
fakes), architecture conformance holds (`presentation` doesn't import `data`, the new
|
|
162
166
|
`*Screen.kt` is automation-reachable — a literal `testTag` or `screenTag =` wiring into a
|
|
163
167
|
registry component — the new `*ViewModel.kt` has a matching test, and it references no
|
|
@@ -3,7 +3,7 @@ name: add-repository
|
|
|
3
3
|
description: >-
|
|
4
4
|
Add a data/domain slice ONLY — model, repository interface + impl, use case, and a
|
|
5
5
|
hand-written fake — to this Compose Multiplatform app, cloned deterministically from the
|
|
6
|
-
|
|
6
|
+
project's configured exemplar feature's data layer (`qa/approvals.json`'s `exemplarFeature` — `home` by default). No screen, no ViewModel, no nav route, no spec clauses. Use this
|
|
7
7
|
when the user wants to "add a repository", "add a data source", "model a new entity", "new
|
|
8
8
|
entity with a repository", or names a domain noun they want backed by data before any UI exists
|
|
9
9
|
(e.g. "add a Tag repository", "I need a data source for Bookmarks"). Works with NO create-cmp
|
|
@@ -14,7 +14,7 @@ description: >-
|
|
|
14
14
|
# add-repository — stamp a data/domain slice only
|
|
15
15
|
|
|
16
16
|
> Deterministic-stamp, gate-proven. The script (`qa/scaffold-feature.mjs --preset repository`)
|
|
17
|
-
> does the mechanical work — copy the
|
|
17
|
+
> does the mechanical work — copy the exemplar's data/domain files, whole-word identifier
|
|
18
18
|
> rename, DI injection for the repository + use case only. You (the AI) only customize the
|
|
19
19
|
> entity's fields and its sample data. You are not done until `node qa/verify.mjs` PASSes and the
|
|
20
20
|
> receipt is committed — see this project's `CLAUDE.md`.
|
|
@@ -3,7 +3,7 @@ name: add-screen
|
|
|
3
3
|
description: >-
|
|
4
4
|
Add a presentation-only slice — Screen + ViewModel + tests + golden tree + spec + nav route —
|
|
5
5
|
for an EXISTING entity/repository in this Compose Multiplatform app, cloned deterministically
|
|
6
|
-
from the
|
|
6
|
+
from the project's configured exemplar feature's presentation layer (`qa/approvals.json`'s `exemplarFeature` — `home` by default). Requires the entity's data layer (model,
|
|
7
7
|
repository, use case, fake) to already exist. Use this when the user wants to "add a screen",
|
|
8
8
|
"add a screen for an existing entity", "add a screen backed by a repository I already have",
|
|
9
9
|
"put a UI on this data", or names an existing domain noun they want a screen for (e.g. "add a
|
|
@@ -86,7 +86,7 @@ failure.
|
|
|
86
86
|
|
|
87
87
|
### 4. Refine the spec, then the behavior
|
|
88
88
|
|
|
89
|
-
Rewrite the clause prose in `specs/<feature>.spec.md` for the entity's real behavior — the
|
|
89
|
+
Rewrite the clause prose in `specs/<feature>.spec.md` for the entity's real behavior — the seven
|
|
90
90
|
clause **ids stay fixed** (specCoverage binds tests to ids, not prose). Propose the rewritten
|
|
91
91
|
clauses to the human; get them confirmed before moving on.
|
|
92
92
|
|
|
@@ -114,12 +114,16 @@ it alongside the feature.
|
|
|
114
114
|
|
|
115
115
|
### 6. Gate
|
|
116
116
|
|
|
117
|
+
**Before the lane — the device journey.** The stamper wrote `qa/e2e/<feature>.yaml` as a placeholder and
|
|
118
|
+
said so; the lane's `e2eCoverage` gate FAILs until that flow is a real journey citing a `<FEATURE>-NN`
|
|
119
|
+
clause it proves. That red is the gate working, not a bug — make the flow real first.
|
|
120
|
+
|
|
117
121
|
```
|
|
118
122
|
node qa/verify.mjs
|
|
119
123
|
```
|
|
120
124
|
|
|
121
|
-
This must PASS. It proves: the spec's
|
|
122
|
-
(specCoverage — `<FEATURE>-01..07` newly bound), the build compiles, unit tests pass, architecture
|
|
125
|
+
This must PASS. It proves: the spec's seven clauses are all bound to a citing test
|
|
126
|
+
(specCoverage — `<FEATURE>-01..07` newly bound), the device journey cites a clause it proves (e2eCoverage), the build compiles, unit tests pass, architecture
|
|
123
127
|
conformance holds (the screen is automation-reachable via a literal `testTag` or `screenTag =`
|
|
124
128
|
wiring into a registry component, and references no `CircularProgressIndicator`/
|
|
125
129
|
`LinearProgressIndicator` directly), the golden tree matches, and accessibility holds. **Not done
|
|
@@ -13,6 +13,15 @@ if [ ! -f qa/receipt-check.mjs ]; then
|
|
|
13
13
|
exit 0 # not a create-cmp project layout — nothing to gate
|
|
14
14
|
fi
|
|
15
15
|
|
|
16
|
+
# First the half nothing else checks: the receipt must be IN the commit being
|
|
17
|
+
# pushed, not merely on disk. receipt-check reads the working tree by hash and
|
|
18
|
+
# has no opinion about git; this line is what makes "committed receipt" true.
|
|
19
|
+
if ! git ls-files --error-unmatch qa/evidence/latest.json >/dev/null 2>&1 || ! git diff --quiet HEAD -- qa/evidence/latest.json; then
|
|
20
|
+
echo ""
|
|
21
|
+
echo "✗ pre-push blocked — qa/evidence/latest.json is not committed (or differs from HEAD)."
|
|
22
|
+
echo " Commit the receipt with your change, then push again."
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
16
25
|
if node qa/receipt-check.mjs; then
|
|
17
26
|
exit 0
|
|
18
27
|
fi
|