@yansirplus/cli 0.5.17

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 (47) hide show
  1. package/PUBLIC_API.md +22 -0
  2. package/README.md +34 -0
  3. package/dist/build/agent-authoring/config.d.ts +177 -0
  4. package/dist/build/agent-authoring/config.js +607 -0
  5. package/dist/build/agent-authoring/manifest-compiler.d.ts +159 -0
  6. package/dist/build/agent-authoring/manifest-compiler.js +737 -0
  7. package/dist/build/agent-authoring/shared.d.ts +10 -0
  8. package/dist/build/agent-authoring/shared.js +57 -0
  9. package/dist/build/agent-authoring/static-target.d.ts +59 -0
  10. package/dist/build/agent-authoring/static-target.js +1857 -0
  11. package/dist/build/agent-authoring.d.ts +9 -0
  12. package/dist/build/agent-authoring.js +5 -0
  13. package/dist/build/build-cli.d.ts +2 -0
  14. package/dist/build/build-cli.js +264 -0
  15. package/dist/check/algorithmic/architecture-checks.mjs +971 -0
  16. package/dist/check/algorithmic/client-boundary-checks.mjs +337 -0
  17. package/dist/check/algorithmic/convergence-smoke-checks.mjs +608 -0
  18. package/dist/check/algorithmic/distribution-checks.mjs +919 -0
  19. package/dist/check/algorithmic/owner-checks.mjs +647 -0
  20. package/dist/check/algorithmic/package-boundary-checks.mjs +985 -0
  21. package/dist/check/algorithmic/projection-boundary-checks.mjs +302 -0
  22. package/dist/check/algorithmic/repo-surface-checks.mjs +267 -0
  23. package/dist/check/algorithmic/runtime-structural-checks.mjs +264 -0
  24. package/dist/check/algorithmic/source-alias-checks.mjs +106 -0
  25. package/dist/check/algorithmic/static-target-checks.mjs +447 -0
  26. package/dist/check/algorithmic-checks.mjs +482 -0
  27. package/dist/check/check-coverage.mjs +231 -0
  28. package/dist/check/command-runner.mjs +22 -0
  29. package/dist/check/default-gate.mjs +51 -0
  30. package/dist/check/gate-selector.mjs +305 -0
  31. package/dist/check/manifest-rules.mjs +223 -0
  32. package/dist/check/package-graph.mjs +464 -0
  33. package/dist/generate/generate-agent-docs.mjs +435 -0
  34. package/dist/generate/generate-carrier-reference.mjs +514 -0
  35. package/dist/generate/generate-docs.mjs +345 -0
  36. package/dist/generate/generate-effect-skill-manifests.mjs +193 -0
  37. package/dist/generate/project-docs-site.mjs +190 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +25 -0
  40. package/dist/lib/agent-docs-model.mjs +888 -0
  41. package/dist/lib/boundary-rules.mjs +63 -0
  42. package/dist/lib/capability-routes.mjs +354 -0
  43. package/dist/lib/projection-sink.mjs +113 -0
  44. package/dist/lib/public-api-model.mjs +306 -0
  45. package/dist/main.mjs +233 -0
  46. package/dist/runner.mjs +127 -0
  47. package/package.json +32 -0
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { listAlgorithmicCheckers } from "./algorithmic-checks.mjs";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
8
+
9
+ const readJson = (relativePath) =>
10
+ JSON.parse(fs.readFileSync(path.join(repoRoot, relativePath), "utf8"));
11
+
12
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
13
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
14
+
15
+ const failures = [];
16
+ const fail = (message) => failures.push(message);
17
+
18
+ const coverage = readJson("packages/cli/src/check/check-coverage.source.json");
19
+ const rulesSource = readJson("docs/agent/boundary-rules.source.json");
20
+ const cliPackage = readJson("packages/cli/package.json");
21
+
22
+ if (coverage.schemaVersion !== 1) fail("check coverage schemaVersion must be 1");
23
+ if (!Array.isArray(coverage.entries) || coverage.entries.length === 0) {
24
+ fail("check coverage entries must be non-empty");
25
+ }
26
+
27
+ const rules = new Map((rulesSource.rules ?? []).map((rule) => [rule.id, rule]));
28
+ const algorithmicCheckers = new Set(listAlgorithmicCheckers());
29
+ const coveredRuleIds = new Set();
30
+ const genericPackageNegativeWitness =
31
+ "package-owned test fails when the owned contract is violated";
32
+
33
+ const assertProofWitnesses = (label, assertionIndex, assertion) => {
34
+ const witnesses = assertion.packageWitnesses;
35
+ if (witnesses === undefined) return;
36
+ if (!Array.isArray(witnesses) || witnesses.length === 0)
37
+ fail(`${label}: assertions[${assertionIndex}] packageWitnesses must be non-empty when present`);
38
+
39
+ for (const [witnessIndex, witness] of witnesses.entries()) {
40
+ const witnessLabel = `${label}: assertions[${assertionIndex}].packageWitnesses[${witnessIndex}]`;
41
+ if (!isRecord(witness)) {
42
+ fail(`${witnessLabel} must be an object`);
43
+ continue;
44
+ }
45
+ if (typeof witness.file !== "string" || !witness.file.startsWith("packages/")) {
46
+ fail(`${witnessLabel}.file must be a package test path`);
47
+ continue;
48
+ }
49
+ if (typeof witness.name !== "string" || witness.name.length === 0) {
50
+ fail(`${witnessLabel}.name must be non-empty`);
51
+ continue;
52
+ }
53
+
54
+ const absolutePath = path.join(repoRoot, witness.file);
55
+ if (!fs.existsSync(absolutePath)) {
56
+ fail(`${witnessLabel}.file does not exist`);
57
+ continue;
58
+ }
59
+ const source = fs.readFileSync(absolutePath, "utf8");
60
+ const testNamePattern = new RegExp(
61
+ `\\b(?:describe|it|test)(?:\\.effect)?\\(\\s*(["'\`])${escapeRegExp(witness.name)}\\1`,
62
+ "u",
63
+ );
64
+ if (!testNamePattern.test(source)) {
65
+ fail(`${witnessLabel}.name was not found as a test or describe name`);
66
+ }
67
+ }
68
+ };
69
+
70
+ for (const [index, entry] of (coverage.entries ?? []).entries()) {
71
+ const label =
72
+ isRecord(entry) && typeof entry.ruleId === "string" ? entry.ruleId : `entry[${index}]`;
73
+ if (!isRecord(entry)) {
74
+ fail(`${label}: coverage entry must be an object`);
75
+ continue;
76
+ }
77
+ if (!isRecord(entry.source)) fail(`${label}: missing source`);
78
+ if (!isRecord(entry.target)) fail(`${label}: missing target`);
79
+ if (!Array.isArray(entry.assertions) || entry.assertions.length === 0) {
80
+ fail(`${label}: assertions must be non-empty`);
81
+ } else {
82
+ for (const [assertionIndex, assertion] of entry.assertions.entries()) {
83
+ if (!isRecord(assertion)) {
84
+ fail(`${label}: assertions[${assertionIndex}] must be an object`);
85
+ continue;
86
+ }
87
+ if (
88
+ typeof assertion.failureCondition !== "string" ||
89
+ assertion.failureCondition.length === 0
90
+ ) {
91
+ fail(`${label}: assertions[${assertionIndex}] missing failureCondition`);
92
+ }
93
+ if (typeof assertion.negativeWitness !== "string" || assertion.negativeWitness.length === 0) {
94
+ fail(`${label}: assertions[${assertionIndex}] missing negativeWitness`);
95
+ }
96
+ if (assertion.negativeWitness === genericPackageNegativeWitness) {
97
+ fail(`${label}: assertions[${assertionIndex}] has generic package negativeWitness`);
98
+ }
99
+ }
100
+ }
101
+
102
+ if (typeof entry.ruleId === "string" && entry.ruleId.includes(",")) {
103
+ for (const ruleId of entry.ruleId.split(",")) coveredRuleIds.add(ruleId);
104
+ } else if (typeof entry.ruleId === "string") {
105
+ coveredRuleIds.add(entry.ruleId);
106
+ }
107
+
108
+ if (entry.target?.kind === "algorithmic") {
109
+ const rule = rules.get(entry.target.ruleId);
110
+ if (!isRecord(rule)) {
111
+ fail(`${label}: algorithmic target references unknown rule ${entry.target.ruleId}`);
112
+ } else if (rule.acceptance?.engine !== "algorithmic") {
113
+ fail(`${label}: algorithmic target rule must use algorithmic acceptance`);
114
+ } else if (rule.acceptance.checker !== entry.target.checker) {
115
+ fail(`${label}: coverage checker ${entry.target.checker} does not match rule acceptance`);
116
+ }
117
+ if (!algorithmicCheckers.has(entry.target.checker)) {
118
+ fail(`${label}: missing algorithmic checker ${entry.target.checker}`);
119
+ }
120
+ }
121
+
122
+ if (entry.target?.kind === "proofClass") {
123
+ const rule = rules.get(entry.target.ruleId);
124
+ if (!isRecord(rule)) {
125
+ fail(`${label}: proofClass target references unknown rule ${entry.target.ruleId}`);
126
+ } else if (rule.acceptance?.engine !== "proofClass") {
127
+ fail(`${label}: proofClass target rule must use proofClass acceptance`);
128
+ } else {
129
+ for (const [assertionIndex, assertion] of (entry.assertions ?? []).entries()) {
130
+ if (isRecord(assertion)) {
131
+ assertProofWitnesses(label, assertionIndex, assertion);
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ if (entry.target?.kind === "manifestRule") {
138
+ const rule = rules.get(entry.target.ruleId);
139
+ if (!isRecord(rule)) {
140
+ fail(`${label}: manifestRule target references unknown rule ${entry.target.ruleId}`);
141
+ } else if (rule.acceptance?.engine !== entry.target.engine) {
142
+ fail(`${label}: manifestRule target engine must match rule acceptance`);
143
+ }
144
+ }
145
+
146
+ if (typeof entry.ruleId === "string" && !entry.ruleId.includes(",")) {
147
+ const rule = rules.get(entry.ruleId);
148
+ if (isRecord(rule) && entry.owner !== rule.owner) {
149
+ fail(
150
+ `${label}: coverage owner ${entry.owner} does not match boundary rule owner ${rule.owner}`,
151
+ );
152
+ }
153
+ }
154
+
155
+ if (entry.target?.kind === "algorithmic-helper") {
156
+ for (const checker of entry.target.checkers ?? []) {
157
+ if (!algorithmicCheckers.has(checker))
158
+ fail(`${label}: helper references unknown checker ${checker}`);
159
+ }
160
+ }
161
+ }
162
+
163
+ for (const rule of rulesSource.rules ?? []) {
164
+ if (!coveredRuleIds.has(rule.id)) fail(`${rule.id}: rule lacks check coverage entry`);
165
+ }
166
+
167
+ const sourceText = fs.readFileSync(
168
+ path.join(repoRoot, "docs/agent/boundary-rules.source.json"),
169
+ "utf8",
170
+ );
171
+ if (sourceText.includes("positiveAcceptance")) {
172
+ fail("docs/agent/boundary-rules.source.json: positiveAcceptance is no longer allowed");
173
+ }
174
+ if (/\s--fix(?:\s|")/u.test(sourceText)) {
175
+ fail("docs/agent/boundary-rules.source.json: check commands must not include --fix");
176
+ }
177
+
178
+ const walk = (relativePath) => {
179
+ const absolutePath = path.join(repoRoot, relativePath);
180
+ const files = [];
181
+ for (const entry of fs.readdirSync(absolutePath, { withFileTypes: true })) {
182
+ const child = path.join(relativePath, entry.name);
183
+ if (entry.isDirectory()) files.push(...walk(child));
184
+ if (entry.isFile()) files.push(child.split(path.sep).join("/"));
185
+ }
186
+ return files;
187
+ };
188
+
189
+ const cliMjsFiles = walk("packages/cli/src").filter((file) => file.endsWith(".mjs"));
190
+ const checkMjsFiles = cliMjsFiles.filter((file) => file.startsWith("packages/cli/src/check/"));
191
+ if (cliMjsFiles.length > 19) {
192
+ fail(`packages/cli/src: expected at most 19 .mjs files; observed ${cliMjsFiles.length}`);
193
+ }
194
+ if (checkMjsFiles.length > 12) {
195
+ fail(`packages/cli/src/check: expected at most 12 .mjs files; observed ${checkMjsFiles.length}`);
196
+ }
197
+ for (const file of checkMjsFiles) {
198
+ const content = fs.readFileSync(path.join(repoRoot, file), "utf8");
199
+ const selfTestFlag = "--" + "self-test";
200
+ if (content.includes(selfTestFlag)) fail(`${file}: per-checker self-test flag is not allowed`);
201
+ const harnessPattern = new RegExp(
202
+ ["mkd" + "te" + "mp", "tm" + "pdir", "Tem" + "por" + "ary", "te" + "mp"].join("|"),
203
+ "i",
204
+ );
205
+ if (file !== "packages/cli/src/check/algorithmic-checks.mjs" && harnessPattern.test(content)) {
206
+ fail(`${file}: per-checker ad hoc harness is not allowed`);
207
+ }
208
+ }
209
+
210
+ const cliDependencies = Object.keys(cliPackage.dependencies ?? {});
211
+ for (const dependency of cliDependencies) {
212
+ if (
213
+ dependency === "@effect/cli" ||
214
+ dependency === "@effect/platform-node" ||
215
+ dependency === "@effect/printer" ||
216
+ dependency === "@effect/printer-ansi" ||
217
+ dependency === "@effect/typeclass"
218
+ ) {
219
+ fail(`packages/cli/package.json: removed Effect CLI dependency returned: ${dependency}`);
220
+ }
221
+ }
222
+ if (cliPackage.dependencies?.effect === "3.21.2") {
223
+ fail("packages/cli/package.json: Effect v3 CLI dependency returned");
224
+ }
225
+
226
+ if (failures.length > 0) {
227
+ console.error(failures.join("\n"));
228
+ process.exit(1);
229
+ }
230
+
231
+ console.log("guard coverage passed");
@@ -0,0 +1,22 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ const completedCommands = new Set();
4
+
5
+ export const runCommand = (command, { cwd, memoize = true } = {}) => {
6
+ if (/\s--fix(?:\s|$)/u.test(command)) {
7
+ throw new Error(`${command}: check commands must not run fix mode`);
8
+ }
9
+ if (memoize && completedCommands.has(command)) {
10
+ console.log(`$ ${command} (already checked)`);
11
+ return;
12
+ }
13
+ console.log(`$ ${command}`);
14
+ const result = spawnSync("sh", ["-c", command], {
15
+ cwd,
16
+ env: process.env,
17
+ stdio: "inherit",
18
+ });
19
+ if (result.signal !== null) throw new Error(`${command} terminated by ${result.signal}`);
20
+ if (result.status !== 0) throw new Error(`${command} exited with ${result.status ?? 1}`);
21
+ if (memoize) completedCommands.add(command);
22
+ };
@@ -0,0 +1,51 @@
1
+ import { spawn } from "node:child_process";
2
+ import { repoRoot } from "./gate-selector.mjs";
3
+ import { deriveAffectedGates, printAffectedGates, runAffectedGates } from "./gate-selector.mjs";
4
+
5
+ const runStage = (label, command, args) =>
6
+ new Promise((resolve) => {
7
+ console.log(`$ ${[command, ...args].join(" ")}`);
8
+ const startedAt = Date.now();
9
+ const child = spawn(command, args, {
10
+ cwd: repoRoot,
11
+ env: process.env,
12
+ stdio: "inherit",
13
+ });
14
+ child.on("error", (error) => {
15
+ resolve({ label, ok: false, durationMs: Date.now() - startedAt, error });
16
+ });
17
+ child.on("exit", (code, signal) => {
18
+ resolve({
19
+ label,
20
+ ok: code === 0 && signal === null,
21
+ durationMs: Date.now() - startedAt,
22
+ error:
23
+ signal === null
24
+ ? code === 0
25
+ ? undefined
26
+ : `${label} exited with ${code ?? 1}`
27
+ : `${label} terminated by ${signal}`,
28
+ });
29
+ });
30
+ });
31
+
32
+ export const runDefaultGate = async () => {
33
+ const startedAt = Date.now();
34
+ const stages = await Promise.all([
35
+ runStage("structural", "bun", ["run", "check:structural"]),
36
+ runStage("typecheck", "bun", ["run", "typecheck"]),
37
+ runStage("test", "bun", ["run", "test"]),
38
+ ]);
39
+ for (const stage of stages) {
40
+ console.log(`${stage.label} duration: ${stage.durationMs}ms`);
41
+ }
42
+ const failed = stages.filter((stage) => !stage.ok);
43
+ if (failed.length > 0) {
44
+ throw new Error(failed.map((stage) => stage.error ?? `${stage.label} failed`).join("\n"));
45
+ }
46
+ const durationMs = Date.now() - startedAt;
47
+ console.log(`fast gate duration: ${durationMs}ms`);
48
+ const result = deriveAffectedGates();
49
+ printAffectedGates(result);
50
+ runAffectedGates(result);
51
+ };
@@ -0,0 +1,305 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { runCommand } from "./command-runner.mjs";
6
+ import {
7
+ packageManifestDependencyEdges,
8
+ packageSourceImportEdges,
9
+ tsconfigReferenceEdges,
10
+ workspacePackageRecords,
11
+ } from "./package-graph.mjs";
12
+
13
+ export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
14
+ const gatesPath = "docs/agent/gates.source.json";
15
+ const compare = (left, right) => left.localeCompare(right);
16
+ const readJson = (relativePath) =>
17
+ JSON.parse(fs.readFileSync(path.join(repoRoot, relativePath), "utf8"));
18
+
19
+ const git = (args) => execFileSync("git", args, { cwd: repoRoot, encoding: "utf8" }).trim();
20
+ const gitLines = (args) =>
21
+ git(args)
22
+ .split("\n")
23
+ .map((line) => line.trim())
24
+ .filter((line) => line.length > 0)
25
+ .map((line) => line.split(path.sep).join("/"));
26
+
27
+ const escapeRegex = (value) => value.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
28
+ const globToRegex = (pattern) => {
29
+ let regex = "^";
30
+ for (let index = 0; index < pattern.length; index += 1) {
31
+ const char = pattern[index];
32
+ const next = pattern[index + 1];
33
+ if (char === "*" && next === "*") {
34
+ regex += ".*";
35
+ index += 1;
36
+ } else if (char === "*") {
37
+ regex += "[^/]*";
38
+ } else {
39
+ regex += escapeRegex(char);
40
+ }
41
+ }
42
+ return new RegExp(`${regex}$`, "u");
43
+ };
44
+
45
+ const matchesPattern = (file, pattern) => globToRegex(pattern).test(file);
46
+ const matchesAny = (file, patterns) => patterns.some((pattern) => matchesPattern(file, pattern));
47
+
48
+ const loadGates = () => readJson(gatesPath);
49
+
50
+ const edgeKey = (edge) => `${edge.from.name}->${edge.to.name}`;
51
+
52
+ const extraEdges = (manifest, records) => {
53
+ const byPath = new Map(records.map((record) => [record.path, record]));
54
+ const byName = new Map(records.map((record) => [record.name, record]));
55
+ const edges = [];
56
+ for (const edge of manifest.extraEdges ?? []) {
57
+ const from = byPath.get(edge.from) ?? byName.get(edge.from);
58
+ const to = byPath.get(edge.to) ?? byName.get(edge.to);
59
+ if (from !== undefined && to !== undefined && from.name !== to.name) {
60
+ edges.push({ from, to, source: "extra-edge", reason: edge.reason ?? "declared extra edge" });
61
+ }
62
+ }
63
+ return edges;
64
+ };
65
+
66
+ const affectedGraph = (manifest, records) => {
67
+ const sourceEdges = packageSourceImportEdges(repoRoot, records);
68
+ const manifestEdges = packageManifestDependencyEdges(repoRoot, records);
69
+ const tsconfigEdges = tsconfigReferenceEdges(repoRoot, records);
70
+ const declaredExtraEdges = extraEdges(manifest, records);
71
+ const edges = [...sourceEdges, ...manifestEdges, ...tsconfigEdges, ...declaredExtraEdges];
72
+ const manifestEdgeKeys = new Set(manifestEdges.map(edgeKey));
73
+ const dependencyDrift = sourceEdges
74
+ .filter((edge) => !manifestEdgeKeys.has(edgeKey(edge)))
75
+ .map((edge) => ({
76
+ from: edge.from.name,
77
+ to: edge.to.name,
78
+ file: edge.file,
79
+ specifier: edge.specifier,
80
+ reason: "source import edge is not declared in package manifest dependencies",
81
+ }));
82
+ const sourceEdgeKeys = new Set(sourceEdges.map(edgeKey));
83
+ const graphEdgeKeys = new Set(edges.map(edgeKey));
84
+ const fidelityFailures = [...sourceEdgeKeys]
85
+ .filter((key) => !graphEdgeKeys.has(key))
86
+ .map((key) => `affected graph is missing source import edge ${key}`);
87
+ return { edges, sourceEdges, dependencyDrift, fidelityFailures };
88
+ };
89
+
90
+ const owningPackage = (records, file) =>
91
+ records
92
+ .filter((record) => file === record.path || file.startsWith(`${record.path}/`))
93
+ .sort((left, right) => right.path.length - left.path.length)[0];
94
+
95
+ const reverseClosure = (records, edges, startNames) => {
96
+ const reverse = new Map(records.map((record) => [record.name, []]));
97
+ for (const edge of edges) reverse.get(edge.to.name)?.push(edge);
98
+ const affected = new Map();
99
+ const queue = [...startNames];
100
+ while (queue.length > 0) {
101
+ const name = queue.shift();
102
+ if (name === undefined || affected.has(name)) continue;
103
+ affected.set(name, true);
104
+ for (const edge of reverse.get(name) ?? []) queue.push(edge.from.name);
105
+ }
106
+ return [...affected.keys()].sort(compare);
107
+ };
108
+
109
+ const packageProof = (manifest, record) => {
110
+ const override = (manifest.packageOverrides ?? []).find(
111
+ (entry) => record.path === entry.path || record.name === entry.name,
112
+ );
113
+ return override ?? manifest.defaultPackageProof;
114
+ };
115
+
116
+ const proofClassCommand = (manifest, proofClass) => {
117
+ const command = manifest.proofClasses?.[proofClass]?.command;
118
+ if (typeof command !== "string" || command.length === 0) {
119
+ throw new Error(`${gatesPath}: proof class ${proofClass} has no command`);
120
+ }
121
+ return command;
122
+ };
123
+
124
+ const fullResult = (manifest, changedPaths, reason) => ({
125
+ mode: "full",
126
+ changedPaths,
127
+ proofClasses: [...(manifest.fullAffectedProofClasses ?? [])].sort(compare),
128
+ run: [...(manifest.fullAffectedProofClasses ?? [])].sort(compare).map((proofClass) => ({
129
+ proofClass,
130
+ command: proofClassCommand(manifest, proofClass),
131
+ reason,
132
+ })),
133
+ skip: [],
134
+ provenance: [{ kind: "full", reason }],
135
+ diagnostics: [],
136
+ });
137
+
138
+ const defaultBase = () => {
139
+ const detached = (() => {
140
+ try {
141
+ git(["symbolic-ref", "--quiet", "--short", "HEAD"]);
142
+ return false;
143
+ } catch {
144
+ return true;
145
+ }
146
+ })();
147
+ if (detached) return { ok: false, reason: "detached HEAD cannot derive a safe default base" };
148
+ if (git(["rev-parse", "--is-shallow-repository"]) === "true") {
149
+ return { ok: false, reason: "shallow repository cannot derive a safe default base" };
150
+ }
151
+ try {
152
+ return { ok: true, base: git(["merge-base", "HEAD", "main"]) };
153
+ } catch {
154
+ return { ok: false, reason: "git merge-base HEAD main failed" };
155
+ }
156
+ };
157
+
158
+ const changedPaths = ({ base, head }) => {
159
+ const paths = new Set();
160
+ if (head !== undefined) {
161
+ for (const file of gitLines(["diff", "--name-only", `${base}..${head}`])) paths.add(file);
162
+ return [...paths].sort(compare);
163
+ }
164
+ for (const file of gitLines(["diff", "--name-only", `${base}...HEAD`])) paths.add(file);
165
+ for (const file of gitLines(["diff", "--name-only"])) paths.add(file);
166
+ for (const file of gitLines(["diff", "--name-only", "--cached"])) paths.add(file);
167
+ return [...paths].sort(compare);
168
+ };
169
+
170
+ export const deriveAffectedGates = (options = {}) => {
171
+ const manifest = loadGates();
172
+ const records = workspacePackageRecords(repoRoot).filter((record) =>
173
+ record.name?.startsWith("@agent-os/"),
174
+ );
175
+ let changed;
176
+ if (options.changedPaths !== undefined) {
177
+ changed = [...options.changedPaths].sort(compare);
178
+ } else {
179
+ const baseResolution =
180
+ options.base === undefined ? defaultBase() : { ok: true, base: options.base };
181
+ if (!baseResolution.ok) return fullResult(manifest, [], baseResolution.reason);
182
+
183
+ try {
184
+ changed = changedPaths({ base: baseResolution.base, head: options.head });
185
+ } catch (error) {
186
+ return fullResult(
187
+ manifest,
188
+ [],
189
+ `git diff failed: ${error instanceof Error ? error.message : String(error)}`,
190
+ );
191
+ }
192
+ }
193
+ if (changed.length === 0) {
194
+ return {
195
+ mode: "affected",
196
+ changedPaths: [],
197
+ proofClasses: [],
198
+ run: [],
199
+ skip: (manifest.expensiveProofClasses ?? []).map((proofClass) => ({
200
+ proofClass,
201
+ reason: "no changed paths",
202
+ })),
203
+ provenance: [],
204
+ diagnostics: [],
205
+ };
206
+ }
207
+
208
+ for (const file of changed) {
209
+ for (const surface of manifest.globalSurfaces ?? []) {
210
+ if (matchesAny(file, surface.patterns ?? [])) {
211
+ return fullResult(manifest, changed, `${file}: ${surface.reason ?? "global surface"}`);
212
+ }
213
+ }
214
+ }
215
+
216
+ const graph = affectedGraph(manifest, records);
217
+ if (graph.fidelityFailures.length > 0) {
218
+ return fullResult(manifest, changed, graph.fidelityFailures.join("; "));
219
+ }
220
+
221
+ const owning = new Map();
222
+ const directProofClasses = new Map();
223
+ for (const file of changed) {
224
+ for (const rule of manifest.changedPathProofRules ?? []) {
225
+ if (matchesAny(file, rule.patterns ?? [])) {
226
+ for (const proofClass of rule.proofClasses ?? []) {
227
+ directProofClasses.set(proofClass, `${file}: ${rule.reason ?? "changed path rule"}`);
228
+ }
229
+ }
230
+ }
231
+ const owner = owningPackage(records, file);
232
+ if (owner === undefined) {
233
+ return fullResult(manifest, changed, `${file}: unknown path owner`);
234
+ }
235
+ owning.set(owner.name, owner);
236
+ }
237
+
238
+ const affectedNames = reverseClosure(records, graph.edges, owning.keys());
239
+ const affectedRecords = affectedNames
240
+ .map((name) => records.find((record) => record.name === name))
241
+ .filter(Boolean);
242
+ const proofReasons = new Map(directProofClasses);
243
+ for (const record of affectedRecords) {
244
+ const proof = packageProof(manifest, record);
245
+ for (const proofClass of proof?.affectedProofClasses ?? []) {
246
+ proofReasons.set(proofClass, `${record.name}: affected package declares ${proofClass} proof`);
247
+ }
248
+ }
249
+
250
+ const proofClasses = [...proofReasons.keys()].sort(compare);
251
+ const run = proofClasses.map((proofClass) => ({
252
+ proofClass,
253
+ command: proofClassCommand(manifest, proofClass),
254
+ reason: proofReasons.get(proofClass),
255
+ }));
256
+ const skip = (manifest.expensiveProofClasses ?? [])
257
+ .filter((proofClass) => !proofReasons.has(proofClass))
258
+ .sort(compare)
259
+ .map((proofClass) => ({
260
+ proofClass,
261
+ reason: "no affected package or changed-path rule declares this proof",
262
+ }));
263
+
264
+ return {
265
+ mode: "affected",
266
+ changedPaths: changed,
267
+ owningPackages: [...owning.values()].map((record) => record.name).sort(compare),
268
+ affectedPackages: affectedNames,
269
+ proofClasses,
270
+ run,
271
+ skip,
272
+ provenance: [
273
+ ...changed.map((file) => ({
274
+ kind: "changed-path",
275
+ file,
276
+ owner: owningPackage(records, file)?.name,
277
+ })),
278
+ ...affectedNames.map((name) => ({ kind: "affected-package", package: name })),
279
+ ],
280
+ diagnostics: graph.dependencyDrift,
281
+ };
282
+ };
283
+
284
+ export const printAffectedGates = (result, { json = false } = {}) => {
285
+ if (json) {
286
+ console.log(JSON.stringify(result, null, 2));
287
+ return;
288
+ }
289
+ console.log(`affected mode: ${result.mode}`);
290
+ for (const entry of result.run) {
291
+ console.log(`run ${entry.proofClass} <- ${entry.reason}`);
292
+ }
293
+ for (const entry of result.skip) {
294
+ console.log(`skip ${entry.proofClass} <- ${entry.reason}`);
295
+ }
296
+ for (const diagnostic of result.diagnostics ?? []) {
297
+ console.log(
298
+ `diag dependency-drift <- ${diagnostic.file}: ${diagnostic.from} imports ${diagnostic.specifier}`,
299
+ );
300
+ }
301
+ };
302
+
303
+ export const runAffectedGates = (result) => {
304
+ for (const entry of result.run) runCommand(entry.command, { cwd: repoRoot });
305
+ };