agent-doc-system 1.0.0 → 1.1.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/README.md +63 -116
- package/adapters/ansible.mjs +306 -0
- package/adapters/github-actions.mjs +5 -5
- package/adapters/index.mjs +2 -1
- package/bin/agentdoc.mjs +59 -43
- package/bin/commands.mjs +169 -0
- package/bin/usage.txt +5 -3
- package/core/audit.mjs +50 -1
- package/core/authority/engine.mjs +27 -4
- package/core/codes.mjs +7 -0
- package/core/compile.mjs +139 -2
- package/core/descriptors.mjs +5 -2
- package/core/fsx.mjs +10 -4
- package/core/graph.mjs +1 -1
- package/core/normative.mjs +171 -0
- package/core/yaml.mjs +5 -3
- package/docs/ADAPTERS.md +36 -1
- package/docs/AUTHORITY.md +10 -1
- package/docs/CI.md +3 -1
- package/docs/MIGRATION.md +72 -4
- package/docs/assets/readme/agentdoc-hero.webp +0 -0
- package/docs/assets/readme/agentdoc-in-action.webp +0 -0
- package/docs/assets/readme/how-agentdoc-works.webp +0 -0
- package/docs/assets/readme/why-agentdoc.webp +0 -0
- package/docs/ci/github-actions.yml +1 -1
- package/package.json +1 -1
- package/schemas/common.schema.json +40 -0
- package/schemas/component.schema.json +5 -1
- package/schemas/config.schema.json +117 -1
- package/skills/agent-doc-system/SKILL.md +5 -1
- package/skills/agent-doc-system/references/cli.md +12 -4
- package/skills/agent-doc-system/references/create-migrate.md +15 -3
- package/templates/ci.yml +1 -1
package/bin/agentdoc.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
// The command list lives in bin/usage.txt and is printed from there, so this
|
|
5
5
|
// header, `--help` and the shipped file cannot drift into three different
|
|
6
|
-
// claims about what the tool does.
|
|
6
|
+
// claims about what the tool does. The flags each command accepts live in
|
|
7
|
+
// bin/commands.mjs, which drives both parsing and `agentdoc <command> --help`.
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -17,6 +18,7 @@ import { audit, scaffoldProposal } from "../core/audit.mjs";
|
|
|
17
18
|
import { observationFreshness, stalenessDiagnostics } from "../core/observations.mjs";
|
|
18
19
|
import { assertNoSecretsInText } from "../core/secrets.mjs";
|
|
19
20
|
import { installTemplates } from "../core/scaffold.mjs";
|
|
21
|
+
import { COMMANDS, flagsOf, parseArgs, commandHelp, UsageError } from "./commands.mjs";
|
|
20
22
|
import { loadSchemaBundle, loadConfig } from "../core/descriptors.mjs";
|
|
21
23
|
import { execFileSync } from "node:child_process";
|
|
22
24
|
|
|
@@ -41,36 +43,27 @@ function findRoot(start) {
|
|
|
41
43
|
const ROOT = findRoot(process.cwd());
|
|
42
44
|
const argv = process.argv.slice(2);
|
|
43
45
|
const cmd = argv[0];
|
|
44
|
-
// Flags
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
46
|
+
// Flags are declared per command in bin/commands.mjs. An undeclared flag is a
|
|
47
|
+
// usage error (exit 2), so a typo in a gate flag can never pass for the gate.
|
|
48
|
+
let parsed = { flags: new Map(), positional: [] };
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(COMMANDS, cmd)) {
|
|
50
|
+
try {
|
|
51
|
+
parsed = parseArgs(cmd, argv.slice(1));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (!(e instanceof UsageError)) throw e;
|
|
54
|
+
console.error(e.message);
|
|
55
|
+
process.exit(2);
|
|
61
56
|
}
|
|
62
|
-
positional.push(a);
|
|
63
57
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (argv[i].startsWith("--" + name + "=")) return argv[i].slice(name.length + 3);
|
|
58
|
+
const positional = parsed.positional;
|
|
59
|
+
// The only way to read a flag. Reading one the command does not declare is a
|
|
60
|
+
// defect in this file, not user input: it would be a flag `--help` never shows.
|
|
61
|
+
function opt(name) {
|
|
62
|
+
if (!Object.prototype.hasOwnProperty.call(flagsOf(cmd), name)) {
|
|
63
|
+
throw new Error("flag --" + name + " is read by '" + cmd + "' but not declared for it in bin/commands.mjs");
|
|
71
64
|
}
|
|
72
|
-
return
|
|
73
|
-
}
|
|
65
|
+
return parsed.flags.has(name) ? parsed.flags.get(name) : null;
|
|
66
|
+
}
|
|
74
67
|
|
|
75
68
|
function fail(errors, stats) {
|
|
76
69
|
for (const e of errors) console.error("ERROR " + (e.format ? e.format() : e.code + ": " + e.message));
|
|
@@ -103,7 +96,7 @@ function gateObservationFreshness(res) {
|
|
|
103
96
|
const fresh = observationFreshness(graph.observations || [], new Date());
|
|
104
97
|
const stale = stalenessDiagnostics(fresh);
|
|
105
98
|
if (!stale.length) return fresh;
|
|
106
|
-
if (!
|
|
99
|
+
if (!opt("allow-stale-observations")) {
|
|
107
100
|
for (const d of stale) console.error("ERROR " + d.code + ": " + d.message);
|
|
108
101
|
process.exit(1);
|
|
109
102
|
}
|
|
@@ -160,7 +153,7 @@ function requireFresh(res, fresh) {
|
|
|
160
153
|
} catch (e) {
|
|
161
154
|
fail([e]);
|
|
162
155
|
}
|
|
163
|
-
if (graph.source.dirty && (
|
|
156
|
+
if (graph.source.dirty && opt("require-clean")) {
|
|
164
157
|
fail([new AgentDocError(
|
|
165
158
|
CODES.DIRTY_SOURCE,
|
|
166
159
|
"catalog inputs differ from HEAD (" + res.stats.commit + ") — CI/publish evidence requires a clean source; commit the change and rebuild"
|
|
@@ -183,9 +176,18 @@ try {
|
|
|
183
176
|
console.log("agentdoc " + VERSION);
|
|
184
177
|
process.exit(0);
|
|
185
178
|
}
|
|
186
|
-
if (cmd === "--help" || cmd === "-h" || cmd === "help"
|
|
179
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
180
|
+
// `agentdoc help <command>` is the same as `agentdoc <command> --help`.
|
|
181
|
+
if (Object.prototype.hasOwnProperty.call(COMMANDS, argv[1])) {
|
|
182
|
+
process.stdout.write(commandHelp(argv[1]));
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
187
185
|
usage(0);
|
|
188
186
|
}
|
|
187
|
+
if (Object.prototype.hasOwnProperty.call(COMMANDS, cmd) && opt("help")) {
|
|
188
|
+
process.stdout.write(commandHelp(cmd));
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
189
191
|
if (cmd === "validate") {
|
|
190
192
|
const res = compile(ROOT);
|
|
191
193
|
printDiagnostics(res);
|
|
@@ -195,7 +197,7 @@ try {
|
|
|
195
197
|
const res = compile(ROOT);
|
|
196
198
|
printDiagnostics(res);
|
|
197
199
|
if (res.errors.length) fail(res.errors, res.stats);
|
|
198
|
-
if ((
|
|
200
|
+
if (opt("require-clean") && res.stats.dirty) {
|
|
199
201
|
fail([new AgentDocError(CODES.DIRTY_SOURCE, "catalog inputs differ from HEAD — commit the change and rebuild")], res.stats);
|
|
200
202
|
}
|
|
201
203
|
fs.mkdirSync(path.dirname(path.join(ROOT, res.graphPath)), { recursive: true });
|
|
@@ -227,7 +229,21 @@ try {
|
|
|
227
229
|
if (gj !== text) {
|
|
228
230
|
fail([new AgentDocError(CODES.STALE_GRAPH, "compiled graph is not canonically serialized — rebuild the graph")]);
|
|
229
231
|
}
|
|
230
|
-
|
|
232
|
+
// `--strict` is the migration gate: the graph is current *and* nothing is
|
|
233
|
+
// left for a human. Every warning is either accepted with pinned evidence
|
|
234
|
+
// or the gate fails, so a normative section nobody reviewed or a
|
|
235
|
+
// requirement nothing proves cannot ride along in a green check. Opt-in, so
|
|
236
|
+
// plain `check` keeps meaning "the committed graph is fresh".
|
|
237
|
+
if (opt("strict")) {
|
|
238
|
+
const open = (res.diagnostics || []).filter((d) => d.severity === "warning" && !d.acceptance);
|
|
239
|
+
if (open.length) {
|
|
240
|
+
fail(
|
|
241
|
+
open.map((d) => new AgentDocError(d.code, (d.subject && !d.message.includes(d.subject) ? d.subject + ": " : "") + d.message)),
|
|
242
|
+
{ ...res.stats, strict: true }
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
console.log("agentdoc check: PASS " + JSON.stringify({ ...res.stats, observationFreshness: fresh.length, ...(opt("strict") ? { strict: true } : {}) }));
|
|
231
247
|
} else if (cmd === "query") {
|
|
232
248
|
const needle = positional[0];
|
|
233
249
|
if (!needle) { console.error("usage: agentdoc query <repo-path|entity-ref> [--json] [--md] [--budget relations=10,verification=4]"); process.exit(2); }
|
|
@@ -243,7 +259,7 @@ try {
|
|
|
243
259
|
// untestable and unreachable from the CLI. An agent working in a tight
|
|
244
260
|
// context needs to be able to say so.
|
|
245
261
|
let budget = {};
|
|
246
|
-
const raw =
|
|
262
|
+
const raw = opt("budget");
|
|
247
263
|
if (raw === true || raw === "") {
|
|
248
264
|
console.error("usage: --budget section=limit,... (limits are positive integers)");
|
|
249
265
|
process.exit(2);
|
|
@@ -259,8 +275,8 @@ try {
|
|
|
259
275
|
}
|
|
260
276
|
}
|
|
261
277
|
const ctx = queryContext(graph, needle, { budget });
|
|
262
|
-
if (
|
|
263
|
-
else if (
|
|
278
|
+
if (opt("md")) out(renderContext(ctx));
|
|
279
|
+
else if (opt("json")) out(ctx, true);
|
|
264
280
|
else out(renderContext(ctx));
|
|
265
281
|
} else if (cmd === "impact") {
|
|
266
282
|
// Impact answers a question about the working tree, so it compiles in
|
|
@@ -270,7 +286,7 @@ try {
|
|
|
270
286
|
const res = compile(ROOT);
|
|
271
287
|
if (res.errors.length) fail(res.errors, res.stats);
|
|
272
288
|
const graph = res.graph;
|
|
273
|
-
const fromRef =
|
|
289
|
+
const fromRef = opt("diff") || opt("from");
|
|
274
290
|
let paths = positional;
|
|
275
291
|
if (fromRef && fromRef !== true) {
|
|
276
292
|
if (!res.repo.hasGit()) {
|
|
@@ -309,7 +325,7 @@ try {
|
|
|
309
325
|
out(report, true);
|
|
310
326
|
} else if (cmd === "scaffold") {
|
|
311
327
|
const res = compile(ROOT);
|
|
312
|
-
const write =
|
|
328
|
+
const write = opt("write");
|
|
313
329
|
// Scaffolding is how a repository reaches a compiling state: requiring the
|
|
314
330
|
// compile to succeed first deadlocks CREATE on every repository whose units
|
|
315
331
|
// are discovered by adapters (go.work, pnpm-workspace, supplementalRoots)
|
|
@@ -337,7 +353,7 @@ try {
|
|
|
337
353
|
: {}),
|
|
338
354
|
}, true);
|
|
339
355
|
} else if (cmd === "init") {
|
|
340
|
-
const result = installTemplates(null, null, { write: true, root: ROOT, force:
|
|
356
|
+
const result = installTemplates(null, null, { write: true, root: ROOT, force: opt("force") });
|
|
341
357
|
out(result, true);
|
|
342
358
|
} else if (cmd === "eval") {
|
|
343
359
|
const which = positional[0] || "routing";
|
|
@@ -355,8 +371,8 @@ try {
|
|
|
355
371
|
}
|
|
356
372
|
throw e;
|
|
357
373
|
}
|
|
358
|
-
const report = runRoutingEval(ROOT, { json:
|
|
359
|
-
if (
|
|
374
|
+
const report = runRoutingEval(ROOT, { json: opt("json") });
|
|
375
|
+
if (opt("json")) { out(report, true); process.exit(report.verdict === "FAIL" ? 1 : 0); }
|
|
360
376
|
else {
|
|
361
377
|
console.log(JSON.stringify(report.metrics, null, 2));
|
|
362
378
|
console.log("thresholds: " + JSON.stringify(report.thresholds));
|
|
@@ -390,7 +406,7 @@ try {
|
|
|
390
406
|
});
|
|
391
407
|
}
|
|
392
408
|
}
|
|
393
|
-
if (
|
|
409
|
+
if (opt("json")) { out({ evidence: results }, true); }
|
|
394
410
|
else {
|
|
395
411
|
for (const r of results) {
|
|
396
412
|
console.log(r.status.padEnd(8) + " " + r.path);
|
|
@@ -435,7 +451,7 @@ try {
|
|
|
435
451
|
const machineOk = machine.nodeSupported && machine.schemaBundle.ok;
|
|
436
452
|
|
|
437
453
|
const report = { scope: "machine", ok: machineOk, machine };
|
|
438
|
-
if (
|
|
454
|
+
if (opt("project")) {
|
|
439
455
|
const configFound = fs.existsSync(path.join(ROOT, "agentdoc", "agentdoc.config.yaml"));
|
|
440
456
|
const project = {
|
|
441
457
|
root: ROOT,
|
package/bin/commands.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Command and flag declarations.
|
|
2
|
+
//
|
|
3
|
+
// One table drives three things: which flags a command accepts, how they are
|
|
4
|
+
// parsed, and what `agentdoc <command> --help` prints. Parsing and help read
|
|
5
|
+
// the same rows, so they cannot drift. A flag that is not declared for the
|
|
6
|
+
// command it is given to is a usage error, never silently ignored: a
|
|
7
|
+
// misspelled gate flag (`check --strcit`) must not turn into a green,
|
|
8
|
+
// non-strict gate.
|
|
9
|
+
|
|
10
|
+
const JSON_FLAG = { json: { desc: "print the machine-readable report" } };
|
|
11
|
+
// Commands whose output is always JSON accept --json too, as a no-op, so a
|
|
12
|
+
// script that says what it expects is not rejected for being explicit.
|
|
13
|
+
const JSON_ALWAYS = { json: { desc: "accepted for explicitness; the output is always JSON" } };
|
|
14
|
+
|
|
15
|
+
export const COMMANDS = {
|
|
16
|
+
init: {
|
|
17
|
+
synopsis: "agentdoc init [--force]",
|
|
18
|
+
summary: "write a configuration and documentation skeleton",
|
|
19
|
+
flags: { force: { desc: "overwrite files that already exist" }, ...JSON_ALWAYS },
|
|
20
|
+
},
|
|
21
|
+
audit: {
|
|
22
|
+
synopsis: "agentdoc audit",
|
|
23
|
+
summary: "inventory the repository and classify every fact (JSON report)",
|
|
24
|
+
flags: { ...JSON_ALWAYS },
|
|
25
|
+
},
|
|
26
|
+
scaffold: {
|
|
27
|
+
synopsis: "agentdoc scaffold [--write]",
|
|
28
|
+
summary: "generate a CREATE-mode skeleton from the audit (dry run by default)",
|
|
29
|
+
flags: { write: { desc: "write the proposed files instead of listing them" }, ...JSON_ALWAYS },
|
|
30
|
+
},
|
|
31
|
+
validate: {
|
|
32
|
+
synopsis: "agentdoc validate",
|
|
33
|
+
summary: "sources, schemas and semantic checks",
|
|
34
|
+
flags: {},
|
|
35
|
+
},
|
|
36
|
+
compile: {
|
|
37
|
+
synopsis: "agentdoc compile [--require-clean]",
|
|
38
|
+
summary: "deterministic graph build (atomic write)",
|
|
39
|
+
flags: { "require-clean": { desc: "refuse when catalog inputs differ from HEAD" } },
|
|
40
|
+
},
|
|
41
|
+
check: {
|
|
42
|
+
synopsis: "agentdoc check [--strict] [--require-clean] [--allow-stale-observations]",
|
|
43
|
+
summary: "freshness, determinism and gate check",
|
|
44
|
+
flags: {
|
|
45
|
+
strict: { desc: "also fail on every warning not accepted in warningAcceptances (the end-of-MIGRATE gate)" },
|
|
46
|
+
"require-clean": { desc: "refuse a graph built from inputs that differ from HEAD" },
|
|
47
|
+
"allow-stale-observations": { desc: "let observations past maxAgeDays through, applying each rule's degradeOnStale" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
query: {
|
|
51
|
+
synopsis: "agentdoc query <path|ref> [--md] [--json] [--budget section=n,...]",
|
|
52
|
+
summary: "route minimal context for a task",
|
|
53
|
+
flags: {
|
|
54
|
+
md: { desc: "compact markdown context (the default)" },
|
|
55
|
+
...JSON_FLAG,
|
|
56
|
+
budget: { value: "section=n,...", desc: "cap sections, e.g. relations=10,verification=4; truncation is reported" },
|
|
57
|
+
"require-clean": { desc: "refuse a graph built from inputs that differ from HEAD" },
|
|
58
|
+
"allow-stale-observations": { desc: "let observations past maxAgeDays through, applying each rule's degradeOnStale" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
impact: {
|
|
62
|
+
synopsis: "agentdoc impact <paths...> | agentdoc impact --diff <git-ref>",
|
|
63
|
+
summary: "what a change forces you to keep consistent",
|
|
64
|
+
flags: {
|
|
65
|
+
diff: { value: "git-ref", desc: "use the files changed since <git-ref> instead of paths" },
|
|
66
|
+
from: { value: "git-ref", desc: "alias of --diff" },
|
|
67
|
+
...JSON_ALWAYS,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
eval: {
|
|
71
|
+
synopsis: "agentdoc eval routing [--json]",
|
|
72
|
+
summary: "agent-routing evaluation against registered scenarios (SKIP when the repo has none)",
|
|
73
|
+
flags: { ...JSON_FLAG },
|
|
74
|
+
},
|
|
75
|
+
"pin-evidence": {
|
|
76
|
+
synopsis: "agentdoc pin-evidence [--json]",
|
|
77
|
+
summary: "verify digests pinned by reviewed overrides",
|
|
78
|
+
flags: { ...JSON_FLAG },
|
|
79
|
+
},
|
|
80
|
+
doctor: {
|
|
81
|
+
synopsis: "agentdoc doctor [--project]",
|
|
82
|
+
summary: "machine self-check: is this CLI install healthy",
|
|
83
|
+
flags: { project: { desc: "also check the current project's catalog health" }, ...JSON_ALWAYS },
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Accepted by every command.
|
|
88
|
+
const COMMON_FLAGS = { help: { desc: "show this help" } };
|
|
89
|
+
|
|
90
|
+
export function flagsOf(cmd) {
|
|
91
|
+
return { ...(COMMANDS[cmd] ? COMMANDS[cmd].flags : {}), ...COMMON_FLAGS };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class UsageError extends Error {}
|
|
95
|
+
|
|
96
|
+
// Parse argv (without the command) against the command's declaration.
|
|
97
|
+
// Returns { flags: Map(name -> true|string), positional }.
|
|
98
|
+
export function parseArgs(cmd, args) {
|
|
99
|
+
const declared = flagsOf(cmd);
|
|
100
|
+
const flags = new Map();
|
|
101
|
+
const positional = [];
|
|
102
|
+
for (let i = 0; i < args.length; i++) {
|
|
103
|
+
const a = args[i];
|
|
104
|
+
if (a === "-h") { flags.set("help", true); continue; }
|
|
105
|
+
if (!a.startsWith("--")) { positional.push(a); continue; }
|
|
106
|
+
const eq = a.indexOf("=");
|
|
107
|
+
const name = a.slice(2, eq < 0 ? undefined : eq);
|
|
108
|
+
const spec = declared[name];
|
|
109
|
+
if (!spec) throw new UsageError(unknownFlagMessage(cmd, name));
|
|
110
|
+
if (!spec.value) {
|
|
111
|
+
if (eq >= 0) throw new UsageError("agentdoc " + cmd + ": --" + name + " does not take a value");
|
|
112
|
+
flags.set(name, true);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
let v = eq >= 0 ? a.slice(eq + 1) : args[i + 1];
|
|
116
|
+
if (eq < 0) {
|
|
117
|
+
if (v === undefined || v.startsWith("--")) v = "";
|
|
118
|
+
else i++;
|
|
119
|
+
}
|
|
120
|
+
if (!v) throw new UsageError("agentdoc " + cmd + ": --" + name + " requires a value (" + spec.value + ")");
|
|
121
|
+
flags.set(name, v);
|
|
122
|
+
}
|
|
123
|
+
return { flags, positional };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function unknownFlagMessage(cmd, name) {
|
|
127
|
+
const own = Object.keys(flagsOf(cmd));
|
|
128
|
+
const near = own
|
|
129
|
+
.map((f) => [f, distance(f, name)])
|
|
130
|
+
.filter(([f, d]) => d <= Math.max(1, Math.floor(f.length / 3)))
|
|
131
|
+
.sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1))[0];
|
|
132
|
+
const elsewhere = Object.keys(COMMANDS).filter((c) => c !== cmd && COMMANDS[c].flags[name]);
|
|
133
|
+
let hint = "";
|
|
134
|
+
if (near) hint = " (did you mean --" + near[0] + "?)";
|
|
135
|
+
else if (elsewhere.length) hint = " (--" + name + " is accepted by: " + elsewhere.join(", ") + ")";
|
|
136
|
+
return "agentdoc " + cmd + ": unknown flag --" + name + hint +
|
|
137
|
+
"\nrun `agentdoc " + cmd + " --help` for the flags this command accepts";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function distance(a, b) {
|
|
141
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
142
|
+
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
143
|
+
for (let i = 1; i <= a.length; i++) {
|
|
144
|
+
for (let j = 1; j <= b.length; j++) {
|
|
145
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
146
|
+
// Adjacent transposition: `strcit` is one slip away from `strict`.
|
|
147
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return dp[a.length][b.length];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function commandHelp(cmd) {
|
|
154
|
+
const c = COMMANDS[cmd];
|
|
155
|
+
const flags = flagsOf(cmd);
|
|
156
|
+
const rows = Object.entries(flags).map(([name, spec]) => ["--" + name + (spec.value ? " <" + spec.value + ">" : ""), spec.desc]);
|
|
157
|
+
const width = Math.max(...rows.map((r) => r[0].length));
|
|
158
|
+
return [
|
|
159
|
+
"usage: " + c.synopsis,
|
|
160
|
+
"",
|
|
161
|
+
c.summary,
|
|
162
|
+
"",
|
|
163
|
+
"flags:",
|
|
164
|
+
...rows.map(([f, d]) => " " + f.padEnd(width) + " " + d),
|
|
165
|
+
"",
|
|
166
|
+
"Exit codes: 0 ok, 1 failure, 2 usage.",
|
|
167
|
+
"",
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
package/bin/usage.txt
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
agentdoc — portable documentation & software-catalog system
|
|
2
2
|
|
|
3
3
|
agentdoc --version print the installed version
|
|
4
|
-
agentdoc --help show this help
|
|
4
|
+
agentdoc --help show this help
|
|
5
|
+
agentdoc <command> --help every flag one command accepts (also: agentdoc help <command>)
|
|
5
6
|
|
|
6
7
|
agentdoc init [--force] write a configuration and documentation skeleton
|
|
7
8
|
agentdoc audit inventory the repository, classify every fact
|
|
8
9
|
agentdoc scaffold [--write] generate a CREATE-mode skeleton from the audit
|
|
9
10
|
agentdoc validate sources + schemas + semantic checks
|
|
10
11
|
agentdoc compile [--require-clean] deterministic graph build (atomic write)
|
|
11
|
-
agentdoc check [--require-clean] [--allow-stale-observations]
|
|
12
|
+
agentdoc check [--strict] [--require-clean] [--allow-stale-observations]
|
|
12
13
|
freshness, determinism and gate check
|
|
13
14
|
agentdoc query <path|ref> [--md] [--json] [--budget k=n]
|
|
14
15
|
route minimal context for a task
|
|
@@ -23,4 +24,5 @@ agentdoc — portable documentation & software-catalog system
|
|
|
23
24
|
Commands that operate on a catalog run from anywhere inside the repository;
|
|
24
25
|
the configuration is found by walking up to agentdoc/agentdoc.config.yaml.
|
|
25
26
|
|
|
26
|
-
Exit codes: 0 ok, 1 failure, 2 usage.
|
|
27
|
+
Exit codes: 0 ok, 1 failure, 2 usage. An unknown flag is a usage error (2),
|
|
28
|
+
never ignored.
|
package/core/audit.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { classifyContractFile } from "./contracts.mjs";
|
|
19
19
|
import { ContextIndex } from "./query.mjs";
|
|
20
20
|
import { expandGlob, isRepoPath } from "./fsx.mjs";
|
|
21
|
+
import { isNormative, NORMATIVE_SEMANTICS_PREFIX } from "./normative.mjs";
|
|
21
22
|
|
|
22
23
|
const DOC_EXT = /\.(md|mdx|adoc|rst|txt)$/;
|
|
23
24
|
|
|
@@ -28,7 +29,11 @@ export function audit(repo, cfg, res) {
|
|
|
28
29
|
const note = (fact, cls, why) => {
|
|
29
30
|
if (!classes.has(fact)) classes.set(fact, { cls, why: [] });
|
|
30
31
|
const cur = classes.get(fact);
|
|
32
|
+
// Strongest class wins regardless of the order sources are visited in:
|
|
33
|
+
// corroboration outranks a single derivation, and a conflict or an open
|
|
34
|
+
// question outranks both.
|
|
31
35
|
if (cls === "CONFLICT" || cls === "UNRESOLVED") cur.cls = cls;
|
|
36
|
+
else if (cls === "CONFIRMED" && cur.cls === "DERIVED") cur.cls = cls;
|
|
32
37
|
if (why) cur.why.push(why);
|
|
33
38
|
};
|
|
34
39
|
|
|
@@ -53,8 +58,35 @@ export function audit(repo, cfg, res) {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
// 2. facts and conflicts
|
|
61
|
+
const normative = res.normative || { sections: [], uncovered: [], requirements: [] };
|
|
62
|
+
const enabledAdapters = new Set((graph.compiler.adapters || []).map((a) => a.name));
|
|
56
63
|
for (const a of graph.assertions) {
|
|
57
64
|
const f = a.subject + " " + a.key;
|
|
65
|
+
if (isNormative(a)) {
|
|
66
|
+
// A requirement names where it came from, and — when nothing proves it —
|
|
67
|
+
// what could have: a missing extractor reduces coverage, it does not
|
|
68
|
+
// make an obligation disappear.
|
|
69
|
+
const cite = a.semantics.slice(NORMATIVE_SEMANTICS_PREFIX.length);
|
|
70
|
+
const all = graph.assertions.filter((b) => b.subject === a.subject && b.key === a.key && b !== a && !isNormative(b));
|
|
71
|
+
const peers = all.filter((b) => b.confidence !== "candidate");
|
|
72
|
+
const candidates = all.filter((b) => b.confidence === "candidate");
|
|
73
|
+
if (!peers.length) {
|
|
74
|
+
const r = normative.requirements.find((x) => x.subject === a.subject && x.key === a.key) || { satisfiedBy: [] };
|
|
75
|
+
const missing = r.satisfiedBy.filter((n) => !enabledAdapters.has(n));
|
|
76
|
+
note(f, "UNRESOLVED", "normative: " + cite + " requires " + JSON.stringify(a.value));
|
|
77
|
+
note(f, "UNRESOLVED",
|
|
78
|
+
"no implementation evidence" +
|
|
79
|
+
(candidates.length ? "; heuristic candidate only: " + (candidates[0].semantics || JSON.stringify(candidates[0].value))
|
|
80
|
+
: missing.length ? "; adapter not enabled: " + missing.join(", ")
|
|
81
|
+
: r.satisfiedBy.length ? "; " + r.satisfiedBy.join(", ") + " derived nothing" : "; no adapter named in satisfiedBy"));
|
|
82
|
+
} else if (a.status === "elected") {
|
|
83
|
+
note(f, "CONFIRMED", "normative: " + cite + " corroborated by " + peers.map((b) => b.evidenceClass).join(", "));
|
|
84
|
+
} else {
|
|
85
|
+
note(f, "CONFLICT", "normative: " + cite + " requires " + JSON.stringify(a.value) + "; implementation evidence says " +
|
|
86
|
+
peers.map((b) => b.evidenceClass + "=" + JSON.stringify(b.value)).join(", "));
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
58
90
|
if (a.evidenceClass === "OBSERVED_RUNTIME") note(f, "OBSERVED", a.observed ? a.observed.sourceSystem + " at " + a.observed.at : "runtime");
|
|
59
91
|
else if (a.status === "unresolved") note(f, "UNRESOLVED", "no deterministic resolution");
|
|
60
92
|
else if (a.status === "candidate") note(f, "UNRESOLVED", "heuristic candidate only");
|
|
@@ -96,7 +128,7 @@ export function audit(repo, cfg, res) {
|
|
|
96
128
|
// loop below; letting one through as a "document" makes `exists: true` for a
|
|
97
129
|
// directory, which is a check that can never fail.
|
|
98
130
|
const configured = Object.entries(cfg.docs || {})
|
|
99
|
-
.filter(([role]) => !role.endsWith("Dir"))
|
|
131
|
+
.filter(([role]) => !role.endsWith("Dir") && role !== "normative")
|
|
100
132
|
.map(([role, p]) => ({ role, path: p, configured: true }));
|
|
101
133
|
// Directory-shaped roles are enumerated rather than listed as a single entry:
|
|
102
134
|
// a directory is not a document, and reporting it as one is a dead check.
|
|
@@ -303,6 +335,13 @@ export function audit(repo, cfg, res) {
|
|
|
303
335
|
}
|
|
304
336
|
}
|
|
305
337
|
|
|
338
|
+
// ── 9. normative coverage ───────────────────────────────────────────────
|
|
339
|
+
// Every section of a normative document, with the digest a reviewer cites,
|
|
340
|
+
// and whether a requirement or an attestation accounts for it. Linking a
|
|
341
|
+
// specification to a component routes it; only this says it was reviewed.
|
|
342
|
+
const unverifiedRequirements = normative.requirements.filter((r) =>
|
|
343
|
+
!graph.assertions.some((b) => b.subject === r.subject && b.key === r.key && !isNormative(b) && b.confidence !== "candidate"));
|
|
344
|
+
|
|
306
345
|
const rows = [...classes.entries()].map(([fact, v]) => ({ fact, classification: v.cls, evidence: v.why.slice(0, 4) }));
|
|
307
346
|
rows.sort((a, b) => (a.fact < b.fact ? -1 : 1));
|
|
308
347
|
const counts = rows.reduce((acc, r) => { acc[r.classification] = (acc[r.classification] || 0) + 1; return acc; }, {});
|
|
@@ -333,7 +372,17 @@ export function audit(repo, cfg, res) {
|
|
|
333
372
|
.filter((c) => c.status !== "unresolved")
|
|
334
373
|
.map((c) => ({ kind: c.kind, subject: c.subject, key: c.key, status: c.status, basis: c.election.basis, ruleId: c.election.ruleId })),
|
|
335
374
|
documentationContradictions: docContradictions,
|
|
375
|
+
normative: {
|
|
376
|
+
sections: normative.sections,
|
|
377
|
+
uncovered: normative.uncovered,
|
|
378
|
+
requirements: normative.requirements.map((r) => ({
|
|
379
|
+
subject: r.subject, id: r.id, key: r.key, value: r.value,
|
|
380
|
+
source: r.source.path + "#" + r.source.section, satisfiedBy: r.satisfiedBy,
|
|
381
|
+
})),
|
|
382
|
+
},
|
|
336
383
|
reviewedAmbiguity: [
|
|
384
|
+
...normative.uncovered.map((s) => ({ kind: "NORMATIVE_UNCOVERED", subject: s.path + "#" + s.section })),
|
|
385
|
+
...unverifiedRequirements.map((r) => ({ kind: "REQUIREMENT_UNVERIFIED", subject: r.subject + " " + r.key })),
|
|
337
386
|
...unresolvedEvents.map((pattern) => ({ kind: "EVENT_WITHOUT_PRODUCER", subject: pattern })),
|
|
338
387
|
...ambiguousOwnership.map((ref) => ({ kind: "COMPONENT_OWNERSHIP", subject: ref })),
|
|
339
388
|
...unclaimedContracts.map((p) => ({ kind: "UNCLAIMED_CONTRACT", subject: p })),
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// fail-closed default: silence is not a resolution.
|
|
11
11
|
import { AgentDocError, CODES } from "../codes.mjs";
|
|
12
12
|
import { EVIDENCE_CLASSES, CONFIDENCE } from "./classes.mjs";
|
|
13
|
+
import { isNormative } from "../normative.mjs";
|
|
13
14
|
|
|
14
15
|
export class AuthorityEngine {
|
|
15
16
|
constructor(rules) {
|
|
@@ -75,6 +76,18 @@ export function resolveAssertions(assertions, authority) {
|
|
|
75
76
|
const [subject, key] = gk.split("\u0000");
|
|
76
77
|
const distinct = distinctValues(list);
|
|
77
78
|
|
|
79
|
+
// A normative requirement is a claim about what the implementation must
|
|
80
|
+
// do, so it can only be corroborated, never elected on its own word. With
|
|
81
|
+
// no implementation evidence beside it, it stays unresolved: the gap the
|
|
82
|
+
// migration must surface.
|
|
83
|
+
const normative = list.some(isNormative);
|
|
84
|
+
if (normative && list.every((a) => isNormative(a) || a.confidence === "candidate")) {
|
|
85
|
+
// A heuristic candidate is not evidence either way: it stays a candidate,
|
|
86
|
+
// and the requirement stays unverified.
|
|
87
|
+
for (const a of list) a.status = isNormative(a) ? "unresolved" : "candidate";
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
if (distinct.length === 1) {
|
|
79
92
|
// Agreement across evidence classes is corroboration, not a conflict —
|
|
80
93
|
// but a lone heuristic candidate is not agreement with anything. It stays
|
|
@@ -121,9 +134,19 @@ export function resolveAssertions(assertions, authority) {
|
|
|
121
134
|
};
|
|
122
135
|
let status = "unresolved";
|
|
123
136
|
|
|
137
|
+
// 0. A normative requirement and the implementation disagree. No rule may
|
|
138
|
+
// settle that: electing the implementation waives the specification,
|
|
139
|
+
// electing the requirement hides the missing implementation. Only a
|
|
140
|
+
// change to one of them resolves it.
|
|
141
|
+
if (normative) {
|
|
142
|
+
election.rationale =
|
|
143
|
+
"a normative requirement disagrees with the implementation evidence; no authority rule may settle it — " +
|
|
144
|
+
"change the implementation, or change the specification and re-review its section";
|
|
145
|
+
}
|
|
146
|
+
|
|
124
147
|
// 1. reviewed override wins outright, but the divergence is still recorded.
|
|
125
148
|
const reviewed = candidates.filter((a) => a.evidenceClass === "REVIEWED_OVERRIDE");
|
|
126
|
-
if (rule && rule.elect.length === 1 && rule.elect[0] === "REVIEWED_OVERRIDE" && reviewed.length === 1) {
|
|
149
|
+
if (!normative && rule && rule.elect.length === 1 && rule.elect[0] === "REVIEWED_OVERRIDE" && reviewed.length === 1) {
|
|
127
150
|
election.elected = reviewed[0];
|
|
128
151
|
election.basis = "reviewed-override";
|
|
129
152
|
election.rationale = rule.rationale;
|
|
@@ -131,7 +154,7 @@ export function resolveAssertions(assertions, authority) {
|
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
// 2. authority rule with a single elected class present among candidates.
|
|
134
|
-
if (!election.elected && rule) {
|
|
157
|
+
if (!normative && !election.elected && rule) {
|
|
135
158
|
const allowed = rule.elect.filter((c) => candidates.some((a) => a.evidenceClass === c));
|
|
136
159
|
if (allowed.length === 1) {
|
|
137
160
|
const winners = candidates.filter((a) => a.evidenceClass === allowed[0]);
|
|
@@ -149,7 +172,7 @@ export function resolveAssertions(assertions, authority) {
|
|
|
149
172
|
}
|
|
150
173
|
|
|
151
174
|
// 3. superset: one candidate's value contains every other candidate's value.
|
|
152
|
-
if (!election.elected && candidates.length > 1) {
|
|
175
|
+
if (!normative && !election.elected && candidates.length > 1) {
|
|
153
176
|
const sup = candidates.find((c) => candidates.every((o) => o === c || containsValue(c.value, o.value)));
|
|
154
177
|
if (sup) {
|
|
155
178
|
election.elected = sup;
|
|
@@ -160,7 +183,7 @@ export function resolveAssertions(assertions, authority) {
|
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
// 4. recency: only when the rule names recency and the winner is a runtime fact.
|
|
163
|
-
if (!election.elected && rule && rule.elect.includes("OBSERVED_RUNTIME")) {
|
|
186
|
+
if (!normative && !election.elected && rule && rule.elect.includes("OBSERVED_RUNTIME")) {
|
|
164
187
|
const runtime = candidates.filter((a) => a.evidenceClass === "OBSERVED_RUNTIME");
|
|
165
188
|
const dated = runtime.filter((a) => a.observed && a.observed.at);
|
|
166
189
|
if (dated.length > 1) {
|
package/core/codes.mjs
CHANGED
|
@@ -58,6 +58,11 @@ export const CODES = {
|
|
|
58
58
|
ELECTION_STALE: "AGENTDOC_ELECTION_STALE",
|
|
59
59
|
UNRESOLVED_FACT: "AGENTDOC_UNRESOLVED_FACT",
|
|
60
60
|
|
|
61
|
+
// normative requirements
|
|
62
|
+
NORMATIVE_UNCOVERED: "AGENTDOC_NORMATIVE_UNCOVERED",
|
|
63
|
+
NORMATIVE_SOURCE_STALE: "AGENTDOC_NORMATIVE_SOURCE_STALE",
|
|
64
|
+
REQUIREMENT_UNVERIFIED: "AGENTDOC_REQUIREMENT_UNVERIFIED",
|
|
65
|
+
|
|
61
66
|
// provenance & determinism
|
|
62
67
|
PROVENANCE: "AGENTDOC_PROVENANCE",
|
|
63
68
|
NONDETERMINISTIC: "AGENTDOC_NONDETERMINISTIC",
|
|
@@ -87,6 +92,8 @@ export const ACCEPTABLE_WARNING_CODES = Object.freeze([
|
|
|
87
92
|
"AGENTDOC_EXTERNAL_DEPENDENCY_UNCLASSIFIED",
|
|
88
93
|
"AGENTDOC_DOC_ORPHANED",
|
|
89
94
|
"AGENTDOC_VERIFICATION_UNTIERED",
|
|
95
|
+
"AGENTDOC_NORMATIVE_UNCOVERED",
|
|
96
|
+
"AGENTDOC_REQUIREMENT_UNVERIFIED",
|
|
90
97
|
]);
|
|
91
98
|
|
|
92
99
|
export class AgentDocError extends Error {
|