@slowcook-ai/cli 0.19.6 → 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.
- package/README.md +8 -0
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/brew/fidelity-loop.d.ts +71 -0
- package/dist/commands/brew/fidelity-loop.d.ts.map +1 -0
- package/dist/commands/brew/fidelity-loop.js +108 -0
- package/dist/commands/brew/fidelity-loop.js.map +1 -0
- package/dist/commands/brew/fidelity-phase.d.ts +49 -0
- package/dist/commands/brew/fidelity-phase.d.ts.map +1 -0
- package/dist/commands/brew/fidelity-phase.js +75 -0
- package/dist/commands/brew/fidelity-phase.js.map +1 -0
- package/dist/commands/eye/index.d.ts +2 -0
- package/dist/commands/eye/index.d.ts.map +1 -0
- package/dist/commands/eye/index.js +64 -0
- package/dist/commands/eye/index.js.map +1 -0
- package/dist/commands/eye/plan.d.ts +61 -0
- package/dist/commands/eye/plan.d.ts.map +1 -0
- package/dist/commands/eye/plan.js +81 -0
- package/dist/commands/eye/plan.js.map +1 -0
- package/dist/commands/eye/run.d.ts +19 -0
- package/dist/commands/eye/run.d.ts.map +1 -0
- package/dist/commands/eye/run.js +47 -0
- package/dist/commands/eye/run.js.map +1 -0
- package/dist/commands/eye/spec-modes.d.ts +5 -0
- package/dist/commands/eye/spec-modes.d.ts.map +1 -0
- package/dist/commands/eye/spec-modes.js +36 -0
- package/dist/commands/eye/spec-modes.js.map +1 -0
- package/dist/commands/gate/github.d.ts +27 -0
- package/dist/commands/gate/github.d.ts.map +1 -0
- package/dist/commands/gate/github.js +46 -0
- package/dist/commands/gate/github.js.map +1 -0
- package/dist/commands/gate/index.d.ts +2 -0
- package/dist/commands/gate/index.d.ts.map +1 -0
- package/dist/commands/gate/index.js +68 -0
- package/dist/commands/gate/index.js.map +1 -0
- package/dist/commands/gate/model.d.ts +55 -0
- package/dist/commands/gate/model.d.ts.map +1 -0
- package/dist/commands/gate/model.js +64 -0
- package/dist/commands/gate/model.js.map +1 -0
- package/dist/commands/gate/reviewers.d.ts +24 -0
- package/dist/commands/gate/reviewers.d.ts.map +1 -0
- package/dist/commands/gate/reviewers.js +69 -0
- package/dist/commands/gate/reviewers.js.map +1 -0
- package/dist/commands/recon/shape-preserve.d.ts +38 -0
- package/dist/commands/recon/shape-preserve.d.ts.map +1 -1
- package/dist/commands/recon/shape-preserve.js +112 -1
- package/dist/commands/recon/shape-preserve.js.map +1 -1
- package/dist/commands/refine/spec-yaml.d.ts +3 -0
- package/dist/commands/refine/spec-yaml.d.ts.map +1 -1
- package/dist/commands/refine/spec-yaml.js +4 -0
- package/dist/commands/refine/spec-yaml.js.map +1 -1
- package/dist/commands/serve/config.d.ts +28 -9
- package/dist/commands/serve/config.d.ts.map +1 -1
- package/dist/commands/serve/config.js +43 -1
- package/dist/commands/serve/config.js.map +1 -1
- package/dist/commands/serve/dev.d.ts +14 -19
- package/dist/commands/serve/dev.d.ts.map +1 -1
- package/dist/commands/serve/dev.js +46 -50
- package/dist/commands/serve/dev.js.map +1 -1
- package/dist/commands/serve/index.d.ts.map +1 -1
- package/dist/commands/serve/index.js +30 -22
- package/dist/commands/serve/index.js.map +1 -1
- package/dist/commands/serve/mock.d.ts +5 -20
- package/dist/commands/serve/mock.d.ts.map +1 -1
- package/dist/commands/serve/mock.js +44 -65
- package/dist/commands/serve/mock.js.map +1 -1
- package/dist/commands/serve/runner.d.ts +52 -0
- package/dist/commands/serve/runner.d.ts.map +1 -0
- package/dist/commands/serve/runner.js +53 -0
- package/dist/commands/serve/runner.js.map +1 -0
- package/dist/commands/serve/staging.d.ts +8 -19
- package/dist/commands/serve/staging.d.ts.map +1 -1
- package/dist/commands/serve/staging.js +53 -91
- package/dist/commands/serve/staging.js.map +1 -1
- package/dist/commands/upsert-agent-docs.d.ts.map +1 -1
- package/dist/commands/upsert-agent-docs.js +12 -0
- package/dist/commands/upsert-agent-docs.js.map +1 -1
- package/dist/commands.manifest.d.ts.map +1 -1
- package/dist/commands.manifest.js +12 -0
- package/dist/commands.manifest.js.map +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const VIEWPORTS = {
|
|
2
|
+
mobile: { width: 390, height: 844 },
|
|
3
|
+
desktop: { width: 1280, height: 800 },
|
|
4
|
+
};
|
|
5
|
+
const SCHEMES = ["light", "dark"];
|
|
6
|
+
/** Full default matrix: {mobile,desktop} × {light,dark}. */
|
|
7
|
+
export const DEFAULT_MATRIX = Object.entries(VIEWPORTS).flatMap(([viewport, dim]) => SCHEMES.map((scheme) => ({ viewport, scheme, ...dim })));
|
|
8
|
+
/**
|
|
9
|
+
* Build the capture matrix from a spec's declared `fidelity.modes` (pure).
|
|
10
|
+
* Tokens are dimension VALUES (`light`/`dark`/`mobile`/`desktop`), expanded to
|
|
11
|
+
* the product: a dimension with no declared value defaults to its full set
|
|
12
|
+
* (so `[dark]` → dark × {mobile,desktop}; `[mobile]` → {light,dark} × mobile).
|
|
13
|
+
* Unrecognised tokens are ignored; if NONE are recognised, the full default
|
|
14
|
+
* matrix is returned (fail-open — a typo never silently checks nothing).
|
|
15
|
+
*/
|
|
16
|
+
export function matrixFromModes(modes) {
|
|
17
|
+
const wanted = new Set(modes.map((m) => String(m).toLowerCase().trim()));
|
|
18
|
+
const viewports = Object.keys(VIEWPORTS).filter((v) => wanted.has(v));
|
|
19
|
+
const schemes = SCHEMES.filter((s) => wanted.has(s));
|
|
20
|
+
if (!viewports.length && !schemes.length)
|
|
21
|
+
return DEFAULT_MATRIX;
|
|
22
|
+
const vps = viewports.length ? viewports : Object.keys(VIEWPORTS);
|
|
23
|
+
const schs = schemes.length ? schemes : SCHEMES;
|
|
24
|
+
return vps.flatMap((viewport) => schs.map((scheme) => ({ viewport, scheme, ...VIEWPORTS[viewport] })));
|
|
25
|
+
}
|
|
26
|
+
/** Narrow a matrix by explicit --viewport / --scheme flags (pure). Validates. */
|
|
27
|
+
export function narrowMatrix(base, opts) {
|
|
28
|
+
let m = base;
|
|
29
|
+
if (opts.viewport) {
|
|
30
|
+
if (!VIEWPORTS[opts.viewport]) {
|
|
31
|
+
throw new Error(`eye: unknown --viewport '${opts.viewport}' (have: ${Object.keys(VIEWPORTS).join(", ")})`);
|
|
32
|
+
}
|
|
33
|
+
m = m.filter((c) => c.viewport === opts.viewport);
|
|
34
|
+
}
|
|
35
|
+
if (opts.scheme) {
|
|
36
|
+
if (opts.scheme !== "light" && opts.scheme !== "dark")
|
|
37
|
+
throw new Error("eye: --scheme must be light|dark");
|
|
38
|
+
m = m.filter((c) => c.scheme === opts.scheme);
|
|
39
|
+
}
|
|
40
|
+
return m;
|
|
41
|
+
}
|
|
42
|
+
function val(args, flag) {
|
|
43
|
+
const i = args.indexOf(flag);
|
|
44
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse `slowcook eye` flags. Required: --reference <url>, --candidate <url>.
|
|
48
|
+
* Optional: --story <id> (derive the matrix from the spec's fidelity.modes),
|
|
49
|
+
* --cwd <dir>, --out <dir>, --viewport <name> + --scheme <light|dark> (narrow),
|
|
50
|
+
* --max-violations <n>, --fail-on <a,b>. The returned `matrix` is the flag-only
|
|
51
|
+
* default; when `story` is set the runner rebuilds it from the spec.
|
|
52
|
+
*/
|
|
53
|
+
export function parseEyeArgs(args) {
|
|
54
|
+
const referenceUrl = val(args, "--reference");
|
|
55
|
+
const candidateUrl = val(args, "--candidate");
|
|
56
|
+
if (!referenceUrl || !candidateUrl) {
|
|
57
|
+
throw new Error("eye: --reference <url> and --candidate <url> are both required");
|
|
58
|
+
}
|
|
59
|
+
const viewport = val(args, "--viewport");
|
|
60
|
+
const scheme = val(args, "--scheme");
|
|
61
|
+
const matrix = narrowMatrix(DEFAULT_MATRIX, { viewport, scheme });
|
|
62
|
+
const maxV = val(args, "--max-violations");
|
|
63
|
+
const failOn = val(args, "--fail-on");
|
|
64
|
+
const gate = {};
|
|
65
|
+
if (maxV !== undefined)
|
|
66
|
+
gate.maxViolations = Number.parseInt(maxV, 10);
|
|
67
|
+
if (failOn)
|
|
68
|
+
gate.failOnAxes = failOn.split(",").map((s) => s.trim());
|
|
69
|
+
return {
|
|
70
|
+
referenceUrl,
|
|
71
|
+
candidateUrl,
|
|
72
|
+
outDir: val(args, "--out") ?? ".brewing/eye",
|
|
73
|
+
matrix,
|
|
74
|
+
gate,
|
|
75
|
+
story: val(args, "--story"),
|
|
76
|
+
cwd: val(args, "--cwd") ?? ".",
|
|
77
|
+
viewport,
|
|
78
|
+
scheme,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../../src/commands/eye/plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FidelityGateOptions, type FidelityGateResult } from "@slowcook-ai/gates";
|
|
2
|
+
import type { EyeContext } from "./plan.js";
|
|
3
|
+
export interface RunEyeOptions {
|
|
4
|
+
referenceUrl: string;
|
|
5
|
+
candidateUrl: string;
|
|
6
|
+
matrix: EyeContext[];
|
|
7
|
+
/** Directory for screenshots. Files named `<label>-<viewport>-<scheme>.png`. */
|
|
8
|
+
outDir: string;
|
|
9
|
+
gate?: FidelityGateOptions;
|
|
10
|
+
/** Screenshot filename prefix (e.g. `eye-pr-123`); default none. */
|
|
11
|
+
shotPrefix?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface RunEyeResult {
|
|
14
|
+
result: FidelityGateResult;
|
|
15
|
+
screenshots: string[];
|
|
16
|
+
}
|
|
17
|
+
/** Render + grade the full matrix. Launches one browser, a fresh context per cell. */
|
|
18
|
+
export declare function runEyeMatrix(opts: RunEyeOptions): Promise<RunEyeResult>;
|
|
19
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/commands/eye/run.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EAExB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,sFAAsF;AACtF,wBAAsB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAmC7E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design #8 — reusable eye runner. Renders a reference (mock) URL + a candidate
|
|
3
|
+
* (brewed) URL across a capture matrix in headless Chromium, screenshots each
|
|
4
|
+
* cell, and grades candidate-vs-reference with the gates fidelity engine.
|
|
5
|
+
* Shared by the `slowcook eye` command (./index.ts) and the eye-driven brew
|
|
6
|
+
* fidelity phase (../brew/fidelity-phase.ts) so both measure identically.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { chromium } from "@playwright/test";
|
|
11
|
+
import { captureSnapshot, gradeFidelity, } from "@slowcook-ai/gates";
|
|
12
|
+
/** Render + grade the full matrix. Launches one browser, a fresh context per cell. */
|
|
13
|
+
export async function runEyeMatrix(opts) {
|
|
14
|
+
mkdirSync(opts.outDir, { recursive: true });
|
|
15
|
+
const prefix = opts.shotPrefix ? `${opts.shotPrefix}-` : "";
|
|
16
|
+
const browser = await chromium.launch();
|
|
17
|
+
const screenshots = [];
|
|
18
|
+
const captureUrl = async (url, label, ctx) => {
|
|
19
|
+
const c = await browser.newContext({
|
|
20
|
+
colorScheme: ctx.scheme,
|
|
21
|
+
viewport: { width: ctx.width, height: ctx.height },
|
|
22
|
+
deviceScaleFactor: 2,
|
|
23
|
+
});
|
|
24
|
+
const page = await c.newPage();
|
|
25
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
26
|
+
await page.waitForTimeout(400); // let lazy / client styles settle
|
|
27
|
+
const shot = join(opts.outDir, `${prefix}${label}-${ctx.viewport}-${ctx.scheme}.png`);
|
|
28
|
+
await page.screenshot({ path: shot, fullPage: true });
|
|
29
|
+
screenshots.push(shot);
|
|
30
|
+
const snap = await captureSnapshot(page, { viewport: ctx.viewport, scheme: ctx.scheme });
|
|
31
|
+
await c.close();
|
|
32
|
+
return snap;
|
|
33
|
+
};
|
|
34
|
+
const pairs = [];
|
|
35
|
+
try {
|
|
36
|
+
for (const ctx of opts.matrix) {
|
|
37
|
+
const reference = await captureUrl(opts.referenceUrl, "reference", ctx);
|
|
38
|
+
const candidate = await captureUrl(opts.candidateUrl, "candidate", ctx);
|
|
39
|
+
pairs.push({ reference, candidate });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await browser.close();
|
|
44
|
+
}
|
|
45
|
+
return { result: gradeFidelity(pairs, opts.gate), screenshots };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/commands/eye/run.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EACL,eAAe,EACf,aAAa,GAId,MAAM,oBAAoB,CAAC;AAmB5B,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAmB;IACpD,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;IACxC,MAAM,WAAW,GAAa,EAAE,CAAC;IAEjC,MAAM,UAAU,GAAG,KAAK,EAAE,GAAW,EAAE,KAAa,EAAE,GAAe,EAA0B,EAAE;QAC/F,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACjC,WAAW,EAAE,GAAG,CAAC,MAAM;YACvB,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;YAClD,iBAAiB,EAAE,CAAC;SACrB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC;QAC/B,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,kCAAkC;QAClE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,MAAM,CAAC,CAAC;QACtF,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACzF,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IAEF,MAAM,KAAK,GAA6D,EAAE,CAAC;IAC3E,IAAI,CAAC;QACH,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC9B,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC;YACxE,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC;YACxE,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Pure: extract `fidelity.modes` from a spec YAML string, or null if absent. */
|
|
2
|
+
export declare function extractFidelityModes(specYaml: string): string[] | null;
|
|
3
|
+
/** Load `fidelity.modes` for a story from `<repoRoot>/specs/story-<id>.yaml`. */
|
|
4
|
+
export declare function loadFidelityModes(repoRoot: string, story: string): string[] | null;
|
|
5
|
+
//# sourceMappingURL=spec-modes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-modes.d.ts","sourceRoot":"","sources":["../../../src/commands/eye/spec-modes.ts"],"names":[],"mappings":"AAeA,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAUtE;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAIlF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design #8 — read a story spec's declared fidelity modes. refine writes
|
|
3
|
+
* `fidelity.modes` on the spec (the contract for which viewport/scheme cells
|
|
4
|
+
* matter); the eye reads it here to build its matrix. Decoupled from the
|
|
5
|
+
* (not-yet-built) #7 `references` field — when that lands, `references.visual[].modes`
|
|
6
|
+
* becomes the per-source override and `fidelity.modes` the spec-level default.
|
|
7
|
+
*
|
|
8
|
+
* # specs/story-020.yaml
|
|
9
|
+
* fidelity:
|
|
10
|
+
* modes: [light, dark, mobile, desktop]
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import YAML from "yaml";
|
|
15
|
+
/** Pure: extract `fidelity.modes` from a spec YAML string, or null if absent. */
|
|
16
|
+
export function extractFidelityModes(specYaml) {
|
|
17
|
+
let doc;
|
|
18
|
+
try {
|
|
19
|
+
doc = YAML.parse(specYaml);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const modes = doc?.fidelity?.modes;
|
|
25
|
+
if (Array.isArray(modes))
|
|
26
|
+
return modes.map((m) => String(m));
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/** Load `fidelity.modes` for a story from `<repoRoot>/specs/story-<id>.yaml`. */
|
|
30
|
+
export function loadFidelityModes(repoRoot, story) {
|
|
31
|
+
const p = join(repoRoot, "specs", `story-${story}.yaml`);
|
|
32
|
+
if (!existsSync(p))
|
|
33
|
+
return null;
|
|
34
|
+
return extractFidelityModes(readFileSync(p, "utf8"));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=spec-modes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-modes.js","sourceRoot":"","sources":["../../../src/commands/eye/spec-modes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,iFAAiF;AACjF,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAI,GAAiD,EAAE,QAAQ,EAAE,KAAK,CAAC;IAClF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,OAAO,IAAI,CAAC;AACd,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,KAAa;IAC/D,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC,CAAC;IACzD,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,oBAAoB,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -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 @@
|
|
|
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"}
|