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.
Files changed (145) hide show
  1. package/bin/create-cmp.mjs +33 -3
  2. package/package.json +10 -4
  3. package/packages/harness/package.json +18 -8
  4. package/packages/harness/src/approve.mjs +46 -18
  5. package/packages/harness/src/console/console-data.mjs +117 -0
  6. package/packages/harness/src/console/console-evidence.mjs +122 -0
  7. package/packages/harness/src/console/console-overview.mjs +642 -0
  8. package/packages/harness/src/console/console-shell.mjs +1139 -0
  9. package/packages/harness/src/console/console-tabs.mjs +2828 -0
  10. package/packages/harness/src/console/contrast.mjs +74 -0
  11. package/packages/harness/src/console/preview-service.mjs +1384 -0
  12. package/packages/harness/src/framework-check.mjs +747 -0
  13. package/packages/harness/src/lib/a11y.mjs +1 -1
  14. package/packages/harness/src/lib/affected-tests.mjs +99 -44
  15. package/packages/harness/src/lib/agent-hold.mjs +234 -0
  16. package/packages/harness/src/lib/approvals.mjs +177 -350
  17. package/packages/harness/src/lib/audit-cadence.mjs +1 -1
  18. package/packages/harness/src/lib/comments.mjs +18 -4
  19. package/packages/harness/src/lib/determinism.mjs +275 -14
  20. package/packages/harness/src/lib/evidence-badge.mjs +38 -6
  21. package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
  22. package/packages/harness/src/lib/evidence-level.mjs +144 -59
  23. package/packages/harness/src/lib/feature-brief.mjs +53 -16
  24. package/packages/harness/src/lib/flight-recorder.mjs +72 -8
  25. package/packages/harness/src/lib/framework-check.mjs +778 -0
  26. package/packages/harness/src/lib/harness-lock.mjs +87 -19
  27. package/packages/harness/src/lib/harness-manifest.mjs +208 -0
  28. package/packages/harness/src/lib/harness-region.mjs +106 -2
  29. package/packages/harness/src/lib/harness-source.mjs +142 -0
  30. package/packages/harness/src/lib/inputs-hash.mjs +180 -22
  31. package/packages/harness/src/lib/lane-markers.mjs +78 -0
  32. package/packages/harness/src/lib/lane-narrator.mjs +4 -2
  33. package/packages/harness/src/lib/lane-runner.mjs +72 -6
  34. package/packages/harness/src/lib/plan.mjs +44 -11
  35. package/packages/harness/src/lib/plant-calibration.mjs +135 -0
  36. package/packages/harness/src/lib/profile-loader.mjs +323 -0
  37. package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
  38. package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
  39. package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
  40. package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  41. package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
  42. package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
  43. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  44. package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
  45. package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
  46. package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
  47. package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
  48. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
  49. package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
  50. package/packages/harness/src/lib/receipt-validate.mjs +84 -16
  51. package/packages/harness/src/lib/spec-coverage.mjs +179 -92
  52. package/packages/harness/src/lib/spec-model.mjs +262 -0
  53. package/packages/harness/src/lib/step-cache.mjs +11 -2
  54. package/packages/harness/src/lib/step-outcomes.mjs +84 -151
  55. package/packages/harness/src/lib/walk.mjs +1 -1
  56. package/packages/harness/src/plan.mjs +30 -1
  57. package/packages/harness/src/preview-gallery.mjs +1 -1
  58. package/packages/harness/src/receipt-check.mjs +95 -26
  59. package/packages/harness/src/record-audit.mjs +1 -1
  60. package/packages/harness/src/scaffold-feature.mjs +10 -2
  61. package/packages/harness/src/verify.mjs +222 -142
  62. package/packages/harness/src/watch.mjs +146 -33
  63. package/packages/receipts/package.json +1 -1
  64. package/packages/receipts/src/index.mjs +2 -2
  65. package/packages/receipts/src/inputs-hash.mjs +180 -22
  66. package/packages/receipts/src/receipt-validate.mjs +84 -16
  67. package/src/commands/attach.mjs +128 -4
  68. package/src/commands/harden.mjs +7 -1
  69. package/src/commands/upgrade.mjs +7 -0
  70. package/src/lib/minimal.mjs +23 -1
  71. package/src/scaffold.mjs +8 -0
  72. package/template/.claude/settings.json +2 -2
  73. package/template/.claude/skills/add-feature/SKILL.md +7 -3
  74. package/template/.claude/skills/add-repository/SKILL.md +2 -2
  75. package/template/.claude/skills/add-screen/SKILL.md +8 -4
  76. package/template/.githooks/pre-push +9 -0
  77. package/template/CLAUDE.md +55 -186
  78. package/template/docs/TESTING.md +1 -1
  79. package/template/gitignore +4 -0
  80. package/template/qa/approve.mjs +46 -18
  81. package/template/qa/comments.json +1 -1
  82. package/template/qa/evidence/schema.json +258 -34
  83. package/template/qa/framework-check.mjs +747 -0
  84. package/template/qa/harness-manifest.json +10 -0
  85. package/template/qa/lib/a11y.mjs +1 -1
  86. package/template/qa/lib/affected-tests.mjs +99 -44
  87. package/template/qa/lib/agent-hold.mjs +234 -0
  88. package/template/qa/lib/approvals.mjs +177 -350
  89. package/template/qa/lib/audit-cadence.mjs +1 -1
  90. package/template/qa/lib/comments.mjs +18 -4
  91. package/template/qa/lib/determinism.mjs +275 -14
  92. package/template/qa/lib/evidence-badge.mjs +38 -6
  93. package/template/qa/lib/evidence-ladder.mjs +202 -0
  94. package/template/qa/lib/evidence-level.mjs +144 -59
  95. package/template/qa/lib/feature-brief.mjs +53 -16
  96. package/template/qa/lib/flight-recorder.mjs +72 -8
  97. package/template/qa/lib/framework-check.mjs +778 -0
  98. package/template/qa/lib/harness-lock.mjs +87 -19
  99. package/template/qa/lib/harness-manifest.mjs +208 -0
  100. package/template/qa/lib/harness-region.mjs +106 -2
  101. package/template/qa/lib/harness-source.mjs +142 -0
  102. package/template/qa/lib/inputs-hash.mjs +180 -22
  103. package/template/qa/lib/lane-markers.mjs +78 -0
  104. package/template/qa/lib/lane-narrator.mjs +4 -2
  105. package/template/qa/lib/lane-runner.mjs +72 -6
  106. package/template/qa/lib/plan.mjs +44 -11
  107. package/template/qa/lib/plant-calibration.mjs +135 -0
  108. package/template/qa/lib/profile-loader.mjs +323 -0
  109. package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
  110. package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
  111. package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
  112. package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  113. package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
  114. package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
  115. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  116. package/template/qa/lib/profiles/cmp/index.mjs +47 -0
  117. package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
  118. package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
  119. package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
  120. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
  121. package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
  122. package/template/qa/lib/receipt-validate.mjs +84 -16
  123. package/template/qa/lib/spec-coverage.mjs +179 -92
  124. package/template/qa/lib/spec-model.mjs +262 -0
  125. package/template/qa/lib/step-cache.mjs +11 -2
  126. package/template/qa/lib/step-outcomes.mjs +84 -151
  127. package/template/qa/lib/walk.mjs +1 -1
  128. package/template/qa/plan.mjs +30 -1
  129. package/template/qa/preview-gallery.mjs +1 -1
  130. package/template/qa/receipt-check.mjs +95 -26
  131. package/template/qa/record-audit.mjs +1 -1
  132. package/template/qa/scaffold-feature.mjs +10 -2
  133. package/template/qa/verified-surface.json +17 -0
  134. package/template/qa/verify.mjs +222 -142
  135. package/template/qa/watch.mjs +146 -33
  136. /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  137. /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  138. /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  139. /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  140. /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
  141. /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  142. /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  143. /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  144. /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  145. /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { parseArgs } from "../src/lib/args.mjs";
