agent-doc-system 1.1.0 → 1.2.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/CHANGELOG.md +269 -0
- package/README.md +38 -0
- package/adapters/ansible.mjs +1 -1
- package/adapters/cache.mjs +8 -2
- package/adapters/contract-consumers.mjs +1 -1
- package/adapters/database.mjs +18 -4
- package/adapters/events.mjs +1 -1
- package/adapters/github-actions.mjs +244 -85
- package/adapters/go.mjs +1 -1
- package/adapters/index.mjs +3 -1
- package/adapters/object-store.mjs +2 -2
- package/adapters/python.mjs +1142 -0
- package/adapters/resource-client.mjs +1 -1
- package/adapters/rust.mjs +987 -0
- package/adapters/typescript.mjs +1 -1
- package/bin/agentdoc.mjs +2 -2
- package/core/audit.mjs +5 -3
- package/core/codes.mjs +4 -0
- package/core/compile.mjs +124 -11
- package/core/dependents.mjs +42 -0
- package/core/discover.mjs +60 -9
- package/core/fsx.mjs +17 -0
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +55 -44
- package/core/query.mjs +18 -8
- package/core/scaffold.mjs +87 -22
- package/core/sourcescan.mjs +11 -2
- package/core/toml.mjs +463 -140
- package/docs/ADAPTERS.md +196 -6
- package/docs/BENCHMARK.md +68 -0
- package/docs/CI.md +1 -1
- package/docs/EVALS.md +33 -2
- package/docs/MIGRATION.md +4 -2
- package/docs/QUERY.md +53 -4
- package/docs/SPEC.md +22 -0
- package/docs/ci/github-actions.yml +1 -1
- package/evals/harness.mjs +32 -6
- package/evals/scenarios/fixture-b-03-change-shared-lib.json +8 -1
- package/evals/scenarios/fixture-c-03-shared-vocabulary.json +7 -1
- package/evals/scenarios/fixture-e-01-change-shared-lib.json +14 -0
- package/evals/scenarios/fixture-e-02-change-manifest.json +9 -0
- package/evals/scenarios/fixture-e-03-local-module.json +9 -0
- package/evals/scenarios/fixture-e-04-ambiguous-crate.json +14 -0
- package/evals/scenarios/fixture-f-01-change-shared-lib.json +9 -0
- package/evals/scenarios/fixture-f-02-change-manifest.json +9 -0
- package/evals/scenarios/fixture-f-03-shadowed-import.json +9 -0
- package/evals/scenarios/fixture-f-04-nested-workspace.json +9 -0
- package/evals/scenarios/fixture-f-05-alembic-database.json +9 -0
- package/evals/scenarios/fixture-f-06-unittest-job.json +9 -0
- package/package.json +2 -1
- package/skills/agent-doc-system/references/create-migrate.md +13 -3
- package/templates/ci.yml +1 -1
|
@@ -1,106 +1,265 @@
|
|
|
1
|
-
// GitHub Actions
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// an agent changing a component is told which gate covers it, instead of
|
|
6
|
-
// guessing or running everything.
|
|
1
|
+
// GitHub Actions is verification authority, not evidence of an artifact.
|
|
2
|
+
// Scope each check from its own invocation; a mention elsewhere in a workflow
|
|
3
|
+
// says nothing about which component that command checks.
|
|
4
|
+
import path from "node:path";
|
|
7
5
|
import { Adapter } from "./registry.mjs";
|
|
8
6
|
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
9
7
|
|
|
10
8
|
export class GitHubActionsAdapter extends Adapter {
|
|
11
9
|
static adapterName = "github-actions";
|
|
12
10
|
constructor() {
|
|
13
|
-
super({ name: "github-actions", version: "1.
|
|
11
|
+
super({ name: "github-actions", version: "1.2.0", kind: "ci" });
|
|
14
12
|
}
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// file for provenance, so a directory marker can only fail. GitHub runs only
|
|
18
|
-
// the repository-root .github/workflows; a nested one (vendored upstream, a
|
|
19
|
-
// subtree) is inert and is ignored here too.
|
|
13
|
+
// GitHub runs only repository-root workflows. A workflow directory is not a
|
|
14
|
+
// unit marker, and nested workflows (vendored code, subtrees) are inert.
|
|
20
15
|
contracts(ctx) {
|
|
21
16
|
const dir = ".github/workflows";
|
|
22
17
|
if (!ctx.repo.isDir(dir)) return [];
|
|
23
|
-
const out = [];
|
|
24
|
-
const files = ctx.repo.walk(dir).filter((f) => /\.ya?ml$/.test(f)).sort();
|
|
25
18
|
const units = ctx.discovery.eligible.filter((u) => u.component);
|
|
26
|
-
for (const f of
|
|
27
|
-
let
|
|
28
|
-
try {
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
19
|
+
for (const f of ctx.repo.walk(dir).filter((f) => /\.ya?ml$/.test(f)).sort()) {
|
|
20
|
+
let doc;
|
|
21
|
+
try { doc = parseYamlDocuments(ctx.repo.readText(f), f)[0]?.doc; } catch { continue; }
|
|
22
|
+
const pr = ctx.prov.add("observed", f, "ci-extractor", []);
|
|
23
|
+
const filters = Object.values(doc?.on || {}).flatMap((trigger) => Array.isArray(trigger?.paths) ? trigger.paths : []);
|
|
24
|
+
for (const [jobName, job] of Object.entries(doc?.jobs || {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
25
|
+
for (const [i, step] of (job?.steps || []).entries()) {
|
|
26
|
+
const action = typeof step?.uses === "string";
|
|
27
|
+
let cwd = action
|
|
28
|
+
? step.with?.["working-directory"] ?? step.with?.working_directory ?? step.with?.path
|
|
29
|
+
: step?.["working-directory"] ?? job.defaults?.run?.["working-directory"] ?? doc.defaults?.run?.["working-directory"];
|
|
30
|
+
const commands = action ? [{ command: step.uses }] : typeof step?.run === "string" ? simpleCommands(step.run) : [];
|
|
31
|
+
for (const [n, { command, words }] of commands.entries()) {
|
|
32
|
+
if (!action && words[0] === "cd") {
|
|
33
|
+
const target = words[1] === "--" ? words.slice(2) : words.slice(1);
|
|
34
|
+
// Only a single literal directory can establish the context of
|
|
35
|
+
// following commands. Resolve successive changes from that cwd.
|
|
36
|
+
const directory = target.length === 1 ? resolveDirectory(cwd, target[0]) : null;
|
|
37
|
+
if (!directory) break;
|
|
38
|
+
cwd = directory;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!action && ["pushd", "popd", "source", ".", "eval", "export"].includes(words[0])) break;
|
|
42
|
+
const invocations = action ? actionInvocations(command) : ciInvocations(words, cwd, ctx.repo);
|
|
43
|
+
if (command.length > 600) continue;
|
|
44
|
+
for (const [task, invocation] of invocations.entries()) {
|
|
45
|
+
const { tier, args, directory: invocationDirectory } = invocation;
|
|
46
|
+
const scopeCwd = invocationDirectory === undefined ? cwd : resolveDirectory(cwd, invocationDirectory);
|
|
47
|
+
if (invocationDirectory !== undefined && !scopeCwd) continue;
|
|
48
|
+
const covered = units.filter((u) => scopedTo(u, args, scopeCwd, filters, units, ctx.repo)).map((u) => u.component.ref).sort();
|
|
49
|
+
const suffix = f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + jobName + ":" + i + (commands.length > 1 ? ":" + n : "") + (invocations.length > 1 ? ":task:" + task : "");
|
|
50
|
+
const id = (covered.length ? "ci:" : "repo-gate:") + suffix;
|
|
51
|
+
const directory = literalPath(cwd);
|
|
52
|
+
const record = {
|
|
53
|
+
id, tier,
|
|
54
|
+
command: !action && directory && directory !== "." ? "cd " + shellQuote(directory) + " && " + command : command,
|
|
55
|
+
configPaths: [f], provRecs: [pr],
|
|
56
|
+
};
|
|
57
|
+
if (covered.length) ctx.addVerification({ ...record, componentRefs: covered });
|
|
58
|
+
else ctx.addGate(record);
|
|
59
|
+
pr.subjects.add((covered.length ? "ver:" : "gate:") + id);
|
|
60
|
+
}
|
|
46
61
|
}
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
|
-
const covered = new Set();
|
|
50
|
-
for (const u of units) {
|
|
51
|
-
if (text.includes(u.root + "/") || (u.pkgName && text.includes(u.pkgName))) covered.add(u.component.ref);
|
|
52
|
-
}
|
|
53
|
-
const pr = ctx.prov.add("observed", f, "ci-extractor", []);
|
|
54
|
-
const subjects = [];
|
|
55
|
-
steps.forEach((s, i) => {
|
|
56
|
-
if (!/[a-z0-9]/i.test(s.run)) return;
|
|
57
|
-
const command = s.run.trim().slice(0, 600);
|
|
58
|
-
// A step that installs dependencies or checks out code is not a check.
|
|
59
|
-
// Recording it as one would bury the commands that actually gate.
|
|
60
|
-
const tier = ciTier(command);
|
|
61
|
-
if (!tier) return;
|
|
62
|
-
const id = "ci:" + f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + s.job + ":" + i;
|
|
63
|
-
if (covered.size) {
|
|
64
|
-
ctx.addVerification({
|
|
65
|
-
id,
|
|
66
|
-
componentRefs: [...covered].sort(),
|
|
67
|
-
tier,
|
|
68
|
-
command,
|
|
69
|
-
configPaths: [f],
|
|
70
|
-
provRecs: [pr],
|
|
71
|
-
});
|
|
72
|
-
subjects.push("ver:" + id);
|
|
73
|
-
} else {
|
|
74
|
-
// A gate that names no component is repository-wide: it applies to
|
|
75
|
-
// everything, so it is recorded once as a gate rather than
|
|
76
|
-
// duplicated onto every component.
|
|
77
|
-
// The graph id must not itself start with the provenance prefix, or
|
|
78
|
-
// the subject key becomes ambiguous. "repo-gate:" is unambiguous.
|
|
79
|
-
const gid = "repo-gate:" + f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + s.job + ":" + i;
|
|
80
|
-
ctx.addGate({ id: gid, tier: ciTier(command), command, configPaths: [f], provRecs: [pr] });
|
|
81
|
-
subjects.push("gate:" + gid);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
for (const s of subjects) pr.subjects.add(s);
|
|
85
|
-
void steps;
|
|
86
|
-
void covered;
|
|
87
64
|
}
|
|
88
|
-
return
|
|
65
|
+
return [];
|
|
89
66
|
}
|
|
90
67
|
}
|
|
91
68
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
69
|
+
// Read only simple shell invocations, splitting newline, `;` and `&&` outside
|
|
70
|
+
// quotes. Control flow, pipelines, substitutions and heredocs require shell
|
|
71
|
+
// interpretation, so refuse the step rather than extract text inside them.
|
|
72
|
+
function simpleCommands(run) {
|
|
73
|
+
run = run.replace(/\\\r?\n/g, " ");
|
|
74
|
+
const out = [];
|
|
75
|
+
let words = [], word = "", quote = null, start = 0;
|
|
76
|
+
const endWord = () => { if (word) words.push(word); word = ""; };
|
|
77
|
+
const endCommand = (end) => {
|
|
78
|
+
endWord();
|
|
79
|
+
if (words.length) out.push({ command: run.slice(start, end).trim(), words });
|
|
80
|
+
words = [];
|
|
81
|
+
};
|
|
82
|
+
for (let i = 0; i < run.length; i++) {
|
|
83
|
+
const c = run[i];
|
|
84
|
+
if (c === "`" || (c === "$" && run[i + 1] === "(")) return [];
|
|
85
|
+
if (c === "\\" && quote !== "'") {
|
|
86
|
+
const next = run[++i];
|
|
87
|
+
if (next !== "\n" && next !== undefined) word += next;
|
|
88
|
+
} else if (quote) {
|
|
89
|
+
if (c === quote) quote = null;
|
|
90
|
+
else if (c === "\n") return [];
|
|
91
|
+
else word += c;
|
|
92
|
+
} else if (c === '"' || c === "'") quote = c;
|
|
93
|
+
else if (c === "#" && !word) {
|
|
94
|
+
endCommand(i);
|
|
95
|
+
while (i < run.length && run[i] !== "\n") i++;
|
|
96
|
+
start = i + 1;
|
|
97
|
+
} else if (c === "\n" || c === ";" || (c === "&" && run[i + 1] === "&")) {
|
|
98
|
+
endCommand(i);
|
|
99
|
+
if (c === "&") i++;
|
|
100
|
+
start = i + 1;
|
|
101
|
+
} else if (/[|&<>(){}]/.test(c)) return [];
|
|
102
|
+
else if (/\s/.test(c)) endWord();
|
|
103
|
+
else word += c;
|
|
104
|
+
}
|
|
105
|
+
if (quote) return [];
|
|
106
|
+
endCommand(run.length);
|
|
107
|
+
if (out.some(({ words: [first] }) => /^(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|function)$/.test(first))) return [];
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
95
110
|
|
|
96
|
-
function
|
|
97
|
-
if (
|
|
98
|
-
if (
|
|
99
|
-
if (/
|
|
100
|
-
if (/
|
|
101
|
-
if (
|
|
102
|
-
if (
|
|
103
|
-
if (
|
|
104
|
-
if (/\bbuild\b|tsc|compile/.test(run)) return "build";
|
|
111
|
+
function taskTier(task) {
|
|
112
|
+
if (!/^(?:test|tests|e2e|integration|lint|typecheck|type-check|build|compile|check|api:check|contract|catalog|drift)(?:$|[:_-])/.test(task || "")) return null;
|
|
113
|
+
if (/(?:^|[:_-])(?:e2e|playwright|cypress)(?:$|[:_-])/.test(task)) return "e2e";
|
|
114
|
+
if (/(?:contract|openapi|api:check|drift|catalog)/.test(task)) return "contract";
|
|
115
|
+
if (/(?:^|[:_-])integration(?:$|[:_-])/.test(task)) return "integration";
|
|
116
|
+
if (/^(?:lint|typecheck|type-check)/.test(task)) return "static";
|
|
117
|
+
if (/^(?:tests?|check)(?:$|[:_-])/.test(task)) return "unit";
|
|
118
|
+
if (/^(?:build|compile)(?:$|[:_-])/.test(task)) return "build";
|
|
105
119
|
return null;
|
|
106
120
|
}
|
|
121
|
+
|
|
122
|
+
const VALUE_OPTIONS = new Set(["--filter", "-F", "--prefix", "--dir", "--cwd", "-C", "--workspace", "-w"]);
|
|
123
|
+
function ciInvocations(words, cwd, repo) {
|
|
124
|
+
let [exe, ...args] = words;
|
|
125
|
+
if (exe === "env") [exe, ...args] = args;
|
|
126
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(exe || "")) [exe, ...args] = args;
|
|
127
|
+
if (["uv", "poetry"].includes(exe) && args[0] === "run") [exe, ...args] = args.slice(1);
|
|
128
|
+
if (["npx", "bunx"].includes(exe)) [exe, ...args] = args;
|
|
129
|
+
const result = (tier, scopeArgs = args) => tier ? [{ tier, args: scopeArgs }] : [];
|
|
130
|
+
if (["npm", "pnpm", "yarn", "bun"].includes(exe)) {
|
|
131
|
+
const selectors = [];
|
|
132
|
+
while (args[0]?.startsWith("-")) {
|
|
133
|
+
const option = args.shift();
|
|
134
|
+
selectors.push(option);
|
|
135
|
+
if (VALUE_OPTIONS.has(option) && args.length) selectors.push(args.shift());
|
|
136
|
+
}
|
|
137
|
+
if (["exec", "dlx"].includes(args[0])) {
|
|
138
|
+
return ciInvocations(args.slice(1), cwd, repo).map((inner) => ({ ...inner, args: [...selectors, ...inner.args] }));
|
|
139
|
+
}
|
|
140
|
+
if (["turbo", "nx"].includes(args[0])) {
|
|
141
|
+
return ciInvocations(args, cwd, repo).map((inner) => ({ ...inner, args: [...selectors, ...inner.args] }));
|
|
142
|
+
}
|
|
143
|
+
if (args[0] === "run") args.shift();
|
|
144
|
+
return result(taskTier(args[0]), [...selectors, ...args.slice(1)]);
|
|
145
|
+
}
|
|
146
|
+
if (exe === "node" && args.includes("--test")) return result("unit");
|
|
147
|
+
if (/^python(?:3(?:\.\d+)?)?$/.test(exe) && args[0] === "-m") return ciInvocations(args.slice(1), cwd, repo);
|
|
148
|
+
if (["bash", "sh", "node"].includes(exe) || /^python(?:3(?:\.\d+)?)?$/.test(exe)) {
|
|
149
|
+
return scriptInvocation(args[0], args.slice(1), cwd, repo);
|
|
150
|
+
}
|
|
151
|
+
if (["pytest", "unittest", "vitest", "jest", "mocha", "ava"].includes(exe)) return result("unit");
|
|
152
|
+
if (["playwright", "cypress"].includes(exe) && ["test", "run"].includes(args[0])) return result("e2e", args.slice(1));
|
|
153
|
+
if (["eslint", "mypy", "golangci-lint"].includes(exe)) return result("static");
|
|
154
|
+
if (["ruff", "biome"].includes(exe) && ["check", "lint"].includes(args[0])) return result("static", args.slice(1));
|
|
155
|
+
if (exe === "tsc") return result(args.includes("--noEmit") ? "static" : "build");
|
|
156
|
+
if (["go", "cargo", "dotnet"].includes(exe)) return result(({ test: "unit", vet: "static", clippy: "static", build: "build" })[args[0]], args.slice(1));
|
|
157
|
+
if (["turbo", "nx", "make", "just"].includes(exe)) return taskInvocations(exe, args);
|
|
158
|
+
if (exe === "agentdoc" && args[0] === "compile") return result("build", args.slice(1));
|
|
159
|
+
if (exe?.includes("/")) return scriptInvocation(exe, args, cwd, repo);
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function scriptInvocation(script, args, cwd, repo) {
|
|
164
|
+
if (typeof script !== "string") return [];
|
|
165
|
+
const tier = taskTier(path.posix.basename(script, path.posix.extname(script)));
|
|
166
|
+
const file = tier && resolveDirectory(cwd, script);
|
|
167
|
+
// A check-like basename alone does not prove a repository script. Resolve
|
|
168
|
+
// from the step directory and keep only literal files inside the checkout.
|
|
169
|
+
if (!file || !repo.exists(file) || repo.isDir(file)) return [];
|
|
170
|
+
return [{ tier, args: [script, ...args] }];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Task names determine tiers; option values are never mistaken for tasks or
|
|
174
|
+
// free-form path scope. Multi-task runs contribute one record per tier.
|
|
175
|
+
function taskInvocations(exe, args) {
|
|
176
|
+
const tasks = [], selectors = [];
|
|
177
|
+
let directory;
|
|
178
|
+
const valueOptions = new Set(["--filter", "-F", "-C", "--directory", "--cwd", "-t", "--target", "--targets", "--projects", "--configuration", "-c", "--parallel", "--concurrency", "--cache-dir", "--output-logs"]);
|
|
179
|
+
for (let i = 0; i < args.length; i++) {
|
|
180
|
+
const token = args[i];
|
|
181
|
+
if (token === "--") break;
|
|
182
|
+
const [option, inline] = token.split(/=(.*)/s);
|
|
183
|
+
if (token.startsWith("-")) {
|
|
184
|
+
const value = inline ?? (valueOptions.has(option) ? args[++i] : undefined);
|
|
185
|
+
if (["--filter", "-F"].includes(option)) selectors.push(option, value || "");
|
|
186
|
+
if (exe === "nx" && option === "--projects") {
|
|
187
|
+
for (const project of (value || "").split(",")) selectors.push("--project", project);
|
|
188
|
+
}
|
|
189
|
+
if (exe === "nx" && ["-t", "--target", "--targets"].includes(option)) tasks.push(...(value || "").split(","));
|
|
190
|
+
if (exe === "make" && ["-C", "--directory"].includes(option)) directory = directory === undefined ? value ?? null : resolveDirectory(directory, value);
|
|
191
|
+
if (exe === "turbo" && option === "--cwd") directory = value ?? null;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (exe === "nx") {
|
|
195
|
+
if (args[0] === "run" && i === 1) {
|
|
196
|
+
const [project, task] = token.split(":");
|
|
197
|
+
selectors.push("--project", project);
|
|
198
|
+
tasks.push(task);
|
|
199
|
+
} else if (["run-many", "affected"].includes(args[0]) && i > 0) tasks.push(...token.split(","));
|
|
200
|
+
} else if (!(exe === "turbo" && token === "run") && !token.includes("=")) tasks.push(token);
|
|
201
|
+
}
|
|
202
|
+
return [...new Set(tasks.map(taskTier).filter(Boolean))].map((tier) => ({ tier, args: selectors, directory }));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function actionInvocations(action) {
|
|
206
|
+
const name = action.split("@")[0].toLowerCase();
|
|
207
|
+
if (/setup|checkout|cache|upload|download|artifact/.test(name) || !/lint|test|check|scan|audit/.test(name)) return [];
|
|
208
|
+
const tier = /e2e|playwright|cypress/.test(name) ? "e2e"
|
|
209
|
+
: /contract|openapi|drift|catalog/.test(name) ? "contract"
|
|
210
|
+
: /integration/.test(name) ? "integration"
|
|
211
|
+
: /lint|typecheck|type-check|scan|audit/.test(name) ? "static"
|
|
212
|
+
: /test/.test(name) ? "unit" : "static";
|
|
213
|
+
return [{ tier, args: [] }];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveDirectory(cwd, target) {
|
|
217
|
+
if (typeof target !== "string" || !target || target.startsWith("-") || /[$`*?!{}\[\]~]/.test(target) || path.posix.isAbsolute(target)) return null;
|
|
218
|
+
const base = cwd === undefined ? "." : literalPath(cwd);
|
|
219
|
+
return base ? literalPath(path.posix.join(base, target)) : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function literalPath(value) {
|
|
223
|
+
if (typeof value !== "string" || !value || /[$`*?!{}]/.test(value) || path.posix.isAbsolute(value)) return null;
|
|
224
|
+
const p = path.posix.normalize(value);
|
|
225
|
+
return p === ".." || p.startsWith("../") ? null : p.replace(/\/$/, "");
|
|
226
|
+
}
|
|
227
|
+
function within(p, root) { return root === "." || p === root || p.startsWith(root + "/"); }
|
|
228
|
+
function shellQuote(p) { return /^[A-Za-z0-9_./-]+$/.test(p) ? p : "'" + p.replace(/'/g, "'\\''") + "'"; }
|
|
229
|
+
|
|
230
|
+
function scopedTo(unit, args, cwd, filters, units, repo) {
|
|
231
|
+
const root = path.posix.normalize(unit.root);
|
|
232
|
+
const directory = literalPath(cwd);
|
|
233
|
+
// A directory belongs to its deepest enclosing component, not every parent.
|
|
234
|
+
if (directory && within(directory, root) && !units.some((u) => u.root !== unit.root && within(directory, u.root) && within(u.root, root))) return true;
|
|
235
|
+
if (cwd !== undefined && !directory) return false;
|
|
236
|
+
for (let i = 0; i < args.length; i++) {
|
|
237
|
+
const token = args[i];
|
|
238
|
+
const [option, inline] = token.split(/=(.*)/s);
|
|
239
|
+
const value = inline ?? args[i + 1];
|
|
240
|
+
if (option === "--project") {
|
|
241
|
+
let name = unit.pkgName;
|
|
242
|
+
const config = path.posix.join(unit.root, "project.json");
|
|
243
|
+
try { if (repo.exists(config)) name = repo.readJson(config).name || name; } catch { /* an unreadable project config cannot name a scope */ }
|
|
244
|
+
if (value === name || value === unit.root) return true;
|
|
245
|
+
i++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// Package names are scope only in an explicit package selector.
|
|
249
|
+
if (unit.pkgName && ["--filter", "-F", "--workspace", "-w"].includes(option) && value === unit.pkgName) return true;
|
|
250
|
+
const arg = token.startsWith("-") ? inline : token;
|
|
251
|
+
if (!arg || /[$`]/.test(arg)) continue;
|
|
252
|
+
if (path.posix.isAbsolute(arg)) continue;
|
|
253
|
+
const p = literalPath(path.posix.join(directory || ".", arg.replace(/\/[*?].*$/, "")));
|
|
254
|
+
if (p && root !== "." && within(p, root)) return true;
|
|
255
|
+
}
|
|
256
|
+
// A literal prefix in a workflow trigger is evidence of component scope.
|
|
257
|
+
// Negative filters cannot create scope and can exclude an entire root.
|
|
258
|
+
const matches = (filter) => {
|
|
259
|
+
if (typeof filter !== "string") return false;
|
|
260
|
+
const prefix = literalPath(filter.replace(/\/[*?].*$/, ""));
|
|
261
|
+
return prefix && prefix !== "." && (within(prefix, root) || within(root, prefix));
|
|
262
|
+
};
|
|
263
|
+
return filters.some((p) => typeof p === "string" && !p.startsWith("!") && matches(p)) &&
|
|
264
|
+
!filters.some((p) => typeof p === "string" && p.startsWith("!") && within(root, literalPath(p.slice(1).replace(/\/\*\*$/, "")) || "\0"));
|
|
265
|
+
}
|
package/adapters/go.mjs
CHANGED
|
@@ -61,7 +61,7 @@ export class GoAdapter extends Adapter {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const hasTests = repo.
|
|
64
|
+
const hasTests = repo.walkUnit(unit.root).some((f) => /_test\.go$/.test(f));
|
|
65
65
|
ctx.addVerification({
|
|
66
66
|
id: compRef + ":go-test",
|
|
67
67
|
componentRefs: [compRef],
|
package/adapters/index.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { PnpmAdapter } from "./pnpm.mjs";
|
|
|
8
8
|
import { TurborepoAdapter } from "./turborepo.mjs";
|
|
9
9
|
import { TypeScriptAdapter } from "./typescript.mjs";
|
|
10
10
|
import { GoAdapter } from "./go.mjs";
|
|
11
|
+
import { RustAdapter } from "./rust.mjs";
|
|
12
|
+
import { PythonAdapter } from "./python.mjs";
|
|
11
13
|
import { OpenApiAdapter } from "./openapi.mjs";
|
|
12
14
|
import { ProtobufAdapter } from "./protobuf.mjs";
|
|
13
15
|
import { GraphQLAdapter } from "./graphql.mjs";
|
|
@@ -25,7 +27,7 @@ import { AnsibleAdapter } from "./ansible.mjs";
|
|
|
25
27
|
const REGISTRY = new Map();
|
|
26
28
|
for (const C of [
|
|
27
29
|
GenericAdapter, NodeAdapter, PnpmAdapter, TurborepoAdapter, TypeScriptAdapter,
|
|
28
|
-
GoAdapter, OpenApiAdapter, ProtobufAdapter, GraphQLAdapter, OidcAdapter,
|
|
30
|
+
GoAdapter, RustAdapter, PythonAdapter, OpenApiAdapter, ProtobufAdapter, GraphQLAdapter, OidcAdapter,
|
|
29
31
|
GitHubActionsAdapter, DockerfileAdapter, WranglerAdapter, DatabaseAdapter,
|
|
30
32
|
EventsAdapter, DeployConfigAdapter, ObjectStoreAdapter, CacheAdapter, AnsibleAdapter,
|
|
31
33
|
]) {
|
|
@@ -10,7 +10,7 @@ const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts|go|py|rb|java|kt|rs)$/;
|
|
|
10
10
|
const TEST_PATH = /(^|\/)(tests?|__tests__|spec)\//;
|
|
11
11
|
const TEST_FILE = /\.(test|spec)\./;
|
|
12
12
|
const BUCKET_LITERAL = /["'`]([a-z][a-z0-9-]{4,60})["'`]/g;
|
|
13
|
-
const STORE_HINT = /(@aws-sdk\/client-s3|minio|google\.cloud\.storage|\bS3\b|bucket|Bucket|BlobService|createBucket)/i;
|
|
13
|
+
const STORE_HINT = /(@aws-sdk\/client-s3|aws-sdk-s3|aws_sdk_s3|minio|google\.cloud\.storage|boto3|\bS3\b|bucket|Bucket|BlobService|createBucket)/i;
|
|
14
14
|
|
|
15
15
|
export class ObjectStoreAdapter extends Adapter {
|
|
16
16
|
static adapterName = "object-store";
|
|
@@ -24,7 +24,7 @@ export class ObjectStoreAdapter extends Adapter {
|
|
|
24
24
|
const usesStore = ctx.unitImports && [...ctx.unitImports.keys()].some((k) => /s3|minio|storage|blob/i.test(k));
|
|
25
25
|
const bindings = [];
|
|
26
26
|
const claimed = new Set();
|
|
27
|
-
for (const f of repo.
|
|
27
|
+
for (const f of repo.walkUnit(unit.root)) {
|
|
28
28
|
if (!SRC_EXT.test(f)) continue;
|
|
29
29
|
if (TEST_PATH.test(f) || TEST_FILE.test(f)) continue;
|
|
30
30
|
const text = repo.readText(f);
|