agent-doc-system 0.0.0-stage → 1.0.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/INSTALL_FOR_AGENTS.md +60 -0
- package/LICENSE +21 -0
- package/README.md +151 -2
- package/adapters/cache.mjs +27 -0
- package/adapters/contract-consumers.mjs +56 -0
- package/adapters/database.mjs +116 -0
- package/adapters/deploy-config.mjs +34 -0
- package/adapters/dockerfile.mjs +26 -0
- package/adapters/events.mjs +145 -0
- package/adapters/generic.mjs +86 -0
- package/adapters/github-actions.mjs +106 -0
- package/adapters/go.mjs +91 -0
- package/adapters/graphql.mjs +28 -0
- package/adapters/index.mjs +55 -0
- package/adapters/node.mjs +118 -0
- package/adapters/object-store.mjs +64 -0
- package/adapters/oidc.mjs +56 -0
- package/adapters/openapi.mjs +50 -0
- package/adapters/pnpm.mjs +42 -0
- package/adapters/protobuf.mjs +35 -0
- package/adapters/registry.mjs +27 -0
- package/adapters/resource-client.mjs +61 -0
- package/adapters/turborepo.mjs +135 -0
- package/adapters/typescript.mjs +85 -0
- package/adapters/wrangler.mjs +206 -0
- package/bin/agentdoc.mjs +478 -0
- package/bin/usage.txt +26 -0
- package/core/acceptances.mjs +69 -0
- package/core/audit.mjs +407 -0
- package/core/authority/classes.mjs +39 -0
- package/core/authority/engine.mjs +243 -0
- package/core/authority/facts.mjs +41 -0
- package/core/codes.mjs +119 -0
- package/core/compile.mjs +881 -0
- package/core/contracts.mjs +121 -0
- package/core/derive.mjs +102 -0
- package/core/descriptors.mjs +486 -0
- package/core/determinism.mjs +95 -0
- package/core/discover.mjs +147 -0
- package/core/facts.mjs +53 -0
- package/core/fsx.mjs +342 -0
- package/core/graph.mjs +260 -0
- package/core/impact.mjs +175 -0
- package/core/indexes.mjs +165 -0
- package/core/jsonschema.mjs +298 -0
- package/core/observations.mjs +129 -0
- package/core/provenance.mjs +83 -0
- package/core/query.mjs +384 -0
- package/core/relations.mjs +286 -0
- package/core/scaffold.mjs +322 -0
- package/core/secrets.mjs +92 -0
- package/core/sourcescan.mjs +88 -0
- package/core/toml.mjs +163 -0
- package/core/yaml.mjs +475 -0
- package/docs/ADAPTERS.md +172 -0
- package/docs/AUTHORITY.md +260 -0
- package/docs/CI.md +136 -0
- package/docs/COMPARISON-KODA.md +164 -0
- package/docs/EVALS.md +166 -0
- package/docs/MIGRATION.md +193 -0
- package/docs/PROVENANCE.md +133 -0
- package/docs/QUERY.md +180 -0
- package/docs/SPEC.md +339 -0
- package/docs/VERIFICATION.md +28 -0
- package/docs/ci/generic-ci.sh +28 -0
- package/docs/ci/github-actions.yml +35 -0
- package/evals/harness.mjs +426 -0
- package/evals/scenarios/fixture-a-01-change-api.json +9 -0
- package/evals/scenarios/fixture-a-02-change-db.json +9 -0
- package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
- package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
- package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
- package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
- package/evals/scenarios/fixture-b-04-journey.json +9 -0
- package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
- package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
- package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
- package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
- package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
- package/evals/scenarios/koda-02-change-db-schema.json +20 -0
- package/evals/scenarios/koda-03-change-auth.json +21 -0
- package/evals/scenarios/koda-04-add-event-producer.json +19 -0
- package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
- package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
- package/evals/scenarios/koda-07-change-shared-package.json +16 -0
- package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
- package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
- package/evals/thresholds.json +11 -0
- package/package.json +46 -4
- package/schemas/api.schema.json +84 -0
- package/schemas/common.schema.json +107 -0
- package/schemas/component.schema.json +40 -0
- package/schemas/config.schema.json +685 -0
- package/schemas/domain.schema.json +14 -0
- package/schemas/graph.schema.json +1084 -0
- package/schemas/journeys.schema.json +47 -0
- package/schemas/observation.schema.json +143 -0
- package/schemas/resource.schema.json +28 -0
- package/schemas/system.schema.json +26 -0
- package/skills/agent-doc-system/SKILL.md +113 -0
- package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
- package/skills/agent-doc-system/references/cli.md +89 -0
- package/skills/agent-doc-system/references/create-migrate.md +101 -0
- package/skills/agent-doc-system/references/maintenance.md +52 -0
- package/templates/ADR.md +26 -0
- package/templates/ARCHITECTURE.md +19 -0
- package/templates/CONSTRAINTS.md +17 -0
- package/templates/PRODUCT.md +17 -0
- package/templates/api.yaml +10 -0
- package/templates/ci.yml +31 -0
- package/templates/component.yaml +14 -0
- package/templates/domain.yaml +6 -0
- package/templates/journey.yaml +7 -0
- package/templates/observation.yaml +19 -0
- package/templates/resource.yaml +8 -0
- package/templates/system.yaml +10 -0
- package/templates/warning-acceptance.yaml +14 -0
package/bin/agentdoc.mjs
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agentdoc — portable documentation & software-catalog system.
|
|
3
|
+
//
|
|
4
|
+
// The command list lives in bin/usage.txt and is printed from there, so this
|
|
5
|
+
// header, `--help` and the shipped file cannot drift into three different
|
|
6
|
+
// claims about what the tool does.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { AgentDocError, CODES } from "../core/codes.mjs";
|
|
11
|
+
import { compile, sha256Hex } from "../core/compile.mjs";
|
|
12
|
+
import { Repo } from "../core/fsx.mjs";
|
|
13
|
+
import { assertFresh, readGraph, COMPILER_VERSION, GRAPH_SCHEMA_VERSION, COMPILER_NAME } from "../core/graph.mjs";
|
|
14
|
+
import { queryContext, renderContext } from "../core/query.mjs";
|
|
15
|
+
import { impact } from "../core/impact.mjs";
|
|
16
|
+
import { audit, scaffoldProposal } from "../core/audit.mjs";
|
|
17
|
+
import { observationFreshness, stalenessDiagnostics } from "../core/observations.mjs";
|
|
18
|
+
import { assertNoSecretsInText } from "../core/secrets.mjs";
|
|
19
|
+
import { installTemplates } from "../core/scaffold.mjs";
|
|
20
|
+
import { loadSchemaBundle, loadConfig } from "../core/descriptors.mjs";
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
|
|
23
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const PACKAGE_ROOT = path.join(HERE, "..");
|
|
25
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version;
|
|
26
|
+
const MIN_NODE_MAJOR = 22;
|
|
27
|
+
// Commands are run from wherever the agent happens to be. Walk up until the
|
|
28
|
+
// configuration is found, so `agentdoc query src/foo.ts` works from a
|
|
29
|
+
// subdirectory instead of failing with a misleading missing-file error.
|
|
30
|
+
function findRoot(start) {
|
|
31
|
+
let dir = start;
|
|
32
|
+
for (let i = 0; i < 24; i++) {
|
|
33
|
+
if (fs.existsSync(path.join(dir, "agentdoc", "agentdoc.config.yaml"))) return dir;
|
|
34
|
+
const up = path.dirname(dir);
|
|
35
|
+
if (up === dir) break;
|
|
36
|
+
dir = up;
|
|
37
|
+
}
|
|
38
|
+
return start;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ROOT = findRoot(process.cwd());
|
|
42
|
+
const argv = process.argv.slice(2);
|
|
43
|
+
const cmd = argv[0];
|
|
44
|
+
// Flags that consume the next token as a value. Anything else is boolean; a
|
|
45
|
+
// `--flag value` pair must not leak the value into `positional`
|
|
46
|
+
// (`agentdoc query --budget relations=10 svc/api` must query svc/api).
|
|
47
|
+
const VALUE_FLAGS = new Set(["budget", "diff", "from"]);
|
|
48
|
+
const flags = new Set();
|
|
49
|
+
const positional = [];
|
|
50
|
+
for (let i = 1; i < argv.length; i++) {
|
|
51
|
+
const a = argv[i];
|
|
52
|
+
if (a.startsWith("--")) {
|
|
53
|
+
const name = a.replace(/^--/, "").split("=")[0];
|
|
54
|
+
if (a.includes("=") && !VALUE_FLAGS.has(name)) {
|
|
55
|
+
console.error("agentdoc: --" + name + " does not take a value");
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
flags.add(name);
|
|
59
|
+
if (!a.includes("=") && VALUE_FLAGS.has(name) && argv[i + 1] && !argv[i + 1].startsWith("--")) i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
positional.push(a);
|
|
63
|
+
}
|
|
64
|
+
const flag = (name, dflt = null) => {
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
if (argv[i] === "--" + name) {
|
|
67
|
+
const next = argv[i + 1];
|
|
68
|
+
return next && !next.startsWith("--") ? next : true;
|
|
69
|
+
}
|
|
70
|
+
if (argv[i].startsWith("--" + name + "=")) return argv[i].slice(name.length + 3);
|
|
71
|
+
}
|
|
72
|
+
return dflt;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function fail(errors, stats) {
|
|
76
|
+
for (const e of errors) console.error("ERROR " + (e.format ? e.format() : e.code + ": " + e.message));
|
|
77
|
+
if (stats) console.error("agentdoc: " + JSON.stringify(stats));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function usage(exitCode) {
|
|
82
|
+
const text = fs.readFileSync(path.join(HERE, "usage.txt"), "utf8");
|
|
83
|
+
(exitCode === 0 ? process.stdout : process.stderr).write(text);
|
|
84
|
+
process.exit(exitCode);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function printDiagnostics(res) {
|
|
88
|
+
for (const d of res.diagnostics || []) {
|
|
89
|
+
const prefix = d.severity === "error" ? "ERROR " : d.acceptance ? "WARN ACCEPTED " : "WARN ";
|
|
90
|
+
console.log(prefix + d.code + ": " + d.message);
|
|
91
|
+
if (d.acceptance) console.log(" " + d.acceptance.classification + ": " + d.acceptance.reason);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function gateObservationFreshness(res) {
|
|
96
|
+
// Time-dependent gates run before artifact gates. A committed graph can be
|
|
97
|
+
// stale for many reasons; when the cause is an observation that has aged out
|
|
98
|
+
// of its promise, saying "rebuild the graph" sends the reader down the wrong
|
|
99
|
+
// path entirely.
|
|
100
|
+
const abs = path.join(ROOT, res.graphPath);
|
|
101
|
+
if (!fs.existsSync(abs)) return null;
|
|
102
|
+
const { graph } = readGraph(res.repo, res.graphPath);
|
|
103
|
+
const fresh = observationFreshness(graph.observations || [], new Date());
|
|
104
|
+
const stale = stalenessDiagnostics(fresh);
|
|
105
|
+
if (!stale.length) return fresh;
|
|
106
|
+
if (!flags.has("allow-stale-observations")) {
|
|
107
|
+
for (const d of stale) console.error("ERROR " + d.code + ": " + d.message);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
// Staleness was explicitly allowed through. This is where a rule's
|
|
111
|
+
// degradeOnStale is applied — at gate time, never at compile time, so a
|
|
112
|
+
// compile stays a pure function of the checkout. `conflict` keeps the
|
|
113
|
+
// election and warns; `unresolved` refuses to stand on a stale observation.
|
|
114
|
+
// Match the elected assertion to the stale observation *set* by capturedAt
|
|
115
|
+
// + environment — capturedAt alone can collide across sets.
|
|
116
|
+
const staleSets = new Set(fresh.filter((f) => f.stale).map((f) => f.capturedAt + "\u0000" + f.environment));
|
|
117
|
+
const assertionById = new Map((graph.assertions || []).map((a) => [a.id, a]));
|
|
118
|
+
const rules = new Map((((res.cfg || {}).authority || {}).rules || []).map((r) => [r.id, r]));
|
|
119
|
+
const degraded = [];
|
|
120
|
+
for (const c of graph.conflicts || []) {
|
|
121
|
+
if (c.status !== "resolved" || !c.election || !c.election.electedAssertionId) continue;
|
|
122
|
+
const rule = rules.get(c.election.ruleId);
|
|
123
|
+
if (!rule || !rule.degradeOnStale) continue;
|
|
124
|
+
const elected = assertionById.get(c.election.electedAssertionId);
|
|
125
|
+
const obs = elected && elected.observed;
|
|
126
|
+
if (!obs || !obs.at || !staleSets.has(obs.at + "\u0000" + obs.environment)) continue;
|
|
127
|
+
if (rule.degradeOnStale === "unresolved") {
|
|
128
|
+
degraded.push(
|
|
129
|
+
c.subject + " " + c.key + " — the elected observation (" + obs.at + ") is past its maxAgeDays " +
|
|
130
|
+
"and authority rule " + rule.id + " requires the election to degrade to unresolved"
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
console.error(
|
|
134
|
+
"WARN " + CODES.ELECTION_STALE + ": " + c.subject + " " + c.key +
|
|
135
|
+
" — the elected observation (" + obs.at + ") is past its maxAgeDays; authority rule " +
|
|
136
|
+
rule.id + " keeps the election"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (degraded.length) {
|
|
141
|
+
for (const m of degraded) console.error("ERROR " + CODES.CONFLICT_UNRESOLVED + ": " + m);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
return fresh;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function requireFresh(res, fresh) {
|
|
148
|
+
const abs = path.join(ROOT, res.graphPath);
|
|
149
|
+
if (!fs.existsSync(abs)) {
|
|
150
|
+
fail([new AgentDocError(CODES.GRAPH_MISSING, "no compiled graph at " + res.graphPath + " — run `agentdoc compile`")]);
|
|
151
|
+
}
|
|
152
|
+
const { graph, text } = readGraph(res.repo, res.graphPath);
|
|
153
|
+
try {
|
|
154
|
+
assertFresh(graph, {
|
|
155
|
+
inputHash: res.stats.inputHash,
|
|
156
|
+
dirty: res.stats.dirty,
|
|
157
|
+
compilerVersion: COMPILER_VERSION,
|
|
158
|
+
schemaVersion: GRAPH_SCHEMA_VERSION,
|
|
159
|
+
});
|
|
160
|
+
} catch (e) {
|
|
161
|
+
fail([e]);
|
|
162
|
+
}
|
|
163
|
+
if (graph.source.dirty && (flags.has("require-clean") || flag("require-clean"))) {
|
|
164
|
+
fail([new AgentDocError(
|
|
165
|
+
CODES.DIRTY_SOURCE,
|
|
166
|
+
"catalog inputs differ from HEAD (" + res.stats.commit + ") — CI/publish evidence requires a clean source; commit the change and rebuild"
|
|
167
|
+
)], res.stats);
|
|
168
|
+
}
|
|
169
|
+
// `fresh` is normally precomputed by the caller — gateObservationFreshness
|
|
170
|
+
// also evaluates election degradation, so calling it twice would print the
|
|
171
|
+
// same staleness warning twice. The fallback keeps a forgotten call-site safe.
|
|
172
|
+
return { graph, text, fresh: fresh === undefined ? (gateObservationFreshness(res) || []) : fresh };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function out(obj, asJson) {
|
|
176
|
+
const text = asJson ? JSON.stringify(obj, null, 2) + "\n" : null;
|
|
177
|
+
if (text) assertNoSecretsInText(text, "output");
|
|
178
|
+
process.stdout.write(text || obj + "\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
183
|
+
console.log("agentdoc " + VERSION);
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help" || flags.has("help") || argv.includes("-h")) {
|
|
187
|
+
usage(0);
|
|
188
|
+
}
|
|
189
|
+
if (cmd === "validate") {
|
|
190
|
+
const res = compile(ROOT);
|
|
191
|
+
printDiagnostics(res);
|
|
192
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
193
|
+
console.log("agentdoc validate: PASS " + JSON.stringify(res.stats));
|
|
194
|
+
} else if (cmd === "compile") {
|
|
195
|
+
const res = compile(ROOT);
|
|
196
|
+
printDiagnostics(res);
|
|
197
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
198
|
+
if ((flags.has("require-clean") || flag("require-clean")) && res.stats.dirty) {
|
|
199
|
+
fail([new AgentDocError(CODES.DIRTY_SOURCE, "catalog inputs differ from HEAD — commit the change and rebuild")], res.stats);
|
|
200
|
+
}
|
|
201
|
+
fs.mkdirSync(path.dirname(path.join(ROOT, res.graphPath)), { recursive: true });
|
|
202
|
+
const tmp = path.join(ROOT, res.graphPath) + ".tmp-" + process.pid;
|
|
203
|
+
fs.writeFileSync(tmp, res.serialized);
|
|
204
|
+
fs.renameSync(tmp, path.join(ROOT, res.graphPath));
|
|
205
|
+
console.log("agentdoc compile: PASS " + JSON.stringify(res.stats));
|
|
206
|
+
console.log("graph: " + res.graphPath);
|
|
207
|
+
} else if (cmd === "check") {
|
|
208
|
+
const res = compile(ROOT);
|
|
209
|
+
printDiagnostics(res);
|
|
210
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
211
|
+
const preFresh = gateObservationFreshness(res);
|
|
212
|
+
const { graph, text, fresh } = requireFresh(res, preFresh);
|
|
213
|
+
// `source.commit` records which checkout produced the graph, and a checkout
|
|
214
|
+
// without VCS — an extracted archive, a vendored dependency — compiles to
|
|
215
|
+
// `0000000`. That is a difference in provenance, not in content, and failing
|
|
216
|
+
// on it would make a committed graph unusable in exactly the situation it
|
|
217
|
+
// is most needed: shipped as a file, with no history. Everything else must
|
|
218
|
+
// still match byte for byte.
|
|
219
|
+
const withoutCommit = (t) => t.replace(/^(\s*)"commit": "[0-9a-f]*",$/m, '$1"commit": "<provenance>",');
|
|
220
|
+
if (withoutCommit(text) !== withoutCommit(res.serialized)) {
|
|
221
|
+
fail([new AgentDocError(
|
|
222
|
+
CODES.STALE_GRAPH,
|
|
223
|
+
"compiled graph differs from a fresh deterministic compile — commit the descriptor or authority change and rebuild"
|
|
224
|
+
)]);
|
|
225
|
+
}
|
|
226
|
+
const gj = JSON.stringify(graph, null, 2) + "\n";
|
|
227
|
+
if (gj !== text) {
|
|
228
|
+
fail([new AgentDocError(CODES.STALE_GRAPH, "compiled graph is not canonically serialized — rebuild the graph")]);
|
|
229
|
+
}
|
|
230
|
+
console.log("agentdoc check: PASS " + JSON.stringify({ ...res.stats, observationFreshness: fresh.length }));
|
|
231
|
+
} else if (cmd === "query") {
|
|
232
|
+
const needle = positional[0];
|
|
233
|
+
if (!needle) { console.error("usage: agentdoc query <repo-path|entity-ref> [--json] [--md] [--budget relations=10,verification=4]"); process.exit(2); }
|
|
234
|
+
const res = compile(ROOT);
|
|
235
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
236
|
+
// Time-dependent gates first, for the same reason as `check`: a stale
|
|
237
|
+
// observation is a different problem from a stale graph and sends the reader
|
|
238
|
+
// somewhere else entirely.
|
|
239
|
+
const preFresh = gateObservationFreshness(res);
|
|
240
|
+
const { graph } = requireFresh(res, preFresh);
|
|
241
|
+
// `--budget` exists because the default budgets are generous enough that a
|
|
242
|
+
// focused query never truncates, which makes the truncation contract
|
|
243
|
+
// untestable and unreachable from the CLI. An agent working in a tight
|
|
244
|
+
// context needs to be able to say so.
|
|
245
|
+
let budget = {};
|
|
246
|
+
const raw = flag("budget");
|
|
247
|
+
if (raw === true || raw === "") {
|
|
248
|
+
console.error("usage: --budget section=limit,... (limits are positive integers)");
|
|
249
|
+
process.exit(2);
|
|
250
|
+
}
|
|
251
|
+
if (raw) {
|
|
252
|
+
for (const pair of raw.split(",")) {
|
|
253
|
+
const [k, v] = pair.split("=");
|
|
254
|
+
if (!k || !/^\d+$/.test(String(v))) {
|
|
255
|
+
console.error("usage: --budget section=limit,... (limits are positive integers)");
|
|
256
|
+
process.exit(2);
|
|
257
|
+
}
|
|
258
|
+
budget[k.trim()] = Number(v);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const ctx = queryContext(graph, needle, { budget });
|
|
262
|
+
if (flags.has("md") || flag("md")) out(renderContext(ctx));
|
|
263
|
+
else if (flags.has("json")) out(ctx, true);
|
|
264
|
+
else out(renderContext(ctx));
|
|
265
|
+
} else if (cmd === "impact") {
|
|
266
|
+
// Impact answers a question about the working tree, so it compiles in
|
|
267
|
+
// memory rather than reading the committed graph. Refusing a stale graph
|
|
268
|
+
// would be right for `query` (which feeds a prompt) and wrong here: the
|
|
269
|
+
// whole point of impact is to run *before* you rebuild.
|
|
270
|
+
const res = compile(ROOT);
|
|
271
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
272
|
+
const graph = res.graph;
|
|
273
|
+
const fromRef = flag("diff") || flag("from");
|
|
274
|
+
let paths = positional;
|
|
275
|
+
if (fromRef && fromRef !== true) {
|
|
276
|
+
if (!res.repo.hasGit()) {
|
|
277
|
+
console.error("agentdoc impact --diff requires a git repository; this checkout has no VCS");
|
|
278
|
+
process.exit(2);
|
|
279
|
+
}
|
|
280
|
+
paths = res.repo.diffPaths(fromRef);
|
|
281
|
+
if (paths === null) {
|
|
282
|
+
console.error("cannot resolve diff ref '" + fromRef + "' — is it a valid commit, branch or range?");
|
|
283
|
+
process.exit(2);
|
|
284
|
+
}
|
|
285
|
+
if (!paths.length) {
|
|
286
|
+
console.error("no changed files between " + fromRef + " and HEAD");
|
|
287
|
+
process.exit(2);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!paths.length) {
|
|
291
|
+
console.error("usage: agentdoc impact <path...> | agentdoc impact --diff <git-ref>");
|
|
292
|
+
process.exit(2);
|
|
293
|
+
}
|
|
294
|
+
out(impact(graph, paths), true);
|
|
295
|
+
} else if (cmd === "audit") {
|
|
296
|
+
const res = compile(ROOT);
|
|
297
|
+
if (!res.graph) {
|
|
298
|
+
// An audit must work on a repository that cannot compile yet; report the
|
|
299
|
+
// compilation failures as part of the audit rather than aborting.
|
|
300
|
+
const report = {
|
|
301
|
+
compileErrors: res.errors.map((e) => ({ code: e.code, message: e.message, path: e.path || null, ref: e.ref || null })),
|
|
302
|
+
stats: res.stats,
|
|
303
|
+
};
|
|
304
|
+
out(report, true);
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
const report = audit(res.repo, res.cfg, res);
|
|
308
|
+
report.scaffold = scaffoldProposal(res.repo, res.cfg, report);
|
|
309
|
+
out(report, true);
|
|
310
|
+
} else if (cmd === "scaffold") {
|
|
311
|
+
const res = compile(ROOT);
|
|
312
|
+
const write = flags.has("write") || flag("write");
|
|
313
|
+
// Scaffolding is how a repository reaches a compiling state: requiring the
|
|
314
|
+
// compile to succeed first deadlocks CREATE on every repository whose units
|
|
315
|
+
// are discovered by adapters (go.work, pnpm-workspace, supplementalRoots)
|
|
316
|
+
// rather than already carrying descriptors. When a usable configuration
|
|
317
|
+
// exists, write what can be proven and report what still blocks the compile.
|
|
318
|
+
if (!res.cfg) {
|
|
319
|
+
out({
|
|
320
|
+
scaffolded: [],
|
|
321
|
+
blocked: res.errors.map((e) => ({ code: e.code, message: e.message })),
|
|
322
|
+
note: "the repository cannot compile yet; install the configuration first with `agentdoc init`",
|
|
323
|
+
}, true);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
const report = res.graph ? audit(res.repo, res.cfg, res) : null;
|
|
327
|
+
const proposal = report ? scaffoldProposal(res.repo, res.cfg, report) : null;
|
|
328
|
+
const result = installTemplates(res.repo, res.cfg, {
|
|
329
|
+
write: Boolean(write),
|
|
330
|
+
eligible: res.discovery && res.discovery.eligible,
|
|
331
|
+
});
|
|
332
|
+
out({
|
|
333
|
+
...result,
|
|
334
|
+
...(proposal ? { proposal } : {}),
|
|
335
|
+
...(res.errors.length
|
|
336
|
+
? { blocked: res.errors.map((e) => ({ code: e.code, message: e.message })) }
|
|
337
|
+
: {}),
|
|
338
|
+
}, true);
|
|
339
|
+
} else if (cmd === "init") {
|
|
340
|
+
const result = installTemplates(null, null, { write: true, root: ROOT, force: flags.has("force") });
|
|
341
|
+
out(result, true);
|
|
342
|
+
} else if (cmd === "eval") {
|
|
343
|
+
const which = positional[0] || "routing";
|
|
344
|
+
if (which !== "routing") { console.error("usage: agentdoc eval routing [--json]"); process.exit(2); }
|
|
345
|
+
// The harness ships, but it is loaded lazily so a trimmed or damaged
|
|
346
|
+
// installation cannot make every other command fail on a module the user
|
|
347
|
+
// never asked for.
|
|
348
|
+
let runRoutingEval;
|
|
349
|
+
try {
|
|
350
|
+
({ runRoutingEval } = await import("../evals/harness.mjs"));
|
|
351
|
+
} catch (e) {
|
|
352
|
+
if (e && e.code === "ERR_MODULE_NOT_FOUND") {
|
|
353
|
+
console.error("agentdoc eval: the evaluation harness is missing from this installation — reinstall the agentdoc CLI, or run it from a source checkout");
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
throw e;
|
|
357
|
+
}
|
|
358
|
+
const report = runRoutingEval(ROOT, { json: flags.has("json") });
|
|
359
|
+
if (flags.has("json")) { out(report, true); process.exit(report.verdict === "FAIL" ? 1 : 0); }
|
|
360
|
+
else {
|
|
361
|
+
console.log(JSON.stringify(report.metrics, null, 2));
|
|
362
|
+
console.log("thresholds: " + JSON.stringify(report.thresholds));
|
|
363
|
+
console.log("verdict: " + report.verdict + " (" + report.failures.join("; ") + ")");
|
|
364
|
+
}
|
|
365
|
+
if (report.verdict === "FAIL") process.exit(1);
|
|
366
|
+
} else if (cmd === "pin-evidence") {
|
|
367
|
+
// Reviewed overrides pin their evidence by content hash, and those digests
|
|
368
|
+
// are re-verified on every compile. That is only workable if an author can
|
|
369
|
+
// obtain the current digest without computing SHA-256 by hand — otherwise
|
|
370
|
+
// the discipline quietly becomes "delete the override", which is worse
|
|
371
|
+
// than not requiring it.
|
|
372
|
+
// Read the configuration directly rather than through `compile`: a stale
|
|
373
|
+
// digest is precisely the condition this command exists to report, and it
|
|
374
|
+
// must be reportable when `compile` refuses to run.
|
|
375
|
+
const repo = new Repo(ROOT);
|
|
376
|
+
const cfg = loadConfig(repo, loadSchemaBundle().bundle);
|
|
377
|
+
const results = [];
|
|
378
|
+
for (const o of cfg.reviewedOverrides || []) {
|
|
379
|
+
for (const ev of o.evidence || []) {
|
|
380
|
+
const p = path.join(ROOT, ev.path);
|
|
381
|
+
if (!fs.existsSync(p)) {
|
|
382
|
+
results.push({ subject: o.subject, path: ev.path, current: null, pinned: ev.sha256, status: "MISSING" });
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const current = sha256Hex(fs.readFileSync(p));
|
|
386
|
+
results.push({
|
|
387
|
+
subject: o.subject, path: ev.path, current,
|
|
388
|
+
pinned: ev.sha256,
|
|
389
|
+
status: current === ev.sha256 ? "CURRENT" : "STALE",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (flags.has("json")) { out({ evidence: results }, true); }
|
|
394
|
+
else {
|
|
395
|
+
for (const r of results) {
|
|
396
|
+
console.log(r.status.padEnd(8) + " " + r.path);
|
|
397
|
+
if (r.status === "STALE") {
|
|
398
|
+
console.log(" pinned: " + r.pinned);
|
|
399
|
+
console.log(" current: " + r.current + " <- update the digest after reviewing the change");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
console.log(results.length + " evidence entries across " + (cfg.reviewedOverrides || []).length + " reviewed overrides");
|
|
403
|
+
}
|
|
404
|
+
if (results.some((r) => r.status !== "CURRENT")) process.exit(1);
|
|
405
|
+
} else if (cmd === "doctor") {
|
|
406
|
+
// Two scopes, kept explicit: `doctor` is a machine check — is this
|
|
407
|
+
// installation of the CLI itself healthy — and never fails because a
|
|
408
|
+
// project happens not to be configured yet. `doctor --project` adds the
|
|
409
|
+
// project checks and fails on those too.
|
|
410
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
411
|
+
const machine = {
|
|
412
|
+
agentdoc: VERSION,
|
|
413
|
+
node: process.version,
|
|
414
|
+
nodeSupported: major >= MIN_NODE_MAJOR,
|
|
415
|
+
nodeRequired: ">=" + MIN_NODE_MAJOR,
|
|
416
|
+
compiler: COMPILER_NAME + "@" + COMPILER_VERSION,
|
|
417
|
+
graphSchema: GRAPH_SCHEMA_VERSION,
|
|
418
|
+
installRoot: PACKAGE_ROOT,
|
|
419
|
+
schemaBundle: null,
|
|
420
|
+
gitAvailable: false,
|
|
421
|
+
externalRuntimeDependencies: [],
|
|
422
|
+
};
|
|
423
|
+
try {
|
|
424
|
+
const { versionInputs } = loadSchemaBundle();
|
|
425
|
+
machine.schemaBundle = { ok: true, schemas: versionInputs.length };
|
|
426
|
+
} catch (e) {
|
|
427
|
+
machine.schemaBundle = { ok: false, error: (e.code ? e.code + ": " : "") + e.message };
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
execFileSync("git", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
431
|
+
machine.gitAvailable = true;
|
|
432
|
+
} catch {
|
|
433
|
+
machine.gitAvailable = false;
|
|
434
|
+
}
|
|
435
|
+
const machineOk = machine.nodeSupported && machine.schemaBundle.ok;
|
|
436
|
+
|
|
437
|
+
const report = { scope: "machine", ok: machineOk, machine };
|
|
438
|
+
if (flags.has("project")) {
|
|
439
|
+
const configFound = fs.existsSync(path.join(ROOT, "agentdoc", "agentdoc.config.yaml"));
|
|
440
|
+
const project = {
|
|
441
|
+
root: ROOT,
|
|
442
|
+
configurationFound: configFound,
|
|
443
|
+
configPath: "agentdoc/agentdoc.config.yaml",
|
|
444
|
+
writable: null,
|
|
445
|
+
graphPresent: false,
|
|
446
|
+
};
|
|
447
|
+
try {
|
|
448
|
+
fs.accessSync(ROOT, fs.constants.W_OK);
|
|
449
|
+
project.writable = true;
|
|
450
|
+
} catch {
|
|
451
|
+
project.writable = false;
|
|
452
|
+
}
|
|
453
|
+
if (configFound) {
|
|
454
|
+
try {
|
|
455
|
+
const cfg = loadConfig(new Repo(ROOT), loadSchemaBundle().bundle);
|
|
456
|
+
project.graphPath = cfg.output?.graph || ".agentdoc/graph.json";
|
|
457
|
+
project.graphPresent = fs.existsSync(path.join(ROOT, project.graphPath));
|
|
458
|
+
} catch (e) {
|
|
459
|
+
project.configError = (e.code ? e.code + ": " : "") + e.message;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
report.scope = "machine+project";
|
|
463
|
+
report.project = project;
|
|
464
|
+
report.ok = machineOk && configFound && project.writable === true && !project.configError;
|
|
465
|
+
}
|
|
466
|
+
out(report, true);
|
|
467
|
+
if (!report.ok) process.exit(1);
|
|
468
|
+
} else {
|
|
469
|
+
usage(2);
|
|
470
|
+
}
|
|
471
|
+
} catch (e) {
|
|
472
|
+
if (e instanceof AgentDocError) {
|
|
473
|
+
console.error("ERROR " + e.format());
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
console.error("agentdoc internal error: " + (e && e.stack ? e.stack : String(e)));
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
package/bin/usage.txt
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
agentdoc — portable documentation & software-catalog system
|
|
2
|
+
|
|
3
|
+
agentdoc --version print the installed version
|
|
4
|
+
agentdoc --help show this help (also: agentdoc <command> --help)
|
|
5
|
+
|
|
6
|
+
agentdoc init [--force] write a configuration and documentation skeleton
|
|
7
|
+
agentdoc audit inventory the repository, classify every fact
|
|
8
|
+
agentdoc scaffold [--write] generate a CREATE-mode skeleton from the audit
|
|
9
|
+
agentdoc validate sources + schemas + semantic checks
|
|
10
|
+
agentdoc compile [--require-clean] deterministic graph build (atomic write)
|
|
11
|
+
agentdoc check [--require-clean] [--allow-stale-observations]
|
|
12
|
+
freshness, determinism and gate check
|
|
13
|
+
agentdoc query <path|ref> [--md] [--json] [--budget k=n]
|
|
14
|
+
route minimal context for a task
|
|
15
|
+
agentdoc impact <paths...> what a change forces you to keep consistent
|
|
16
|
+
agentdoc impact --diff <git-ref> the same, for a commit range
|
|
17
|
+
agentdoc eval routing [--json] agent-routing evaluation against registered
|
|
18
|
+
scenarios (SKIP when the repo has none)
|
|
19
|
+
agentdoc pin-evidence [--json] verify digests pinned by reviewed overrides
|
|
20
|
+
agentdoc doctor machine self-check: is this CLI install healthy
|
|
21
|
+
agentdoc doctor --project the same, plus the current project's catalog health
|
|
22
|
+
|
|
23
|
+
Commands that operate on a catalog run from anywhere inside the repository;
|
|
24
|
+
the configuration is found by walking up to agentdoc/agentdoc.config.yaml.
|
|
25
|
+
|
|
26
|
+
Exit codes: 0 ok, 1 failure, 2 usage.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Explicit warning dispositions.
|
|
2
|
+
//
|
|
3
|
+
// An accepted warning is not an ignored warning. It carries a typed identity, a
|
|
4
|
+
// classification, a reason, a review condition, the exact observed refs it was
|
|
5
|
+
// written against, and content hashes of the evidence. If the evidence changes,
|
|
6
|
+
// the acceptance stops matching and compilation fails: a review is forced, not
|
|
7
|
+
// inherited. Acceptance can never downgrade an error.
|
|
8
|
+
import { AgentDocError, CODES, ACCEPTABLE_WARNING_CODES } from "./codes.mjs";
|
|
9
|
+
import { assertRepoPath, sha256Hex } from "./fsx.mjs";
|
|
10
|
+
import { scanForSecrets } from "./secrets.mjs";
|
|
11
|
+
import { CONFIG_PATH } from "./descriptors.mjs";
|
|
12
|
+
|
|
13
|
+
export function applyWarningAcceptances(repo, acceptances, diagnostics) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
for (const acceptance of acceptances) {
|
|
17
|
+
try {
|
|
18
|
+
const { code, subject, observedRefs, evidence } = acceptance;
|
|
19
|
+
if (!ACCEPTABLE_WARNING_CODES.includes(code)) {
|
|
20
|
+
throw new AgentDocError(
|
|
21
|
+
CODES.WARNING_ACCEPTANCE,
|
|
22
|
+
"warning code " + code + " is not acceptable; only " + ACCEPTABLE_WARNING_CODES.join(", ") + " may be accepted",
|
|
23
|
+
{ path: CONFIG_PATH }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const key = code + "|" + subject;
|
|
27
|
+
if (seen.has(key)) throw new AgentDocError(CODES.WARNING_ACCEPTANCE, "duplicate warning acceptance for " + key, { path: CONFIG_PATH });
|
|
28
|
+
seen.add(key);
|
|
29
|
+
scanForSecrets(acceptance, CONFIG_PATH);
|
|
30
|
+
|
|
31
|
+
const matches = diagnostics.filter((d) => d.code === code && d.subject === subject);
|
|
32
|
+
if (matches.length !== 1 || matches[0].severity !== "warning") {
|
|
33
|
+
throw new AgentDocError(
|
|
34
|
+
CODES.WARNING_ACCEPTANCE,
|
|
35
|
+
"warning acceptance must match exactly one current warning: " + key + " — remove it or review the underlying fact",
|
|
36
|
+
{ path: CONFIG_PATH }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const diagnostic = matches[0];
|
|
40
|
+
if (JSON.stringify([...observedRefs].sort()) !== JSON.stringify([...(diagnostic.refs || [])].sort())) {
|
|
41
|
+
throw new AgentDocError(
|
|
42
|
+
CODES.WARNING_ACCEPTANCE,
|
|
43
|
+
"observed refs changed for " + key + " — re-review the current consumers or component",
|
|
44
|
+
{ path: CONFIG_PATH }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const paths = new Set();
|
|
48
|
+
for (const item of evidence) {
|
|
49
|
+
assertRepoPath(item.path, CONFIG_PATH);
|
|
50
|
+
if (paths.has(item.path)) {
|
|
51
|
+
throw new AgentDocError(CODES.WARNING_ACCEPTANCE, "duplicate acceptance evidence path: " + item.path, { path: CONFIG_PATH });
|
|
52
|
+
}
|
|
53
|
+
paths.add(item.path);
|
|
54
|
+
if (sha256Hex(repo.readBytes(item.path)) !== item.sha256) {
|
|
55
|
+
throw new AgentDocError(
|
|
56
|
+
CODES.WARNING_ACCEPTANCE,
|
|
57
|
+
"warning acceptance evidence changed: " + item.path + " — review the disposition before updating its digest",
|
|
58
|
+
{ path: CONFIG_PATH }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
diagnostic.acceptance = acceptance;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
if (!(e instanceof AgentDocError)) throw e;
|
|
65
|
+
errors.push(e);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return errors;
|
|
69
|
+
}
|