17
17
 
18
- const COMMANDS = new Set(["create", "doctor", "upgrade", "clean", "verify", "harden", "attach", "help"]);
18
+ const COMMANDS = new Set(["create", "doctor", "upgrade", "clean", "verify", "harden", "attach", "harness", "help"]);
19
19
 
20
20
  async function main() {
21
21
  const argv = process.argv.slice(2);
@@ -63,6 +63,30 @@ async function main() {
63
63
  await runAttach(flags, rest[0]);
64
64
  return;
65
65
  }
66
+ // `harness` takes a subcommand because it is stack-neutral and will grow
67
+ // siblings (`upgrade`, `doctor`) that must not collide with the Compose
68
+ // verbs above. It is also the form that survived the rename it predicted:
69
+ // since 2026-09-08 the implementation lives in the harness package and its
70
+ // own binary spells these `prooflane init` / `prooflane relock`. This
71
+ // dispatch DELEGATES rather than duplicating — `invocation` is the only
72
+ // difference, and it decides which command names the output prints back.
73
+ case "harness": {
74
+ const sub = rest[0];
75
+ if (sub === "init") {
76
+ const { runHarnessInit } = await import("../packages/harness/install/init.mjs");
77
+ process.exit((await runHarnessInit(flags, rest[1], { invocation: "create-cmp" })) ?? 0);
78
+ }
79
+ if (sub === "relock") {
80
+ const { runHarnessRelock } = await import("../packages/harness/install/relock.mjs");
81
+ process.exit((await runHarnessRelock(flags, rest[1], { invocation: "create-cmp" })) ?? 0);
82
+ }
83
+ process.stderr.write(
84
+ `create-cmp harness: unknown subcommand ${JSON.stringify(sub ?? "")}\n` +
85
+ ` usage: create-cmp harness init [--profile <id>] [--target-dir <dir>] [--dry-run]\n` +
86
+ ` create-cmp harness relock [--target-dir <dir>] [--dry-run]\n`
87
+ );
88
+ process.exit(2);
89
+ }
66
90
  case "create":
67
91
  default: {
68
92
  const { runCreate } = await import("../src/commands/create.mjs");
@@ -85,7 +109,9 @@ function printHelp() {
85
109
  ` npx create-cmp clean ~/.konan + Gradle build-output hygiene (consent-gated)\n` +
86
110
  ` npx create-cmp verify run the green-build gate on an existing project\n` +
87
111
  ` npx create-cmp harden install the full harness into a --minimal scaffold\n` +
88
- ` npx create-cmp attach wire the agent contract into an EXISTING Compose/KMP repo\n\n` +
112
+ ` npx create-cmp attach wire the agent contract into an EXISTING Compose/KMP repo\n` +
113
+ ` npx create-cmp harness init install the verify lane into a repo of ANY stack\n` +
114
+ ` npx create-cmp harness relock re-take the lock after editing YOUR profile or declarations\n\n` +
89
115
  `create (scaffold) flags:\n` +
90
116
  ` --name --package --bundle-id --region --theme-prefix\n` +
91
117
  ` --minimal (light scaffold: app + tests + previews, no verify lane/receipts —\n` +
@@ -103,7 +129,11 @@ function printHelp() {
103
129
  `clean flags: --target-dir <dir> --dry-run --yes\n` +
104
130
  `verify flags: --target-dir <dir> --no-ios --dry-run\n` +
105
131
  `harden flags: --target-dir <dir> --dry-run --yes --verify (run the lane after install)\n` +
106
- `attach flags: --target-dir <dir> --dry-run --yes\n`
132
+ `attach flags: --target-dir <dir> --dry-run --yes\n` +
133
+ `harness init flags: --profile <id> --target-dir <dir> --dry-run\n` +
134
+ `harness relock flags: --target-dir <dir> --dry-run\n` +
135
+ ` (relock covers qa/lib/profiles/<id>/** and qa/{verified-surface,harness-manifest}.json only —\n` +
136
+ ` a machine-owned edit is a fork and is refused by name; \`upgrade --harness\` restores it)\n`
107
137
  );
108
138
  }
109
139
 
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "create-cmp-cli",
3
- "version": "0.23.0",
4
- "description": "Create production mobile apps (Android + iOS, one Kotlin codebase) with AI the delivery harness for Compose Multiplatform, the current generation of cross-platform (Google-backed KMP, iOS stable since May 2025). A deterministic, non-interactive generator that scaffolds a green-building app in minutes, then holds AI-driven changes to a machine-enforced verify lane with a committed evidence receipt. Every app carries a device-free UI preview loop (real screens rendered headlessly on save; changed-screen attribution and compile-error surfacing for coding agents, a live gallery for humans) plus agent-first docs (CLAUDE.md + AGENTS.md). Installs the `create-cmp` command.",
3
+ "version": "0.25.0",
4
+ "description": "Create production mobile apps (Android + iOS, one Kotlin codebase) with AI \u2014 the delivery harness for Compose Multiplatform, the current generation of cross-platform (Google-backed KMP, iOS stable since May 2025). A deterministic, non-interactive generator that scaffolds a green-building app in minutes, then holds AI-driven changes to a machine-enforced verify lane with a committed evidence receipt. Every app carries a device-free UI preview loop (real screens rendered headlessly on save; changed-screen attribution and compile-error surfacing for coding agents, a live gallery for humans) plus agent-first docs (CLAUDE.md + AGENTS.md). Installs the `create-cmp` command.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-cmp": "bin/create-cmp.mjs",
8
8
  "create-cmp-cli": "bin/create-cmp.mjs"
9
9
  },
10
10
  "engines": {
11
- "node": ">=18"
11
+ "node": ">=20.19.0"
12
12
  },
13
13
  "files": [
14
14
  "bin",
@@ -74,5 +74,11 @@
74
74
  "dependencies": {
75
75
  "prompts": "^2.4.2",
76
76
  "picocolors": "^1.1.1"
77
- }
77
+ },
78
+ "workspaces": [
79
+ "packages/harness",
80
+ "packages/receipts",
81
+ "packages/aliases/*",
82
+ "inspector/mcp"
83
+ ]
78
84
  }
