create-cmp-cli 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) 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 +10 -11
  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 +271 -37
  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/approvals.mjs +177 -350
  16. package/packages/harness/src/lib/audit-cadence.mjs +1 -1
  17. package/packages/harness/src/lib/comments.mjs +18 -4
  18. package/packages/harness/src/lib/determinism.mjs +275 -14
  19. package/packages/harness/src/lib/evidence-badge.mjs +34 -2
  20. package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
  21. package/packages/harness/src/lib/evidence-level.mjs +144 -59
  22. package/packages/harness/src/lib/feature-brief.mjs +53 -16
  23. package/packages/harness/src/lib/flight-recorder.mjs +72 -8
  24. package/packages/harness/src/lib/framework-check.mjs +434 -53
  25. package/packages/harness/src/lib/harness-lock.mjs +87 -19
  26. package/packages/harness/src/lib/harness-manifest.mjs +208 -0
  27. package/packages/harness/src/lib/harness-region.mjs +106 -2
  28. package/packages/harness/src/lib/harness-source.mjs +142 -0
  29. package/packages/harness/src/lib/inputs-hash.mjs +175 -22
  30. package/packages/harness/src/lib/lane-markers.mjs +78 -0
  31. package/packages/harness/src/lib/lane-narrator.mjs +4 -2
  32. package/packages/harness/src/lib/lane-runner.mjs +72 -6
  33. package/packages/harness/src/lib/plan.mjs +44 -11
  34. package/packages/harness/src/lib/plant-calibration.mjs +135 -0
  35. package/packages/harness/src/lib/profile-loader.mjs +323 -0
  36. package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
  37. package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
  38. package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
  39. package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  40. package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
  41. package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
  42. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  43. package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
  44. package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
  45. package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
  46. package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
  47. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
  48. package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +229 -27
  49. package/packages/harness/src/lib/receipt-validate.mjs +84 -16
  50. package/packages/harness/src/lib/spec-coverage.mjs +179 -92
  51. package/packages/harness/src/lib/spec-model.mjs +262 -0
  52. package/packages/harness/src/lib/step-cache.mjs +11 -2
  53. package/packages/harness/src/lib/step-outcomes.mjs +84 -151
  54. package/packages/harness/src/lib/walk.mjs +1 -1
  55. package/packages/harness/src/preview-gallery.mjs +1 -1
  56. package/packages/harness/src/receipt-check.mjs +80 -24
  57. package/packages/harness/src/record-audit.mjs +1 -1
  58. package/packages/harness/src/scaffold-feature.mjs +10 -2
  59. package/packages/harness/src/verify.mjs +222 -142
  60. package/packages/harness/src/watch.mjs +146 -33
  61. package/packages/receipts/package.json +1 -1
  62. package/packages/receipts/src/index.mjs +2 -2
  63. package/packages/receipts/src/inputs-hash.mjs +175 -22
  64. package/packages/receipts/src/receipt-validate.mjs +84 -16
  65. package/src/commands/attach.mjs +128 -4
  66. package/src/commands/harden.mjs +7 -1
  67. package/src/commands/upgrade.mjs +7 -0
  68. package/src/lib/minimal.mjs +23 -1
  69. package/src/scaffold.mjs +8 -0
  70. package/template/.claude/settings.json +2 -2
  71. package/template/.claude/skills/add-feature/SKILL.md +7 -3
  72. package/template/.claude/skills/add-repository/SKILL.md +2 -2
  73. package/template/.claude/skills/add-screen/SKILL.md +8 -4
  74. package/template/.githooks/pre-push +9 -0
  75. package/template/CLAUDE.md +55 -186
  76. package/template/docs/TESTING.md +1 -1
  77. package/template/gitignore +3 -0
  78. package/template/qa/approve.mjs +10 -11
  79. package/template/qa/comments.json +1 -1
  80. package/template/qa/evidence/schema.json +258 -34
  81. package/template/qa/framework-check.mjs +271 -37
  82. package/template/qa/harness-manifest.json +10 -0
  83. package/template/qa/lib/a11y.mjs +1 -1
  84. package/template/qa/lib/affected-tests.mjs +99 -44
  85. package/template/qa/lib/approvals.mjs +177 -350
  86. package/template/qa/lib/audit-cadence.mjs +1 -1
  87. package/template/qa/lib/comments.mjs +18 -4
  88. package/template/qa/lib/determinism.mjs +275 -14
  89. package/template/qa/lib/evidence-badge.mjs +34 -2
  90. package/template/qa/lib/evidence-ladder.mjs +202 -0
  91. package/template/qa/lib/evidence-level.mjs +144 -59
  92. package/template/qa/lib/feature-brief.mjs +53 -16
  93. package/template/qa/lib/flight-recorder.mjs +72 -8
  94. package/template/qa/lib/framework-check.mjs +434 -53
  95. package/template/qa/lib/harness-lock.mjs +87 -19
  96. package/template/qa/lib/harness-manifest.mjs +208 -0
  97. package/template/qa/lib/harness-region.mjs +106 -2
  98. package/template/qa/lib/harness-source.mjs +142 -0
  99. package/template/qa/lib/inputs-hash.mjs +175 -22
  100. package/template/qa/lib/lane-markers.mjs +78 -0
  101. package/template/qa/lib/lane-narrator.mjs +4 -2
  102. package/template/qa/lib/lane-runner.mjs +72 -6
  103. package/template/qa/lib/plan.mjs +44 -11
  104. package/template/qa/lib/plant-calibration.mjs +135 -0
  105. package/template/qa/lib/profile-loader.mjs +323 -0
  106. package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
  107. package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
  108. package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
  109. package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  110. package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
  111. package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
  112. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  113. package/template/qa/lib/profiles/cmp/index.mjs +47 -0
  114. package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
  115. package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
  116. package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
  117. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
  118. package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +229 -27
  119. package/template/qa/lib/receipt-validate.mjs +84 -16
  120. package/template/qa/lib/spec-coverage.mjs +179 -92
  121. package/template/qa/lib/spec-model.mjs +262 -0
  122. package/template/qa/lib/step-cache.mjs +11 -2
  123. package/template/qa/lib/step-outcomes.mjs +84 -151
  124. package/template/qa/lib/walk.mjs +1 -1
  125. package/template/qa/preview-gallery.mjs +1 -1
  126. package/template/qa/receipt-check.mjs +80 -24
  127. package/template/qa/record-audit.mjs +1 -1
  128. package/template/qa/scaffold-feature.mjs +10 -2
  129. package/template/qa/verified-surface.json +17 -0
  130. package/template/qa/verify.mjs +222 -142
  131. package/template/qa/watch.mjs +146 -33
  132. /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  133. /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  134. /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  135. /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  136. /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
  137. /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  138. /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  139. /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  140. /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  141. /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
