agent-doc-system 1.3.0 → 1.5.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 +77 -1
- package/INSTALL_FOR_AGENTS.md +1 -1
- package/README.md +5 -1
- package/adapters/database.mjs +153 -5
- package/bin/agentdoc.mjs +16 -1
- package/bin/commands.mjs +15 -0
- package/bin/usage.txt +4 -0
- package/core/cache/compiled.mjs +34 -0
- package/core/cache/parses.mjs +33 -0
- package/core/codes.mjs +13 -0
- package/core/compile.mjs +30 -2
- package/core/context/index.mjs +173 -0
- package/core/context/symbols.mjs +81 -0
- package/core/descriptors.mjs +49 -0
- package/core/fsx.mjs +50 -3
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +74 -11
- package/core/query.mjs +58 -7
- package/core/session/results.mjs +138 -0
- package/core/snapshot.mjs +123 -0
- package/docs/ADAPTERS.md +48 -4
- package/docs/CI.md +3 -1
- package/docs/CONTEXT.md +136 -0
- package/docs/EVALS.md +124 -0
- package/docs/IMPLEMENTATION-PLAN.md +310 -0
- package/docs/QUERY.md +48 -9
- package/docs/SPEC.md +42 -0
- package/docs/ci/github-actions.yml +1 -1
- package/evals/agent/codex-driver.mjs +32 -0
- package/evals/agent/optimize.mjs +48 -0
- package/evals/agent/results/smoke-gpt-6.1-sol.json +81 -0
- package/evals/agent/run.mjs +163 -0
- package/evals/agent/tasks/corpus.mjs +53 -0
- package/evals/agent/tasks/pins.json +32 -0
- package/package.json +9 -5
- package/schemas/context.schema.json +222 -0
- package/schemas/resource.schema.json +9 -1
- package/skills/agent-doc-system/SKILL.md +6 -0
- package/templates/ci.yml +1 -1
package/core/query.mjs
CHANGED
|
@@ -130,7 +130,7 @@ function contractsOf(graph) {
|
|
|
130
130
|
|
|
131
131
|
export function queryContext(graph, needle, opts = {}) {
|
|
132
132
|
const budget = { ...DEFAULT_BUDGET, ...(opts.budget || {}) };
|
|
133
|
-
const idx = new ContextIndex(graph);
|
|
133
|
+
const idx = opts.index || new ContextIndex(graph);
|
|
134
134
|
const entity = idx.resolve(needle);
|
|
135
135
|
if (!entity) {
|
|
136
136
|
throw new AgentDocError(
|
|
@@ -379,8 +379,21 @@ export function renderContext(ctx, opts = {}) {
|
|
|
379
379
|
if (ctx.placement.domains.length) L.push("- domain: " + ctx.placement.domains.join(", "));
|
|
380
380
|
if (ctx.apis.provided.length) L.push("- provides: " + ctx.apis.provided.map((a) => a.api).join(", "));
|
|
381
381
|
if (ctx.apis.consumed.length) L.push("- consumes: " + ctx.apis.consumed.map((a) => a.api + " (via " + a.provider + ")").join(", "));
|
|
382
|
+
if (ctx.apis.provider) L.push("- API provider: " + ctx.apis.provider);
|
|
383
|
+
if (ctx.apis.consumers.length) L.push("- API consumers: " + ctx.apis.consumers.join(", "));
|
|
384
|
+
if (ctx.apis.contract) L.push("- API contract: " + JSON.stringify(ctx.apis.contract));
|
|
382
385
|
if (ctx.resources.used.length) L.push("- uses resource: " + ctx.resources.used.join(", "));
|
|
383
|
-
if (ctx.
|
|
386
|
+
if (ctx.resources.usedBy.length) L.push("- resource users: " + ctx.resources.usedBy.join(", "));
|
|
387
|
+
if (ctx.relations.items.length) {
|
|
388
|
+
L.push("- relations:");
|
|
389
|
+
for (const r of ctx.relations.items) {
|
|
390
|
+
L.push(" - " + r.direction + " " + r.type + " " + r.other + " [" + r.evidenceClass + "]" +
|
|
391
|
+
(r.attributes ? " " + JSON.stringify(r.attributes) : ""));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (ctx.events.items.length) L.push("- events: " + ctx.events.items.map((x) => x.pattern + " [" + x.resolution + "]" +
|
|
395
|
+
" producers=" + JSON.stringify(x.producers) + " consumers=" + JSON.stringify(x.consumers) +
|
|
396
|
+
(x.contract ? " contract=" + JSON.stringify(x.contract) : "")).join(", "));
|
|
384
397
|
if (ctx.externalDependencies.items.length) L.push("- external: " + ctx.externalDependencies.items.map((x) => x.name + " (" + x.role + ")").join(", "));
|
|
385
398
|
if (ctx.capabilities.items.length) L.push("- capabilities: " + ctx.capabilities.items.map((x) => x.name).join(", "));
|
|
386
399
|
for (const key of ["constraints", "docs", "runbooks"]) {
|
|
@@ -395,13 +408,36 @@ export function renderContext(ctx, opts = {}) {
|
|
|
395
408
|
}
|
|
396
409
|
if (ctx.verification.items.length) {
|
|
397
410
|
L.push("- verify:");
|
|
398
|
-
for (const v of ctx.verification.items) L.push(" - [" + v.tier + "] " + v.command
|
|
411
|
+
for (const v of ctx.verification.items) L.push(" - [" + v.tier + "] " + v.command + " (" + v.id +
|
|
412
|
+
(v.configPaths?.length ? "; config: " + v.configPaths.join(", ") : "") + ")");
|
|
413
|
+
}
|
|
414
|
+
if (ctx.globalGates.length) {
|
|
415
|
+
L.push("- repository gates (required in addition to scoped checks):");
|
|
416
|
+
for (const v of ctx.globalGates) L.push(" - [" + v.tier + "] " + v.command + " (" + v.id + ")");
|
|
399
417
|
}
|
|
400
418
|
if (ctx.journeys.items.length) L.push("- journeys: " + ctx.journeys.items.map((j) => j.id).join(", "));
|
|
401
|
-
if (ctx.
|
|
402
|
-
|
|
403
|
-
|
|
419
|
+
if (ctx.journeyPeers.length) L.push("- journey peers: " + ctx.journeyPeers.join(", "));
|
|
420
|
+
if (ctx.contracts.items.length) {
|
|
421
|
+
L.push("- contracts:");
|
|
422
|
+
for (const c of ctx.contracts.items) L.push(" - " + JSON.stringify(c));
|
|
423
|
+
}
|
|
424
|
+
if (ctx.authority.assertions.items.length) {
|
|
425
|
+
L.push("- assertions:");
|
|
426
|
+
for (const a of ctx.authority.assertions.items) {
|
|
427
|
+
L.push(" - " + a.key + " [" + a.evidenceClass + "/" + a.confidence + "/" + a.status + "]: " +
|
|
428
|
+
JSON.stringify(a.value) + (a.semantics ? " (" + a.semantics + ")" : ""));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const [settled, title] of [
|
|
432
|
+
[false, "CONFLICTS (do not assume one is true)"],
|
|
433
|
+
[true, "RESOLVED CONFLICTS (use the recorded election; re-check its review condition)"],
|
|
434
|
+
]) {
|
|
435
|
+
const conflicts = ctx.authority.conflicts.items.filter((c) => (c.status !== "unresolved") === settled);
|
|
436
|
+
if (!conflicts.length) continue;
|
|
437
|
+
L.push("- " + title + ":");
|
|
438
|
+
for (const c of conflicts) {
|
|
404
439
|
L.push(" - " + c.key + " [" + c.kind + "/" + c.status + "] " + (c.detail || ""));
|
|
440
|
+
if (c.election) L.push(" - election: " + JSON.stringify(c.election));
|
|
405
441
|
for (const a of c.assertions) L.push(" - " + a.evidenceClass + "/" + a.confidence + ": " + JSON.stringify(a.value));
|
|
406
442
|
}
|
|
407
443
|
}
|
|
@@ -421,9 +457,24 @@ export function renderContext(ctx, opts = {}) {
|
|
|
421
457
|
const obs = ctx.authority.observations;
|
|
422
458
|
if (obs.length) {
|
|
423
459
|
L.push("- RUNTIME OBSERVATIONS:");
|
|
424
|
-
for (const o of obs)
|
|
460
|
+
for (const o of obs) {
|
|
461
|
+
L.push(" - " + o.environment + " via " + o.sourceSystem + " at " + o.capturedAt + " (evidence " + o.evidenceBundle + ")");
|
|
462
|
+
for (const f of o.facts) L.push(" - " + f.key + ": " + JSON.stringify(f.value) +
|
|
463
|
+
(f.semantics ? " (" + f.semantics + ")" : ""));
|
|
464
|
+
}
|
|
425
465
|
}
|
|
426
466
|
const neighbours = ctx.neighbours.filter((n) => n.ref !== e.ref);
|
|
427
467
|
if (neighbours.length) L.push("- neighbours: " + neighbours.map((n) => n.ref).join(", "));
|
|
468
|
+
if (ctx.provenance.items.length) {
|
|
469
|
+
L.push("- evidence:");
|
|
470
|
+
for (const p of ctx.provenance.items) L.push(" - " + p.id + " " + p.path + " [" + p.class + "] " + p.contentHash);
|
|
471
|
+
}
|
|
472
|
+
if (ctx.truncated.length) {
|
|
473
|
+
L.push("- INCOMPLETE CONTEXT (request a larger --budget before treating these sections as complete):");
|
|
474
|
+
for (const name of ctx.truncated) {
|
|
475
|
+
const section = name.split(".").reduce((value, key) => value[key], ctx);
|
|
476
|
+
L.push(" - " + name + " [truncated: " + section.items.length + " of " + section.total + "]");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
428
479
|
return L.join("\n");
|
|
429
480
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { CODES } from "../codes.mjs";
|
|
2
|
+
// Imported runner evidence is scoped to one repository snapshot and execution
|
|
3
|
+
// context. This module never executes a check or promotes a recommendation into
|
|
4
|
+
// a success. Any changed graph or evidence invalidates the imported result.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { compileSnapshot } from "../snapshot.mjs";
|
|
9
|
+
import { canonicalPath } from "../paths.mjs";
|
|
10
|
+
import { observationFreshness } from "../observations.mjs";
|
|
11
|
+
import { assertNoSecretsInText } from "../secrets.mjs";
|
|
12
|
+
|
|
13
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
14
|
+
const graphHash = (graph) => digest(JSON.stringify(graph));
|
|
15
|
+
function reject(message) { throw Object.assign(new Error(message), { code: CODES.SESSION_RESULT }); }
|
|
16
|
+
function object(value, keys, name) {
|
|
17
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) reject(name + " must be an object");
|
|
18
|
+
for (const key of Object.keys(value)) if (!keys.includes(key)) reject("unknown " + name + " field: " + key);
|
|
19
|
+
}
|
|
20
|
+
function nonempty(value, name) {
|
|
21
|
+
if (typeof value !== "string" || !value.trim()) reject(name + " must be a nonempty string");
|
|
22
|
+
}
|
|
23
|
+
function sessionFile(root, session) {
|
|
24
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(session || "")) reject("invalid session reference");
|
|
25
|
+
return path.join(path.resolve(root), ".agentdoc", "sessions", "check-results", session + ".json");
|
|
26
|
+
}
|
|
27
|
+
function repositoryPath(root, value, directory = false) {
|
|
28
|
+
const rel = canonicalPath(value);
|
|
29
|
+
if (rel === null || typeof value !== "string" || !value) reject("execution paths must stay inside the repository");
|
|
30
|
+
let real;
|
|
31
|
+
try {
|
|
32
|
+
real = fs.realpathSync(path.join(root, rel));
|
|
33
|
+
const stat = fs.statSync(real);
|
|
34
|
+
if (directory ? !stat.isDirectory() : !stat.isFile()) reject("wrong execution path kind: " + rel);
|
|
35
|
+
} catch { reject("execution path unavailable: " + rel); }
|
|
36
|
+
const base = fs.realpathSync(root);
|
|
37
|
+
if (real !== base && !real.startsWith(base + path.sep)) reject("execution path escapes repository: " + rel);
|
|
38
|
+
return { rel, real };
|
|
39
|
+
}
|
|
40
|
+
function current(root, snapshot) {
|
|
41
|
+
if (!snapshot || !/^[a-f0-9]{64}$/.test(snapshot.graphHash || "")) reject("snapshot.graphHash must identify the full graph");
|
|
42
|
+
const res = compileSnapshot(root);
|
|
43
|
+
if (res.errors.length) reject("cannot validate results against a failing compilation");
|
|
44
|
+
if (snapshot.graphHash !== graphHash(res.graph)) reject("snapshot changed; imported checks must be run again");
|
|
45
|
+
if (observationFreshness(res.graph.observations).some((o) => o.stale)) reject("runtime observation expired; refresh context before importing checks");
|
|
46
|
+
return res.graph;
|
|
47
|
+
}
|
|
48
|
+
function execution(result, root, graph) {
|
|
49
|
+
object(result, ["checkId", "command", "cwd", "status", "exitCode", "runner", "evidence"], "result");
|
|
50
|
+
nonempty(result.checkId, "checkId");
|
|
51
|
+
nonempty(result.command, "command");
|
|
52
|
+
const check = [...graph.verification, ...graph.gates].find((v) => v.id === result.checkId);
|
|
53
|
+
if (!check || check.command !== result.command) reject("result must name the exact registered check and command");
|
|
54
|
+
const cwd = repositoryPath(root, result.cwd, true).rel;
|
|
55
|
+
if (!["succeeded", "failed"].includes(result.status) || !Number.isSafeInteger(result.exitCode) || result.exitCode < 0 ||
|
|
56
|
+
(result.status === "succeeded") !== (result.exitCode === 0)) reject("execution status must agree with its exit code");
|
|
57
|
+
object(result.runner, ["name", "version"], "runner");
|
|
58
|
+
nonempty(result.runner.name, "runner.name"); nonempty(result.runner.version, "runner.version");
|
|
59
|
+
object(result.evidence, ["path", "sha256"], "evidence");
|
|
60
|
+
if (!/^[a-f0-9]{64}$/.test(result.evidence.sha256 || "")) reject("evidence requires a SHA-256 digest");
|
|
61
|
+
const evidencePath = repositoryPath(root, result.evidence.path);
|
|
62
|
+
const bytes = fs.readFileSync(evidencePath.real);
|
|
63
|
+
if (digest(bytes) !== result.evidence.sha256) reject("execution evidence changed");
|
|
64
|
+
let report;
|
|
65
|
+
try { report = JSON.parse(bytes.toString("utf8")); } catch { reject("execution evidence must be a JSON runner report"); }
|
|
66
|
+
if (!report || typeof report !== "object" || report.snapshot?.graphHash !== graphHash(graph) ||
|
|
67
|
+
report.checkId !== result.checkId || report.command !== result.command || report.cwd !== cwd ||
|
|
68
|
+
report.status !== result.status || report.exitCode !== result.exitCode ||
|
|
69
|
+
report.runner?.name !== result.runner.name || report.runner?.version !== result.runner.version) reject("runner report does not corroborate the imported result");
|
|
70
|
+
return { ...result, cwd, runner: { ...result.runner }, evidence: { path: evidencePath.rel, sha256: result.evidence.sha256 } };
|
|
71
|
+
}
|
|
72
|
+
function read(root, session) {
|
|
73
|
+
const file = sessionFile(root, session);
|
|
74
|
+
if (!fs.existsSync(file)) return null;
|
|
75
|
+
try {
|
|
76
|
+
const stored = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
77
|
+
if (stored.digest !== digest(JSON.stringify(stored.data)) || stored.data.repository !== fs.realpathSync(root) ||
|
|
78
|
+
stored.data.session !== session || stored.data.schemaVersion !== "agentdoc.dev/check-results/v1" || !Array.isArray(stored.data.results)) reject("invalid stored results");
|
|
79
|
+
return stored.data;
|
|
80
|
+
} catch { reject("stored session results are invalid; forget the session and rerun its checks"); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function importCheckResults(root, request) {
|
|
84
|
+
object(request, ["session", "snapshot", "results"], "import");
|
|
85
|
+
const file = sessionFile(root, request.session);
|
|
86
|
+
if (!Array.isArray(request.results) || !request.results.length) reject("results must be a nonempty execution list");
|
|
87
|
+
const graph = current(root, request.snapshot);
|
|
88
|
+
const incoming = request.results.map((r) => execution(r, root, graph));
|
|
89
|
+
const previous = read(root, request.session);
|
|
90
|
+
const combined = new Map();
|
|
91
|
+
if (previous?.graphHash === request.snapshot.graphHash) {
|
|
92
|
+
for (const r of previous.results) {
|
|
93
|
+
try { const valid = execution(r, root, graph); combined.set(JSON.stringify([valid.checkId, valid.command, valid.cwd, valid.runner]), valid); }
|
|
94
|
+
catch { /* An old evidence bundle may have been replaced by a rerun. */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const r of incoming) combined.set(JSON.stringify([r.checkId, r.command, r.cwd, r.runner]), r);
|
|
98
|
+
const data = { schemaVersion: "agentdoc.dev/check-results/v1", repository: fs.realpathSync(root), session: request.session,
|
|
99
|
+
graphHash: request.snapshot.graphHash, results: [...combined.values()] };
|
|
100
|
+
const serialized = JSON.stringify({ data, digest: digest(JSON.stringify(data)) });
|
|
101
|
+
assertNoSecretsInText(serialized, "session check results");
|
|
102
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
103
|
+
const temp = file + "." + randomUUID() + ".tmp";
|
|
104
|
+
try { fs.writeFileSync(temp, serialized, { mode: 0o600 }); fs.renameSync(temp, file); }
|
|
105
|
+
finally { fs.rmSync(temp, { force: true }); }
|
|
106
|
+
return { session: request.session, graphHash: data.graphHash, results: data.results };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function validResultsForSnapshot(root, session, snapshot) {
|
|
110
|
+
const data = read(root, session);
|
|
111
|
+
if (!data || data.graphHash !== snapshot?.graphHash) return [];
|
|
112
|
+
let graph;
|
|
113
|
+
try { graph = current(root, snapshot); } catch { return []; }
|
|
114
|
+
const valid = [];
|
|
115
|
+
for (const result of data.results) {
|
|
116
|
+
try { valid.push(execution(result, root, graph)); } catch { /* Expired or changed evidence is not a valid result. */ }
|
|
117
|
+
}
|
|
118
|
+
return valid;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function forgetSession(root, session) {
|
|
122
|
+
const file = sessionFile(root, session);
|
|
123
|
+
const existed = fs.existsSync(file);
|
|
124
|
+
fs.rmSync(file, { force: true });
|
|
125
|
+
return existed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function clearSessions(root) {
|
|
129
|
+
const dir = path.dirname(sessionFile(root, "session"));
|
|
130
|
+
if (!fs.existsSync(dir)) return 0;
|
|
131
|
+
let removed = 0;
|
|
132
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
133
|
+
if (entry.isFile() && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}\.json$/.test(entry.name)) {
|
|
134
|
+
fs.unlinkSync(path.join(dir, entry.name)); removed++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return removed;
|
|
138
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Conservative persistent compilation reuse. Every visible file is hashed;
|
|
2
|
+
// this deliberately over-invalidates rather than inventing extractor-level
|
|
3
|
+
// dependencies. Full compile remains the oracle and is always available.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Repo, sha256Hex } from "./fsx.mjs";
|
|
8
|
+
import { compile, CompileResult } from "./compile.mjs";
|
|
9
|
+
import { loadConfig, loadSchemaBundle } from "./descriptors.mjs";
|
|
10
|
+
import { AgentDocError, CODES } from "./codes.mjs";
|
|
11
|
+
import { readCompiledCache, writeCompiledCache } from "./cache/compiled.mjs";
|
|
12
|
+
import { ParseCache } from "./cache/parses.mjs";
|
|
13
|
+
|
|
14
|
+
const implementationRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
|
|
16
|
+
function implementationHash() {
|
|
17
|
+
const records = [];
|
|
18
|
+
for (const dir of ["core", "adapters", "schemas"]) {
|
|
19
|
+
const walk = (base) => {
|
|
20
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
21
|
+
const file = path.join(base, entry.name);
|
|
22
|
+
if (entry.isDirectory()) walk(file);
|
|
23
|
+
else if (entry.isFile()) records.push([path.relative(implementationRoot, file), sha256Hex(fs.readFileSync(file))]);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
walk(path.join(implementationRoot, dir));
|
|
27
|
+
}
|
|
28
|
+
return sha256Hex(JSON.stringify(records));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A running process keeps its imported implementation even if files on disk
|
|
32
|
+
// are replaced by an upgrade. Never label old code's result with the new hash.
|
|
33
|
+
const implementationVersion = implementationHash();
|
|
34
|
+
const sharedParses = new ParseCache();
|
|
35
|
+
|
|
36
|
+
function capture(root, opts) {
|
|
37
|
+
const schema = loadSchemaBundle();
|
|
38
|
+
const cfg = loadConfig(new Repo(root, { noGit: true }), schema.bundle);
|
|
39
|
+
const repo = new Repo(root, { noGit: opts.noGit, generatedDirs: cfg.discovery.generatedDirs || opts.generatedDirs, snapshotReads: true });
|
|
40
|
+
const files = repo.walk("").filter((file) => file !== cfg.output.graph);
|
|
41
|
+
const content = files.map((file) => [file, sha256Hex(repo.readBytes(file))]);
|
|
42
|
+
// Empty directories and path kinds are captured too: exists/isDir/listDir
|
|
43
|
+
// probes may affect resolution even when there is no readable file yet.
|
|
44
|
+
let symlinks = false;
|
|
45
|
+
const listings = [];
|
|
46
|
+
const visit = (dir) => {
|
|
47
|
+
if (repo.isIgnored(dir)) return;
|
|
48
|
+
const entries = fs.readdirSync(repo.abs(dir), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
const visible = [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const rel = dir === "." ? entry.name : dir + "/" + entry.name;
|
|
52
|
+
if (repo.generatedDirs.has(entry.name) || repo.isIgnored(rel)) continue;
|
|
53
|
+
visible.push([entry.name, entry.isSymbolicLink() ? "link" : entry.isDirectory() ? "dir" : "file"]);
|
|
54
|
+
if (entry.isSymbolicLink()) symlinks = true;
|
|
55
|
+
if (entry.isDirectory()) visit(rel);
|
|
56
|
+
}
|
|
57
|
+
listings.push([dir, visible]);
|
|
58
|
+
};
|
|
59
|
+
visit(".");
|
|
60
|
+
const state = {
|
|
61
|
+
version: 1, implementation: implementationVersion, schemas: schema.versionInputs, noGit: Boolean(opts.noGit),
|
|
62
|
+
generatedDirs: [...repo.generatedDirs].sort(), commit: repo.headCommit(),
|
|
63
|
+
status: repo.statusRecords()?.filter((r) => r.xy !== "!!" && r.path !== cfg.output.graph) || null, content, listings,
|
|
64
|
+
};
|
|
65
|
+
return { key: sha256Hex(JSON.stringify(state)), repo, cfg, files: new Set(files), digests: new Map(content), symlinks };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read-only result consumer API. Extraction-only fields (ctx/discovery) are
|
|
69
|
+
* available on a miss only; callers needing those must use compile().
|
|
70
|
+
* Time-dependent observation gates must run after every call, including hits.
|
|
71
|
+
*/
|
|
72
|
+
export function compileSnapshot(root, opts = {}) {
|
|
73
|
+
const started = performance.now();
|
|
74
|
+
const file = path.join(path.resolve(root), ".agentdoc", "cache", "compiled-v1.json");
|
|
75
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
76
|
+
let before;
|
|
77
|
+
try { before = capture(root, opts); }
|
|
78
|
+
catch { return compile(root, { ...opts, snapshotReads: false }); }
|
|
79
|
+
// Explicit authored paths can follow a symlink even though discovery does
|
|
80
|
+
// not. Without a target manifest, reuse would miss changes outside the walk.
|
|
81
|
+
if (before.symlinks) return compile(root, { ...opts, snapshotReads: false });
|
|
82
|
+
const cached = opts.cache === false ? null : readCompiledCache(file, before.key);
|
|
83
|
+
let result;
|
|
84
|
+
if (cached) {
|
|
85
|
+
try {
|
|
86
|
+
const graph = JSON.parse(cached.serialized);
|
|
87
|
+
const invalid = loadSchemaBundle().bundle.validator("agentdoc.dev/schema/graph.schema.json").validate(graph);
|
|
88
|
+
if (!invalid) {
|
|
89
|
+
result = new CompileResult();
|
|
90
|
+
Object.assign(result, { graph, serialized: cached.serialized, stats: cached.stats, diagnostics: graph.diagnostics,
|
|
91
|
+
cfg: before.cfg, repo: before.repo, graphPath: before.cfg.output.graph });
|
|
92
|
+
}
|
|
93
|
+
} catch { /* corrupt cache: compile instead */ }
|
|
94
|
+
}
|
|
95
|
+
const hit = Boolean(result);
|
|
96
|
+
result ||= compile(root, { ...opts, snapshotReads: true, telemetry: true, parseCache: opts.parseCache || sharedParses });
|
|
97
|
+
if (!hit && result.repo) {
|
|
98
|
+
const excluded = [...result.repo.readPaths.keys()].some((p) => !before.files.has(p)) ||
|
|
99
|
+
[...result.repo.probes.keys()].some((p) => p.split("/").some((seg) => before.repo.generatedDirs.has(seg)) || p === before.cfg.output.graph);
|
|
100
|
+
if (excluded) return compile(root, { ...opts, snapshotReads: false });
|
|
101
|
+
}
|
|
102
|
+
let after;
|
|
103
|
+
try { after = capture(root, opts); }
|
|
104
|
+
catch { continue; }
|
|
105
|
+
if (before.key !== after.key) continue;
|
|
106
|
+
if (!hit && result.repo) {
|
|
107
|
+
if ([...result.repo.readPaths].some(([p, digest]) => after.digests.get(p) !== digest)) continue;
|
|
108
|
+
if ([...result.repo.probeRecords.values()].some((probe) => after.repo[probe.kind](probe.path) !== probe.value)) continue;
|
|
109
|
+
if ([...result.repo.listings].some(([p, names]) => JSON.stringify(after.repo.listDir(p)) !== JSON.stringify(names))) continue;
|
|
110
|
+
if ([...result.repo.walkInputs].some(([dir, entries]) => JSON.stringify(after.repo.directoryState(dir)) !== JSON.stringify(entries))) continue;
|
|
111
|
+
}
|
|
112
|
+
if (!hit && !result.errors.length && opts.cache !== false) writeCompiledCache(file, {
|
|
113
|
+
key: before.key, serialized: result.serialized, stats: result.stats,
|
|
114
|
+
});
|
|
115
|
+
// Consumers read evidence from the captured bytes, including on cache hits.
|
|
116
|
+
result.repo = before.repo;
|
|
117
|
+
result.cache = { hit, key: before.key, mode: "whole-compile", persisted: opts.cache !== false };
|
|
118
|
+
result.telemetry = { ...(result.telemetry || {}), durationMs: performance.now() - started,
|
|
119
|
+
captureReads: before.repo.metrics.physicalReads + after.repo.metrics.physicalReads };
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
throw new AgentDocError(CODES.SNAPSHOT_UNSTABLE, "repository changed during context capture; retry once edits have settled");
|
|
123
|
+
}
|
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.5.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/CONTEXT.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Task context protocol
|
|
2
|
+
|
|
3
|
+
`prepare`, `expand` and `refresh` accept one JSON request on stdin (or `--request file`) and emit one compact JSON packet plus a newline. They operate on the current working tree and do not require a committed graph. They never execute repository verification commands.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
printf '%s\n' '{"task":"Change the engine API","paths":["crates/engine"],"intent":"change-contract","budget":{"tokens":4096,"encoding":"o200k_base"}}' | agentdoc prepare
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Intents are `implement` (default), `change-contract`, `debug-runtime` and `review`. The first implementation conservatively retains the same obligation closure across intents. The packet contains typed, versioned records, evidence references, selected checks, analysis limitations and stale runtime observation IDs. It does not infer that unsupported or dynamic dependencies are absent.
|
|
10
|
+
|
|
11
|
+
## Budget and continuation
|
|
12
|
+
|
|
13
|
+
The exact serialized output, including its final newline, metadata and continuation, is counted using pinned `js-tiktoken@1.0.21`. Select `o200k_base` or `cl100k_base` explicitly to match the encoding used by your model; AgentDoc does not guess model tokenization. The supported budget is 256–1,000,000 tokens. A budget too small for the envelope or next indivisible record fails with `AGENTDOC_CONTEXT_BUDGET` and must be increased.
|
|
14
|
+
|
|
15
|
+
`delivery.partial` and `delivery.criticalOmitted` report incomplete delivery. Critical obligations are selected first, but a partial page must never be interpreted as the complete check or constraint list. Follow `continuation` until it is null:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{"packet":"<64-character packet reference>","offset":8,"tokens":8192}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Pass that object to `agentdoc expand`. Continuations are tied to the original request, snapshot and protocol. A changed graph or runtime freshness state rejects expansion with `AGENTDOC_CONTEXT_STALE`; use `refresh` or `prepare`. Larger budgets allow reading a long constraint document without silently shortening it.
|
|
22
|
+
|
|
23
|
+
## Session refresh
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{"packet":"<previous packet reference>","acknowledged":["<retained record ID>"]}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Send this to `agentdoc refresh`. Acknowledge only records still present in the agent's active context. Without acknowledgements, all records are delivered again. `reset:true` also requests full delivery. Refresh emits new or changed records, explicit retractions and active critical obligations. Critical records are repeated even if acknowledged. The resulting delta has its own packet reference and continuation.
|
|
30
|
+
|
|
31
|
+
The harness must apply retractions, replace changed versions and manage its active context. Merely appending delta messages does not bound the model's history. Following a lost or compacted session, request a reset. A changed snapshot conservatively invalidates prior execution evidence.
|
|
32
|
+
|
|
33
|
+
Packets are stored locally under `.agentdoc/context/`, excluded from compiler inputs. Treat references as local to a repository. The JavaScript API exposes `forget(root, packet)` to remove a retained packet. At most 64 packets are retained per repository; older continuations may require preparing again. The caller can delete packets sooner; packet files are not published with the repository.
|
|
34
|
+
|
|
35
|
+
## Checks and evidence
|
|
36
|
+
|
|
37
|
+
A recommended shell command is not an execution result. In this initial protocol, checks expose `execution.status:"requires-context"` and `cwd:null` when the compiler has not established a working directory. Resolve that context from the check's owner and evidence before executing it. Identical command text for different checks remains distinct.
|
|
38
|
+
|
|
39
|
+
Constraint and document records carry their content and a content fingerprint. Other records carry graph provenance identifiers or paths. Facts retain their authority data; contradictory claims are not merged into one assumed truth. Stale observations are marked explicitly and must not be treated as current production evidence.
|
|
40
|
+
|
|
41
|
+
## Cache and compatibility
|
|
42
|
+
|
|
43
|
+
`cache:false` in a prepare request bypasses persistent compilation reuse. The full compiler remains the correctness reference. Cache reuse is conservative and does not imply extractor-level incremental recompilation. Protocol version `agentdoc.dev/context/v1` is separate from graph version `agentdoc.dev/graph/v1`.
|
|
44
|
+
|
|
45
|
+
Existing `query` and `impact` keep their established freshness policies. See [QUERY.md](QUERY.md) for those commands.
|
|
46
|
+
|
|
47
|
+
## Importing check execution results from JavaScript
|
|
48
|
+
|
|
49
|
+
The result API accepts reports produced by the harness after execution. It does
|
|
50
|
+
not run the command or independently observe its execution. Each report must name
|
|
51
|
+
a registered check and its exact command, an explicit repository-relative working
|
|
52
|
+
directory, a runner name/version, and a status matching its exit code:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"snapshot": { "graphHash": "<graphHash from the prepared packet>" },
|
|
57
|
+
"checkId": "<registered check ID>",
|
|
58
|
+
"command": "<exact registered command>",
|
|
59
|
+
"cwd": "service",
|
|
60
|
+
"status": "succeeded",
|
|
61
|
+
"exitCode": 0,
|
|
62
|
+
"runner": { "name": "<harness name>", "version": "<harness version>" }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use `failed` with a positive integer exit code for a failed execution. A
|
|
67
|
+
recommendation, missing working directory or disagreement between the status and
|
|
68
|
+
exit code is refused. Store the report under `.agentdoc/executions/` so writing
|
|
69
|
+
execution evidence does not itself change the source snapshot. The report's
|
|
70
|
+
`graphHash` must be the snapshot actually used for the run, not a new snapshot
|
|
71
|
+
captured after unrelated edits.
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import fs from 'node:fs';
|
|
75
|
+
import path from 'node:path';
|
|
76
|
+
import { createHash } from 'node:crypto';
|
|
77
|
+
import {
|
|
78
|
+
importCheckResults, validResultsForSnapshot, forgetSession, clearSessions,
|
|
79
|
+
} from 'agent-doc-system/core/session/results.mjs';
|
|
80
|
+
|
|
81
|
+
// root is the repository root; packet is its prepared context packet.
|
|
82
|
+
const reportPath = '.agentdoc/executions/check.json';
|
|
83
|
+
const bytes = fs.readFileSync(path.join(root, reportPath));
|
|
84
|
+
const report = JSON.parse(bytes.toString('utf8'));
|
|
85
|
+
const { checkId, command, cwd, status, exitCode, runner } = report;
|
|
86
|
+
importCheckResults(root, {
|
|
87
|
+
session: 'change-engine',
|
|
88
|
+
snapshot: packet.snapshot,
|
|
89
|
+
results: [{ checkId, command, cwd, status, exitCode, runner, evidence: {
|
|
90
|
+
path: reportPath,
|
|
91
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
92
|
+
} }],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const results = validResultsForSnapshot(root, 'change-engine', packet.snapshot);
|
|
96
|
+
const successes = results.filter(result => result.status === 'succeeded');
|
|
97
|
+
forgetSession(root, 'change-engine'); // Returns whether a result record existed.
|
|
98
|
+
clearSessions(root); // Removes all retained result sessions; returns their count.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Import verifies the current graph, registered command and hash-pinned JSON report.
|
|
102
|
+
Reads verify them again: a changed graph, changed evidence, unavailable execution
|
|
103
|
+
directory or expired runtime observation invalidates prior results. Both valid
|
|
104
|
+
successes and valid failures are returned; the harness must inspect `status`.
|
|
105
|
+
All import validation errors use `AGENTDOC_SESSION_RESULT`.
|
|
106
|
+
|
|
107
|
+
Session names contain 1–64 ASCII letters, digits, underscores or hyphens and start
|
|
108
|
+
with a letter or digit. Result records live under
|
|
109
|
+
`.agentdoc/sessions/check-results/`, independently of context packets. Cleanup
|
|
110
|
+
removes result records only; it preserves context packets and execution reports.
|
|
111
|
+
Use the context API's `forget(root, packet)` separately when dropping a packet.
|
|
112
|
+
|
|
113
|
+
## TypeScript symbol candidates from JavaScript
|
|
114
|
+
|
|
115
|
+
The optional symbol experiment indexes only explicitly requested TypeScript
|
|
116
|
+
files. It returns declarations and reference candidates with source line/column
|
|
117
|
+
locations and a SHA-256 content fingerprint:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
import { indexTypeScriptSymbols } from 'agent-doc-system/core/context/symbols.mjs';
|
|
121
|
+
|
|
122
|
+
const candidates = indexTypeScriptSymbols(root, ['src/api.ts', 'src/consumer.ts']);
|
|
123
|
+
for (const file of candidates.files) {
|
|
124
|
+
// file.path, file.sha256, file.declarations, file.references
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Supported paths end in `.ts`, `.tsx`, `.mts` or `.cts`; paths escaping the
|
|
129
|
+
repository, including through a symlink, are refused. The experiment masks
|
|
130
|
+
comments and quoted text, but does not resolve lexical scopes, imports,
|
|
131
|
+
overloads, dynamic references, regex literals or template interpolation. Its
|
|
132
|
+
output always declares `coverage.partial:true` and
|
|
133
|
+
`coverage.checkSelection:"unchanged"`. Use candidates to navigate source, never
|
|
134
|
+
to infer that an unlisted dependency is absent or to remove a required check.
|
|
135
|
+
The experiment is a JavaScript API and is not enabled by the default context
|
|
136
|
+
commands.
|