@slowcook-ai/cli 0.19.5 → 0.20.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 (99) hide show
  1. package/README.md +192 -28
  2. package/dist/cli.js +65 -73
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/brew/fidelity-loop.d.ts +71 -0
  5. package/dist/commands/brew/fidelity-loop.d.ts.map +1 -0
  6. package/dist/commands/brew/fidelity-loop.js +108 -0
  7. package/dist/commands/brew/fidelity-loop.js.map +1 -0
  8. package/dist/commands/brew/fidelity-phase.d.ts +49 -0
  9. package/dist/commands/brew/fidelity-phase.d.ts.map +1 -0
  10. package/dist/commands/brew/fidelity-phase.js +75 -0
  11. package/dist/commands/brew/fidelity-phase.js.map +1 -0
  12. package/dist/commands/eye/index.d.ts +2 -0
  13. package/dist/commands/eye/index.d.ts.map +1 -0
  14. package/dist/commands/eye/index.js +64 -0
  15. package/dist/commands/eye/index.js.map +1 -0
  16. package/dist/commands/eye/plan.d.ts +61 -0
  17. package/dist/commands/eye/plan.d.ts.map +1 -0
  18. package/dist/commands/eye/plan.js +81 -0
  19. package/dist/commands/eye/plan.js.map +1 -0
  20. package/dist/commands/eye/run.d.ts +19 -0
  21. package/dist/commands/eye/run.d.ts.map +1 -0
  22. package/dist/commands/eye/run.js +47 -0
  23. package/dist/commands/eye/run.js.map +1 -0
  24. package/dist/commands/eye/spec-modes.d.ts +5 -0
  25. package/dist/commands/eye/spec-modes.d.ts.map +1 -0
  26. package/dist/commands/eye/spec-modes.js +36 -0
  27. package/dist/commands/eye/spec-modes.js.map +1 -0
  28. package/dist/commands/gate/github.d.ts +27 -0
  29. package/dist/commands/gate/github.d.ts.map +1 -0
  30. package/dist/commands/gate/github.js +46 -0
  31. package/dist/commands/gate/github.js.map +1 -0
  32. package/dist/commands/gate/index.d.ts +2 -0
  33. package/dist/commands/gate/index.d.ts.map +1 -0
  34. package/dist/commands/gate/index.js +68 -0
  35. package/dist/commands/gate/index.js.map +1 -0
  36. package/dist/commands/gate/model.d.ts +55 -0
  37. package/dist/commands/gate/model.d.ts.map +1 -0
  38. package/dist/commands/gate/model.js +64 -0
  39. package/dist/commands/gate/model.js.map +1 -0
  40. package/dist/commands/gate/reviewers.d.ts +24 -0
  41. package/dist/commands/gate/reviewers.d.ts.map +1 -0
  42. package/dist/commands/gate/reviewers.js +69 -0
  43. package/dist/commands/gate/reviewers.js.map +1 -0
  44. package/dist/commands/recon/shape-preserve.d.ts +38 -0
  45. package/dist/commands/recon/shape-preserve.d.ts.map +1 -1
  46. package/dist/commands/recon/shape-preserve.js +112 -1
  47. package/dist/commands/recon/shape-preserve.js.map +1 -1
  48. package/dist/commands/refine/spec-yaml.d.ts +3 -0
  49. package/dist/commands/refine/spec-yaml.d.ts.map +1 -1
  50. package/dist/commands/refine/spec-yaml.js +4 -0
  51. package/dist/commands/refine/spec-yaml.js.map +1 -1
  52. package/dist/commands/serve/config.d.ts +159 -0
  53. package/dist/commands/serve/config.d.ts.map +1 -0
  54. package/dist/commands/serve/config.js +216 -0
  55. package/dist/commands/serve/config.js.map +1 -0
  56. package/dist/commands/serve/detect.d.ts +31 -0
  57. package/dist/commands/serve/detect.d.ts.map +1 -0
  58. package/dist/commands/serve/detect.js +56 -0
  59. package/dist/commands/serve/detect.js.map +1 -0
  60. package/dist/commands/serve/dev.d.ts +46 -0
  61. package/dist/commands/serve/dev.d.ts.map +1 -0
  62. package/dist/commands/serve/dev.js +122 -0
  63. package/dist/commands/serve/dev.js.map +1 -0
  64. package/dist/commands/serve/index.d.ts +28 -0
  65. package/dist/commands/serve/index.d.ts.map +1 -0
  66. package/dist/commands/serve/index.js +224 -0
  67. package/dist/commands/serve/index.js.map +1 -0
  68. package/dist/commands/serve/mock.d.ts +24 -0
  69. package/dist/commands/serve/mock.d.ts.map +1 -0
  70. package/dist/commands/serve/mock.js +111 -0
  71. package/dist/commands/serve/mock.js.map +1 -0
  72. package/dist/commands/serve/runner.d.ts +52 -0
  73. package/dist/commands/serve/runner.d.ts.map +1 -0
  74. package/dist/commands/serve/runner.js +53 -0
  75. package/dist/commands/serve/runner.js.map +1 -0
  76. package/dist/commands/serve/staging.d.ts +28 -0
  77. package/dist/commands/serve/staging.d.ts.map +1 -0
  78. package/dist/commands/serve/staging.js +152 -0
  79. package/dist/commands/serve/staging.js.map +1 -0
  80. package/dist/commands/stories/index.d.ts +15 -0
  81. package/dist/commands/stories/index.d.ts.map +1 -0
  82. package/dist/commands/stories/index.js +280 -0
  83. package/dist/commands/stories/index.js.map +1 -0
  84. package/dist/commands/stories/status.d.ts +74 -0
  85. package/dist/commands/stories/status.d.ts.map +1 -0
  86. package/dist/commands/stories/status.js +176 -0
  87. package/dist/commands/stories/status.js.map +1 -0
  88. package/dist/commands/upsert-agent-docs.d.ts.map +1 -1
  89. package/dist/commands/upsert-agent-docs.js +12 -0
  90. package/dist/commands/upsert-agent-docs.js.map +1 -1
  91. package/dist/commands.manifest.d.ts +37 -0
  92. package/dist/commands.manifest.d.ts.map +1 -0
  93. package/dist/commands.manifest.js +301 -0
  94. package/dist/commands.manifest.js.map +1 -0
  95. package/dist/help.d.ts +43 -0
  96. package/dist/help.d.ts.map +1 -0
  97. package/dist/help.js +131 -0
  98. package/dist/help.js.map +1 -0
  99. package/package.json +8 -6
