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
package/adapters/typescript.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export class TypeScriptAdapter extends Adapter {
|
|
|
44
44
|
const routes = [];
|
|
45
45
|
for (const base of ["src/app", "app", "src/pages", "pages"]) {
|
|
46
46
|
if (!repo.isDir(unit.root + "/" + base)) continue;
|
|
47
|
-
for (const f of repo.
|
|
47
|
+
for (const f of repo.walkUnit(unit.root, unit.root + "/" + base)) {
|
|
48
48
|
const routePath = routeFor(unit.root, base, f);
|
|
49
49
|
if (!routePath) continue;
|
|
50
50
|
const rpr = prov.add("observed", f, "route-extractor", ["der:" + compRef + ":/routes"]);
|
package/bin/agentdoc.mjs
CHANGED
|
@@ -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
|
package/core/audit.mjs
CHANGED
|
@@ -160,7 +160,9 @@ export function audit(repo, cfg, res) {
|
|
|
160
160
|
if (!DOC_EXT.test(f)) continue;
|
|
161
161
|
const owners = graph.entities.filter((e) => {
|
|
162
162
|
const d = e.derived || {};
|
|
163
|
-
|
|
163
|
+
const context = e.entity.spec?.context || {};
|
|
164
|
+
return [d, context].some((c) =>
|
|
165
|
+
["docs", "constraints", "runbooks"].some((key) => (c[key] || []).includes(f)));
|
|
164
166
|
}).map((e) => e.ref);
|
|
165
167
|
const role = (configured.find((h) => h.path === f) || {}).role || null;
|
|
166
168
|
docInventory.push({ path: f, linkedTo: owners.sort(), role });
|
|
@@ -193,9 +195,9 @@ export function audit(repo, cfg, res) {
|
|
|
193
195
|
subject: d.path,
|
|
194
196
|
refs: [d.path],
|
|
195
197
|
message:
|
|
196
|
-
"document is claimed by no
|
|
198
|
+
"document is claimed by no entity" +
|
|
197
199
|
(d.role ? " and its configured role (" + d.role + ") is not a repository-wide role" : "") +
|
|
198
|
-
". Link it from
|
|
200
|
+
". Link it from an entity descriptor, or remove it.",
|
|
199
201
|
});
|
|
200
202
|
}
|
|
201
203
|
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,13 @@ 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";
|
|
25
27
|
import {
|
|
26
28
|
COMPILER_NAME, COMPILER_VERSION, GRAPH_SCHEMA_VERSION,
|
|
27
29
|
assembleGraph, serializeGraph, finalizeSerialization, subjectResolver,
|
|
@@ -130,11 +132,6 @@ export function compile(root, opts = {}) {
|
|
|
130
132
|
for (const api of sources.entities.filter((e) => e.kind === "API")) {
|
|
131
133
|
collect(() => validateContractFile(repo, bundle, api), res.errors);
|
|
132
134
|
}
|
|
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
135
|
|
|
139
136
|
let adapters;
|
|
140
137
|
try {
|
|
@@ -147,16 +144,22 @@ export function compile(root, opts = {}) {
|
|
|
147
144
|
throw e;
|
|
148
145
|
}
|
|
149
146
|
const discovery = discoverUnits(repo, cfg, adapters);
|
|
147
|
+
repo.nestedUnitRoots = discovery.nestedRoots;
|
|
150
148
|
// Eligibility is the compiler's own answer to "which directories are units" —
|
|
151
149
|
// exposed on the result so scaffold can write descriptors for exactly these
|
|
152
150
|
// roots rather than re-deriving them from filesystem markers.
|
|
153
151
|
res.discovery = discovery;
|
|
154
|
-
const match = matchComponents(discovery.eligible, sources.entities.filter((e) => e.kind === "Component"));
|
|
155
|
-
res.errors.push(...match.errors);
|
|
156
152
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
153
|
+
// A provider descriptor sits at its unit root. Check its source only after
|
|
154
|
+
// ownership is known, while still reporting this error alongside other
|
|
155
|
+
// descriptor/contract errors rather than hiding it behind an early return.
|
|
159
156
|
res.errors.push(...validateOidcProviders({ repo, sources, discovery }));
|
|
157
|
+
if (res.errors.length) {
|
|
158
|
+
return earlyOut(res, { entities: sources.entities.length });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const match = matchComponents(discovery.eligible, sources.entities.filter((e) => e.kind === "Component"), cfg);
|
|
162
|
+
res.errors.push(...match.errors);
|
|
160
163
|
|
|
161
164
|
if (res.errors.length) {
|
|
162
165
|
return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
|
|
@@ -285,6 +288,17 @@ export function compile(root, opts = {}) {
|
|
|
285
288
|
// ── manifest pre-pass: identify each unit's package / module namespace ────
|
|
286
289
|
// Done before extraction so cross-unit resolution never depends on the order
|
|
287
290
|
// in which units happen to be processed.
|
|
291
|
+
// Cargo.toml is read only when the rust adapter is on. An existence probe is
|
|
292
|
+
// part of the input hash, so probing it for every unit would change every
|
|
293
|
+
// graph that does not enable rust.
|
|
294
|
+
const rustOn = adapters.some((a) => a.name === "rust");
|
|
295
|
+
ctx.unitByCrateName = new Map();
|
|
296
|
+
ctx.ambiguousCrateNames = new Map();
|
|
297
|
+
ctx.rustAmbiguity = new Map();
|
|
298
|
+
// A Python manifest is read only when the python adapter is on. An existence
|
|
299
|
+
// probe is part of the input hash, so probing one for every unit would change
|
|
300
|
+
// every graph that does not enable python.
|
|
301
|
+
const pythonOn = adapters.some((a) => a.name === "python");
|
|
288
302
|
for (const unit of discovery.eligible) {
|
|
289
303
|
const pkgPath = unit.root + "/package.json";
|
|
290
304
|
if (repo.exists(pkgPath)) {
|
|
@@ -301,15 +315,68 @@ export function compile(root, opts = {}) {
|
|
|
301
315
|
const m = /^module\s+(\S+)/m.exec(repo.readText(gomodPath));
|
|
302
316
|
unit.goModule = m ? m[1] : null;
|
|
303
317
|
}
|
|
318
|
+
if (rustOn) loadCargoUnit(repo, unit);
|
|
319
|
+
if (pythonOn) {
|
|
320
|
+
const py = readPythonUnit(repo, unit.root);
|
|
321
|
+
if (py.manifest) {
|
|
322
|
+
unit.pythonMarker = py.manifest;
|
|
323
|
+
unit.pythonManifest = py.manifest;
|
|
324
|
+
unit.pythonDist = py.dist;
|
|
325
|
+
unit.pythonImports = py.ambiguousImports ? null : py.importNames;
|
|
326
|
+
unit.pythonProdDeps = [...new Set([
|
|
327
|
+
...py.prodDeps,
|
|
328
|
+
...py.sourceDeps.filter((n) => !py.devDeps.includes(n) && !py.prodDeps.includes(n)),
|
|
329
|
+
])].sort();
|
|
330
|
+
unit.pythonDevDeps = py.devDeps;
|
|
331
|
+
unit.pythonPathDeps = py.pathDeps;
|
|
332
|
+
if (py.error) res.errors.push(py.error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
304
335
|
}
|
|
305
336
|
const unitByPkgName = new Map();
|
|
306
337
|
const unitByGoModule = new Map();
|
|
338
|
+
const unitByPythonDist = new Map();
|
|
339
|
+
const unitByPythonImport = new Map();
|
|
340
|
+
const distOwners = new Map();
|
|
341
|
+
const importOwners = new Map();
|
|
307
342
|
for (const unit of discovery.eligible) {
|
|
308
343
|
if (unit.pkgName && !unitByPkgName.has(unit.pkgName)) unitByPkgName.set(unit.pkgName, unit);
|
|
309
344
|
if (unit.goModule && !unitByGoModule.has(unit.goModule)) unitByGoModule.set(unit.goModule, unit);
|
|
345
|
+
if (unit.pythonDist) {
|
|
346
|
+
if (!distOwners.has(unit.pythonDist)) distOwners.set(unit.pythonDist, []);
|
|
347
|
+
distOwners.get(unit.pythonDist).push(unit);
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(unit.pythonImports)) {
|
|
350
|
+
for (const name of unit.pythonImports) {
|
|
351
|
+
if (!importOwners.has(name)) importOwners.set(name, []);
|
|
352
|
+
importOwners.get(name).push(unit);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const [name, owners] of distOwners) {
|
|
357
|
+
if (owners.length === 1) unitByPythonDist.set(name, owners[0]);
|
|
358
|
+
else for (const u of owners) u.pythonDistConflict = true;
|
|
359
|
+
}
|
|
360
|
+
for (const [name, owners] of importOwners) {
|
|
361
|
+
if (owners.length === 1) unitByPythonImport.set(name, owners[0]);
|
|
362
|
+
else for (const u of owners) u.pythonImportConflict = true;
|
|
310
363
|
}
|
|
311
364
|
ctx.unitByPkgName = unitByPkgName;
|
|
312
365
|
ctx.unitByGoModule = unitByGoModule;
|
|
366
|
+
if (rustOn) {
|
|
367
|
+
const owners = new Map();
|
|
368
|
+
for (const unit of discovery.eligible) {
|
|
369
|
+
if (!unit.crateName) continue;
|
|
370
|
+
if (!owners.has(unit.crateName)) owners.set(unit.crateName, []);
|
|
371
|
+
owners.get(unit.crateName).push(unit);
|
|
372
|
+
}
|
|
373
|
+
for (const [name, list] of [...owners.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
374
|
+
if (list.length === 1) ctx.unitByCrateName.set(name, list[0]);
|
|
375
|
+
else ctx.ambiguousCrateNames.set(name, list.slice().sort((a, b) => (a.root < b.root ? -1 : 1)));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
ctx.unitByPythonDist = unitByPythonDist;
|
|
379
|
+
ctx.unitByPythonImport = unitByPythonImport;
|
|
313
380
|
|
|
314
381
|
// Manifest-declared unit dependencies, computed before any adapter runs so
|
|
315
382
|
// that cross-unit reasoning never depends on adapter execution order.
|
|
@@ -317,6 +384,7 @@ export function compile(root, opts = {}) {
|
|
|
317
384
|
// matches a dependency name rather than a blob of source.
|
|
318
385
|
const productionDependencies = new Map();
|
|
319
386
|
const dependencyMap = new Map();
|
|
387
|
+
const unitsByRoot = new Map(discovery.eligible.map((u) => [u.root, u]));
|
|
320
388
|
for (const unit of discovery.eligible) {
|
|
321
389
|
if (!unit.component) continue;
|
|
322
390
|
const deps = new Set();
|
|
@@ -338,6 +406,32 @@ export function compile(root, opts = {}) {
|
|
|
338
406
|
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
339
407
|
}
|
|
340
408
|
}
|
|
409
|
+
if (rustOn && unit.cargoPackage) {
|
|
410
|
+
const cargo = analyzeCargoDeps(unit, unitsByRoot);
|
|
411
|
+
productionDependencies.set(unit.component.ref, [
|
|
412
|
+
...(productionDependencies.get(unit.component.ref) || []),
|
|
413
|
+
...cargo.prodNames,
|
|
414
|
+
]);
|
|
415
|
+
for (const hit of cargo.resolved) {
|
|
416
|
+
if (hit.ambiguous || !hit.target || !hit.target.component) continue;
|
|
417
|
+
if (hit.target.component.ref !== unit.component.ref) deps.add(hit.target.component.ref);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (unit.pythonMarker) {
|
|
421
|
+
const pyNames = [...new Set([...(unit.pythonProdDeps || []), ...(unit.pythonDevDeps || [])])];
|
|
422
|
+
productionDependencies.set(unit.component.ref, [
|
|
423
|
+
...(productionDependencies.get(unit.component.ref) || []),
|
|
424
|
+
...(unit.pythonProdDeps || []),
|
|
425
|
+
]);
|
|
426
|
+
for (const name of pyNames) {
|
|
427
|
+
const t = unitByPythonDist.get(name);
|
|
428
|
+
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
429
|
+
}
|
|
430
|
+
for (const p of unit.pythonPathDeps || []) {
|
|
431
|
+
const t = discovery.eligible.find((u) => u.root === p.dir);
|
|
432
|
+
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
341
435
|
dependencyMap.set(unit.component.ref, deps);
|
|
342
436
|
}
|
|
343
437
|
ctx.dependencyMap = dependencyMap;
|
|
@@ -942,9 +1036,13 @@ const GO_IMPORT_RE = /["`]([a-z0-9]+\.[a-z0-9.-]+\/[a-z0-9./_-]+|[a-z0-9]+\.[a-z
|
|
|
942
1036
|
function buildImportIndex(repo, discovery) {
|
|
943
1037
|
const byUnit = new Map();
|
|
944
1038
|
const goPathsByUnit = new Map();
|
|
1039
|
+
const rustIdentsByUnit = new Map();
|
|
1040
|
+
const pythonImportsByUnit = new Map();
|
|
945
1041
|
for (const unit of discovery.eligible) {
|
|
946
1042
|
const map = new Map();
|
|
947
1043
|
const goPaths = new Set();
|
|
1044
|
+
const rustIdents = new Map();
|
|
1045
|
+
const pyImports = new Map();
|
|
948
1046
|
for (const f of sourceFiles(repo, unit.root, { test: false })) {
|
|
949
1047
|
const specifiers = importSpecifiers(repo.readText(f));
|
|
950
1048
|
for (const s of specifiers) {
|
|
@@ -958,11 +1056,26 @@ function buildImportIndex(repo, discovery) {
|
|
|
958
1056
|
// importing it, with no require line at all.
|
|
959
1057
|
for (const m of text.matchAll(GO_IMPORT_RE) ) goPaths.add(m[1]);
|
|
960
1058
|
}
|
|
1059
|
+
if (unit.cargoPackage && f.endsWith(".rs")) {
|
|
1060
|
+
const text = repo.readText(f);
|
|
1061
|
+
for (const ident of rustIdentsInSource(text)) {
|
|
1062
|
+
if (!rustIdents.has(ident)) rustIdents.set(ident, new Set());
|
|
1063
|
+
rustIdents.get(ident).add(f);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (unit.pythonMarker && f.endsWith(".py")) {
|
|
1067
|
+
for (const name of pythonImportNames(repo.readText(f))) {
|
|
1068
|
+
if (!pyImports.has(name)) pyImports.set(name, new Set());
|
|
1069
|
+
pyImports.get(name).add(f);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
961
1072
|
}
|
|
962
1073
|
byUnit.set(unit, map);
|
|
963
1074
|
goPathsByUnit.set(unit, goPaths);
|
|
1075
|
+
rustIdentsByUnit.set(unit, rustIdents);
|
|
1076
|
+
pythonImportsByUnit.set(unit, pyImports);
|
|
964
1077
|
}
|
|
965
|
-
return { byUnit, goPathsByUnit };
|
|
1078
|
+
return { byUnit, goPathsByUnit, rustIdentsByUnit, pythonImportsByUnit };
|
|
966
1079
|
}
|
|
967
1080
|
|
|
968
1081
|
function mergeDerived(target, extra) {
|
|
@@ -995,7 +1108,7 @@ function validateOidcProviders({ repo, sources, discovery }) {
|
|
|
995
1108
|
const roots = unit ? [unit.root] : [dirOf(provider.file)];
|
|
996
1109
|
let found = false;
|
|
997
1110
|
for (const root of roots) {
|
|
998
|
-
for (const f of repo.
|
|
1111
|
+
for (const f of repo.walkUnit(root)) {
|
|
999
1112
|
if (!/\.(ts|tsx|js|mjs|jsx|py|go|rb|java|kt|rs)$/.test(f)) continue;
|
|
1000
1113
|
if (/(^|\/)(test|tests|__tests__)\//.test(f) || /\.(test|spec)\./.test(f)) continue;
|
|
1001
1114
|
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/discover.mjs
CHANGED
|
@@ -8,12 +8,22 @@ import path from "node:path";
|
|
|
8
8
|
import { AgentDocError, CODES } from "./codes.mjs";
|
|
9
9
|
import { expandGlob, globToRegExp, assertGlob } from "../core/fsx.mjs";
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// Roots excluded from inference and scaffolding. Authored descriptors strictly
|
|
12
|
+
// below them can still claim units; descriptors at these exact roots cannot.
|
|
13
13
|
export const DEFAULT_NOT_UNIT_ROOTS = ["agentdoc", "docs", "scripts", "node_modules", ".github"];
|
|
14
14
|
|
|
15
|
+
function notUnitRoots(cfg) {
|
|
16
|
+
return cfg?.discovery.notUnitRoots || DEFAULT_NOT_UNIT_ROOTS;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Inference excludes the whole subtree, not similarly named siblings. CREATE
|
|
20
|
+
// mode shares this policy even for units with an existing authored descriptor.
|
|
21
|
+
export function isNotUnitRoot(root, cfg) {
|
|
22
|
+
return notUnitRoots(cfg)
|
|
23
|
+
.some((excluded) => root === excluded || root.startsWith(excluded + "/"));
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
export function discoverUnits(repo, cfg, adapters) {
|
|
16
|
-
const notUnit = new Set(cfg.discovery.notUnitRoots || DEFAULT_NOT_UNIT_ROOTS);
|
|
17
27
|
const extraDeployable = new Set();
|
|
18
28
|
const extraImportable = new Set();
|
|
19
29
|
const units = new Map();
|
|
@@ -35,13 +45,17 @@ export function discoverUnits(repo, cfg, adapters) {
|
|
|
35
45
|
assertGlob(g, "agentdoc config");
|
|
36
46
|
for (const f of expandGlob(repo, g)) {
|
|
37
47
|
const dir = path.posix.dirname(f);
|
|
38
|
-
if (
|
|
48
|
+
if (repo.isDir(dir)) {
|
|
39
49
|
candidates.add(dir);
|
|
40
50
|
descriptorRoots.add(dir);
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
|
-
for (const r of cfg.discovery.supplementalRoots || [])
|
|
54
|
+
for (const r of cfg.discovery.supplementalRoots || []) {
|
|
55
|
+
candidates.add(r);
|
|
56
|
+
// The descriptor loader also reads these fixed paths outside the globs.
|
|
57
|
+
if (repo.exists(r + "/" + cfg.discovery.componentDescriptorName)) descriptorRoots.add(r);
|
|
58
|
+
}
|
|
45
59
|
|
|
46
60
|
// Adapters may contribute roots the descriptor globs cannot know about: a
|
|
47
61
|
// Go workspace member, a workspace file member with no descriptor yet. The
|
|
@@ -59,7 +73,10 @@ export function discoverUnits(repo, cfg, adapters) {
|
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
for (const root of [...candidates].sort()) {
|
|
62
|
-
|
|
76
|
+
// Preserve explicit human claims below an exclusion, while keeping inferred
|
|
77
|
+
// roots out of the subtree and refusing descriptors at the exact root.
|
|
78
|
+
if (notUnitRoots(cfg).includes(root)) continue;
|
|
79
|
+
if (isNotUnitRoot(root, cfg) && !descriptorRoots.has(root)) continue;
|
|
63
80
|
if (!repo.isDir(root)) continue;
|
|
64
81
|
const u = get(root);
|
|
65
82
|
u.explicit = (cfg.discovery.supplementalRoots || []).includes(root) || descriptorRoots.has(root);
|
|
@@ -72,13 +89,38 @@ export function discoverUnits(repo, cfg, adapters) {
|
|
|
72
89
|
const shape = markerShape(marker);
|
|
73
90
|
if (shape === "deployable" || shape === "both") u.deployable = true;
|
|
74
91
|
if (shape === "importable" || shape === "both") u.importable = true;
|
|
92
|
+
// A manifest with no artifact shape is still a unit. deployable and
|
|
93
|
+
// importable stay false, so scaffold does not invent a service or a
|
|
94
|
+
// library. A descriptor is not required before the unit is recognised.
|
|
95
|
+
if (shape === "present") u.present = true;
|
|
75
96
|
}
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
const list = [...units.values()].sort((a, b) => (a.root < b.root ? -1 : 1));
|
|
80
|
-
const eligible = list.filter((u) => u.deployable || u.importable || u.explicit);
|
|
81
|
-
|
|
101
|
+
const eligible = list.filter((u) => u.deployable || u.importable || u.explicit || u.present);
|
|
102
|
+
// Only eligible units create ownership boundaries. Record immediate children
|
|
103
|
+
// once; pruning one of them also prunes every deeper descendant. Normalise
|
|
104
|
+
// the repository root so descriptor roots (".") and workspace roots agree.
|
|
105
|
+
const nestedRoots = new Map(eligible.map((u) => [unitRoot(u.root), new Set()]));
|
|
106
|
+
for (const root of nestedRoots.keys()) {
|
|
107
|
+
if (!root) continue;
|
|
108
|
+
let parent = unitRoot(path.posix.dirname(root));
|
|
109
|
+
while (true) {
|
|
110
|
+
if (nestedRoots.has(parent)) {
|
|
111
|
+
nestedRoots.get(parent).add(root);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (!parent) break;
|
|
115
|
+
parent = unitRoot(path.posix.dirname(parent));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { units: list, eligible, nestedRoots };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function unitRoot(root) {
|
|
122
|
+
const normalized = path.posix.normalize(root || ".");
|
|
123
|
+
return normalized === "." ? "" : normalized;
|
|
82
124
|
}
|
|
83
125
|
|
|
84
126
|
function markerShape(marker) {
|
|
@@ -93,7 +135,7 @@ export function descriptorDir(file) {
|
|
|
93
135
|
}
|
|
94
136
|
|
|
95
137
|
// Exactly one descriptor per eligible unit, and no descriptor outside one.
|
|
96
|
-
export function matchComponents(eligible, componentEntities) {
|
|
138
|
+
export function matchComponents(eligible, componentEntities, cfg) {
|
|
97
139
|
const errors = [];
|
|
98
140
|
const byDir = new Map();
|
|
99
141
|
for (const e of componentEntities) {
|
|
@@ -124,6 +166,15 @@ export function matchComponents(eligible, componentEntities) {
|
|
|
124
166
|
for (const e of componentEntities) {
|
|
125
167
|
const dir = descriptorDir(e.file);
|
|
126
168
|
if (unitRoots.has(dir)) continue;
|
|
169
|
+
if (notUnitRoots(cfg).includes(dir)) {
|
|
170
|
+
errors.push(new AgentDocError(
|
|
171
|
+
CODES.COMPONENT_UNCORROBORATED,
|
|
172
|
+
"component descriptor at '" + dir + "' is in discovery.notUnitRoots (or a default not-unit root); " +
|
|
173
|
+
"remove it from notUnitRoots or move the descriptor",
|
|
174
|
+
{ path: e.file, ref: e.ref }
|
|
175
|
+
));
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
127
178
|
// A descriptor is a human claim that a component exists. When no adapter
|
|
128
179
|
// can corroborate a deployable or importable artifact in that source root,
|
|
129
180
|
// the claim is unsupported. This is reported rather than ignored: it is
|
package/core/fsx.mjs
CHANGED
|
@@ -59,6 +59,7 @@ export class Repo {
|
|
|
59
59
|
this.probes = new Map();
|
|
60
60
|
this.walks = new Map();
|
|
61
61
|
this.listings = new Map();
|
|
62
|
+
this.nestedUnitRoots = new Map();
|
|
62
63
|
this.generatedDirs = new Set(opts.generatedDirs || DEFAULT_GENERATED_DIRS);
|
|
63
64
|
this.noGit = Boolean(opts.noGit);
|
|
64
65
|
}
|
|
@@ -140,6 +141,22 @@ export class Repo {
|
|
|
140
141
|
this.walks.set(relDir || ".", out);
|
|
141
142
|
return out;
|
|
142
143
|
}
|
|
144
|
+
// Per-unit scans use the discovery result, never filesystem heuristics.
|
|
145
|
+
// Keep walk() repository-wide for discovery, contracts and authored inputs.
|
|
146
|
+
ownsUnitPath(unitRoot, rel) {
|
|
147
|
+
const normalize = (p) => path.posix.normalize(p || ".").replace(/^\.$/, "");
|
|
148
|
+
const root = normalize(unitRoot);
|
|
149
|
+
const target = normalize(rel);
|
|
150
|
+
if (root && target !== root && !target.startsWith(root + "/")) return false;
|
|
151
|
+
for (const child of this.nestedUnitRoots.get(root) || []) {
|
|
152
|
+
if (target === child || target.startsWith(child + "/")) return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
walkUnit(unitRoot, relDir = unitRoot, { exclude = () => false } = {}) {
|
|
157
|
+
if (!this.ownsUnitPath(unitRoot, relDir)) return [];
|
|
158
|
+
return this.walk(relDir, { exclude: (rel, ent) => !this.ownsUnitPath(unitRoot, rel) || exclude(rel, ent) });
|
|
159
|
+
}
|
|
143
160
|
// Files under a path or a path *prefix*. A contract is often named by a module
|
|
144
161
|
// prefix rather than a single file (`events.go`, `events/*.go`), and
|
|
145
162
|
// requiring an exact directory would reject a perfectly good contract.
|
package/core/graph.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { assertDeterministic, structuralProjection } from "./determinism.mjs";
|
|
|
9
9
|
import { sha256Hex } from "./fsx.mjs";
|
|
10
10
|
|
|
11
11
|
export const COMPILER_NAME = "agentdoc-compiler";
|
|
12
|
-
export const COMPILER_VERSION = "1.
|
|
12
|
+
export const COMPILER_VERSION = "1.2.0";
|
|
13
13
|
export const GRAPH_SCHEMA_VERSION = "agentdoc.dev/graph/v1";
|
|
14
14
|
|
|
15
15
|
// Resolve symbolic provenance subjects to RFC 6901 pointers into the graph.
|