@@ -1,33 +1,43 @@
1
1
  {
2
- "name": "@create-cmp/harness",
3
- "version": "0.18.0",
4
- "description": "The create-cmp verify lane the machine-owned harness code every stamped app carries byte-identical: evidence receipts, spec coverage, approvals, conformance reporting, golden trees, a11y, and the preview/inspector libs. Dependency-free ESM, vendored into each generated project so the lane runs offline with no install step, and content-hashed so a receipt can name the exact lane that issued it.",
2
+ "name": "prooflane-harness",
3
+ "version": "0.21.0",
4
+ "description": "The prooflane verify lane: a stack-agnostic evidence harness that derives done from evidence rather than an agent's word. Profile loader, lane runner, receipts, hash-locked region, Stop hook, spec-to-citation binding and the Rule 0/1/2 instruments. Dependency-free ESM, vendored into each project so a receipt is checkable offline from the repo alone, and content-hashed so a receipt names the exact lane that issued it. NOTE: this release still carries the `cmp` (Compose Multiplatform) stack profile as its source of truth; it moves to prooflane-profile-cmp in the package split.",
5
5
  "type": "module",
6
6
  "main": "src/verify.mjs",
7
7
  "exports": {
8
8
  ".": "./src/verify.mjs",
9
9
  "./harness-region": "./src/lib/harness-region.mjs",
10
10
  "./lib/*.mjs": "./src/lib/*.mjs",
11
- "./lib/*": "./src/lib/*.mjs"
11
+ "./lib/*": "./src/lib/*.mjs",
12
+ "./console": "./src/console/preview-service.mjs",
13
+ "./console/*.mjs": "./src/console/*.mjs",
14
+ "./console/*": "./src/console/*.mjs"
15
+ },
16
+ "bin": {
17
+ "prooflane": "bin/prooflane.mjs"
12
18
  },
13
19
  "files": [
14
20
  "src",
21
+ "install",
22
+ "bin",
23
+ "evidence",
15
24
  "README.md"
16
25
  ],
17
26
  "scripts": {
18
27
  "test": "node --test"
19
28
  },
20
29
  "engines": {
21
- "node": ">=18"
30
+ "node": ">=20.19.0"
22
31
  },
23
32
  "keywords": [
24
- "create-cmp",
33
+ "prooflane",
25
34
  "verify",
26
35
  "evidence",
27
36
  "receipt",
28
37
  "harness",
29
- "compose-multiplatform",
30
- "kotlin-multiplatform"
38
+ "stack-agnostic",
39
+ "ci",
40
+ "agents"
31
41
  ],
32
42
  "license": "MIT",
33
43
  "author": "Karel van der Merwe",
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // The approvals CLI — thin shell over qa/lib/approvals.mjs.
3
3
  //
4
- // node qa/approve.mjs <artifact> records approval (recomputes the artifact's
5
- // hash now, stamps the time, writes qa/approvals.json)
4
+ // node qa/approve.mjs <artifact> [<artifact> …] --as "Name <email>"
5
+ // records approval (recomputes each artifact's
6
+ // hash now, stamps the time, writes qa/approvals.json).
7
+ // Sign every pending artifact in ONE command: each
8
+ // signature moves the receipt's input hash, so N
9
+ // separate signings cost N lane re-runs.
6
10
  // node qa/approve.mjs --status lists every governed artifact + live state
7
11
  // (unreviewed / approved / changed-since-approval /
8
12
  // reopened, + mode when set) + short hash
@@ -44,7 +48,7 @@ import {
44
48
  approveArtifact,
45
49
  getApprovalStatuses,
46
50
  getFeatureBoard,
47
- isPackageResolvable,
51
+ isProjectGovernable,
48
52
  listGovernedArtifacts,
49
53
  readJournal,
50
54
  reopenArtifact,
@@ -118,17 +122,16 @@ if (args.includes("--status")) {
118
122
  process.exit(0);
119
123
  }
120
124
 
121
- // Write guard: refuse to RECORD approvals in a tree whose package is not
122
- // resolvable (the raw template / a pre-stamp tree). Approvals belong to a
123
- // generated project; writing qa/approvals.json into the template pollutes the
125
+ // Write guard: refuse to RECORD approvals in a tree that cannot be governed —
126
+ // no manifest, no loadable profile, or a tree the profile itself refuses (the
127
+ // raw template, whose package is still a placeholder). Approvals belong to a
128
+ // real project; writing qa/approvals.json into the template pollutes the
124
129
  // template itself. Read-only --status (above) stays available anywhere. Applies
125
130
  // to every write operation below (single approve, express lane, reopen).
126
131
  function refuseIfUnresolvable() {
127
- if (isPackageResolvable(ROOT)) return;
128
- console.error(
129
- "error: this tree's package is not resolvable (composeApp/build.gradle.kts namespace is missing or still a placeholder) — " +
130
- "this looks like the raw template or a pre-stamp tree. Approvals are recorded in a generated project; refusing to write qa/approvals.json here.",
131
- );
132
+ const v = isProjectGovernable(ROOT);
133
+ if (v.ok) return;
134
+ console.error(`error: ${v.reason}`);
132
135
  process.exit(1);
133
136
  }
134
137
 
@@ -243,7 +246,7 @@ if (acceptFlagIdx !== -1) {
243
246
  if (args.length === 0) {
244
247
  const ids = listGovernedArtifacts(ROOT).map((a) => a.id);
245
248
  console.error(
246
- 'usage: node qa/approve.mjs <artifact> | --status | --log | --accept-defaults | --reopen <artifact> --reason "…" | --reopen-feature <name> --reason "…" | --accept <name>\n' +
249
+ 'usage: node qa/approve.mjs <artifact> --as "Name <email>" | --status | --log | --accept-defaults | --reopen <artifact> --reason "…" | --reopen-feature <name> --reason "…" | --accept <name>\n' +
247
250
  ` valid artifacts: ${ids.length > 0 ? ids.join(", ") : "(none resolved in this project)"}`,
248
251
  );
249
252
  process.exit(1);
@@ -251,7 +254,27 @@ if (args.length === 0) {
251
254
 
252
255
  refuseIfUnresolvable();
253
256
 
254
- const artifactId = args[0];
257
+ // EVERY artifact named in one command — one write, one invalidation.
258
+ //
259
+ // A signature changes qa/approvals.json's `status`, which the approvals gate
260
+ // reads, so it legitimately moves the receipt's input hash. Signed one at a
261
+ // time AFTER a green lane, that means N signatures invalidate the receipt N
262
+ // times and cost N lane re-runs and N bookkeeping commits. Measured in
263
+ // payment-blueprint's log on 2026-09-04: of 21 commits in one session, 8 were
264
+ // "sign the approval" / "bind the receipt" with no product content, and every
265
+ // code change cost two to three commits. It compounds as a repo accumulates
266
+ // governed artifacts, which is what makes a session start fast and grind later.
267
+ //
268
+ // Two halves to the fix: sign them together (here), and sign BEFORE the final
269
+ // lane run so the receipt you keep is already the one that covers the
270
+ // signatures. `--reopen-feature` established the idiom — one recorded change,
271
+ // not N commands.
272
+ const artifactIds = args.filter((a, i) => !a.startsWith("--") && (i === 0 || !args[i - 1].startsWith("--")));
273
+ // An unrecognised flag must still be REFUSED by name, never silently skipped:
274
+ // filtering flags out is how `node qa/approve.mjs --delivr meal` would quietly
275
+ // approve nothing and exit 0. Falling back to the first argument reproduces the
276
+ // exact refusal an unknown verb has always produced.
277
+ if (artifactIds.length === 0) artifactIds.push(args[0]);
255
278
  // The signer is REQUIRED, not optional: see approveArtifact's refusal. Parsed
256
279
  // here rather than defaulted from git config on purpose — `git config user.name`
257
280
  // is whatever the machine says, and an agent running on a developer's laptop
@@ -267,9 +290,14 @@ if (!approvedBy || approvedBy.startsWith("--")) {
267
290
  );
268
291
  process.exit(1);
269
292
  }
270
- const result = approveArtifact(ROOT, artifactId, { via: "cli", approvedBy });
271
- if (!result.ok) {
272
- console.error(`error: ${result.reason}`);
273
- process.exit(1);
293
+ const results = artifactIds.map((id) => ({ id, result: approveArtifact(ROOT, id, { via: "cli", approvedBy }) }));
294
+ const failed = results.filter((r) => !r.result.ok);
295
+ for (const { id, result } of results) {
296
+ if (result.ok) console.log(`✓ approved ${result.artifact} — hash ${shortHash(result.hash)}, at ${result.approvedAt}`);
297
+ else console.error(`error: ${id}: ${result.reason}`);
298
+ }
299
+ if (results.length > 1) {
300
+ const signed = results.length - failed.length;
301
+ console.log(`\n${signed} signature${signed === 1 ? "" : "s"} in one write. Run the lane AFTER signing, not before — a signature moves the receipt's input hash, so signing after a green run costs you that run.`);
274
302
  }
275
- console.log(`✓ approved ${result.artifact} — hash ${shortHash(result.hash)}, at ${result.approvedAt}`);
303
+ if (failed.length) process.exit(1);
@@ -0,0 +1,117 @@
1
+ // console-data.mjs — the pure derivations the console's own section renderers
2
+ // need, and nothing else.
3
+ //
4
+ // MOVED HERE from inspector/mcp/src/lib/ (design-language.mjs and
5
+ // components.mjs) when the console moved into this package — NORTH-STAR §9,
6
+ // stage 0.5, "the console into the harness". A reader coming from either of
7
+ // those files should know why these four functions left them.
8
+ //
9
+ // The reason is the package boundary, not tidiness. `packages/harness` is
10
+ // `prooflane-harness`, whose `files` list ships `src/` and whose whole claim
11
+ // (PACKAGE-SPLIT.md §3) is "the console ... Knows no stack. THIS IS THE
12
+ // PRODUCT." A console module that imported back into inspector/mcp would ship
13
+ // with a dangling relative path — the sibling package is not in the tarball —
14
+ // so the console cannot have a single edge pointing that way, and these were
15
+ // the only three it had.
16
+ //
17
+ // What stayed behind is exactly the stack-coupled half. `getTokenUsage` scans
18
+ // Kotlin source for a declaring `object`; `walkKtFiles`/`getComponentsData`
19
+ // parse `.kt` signatures. Those are the eyes (PACKAGE-SPLIT.md §3,
20
+ // `prooflane-studio-cmp`) and they stay in inspector/mcp. What moved is what a
21
+ // console for ANY stack could use: bucket a dimens catalog by name, compute a
22
+ // WCAG pair table from a colour catalog, and spell a component's story id.
23
+ // None of them reads a file or names a path.
24
+ //
25
+ // The originals are re-exports now, so every existing import site and both
26
+ // unit tests still resolve — one definition, two doors.
27
+
28
+ import { contrastRatio } from "./contrast.mjs";
29
+
30
+ // --- component story ids (was components.mjs) ---------------------------------
31
+
32
+ /**
33
+ * PascalCase/camelCase → kebab-case: AppHeader → app-header, ListItemCard →
34
+ * list-item-card. Mirrors the template's qa/lib/component-stories.mjs (the
35
+ * lane-side parity gate) — keep the two in sync, same contract as navSlug.
36
+ */
37
+ export function kebabCase(name) {
38
+ return String(name)
39
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
40
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
41
+ .toLowerCase();
42
+ }
43
+
44
+ /** The preview-registry story id a component of this name registers (§3.3): `component.<kebab>`. */
45
+ export function componentStoryId(name) {
46
+ return `component.${kebabCase(name)}`;
47
+ }
48
+
49
+ // --- dimens classification ----------------------------------------------------
50
+
51
+ const DP_RE = /^(\d+(?:\.\d+)?)\s*dp$/;
52
+
53
+ /**
54
+ * @param {Record<string, string>} dimens catalog dimens (name -> "16dp"-style value)
55
+ * @returns {{spacing: Array<{name:string,value:string,dp:number}>,
56
+ * radius: Array<{name:string,value:string,dp:(number|null)}>,
57
+ * elevation: Array<{name:string,value:string,dp:(number|null)}>,
58
+ * other: Array<{name:string,value:string,dp:(number|null)}>}}
59
+ * spacing sorted ascending by dp (it is rendered as a scale); the rest in
60
+ * catalog order. A `Padding`/`Gap` token whose value doesn't parse as dp
61
+ * cannot be drawn to scale — it goes to `other` (stated, not stretched).
62
+ */
63
+ export function classifyDimens(dimens = {}) {
64
+ const spacing = [];
65
+ const radius = [];
66
+ const elevation = [];
67
+ const other = [];
68
+ for (const [name, value] of Object.entries(dimens)) {
69
+ const m = DP_RE.exec(String(value).trim());
70
+ const dp = m ? Number(m[1]) : null;
71
+ if (/^(Padding|Gap|Spacing)/.test(name) && dp !== null) spacing.push({ name, value, dp });
72
+ else if (/^Radius/.test(name)) radius.push({ name, value, dp });
73
+ else if (/^Elevation/.test(name)) elevation.push({ name, value, dp });
74
+ else other.push({ name, value, dp });
75
+ }
76
+ spacing.sort((a, b) => a.dp - b.dp || a.name.localeCompare(b.name));
77
+ return { spacing, radius, elevation, other };
78
+ }
79
+
80
+ // --- WCAG contrast pairs ------------------------------------------------------
81
+
82
+ // Normal-text thresholds, WCAG 2.2 SC 1.4.3 (AA) / SC 1.4.6 (AAA).
83
+ export const WCAG_AA_NORMAL = 4.5;
84
+ export const WCAG_AAA_NORMAL = 7;
85
+
86
+ /**
87
+ * @param {Record<string, string>} colors catalog colors (name -> hex)
88
+ * @returns {Array<{fg:string,bg:string,fgHex:string,bgHex:string,ratio:number,aa:boolean,aaa:boolean,role:string}>}
89
+ */
90
+ export function deriveContrastPairs(colors = {}) {
91
+ const pairs = [];
92
+ const seen = new Set();
93
+ const add = (fg, bg, role) => {
94
+ if (!(fg in colors) || !(bg in colors)) return;
95
+ const key = `${fg}/${bg}`;
96
+ if (seen.has(key)) return;
97
+ const ratio = contrastRatio(colors[fg], colors[bg]);
98
+ if (ratio === null) return; // unparseable hex -> the pair is absent, not guessed
99
+ seen.add(key);
100
+ pairs.push({
101
+ fg,
102
+ bg,
103
+ fgHex: colors[fg],
104
+ bgHex: colors[bg],
105
+ ratio,
106
+ aa: ratio >= WCAG_AA_NORMAL,
107
+ aaa: ratio >= WCAG_AAA_NORMAL,
108
+ role,
109
+ });
110
+ };
111
+ for (const name of Object.keys(colors)) {
112
+ if (/^On.+/.test(name)) add(name, name.slice(2), "text on its own surface");
113
+ }
114
+ add("OnSurfaceVariant", "Surface", "secondary text on Surface");
115
+ add("OnSurface", "Background", "body text on Background");
116
+ return pairs;
117
+ }
@@ -0,0 +1,122 @@
1
+ // console-evidence.mjs — THE RUNG'S ONE SPELLING IN THIS CONSOLE.
2
+ //
3
+ // NORTH-STAR.md §6.5 ends with the rule this file exists to keep: "every
4
+ // surface that shows a rung shows the pack". §8.9 says why it is a guarantee
5
+ // and not a preference — "a `cmp` L2 and any other pack's L2 are different
6
+ // claims and are shown as such". L2 means "a phone drove the app" under one
7
+ // pack and "proven against a real database" under another, and the letter alone
8
+ // does not say which. A rung with no pack beside it is therefore a claim its
9
+ // reader cannot check, which is the one thing this product refuses everywhere
10
+ // else; §3 already promises Gatekeeper "shows the pack beside the rung".
11
+ //
12
+ // WHY A MODULE AND NOT SEVEN EDITS. The console renders a rung in SEVEN places:
13
+ // the rail foot, the front door's standing line, the recent-requests rows, the
14
+ // Evidence section's status line, its headline chip, the committed-receipt
15
+ // timeline, and the digest's lane-run table. NORTH-STAR.md §9.2 says "five",
16
+ // lists six locations, and the count under the scan is seven — the miss being
17
+ // the recent-requests rows, whose rung OUTLIVES the run that earned it. That
18
+ // arithmetic is the argument: §9.2 closes by naming what a list costs — "a fix
19
+ // applied to the instances rather than to the class comes back", a price that
20
+ // section records this repository paying twice in one file. So the console does
21
+ // not hold seven spellings of a rung. It holds one function, every surface
22
+ // calls it, and test/evidence-ladder.test.mjs refuses any console module that
23
+ // reads a rung another way — so an EIGHTH surface is covered the day it is
24
+ // written, by the same inversion the agnostic lint uses, and by nobody
25
+ // remembering a list.
26
+ //
27
+ // ONLY THE ID IS EVER SHOWN. `pack.version` on a receipt is today the harness
28
+ // LOCK's version rather than the profile's — the code that writes it says so
29
+ // ("its version is the lock's until the profile loader gives it its own") and
30
+ // docs/adr/0008-a-resolved-harness-is-still-a-vendored-one.md decided that
31
+ // number "must become null, not inherited". Until it does, a surface printing
32
+ // it would be naming a version of the wrong thing, so nothing here reads it.
33
+ // The id is the part that carries the meaning, and the id is the profile's own.
34
+ //
35
+ // Pure text, never HTML: every caller already owns an `esc` and its own markup,
36
+ // and a derivation that returned tags could not be shared by a rail line, a
37
+ // badge span and a table cell without one of them wearing another's chrome —
38
+ // which console-shell.mjs's design rules forbid outright.
39
+
40
+ /**
41
+ * The pack id out of whatever the caller is holding, or null.
42
+ *
43
+ * Three shapes reach the console and all three are honoured, because forcing
44
+ * one on the callers would mean a bridge, a git-history row and a plan-trail
45
+ * entry each having to remember the same normalisation:
46
+ * - `"cmp"` — the flat id, how the receipt bridge and the plan trail carry it;
47
+ * - `{id: "cmp", version: …}` — the receipt's own `pack` object, verbatim,
48
+ * for any caller that holds the parsed receipt rather than a summary of it;
49
+ * - null/undefined/`{}`/`{id: " "}` — all of them mean the same thing, that
50
+ * nothing here names a pack, and all of them are rendered as that rather
51
+ * than as an absence the reader has to notice for themselves.
52
+ */
53
+ function packIdOf(pack) {
54
+ if (typeof pack === "string") return pack.trim() || null;
55
+ if (pack && typeof pack === "object" && typeof pack.id === "string") return pack.id.trim() || null;
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * The rung as a reader may safely quote it: the grade, its name when there is
61
+ * one, and the pack that graded it — or the statement that no pack is named.
62
+ *
63
+ * `null` when there is no rung to show. That is the honest absence and it has
64
+ * three causes the console must not tell apart: a FAILed lane (the lane itself
65
+ * records no level), a receipt written before the ladder existed, and a
66
+ * malformed field. None of them may be rendered as a rung, so none of them is.
67
+ *
68
+ * @param {{rung?: string, name?: string}|string|null|undefined} level the
69
+ * receipt's own derived `evidenceLevel`, read verbatim and never re-derived
70
+ * here — the lane is the law. A bare string is accepted for the two trails
71
+ * that store only the grade (the plan history's receipt glance, the digest's
72
+ * lane runs), so those surfaces get the pack without inventing a name.
73
+ * @param {{id?: string}|string|null|undefined} pack the pack id, or the
74
+ * receipt's `pack` object.
75
+ * @returns {string|null} e.g. `"L2 device · pack cmp"`, or
76
+ * `"L2 device · pack unnamed"` when the receipt named none. The grade and its
77
+ * name are joined by a SPACE and the pack by a middle dot, which is the
78
+ * console's existing spelling of a rung with one clause added rather than a
79
+ * new grammar: `L2 device` is what the rail foot, the front door and the
80
+ * Evidence status line already read, and the dot is what the done-gate CLI
81
+ * already puts before `pack`. A shared derivation is worth having; a shared
82
+ * derivation that also restyles six pages is two changes wearing one commit.
83
+ */
84
+ export function rungWithPack(level, pack) {
85
+ const grade =
86
+ typeof level === "string"
87
+ ? level.trim()
88
+ : level && typeof level === "object" && typeof level.rung === "string"
89
+ ? level.rung.trim()
90
+ : "";
91
+ if (!grade) return null;
92
+ const name =
93
+ level && typeof level === "object" && typeof level.name === "string" && level.name.trim() ? level.name.trim() : "";
94
+ const id = packIdOf(pack);
95
+ // "pack unnamed" is the wording the README badge already uses for this exact
96
+ // state (packages/harness/src/lib/evidence-badge.mjs) and the done-gate CLI
97
+ // says the same thing at greater length. One vocabulary across the surfaces
98
+ // matters more than each one's best phrasing: a reader who learns it on the
99
+ // badge reads it on the console without being taught twice.
100
+ return `${grade}${name ? ` ${name}` : ""} · ${id ? `pack ${id}` : "pack unnamed"}`;
101
+ }
102
+
103
+ /**
104
+ * The sentence that belongs in a `title=` beside a rung — why the pack is
105
+ * there, in the words the badge and the done-gate CLI already use.
106
+ *
107
+ * Two states, and the second is the load-bearing one. A receipt that names no
108
+ * pack is SAID to name none, because an unattributed rung is comparable to
109
+ * nothing and hiding that fact would be the overclaim the whole ladder exists
110
+ * to refuse. `null` when there is no rung, so a caller can build its tooltip
111
+ * without a second absence check.
112
+ *
113
+ * @returns {string|null}
114
+ */
115
+ export function rungPackNote(level, pack) {
116
+ if (!rungWithPack(level, pack)) return null;
117
+ const grade = typeof level === "string" ? level.trim() : String(level.rung).trim();
118
+ const id = packIdOf(pack);
119
+ return id
120
+ ? `this rung is pack ${id}'s: a ${id} ${grade} and another pack's ${grade} are different claims`
121
+ : "the receipt names no pack, so this rung is comparable to nothing";
122
+ }