@@ -0,0 +1,47 @@
1
+ // profiles/cmp — the Compose Multiplatform stack profile. Mobile, by definition.
2
+ //
3
+ // This is the first profile and, for now, the only one — and it is loaded the
4
+ // same way every other profile ever will be: by id, from qa/harness-manifest.json,
5
+ // through qa/lib/profile-loader.mjs. The runner does not know this file exists.
6
+ //
7
+ // Stage 0 (docs/proposals/AGNOSTIC-HARNESS-ARCHITECTURE.md §11.3): PR 2 made
8
+ // this module re-export the step pack; PR 4 added `layout` and `tiers`
9
+ // (./declarations.mjs) — the paths and tier names the spec scanner used to
10
+ // hardcode. The other declarations — artifacts, architecture, ladder, review,
11
+ // plants, providers (§4.2) — still live where they always did and move here
12
+ // in the PRs that follow, one at a time, with the mobile fleet-check green
13
+ // after each. A profile that claimed all nine today would be claiming code it
14
+ // does not own yet.
15
+
16
+ import { createCmpSteps } from "./steps-cmp.mjs";
17
+
18
+ export const id = "cmp";
19
+ export const protocol = 1;
20
+
21
+ /** Where specs, sources, tests and flows live; which tiers observe what. */
22
+ export { layout, tiers, grammar, reports, detect, tools } from "./declarations.mjs";
23
+ export { copy as console } from "./console-copy.mjs";
24
+
25
+ /** What a human signs, in definition order; and whether this tree may record signatures at all. */
26
+ export { artifacts, governable } from "./artifacts.mjs";
27
+
28
+ /**
29
+ * The evidence ladder: which of THIS profile's steps earn which rung, and what
30
+ * the rungs are called. Exported at the top level (not only on the pack's
31
+ * return) so a reader that must not start a lane — the Stop hook deciding
32
+ * whether a tier that could have run did — can ask without instantiating
33
+ * anything. AGNOSTIC-HARNESS-ARCHITECTURE.md §4.2 #6.
34
+ */
35
+ export { CMP_LADDER as ladder } from "./ladder.mjs";
36
+
37
+ /**
38
+ * The source this profile's Rule 0 instrument plants — a citation must sit on
39
+ * a test, so the instrument has to write one in this stack's language
40
+ * (AGNOSTIC-HARNESS-ARCHITECTURE.md §4.2 #8, §7.2).
41
+ */
42
+ export { plants } from "./plants.mjs";
43
+
44
+ /** The step pack — today's createCmpSteps, unchanged. */
45
+ export function steps(ctx) {
46
+ return createCmpSteps(ctx);
47
+ }
@@ -0,0 +1,67 @@
1
+ // profiles/cmp/ladder.mjs — the Compose Multiplatform profile's evidence
2
+ // ladder: which of ITS steps earn which rung. Mobile's, by definition.
3
+ //
4
+ // L0 "scaffold" — the scaffold profile's checks passed (stamp-time green
5
+ // build: build + unit tests + the pure-Node gates).
6
+ // L1 "desktop" — full static + JVM evidence: everything L0 proves PLUS
7
+ // conformance, golden trees, a11y, and the release COMPILE
8
+ // (releaseBuild) — a green lane with no on-device step run.
9
+ // L2 "device" — L1 plus at least one on-device EXECUTION step PASSed
10
+ // (e2eSmoke, androidChecks, or the live tokenDrift tier).
11
+ // L3 "release" — L2 plus releaseSmoke PASSed (the release APK installed
12
+ // and driven on a device).
13
+ //
14
+ // THE LADDER IS THE PROFILE'S, NOT THE SPINE'S (2026-09-03): vendored into a
15
+ // Kotlin backend, these names graded its strongest run — detekt, Konsist,
16
+ // gitleaks — as L0 "scaffold" and made L1 unreachable by construction. A
17
+ // fixed-amount understatement is not conservative, it is wrong, and receipts
18
+ // are where labels get quoted. So the spine's rung derivation
19
+ // (qa/lib/evidence-level.mjs) takes a ladder from the profile's step pack
20
+ // (`evidenceLadder` on steps()' return) and defaults to NOTHING: a profile that
21
+ // declares no ladder earns no rung, which is the honest grade for a ladder
22
+ // nobody has calibrated. Stage 0 PR 3 moved this constant out of the spine so
23
+ // the spine carries no Compose step names at all.
24
+ //
25
+ // Every field is a list of step names except `release`, one name. `names`
26
+ // maps rung → label. A `cmp` L2 and another profile's L2 are different claims
27
+ // (receipt.pack says which); see AGNOSTIC-HARNESS-ARCHITECTURE.md §8.2.
28
+
29
+ /** The scaffold profile's step set (steps-cmp.mjs stepsForProfile.scaffold). */
30
+ const SCAFFOLD_CORE = [
31
+ "specCoverage",
32
+ "approvals",
33
+ "componentStories",
34
+ "reachability",
35
+ "e2eCoverage",
36
+ "archDoc",
37
+ "schemaHistory",
38
+ "build",
39
+ "unitTests",
40
+ ];
41
+
42
+ /** Steps every PASS must carry to claim even L0 — they run in every profile and never SKIP. */
43
+ const L0_REQUIRED = ["build", "unitTests"];
44
+
45
+ /**
46
+ * The steps that distinguish full desktop evidence (L1) from the scaffold
47
+ * checks. None of these can SKIP — they PASS or FAIL — so "PASSed" is exactly
48
+ * "ran green".
49
+ */
50
+ const L1_REQUIRED = ["releaseBuild", "conformance", "goldenTrees", "a11y"];
51
+
52
+ /** On-device EXECUTION steps — the only steps that can earn L2. */
53
+ const DEVICE_EXECUTION = ["e2eSmoke", "tokenDrift", "androidChecks"];
54
+
55
+ /** The one step that can lift L2 to L3. */
56
+ const RELEASE_EXECUTION = "releaseSmoke";
57
+
58
+ const RUNG_NAMES = { L0: "scaffold", L1: "desktop", L2: "device", L3: "release" };
59
+
60
+ export const CMP_LADDER = Object.freeze({
61
+ scaffoldCore: Object.freeze(SCAFFOLD_CORE),
62
+ l0Required: Object.freeze(L0_REQUIRED),
63
+ l1Required: Object.freeze(L1_REQUIRED),
64
+ deviceExecution: Object.freeze(DEVICE_EXECUTION),
65
+ release: RELEASE_EXECUTION,
66
+ names: Object.freeze(RUNG_NAMES),
67
+ });
@@ -0,0 +1,113 @@
1
+ // profiles/cmp/maestro.mjs — the Maestro directory run's verdict, and the
2
+ // device-log sweep scoped to the app under test. Mobile's, by definition.
3
+ //
4
+ // These three lived in qa/lib/step-outcomes.mjs beside the neutral verdict
5
+ // helpers (StepTimeout, stepErrorResult, spawnTimedOut). They are not neutral:
6
+ // a JUnit report from `maestro test <dir>` and an `adb logcat` crash buffer are
7
+ // facts about an Android device, and a backend profile has neither. Stage 0
8
+ // PR 3 moves them here so step-outcomes.mjs is the spine's again. Bodies are
9
+ // verbatim — the tests that pinned them moved with them.
10
+
11
+ // ── Maestro directory run → per-flow outcome ────────────────────────────────
12
+ /**
13
+ * Parse Maestro's JUnit report (`maestro test <dir> --format junit --output f`)
14
+ * into one row per flow. Tolerant: a report that is missing or unparsable
15
+ * returns null and the caller falls back to the exit code — never a fabricated
16
+ * per-flow list.
17
+ * @param {string|null} xml
18
+ * @returns {Array<{flow: string, ok: boolean, message?: string}>|null}
19
+ */
20
+ export function parseMaestroJunit(xml) {
21
+ if (typeof xml !== "string" || !/<testcase\b/.test(xml)) return null;
22
+ const rows = [];
23
+ const caseRe = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
24
+ let m;
25
+ while ((m = caseRe.exec(xml))) {
26
+ const attrs = m[1] || "";
27
+ const body = m[2] || "";
28
+ const name = (attrs.match(/\bname="([^"]*)"/) || [])[1] ?? (attrs.match(/\bid="([^"]*)"/) || [])[1] ?? "?";
29
+ const status = (attrs.match(/\bstatus="([^"]*)"/) || [])[1];
30
+ const failed = /<(failure|error)\b/.test(body) || (status && !/^(SUCCESS|PASSED?|OK)$/i.test(status));
31
+ const message = failed ? ((body.match(/<(?:failure|error)\b[^>]*message="([^"]*)"/) || [])[1] ?? body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 300)) : undefined;
32
+ rows.push(failed ? { flow: name, ok: false, message } : { flow: name, ok: true });
33
+ }
34
+ return rows.length ? rows : null;
35
+ }
36
+
37
+ /**
38
+ * The e2e step's verdict from the Maestro run: exit code + per-flow report +
39
+ * the list of flows the directory held. FAIL names every failing flow; a run
40
+ * whose report lists fewer flows than the directory holds is ERROR — the lane
41
+ * cannot claim flows it has no row for.
42
+ * @param {{ok: boolean, out: string}} res
43
+ * @param {Array<{flow: string, ok: boolean, message?: string}>|null} perFlow
44
+ * @param {string[]} flows root-relative flow files the directory holds
45
+ * @returns {{verdict: "PASS"|"FAIL"|"ERROR", reason?: string, details: object}}
46
+ */
47
+ export function maestroOutcome(res, perFlow, flows) {
48
+ const details = { flows, results: perFlow ?? undefined };
49
+ if (perFlow) {
50
+ const failed = perFlow.filter((r) => !r.ok);
51
+ if (failed.length) {
52
+ return {
53
+ verdict: "FAIL",
54
+ reason: `Maestro: ${failed.length} of ${perFlow.length} flow${perFlow.length === 1 ? "" : "s"} failed — ${failed.map((r) => `${r.flow}${r.message ? ` (${r.message.split("\n")[0].slice(0, 120)})` : ""}`).join("; ")}`,
55
+ details,
56
+ };
57
+ }
58
+ if (perFlow.length < flows.length) {
59
+ return {
60
+ verdict: "ERROR",
61
+ reason: `Maestro reported ${perFlow.length} flow${perFlow.length === 1 ? "" : "s"} but ${E2E_DIR_LABEL} holds ${flows.length} — the run did not cover every flow, so no verdict can be claimed for the rest`,
62
+ details,
63
+ };
64
+ }
65
+ if (!res.ok) {
66
+ return { verdict: "FAIL", reason: `Maestro exited non-zero with every flow reported green — treat as a run failure:\n${String(res.out).split("\n").slice(-10).join("\n")}`, details };
67
+ }
68
+ return { verdict: "PASS", details };
69
+ }
70
+ if (!res.ok) {
71
+ return { verdict: "FAIL", reason: `Maestro failed (no per-flow report was written):\n${String(res.out).split("\n").slice(-15).join("\n")}`, details };
72
+ }
73
+ return { verdict: "PASS", reason: "Maestro exited 0 but wrote no per-flow report — verdict from the exit code only", details };
74
+ }
75
+ const E2E_DIR_LABEL = "qa/e2e";
76
+
77
+ // ── Device-log incidents, scoped to the app under test ──────────────────────
78
+ /**
79
+ * ANR / fatal-exception lines from `adb logcat -d -b system,crash,main` that
80
+ * belong to one of `appIds` (an app's process may be `pkg` or `pkg:remote`).
81
+ * An emulator carries other apps — on 2026-09-03 the first self-booted lane
82
+ * went red on `ANR in com.karel.bratometer` while driving com.fleet.check —
83
+ * so an incident in another package is NOT this lane's failure. With no
84
+ * appIds known the sweep stays unscoped (every incident counts) and says so.
85
+ * @param {string} log
86
+ * @param {string[]} appIds
87
+ * @returns {{lines: string[], scoped: boolean}}
88
+ */
89
+ export function deviceLogIncidents(log, appIds = []) {
90
+ const lines = String(log ?? "").split("\n");
91
+ const ids = appIds.filter((id) => typeof id === "string" && id.trim() && id !== "__PACKAGE__");
92
+ const scoped = ids.length > 0;
93
+ const ours = (proc) => !scoped || ids.some((id) => proc === id || proc.startsWith(`${id}:`));
94
+ const out = [];
95
+ lines.forEach((line, i) => {
96
+ const anr = line.match(/ANR in (\S+?)(?:\s|,|$)/);
97
+ if (anr) {
98
+ if (ours(anr[1].replace(/[,)]$/, ""))) out.push(line);
99
+ return;
100
+ }
101
+ if (/FATAL EXCEPTION/i.test(line)) {
102
+ if (!scoped) {
103
+ out.push(line);
104
+ return;
105
+ }
106
+ // The crash buffer names the process on one of the next few lines.
107
+ const window = lines.slice(i, i + 8).join("\n");
108
+ const proc = window.match(/Process:\s*(\S+?)(?:,|\s|$)/);
109
+ if (proc && ours(proc[1])) out.push(line);
110
+ }
111
+ });
112
+ return { lines: out, scoped };
113
+ }
@@ -0,0 +1,95 @@
1
+ // profiles/cmp/plants.mjs — the SOURCE this profile's Rule 0 instrument plants
2
+ // when it proves specCoverage can still read a citation. Mobile's, by
3
+ // definition (docs/NORTH-STAR.md §6; GATE-RULES.md Rule 0/Rule 1).
4
+ //
5
+ // Stage 0 PR 7. Most of qa/framework-check.mjs is already stack-free: which
6
+ // plants a tree can support is derived from the tree through the spec model
7
+ // (PR 4), the clause grammar and the citation marker are the core scanner's,
8
+ // and stripping a flow's citations uses the core's own tag regex. What is NOT
9
+ // stack-free is the file a planted citation has to live in: a citation must sit
10
+ // on a test, so the instrument has to write a test — in this stack's language,
11
+ // with this stack's test-declaration syntax, at a path this stack compiles.
12
+ //
13
+ // Two plants need it, and they are the two that matter most, because each
14
+ // closes an escape that turns a red gate green with no assertions:
15
+ //
16
+ // unbound a `// SPEC:` tag on a CLASS with no test inside the binding
17
+ // window. This is payment-blueprint's real drift: the tag sat on
18
+ // `class PaymentWorkerTest`, three properties above a genuine
19
+ // @Test, and vouched for the whole file. The class body here is
20
+ // deliberately longer than BINDING_WINDOW so a real test cannot
21
+ // wander into range and launder it.
22
+ // tierUnmet a clause only the device tier can observe, cited from a
23
+ // host-tier test that compiles and runs and can never see it.
24
+ //
25
+ // A profile that declares no plants ships without those two, and the
26
+ // instrument says so per plant rather than reporting a quieter green — the
27
+ // §5.2 rule: no plants, no badge.
28
+
29
+ import { PLANT_KINDS } from "../../framework-check.mjs";
30
+
31
+ /** The file a planted citation lives in, relative to the test directory the instrument found. */
32
+ export const testFileBasename = "CmpFrameworkCheckPlanted.kt";
33
+
34
+ /**
35
+ * A citation on a type declaration with no test under it. The body must be
36
+ * longer than the core's BINDING_WINDOW so proximity alone cannot bind it.
37
+ * @param {string} clause the planted clause id
38
+ * @returns {string}
39
+ */
40
+ export function unboundCitationSource(clause) {
41
+ return `// Planted by qa/framework-check.mjs — reverted automatically.\n\n// SPEC: ${clause}\nclass CmpFrameworkCheckPlanted {\n val a = 1\n val b = 2\n val c = 3\n val d = 4\n val e = 5\n val f = 6\n fun helper() {}\n}\n`;
42
+ }
43
+
44
+ /**
45
+ * A real, compiling, running host-tier test that cites a clause only the
46
+ * device tier could observe.
47
+ * @param {string} clause the planted clause id
48
+ * @returns {string}
49
+ */
50
+ export function tierUnmetCitationSource(clause) {
51
+ return `// Planted by qa/framework-check.mjs — reverted automatically.\n\nimport kotlin.test.Test\n\nclass CmpFrameworkCheckPlanted {\n // SPEC: ${clause}\n @Test\n fun planted() {}\n}\n`;
52
+ }
53
+
54
+ /**
55
+ * The tier requirement the tierUnmet plant declares on its clause. It must be
56
+ * one this profile's `tiers.satisfying` defines and that a host-tier test
57
+ * cannot satisfy — otherwise the plant would not plant anything.
58
+ */
59
+ export const unmeetableTier = "e2e";
60
+
61
+ /**
62
+ * WHICH OF THIS PACK'S STEPS CATCHES EACH KIND OF PLANTED VIOLATION.
63
+ *
64
+ * `specCoverage` and `e2eCoverage` are OUR words. They used to be written into
65
+ * the core's plant selector as literals and asserted against every profile
66
+ * there will ever be — the same defect NORTH-STAR §9.1 records for
67
+ * `harnessIntegrity`, in the same file, one fix later. A pack spelling its
68
+ * coverage step `spec_coverage` had this instrument stop at the first plant
69
+ * with "specCoverage: no row — the guard did not FAIL BY NAME", about a gate
70
+ * that had just failed by name on the row beside it, and the two floor plants
71
+ * behind it never ran at all.
72
+ *
73
+ * So the names live here, where they are true, keyed by the CORE's plant kinds
74
+ * so neither side has to learn the other's vocabulary. A pack that omits this
75
+ * loses nothing it had: its plants assert over the lane instead of over one row
76
+ * (lib/framework-check.mjs `observingStep`). Declaring is how a pack asks for
77
+ * the sharper assertion — that THIS gate, not merely some gate, is the one that
78
+ * caught it.
79
+ */
80
+ export const observedBy = Object.freeze({
81
+ [PLANT_KINDS.ORPHANED_CITATION]: "specCoverage",
82
+ [PLANT_KINDS.UNBOUND_CITATION]: "specCoverage",
83
+ [PLANT_KINDS.TIER_UNMET]: "specCoverage",
84
+ [PLANT_KINDS.FEATURE_WITHOUT_FLOW]: "e2eCoverage",
85
+ [PLANT_KINDS.NESTED_FLOW]: "e2eCoverage",
86
+ });
87
+
88
+ /** What the instrument reads off the profile. */
89
+ export const plants = Object.freeze({
90
+ testFileBasename,
91
+ unboundCitationSource,
92
+ tierUnmetCitationSource,
93
+ unmeetableTier,
94
+ observedBy,
95
+ });
@@ -34,7 +34,7 @@
34
34
  import fs from "node:fs";
35
35
  import path from "node:path";
36
36
 
37
- import { parseFeatureBlock } from "./feature-brief.mjs";
37
+ import { parseFeatureBlock } from "../../feature-brief.mjs";
38
38
 
39
39
  // Mirrors component-stories.mjs's walkDirs: every directory named `wanted`
40
40
  // anywhere under `root`, recursive.