agent-doc-system 1.1.0 → 1.3.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 +402 -0
- package/README.md +38 -0
- package/adapters/ansible.mjs +1 -1
- package/adapters/cache.mjs +9 -2
- package/adapters/contract-consumers.mjs +1 -1
- package/adapters/database.mjs +19 -4
- package/adapters/deploy-config.mjs +3 -2
- package/adapters/dockerfile.mjs +3 -2
- package/adapters/events.mjs +1 -1
- package/adapters/generic.mjs +2 -1
- package/adapters/github-actions.mjs +245 -85
- package/adapters/go.mjs +5 -4
- package/adapters/index.mjs +3 -1
- package/adapters/node.mjs +3 -2
- package/adapters/object-store.mjs +2 -2
- package/adapters/python.mjs +1142 -0
- package/adapters/resource-client.mjs +1 -1
- package/adapters/rust.mjs +990 -0
- package/adapters/turborepo.mjs +3 -2
- package/adapters/typescript.mjs +7 -5
- package/adapters/wrangler.mjs +3 -2
- package/bin/agentdoc.mjs +11 -4
- package/bin/usage.txt +2 -0
- package/core/audit.mjs +12 -8
- package/core/codes.mjs +4 -0
- package/core/compile.mjs +131 -16
- package/core/dependents.mjs +42 -0
- package/core/derive.mjs +4 -3
- package/core/descriptors.mjs +3 -2
- package/core/discover.mjs +61 -9
- package/core/fsx.mjs +185 -36
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +66 -48
- package/core/paths.mjs +71 -0
- package/core/query.mjs +76 -31
- package/core/scaffold.mjs +216 -44
- package/core/sourcescan.mjs +14 -2
- package/core/toml.mjs +463 -140
- package/docs/ADAPTERS.md +196 -6
- package/docs/BENCHMARK.md +130 -0
- package/docs/CI.md +7 -1
- package/docs/EVALS.md +239 -16
- package/docs/MIGRATION.md +26 -6
- package/docs/PROVENANCE.md +6 -0
- package/docs/QUERY.md +78 -7
- package/docs/SPEC.md +69 -2
- package/docs/ci/github-actions.yml +1 -1
- package/evals/baseline-search.mjs +272 -0
- package/evals/facts.mjs +28 -0
- package/evals/harness.mjs +149 -119
- 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 +5 -2
- package/skills/agent-doc-system/SKILL.md +2 -1
- package/skills/agent-doc-system/references/cli.md +6 -1
- package/skills/agent-doc-system/references/create-migrate.md +21 -5
- package/templates/ci.yml +1 -1
package/adapters/turborepo.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// verification answer available for a monorepo unit.
|
|
5
5
|
import { Adapter } from "./registry.mjs";
|
|
6
6
|
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
7
|
+
import { isRepoRoot, joinPath } from "../core/paths.mjs";
|
|
7
8
|
|
|
8
9
|
// Whether a declared task is a check is decided by `taskTier` alone, not by a
|
|
9
10
|
// second, narrower pattern. Two lists meant two places to disagree: a task that
|
|
@@ -19,7 +20,7 @@ export class TurborepoAdapter extends Adapter {
|
|
|
19
20
|
super({ name: "turborepo", version: "1.0.0", kind: "ci" });
|
|
20
21
|
}
|
|
21
22
|
detect(ctx, root) {
|
|
22
|
-
const p = root
|
|
23
|
+
const p = joinPath(root, "turbo.json");
|
|
23
24
|
if (!ctx.repo.exists(p)) return null;
|
|
24
25
|
return { path: p, shape: "importable" };
|
|
25
26
|
}
|
|
@@ -122,7 +123,7 @@ function taskTier(t) {
|
|
|
122
123
|
|
|
123
124
|
// A task graph normally lives at the workspace root, not in each package.
|
|
124
125
|
function nearestTurboConfig(repo, root) {
|
|
125
|
-
let dir = root;
|
|
126
|
+
let dir = isRepoRoot(root) ? "" : root;
|
|
126
127
|
for (let i = 0; i < 12; i++) {
|
|
127
128
|
const p = dir ? dir + "/turbo.json" : "turbo.json";
|
|
128
129
|
if (repo.exists(p)) return p;
|
package/adapters/typescript.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { Adapter } from "./registry.mjs";
|
|
8
8
|
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
9
|
+
import { joinPath } from "../core/paths.mjs";
|
|
9
10
|
|
|
10
11
|
export class TypeScriptAdapter extends Adapter {
|
|
11
12
|
static adapterName = "typescript";
|
|
@@ -13,13 +14,13 @@ export class TypeScriptAdapter extends Adapter {
|
|
|
13
14
|
super({ name: "typescript", version: "1.0.0", kind: "unit-marker" });
|
|
14
15
|
}
|
|
15
16
|
detect(ctx, root) {
|
|
16
|
-
const p = root
|
|
17
|
+
const p = joinPath(root, "tsconfig.json");
|
|
17
18
|
if (!ctx.repo.exists(p)) return null;
|
|
18
19
|
return { path: p, shape: "importable" };
|
|
19
20
|
}
|
|
20
21
|
extract(ctx, unit) {
|
|
21
22
|
const { repo, prov, compRef } = ctx;
|
|
22
|
-
const p = unit.root
|
|
23
|
+
const p = joinPath(unit.root, "tsconfig.json");
|
|
23
24
|
if (!repo.exists(p)) return {};
|
|
24
25
|
const derived = { languages: new Set(["typescript"]), runtimes: new Set(["node"]) };
|
|
25
26
|
let ts;
|
|
@@ -43,8 +44,9 @@ export class TypeScriptAdapter extends Adapter {
|
|
|
43
44
|
// real HTTP surface and must be visible to the query surface.
|
|
44
45
|
const routes = [];
|
|
45
46
|
for (const base of ["src/app", "app", "src/pages", "pages"]) {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const dir = joinPath(unit.root, base);
|
|
48
|
+
if (!repo.isDir(dir)) continue;
|
|
49
|
+
for (const f of repo.walkUnit(unit.root, dir)) {
|
|
48
50
|
const routePath = routeFor(unit.root, base, f);
|
|
49
51
|
if (!routePath) continue;
|
|
50
52
|
const rpr = prov.add("observed", f, "route-extractor", ["der:" + compRef + ":/routes"]);
|
|
@@ -63,7 +65,7 @@ export class TypeScriptAdapter extends Adapter {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function routeFor(root, base, file) {
|
|
66
|
-
const rel = file.slice((root
|
|
68
|
+
const rel = file.slice(joinPath(root, base).length + 1);
|
|
67
69
|
if (base.endsWith("app")) {
|
|
68
70
|
if (!rel.endsWith("/route.ts") && !rel.endsWith("/route.tsx") && !rel.endsWith("/route.js")) return null;
|
|
69
71
|
const segs = rel.split("/").slice(0, -1);
|
package/adapters/wrangler.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Adapter } from "./registry.mjs";
|
|
12
12
|
import { parse as parseToml } from "../core/toml.mjs";
|
|
13
13
|
import { sourceFiles } from "../core/sourcescan.mjs";
|
|
14
|
+
import { joinPath } from "../core/paths.mjs";
|
|
14
15
|
|
|
15
16
|
const MANIFESTS = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"];
|
|
16
17
|
|
|
@@ -21,13 +22,13 @@ export class WranglerAdapter extends Adapter {
|
|
|
21
22
|
}
|
|
22
23
|
detect(ctx, root) {
|
|
23
24
|
for (const n of MANIFESTS) {
|
|
24
|
-
if (ctx.repo.exists(root
|
|
25
|
+
if (ctx.repo.exists(joinPath(root, n))) return { path: joinPath(root, n), shape: "deployable" };
|
|
25
26
|
}
|
|
26
27
|
return null;
|
|
27
28
|
}
|
|
28
29
|
extract(ctx, unit) {
|
|
29
30
|
const { repo, prov, compRef } = ctx;
|
|
30
|
-
const file = MANIFESTS.map((n) => unit.root
|
|
31
|
+
const file = MANIFESTS.map((n) => joinPath(unit.root, n)).find((p) => repo.exists(p));
|
|
31
32
|
if (!file) return {};
|
|
32
33
|
let toml;
|
|
33
34
|
try {
|
package/bin/agentdoc.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { observationFreshness, stalenessDiagnostics } from "../core/observations
|
|
|
19
19
|
import { assertNoSecretsInText } from "../core/secrets.mjs";
|
|
20
20
|
import { installTemplates } from "../core/scaffold.mjs";
|
|
21
21
|
import { COMMANDS, flagsOf, parseArgs, commandHelp, UsageError } from "./commands.mjs";
|
|
22
|
-
import { loadSchemaBundle, loadConfig } from "../core/descriptors.mjs";
|
|
22
|
+
import { loadSchemaBundle, loadConfig, CONFIG_PATH } from "../core/descriptors.mjs";
|
|
23
23
|
import { execFileSync } from "node:child_process";
|
|
24
24
|
|
|
25
25
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -275,9 +275,9 @@ try {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
const ctx = queryContext(graph, needle, { budget });
|
|
278
|
-
if (opt("md")) out(renderContext(ctx));
|
|
278
|
+
if (opt("md")) out(renderContext(ctx, { budget }));
|
|
279
279
|
else if (opt("json")) out(ctx, true);
|
|
280
|
-
else out(renderContext(ctx));
|
|
280
|
+
else out(renderContext(ctx, { budget }));
|
|
281
281
|
} else if (cmd === "impact") {
|
|
282
282
|
// Impact answers a question about the working tree, so it compiles in
|
|
283
283
|
// memory rather than reading the committed graph. Refusing a stale graph
|
|
@@ -353,7 +353,14 @@ try {
|
|
|
353
353
|
: {}),
|
|
354
354
|
}, true);
|
|
355
355
|
} else if (cmd === "init") {
|
|
356
|
-
|
|
356
|
+
// An existing configuration is the authority on where documentation lives
|
|
357
|
+
// (issue #49). Load it through the compiler's own loader, so init, scaffold,
|
|
358
|
+
// audit and compile cannot disagree, and so an invalid or escaping docs path
|
|
359
|
+
// is rejected before anything is written.
|
|
360
|
+
const docsConfig = fs.existsSync(path.join(ROOT, CONFIG_PATH))
|
|
361
|
+
? loadConfig(new Repo(ROOT, { noGit: true }), loadSchemaBundle().bundle)
|
|
362
|
+
: null;
|
|
363
|
+
const result = installTemplates(null, null, { write: true, root: ROOT, force: opt("force"), docsConfig });
|
|
357
364
|
out(result, true);
|
|
358
365
|
} else if (cmd === "eval") {
|
|
359
366
|
const which = positional[0] || "routing";
|
package/bin/usage.txt
CHANGED
|
@@ -5,6 +5,8 @@ agentdoc — portable documentation & software-catalog system
|
|
|
5
5
|
agentdoc <command> --help every flag one command accepts (also: agentdoc help <command>)
|
|
6
6
|
|
|
7
7
|
agentdoc init [--force] write a configuration and documentation skeleton
|
|
8
|
+
(docs skeleton goes to the configured docs.* paths;
|
|
9
|
+
agentdoc/docs/ when there is no configuration yet)
|
|
8
10
|
agentdoc audit inventory the repository, classify every fact
|
|
9
11
|
agentdoc scaffold [--write] generate a CREATE-mode skeleton from the audit
|
|
10
12
|
agentdoc validate sources + schemas + semantic checks
|
package/core/audit.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { classifyContractFile } from "./contracts.mjs";
|
|
|
19
19
|
import { ContextIndex } from "./query.mjs";
|
|
20
20
|
import { expandGlob, isRepoPath } from "./fsx.mjs";
|
|
21
21
|
import { isNormative, NORMATIVE_SEMANTICS_PREFIX } from "./normative.mjs";
|
|
22
|
+
import { isRepoRoot, longestOwner } from "./paths.mjs";
|
|
22
23
|
|
|
23
24
|
const DOC_EXT = /\.(md|mdx|adoc|rst|txt)$/;
|
|
24
25
|
|
|
@@ -143,11 +144,12 @@ export function audit(repo, cfg, res) {
|
|
|
143
144
|
configured.push({ role, path: dir, configured: false });
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
// The most specific unit root containing the path. A root unit is left out
|
|
148
|
+
// on purpose: it contains the whole repository, so counting it would make
|
|
149
|
+
// every repository-wide hierarchy document "component-scoped" and the
|
|
150
|
+
// distinction this check draws would disappear.
|
|
151
|
+
const unitRoots = [...new Set(res.discovery.units.map((u) => u.root))].filter((r) => !isRepoRoot(r));
|
|
152
|
+
const insideComponent = (p) => longestOwner(unitRoots, p);
|
|
151
153
|
|
|
152
154
|
// The audit reports; it does not gate. These are therefore collected as audit
|
|
153
155
|
// findings on the report rather than pushed into the compile-time diagnostics,
|
|
@@ -160,7 +162,9 @@ export function audit(repo, cfg, res) {
|
|
|
160
162
|
if (!DOC_EXT.test(f)) continue;
|
|
161
163
|
const owners = graph.entities.filter((e) => {
|
|
162
164
|
const d = e.derived || {};
|
|
163
|
-
|
|
165
|
+
const context = e.entity.spec?.context || {};
|
|
166
|
+
return [d, context].some((c) =>
|
|
167
|
+
["docs", "constraints", "runbooks"].some((key) => (c[key] || []).includes(f)));
|
|
164
168
|
}).map((e) => e.ref);
|
|
165
169
|
const role = (configured.find((h) => h.path === f) || {}).role || null;
|
|
166
170
|
docInventory.push({ path: f, linkedTo: owners.sort(), role });
|
|
@@ -193,9 +197,9 @@ export function audit(repo, cfg, res) {
|
|
|
193
197
|
subject: d.path,
|
|
194
198
|
refs: [d.path],
|
|
195
199
|
message:
|
|
196
|
-
"document is claimed by no
|
|
200
|
+
"document is claimed by no entity" +
|
|
197
201
|
(d.role ? " and its configured role (" + d.role + ") is not a repository-wide role" : "") +
|
|
198
|
-
". Link it from
|
|
202
|
+
". Link it from an entity descriptor, or remove it.",
|
|
199
203
|
});
|
|
200
204
|
}
|
|
201
205
|
for (const h of hierarchyDocs) {
|
package/core/codes.mjs
CHANGED
|
@@ -77,6 +77,9 @@ export const CODES = {
|
|
|
77
77
|
WARNING_ACCEPTANCE: "AGENTDOC_WARNING_ACCEPTANCE",
|
|
78
78
|
OVERRIDE_STALE: "AGENTDOC_OVERRIDE_STALE",
|
|
79
79
|
JOURNEY: "AGENTDOC_JOURNEY",
|
|
80
|
+
|
|
81
|
+
// language manifests
|
|
82
|
+
PYTHON_SPECIFIER: "AGENTDOC_PYTHON_SPECIFIER",
|
|
80
83
|
};
|
|
81
84
|
|
|
82
85
|
// Warning codes that a reviewer may explicitly accept. Anything outside this
|
|
@@ -94,6 +97,7 @@ export const ACCEPTABLE_WARNING_CODES = Object.freeze([
|
|
|
94
97
|
"AGENTDOC_VERIFICATION_UNTIERED",
|
|
95
98
|
"AGENTDOC_NORMATIVE_UNCOVERED",
|
|
96
99
|
"AGENTDOC_REQUIREMENT_UNVERIFIED",
|
|
100
|
+
"AGENTDOC_PYTHON_SPECIFIER",
|
|
97
101
|
]);
|
|
98
102
|
|
|
99
103
|
export class AgentDocError extends Error {
|
package/core/compile.mjs
CHANGED
|
@@ -17,11 +17,14 @@ import { redactCommand } from "./secrets.mjs";
|
|
|
17
17
|
import { formatErrors } from "./jsonschema.mjs";
|
|
18
18
|
import { validateContractFile } from "./contracts.mjs";
|
|
19
19
|
import { sourceFiles, importSpecifiers } from "./sourcescan.mjs";
|
|
20
|
+
import { readPythonUnit, pythonImportNames } from "../adapters/python.mjs";
|
|
20
21
|
import { applyWarningAcceptances } from "./acceptances.mjs";
|
|
21
22
|
import {
|
|
22
23
|
loadNormativeDocuments, verifyCitation, normativeCoverage, isNormative, isRequirementKey, NORMATIVE_SEMANTICS_PREFIX,
|
|
23
24
|
} from "./normative.mjs";
|
|
24
25
|
import { loadAdapters } from "../adapters/index.mjs";
|
|
26
|
+
import { loadCargoUnit, analyzeCargoDeps, rustIdentsInSource } from "../adapters/rust.mjs";
|
|
27
|
+
import { joinPath, ownsPath } from "./paths.mjs";
|
|
25
28
|
import {
|
|
26
29
|
COMPILER_NAME, COMPILER_VERSION, GRAPH_SCHEMA_VERSION,
|
|
27
30
|
assembleGraph, serializeGraph, finalizeSerialization, subjectResolver,
|
|
@@ -130,11 +133,6 @@ export function compile(root, opts = {}) {
|
|
|
130
133
|
for (const api of sources.entities.filter((e) => e.kind === "API")) {
|
|
131
134
|
collect(() => validateContractFile(repo, bundle, api), res.errors);
|
|
132
135
|
}
|
|
133
|
-
res.errors.push(...validateOidcProviders({ repo, sources, discovery: null }));
|
|
134
|
-
|
|
135
|
-
if (res.errors.length) {
|
|
136
|
-
return earlyOut(res, { entities: sources.entities.length });
|
|
137
|
-
}
|
|
138
136
|
|
|
139
137
|
let adapters;
|
|
140
138
|
try {
|
|
@@ -147,16 +145,22 @@ export function compile(root, opts = {}) {
|
|
|
147
145
|
throw e;
|
|
148
146
|
}
|
|
149
147
|
const discovery = discoverUnits(repo, cfg, adapters);
|
|
148
|
+
repo.nestedUnitRoots = discovery.nestedRoots;
|
|
150
149
|
// Eligibility is the compiler's own answer to "which directories are units" —
|
|
151
150
|
// exposed on the result so scaffold can write descriptors for exactly these
|
|
152
151
|
// roots rather than re-deriving them from filesystem markers.
|
|
153
152
|
res.discovery = discovery;
|
|
154
|
-
const match = matchComponents(discovery.eligible, sources.entities.filter((e) => e.kind === "Component"));
|
|
155
|
-
res.errors.push(...match.errors);
|
|
156
153
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
154
|
+
// A provider descriptor sits at its unit root. Check its source only after
|
|
155
|
+
// ownership is known, while still reporting this error alongside other
|
|
156
|
+
// descriptor/contract errors rather than hiding it behind an early return.
|
|
159
157
|
res.errors.push(...validateOidcProviders({ repo, sources, discovery }));
|
|
158
|
+
if (res.errors.length) {
|
|
159
|
+
return earlyOut(res, { entities: sources.entities.length });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const match = matchComponents(discovery.eligible, sources.entities.filter((e) => e.kind === "Component"), cfg);
|
|
163
|
+
res.errors.push(...match.errors);
|
|
160
164
|
|
|
161
165
|
if (res.errors.length) {
|
|
162
166
|
return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
|
|
@@ -285,8 +289,19 @@ export function compile(root, opts = {}) {
|
|
|
285
289
|
// ── manifest pre-pass: identify each unit's package / module namespace ────
|
|
286
290
|
// Done before extraction so cross-unit resolution never depends on the order
|
|
287
291
|
// in which units happen to be processed.
|
|
292
|
+
// Cargo.toml is read only when the rust adapter is on. An existence probe is
|
|
293
|
+
// part of the input hash, so probing it for every unit would change every
|
|
294
|
+
// graph that does not enable rust.
|
|
295
|
+
const rustOn = adapters.some((a) => a.name === "rust");
|
|
296
|
+
ctx.unitByCrateName = new Map();
|
|
297
|
+
ctx.ambiguousCrateNames = new Map();
|
|
298
|
+
ctx.rustAmbiguity = new Map();
|
|
299
|
+
// A Python manifest is read only when the python adapter is on. An existence
|
|
300
|
+
// probe is part of the input hash, so probing one for every unit would change
|
|
301
|
+
// every graph that does not enable python.
|
|
302
|
+
const pythonOn = adapters.some((a) => a.name === "python");
|
|
288
303
|
for (const unit of discovery.eligible) {
|
|
289
|
-
const pkgPath = unit.root
|
|
304
|
+
const pkgPath = joinPath(unit.root, "package.json");
|
|
290
305
|
if (repo.exists(pkgPath)) {
|
|
291
306
|
try {
|
|
292
307
|
const pkg = repo.readJson(pkgPath);
|
|
@@ -296,20 +311,73 @@ export function compile(root, opts = {}) {
|
|
|
296
311
|
res.errors.push(new AgentDocError(CODES.SCHEMA, "package.json is not valid JSON: " + pkgPath, { path: pkgPath }));
|
|
297
312
|
}
|
|
298
313
|
}
|
|
299
|
-
const gomodPath = unit.root
|
|
314
|
+
const gomodPath = joinPath(unit.root, "go.mod");
|
|
300
315
|
if (repo.exists(gomodPath)) {
|
|
301
316
|
const m = /^module\s+(\S+)/m.exec(repo.readText(gomodPath));
|
|
302
317
|
unit.goModule = m ? m[1] : null;
|
|
303
318
|
}
|
|
319
|
+
if (rustOn) loadCargoUnit(repo, unit);
|
|
320
|
+
if (pythonOn) {
|
|
321
|
+
const py = readPythonUnit(repo, unit.root);
|
|
322
|
+
if (py.manifest) {
|
|
323
|
+
unit.pythonMarker = py.manifest;
|
|
324
|
+
unit.pythonManifest = py.manifest;
|
|
325
|
+
unit.pythonDist = py.dist;
|
|
326
|
+
unit.pythonImports = py.ambiguousImports ? null : py.importNames;
|
|
327
|
+
unit.pythonProdDeps = [...new Set([
|
|
328
|
+
...py.prodDeps,
|
|
329
|
+
...py.sourceDeps.filter((n) => !py.devDeps.includes(n) && !py.prodDeps.includes(n)),
|
|
330
|
+
])].sort();
|
|
331
|
+
unit.pythonDevDeps = py.devDeps;
|
|
332
|
+
unit.pythonPathDeps = py.pathDeps;
|
|
333
|
+
if (py.error) res.errors.push(py.error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
304
336
|
}
|
|
305
337
|
const unitByPkgName = new Map();
|
|
306
338
|
const unitByGoModule = new Map();
|
|
339
|
+
const unitByPythonDist = new Map();
|
|
340
|
+
const unitByPythonImport = new Map();
|
|
341
|
+
const distOwners = new Map();
|
|
342
|
+
const importOwners = new Map();
|
|
307
343
|
for (const unit of discovery.eligible) {
|
|
308
344
|
if (unit.pkgName && !unitByPkgName.has(unit.pkgName)) unitByPkgName.set(unit.pkgName, unit);
|
|
309
345
|
if (unit.goModule && !unitByGoModule.has(unit.goModule)) unitByGoModule.set(unit.goModule, unit);
|
|
346
|
+
if (unit.pythonDist) {
|
|
347
|
+
if (!distOwners.has(unit.pythonDist)) distOwners.set(unit.pythonDist, []);
|
|
348
|
+
distOwners.get(unit.pythonDist).push(unit);
|
|
349
|
+
}
|
|
350
|
+
if (Array.isArray(unit.pythonImports)) {
|
|
351
|
+
for (const name of unit.pythonImports) {
|
|
352
|
+
if (!importOwners.has(name)) importOwners.set(name, []);
|
|
353
|
+
importOwners.get(name).push(unit);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const [name, owners] of distOwners) {
|
|
358
|
+
if (owners.length === 1) unitByPythonDist.set(name, owners[0]);
|
|
359
|
+
else for (const u of owners) u.pythonDistConflict = true;
|
|
360
|
+
}
|
|
361
|
+
for (const [name, owners] of importOwners) {
|
|
362
|
+
if (owners.length === 1) unitByPythonImport.set(name, owners[0]);
|
|
363
|
+
else for (const u of owners) u.pythonImportConflict = true;
|
|
310
364
|
}
|
|
311
365
|
ctx.unitByPkgName = unitByPkgName;
|
|
312
366
|
ctx.unitByGoModule = unitByGoModule;
|
|
367
|
+
if (rustOn) {
|
|
368
|
+
const owners = new Map();
|
|
369
|
+
for (const unit of discovery.eligible) {
|
|
370
|
+
if (!unit.crateName) continue;
|
|
371
|
+
if (!owners.has(unit.crateName)) owners.set(unit.crateName, []);
|
|
372
|
+
owners.get(unit.crateName).push(unit);
|
|
373
|
+
}
|
|
374
|
+
for (const [name, list] of [...owners.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
375
|
+
if (list.length === 1) ctx.unitByCrateName.set(name, list[0]);
|
|
376
|
+
else ctx.ambiguousCrateNames.set(name, list.slice().sort((a, b) => (a.root < b.root ? -1 : 1)));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
ctx.unitByPythonDist = unitByPythonDist;
|
|
380
|
+
ctx.unitByPythonImport = unitByPythonImport;
|
|
313
381
|
|
|
314
382
|
// Manifest-declared unit dependencies, computed before any adapter runs so
|
|
315
383
|
// that cross-unit reasoning never depends on adapter execution order.
|
|
@@ -317,6 +385,7 @@ export function compile(root, opts = {}) {
|
|
|
317
385
|
// matches a dependency name rather than a blob of source.
|
|
318
386
|
const productionDependencies = new Map();
|
|
319
387
|
const dependencyMap = new Map();
|
|
388
|
+
const unitsByRoot = new Map(discovery.eligible.map((u) => [u.root, u]));
|
|
320
389
|
for (const unit of discovery.eligible) {
|
|
321
390
|
if (!unit.component) continue;
|
|
322
391
|
const deps = new Set();
|
|
@@ -330,7 +399,7 @@ export function compile(root, opts = {}) {
|
|
|
330
399
|
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
331
400
|
}
|
|
332
401
|
if (unit.goModule) {
|
|
333
|
-
const gomod = repo.exists(unit.root
|
|
402
|
+
const gomod = repo.exists(joinPath(unit.root, "go.mod")) ? repo.readText(joinPath(unit.root, "go.mod")) : "";
|
|
334
403
|
const goReqs = [...gomod.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)].map((m) => m[1]);
|
|
335
404
|
productionDependencies.set(unit.component.ref, [...(productionDependencies.get(unit.component.ref) || []), ...goReqs]);
|
|
336
405
|
for (const m of gomod.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)) {
|
|
@@ -338,6 +407,32 @@ export function compile(root, opts = {}) {
|
|
|
338
407
|
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
339
408
|
}
|
|
340
409
|
}
|
|
410
|
+
if (rustOn && unit.cargoPackage) {
|
|
411
|
+
const cargo = analyzeCargoDeps(unit, unitsByRoot);
|
|
412
|
+
productionDependencies.set(unit.component.ref, [
|
|
413
|
+
...(productionDependencies.get(unit.component.ref) || []),
|
|
414
|
+
...cargo.prodNames,
|
|
415
|
+
]);
|
|
416
|
+
for (const hit of cargo.resolved) {
|
|
417
|
+
if (hit.ambiguous || !hit.target || !hit.target.component) continue;
|
|
418
|
+
if (hit.target.component.ref !== unit.component.ref) deps.add(hit.target.component.ref);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (unit.pythonMarker) {
|
|
422
|
+
const pyNames = [...new Set([...(unit.pythonProdDeps || []), ...(unit.pythonDevDeps || [])])];
|
|
423
|
+
productionDependencies.set(unit.component.ref, [
|
|
424
|
+
...(productionDependencies.get(unit.component.ref) || []),
|
|
425
|
+
...(unit.pythonProdDeps || []),
|
|
426
|
+
]);
|
|
427
|
+
for (const name of pyNames) {
|
|
428
|
+
const t = unitByPythonDist.get(name);
|
|
429
|
+
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
430
|
+
}
|
|
431
|
+
for (const p of unit.pythonPathDeps || []) {
|
|
432
|
+
const t = discovery.eligible.find((u) => u.root === p.dir);
|
|
433
|
+
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
341
436
|
dependencyMap.set(unit.component.ref, deps);
|
|
342
437
|
}
|
|
343
438
|
ctx.dependencyMap = dependencyMap;
|
|
@@ -702,14 +797,15 @@ export function compile(root, opts = {}) {
|
|
|
702
797
|
...schemaInputs,
|
|
703
798
|
...adapters.map((a) => ADAPTER_VERSION_INPUT_PREFIX + a.name + "@" + a.version),
|
|
704
799
|
]);
|
|
705
|
-
|
|
800
|
+
// The same git status snapshot the ignore oracle answered from (fsx.mjs).
|
|
801
|
+
const dirtySet = new Set(repo.dirtyPaths({ snapshot: true }).filter((p) => p !== graphPath));
|
|
706
802
|
const walkedDirs = [...repo.walks.keys()];
|
|
707
803
|
const dirty = [...dirtySet].some(
|
|
708
804
|
(p) =>
|
|
709
805
|
repo.readPaths.has(p) ||
|
|
710
806
|
repo.probes.has(p) ||
|
|
711
807
|
repo.listings.has(p) ||
|
|
712
|
-
walkedDirs.some((d) => (d
|
|
808
|
+
walkedDirs.some((d) => ownsPath(d, p))
|
|
713
809
|
);
|
|
714
810
|
graph.source = { commit, dirty, inputHash };
|
|
715
811
|
|
|
@@ -942,9 +1038,13 @@ const GO_IMPORT_RE = /["`]([a-z0-9]+\.[a-z0-9.-]+\/[a-z0-9./_-]+|[a-z0-9]+\.[a-z
|
|
|
942
1038
|
function buildImportIndex(repo, discovery) {
|
|
943
1039
|
const byUnit = new Map();
|
|
944
1040
|
const goPathsByUnit = new Map();
|
|
1041
|
+
const rustIdentsByUnit = new Map();
|
|
1042
|
+
const pythonImportsByUnit = new Map();
|
|
945
1043
|
for (const unit of discovery.eligible) {
|
|
946
1044
|
const map = new Map();
|
|
947
1045
|
const goPaths = new Set();
|
|
1046
|
+
const rustIdents = new Map();
|
|
1047
|
+
const pyImports = new Map();
|
|
948
1048
|
for (const f of sourceFiles(repo, unit.root, { test: false })) {
|
|
949
1049
|
const specifiers = importSpecifiers(repo.readText(f));
|
|
950
1050
|
for (const s of specifiers) {
|
|
@@ -958,11 +1058,26 @@ function buildImportIndex(repo, discovery) {
|
|
|
958
1058
|
// importing it, with no require line at all.
|
|
959
1059
|
for (const m of text.matchAll(GO_IMPORT_RE) ) goPaths.add(m[1]);
|
|
960
1060
|
}
|
|
1061
|
+
if (unit.cargoPackage && f.endsWith(".rs")) {
|
|
1062
|
+
const text = repo.readText(f);
|
|
1063
|
+
for (const ident of rustIdentsInSource(text)) {
|
|
1064
|
+
if (!rustIdents.has(ident)) rustIdents.set(ident, new Set());
|
|
1065
|
+
rustIdents.get(ident).add(f);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (unit.pythonMarker && f.endsWith(".py")) {
|
|
1069
|
+
for (const name of pythonImportNames(repo.readText(f))) {
|
|
1070
|
+
if (!pyImports.has(name)) pyImports.set(name, new Set());
|
|
1071
|
+
pyImports.get(name).add(f);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
961
1074
|
}
|
|
962
1075
|
byUnit.set(unit, map);
|
|
963
1076
|
goPathsByUnit.set(unit, goPaths);
|
|
1077
|
+
rustIdentsByUnit.set(unit, rustIdents);
|
|
1078
|
+
pythonImportsByUnit.set(unit, pyImports);
|
|
964
1079
|
}
|
|
965
|
-
return { byUnit, goPathsByUnit };
|
|
1080
|
+
return { byUnit, goPathsByUnit, rustIdentsByUnit, pythonImportsByUnit };
|
|
966
1081
|
}
|
|
967
1082
|
|
|
968
1083
|
function mergeDerived(target, extra) {
|
|
@@ -995,7 +1110,7 @@ function validateOidcProviders({ repo, sources, discovery }) {
|
|
|
995
1110
|
const roots = unit ? [unit.root] : [dirOf(provider.file)];
|
|
996
1111
|
let found = false;
|
|
997
1112
|
for (const root of roots) {
|
|
998
|
-
for (const f of repo.
|
|
1113
|
+
for (const f of repo.walkUnit(root)) {
|
|
999
1114
|
if (!/\.(ts|tsx|js|mjs|jsx|py|go|rb|java|kt|rs)$/.test(f)) continue;
|
|
1000
1115
|
if (/(^|\/)(test|tests|__tests__)\//.test(f) || /\.(test|spec)\./.test(f)) continue;
|
|
1001
1116
|
if (repo.readText(f).includes(dp)) { found = true; break; }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Change routing follows dependency edges in reverse. Normalize inverse edges
|
|
2
|
+
// here so query and impact agree even when the graph includes both spellings.
|
|
3
|
+
export function dependentScope(graph, seeds) {
|
|
4
|
+
const reverse = new Map();
|
|
5
|
+
for (const r of graph.relations) {
|
|
6
|
+
let dependency, dependent;
|
|
7
|
+
if (r.type === "buildDependsOn" || r.type === "testDependsOn") {
|
|
8
|
+
dependency = r.targetRef;
|
|
9
|
+
dependent = r.sourceRef;
|
|
10
|
+
} else if (r.type === "buildDependencyOf" || r.type === "testDependencyOf") {
|
|
11
|
+
dependency = r.sourceRef;
|
|
12
|
+
dependent = r.targetRef;
|
|
13
|
+
} else continue;
|
|
14
|
+
if (!reverse.has(dependency)) reverse.set(dependency, []);
|
|
15
|
+
reverse.get(dependency).push({ ref: dependent, unitOnly: r.type.startsWith("test") });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// false means all tiers; true means only unit checks. An all-build route
|
|
19
|
+
// upgrades a prior test-only route, including its downstream dependents.
|
|
20
|
+
const scope = new Map(seeds.map((ref) => [ref, false]));
|
|
21
|
+
const queue = [...scope.keys()];
|
|
22
|
+
for (let i = 0; i < queue.length; i++) {
|
|
23
|
+
const current = queue[i];
|
|
24
|
+
for (const edge of reverse.get(current) || []) {
|
|
25
|
+
const unitOnly = scope.get(current) || edge.unitOnly;
|
|
26
|
+
if (!scope.has(edge.ref) || (scope.get(edge.ref) && !unitOnly)) {
|
|
27
|
+
scope.set(edge.ref, unitOnly);
|
|
28
|
+
queue.push(edge.ref);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return scope;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function verificationForChange(graph, seeds, priorityRefs = seeds) {
|
|
36
|
+
const own = new Set(priorityRefs);
|
|
37
|
+
const scope = dependentScope(graph, seeds);
|
|
38
|
+
const rank = (v) => v.componentRefs.some((ref) => own.has(ref)) ? 0 : 1;
|
|
39
|
+
return graph.verification
|
|
40
|
+
.filter((v) => v.componentRefs.some((ref) => scope.has(ref) && (!scope.get(ref) || v.tier === "unit")))
|
|
41
|
+
.sort((a, b) => rank(a) - rank(b) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
42
|
+
}
|
package/core/derive.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { assertRepoPath } from "./fsx.mjs";
|
|
9
9
|
import { CODES } from "./codes.mjs";
|
|
10
|
+
import { joinPath } from "./paths.mjs";
|
|
10
11
|
|
|
11
12
|
const DEFAULT_DOC_CANDIDATES = ["README.md", "ARCHITECTURE.md", "AGENTS.md", "CONTRIBUTING.md", "DESIGN.md", "OVERVIEW.md"];
|
|
12
13
|
const DEFAULT_CONSTRAINT_CANDIDATES = ["CONSTRAINTS.md", "INVARIANTS.md"];
|
|
@@ -33,15 +34,15 @@ export function genericFacts(ctx, unit) {
|
|
|
33
34
|
const constraints = [];
|
|
34
35
|
const runbooks = [];
|
|
35
36
|
for (const n of docNames) {
|
|
36
|
-
const p = root
|
|
37
|
+
const p = joinPath(root, n);
|
|
37
38
|
if (repo.exists(p)) { docs.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/docs"]); }
|
|
38
39
|
}
|
|
39
40
|
for (const n of constraintNames) {
|
|
40
|
-
const p = root
|
|
41
|
+
const p = joinPath(root, n);
|
|
41
42
|
if (repo.exists(p)) { constraints.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/constraints"]); }
|
|
42
43
|
}
|
|
43
44
|
for (const n of runbookNames) {
|
|
44
|
-
const p = root
|
|
45
|
+
const p = joinPath(root, n);
|
|
45
46
|
if (repo.exists(p)) { runbooks.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/runbooks"]); }
|
|
46
47
|
}
|
|
47
48
|
const spec = ctx.componentEntity().doc.spec || {};
|
package/core/descriptors.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { parseYamlDocuments } from "./yaml.mjs";
|
|
|
20
20
|
import { scanForSecrets } from "./secrets.mjs";
|
|
21
21
|
import { formatErrors } from "./jsonschema.mjs";
|
|
22
22
|
import { SchemaBundle } from "./jsonschema.mjs";
|
|
23
|
+
import { joinPath, ownsPath } from "./paths.mjs";
|
|
23
24
|
|
|
24
25
|
export const KINDS = Object.freeze(["Domain", "System", "Component", "API", "Resource"]);
|
|
25
26
|
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -177,7 +178,7 @@ function descriptorGlobs(repo, cfg) {
|
|
|
177
178
|
// glob — its descriptor lives at a fixed path and must be loaded even when
|
|
178
179
|
// no componentDescriptors glob happens to cover that directory.
|
|
179
180
|
for (const r of cfg.discovery.supplementalRoots || []) {
|
|
180
|
-
const f = String(r)
|
|
181
|
+
const f = joinPath(String(r), cfg.discovery.componentDescriptorName);
|
|
181
182
|
if (repo.exists(f)) componentFiles.add(f);
|
|
182
183
|
}
|
|
183
184
|
const centralFiles = new Set();
|
|
@@ -473,7 +474,7 @@ export function loadReviewedOverrides(repo, cfg, refs, byRef) {
|
|
|
473
474
|
: null;
|
|
474
475
|
for (const ev of o.evidence) {
|
|
475
476
|
const p = ev.path;
|
|
476
|
-
const scoped = p === o.contractRef ||
|
|
477
|
+
const scoped = p === o.contractRef || ownsPath(subjectRoot, p) || (targetRoot !== null && ownsPath(targetRoot, p));
|
|
477
478
|
if (!scoped) {
|
|
478
479
|
throw new AgentDocError(
|
|
479
480
|
CODES.RELATION_KIND,
|