@@ -0,0 +1,27 @@
1
+ /**
2
+ * design #9 — map GitHub PR reviews to the Approval shape `isGateSatisfied`
3
+ * grades. Pure + unit-tested; the live `gh api` fetch lives in ./index.ts.
4
+ *
5
+ * The identity classification is the load-bearing bit: a review authored by a
6
+ * Bot account (GitHub `user.type === "Bot"`, or a login ending in `[bot]`, or a
7
+ * known slowcook bot handle) is marked `identityType: "bot"` so it can never
8
+ * satisfy a human-review gate — the automation cannot self-approve.
9
+ */
10
+ import type { Approval } from "./model.js";
11
+ /** Subset of the GitHub PR-review payload we consume. */
12
+ export interface GhReview {
13
+ user?: {
14
+ login?: string;
15
+ type?: string;
16
+ } | null;
17
+ state?: string;
18
+ }
19
+ /** Logins always treated as bots regardless of the GitHub `type` field. */
20
+ export declare const BOT_LOGINS: ReadonlyArray<string>;
21
+ /**
22
+ * Convert raw PR reviews into Approvals. Dismissed/pending reviews are dropped.
23
+ * Only the latest review per author is kept (GitHub returns reviews
24
+ * chronologically; a later review supersedes an earlier one from the same user).
25
+ */
26
+ export declare function mapReviewsToApprovals(reviews: GhReview[]): Approval[];
27
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/github.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,yDAAyD;AACzD,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,2EAA2E;AAC3E,eAAO,MAAM,UAAU,EAAE,aAAa,CAAC,MAAM,CAAsB,CAAC;AAsBpE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAcrE"}
@@ -0,0 +1,46 @@
1
+ /** Logins always treated as bots regardless of the GitHub `type` field. */
2
+ export const BOT_LOGINS = ["github-actions"];
3
+ function isBot(login, type) {
4
+ if (type === "Bot")
5
+ return true;
6
+ if (login.endsWith("[bot]"))
7
+ return true;
8
+ if (BOT_LOGINS.includes(login))
9
+ return true;
10
+ return false;
11
+ }
12
+ function mapState(state) {
13
+ switch (state) {
14
+ case "APPROVED":
15
+ return "approved";
16
+ case "CHANGES_REQUESTED":
17
+ return "rejected";
18
+ case "COMMENTED":
19
+ return "commented";
20
+ default:
21
+ return null; // DISMISSED / PENDING / unknown → not a signal
22
+ }
23
+ }
24
+ /**
25
+ * Convert raw PR reviews into Approvals. Dismissed/pending reviews are dropped.
26
+ * Only the latest review per author is kept (GitHub returns reviews
27
+ * chronologically; a later review supersedes an earlier one from the same user).
28
+ */
29
+ export function mapReviewsToApprovals(reviews) {
30
+ const latestByAuthor = new Map();
31
+ for (const r of reviews) {
32
+ const login = (r.user?.login ?? "").toLowerCase();
33
+ if (!login)
34
+ continue;
35
+ const state = mapState(r.state);
36
+ if (!state)
37
+ continue;
38
+ latestByAuthor.set(login, {
39
+ byHandle: login,
40
+ state,
41
+ identityType: isBot(login, r.user?.type ?? undefined) ? "bot" : "human",
42
+ });
43
+ }
44
+ return [...latestByAuthor.values()];
45
+ }
46
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../../src/commands/gate/github.ts"],"names":[],"mappings":"AAiBA,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAA0B,CAAC,gBAAgB,CAAC,CAAC;AAEpE,SAAS,KAAK,CAAC,KAAa,EAAE,IAAwB;IACpD,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,KAAyB;IACzC,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB,KAAK,mBAAmB;YACtB,OAAO,UAAU,CAAC;QACpB,KAAK,WAAW;YACd,OAAO,WAAW,CAAC;QACrB;YACE,OAAO,IAAI,CAAC,CAAC,+CAA+C;IAChE,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAmB;IACvD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAoB,CAAC;IACnD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QAClD,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE;YACxB,QAAQ,EAAE,KAAK;YACf,KAAK;YACL,YAAY,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO;SACxE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,CAAC,GAAG,cAAc,CAAC,MAAM,EAAE,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function gate(args: string[], _version: string): Promise<void>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"AAmCA,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC1E"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * design #9 — `slowcook gate check`. The dispatch-time HITL halt: refuse to let
3
+ * a stage proceed until a human in the required role has approved on the PR.
4
+ *
5
+ * slowcook gate check --stage <refine|plate|brew> --pr <n> [--repo owner/name]
6
+ *
7
+ * Exit 0 = gate satisfied (advance). Exit 1 = blocked (a human in the required
8
+ * role(s) must approve, or a rejection must be resolved). Because approvals are
9
+ * classified by identity (./github.ts) and only human reviewers in the role's
10
+ * handle-list count (./model.ts), the automation cannot satisfy its own gate.
11
+ */
12
+ import { execFileSync } from "node:child_process";
13
+ import { loadReviewers } from "./reviewers.js";
14
+ import { DEFAULT_GATES, isGateSatisfied } from "./model.js";
15
+ import { mapReviewsToApprovals } from "./github.js";
16
+ function val(args, flag) {
17
+ const i = args.indexOf(flag);
18
+ return i >= 0 ? args[i + 1] : undefined;
19
+ }
20
+ function fetchReviews(repo, pr) {
21
+ const out = execFileSync("gh", ["api", `repos/${repo}/pulls/${pr}/reviews`, "--paginate"], {
22
+ encoding: "utf8",
23
+ });
24
+ return JSON.parse(out);
25
+ }
26
+ function detectRepo() {
27
+ const out = execFileSync("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], {
28
+ encoding: "utf8",
29
+ });
30
+ return out.trim();
31
+ }
32
+ export async function gate(args, _version) {
33
+ const sub = args[0];
34
+ if (sub !== "check") {
35
+ console.error("usage: slowcook gate check --stage <stage> --pr <n> [--repo owner/name]");
36
+ process.exit(64);
37
+ }
38
+ const rest = args.slice(1);
39
+ const stage = val(rest, "--stage");
40
+ const pr = val(rest, "--pr");
41
+ if (!stage || !pr) {
42
+ console.error("gate check: --stage and --pr are required");
43
+ process.exit(64);
44
+ }
45
+ const gateDef = DEFAULT_GATES.find((g) => g.stage === stage);
46
+ if (!gateDef) {
47
+ console.error(`gate check: no gate defined for stage '${stage}' (have: ${DEFAULT_GATES.map((g) => g.stage).join(", ")})`);
48
+ process.exit(64);
49
+ }
50
+ const repo = val(rest, "--repo") ?? detectRepo();
51
+ const reviewers = loadReviewers(process.cwd());
52
+ const approvals = mapReviewsToApprovals(fetchReviews(repo, pr));
53
+ const verdict = isGateSatisfied(gateDef, reviewers, approvals);
54
+ if (verdict.satisfied) {
55
+ console.log(`gate '${stage}' ✓ satisfied — ${verdict.reason}`);
56
+ return;
57
+ }
58
+ // Blocked. Name exactly who must act.
59
+ const need = verdict.rejected
60
+ ? verdict.reason
61
+ : verdict.missingRoles
62
+ .map((r) => `${r} (${reviewers.roles[r]?.join(", ") || "no reviewers configured in .brewing/reviewers.yaml"})`)
63
+ .join("; ");
64
+ console.error(`gate '${stage}' ✗ blocked-on-review — ${verdict.reason}`);
65
+ console.error(` needs approval from: ${need}`);
66
+ process.exit(1);
67
+ }
68
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * design #9 — HITL role gates: the gate-integrity core.
3
+ *
4
+ * Decides whether a pipeline stage may proceed past a human-review gate.
5
+ * The load-bearing security property: an approval only counts if it
6
+ * comes from a HUMAN identity that is in the configured handle-list for
7
+ * the required role. A bot/agent approval, or an approval from someone
8
+ * not assigned that role, NEVER satisfies a gate. This is what makes the
9
+ * halt unforgeable by the automation driving the pipeline.
10
+ */
11
+ import { type ReviewersConfig } from "./reviewers.js";
12
+ export type Role = "pm" | "designer" | "qa";
13
+ export type Stage = "refine" | "plate" | "brew" | string;
14
+ export interface Gate {
15
+ stage: Stage;
16
+ requiredRoles: Role[];
17
+ approvalSignal: "review" | "comment";
18
+ onRejectTarget: Stage;
19
+ }
20
+ export interface Approval {
21
+ /** GitHub handle of the approver. */
22
+ byHandle: string;
23
+ state: "approved" | "rejected" | "commented";
24
+ /** bot = slowcook-*[bot] / the driving agent; human = a real reviewer. */
25
+ identityType: "human" | "bot";
26
+ }
27
+ export interface GateVerdict {
28
+ satisfied: boolean;
29
+ /** required roles lacking a valid human approval. */
30
+ missingRoles: Role[];
31
+ /** a valid human reviewer for a required role rejected. */
32
+ rejected: boolean;
33
+ /** human-readable summary. */
34
+ reason: string;
35
+ }
36
+ /**
37
+ * The standard pipeline gates. refine is signed off by a PM, plate by a
38
+ * designer, and brew needs BOTH qa and designer before code ships.
39
+ */
40
+ export declare const DEFAULT_GATES: Gate[];
41
+ /**
42
+ * Evaluate a gate against the reviewer roster and the observed approvals.
43
+ *
44
+ * - A valid approval for role R = human + state 'approved' + handle in
45
+ * the role's configured list.
46
+ * - A valid rejection for role R = same, but state 'rejected'.
47
+ * - `rejected` is true if ANY required role has a valid rejection; a
48
+ * rejected gate is never satisfied (and routes back to onRejectTarget,
49
+ * handled by the caller).
50
+ * - `missingRoles` lists required roles with no valid approval.
51
+ * - `satisfied` requires every required role to have a valid approval
52
+ * AND no valid rejection.
53
+ */
54
+ export declare function isGateSatisfied(gate: Gate, reviewers: ReviewersConfig, approvals: Approval[]): GateVerdict;
55
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAe,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEnE,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,UAAU,GAAG,IAAI,CAAC;AAC5C,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEzD,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,IAAI,EAAE,CAAC;IACtB,cAAc,EAAE,QAAQ,GAAG,SAAS,CAAC;IACrC,cAAc,EAAE,KAAK,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC;IAC7C,0EAA0E;IAC1E,YAAY,EAAE,OAAO,GAAG,KAAK,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,qDAAqD;IACrD,YAAY,EAAE,IAAI,EAAE,CAAC;IACrB,2DAA2D;IAC3D,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,IAAI,EAI/B,CAAC;AAwBF;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,eAAe,EAC1B,SAAS,EAAE,QAAQ,EAAE,GACpB,WAAW,CAoBb"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * design #9 — HITL role gates: the gate-integrity core.
3
+ *
4
+ * Decides whether a pipeline stage may proceed past a human-review gate.
5
+ * The load-bearing security property: an approval only counts if it
6
+ * comes from a HUMAN identity that is in the configured handle-list for
7
+ * the required role. A bot/agent approval, or an approval from someone
8
+ * not assigned that role, NEVER satisfies a gate. This is what makes the
9
+ * halt unforgeable by the automation driving the pipeline.
10
+ */
11
+ import { resolveRole } from "./reviewers.js";
12
+ /**
13
+ * The standard pipeline gates. refine is signed off by a PM, plate by a
14
+ * designer, and brew needs BOTH qa and designer before code ships.
15
+ */
16
+ export const DEFAULT_GATES = [
17
+ { stage: "refine", requiredRoles: ["pm"], approvalSignal: "review", onRejectTarget: "refine" },
18
+ { stage: "plate", requiredRoles: ["designer"], approvalSignal: "review", onRejectTarget: "plate" },
19
+ { stage: "brew", requiredRoles: ["qa", "designer"], approvalSignal: "review", onRejectTarget: "brew" },
20
+ ];
21
+ /**
22
+ * True when `approvals` contains an approval in `state` for role `role`
23
+ * that is BOTH human-authored AND from a handle configured for that role
24
+ * in `reviewers`. Handle matching is case-insensitive (both sides
25
+ * lowercased). This is the single chokepoint enforcing the integrity
26
+ * property — a bot identity or an unconfigured handle can never pass.
27
+ */
28
+ function hasValidSignal(reviewers, approvals, role, state) {
29
+ const allowed = new Set(resolveRole(reviewers, role)); // already lowercased on load
30
+ return approvals.some((a) => a.identityType === "human" &&
31
+ a.state === state &&
32
+ allowed.has(a.byHandle.toLowerCase()));
33
+ }
34
+ /**
35
+ * Evaluate a gate against the reviewer roster and the observed approvals.
36
+ *
37
+ * - A valid approval for role R = human + state 'approved' + handle in
38
+ * the role's configured list.
39
+ * - A valid rejection for role R = same, but state 'rejected'.
40
+ * - `rejected` is true if ANY required role has a valid rejection; a
41
+ * rejected gate is never satisfied (and routes back to onRejectTarget,
42
+ * handled by the caller).
43
+ * - `missingRoles` lists required roles with no valid approval.
44
+ * - `satisfied` requires every required role to have a valid approval
45
+ * AND no valid rejection.
46
+ */
47
+ export function isGateSatisfied(gate, reviewers, approvals) {
48
+ const rejectingRoles = gate.requiredRoles.filter((role) => hasValidSignal(reviewers, approvals, role, "rejected"));
49
+ const missingRoles = gate.requiredRoles.filter((role) => !hasValidSignal(reviewers, approvals, role, "approved"));
50
+ const rejected = rejectingRoles.length > 0;
51
+ const satisfied = !rejected && missingRoles.length === 0;
52
+ let reason;
53
+ if (rejected) {
54
+ reason = `rejected by ${rejectingRoles.join(", ")}`;
55
+ }
56
+ else if (missingRoles.length > 0) {
57
+ reason = `blocked: missing ${missingRoles.join(", ")} approval`;
58
+ }
59
+ else {
60
+ reason = "satisfied";
61
+ }
62
+ return { satisfied, missingRoles, rejected, reason };
63
+ }
64
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../../src/commands/gate/model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,WAAW,EAAwB,MAAM,gBAAgB,CAAC;AA8BnE;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAW;IACnC,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE;IAC9F,EAAE,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,UAAU,CAAC,EAAE,cAAc,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE;IAClG,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,cAAc,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE;CACvG,CAAC;AAEF;;;;;;GAMG;AACH,SAAS,cAAc,CACrB,SAA0B,EAC1B,SAAqB,EACrB,IAAU,EACV,KAA8B;IAE9B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,6BAA6B;IACpF,OAAO,SAAS,CAAC,IAAI,CACnB,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,YAAY,KAAK,OAAO;QAC1B,CAAC,CAAC,KAAK,KAAK,KAAK;QACjB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CACxC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAU,EACV,SAA0B,EAC1B,SAAqB;IAErB,MAAM,cAAc,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACxD,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,CAAC,CACvD,CAAC;IACF,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAC5C,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,CAAC,CAClE,CAAC;IACF,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,CAAC,QAAQ,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC;IAEzD,IAAI,MAAc,CAAC;IACnB,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,GAAG,eAAe,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACtD,CAAC;SAAM,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,GAAG,oBAAoB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;IAClE,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,WAAW,CAAC;IACvB,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACvD,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ declare const ReviewersConfigSchema: z.ZodObject<{
3
+ schema_version: z.ZodLiteral<1>;
4
+ roles: z.ZodPipe<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>, z.ZodTransform<Record<string, string[]>, Record<string, string[]>>>;
5
+ }, z.core.$strip>;
6
+ export type ReviewersConfig = z.infer<typeof ReviewersConfigSchema>;
7
+ declare const EMPTY_DEFAULT: ReviewersConfig;
8
+ /**
9
+ * Load `.brewing/reviewers.yaml`. Returns an empty roster
10
+ * (`{ schema_version: 1, roles: {} }`) when the file is absent — a repo
11
+ * with no roster has no configured reviewers, so every role gate is
12
+ * unsatisfiable until one is authored (fail-closed). Throws on parse
13
+ * error / schema violation so a mis-authored roster surfaces loudly
14
+ * rather than silently granting or denying approvals.
15
+ */
16
+ export declare function loadReviewers(repoRoot: string): ReviewersConfig;
17
+ /**
18
+ * Returns the configured handles for a role (already lowercased), or an
19
+ * empty array when the role is unset. An empty array means the role can
20
+ * never be satisfied — fail-closed by design.
21
+ */
22
+ export declare function resolveRole(cfg: ReviewersConfig, role: string): string[];
23
+ export { EMPTY_DEFAULT };
24
+ //# sourceMappingURL=reviewers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewers.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/reviewers.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,QAAA,MAAM,qBAAqB;;;iBAazB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,QAAA,MAAM,aAAa,EAAE,eAGpB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAa/D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAExE;AAED,OAAO,EAAE,aAAa,EAAE,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * design #9 — HITL role gates: reviewer roster.
3
+ *
4
+ * Reads `.brewing/reviewers.yaml` to map review roles (pm, designer, qa,
5
+ * …) to the GitHub handles authorised to satisfy that role's gate. This
6
+ * roster is the trust anchor for the gate-integrity core: an approval
7
+ * only counts if its author is a configured handle for the required
8
+ * role (see `./model.js`).
9
+ *
10
+ * Handles are lowercased on load so downstream matching against the
11
+ * (also lowercased) approver handle is case-insensitive — GitHub login
12
+ * comparison is case-insensitive and a gate must not be bypassable by a
13
+ * casing mismatch.
14
+ *
15
+ * Single source of truth: nothing else should hard-code the roster
16
+ * location or shape.
17
+ */
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import YAML from "yaml";
21
+ import { z } from "zod";
22
+ const ReviewersConfigSchema = z.object({
23
+ schema_version: z.literal(1),
24
+ // role -> list of GitHub handles. Lowercased on load (see transform).
25
+ roles: z
26
+ .record(z.string(), z.array(z.string()))
27
+ .default({})
28
+ .transform((roles) => {
29
+ const out = {};
30
+ for (const [role, handles] of Object.entries(roles)) {
31
+ out[role] = handles.map((h) => h.toLowerCase());
32
+ }
33
+ return out;
34
+ }),
35
+ });
36
+ const EMPTY_DEFAULT = {
37
+ schema_version: 1,
38
+ roles: {},
39
+ };
40
+ /**
41
+ * Load `.brewing/reviewers.yaml`. Returns an empty roster
42
+ * (`{ schema_version: 1, roles: {} }`) when the file is absent — a repo
43
+ * with no roster has no configured reviewers, so every role gate is
44
+ * unsatisfiable until one is authored (fail-closed). Throws on parse
45
+ * error / schema violation so a mis-authored roster surfaces loudly
46
+ * rather than silently granting or denying approvals.
47
+ */
48
+ export function loadReviewers(repoRoot) {
49
+ const p = join(repoRoot, ".brewing", "reviewers.yaml");
50
+ if (!existsSync(p)) {
51
+ return { schema_version: 1, roles: {} };
52
+ }
53
+ const raw = YAML.parse(readFileSync(p, "utf8"));
54
+ const parsed = ReviewersConfigSchema.safeParse(raw);
55
+ if (!parsed.success) {
56
+ throw new Error(`Invalid .brewing/reviewers.yaml: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
57
+ }
58
+ return parsed.data;
59
+ }
60
+ /**
61
+ * Returns the configured handles for a role (already lowercased), or an
62
+ * empty array when the role is unset. An empty array means the role can
63
+ * never be satisfied — fail-closed by design.
64
+ */
65
+ export function resolveRole(cfg, role) {
66
+ return cfg.roles[role] ?? [];
67
+ }
68
+ export { EMPTY_DEFAULT };
69
+ //# sourceMappingURL=reviewers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewers.js","sourceRoot":"","sources":["../../../src/commands/gate/reviewers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,cAAc,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5B,sEAAsE;IACtE,KAAK,EAAE,CAAC;SACL,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;SACvC,OAAO,CAAC,EAAE,CAAC;SACX,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QACnB,MAAM,GAAG,GAA6B,EAAE,CAAC;QACzC,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACpD,GAAG,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;CACL,CAAC,CAAC;AAIH,MAAM,aAAa,GAAoB;IACrC,cAAc,EAAE,CAAC;IACjB,KAAK,EAAE,EAAE;CACV,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC;IACvD,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;QACnB,OAAO,EAAE,cAAc,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC1C,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,oCAAoC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC3F,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,GAAoB,EAAE,IAAY;IAC5D,OAAO,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;AAC/B,CAAC;AAED,OAAO,EAAE,aAAa,EAAE,CAAC"}
@@ -30,12 +30,50 @@ export interface ExtractedShape {
30
30
  hasHeader: boolean;
31
31
  /** Component name (best-effort). */
32
32
  componentName: string | null;
33
+ /**
34
+ * design #10 — dense, paradigm-aware per-element class facts. Unlike
35
+ * `visualTokens` (filtered through the 17-item TOKENS_OF_INTEREST
36
+ * allowlist), this captures the element's *actual* utility-class set so
37
+ * the shape test can assert `brewed ⊇ mock` containment per element and
38
+ * drift can't hide in unpinned classes. Optional + additive: absent on
39
+ * hand-built fixtures and pre-#10 callers.
40
+ */
41
+ elementClasses?: ElementClassFacts[];
42
+ }
43
+ /** design #10 — one element's testid anchor + its utility-class tokens. */
44
+ export interface ElementClassFacts {
45
+ /** data-testid on the element, or null (anchor for per-element assertions). */
46
+ testid: string | null;
47
+ /** Utility-shaped class tokens on the element (string-literal classNames only). */
48
+ tokens: string[];
33
49
  }
34
50
  /**
35
51
  * Scan a mock UI file, return the SHAPE-relevant facts after stripping
36
52
  * mock-only chrome subtrees.
37
53
  */
38
54
  export declare function extractShape(absFile: string, repoRoot: string): ExtractedShape | null;
55
+ /**
56
+ * design #10 — extract per-element utility-class facts from a (mock-chrome-
57
+ * stripped) source. Paradigm-aware: only reads STRING-LITERAL classNames
58
+ * (`className="..."` / `'...'` / plain backtick). `className={...}` brace
59
+ * expressions — CSS-modules (`{styles.x}`), `cx(...)`, conditional template
60
+ * literals with `${}` — are SKIPPED, because their tokens are either hashed
61
+ * identifiers (noise, not design intent) or computed; extracting them would
62
+ * be over-extraction. Styling fidelity for those mocks defers to the #8 eye.
63
+ *
64
+ * Pairs each element's tokens with its `data-testid` (the stable anchor for
65
+ * per-element containment assertions). Only elements carrying ≥1 utility
66
+ * token are returned.
67
+ */
68
+ export declare function extractElementClasses(source: string): ElementClassFacts[];
69
+ /**
70
+ * Heuristic: is `t` a utility-class token (Tailwind-shaped) rather than a
71
+ * component/module identifier? Keeps lowercase-led tokens + any token with
72
+ * a utility marker (`-`, `:`, `[`); drops capitalised identifiers and the
73
+ * empty string. Errs permissive — string-literal classNames in a utility
74
+ * mock are virtually all utilities.
75
+ */
76
+ export declare function isUtilityShaped(t: string): boolean;
39
77
  /**
40
78
  * Strip JSX subtrees marked as mock-only chrome. Two conventions:
41
79
  * 1. data-mock-chrome="true" attribute on the wrapping element
@@ -1 +1 @@
1
- {"version":3,"file":"shape-preserve.d.ts","sourceRoot":"","sources":["../../../src/commands/recon/shape-preserve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,IAAI,EAAY,MAAM,WAAW,CAAC;AAE3C,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,iFAAiF;IACjF,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB,oCAAoC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAWD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAiBrF;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAY9D;AA0GD,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAK/D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAiFjE;AAkFD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+E/E;AAGD,OAAO,EAAE,IAAI,IAAI,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"shape-preserve.d.ts","sourceRoot":"","sources":["../../../src/commands/recon/shape-preserve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,IAAI,EAAY,MAAM,WAAW,CAAC;AAE3C,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,iFAAiF;IACjF,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB,oCAAoC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACtC;AAED,2EAA2E;AAC3E,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,mFAAmF;IACnF,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAWD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAkBrF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAoBzE;AAkBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAIlD;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAY9D;AA0GD,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAK/D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAqHjE;AA6FD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+E/E;AAGD,OAAO,EAAE,IAAI,IAAI,KAAK,EAAE,CAAC"}
@@ -45,7 +45,76 @@ export function extractShape(absFile, repoRoot) {
45
45
  const testids = [...new Set(extractTestids(cleaned))];
46
46
  const visualTokens = [...new Set(extractTokens(cleaned, TOKENS_OF_INTEREST))];
47
47
  const hasHeader = /<header\b/.test(cleaned);
48
- return { file: fileRel, testids, visualTokens, hasHeader, componentName };
48
+ const elementClasses = extractElementClasses(cleaned);
49
+ return { file: fileRel, testids, visualTokens, hasHeader, componentName, elementClasses };
50
+ }
51
+ /**
52
+ * design #10 — extract per-element utility-class facts from a (mock-chrome-
53
+ * stripped) source. Paradigm-aware: only reads STRING-LITERAL classNames
54
+ * (`className="..."` / `'...'` / plain backtick). `className={...}` brace
55
+ * expressions — CSS-modules (`{styles.x}`), `cx(...)`, conditional template
56
+ * literals with `${}` — are SKIPPED, because their tokens are either hashed
57
+ * identifiers (noise, not design intent) or computed; extracting them would
58
+ * be over-extraction. Styling fidelity for those mocks defers to the #8 eye.
59
+ *
60
+ * Pairs each element's tokens with its `data-testid` (the stable anchor for
61
+ * per-element containment assertions). Only elements carrying ≥1 utility
62
+ * token are returned.
63
+ */
64
+ export function extractElementClasses(source) {
65
+ const facts = [];
66
+ // Iterate JSX opening tags (and self-closing). Attr blob is captured
67
+ // non-greedily up to the tag close.
68
+ for (const m of source.matchAll(/<[A-Za-z][\w.]*\b([^>]*?)\/?>/g)) {
69
+ const attrs = m[1] ?? "";
70
+ const className = literalClassName(attrs);
71
+ if (className === null)
72
+ continue; // dynamic/module/no className → skip
73
+ const tokens = className
74
+ .split(/\s+/)
75
+ .map((t) => t.trim())
76
+ .filter((t) => isUtilityShaped(t));
77
+ if (tokens.length === 0)
78
+ continue;
79
+ const tidMatch = attrs.match(/data-testid\s*=\s*["']([^"']+)["']/);
80
+ facts.push({
81
+ testid: tidMatch?.[1] ?? null,
82
+ tokens: [...new Set(tokens)],
83
+ });
84
+ }
85
+ return facts;
86
+ }
87
+ /**
88
+ * Return the string-literal value of a `className` attribute from a tag's
89
+ * attr blob, or null when the className is absent or a brace expression
90
+ * (`className={...}` — dynamic/module/computed; intentionally not read).
91
+ * Plain backtick literals are read only when they contain no `${}`.
92
+ */
93
+ function literalClassName(attrs) {
94
+ const dq = attrs.match(/className\s*=\s*"([^"]*)"/);
95
+ if (dq)
96
+ return dq[1] ?? "";
97
+ const sq = attrs.match(/className\s*=\s*'([^']*)'/);
98
+ if (sq)
99
+ return sq[1] ?? "";
100
+ const bt = attrs.match(/className\s*=\s*`([^`]*)`/);
101
+ if (bt && !bt[1]?.includes("${"))
102
+ return bt[1] ?? "";
103
+ return null;
104
+ }
105
+ /**
106
+ * Heuristic: is `t` a utility-class token (Tailwind-shaped) rather than a
107
+ * component/module identifier? Keeps lowercase-led tokens + any token with
108
+ * a utility marker (`-`, `:`, `[`); drops capitalised identifiers and the
109
+ * empty string. Errs permissive — string-literal classNames in a utility
110
+ * mock are virtually all utilities.
111
+ */
112
+ export function isUtilityShaped(t) {
113
+ if (!t)
114
+ return false;
115
+ if (/[-:[]/.test(t))
116
+ return true; // bg-primary, hover:..., min-h-[44px]
117
+ return /^[a-z][a-z0-9]*$/.test(t); // flex, grid, block, hidden, relative
49
118
  }
50
119
  /**
51
120
  * Strip JSX subtrees marked as mock-only chrome. Two conventions:
@@ -272,6 +341,39 @@ export function synthesiseShapeTestFileV2(opts) {
272
341
  lines.push(` expect(container.querySelector("header")).toBeTruthy();`);
273
342
  lines.push(` });`);
274
343
  }
344
+ // design #10 — per-element class containment (brewed ⊇ mock), anchored
345
+ // by testid. Asserts the rendered element keeps the mock's utility-class
346
+ // set; brew may ADD real-data classes (containment, not equality) but
347
+ // not DROP the mock's styling.
348
+ for (const ec of shape.elementClasses ?? []) {
349
+ if (!ec.testid || ec.tokens.length === 0)
350
+ continue;
351
+ lines.push(` it("[data-testid=${ec.testid}] keeps mock class tokens", () => {`);
352
+ lines.push(` const { queryByTestId } = render(<${cn} />);`);
353
+ lines.push(` const el = queryByTestId(${JSON.stringify(ec.testid)});`);
354
+ lines.push(` expect(el, ${JSON.stringify(`expected [data-testid=${ec.testid}] in rendered <${cn} />`)}).toBeTruthy();`);
355
+ lines.push(` const have = (el?.getAttribute("class") ?? "").split(/\\s+/);`);
356
+ lines.push(` const missing = ${JSON.stringify(ec.tokens)}.filter((t) => !have.includes(t));`);
357
+ lines.push(` expect(missing, "dropped mock class tokens").toEqual([]);`);
358
+ lines.push(` });`);
359
+ }
360
+ // design #10 — no-testid dense coverage: tokens on unanchored elements
361
+ // can't be matched per-element, so assert each survives SOMEWHERE in the
362
+ // rendered tree (file-level containment via [class*=]). Excludes tokens
363
+ // already covered by a testid-anchored or visualToken assertion.
364
+ const anchoredV2 = new Set((shape.elementClasses ?? []).filter((e) => e.testid).flatMap((e) => e.tokens));
365
+ const noTestidTokens = [
366
+ ...new Set((shape.elementClasses ?? [])
367
+ .filter((e) => !e.testid)
368
+ .flatMap((e) => e.tokens)
369
+ .filter((t) => !anchoredV2.has(t) && !shape.visualTokens.includes(t))),
370
+ ];
371
+ for (const token of noTestidTokens) {
372
+ lines.push(` it("rendered DOM keeps mock class token '${token}'", () => {`);
373
+ lines.push(` const { container } = render(<${cn} />);`);
374
+ lines.push(` expect(container.querySelector('[class*="${token}"]')).toBeTruthy();`);
375
+ lines.push(` });`);
376
+ }
275
377
  lines.push(` });`);
276
378
  lines.push(``);
277
379
  }
@@ -333,6 +435,15 @@ function synthesiseShapeTestFileV1(opts) {
333
435
  lines.push(` expect(src).toMatch(/className=[^>]*\\b${escapeRegexForTest(token)}\\b/);`);
334
436
  lines.push(` });`);
335
437
  }
438
+ // design #10 — dense token containment (file-level, source-grep).
439
+ // Coarse vs v2's per-element render assertion, but robust to bracket/
440
+ // colon/slash tokens; ensures the mock's full utility set survives brew.
441
+ const denseTokens = [...new Set((shape.elementClasses ?? []).flatMap((e) => e.tokens))].filter((t) => !shape.visualTokens.includes(t));
442
+ for (const token of denseTokens) {
443
+ lines.push(` it("preserves mock class token '${token}'", () => {`);
444
+ lines.push(` expect(src).toContain(${JSON.stringify(token)});`);
445
+ lines.push(` });`);
446
+ }
336
447
  if (shape.hasHeader) {
337
448
  lines.push(` it("retains a <header> element (semantic landmark)", () => {`);
338
449
  lines.push(` expect(src).toMatch(/<header\\b/);`);