agent-doc-system 1.2.0 → 1.4.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 +188 -1
- package/README.md +1 -1
- package/adapters/cache.mjs +5 -4
- package/adapters/database.mjs +155 -6
- package/adapters/deploy-config.mjs +3 -2
- package/adapters/dockerfile.mjs +3 -2
- package/adapters/generic.mjs +2 -1
- package/adapters/github-actions.mjs +2 -1
- package/adapters/go.mjs +4 -3
- package/adapters/node.mjs +3 -2
- package/adapters/rust.mjs +3 -0
- package/adapters/turborepo.mjs +3 -2
- package/adapters/typescript.mjs +7 -5
- package/adapters/wrangler.mjs +3 -2
- package/bin/agentdoc.mjs +9 -2
- package/bin/usage.txt +2 -0
- package/core/audit.mjs +7 -5
- package/core/codes.mjs +2 -0
- package/core/compile.mjs +12 -6
- package/core/derive.mjs +4 -3
- package/core/descriptors.mjs +52 -2
- package/core/discover.mjs +3 -2
- package/core/fsx.mjs +174 -42
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +84 -14
- package/core/paths.mjs +71 -0
- package/core/query.mjs +58 -23
- package/core/scaffold.mjs +130 -23
- package/core/sourcescan.mjs +4 -1
- package/docs/ADAPTERS.md +49 -5
- package/docs/BENCHMARK.md +83 -21
- package/docs/CI.md +9 -1
- package/docs/EVALS.md +209 -17
- package/docs/MIGRATION.md +22 -4
- package/docs/PROVENANCE.md +6 -0
- package/docs/QUERY.md +50 -7
- package/docs/SPEC.md +89 -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 +120 -116
- package/package.json +4 -2
- package/schemas/resource.schema.json +9 -1
- 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 +8 -2
- package/templates/ci.yml +1 -1
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));
|
|
@@ -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,
|
package/core/codes.mjs
CHANGED
|
@@ -23,6 +23,7 @@ export const CODES = {
|
|
|
23
23
|
GLOB: "AGENTDOC_GLOB",
|
|
24
24
|
PATH_ESCAPE: "AGENTDOC_PATH_ESCAPE",
|
|
25
25
|
PATH_UNRESOLVED: "AGENTDOC_PATH_UNRESOLVED",
|
|
26
|
+
MIGRATION_PATH: "AGENTDOC_MIGRATION_PATH",
|
|
26
27
|
SECRET: "AGENTDOC_SECRET",
|
|
27
28
|
|
|
28
29
|
// identity & references
|
|
@@ -98,6 +99,7 @@ export const ACCEPTABLE_WARNING_CODES = Object.freeze([
|
|
|
98
99
|
"AGENTDOC_NORMATIVE_UNCOVERED",
|
|
99
100
|
"AGENTDOC_REQUIREMENT_UNVERIFIED",
|
|
100
101
|
"AGENTDOC_PYTHON_SPECIFIER",
|
|
102
|
+
"AGENTDOC_MIGRATION_DIR_UNRESOLVED",
|
|
101
103
|
]);
|
|
102
104
|
|
|
103
105
|
export class AgentDocError extends Error {
|
package/core/compile.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./normative.mjs";
|
|
25
25
|
import { loadAdapters } from "../adapters/index.mjs";
|
|
26
26
|
import { loadCargoUnit, analyzeCargoDeps, rustIdentsInSource } from "../adapters/rust.mjs";
|
|
27
|
+
import { joinPath, ownsPath } from "./paths.mjs";
|
|
27
28
|
import {
|
|
28
29
|
COMPILER_NAME, COMPILER_VERSION, GRAPH_SCHEMA_VERSION,
|
|
29
30
|
assembleGraph, serializeGraph, finalizeSerialization, subjectResolver,
|
|
@@ -300,7 +301,7 @@ export function compile(root, opts = {}) {
|
|
|
300
301
|
// every graph that does not enable python.
|
|
301
302
|
const pythonOn = adapters.some((a) => a.name === "python");
|
|
302
303
|
for (const unit of discovery.eligible) {
|
|
303
|
-
const pkgPath = unit.root
|
|
304
|
+
const pkgPath = joinPath(unit.root, "package.json");
|
|
304
305
|
if (repo.exists(pkgPath)) {
|
|
305
306
|
try {
|
|
306
307
|
const pkg = repo.readJson(pkgPath);
|
|
@@ -310,7 +311,7 @@ export function compile(root, opts = {}) {
|
|
|
310
311
|
res.errors.push(new AgentDocError(CODES.SCHEMA, "package.json is not valid JSON: " + pkgPath, { path: pkgPath }));
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
|
-
const gomodPath = unit.root
|
|
314
|
+
const gomodPath = joinPath(unit.root, "go.mod");
|
|
314
315
|
if (repo.exists(gomodPath)) {
|
|
315
316
|
const m = /^module\s+(\S+)/m.exec(repo.readText(gomodPath));
|
|
316
317
|
unit.goModule = m ? m[1] : null;
|
|
@@ -398,7 +399,7 @@ export function compile(root, opts = {}) {
|
|
|
398
399
|
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
399
400
|
}
|
|
400
401
|
if (unit.goModule) {
|
|
401
|
-
const gomod = repo.exists(unit.root
|
|
402
|
+
const gomod = repo.exists(joinPath(unit.root, "go.mod")) ? repo.readText(joinPath(unit.root, "go.mod")) : "";
|
|
402
403
|
const goReqs = [...gomod.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)].map((m) => m[1]);
|
|
403
404
|
productionDependencies.set(unit.component.ref, [...(productionDependencies.get(unit.component.ref) || []), ...goReqs]);
|
|
404
405
|
for (const m of gomod.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)) {
|
|
@@ -497,6 +498,10 @@ export function compile(root, opts = {}) {
|
|
|
497
498
|
"entf:" + e.ref + ":/metadata/description",
|
|
498
499
|
];
|
|
499
500
|
if (e.doc.spec !== undefined) subjects.push("entf:" + e.ref + ":/spec");
|
|
501
|
+
// A declared migration path is the one authored path whose meaning reaches
|
|
502
|
+
// past the descriptor (impact routes every change below it to this
|
|
503
|
+
// Resource), so it is pointed at directly rather than through `/spec`.
|
|
504
|
+
if (e.doc.spec?.migrationPaths) subjects.push("entf:" + e.ref + ":/spec/migrationPaths");
|
|
500
505
|
prov.add("declared", e.file, "descriptor-reader", subjects);
|
|
501
506
|
}
|
|
502
507
|
|
|
@@ -776,7 +781,7 @@ export function compile(root, opts = {}) {
|
|
|
776
781
|
return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
|
|
777
782
|
}
|
|
778
783
|
for (const d of derivedMap.values()) {
|
|
779
|
-
for (const list of [d.bindings || [], d.healthContracts || []]) {
|
|
784
|
+
for (const list of [d.bindings || [], d.healthContracts || [], d.migrationDirs || []]) {
|
|
780
785
|
for (const b of list) {
|
|
781
786
|
b.provenanceIds = [...(b._provs || [])].map((p) => provFinal.idOf.get(p)).filter(Boolean).sort();
|
|
782
787
|
delete b._provs;
|
|
@@ -796,14 +801,15 @@ export function compile(root, opts = {}) {
|
|
|
796
801
|
...schemaInputs,
|
|
797
802
|
...adapters.map((a) => ADAPTER_VERSION_INPUT_PREFIX + a.name + "@" + a.version),
|
|
798
803
|
]);
|
|
799
|
-
|
|
804
|
+
// The same git status snapshot the ignore oracle answered from (fsx.mjs).
|
|
805
|
+
const dirtySet = new Set(repo.dirtyPaths({ snapshot: true }).filter((p) => p !== graphPath));
|
|
800
806
|
const walkedDirs = [...repo.walks.keys()];
|
|
801
807
|
const dirty = [...dirtySet].some(
|
|
802
808
|
(p) =>
|
|
803
809
|
repo.readPaths.has(p) ||
|
|
804
810
|
repo.probes.has(p) ||
|
|
805
811
|
repo.listings.has(p) ||
|
|
806
|
-
walkedDirs.some((d) => (d
|
|
812
|
+
walkedDirs.some((d) => ownsPath(d, p))
|
|
807
813
|
);
|
|
808
814
|
graph.source = { commit, dirty, inputHash };
|
|
809
815
|
|
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();
|
|
@@ -296,6 +297,7 @@ export function loadDescriptors(repo, cfg, bundle, refs) {
|
|
|
296
297
|
throw new AgentDocError(CODES.PATH_UNRESOLVED, "context link does not resolve: " + p, { path: e.file, ref: e.ref });
|
|
297
298
|
}
|
|
298
299
|
}
|
|
300
|
+
if (e.kind === "Resource" && spec.migrationPaths) checkMigrationPaths(repo, e);
|
|
299
301
|
if (e.kind === "API") {
|
|
300
302
|
const prov = spec.provider;
|
|
301
303
|
if (!refs.isValidRef(prov) || refs.kindOfRef(prov) !== "Component") {
|
|
@@ -334,9 +336,57 @@ export function loadDescriptors(repo, cfg, bundle, refs) {
|
|
|
334
336
|
}, errors);
|
|
335
337
|
}
|
|
336
338
|
|
|
339
|
+
// One directory migrates one database. Two Resources declaring the same path
|
|
340
|
+
// would make every migration under it ambiguous, and impact must not pick.
|
|
341
|
+
// Nested declarations are fine: the longest prefix wins, as for ownership.
|
|
342
|
+
const declaredBy = new Map();
|
|
343
|
+
for (const e of entities) {
|
|
344
|
+
if (e.kind !== "Resource") continue;
|
|
345
|
+
for (const p of e.doc.spec?.migrationPaths || []) {
|
|
346
|
+
if (!declaredBy.has(p)) declaredBy.set(p, []);
|
|
347
|
+
declaredBy.get(p).push(e);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const [p, owners] of [...declaredBy.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
351
|
+
if (owners.length < 2) continue;
|
|
352
|
+
const names = owners.map((o) => o.ref).sort();
|
|
353
|
+
errors.push(new AgentDocError(
|
|
354
|
+
CODES.MIGRATION_PATH,
|
|
355
|
+
"migration path " + JSON.stringify(p) + " is declared by several Resources (" + names.join(", ") + "); a migration directory migrates one logical database",
|
|
356
|
+
{ path: owners[0].file, ref: names[0] }
|
|
357
|
+
));
|
|
358
|
+
}
|
|
359
|
+
|
|
337
360
|
return { entities, byRef, byKindName, componentFiles: new Set(componentFiles), errors, componentSet, centralSet };
|
|
338
361
|
}
|
|
339
362
|
|
|
363
|
+
// `spec.migrationPaths` on a Resource: repository directories (or files) whose
|
|
364
|
+
// changes are changes to this database. They are whole-segment prefixes with
|
|
365
|
+
// the semantics of `core/paths.mjs`, never globs: a glob would need an engine
|
|
366
|
+
// this system does not ship, and `drizzle/**` means nothing a prefix does not.
|
|
367
|
+
function checkMigrationPaths(repo, e) {
|
|
368
|
+
if (e.doc.spec.type !== "logical-database") {
|
|
369
|
+
throw new AgentDocError(
|
|
370
|
+
CODES.MIGRATION_PATH,
|
|
371
|
+
"spec.migrationPaths is only meaningful on a logical-database Resource, not on type '" + e.doc.spec.type + "'",
|
|
372
|
+
{ path: e.file, ref: e.ref }
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
for (const p of e.doc.spec.migrationPaths) {
|
|
376
|
+
assertRepoPath(p, e.file);
|
|
377
|
+
if (/[*?[\]{}!]/.test(p)) {
|
|
378
|
+
throw new AgentDocError(
|
|
379
|
+
CODES.MIGRATION_PATH,
|
|
380
|
+
"migration path " + JSON.stringify(p) + " is a glob; declare the directory itself (e.g. \"" + p.replace(/\/?[^/]*[*?[\]{}!].*$/, "") + "\"), which covers everything below it",
|
|
381
|
+
{ path: e.file, ref: e.ref }
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (!repo.exists(p)) {
|
|
385
|
+
throw new AgentDocError(CODES.PATH_UNRESOLVED, "migration path does not resolve: " + p, { path: e.file, ref: e.ref });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
340
390
|
export function loadJourneys(repo, cfg, bundle, byRef) {
|
|
341
391
|
const errors = [];
|
|
342
392
|
const jPath = cfg.discovery.journeyDefinitions;
|
|
@@ -473,7 +523,7 @@ export function loadReviewedOverrides(repo, cfg, refs, byRef) {
|
|
|
473
523
|
: null;
|
|
474
524
|
for (const ev of o.evidence) {
|
|
475
525
|
const p = ev.path;
|
|
476
|
-
const scoped = p === o.contractRef ||
|
|
526
|
+
const scoped = p === o.contractRef || ownsPath(subjectRoot, p) || (targetRoot !== null && ownsPath(targetRoot, p));
|
|
477
527
|
if (!scoped) {
|
|
478
528
|
throw new AgentDocError(
|
|
479
529
|
CODES.RELATION_KIND,
|
package/core/discover.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { AgentDocError, CODES } from "./codes.mjs";
|
|
9
9
|
import { expandGlob, globToRegExp, assertGlob } from "../core/fsx.mjs";
|
|
10
|
+
import { joinPath, ownsPath } from "./paths.mjs";
|
|
10
11
|
|
|
11
12
|
// Roots excluded from inference and scaffolding. Authored descriptors strictly
|
|
12
13
|
// below them can still claim units; descriptors at these exact roots cannot.
|
|
@@ -20,7 +21,7 @@ function notUnitRoots(cfg) {
|
|
|
20
21
|
// mode shares this policy even for units with an existing authored descriptor.
|
|
21
22
|
export function isNotUnitRoot(root, cfg) {
|
|
22
23
|
return notUnitRoots(cfg)
|
|
23
|
-
.some((excluded) =>
|
|
24
|
+
.some((excluded) => ownsPath(excluded, root));
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function discoverUnits(repo, cfg, adapters) {
|
|
@@ -54,7 +55,7 @@ export function discoverUnits(repo, cfg, adapters) {
|
|
|
54
55
|
for (const r of cfg.discovery.supplementalRoots || []) {
|
|
55
56
|
candidates.add(r);
|
|
56
57
|
// The descriptor loader also reads these fixed paths outside the globs.
|
|
57
|
-
if (repo.exists(r
|
|
58
|
+
if (repo.exists(joinPath(r, cfg.discovery.componentDescriptorName))) descriptorRoots.add(r);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// Adapters may contribute roots the descriptor globs cannot know about: a
|