create-cmp-cli 0.11.0 → 0.13.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/README.md +11 -9
- package/bin/create-cmp.mjs +3 -0
- package/package.json +1 -1
- package/src/commands/upgrade.mjs +287 -0
- package/src/lib/harness-upgrade.mjs +364 -0
- package/src/lib/package-name.mjs +72 -0
- package/src/scaffold.mjs +7 -2
- package/template/.claude/settings.json +30 -0
- package/template/CLAUDE.md +51 -6
- package/template/README.md +4 -0
- package/template/composeApp/build.gradle.kts +44 -0
- package/template/composeApp/src/androidDebug/AndroidManifest.xml +9 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/PlatformBehaviorSeamTest.kt +277 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/RuntimeStateSeamTest.kt +308 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/AlarmAsserts.kt +152 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/ConfigControl.kt +124 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/DozeControl.kt +113 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/NetworkControl.kt +137 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/NotificationAsserts.kt +163 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/PermissionControl.kt +132 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/ProcessControl.kt +217 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/Shell.kt +79 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/SystemState.kt +113 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/TimeWarp.kt +114 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +5 -2
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppBottomBar.kt +1 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppButton.kt +1 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppIconButton.kt +1 -1
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +58 -0
- package/template/docs/ARCHITECTURE.md +41 -2
- package/template/docs/TESTING.md +165 -0
- package/template/gradle/libs.versions.toml +15 -0
- package/template/manifest.json +1 -0
- package/template/qa/evidence/schema.json +20 -2
- package/template/qa/lib/affected-tests.mjs +147 -0
- package/template/qa/lib/audit-cadence.mjs +290 -0
- package/template/qa/lib/determinism.mjs +179 -0
- package/template/qa/lib/device-lease.mjs +249 -0
- package/template/qa/lib/evidence-badge.mjs +158 -0
- package/template/qa/lib/evidence-level.mjs +117 -0
- package/template/qa/lib/flight-recorder.mjs +332 -0
- package/template/qa/lib/inputs-hash.mjs +16 -1
- package/template/qa/lib/spec-coverage.mjs +54 -3
- package/template/qa/lib/step-cache.mjs +221 -0
- package/template/qa/receipt-check.mjs +22 -2
- package/template/qa/record-audit.mjs +83 -0
- package/template/qa/retrospective.mjs +51 -0
- package/template/qa/scaffold-feature.mjs +20 -1
- package/template/qa/verify.mjs +934 -57
- package/template/qa/watch.mjs +622 -0
- package/template/specs/app-base.spec.md +11 -0
|
@@ -46,7 +46,25 @@ function evaluate() {
|
|
|
46
46
|
if (receipt === null) {
|
|
47
47
|
return { valid: false, reason: "no receipt — run `node qa/verify.mjs`", profile: undefined };
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
// A fast-mode receipt (verify --fast) is an inner-loop signal, never done
|
|
50
|
+
// evidence — refused here before the hash is even recomputed, so a session
|
|
51
|
+
// can never end on "done" while its evidence trail's last run was --fast.
|
|
52
|
+
if (receipt.mode === "fast") {
|
|
53
|
+
return {
|
|
54
|
+
valid: false,
|
|
55
|
+
reason: "the last verify run was --fast (inner-loop only); run the full lane (`node qa/verify.mjs`) before finishing",
|
|
56
|
+
profile: receipt.profile,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const result = evaluateReceipt(receipt, () => computeInputsHash(ROOT));
|
|
60
|
+
// Surface the receipt's evidence rung (the ladder — qa/lib/evidence-level.mjs)
|
|
61
|
+
// alongside the verdict: the rung is the receipt's own derived field, read
|
|
62
|
+
// verbatim, never recomputed here. Older receipts without it stay valid.
|
|
63
|
+
const level = receipt.evidenceLevel;
|
|
64
|
+
if (level && typeof level === "object" && typeof level.rung === "string") {
|
|
65
|
+
result.evidenceLevel = level;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
const result = evaluate();
|
|
@@ -65,10 +83,12 @@ if (asHook) {
|
|
|
65
83
|
process.exit(0);
|
|
66
84
|
}
|
|
67
85
|
|
|
86
|
+
const rungSuffix = result.evidenceLevel ? ` — evidence ${result.evidenceLevel.rung} · ${result.evidenceLevel.name}` : "";
|
|
87
|
+
|
|
68
88
|
if (asJson) {
|
|
69
89
|
console.log(JSON.stringify(result, null, 2));
|
|
70
90
|
} else if (result.valid) {
|
|
71
|
-
console.log(`VALID — ${result.reason}`);
|
|
91
|
+
console.log(`VALID — ${result.reason}${rungSuffix}`);
|
|
72
92
|
} else {
|
|
73
93
|
console.error(`INVALID — ${result.reason}`);
|
|
74
94
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Record that a cmp-audit of one androidMain subsystem happened — the
|
|
3
|
+
// write half of the audit-cadence report (qa/lib/audit-cadence.mjs).
|
|
4
|
+
//
|
|
5
|
+
// node qa/record-audit.mjs <subsystem> [--by <who-or-what>]
|
|
6
|
+
// node qa/record-audit.mjs --list
|
|
7
|
+
//
|
|
8
|
+
// Recording is a CLAIM — "this subsystem, as of this commit, was audited" —
|
|
9
|
+
// so the entry's sha is derived from HEAD by the library, never passed in,
|
|
10
|
+
// and the write is REFUSED when there is no git history, when the subsystem
|
|
11
|
+
// is not one this app actually has (derived from the tree, printed on
|
|
12
|
+
// refusal), or when the subsystem's files differ from HEAD (the record
|
|
13
|
+
// would name a commit the audited bytes did not match — commit first).
|
|
14
|
+
// Refusal over fabrication, the same stance as qa/approve.mjs.
|
|
15
|
+
//
|
|
16
|
+
// This CLI exists so the audit loop closes mechanically: the release
|
|
17
|
+
// profile's receipt nudges "changed since last audit → cmp-audit <name>",
|
|
18
|
+
// and the auditor's last act is this one command.
|
|
19
|
+
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
import { AUDITS_REL_PATH, ROOT_SUBSYSTEM, androidMainPackageRoot, evaluateAuditCadence, listSubsystems, recordAudit } from "./lib/audit-cadence.mjs";
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
+
|
|
27
|
+
const USAGE = `node qa/record-audit.mjs <subsystem> [--by <who-or-what>]
|
|
28
|
+
|
|
29
|
+
Appends one audit record (subsystem, HEAD sha, ISO timestamp, recorder) to
|
|
30
|
+
${AUDITS_REL_PATH}. The verify lane's release profile reports which
|
|
31
|
+
subsystems changed since their last record. Subsystems are derived from the
|
|
32
|
+
tree: the immediate package directories under the androidMain Kotlin source
|
|
33
|
+
root ("${ROOT_SUBSYSTEM}" for files directly at the package root).
|
|
34
|
+
|
|
35
|
+
--list print the derived subsystems and their audit status
|
|
36
|
+
--by <name> who/what recorded this (default: git user.name)
|
|
37
|
+
--help, -h this usage
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
|
|
42
|
+
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
43
|
+
console.log(USAGE);
|
|
44
|
+
process.exit(args.length === 0 ? 2 : 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (args.includes("--list")) {
|
|
48
|
+
const report = evaluateAuditCadence(ROOT);
|
|
49
|
+
if (!report.ok) {
|
|
50
|
+
console.log(report.reason);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
console.log(`androidMain subsystems under ${report.packageRoot} (${report.summary}):`);
|
|
54
|
+
for (const s of report.subsystems) {
|
|
55
|
+
const when = s.audit?.at ? ` — last audit ${s.audit.at.slice(0, 10)} (${s.audit.sha.slice(0, 7)}, by ${s.audit.by ?? "unknown"})` : "";
|
|
56
|
+
console.log(` ${s.name}: ${s.status}${when}`);
|
|
57
|
+
}
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const byIdx = args.indexOf("--by");
|
|
62
|
+
const by = byIdx >= 0 ? args[byIdx + 1] : undefined;
|
|
63
|
+
if (byIdx >= 0 && !by) {
|
|
64
|
+
console.error("--by needs a value");
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
const positional = args.filter((a, i) => !(byIdx >= 0 && (i === byIdx || i === byIdx + 1)));
|
|
68
|
+
if (positional.length !== 1 || positional[0].startsWith("--")) {
|
|
69
|
+
console.error(`expected exactly one subsystem name — run node qa/record-audit.mjs --help`);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const res = recordAudit(ROOT, { subsystem: positional[0], by });
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
console.error(`refused: ${res.reason}`);
|
|
76
|
+
const pkgRoot = androidMainPackageRoot(ROOT);
|
|
77
|
+
if (pkgRoot.ok) {
|
|
78
|
+
console.error(`derived subsystems: ${listSubsystems(ROOT, pkgRoot.rel).join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
console.log(`recorded: audit of ${res.entry.subsystem} against ${res.sha.slice(0, 7)} (by ${res.entry.by}) → ${AUDITS_REL_PATH}`);
|
|
83
|
+
console.log("commit the ledger with your change — the release profile's receipt reads it.");
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The flight recorder's reader — "did this project drift from its tooling?"
|
|
3
|
+
// answered mechanically, from qa/flight-recorder.jsonl alone.
|
|
4
|
+
//
|
|
5
|
+
// node qa/retrospective.mjs [--json]
|
|
6
|
+
//
|
|
7
|
+
// The verify lane appends one journal line per run (qa/lib/flight-recorder.mjs);
|
|
8
|
+
// this CLI turns those lines into the ten-second report a retrospective
|
|
9
|
+
// starts from: fast vs full ratio, which steps SKIP most and why (verbatim
|
|
10
|
+
// reasons, grouped), whether the device tier is ever actually reached, the
|
|
11
|
+
// longest recorded stretch with no full lane, and which degraded paths fired.
|
|
12
|
+
//
|
|
13
|
+
// HONESTY RULES — the reader is only as good as its refusals:
|
|
14
|
+
// - it states only what the journal recorded; a missing journal is
|
|
15
|
+
// "no flight data recorded yet" and exit 0, never an error, never a
|
|
16
|
+
// fabricated baseline;
|
|
17
|
+
// - a short journal says it is short instead of letting two entries read
|
|
18
|
+
// as a trend;
|
|
19
|
+
// - it never editorializes about the developer — every line is a count or
|
|
20
|
+
// a date about lane runs, and the human draws the conclusions.
|
|
21
|
+
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
import { readFlightJournal, renderFlightReport, summarizeFlightJournal } from "./lib/flight-recorder.mjs";
|
|
26
|
+
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
|
+
const asJson = process.argv.includes("--json");
|
|
29
|
+
|
|
30
|
+
const journal = readFlightJournal(ROOT);
|
|
31
|
+
|
|
32
|
+
if (!journal.exists) {
|
|
33
|
+
if (asJson) console.log(JSON.stringify({ recorded: false }));
|
|
34
|
+
else console.log("no flight data recorded yet — the journal (qa/flight-recorder.jsonl) appears after the first verify-lane run");
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
if (journal.error) {
|
|
38
|
+
console.error(`qa/flight-recorder.jsonl exists but could not be read: ${journal.error}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const summary = summarizeFlightJournal(journal.entries);
|
|
43
|
+
|
|
44
|
+
if (asJson) {
|
|
45
|
+
console.log(JSON.stringify({ recorded: true, malformed: journal.malformed, summary }, null, 2));
|
|
46
|
+
} else {
|
|
47
|
+
for (const line of renderFlightReport(summary, { malformed: journal.malformed })) {
|
|
48
|
+
console.log(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
process.exit(0);
|
|
@@ -370,6 +370,9 @@ function defaultSpec() {
|
|
|
370
370
|
|
|
371
371
|
> Generated by \`scaffold-feature.mjs\` from the \`${SOURCE_f}\` exemplar shape. Refine the clause
|
|
372
372
|
> prose below for ${F}'s real behavior (ids stay fixed) before running the verify lane.
|
|
373
|
+
> Platform-behavior clauses (alarms, notifications, lock screen, audio routing) cannot be
|
|
374
|
+
> proven by any desktop tier — cite them from an instrumented test in
|
|
375
|
+
> \`composeApp/src/androidInstrumentedTest\` (see docs/TESTING.md, "The instrumented tier").
|
|
373
376
|
|
|
374
377
|
- **${F_UPPER}-01** — Given the ${F} screen opens, When ${f} are being loaded, Then a loading
|
|
375
378
|
indicator is shown and no ${f} are visible.
|
|
@@ -553,6 +556,22 @@ const APP_NAV_HOST = path.join(SRC("commonMain"), "presentation/navigation/AppNa
|
|
|
553
556
|
// up with zero hand edits — the same reason we wire nav/DI automatically.
|
|
554
557
|
const PREVIEW_REGISTRY = path.join(SRC("desktopMain"), "inspector/PreviewRegistry.kt");
|
|
555
558
|
|
|
559
|
+
// ARCH-14: ViewModels are registered with EXPLICIT `viewModel { … }` factories, never
|
|
560
|
+
// reflection-based `viewModelOf` (it silently ignores Kotlin constructor default
|
|
561
|
+
// parameter values — a compile-time wiring error becomes a runtime resolution crash).
|
|
562
|
+
// The stamped registration is cloned from the EXEMPLAR's own factory line in
|
|
563
|
+
// AppModule.kt (rename applied), so its get() arity always matches the cloned
|
|
564
|
+
// ViewModel's constructor whatever shape the configured exemplar has. The canonical
|
|
565
|
+
// single-use-case shape is the fallback when the exemplar's line isn't found.
|
|
566
|
+
function explicitViewModelRegistration(appModuleContent) {
|
|
567
|
+
const exemplarFactory = new RegExp(
|
|
568
|
+
`viewModel\\s*\\{\\s*${escapeRegExp(SOURCE_F)}ViewModel\\([^)]*\\)\\s*\\}`,
|
|
569
|
+
);
|
|
570
|
+
const m = appModuleContent.match(exemplarFactory);
|
|
571
|
+
if (m) return applyRename(m[0].replace(/\s+/g, " "));
|
|
572
|
+
return `viewModel { ${F}ViewModel(get()) }`;
|
|
573
|
+
}
|
|
574
|
+
|
|
556
575
|
// Each step is tagged with the presets it belongs to, same mechanism as
|
|
557
576
|
// FILES above: `repository` gets repo+usecase DI (+ imports) only; `screen`
|
|
558
577
|
// gets viewModel DI (+ import) + nav route + import only; `feature` gets the
|
|
@@ -567,7 +586,7 @@ const ALL_INJECTION_PLANS = [
|
|
|
567
586
|
{ presets: ["feature", "screen"], apply: (c) => injectImport(c, APP_MODULE, `import ${PACKAGE}.presentation.${f}.${F}ViewModel`) },
|
|
568
587
|
{ presets: ["feature", "repository"], apply: (c) => injectAtAnchor(c, APP_MODULE, "di-repositories", `single<${E}Repository> { ${E}RepositoryImpl() }`) },
|
|
569
588
|
{ presets: ["feature", "repository"], apply: (c) => injectAtAnchor(c, APP_MODULE, "di-usecases", `factory { Get${E}sUseCase(get()) }`) },
|
|
570
|
-
{ presets: ["feature", "screen"], apply: (c) => injectAtAnchor(c, APP_MODULE, "di-viewmodels",
|
|
589
|
+
{ presets: ["feature", "screen"], apply: (c) => injectAtAnchor(c, APP_MODULE, "di-viewmodels", explicitViewModelRegistration(c)) },
|
|
571
590
|
],
|
|
572
591
|
},
|
|
573
592
|
{
|