agent-doc-system 1.3.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 +55 -1
- package/adapters/database.mjs +153 -5
- package/core/codes.mjs +2 -0
- package/core/compile.mjs +5 -1
- package/core/descriptors.mjs +49 -0
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +74 -11
- package/docs/ADAPTERS.md +48 -4
- package/docs/CI.md +3 -1
- package/docs/QUERY.md +25 -4
- package/docs/SPEC.md +42 -0
- package/docs/ci/github-actions.yml +1 -1
- package/package.json +1 -1
- package/schemas/resource.schema.json +9 -1
- package/templates/ci.yml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,58 @@ refused by the next with `AGENTDOC_COMPILER_MISMATCH` until it is recompiled.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.4.0] - 2026-11-10
|
|
13
|
+
|
|
14
|
+
`impact` now treats a change under a Drizzle migration directory as a change
|
|
15
|
+
to the database it migrates. It reaches every component bound to that
|
|
16
|
+
database, not only the component that holds the file. Any other ORM layout can
|
|
17
|
+
be declared on the database Resource with the new `spec.migrationPaths`.
|
|
18
|
+
Existing `migrations/` and `alembic/versions/` paths give the same answer as
|
|
19
|
+
before.
|
|
20
|
+
|
|
21
|
+
Before you upgrade:
|
|
22
|
+
|
|
23
|
+
- **Recompile committed graphs.** As with every release, a graph compiled by
|
|
24
|
+
1.3.0 is refused with `AGENTDOC_COMPILER_MISMATCH` until you run
|
|
25
|
+
`agentdoc compile` again. Graphs of repositories that enable the `database`
|
|
26
|
+
adapter also gain `derived.migrationDirs` and the provenance behind it.
|
|
27
|
+
- **A Drizzle config whose `out` is not a plain string** (an environment
|
|
28
|
+
variable, an expression, a spread) now raises the warning
|
|
29
|
+
`AGENTDOC_MIGRATION_DIR_UNRESOLVED`. Declare the directory with
|
|
30
|
+
`spec.migrationPaths`, or accept the warning ([#63]).
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- A `logical-database` Resource can declare where its schema migrations live,
|
|
35
|
+
with `spec.migrationPaths` ([#63]). A change under a declared directory is a
|
|
36
|
+
change to that Resource, whichever component owns the file, so `impact`
|
|
37
|
+
reaches every component bound to the database. Paths are whole-segment
|
|
38
|
+
prefixes, not globs: `frontend/apps/web/drizzle` covers `0036_x.sql` and
|
|
39
|
+
`meta/_journal.json`, and never `drizzle-extra/`. A glob, a declaration on
|
|
40
|
+
another Resource type, or one path declared by two Resources fails with the
|
|
41
|
+
new `AGENTDOC_MIGRATION_PATH`; a path outside the repository fails with
|
|
42
|
+
`AGENTDOC_PATH_ESCAPE`, a missing one with `AGENTDOC_PATH_UNRESOLVED`. The
|
|
43
|
+
declaration never creates a binding.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `impact` now classifies a change under a Drizzle migration directory as a
|
|
48
|
+
database change ([#63]). Only `migrations/`, `migration/` and
|
|
49
|
+
`alembic/versions/` used to count, so `impact
|
|
50
|
+
frontend/apps/web/drizzle/0036_email_locale.sql` reported the web component
|
|
51
|
+
alone and missed every other component bound to the same database. The
|
|
52
|
+
`database` adapter now reads Drizzle Kit's `out` from `drizzle.config.ts`
|
|
53
|
+
(`.js`, `.mjs`) as a string literal, without running it, and defaults to
|
|
54
|
+
`./drizzle`. An `out` it cannot read statically is reported with the
|
|
55
|
+
acceptable warning `AGENTDOC_MIGRATION_DIR_UNRESOLVED` instead of being
|
|
56
|
+
guessed. Detected directories, including `migrations` and `alembic/versions`,
|
|
57
|
+
are recorded in the graph as `derived.migrationDirs` with provenance, and
|
|
58
|
+
`impact` classifies from them. A detected directory still reaches a database
|
|
59
|
+
only through the component's existing bindings. The answer for an existing
|
|
60
|
+
`migrations/` or `alembic/versions/` path is unchanged. Committed graphs of
|
|
61
|
+
repositories that enable the `database` adapter gain `migrationDirs` and
|
|
62
|
+
must be recompiled.
|
|
63
|
+
|
|
12
64
|
## [1.3.0] - 2026-11-07
|
|
13
65
|
|
|
14
66
|
A graph compiled on your machine now passes `check` in CI and in a clean
|
|
@@ -367,7 +419,8 @@ First public release.
|
|
|
367
419
|
for project health.
|
|
368
420
|
- An Agent Skill under `skills/agent-doc-system/`.
|
|
369
421
|
|
|
370
|
-
[Unreleased]: https://github.com/lbframe/agent-doc-system/compare/v1.
|
|
422
|
+
[Unreleased]: https://github.com/lbframe/agent-doc-system/compare/v1.4.0...HEAD
|
|
423
|
+
[1.4.0]: https://github.com/lbframe/agent-doc-system/compare/v1.3.0...v1.4.0
|
|
371
424
|
[1.3.0]: https://github.com/lbframe/agent-doc-system/compare/v1.2.0...v1.3.0
|
|
372
425
|
[1.2.0]: https://github.com/lbframe/agent-doc-system/compare/v1.1.0...v1.2.0
|
|
373
426
|
[1.1.0]: https://github.com/lbframe/agent-doc-system/compare/v1.0.0...v1.1.0
|
|
@@ -400,3 +453,4 @@ First public release.
|
|
|
400
453
|
[#51]: https://github.com/lbframe/agent-doc-system/issues/51
|
|
401
454
|
[#55]: https://github.com/lbframe/agent-doc-system/issues/55
|
|
402
455
|
[#59]: https://github.com/lbframe/agent-doc-system/issues/59
|
|
456
|
+
[#63]: https://github.com/lbframe/agent-doc-system/issues/63
|
package/adapters/database.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// library shipped migrations but explicitly did not own a database.
|
|
10
10
|
import { Adapter } from "./registry.mjs";
|
|
11
11
|
import { firstExistingFile, findMigrationDirs } from "../core/sourcescan.mjs";
|
|
12
|
-
import { joinPath } from "../core/paths.mjs";
|
|
12
|
+
import { canonicalPath, joinPath } from "../core/paths.mjs";
|
|
13
13
|
|
|
14
14
|
const ORM_CONFIG = [
|
|
15
15
|
"drizzle.config.ts", "drizzle.config.js", "drizzle.config.mjs",
|
|
@@ -48,6 +48,12 @@ export class DatabaseAdapter extends Adapter {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
const pr = prov.add("observed", evidencePath, "migration-extractor", ["der:" + compRef + ":/bindings"]);
|
|
51
|
+
// Where this unit's migrations live, recorded in the graph so `impact`
|
|
52
|
+
// classifies a change from the graph rather than from a list of names.
|
|
53
|
+
// Authority over a directory, never ownership of a database: these records
|
|
54
|
+
// create no binding.
|
|
55
|
+
const recorded = recordMigrationDirs(ctx, unit, migrationDirs, ormConfig, pr);
|
|
56
|
+
const withDirs = (out) => (recorded.length ? { ...out, migrationDirs: recorded } : out);
|
|
51
57
|
|
|
52
58
|
// A component that both migrates and opens a connection owns the database.
|
|
53
59
|
const opensConnection = hasConnectionEvidence(ctx, unit);
|
|
@@ -66,7 +72,7 @@ export class DatabaseAdapter extends Adapter {
|
|
|
66
72
|
evidenceClass: "DERIVED", confidence: "candidate", provRecs: [pr],
|
|
67
73
|
semantics: "a schema exists here, but which component owns the logical database is unresolved",
|
|
68
74
|
});
|
|
69
|
-
return { migrationAuthority: evidencePath };
|
|
75
|
+
return withDirs({ migrationAuthority: evidencePath });
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
const name = compRef.split("/")[1];
|
|
@@ -89,7 +95,7 @@ export class DatabaseAdapter extends Adapter {
|
|
|
89
95
|
evidenceClass: "DERIVED", confidence: "candidate", provRecs: [pr],
|
|
90
96
|
semantics: "this component opens a database connection, but no authored Resource claims ownership",
|
|
91
97
|
});
|
|
92
|
-
return { migrationAuthority: evidencePath };
|
|
98
|
+
return withDirs({ migrationAuthority: evidencePath });
|
|
93
99
|
}
|
|
94
100
|
ctx.addFact(compRef, "resource.ownership", target.name, {
|
|
95
101
|
evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
|
|
@@ -101,10 +107,10 @@ export class DatabaseAdapter extends Adapter {
|
|
|
101
107
|
const client = ctx.clientKeyForResourceType("logical-database");
|
|
102
108
|
const attrs = client ? { via: "migrations+connection", client } : { via: "migrations+connection" };
|
|
103
109
|
ctx.addEdge("usesResource", compRef, target.ref, attrs, [pr], "DERIVED");
|
|
104
|
-
return {
|
|
110
|
+
return withDirs({
|
|
105
111
|
migrationAuthority: evidencePath,
|
|
106
112
|
bindings: [{ kind: "database", logicalResourceRef: target.ref, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) }],
|
|
107
|
-
};
|
|
113
|
+
});
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -129,3 +135,145 @@ function findDatabaseResource(ctx, name) {
|
|
|
129
135
|
);
|
|
130
136
|
return cands.length === 1 ? cands[0] : null;
|
|
131
137
|
}
|
|
138
|
+
|
|
139
|
+
// ── migration directories ───────────────────────────────────────────────────
|
|
140
|
+
//
|
|
141
|
+
// Two routes, each with its own evidence:
|
|
142
|
+
// - a directory named by convention (`migrations`, `migration`,
|
|
143
|
+
// `alembic/versions`), evidenced by a file inside it;
|
|
144
|
+
// - Drizzle Kit's `out`, evidenced by the config file that states it.
|
|
145
|
+
// Prisma Migrate writes to `prisma/migrations`, which the first route covers.
|
|
146
|
+
function recordMigrationDirs(ctx, unit, conventionDirs, ormConfig, pr) {
|
|
147
|
+
const { repo, prov, compRef } = ctx;
|
|
148
|
+
const subject = "der:" + compRef + ":/migrationDirs";
|
|
149
|
+
const byPath = new Map();
|
|
150
|
+
const add = (dir, via, rec) => {
|
|
151
|
+
if (!byPath.has(dir)) byPath.set(dir, { path: dir, via: new Set(), _provs: new Set() });
|
|
152
|
+
byPath.get(dir).via.add(via);
|
|
153
|
+
byPath.get(dir)._provs.add(rec);
|
|
154
|
+
};
|
|
155
|
+
for (const dir of conventionDirs) {
|
|
156
|
+
// A directory with no file in the input set has nothing a change could
|
|
157
|
+
// touch, and nothing to cite as evidence.
|
|
158
|
+
const file = repo.walkUnit(unit.root, dir)[0];
|
|
159
|
+
if (!file) continue;
|
|
160
|
+
let rec;
|
|
161
|
+
if (file === pr.path) {
|
|
162
|
+
rec = pr;
|
|
163
|
+
pr.subjects.add(subject);
|
|
164
|
+
} else {
|
|
165
|
+
rec = prov.add("observed", file, "migration-extractor", [subject]);
|
|
166
|
+
}
|
|
167
|
+
add(dir, /(^|\/)alembic\/versions$/.test(dir) ? "alembic-versions" : "directory-name", rec);
|
|
168
|
+
}
|
|
169
|
+
if (ormConfig && /(^|\/)drizzle\.config\.[cm]?[jt]s$/.test(ormConfig)) {
|
|
170
|
+
const dir = drizzleOutDir(ctx, unit, ormConfig);
|
|
171
|
+
if (dir) add(dir, "drizzle-config", prov.add("observed", ormConfig, "drizzle-config-extractor", [subject]));
|
|
172
|
+
}
|
|
173
|
+
return [...byPath.values()]
|
|
174
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1))
|
|
175
|
+
.map((d) => ({ path: d.path, via: [...d.via].sort(), provenanceIds: null, _provs: d._provs }));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Drizzle Kit writes migrations to `out`, `./drizzle` when absent. The config
|
|
179
|
+
// is code, and it is never executed: only a string literal is read. Anything
|
|
180
|
+
// else — an expression, an environment variable, a spread that could carry
|
|
181
|
+
// `out`, several `out` keys — is reported and not guessed, because a wrong
|
|
182
|
+
// directory would route migrations to the wrong database silently.
|
|
183
|
+
function drizzleOutDir(ctx, unit, configPath) {
|
|
184
|
+
const { repo, compRef } = ctx;
|
|
185
|
+
const unresolved = (why) => {
|
|
186
|
+
ctx.diagnostics.push({
|
|
187
|
+
severity: "warning",
|
|
188
|
+
code: "AGENTDOC_MIGRATION_DIR_UNRESOLVED",
|
|
189
|
+
subject: compRef,
|
|
190
|
+
refs: [compRef],
|
|
191
|
+
message:
|
|
192
|
+
"Drizzle migration directory in '" + configPath + "' is not recorded: " + why + ". " +
|
|
193
|
+
"Declare it with spec.migrationPaths on the logical-database Resource it migrates",
|
|
194
|
+
paths: [configPath],
|
|
195
|
+
});
|
|
196
|
+
return null;
|
|
197
|
+
};
|
|
198
|
+
const parsed = drizzleOut(repo.readText(configPath));
|
|
199
|
+
if (parsed.error) return unresolved(parsed.error);
|
|
200
|
+
const value = parsed.value ?? "./drizzle";
|
|
201
|
+
// Relative to the directory drizzle-kit runs from, which is the config's own
|
|
202
|
+
// directory in the documented layout (and the unit root here).
|
|
203
|
+
const base = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/")) : ".";
|
|
204
|
+
const dir = value.startsWith("/") ? null : canonicalPath(joinPath(base, value));
|
|
205
|
+
if (!dir || dir === "." || dir === canonicalPath(unit.root)) {
|
|
206
|
+
return unresolved("out " + JSON.stringify(value) + " is not a directory inside the unit at '" + unit.root + "'");
|
|
207
|
+
}
|
|
208
|
+
if (!repo.ownsUnitPath(unit.root, dir)) {
|
|
209
|
+
return unresolved("out " + JSON.stringify(value) + " is outside the unit at '" + unit.root + "'");
|
|
210
|
+
}
|
|
211
|
+
// Not generated yet: nothing to classify. The probe is an input, so the
|
|
212
|
+
// first generated migration invalidates the graph.
|
|
213
|
+
if (!repo.isDir(dir)) return null;
|
|
214
|
+
return dir;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Static read of `out` from a Drizzle config. Comments are removed and string
|
|
218
|
+
// literals set aside first, so neither `// out: "x"` nor a string containing
|
|
219
|
+
// "out:" is mistaken for the key.
|
|
220
|
+
export function drizzleOut(text) {
|
|
221
|
+
const strings = [];
|
|
222
|
+
let code = "";
|
|
223
|
+
for (let i = 0; i < text.length; ) {
|
|
224
|
+
const c = text[i];
|
|
225
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
226
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
230
|
+
const end = text.indexOf("*/", i + 2);
|
|
231
|
+
i = end < 0 ? text.length : end + 2;
|
|
232
|
+
code += " ";
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
236
|
+
let j = i + 1;
|
|
237
|
+
let value = "";
|
|
238
|
+
let closed = false;
|
|
239
|
+
while (j < text.length) {
|
|
240
|
+
if (text[j] === "\\") {
|
|
241
|
+
value += text[j + 1] ?? "";
|
|
242
|
+
j += 2;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (text[j] === c) { closed = true; break; }
|
|
246
|
+
if (c !== "`" && text[j] === "\n") break;
|
|
247
|
+
value += text[j];
|
|
248
|
+
j++;
|
|
249
|
+
}
|
|
250
|
+
if (!closed) return { error: "the config could not be read statically" };
|
|
251
|
+
// A template with a substitution is an expression, not a literal.
|
|
252
|
+
strings.push(c === "`" && value.includes("${") ? null : value);
|
|
253
|
+
code += "\u0000" + (strings.length - 1) + "\u0000";
|
|
254
|
+
i = j + 1;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
code += c;
|
|
258
|
+
i++;
|
|
259
|
+
}
|
|
260
|
+
const hits = [];
|
|
261
|
+
for (const m of code.matchAll(/(?:^|[{,\s])(?:out|\u0000(\d+)\u0000)\s*(:|[,}])/g)) {
|
|
262
|
+
// A quoted key counts only when it says "out".
|
|
263
|
+
if (m[1] !== undefined && strings[Number(m[1])] !== "out") continue;
|
|
264
|
+
hits.push(m);
|
|
265
|
+
}
|
|
266
|
+
if (hits.length === 0) {
|
|
267
|
+
// A spread could bring `out` in from elsewhere; the default is then a guess.
|
|
268
|
+
if (/\.\.\.\s*[A-Za-z_$(]/.test(code)) return { error: "the config spreads another object that may set out" };
|
|
269
|
+
return { value: null };
|
|
270
|
+
}
|
|
271
|
+
if (hits.length > 1) return { error: "out is set more than once" };
|
|
272
|
+
const m = hits[0];
|
|
273
|
+
if (m[2] !== ":") return { error: "out is a shorthand property, not a string literal" };
|
|
274
|
+
const rest = code.slice(m.index + m[0].length);
|
|
275
|
+
const lit = /^\s*\u0000(\d+)\u0000\s*(?:[,}]|$)/.exec(rest);
|
|
276
|
+
const value = lit ? strings[Number(lit[1])] : null;
|
|
277
|
+
if (typeof value !== "string" || value.trim() === "") return { error: "out is not a string literal" };
|
|
278
|
+
return { value };
|
|
279
|
+
}
|
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
|
@@ -498,6 +498,10 @@ export function compile(root, opts = {}) {
|
|
|
498
498
|
"entf:" + e.ref + ":/metadata/description",
|
|
499
499
|
];
|
|
500
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");
|
|
501
505
|
prov.add("declared", e.file, "descriptor-reader", subjects);
|
|
502
506
|
}
|
|
503
507
|
|
|
@@ -777,7 +781,7 @@ export function compile(root, opts = {}) {
|
|
|
777
781
|
return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
|
|
778
782
|
}
|
|
779
783
|
for (const d of derivedMap.values()) {
|
|
780
|
-
for (const list of [d.bindings || [], d.healthContracts || []]) {
|
|
784
|
+
for (const list of [d.bindings || [], d.healthContracts || [], d.migrationDirs || []]) {
|
|
781
785
|
for (const b of list) {
|
|
782
786
|
b.provenanceIds = [...(b._provs || [])].map((p) => provFinal.idOf.get(p)).filter(Boolean).sort();
|
|
783
787
|
delete b._provs;
|
package/core/descriptors.mjs
CHANGED
|
@@ -297,6 +297,7 @@ export function loadDescriptors(repo, cfg, bundle, refs) {
|
|
|
297
297
|
throw new AgentDocError(CODES.PATH_UNRESOLVED, "context link does not resolve: " + p, { path: e.file, ref: e.ref });
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
+
if (e.kind === "Resource" && spec.migrationPaths) checkMigrationPaths(repo, e);
|
|
300
301
|
if (e.kind === "API") {
|
|
301
302
|
const prov = spec.provider;
|
|
302
303
|
if (!refs.isValidRef(prov) || refs.kindOfRef(prov) !== "Component") {
|
|
@@ -335,9 +336,57 @@ export function loadDescriptors(repo, cfg, bundle, refs) {
|
|
|
335
336
|
}, errors);
|
|
336
337
|
}
|
|
337
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
|
+
|
|
338
360
|
return { entities, byRef, byKindName, componentFiles: new Set(componentFiles), errors, componentSet, centralSet };
|
|
339
361
|
}
|
|
340
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
|
+
|
|
341
390
|
export function loadJourneys(repo, cfg, bundle, byRef) {
|
|
342
391
|
const errors = [];
|
|
343
392
|
const jPath = cfg.discovery.journeyDefinitions;
|
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.4.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.
|
package/core/impact.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// from the graph alone, so the answer is available before any exploration.
|
|
6
6
|
import { ContextIndex } from "./query.mjs";
|
|
7
7
|
import { dependentScope, verificationForChange } from "./dependents.mjs";
|
|
8
|
-
import { canonicalPath, ownsPath } from "./paths.mjs";
|
|
8
|
+
import { canonicalPath, ownsPath, longestOwner } from "./paths.mjs";
|
|
9
9
|
|
|
10
10
|
// Changed paths are compared in the graph's canonical form (git's: no "./").
|
|
11
11
|
// A path that is not repository-relative is kept verbatim so it is reported
|
|
@@ -23,7 +23,10 @@ const canonical = (p) => canonicalPath(p) ?? p;
|
|
|
23
23
|
// - every extractor identity, which names the technology -> dependency /
|
|
24
24
|
// verification
|
|
25
25
|
// - every documentation and constraint path on an entity -> documentation
|
|
26
|
-
|
|
26
|
+
// - every migration directory: declared on a logical-database -> resource
|
|
27
|
+
// Resource (`spec.migrationPaths`), or detected per component
|
|
28
|
+
// by the database adapter (`derived.migrationDirs`)
|
|
29
|
+
function classify(graph, p, idx, migrations) {
|
|
27
30
|
for (const c of graph.contracts) if (c.ref === p) return "contract";
|
|
28
31
|
for (const e of graph.entities) {
|
|
29
32
|
const d = e.derived || {};
|
|
@@ -32,6 +35,13 @@ function classify(graph, p, idx) {
|
|
|
32
35
|
for (const x of d.runbooks || []) if (x === p) return "documentation";
|
|
33
36
|
}
|
|
34
37
|
if (/\.(md|mdx|adoc|rst|txt)$/.test(p)) return "documentation";
|
|
38
|
+
if (migrationHit(migrations, p)) return "resource";
|
|
39
|
+
// Kept as a fallback, and the one name-based rule left: before the graph
|
|
40
|
+
// recorded migration directories this regex was the whole classification,
|
|
41
|
+
// and it still has to answer where nothing is recorded — a repository that
|
|
42
|
+
// does not enable the database adapter, a migration directory deeper than
|
|
43
|
+
// the adapter's bounded walk, a graph built by hand. Dropping it would change
|
|
44
|
+
// the answer for those repositories; what the graph records only adds to it.
|
|
35
45
|
if (/(^|\/)(migrations?|alembic\/versions)\//.test(p)) return "resource";
|
|
36
46
|
// A Resource named directly — by ref, or by the descriptor file that declares
|
|
37
47
|
// it — is a resource change. Without this, `impact resource:default/x` reported
|
|
@@ -46,8 +56,43 @@ function classify(graph, p, idx) {
|
|
|
46
56
|
return null;
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
// Migration directories recorded in the graph, with what made them one.
|
|
60
|
+
function migrationDirs(graph) {
|
|
61
|
+
const declared = new Map(); // path -> Resource ref
|
|
62
|
+
const detected = new Map(); // path -> { componentRef, via }
|
|
63
|
+
for (const e of graph.entities) {
|
|
64
|
+
if (e.kind === "Resource") {
|
|
65
|
+
for (const p of e.entity?.spec?.migrationPaths || []) declared.set(p, e.ref);
|
|
66
|
+
}
|
|
67
|
+
if (e.kind === "Component") {
|
|
68
|
+
for (const d of e.derived?.migrationDirs || []) detected.set(d.path, { componentRef: e.ref, via: d.via || [] });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { declared, detected };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The migration directory a path is under, by whole path segments, the most
|
|
75
|
+
// specific first. An authored declaration beats a detected directory: it names
|
|
76
|
+
// the database, where detection only names the component whose bindings say
|
|
77
|
+
// which database it is.
|
|
78
|
+
function migrationHit(migrations, p) {
|
|
79
|
+
if (!migrations) return null;
|
|
80
|
+
const d = longestOwner(migrations.declared.keys(), p);
|
|
81
|
+
if (d) return { dir: d, resourceRef: migrations.declared.get(d), how: "declared on " + migrations.declared.get(d) };
|
|
82
|
+
const x = longestOwner(migrations.detected.keys(), p);
|
|
83
|
+
if (!x) return null;
|
|
84
|
+
const { componentRef, via } = migrations.detected.get(x);
|
|
85
|
+
// A directory found by its conventional name is exactly what the fallback
|
|
86
|
+
// regex already classified; only a directory nothing but the graph knows
|
|
87
|
+
// about is worth a reason of its own.
|
|
88
|
+
const conventional = via.every((v) => v === "directory-name" || v === "alembic-versions");
|
|
89
|
+
return { dir: x, componentRef, conventional, how: "detected from " + via.join(", ") + " in " + componentRef };
|
|
90
|
+
}
|
|
91
|
+
|
|
49
92
|
export function impact(graph, changedPaths) {
|
|
50
93
|
const idx = new ContextIndex(graph);
|
|
94
|
+
const migrations = migrationDirs(graph);
|
|
95
|
+
const migrationReasons = new Map(); // Resource ref -> Set(reason)
|
|
51
96
|
const affectedComponents = new Map(); // ref -> Set(reason)
|
|
52
97
|
const add = (ref, reason) => {
|
|
53
98
|
if (idx.byRef.get(ref)?.kind !== "Component") return;
|
|
@@ -72,18 +117,35 @@ export function impact(graph, changedPaths) {
|
|
|
72
117
|
// while the components actually bound to it went unmentioned.
|
|
73
118
|
if (entity && entity.kind === "Component") add(entity.ref, "direct file change inside " + entity.ref);
|
|
74
119
|
else unmatched.push(p);
|
|
75
|
-
const kind = classify(graph, p, idx);
|
|
120
|
+
const kind = classify(graph, p, idx, migrations);
|
|
76
121
|
if (kind === "contract") contracts.add(p);
|
|
77
122
|
if (kind === "documentation") docs.add(p);
|
|
78
123
|
if (kind === "resource") {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
124
|
+
const hit = migrationHit(migrations, p);
|
|
125
|
+
const because = (ref) => {
|
|
126
|
+
if (!hit || hit.conventional) return;
|
|
127
|
+
if (!migrationReasons.has(ref)) migrationReasons.set(ref, new Set());
|
|
128
|
+
migrationReasons.get(ref).add("migration " + p + " changes " + ref + " (migration directory " + hit.dir + ", " + hit.how + ")");
|
|
129
|
+
};
|
|
130
|
+
if (hit?.resourceRef) {
|
|
131
|
+
// Authored: the declaration names the database, whichever component
|
|
132
|
+
// owns the file and whatever that component is bound to.
|
|
133
|
+
resources.add(hit.resourceRef);
|
|
134
|
+
because(hit.resourceRef);
|
|
135
|
+
} else {
|
|
136
|
+
resources.add(entity?.kind === "Resource" ? entity.ref : p);
|
|
137
|
+
// Migration authority is not ownership. Only follow an existing binding
|
|
138
|
+
// from the component containing this migration to a logical database.
|
|
139
|
+
const owner = hit?.componentRef ?? (entity?.kind === "Component" ? entity.ref : null);
|
|
140
|
+
if (owner) {
|
|
141
|
+
for (const rel of graph.relations) {
|
|
142
|
+
const bound = rel.type === "usesResource" && rel.sourceRef === owner ? rel.targetRef
|
|
143
|
+
: rel.type === "resourceUsedBy" && rel.targetRef === owner ? rel.sourceRef : null;
|
|
144
|
+
if (idx.byRef.get(bound)?.entity.spec.type === "logical-database") {
|
|
145
|
+
resources.add(bound);
|
|
146
|
+
because(bound);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
87
149
|
}
|
|
88
150
|
}
|
|
89
151
|
}
|
|
@@ -135,6 +197,7 @@ export function impact(graph, changedPaths) {
|
|
|
135
197
|
const user = rel.type === "usesResource" ? rel.sourceRef : rel.targetRef;
|
|
136
198
|
if (r === bound) {
|
|
137
199
|
add(user, "resource change touches " + bound);
|
|
200
|
+
for (const reason of migrationReasons.get(bound) || []) add(user, reason);
|
|
138
201
|
}
|
|
139
202
|
}
|
|
140
203
|
}
|
package/docs/ADAPTERS.md
CHANGED
|
@@ -107,7 +107,7 @@ cannot name the file a fact came from, you cannot add the fact.
|
|
|
107
107
|
| `dockerfile` | `Dockerfile`, `Containerfile` | deployable evidence |
|
|
108
108
|
| `deploy-config` | `fly.toml`, `Procfile`, … | deployable evidence |
|
|
109
109
|
| `wrangler` | `wrangler.toml/json` | bindings, crons, service bindings, trigger targets |
|
|
110
|
-
| `database` | migrations, ORM config, pool evidence | logical-database ownership, or a candidate |
|
|
110
|
+
| `database` | migrations, ORM config, pool evidence | logical-database ownership, or a candidate; the unit's migration directories |
|
|
111
111
|
| `object-store` | bucket literals | logical-dataset edges, or an ambiguity |
|
|
112
112
|
| `cache` | cache client evidence | cache bindings |
|
|
113
113
|
| `events` | subject literals, declared constants, event-contract files | producers, consumers, reserved subjects |
|
|
@@ -312,6 +312,45 @@ the result is `AGENTDOC_BINDING_AMBIGUOUS` and no edge.
|
|
|
312
312
|
file pulled in with `-r` / `-c`; a lockfile's resolved versions; a PDM
|
|
313
313
|
workspace; a workspace manifest deeper than the search bound.
|
|
314
314
|
|
|
315
|
+
### `database`
|
|
316
|
+
|
|
317
|
+
Migration authority and a connection string in one unit resolve to an authored
|
|
318
|
+
logical-database Resource (by name: `<component>-postgres`, `-database`,
|
|
319
|
+
`-db`, or the single Resource whose name starts with the component's).
|
|
320
|
+
Migrations alone give a candidate and `AGENTDOC_RESOURCE_CANDIDATE`, never an
|
|
321
|
+
edge.
|
|
322
|
+
|
|
323
|
+
The adapter also records where the unit's migrations live, as
|
|
324
|
+
`derived.migrationDirs` (`path`, `via`, `provenanceIds`). `impact` classifies a
|
|
325
|
+
change under one of them as a resource change. Two routes, each with its own
|
|
326
|
+
evidence:
|
|
327
|
+
|
|
328
|
+
| `via` | directory | evidence |
|
|
329
|
+
|---|---|---|
|
|
330
|
+
| `directory-name` | a directory named `migrations` or `migration` inside the unit, at most five levels down; Prisma Migrate's `prisma/migrations` is one | the first file in it |
|
|
331
|
+
| `alembic-versions` | `alembic/versions` | the first file in it |
|
|
332
|
+
| `drizzle-config` | Drizzle Kit's `out` in the unit-root `drizzle.config.ts`, `.js` or `.mjs`; `./drizzle` when `out` is absent | the config file |
|
|
333
|
+
|
|
334
|
+
The Drizzle config is code and is never executed. `out` is read only as a
|
|
335
|
+
single string literal (a template literal without `${...}` counts), after
|
|
336
|
+
comments are removed, and resolved against the config's directory. An `out`
|
|
337
|
+
that is an expression, an environment variable, a shorthand property, set more
|
|
338
|
+
than once, or possibly brought in by a spread is not guessed: the adapter
|
|
339
|
+
records nothing and reports `AGENTDOC_MIGRATION_DIR_UNRESOLVED`, a warning that
|
|
340
|
+
can be accepted. An `out` outside the unit is reported the same way. An `out`
|
|
341
|
+
directory that does not exist yet records nothing; the existence probe is an
|
|
342
|
+
input, so generating the first migration invalidates the graph.
|
|
343
|
+
|
|
344
|
+
A recorded directory is migration authority, not ownership: it creates no
|
|
345
|
+
binding. A migration change reaches a database only through the enclosing
|
|
346
|
+
component's existing `usesResource` relations, or through a Resource that
|
|
347
|
+
declares the directory in `spec.migrationPaths` (see `SPEC.md`).
|
|
348
|
+
|
|
349
|
+
**What it cannot see:** a Drizzle config passed with `--config` under another
|
|
350
|
+
name, a config below the unit root, and the migration layouts of Knex, Goose,
|
|
351
|
+
Sequelize or TypeORM outside a `migrations` directory. Declare those with
|
|
352
|
+
`spec.migrationPaths`.
|
|
353
|
+
|
|
315
354
|
`asyncapi` is a contract **type** (declared on an API descriptor, validated by
|
|
316
355
|
the contract layer), not an adapter — there is no `asyncapi` entry in
|
|
317
356
|
`adapters: [...]`.
|
|
@@ -373,9 +412,14 @@ places, and those are the honest exceptions worth naming:
|
|
|
373
412
|
part of the input hash. A repository that does not enable `python` is not
|
|
374
413
|
probed, and its graph does not change.
|
|
375
414
|
- `core/impact.mjs` classifies a changed file by consulting the compiled graph —
|
|
376
|
-
its contracts, its provenance paths, its extractor identities
|
|
377
|
-
|
|
378
|
-
|
|
415
|
+
its contracts, its provenance paths, its extractor identities, its declared
|
|
416
|
+
and detected migration directories — rather than a hand-written list of
|
|
417
|
+
technology filenames. A list would silently under-report for every
|
|
418
|
+
repository that is not Node-and-Go. One name rule remains as a fallback: a
|
|
419
|
+
path under a `migrations/`, `migration/` or `alembic/versions/` segment is a
|
|
420
|
+
resource change even where the graph records no migration directory (the
|
|
421
|
+
`database` adapter is off, or the directory is deeper than its walk), so
|
|
422
|
+
those repositories keep the answer they had.
|
|
379
423
|
- `core/discover.mjs` excludes `agentdoc`, `docs`, `scripts`, `node_modules`, and
|
|
380
424
|
`.github` by default. `discovery.notUnitRoots` replaces that list (`[]`
|
|
381
425
|
disables it). These roots and their descendants cannot be inferred from
|
package/docs/CI.md
CHANGED
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
with: { fetch-depth: 0 }
|
|
21
21
|
- uses: actions/setup-node@v4
|
|
22
22
|
with: { node-version: '24' }
|
|
23
|
-
# Pin a tag for reproducibility, e.g. github:lbframe/agent-doc-system#v1.
|
|
23
|
+
# Pin a tag for reproducibility, e.g. github:lbframe/agent-doc-system#v1.4.0 —
|
|
24
24
|
# a compiler upgrade intentionally invalidates every committed graph.
|
|
25
25
|
- run: npm install -g github:lbframe/agent-doc-system
|
|
26
26
|
- run: agentdoc validate
|
|
@@ -93,6 +93,8 @@ YAML or schema error names its file and line and needs no table.
|
|
|
93
93
|
| `AGENTDOC_COMPILER_MISMATCH` | the compiler was upgraded | `compile` and commit |
|
|
94
94
|
| `AGENTDOC_OVERRIDE_STALE` | a reviewed override's cited evidence changed | review the interpretation, then `pin-evidence` and update the digest |
|
|
95
95
|
| `AGENTDOC_VERIFICATION_UNTIERED` | a check task has no recognisable tier | name the task, or extend the adapter |
|
|
96
|
+
| `AGENTDOC_MIGRATION_PATH` | a Resource's `spec.migrationPaths` is a glob, is on a non-database Resource, or is shared by two Resources | declare the directory itself, once, on the logical database it migrates |
|
|
97
|
+
| `AGENTDOC_MIGRATION_DIR_UNRESOLVED` | a Drizzle config's `out` is not a single string literal inside the unit | declare the directory with `spec.migrationPaths`, or accept the warning |
|
|
96
98
|
|
|
97
99
|
## Ordering inside `check`
|
|
98
100
|
|
package/docs/QUERY.md
CHANGED
|
@@ -215,10 +215,31 @@ receives the files no other component owns. A file nothing owns is listed in
|
|
|
215
215
|
|
|
216
216
|
Reachability rules: a contract change reaches its provider and every consumer;
|
|
217
217
|
a Resource change reaches components bound to that exact Resource. A migration
|
|
218
|
-
change
|
|
219
|
-
|
|
220
|
-
`
|
|
221
|
-
|
|
218
|
+
change is a Resource change. Which Resource is decided from the graph, most
|
|
219
|
+
specific directory first, by whole path segment (`drizzle` never matches
|
|
220
|
+
`drizzle-extra/`):
|
|
221
|
+
|
|
222
|
+
1. **Declared.** Under a path a logical-database Resource lists in
|
|
223
|
+
`spec.migrationPaths`: that Resource, whichever component owns the file and
|
|
224
|
+
whatever that component is bound to. An authored declaration beats a
|
|
225
|
+
detected directory.
|
|
226
|
+
2. **Detected.** Under a directory the `database` adapter recorded in a
|
|
227
|
+
component's `derived.migrationDirs` (a `migrations` directory, Alembic
|
|
228
|
+
`alembic/versions`, Drizzle Kit's `out`): the logical databases bound to
|
|
229
|
+
that component through existing `usesResource` / `resourceUsedBy`
|
|
230
|
+
relations. The changed path is listed too.
|
|
231
|
+
3. **By name.** Under a `migrations/`, `migration/` or `alembic/versions/`
|
|
232
|
+
segment the graph does not record (the `database` adapter is off): the same
|
|
233
|
+
as detected, from the enclosing component.
|
|
234
|
+
|
|
235
|
+
Every component bound to a reached Resource is affected, with the reason
|
|
236
|
+
`resource change touches <ref>`. When the directory was declared or found
|
|
237
|
+
through an ORM config, a second reason names the migration:
|
|
238
|
+
`migration drizzle/0001_x.sql changes resource:default/app-db (migration
|
|
239
|
+
directory drizzle, detected from drizzle-config in component:default/web)`.
|
|
240
|
+
Migration authority alone never establishes ownership or invents a Resource
|
|
241
|
+
binding: a detected directory whose component has no binding reaches no
|
|
242
|
+
Resource, and a declaration routes changes without creating a relation.
|
|
222
243
|
|
|
223
244
|
A component change reaches its reverse build/test dependency closure, plus
|
|
224
245
|
direct runtime-call and schedule peers. Dependencies are not affected just
|
package/docs/SPEC.md
CHANGED
|
@@ -77,6 +77,47 @@ authored in exactly one file, and the location is validated.
|
|
|
77
77
|
The namespace comes from the configuration, so two repositories in one graph
|
|
78
78
|
never collide.
|
|
79
79
|
|
|
80
|
+
### Resource migration paths
|
|
81
|
+
|
|
82
|
+
A `logical-database` Resource may list the repository directories that hold its
|
|
83
|
+
schema migrations:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
apiVersion: agentdoc.dev/v1
|
|
87
|
+
kind: Resource
|
|
88
|
+
metadata:
|
|
89
|
+
name: app-postgres
|
|
90
|
+
description: "Primary Postgres database of the application."
|
|
91
|
+
spec:
|
|
92
|
+
type: logical-database
|
|
93
|
+
system: app
|
|
94
|
+
migrationPaths:
|
|
95
|
+
- frontend/apps/web/drizzle
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Each entry is a repository path matched as a whole-segment prefix (the rules
|
|
99
|
+
of `core/paths.mjs`): `frontend/apps/web/drizzle` covers every file below it,
|
|
100
|
+
including `meta/_journal.json`, and never `frontend/apps/web/drizzle-extra/`.
|
|
101
|
+
`impact` treats a change under it as a change to this Resource, whichever
|
|
102
|
+
component owns the file, and reaches every component bound to the Resource.
|
|
103
|
+
The declaration is AUTHORED: it beats a migration directory the `database`
|
|
104
|
+
adapter detects, and it is pointed at by its own provenance subject
|
|
105
|
+
(`/entities/<i>/entity/spec/migrationPaths`). It never creates a
|
|
106
|
+
`usesResource` relation; which components use the database is still derived.
|
|
107
|
+
|
|
108
|
+
Validation, at load time:
|
|
109
|
+
|
|
110
|
+
| case | code |
|
|
111
|
+
|---|---|
|
|
112
|
+
| absolute, backslash, `.` or `..` segment, whitespace | `AGENTDOC_PATH_ESCAPE` |
|
|
113
|
+
| a path that does not exist (or is git-ignored) | `AGENTDOC_PATH_UNRESOLVED` |
|
|
114
|
+
| a glob (`*`, `?`, `[`, `{`, `!`): declare the directory itself | `AGENTDOC_MIGRATION_PATH` |
|
|
115
|
+
| the field on a Resource whose type is not `logical-database` | `AGENTDOC_MIGRATION_PATH` |
|
|
116
|
+
| the same path declared by two Resources | `AGENTDOC_MIGRATION_PATH` |
|
|
117
|
+
|
|
118
|
+
Nested declarations on different Resources are allowed; the longest prefix
|
|
119
|
+
wins.
|
|
120
|
+
|
|
80
121
|
### Why no further entities
|
|
81
122
|
|
|
82
123
|
A `Team`, `Milestone` or `Tag` entity was considered and rejected: none of them
|
|
@@ -142,6 +183,7 @@ least one provenance id and an evidence class.
|
|
|
142
183
|
```
|
|
143
184
|
schemaVersion, source{commit, dirty, inputHash}, compiler{name, version, adapters}
|
|
144
185
|
entities[] ref, kind, name, entity, source, sourcePaths[], derived?
|
|
186
|
+
(derived.migrationDirs[]: path, via[], provenanceIds[])
|
|
145
187
|
relations[] type, sourceRef, targetRef, evidenceClass, attributes, provenanceIds[]
|
|
146
188
|
interfaces.events[] pattern, producers, consumers, contract, resolution
|
|
147
189
|
externalDependencies[] componentRef, name, role, mechanism, evidenceClass
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
with: { node-version: '24' }
|
|
22
22
|
- name: Install the agentdoc CLI
|
|
23
23
|
# Unpinned tracks main. For CI reproducibility pin a tag or commit:
|
|
24
|
-
# github:lbframe/agent-doc-system#v1.
|
|
24
|
+
# github:lbframe/agent-doc-system#v1.4.0
|
|
25
25
|
# A compiler upgrade intentionally invalidates committed graphs.
|
|
26
26
|
run: npm install -g github:lbframe/agent-doc-system
|
|
27
27
|
- name: Validate and compile
|
package/package.json
CHANGED
|
@@ -16,7 +16,15 @@
|
|
|
16
16
|
"type": { "enum": ["logical-database", "logical-dataset", "event-bus", "queue", "cache", "object-store", "search-index", "secret-store", "other"] },
|
|
17
17
|
"placementRationale": { "$ref": "common.json#/$defs/rationale" },
|
|
18
18
|
"system": { "$ref": "common.json#/$defs/name" },
|
|
19
|
-
"context": { "$ref": "common.json#/$defs/context" }
|
|
19
|
+
"context": { "$ref": "common.json#/$defs/context" },
|
|
20
|
+
"migrationPaths": {
|
|
21
|
+
"description": "Repository directories (or files) holding this logical database's schema migrations, matched as whole-segment path prefixes. Authoritative for change impact: a change under one of them is a change to this Resource, whichever component owns the file. Never a binding: which components use the database is still derived. Only a logical-database may declare them.",
|
|
22
|
+
"type": "array",
|
|
23
|
+
"minItems": 1,
|
|
24
|
+
"maxItems": 32,
|
|
25
|
+
"uniqueItems": true,
|
|
26
|
+
"items": { "$ref": "common.json#/$defs/repoPath" }
|
|
27
|
+
}
|
|
20
28
|
},
|
|
21
29
|
"oneOf": [
|
|
22
30
|
{ "required": ["system"] },
|
package/templates/ci.yml
CHANGED
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
with: { node-version: '24' }
|
|
18
18
|
- name: Install the agentdoc CLI
|
|
19
19
|
# Unpinned tracks main. For CI reproducibility pin a tag or commit:
|
|
20
|
-
# github:lbframe/agent-doc-system#v1.
|
|
20
|
+
# github:lbframe/agent-doc-system#v1.4.0
|
|
21
21
|
# A compiler upgrade intentionally invalidates committed graphs.
|
|
22
22
|
run: npm install -g github:lbframe/agent-doc-system
|
|
23
23
|
- name: Validate and compile
|