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/core/audit.mjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// Repository audit and migration report.
|
|
2
|
+
//
|
|
3
|
+
// MIGRATE mode does not trust documentation. It inventories four sources of
|
|
4
|
+
// truth — the repository's manifests and code, its machine-readable contracts,
|
|
5
|
+
// its authored documentation, and its authoritative external systems — and then
|
|
6
|
+
// reports, per fact, which of these supports it. Every major fact lands in
|
|
7
|
+
// exactly one class:
|
|
8
|
+
//
|
|
9
|
+
// CONFIRMED two or more independent sources agree
|
|
10
|
+
// DERIVED deterministically derivable from the repository
|
|
11
|
+
// OBSERVED read from an authoritative external system
|
|
12
|
+
// CONFLICT sources disagree and no authority rule settles it
|
|
13
|
+
// UNRESOLVED the repository cannot answer, and neither can we
|
|
14
|
+
//
|
|
15
|
+
// Uncertainty is never promoted to authored truth. `agentdoc scaffold` writes
|
|
16
|
+
// only CONFIRMED and DERIVED facts; OBSERVED stays in an ObservationSet;
|
|
17
|
+
// CONFLICT and UNRESOLVED go into the reviewed-ambiguity list for a human.
|
|
18
|
+
import { classifyContractFile } from "./contracts.mjs";
|
|
19
|
+
import { ContextIndex } from "./query.mjs";
|
|
20
|
+
import { expandGlob, isRepoPath } from "./fsx.mjs";
|
|
21
|
+
|
|
22
|
+
const DOC_EXT = /\.(md|mdx|adoc|rst|txt)$/;
|
|
23
|
+
|
|
24
|
+
export function audit(repo, cfg, res) {
|
|
25
|
+
const graph = res.graph;
|
|
26
|
+
const idx = new ContextIndex(graph);
|
|
27
|
+
const classes = new Map(); // fact -> class
|
|
28
|
+
const note = (fact, cls, why) => {
|
|
29
|
+
if (!classes.has(fact)) classes.set(fact, { cls, why: [] });
|
|
30
|
+
const cur = classes.get(fact);
|
|
31
|
+
if (cls === "CONFLICT" || cls === "UNRESOLVED") cur.cls = cls;
|
|
32
|
+
if (why) cur.why.push(why);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 1. entities: confirmed when corroborated by a real artifact on disk
|
|
36
|
+
for (const e of graph.entities) {
|
|
37
|
+
const f = e.ref;
|
|
38
|
+
if (e.kind === "Component") {
|
|
39
|
+
const unit = res.discovery.units.find((u) => u.component && u.component.ref === e.ref);
|
|
40
|
+
const markers = (unit && unit.markers) || [];
|
|
41
|
+
if (markers.length >= 1) note(f, "CONFIRMED", "descriptor + " + markers.map((m) => m.adapter).join(","));
|
|
42
|
+
else note(f, "UNRESOLVED", "descriptor exists but no artifact evidence was found in " + (e.derived?.sourcePath || "?"));
|
|
43
|
+
} else if (e.kind === "API") {
|
|
44
|
+
const cref = e.entity.spec.contract?.ref;
|
|
45
|
+
if (e.entity.spec.type === "oidc") note(f, "CONFIRMED", "oidc surface validated against provider source and discovery path");
|
|
46
|
+
else if (cref && repo.exists(cref)) {
|
|
47
|
+
const consumers = graph.relations.filter((r) => r.type === "consumesApi" && r.targetRef === f);
|
|
48
|
+
note(f, consumers.length ? "CONFIRMED" : "DERIVED", "canonical contract " + cref + (consumers.length ? " with " + consumers.length + " discovered consumer(s)" : " with no discovered consumer"));
|
|
49
|
+
} else note(f, "UNRESOLVED", "API has no resolvable canonical contract");
|
|
50
|
+
} else {
|
|
51
|
+
note(f, "DERIVED", "authored placement entity");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. facts and conflicts
|
|
56
|
+
for (const a of graph.assertions) {
|
|
57
|
+
const f = a.subject + " " + a.key;
|
|
58
|
+
if (a.evidenceClass === "OBSERVED_RUNTIME") note(f, "OBSERVED", a.observed ? a.observed.sourceSystem + " at " + a.observed.at : "runtime");
|
|
59
|
+
else if (a.status === "unresolved") note(f, "UNRESOLVED", "no deterministic resolution");
|
|
60
|
+
else if (a.status === "candidate") note(f, "UNRESOLVED", "heuristic candidate only");
|
|
61
|
+
else if (a.evidenceClass === "AUTHORED") note(f, "DERIVED", "authored");
|
|
62
|
+
else if (a.confidence === "deterministic") note(f, "DERIVED", "deterministic extraction");
|
|
63
|
+
else note(f, "DERIVED", a.evidenceClass);
|
|
64
|
+
}
|
|
65
|
+
for (const c of graph.conflicts) {
|
|
66
|
+
const f = c.subject + " " + c.key;
|
|
67
|
+
// A divergence a rule settled is a known fact with a documented basis, not
|
|
68
|
+
// something awaiting a human. Reporting it as CONFLICT would put settled
|
|
69
|
+
// questions in the reviewed-ambiguity list forever.
|
|
70
|
+
const settled = c.status === "resolved" || c.status === "accepted";
|
|
71
|
+
note(f, settled ? "DERIVED" : "CONFLICT",
|
|
72
|
+
c.kind + ": " + (c.detail || "") + " (" + c.status +
|
|
73
|
+
(settled ? ", elected " + (c.election.basis || "?") + " by " + (c.election.ruleId || "?") : "") + ")");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. relations
|
|
77
|
+
for (const r of graph.relations) {
|
|
78
|
+
if (r.type === "partOf" || r.type === "hasPart" || r.type === "providesApi" || r.type === "apiProvidedBy") continue;
|
|
79
|
+
const f = r.type + " " + r.sourceRef + " -> " + r.targetRef;
|
|
80
|
+
note(f, r.evidenceClass === "REVIEWED_OVERRIDE" ? "DERIVED" : "DERIVED", r.evidenceClass);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. documentation inventory
|
|
84
|
+
// The configured documentation hierarchy is documentation too. A PRODUCT.md
|
|
85
|
+
// nobody routes to is an orphan, and saying so is the point.
|
|
86
|
+
//
|
|
87
|
+
// The exemption below is narrow on purpose. Only the fixed repository-wide
|
|
88
|
+
// roles are exempt, because only those are repository-wide *by definition*:
|
|
89
|
+
// they are reached from the configuration rather than from a component. Any
|
|
90
|
+
// other configured path could just as easily name a component-scoped file, and
|
|
91
|
+
// exempting those would let a real orphan be silenced by pointing at it in
|
|
92
|
+
// configuration. A `docs.architecture` entry that resolves inside a component
|
|
93
|
+
// root is therefore reported, not excused.
|
|
94
|
+
const HIERARCHY_FILE_ROLES = new Set(["product", "architecture", "constraints"]);
|
|
95
|
+
// Only file-shaped roles become entries. The `*Dir` keys are handled by the
|
|
96
|
+
// loop below; letting one through as a "document" makes `exists: true` for a
|
|
97
|
+
// directory, which is a check that can never fail.
|
|
98
|
+
const configured = Object.entries(cfg.docs || {})
|
|
99
|
+
.filter(([role]) => !role.endsWith("Dir"))
|
|
100
|
+
.map(([role, p]) => ({ role, path: p, configured: true }));
|
|
101
|
+
// Directory-shaped roles are enumerated rather than listed as a single entry:
|
|
102
|
+
// a directory is not a document, and reporting it as one is a dead check.
|
|
103
|
+
for (const [role, key] of [["adr", "adrDir"], ["runbook", "runbookDir"]]) {
|
|
104
|
+
const dir = (cfg.docs || {})[key];
|
|
105
|
+
if (!dir) continue;
|
|
106
|
+
if (repo.isDir(dir)) {
|
|
107
|
+
for (const f of repo.walk(dir)) if (DOC_EXT.test(f)) configured.push({ role, path: f, configured: false });
|
|
108
|
+
} else {
|
|
109
|
+
// A directory role that does not exist is reported through `exists:false`
|
|
110
|
+
// below, so it needs no separate flag here.
|
|
111
|
+
configured.push({ role, path: dir, configured: false });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const unitRoots = new Set(res.discovery.units.map((u) => u.root));
|
|
115
|
+
const insideComponent = (p) => {
|
|
116
|
+
for (const r of unitRoots) if (p === r || p.startsWith(r + "/")) return r;
|
|
117
|
+
return null;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// The audit reports; it does not gate. These are therefore collected as audit
|
|
121
|
+
// findings on the report rather than pushed into the compile-time diagnostics,
|
|
122
|
+
// where a `warning` would be gated by `check` and a migration could not even
|
|
123
|
+
// see what it is being asked to fix. Each is emitted with the same code a
|
|
124
|
+
// compile-time gate would use, so the vocabulary is shared.
|
|
125
|
+
const docFindings = [];
|
|
126
|
+
const docInventory = [];
|
|
127
|
+
for (const f of repo.walk("")) {
|
|
128
|
+
if (!DOC_EXT.test(f)) continue;
|
|
129
|
+
const owners = graph.entities.filter((e) => {
|
|
130
|
+
const d = e.derived || {};
|
|
131
|
+
return (d.docs || []).includes(f) || (d.constraints || []).includes(f) || (d.runbooks || []).includes(f);
|
|
132
|
+
}).map((e) => e.ref);
|
|
133
|
+
const role = (configured.find((h) => h.path === f) || {}).role || null;
|
|
134
|
+
docInventory.push({ path: f, linkedTo: owners.sort(), role });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Built from the *configured* list, not from the walk, so a configured path
|
|
138
|
+
// that does not exist is reported. Filtering the walk made `exists` a constant
|
|
139
|
+
// `true`, which is the one thing a reader would want it to catch.
|
|
140
|
+
const hierarchyDocs = configured.map((h) => ({
|
|
141
|
+
role: h.role,
|
|
142
|
+
path: h.path,
|
|
143
|
+
exists: repo.exists(h.path),
|
|
144
|
+
// The component root this path sits inside, when it sits inside one. A
|
|
145
|
+
// repository-wide role that resolves inside a component is the case worth
|
|
146
|
+
// reporting, so this is computed for every entry rather than only for the
|
|
147
|
+
// ones that look exempt.
|
|
148
|
+
componentScoped: insideComponent(h.path),
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const exempt = (d) => {
|
|
152
|
+
if (d.linkedTo.length) return true;
|
|
153
|
+
if (d.role && HIERARCHY_FILE_ROLES.has(d.role) && !insideComponent(d.path)) return true;
|
|
154
|
+
return false;
|
|
155
|
+
};
|
|
156
|
+
const orphanDocs = docInventory.filter((d) => !exempt(d)).map((d) => d.path);
|
|
157
|
+
for (const d of docInventory.filter((x) => !exempt(x))) {
|
|
158
|
+
docFindings.push({
|
|
159
|
+
severity: "warning",
|
|
160
|
+
code: "AGENTDOC_DOC_ORPHANED",
|
|
161
|
+
subject: d.path,
|
|
162
|
+
refs: [d.path],
|
|
163
|
+
message:
|
|
164
|
+
"document is claimed by no component" +
|
|
165
|
+
(d.role ? " and its configured role (" + d.role + ") is not a repository-wide role" : "") +
|
|
166
|
+
". Link it from a component descriptor, or remove it.",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
for (const h of hierarchyDocs) {
|
|
170
|
+
if (!h.exists) {
|
|
171
|
+
docFindings.push({
|
|
172
|
+
severity: "warning",
|
|
173
|
+
code: "AGENTDOC_DOC_ORPHANED",
|
|
174
|
+
subject: h.path,
|
|
175
|
+
refs: [h.path],
|
|
176
|
+
message: "configured documentation path does not exist: " + h.path,
|
|
177
|
+
});
|
|
178
|
+
} else if (h.componentScoped) {
|
|
179
|
+
docFindings.push({
|
|
180
|
+
severity: "warning",
|
|
181
|
+
code: "AGENTDOC_DOC_ORPHANED",
|
|
182
|
+
subject: h.path,
|
|
183
|
+
refs: [h.path],
|
|
184
|
+
message:
|
|
185
|
+
"configured as repository-wide documentation (" + h.role + ") but resolves inside component " +
|
|
186
|
+
h.componentScoped + ". Either move it or claim it from that component's descriptor.",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 5. contract inventory
|
|
192
|
+
const claimedContracts = new Set(
|
|
193
|
+
graph.entities.filter((e) => e.kind === "API" && e.entity.spec.contract?.ref).map((e) => e.entity.spec.contract.ref)
|
|
194
|
+
);
|
|
195
|
+
const contractInventory = [];
|
|
196
|
+
const scanRoots = new Set([
|
|
197
|
+
...(cfg.discovery.contractRoots || []),
|
|
198
|
+
...graph.entities.filter((e) => e.kind === "API" && e.entity.spec.contract?.ref).map((e) => e.entity.spec.contract.ref.split("/").slice(0, -1).join("/")),
|
|
199
|
+
]);
|
|
200
|
+
for (const r of scanRoots) {
|
|
201
|
+
if (!r || !repo.isDir(r)) continue;
|
|
202
|
+
for (const f of repo.walk(r)) {
|
|
203
|
+
if (!classifyContractFile(f)) continue;
|
|
204
|
+
contractInventory.push({ path: f, claimedBy: claimedContracts.has(f) ? "yes" : "no" });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const unclaimedContracts = contractInventory.filter((c) => c.claimedBy === "no").map((c) => c.path);
|
|
208
|
+
|
|
209
|
+
// 6. events without producers
|
|
210
|
+
const unresolvedEvents = graph.interfaces.events.filter((e) => e.resolution === "unresolved").map((e) => e.pattern);
|
|
211
|
+
|
|
212
|
+
// 7. components without documentation or verification
|
|
213
|
+
const undocumented = graph.entities
|
|
214
|
+
.filter((e) => e.kind === "Component")
|
|
215
|
+
.filter((e) => !((e.derived || {}).docs || []).length)
|
|
216
|
+
.map((e) => e.ref);
|
|
217
|
+
const unverified = graph.entities
|
|
218
|
+
.filter((e) => e.kind === "Component")
|
|
219
|
+
.filter((e) => !graph.verification.some((v) => v.componentRefs.includes(e.ref)))
|
|
220
|
+
.map((e) => e.ref);
|
|
221
|
+
|
|
222
|
+
const ambiguousOwnership = graph.conflicts
|
|
223
|
+
.filter((c) => c.status === "unresolved" && c.key.startsWith("placement."))
|
|
224
|
+
.map((c) => c.subject);
|
|
225
|
+
|
|
226
|
+
const staleObservations = graph.observations.map((o) => ({
|
|
227
|
+
id: o.id, capturedAt: o.capturedAt, maxAgeDays: o.maxAgeDays, environment: o.environment,
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
// ── 8. documentation claims that contradict what the graph owns ─────────
|
|
231
|
+
// Only claims about vocabulary the graph actually owns are checked. A doc
|
|
232
|
+
// saying "there is no edge component" is checkable and worth failing on; a
|
|
233
|
+
// doc saying "we use Kafka" is not, because the graph has no opinion until a
|
|
234
|
+
// Resource descriptor says so.
|
|
235
|
+
const docContradictions = [];
|
|
236
|
+
const knownNames = new Map();
|
|
237
|
+
for (const e of graph.entities) knownNames.set(normaliseName(e.name), e.ref);
|
|
238
|
+
const portByComponent = new Map();
|
|
239
|
+
for (const a of graph.assertions) {
|
|
240
|
+
if (a.key === "component.port" && a.status === "elected") portByComponent.set(a.subject, a.value);
|
|
241
|
+
}
|
|
242
|
+
for (const d of docInventory) {
|
|
243
|
+
if (!/\.(md|mdx|adoc|rst)$/.test(d.path)) continue;
|
|
244
|
+
const owners = d.linkedTo;
|
|
245
|
+
let text;
|
|
246
|
+
try {
|
|
247
|
+
text = repo.readText(d.path);
|
|
248
|
+
} catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
for (const m of text.matchAll(/\b(?:there (?:is|are)|we have)\s+no\s+([a-z][a-z0-9-]{2,30})\b/gi)) {
|
|
252
|
+
const ref = knownNames.get(normaliseName(m[1]));
|
|
253
|
+
if (ref) {
|
|
254
|
+
docContradictions.push({
|
|
255
|
+
kind: "NEGATED_COMPONENT",
|
|
256
|
+
doc: d.path,
|
|
257
|
+
claim: m[0].trim(),
|
|
258
|
+
contradicts: ref,
|
|
259
|
+
detail: "the document denies a component the graph proves exists",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const m of text.matchAll(/\bport\s*(\d{4,5})\b/gi)) {
|
|
264
|
+
const claimed = Number(m[1]);
|
|
265
|
+
for (const owner of owners) {
|
|
266
|
+
const actual = portByComponent.get(owner);
|
|
267
|
+
if (actual !== undefined && actual !== claimed) {
|
|
268
|
+
docContradictions.push({
|
|
269
|
+
kind: "PORT_MISMATCH",
|
|
270
|
+
doc: d.path,
|
|
271
|
+
claim: m[0].trim(),
|
|
272
|
+
contradicts: owner,
|
|
273
|
+
detail: "the document states port " + claimed + "; the source binds port " + actual,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!owners.length) {
|
|
278
|
+
docContradictions.push({
|
|
279
|
+
kind: "UNLINKED_PORT_CLAIM",
|
|
280
|
+
doc: d.path,
|
|
281
|
+
claim: m[0].trim(),
|
|
282
|
+
contradicts: null,
|
|
283
|
+
detail: "the document states a port but is not linked to any component, so the claim cannot be checked",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// How many production dependencies exist that nobody classified as an
|
|
290
|
+
// external service. Reported as a number, not as hundreds of warnings: the
|
|
291
|
+
// decision of which third-party service is architectural belongs to a human.
|
|
292
|
+
const declared = new Set((cfg.discovery.externalDependencies || []).map((x) => x.name));
|
|
293
|
+
const unclassified = new Set();
|
|
294
|
+
for (const e of graph.entities) {
|
|
295
|
+
if (e.kind !== "Component") continue;
|
|
296
|
+
const unit = res.discovery.units.find((u) => u.component && u.component.ref === e.ref);
|
|
297
|
+
if (!unit) continue;
|
|
298
|
+
for (const d of Object.keys((unit.pkg || {}).dependencies || {})) {
|
|
299
|
+
const norm = String(d).replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
300
|
+
if (declared.has(norm)) continue;
|
|
301
|
+
if (graph.externalDependencies.some((x) => x.componentRef === e.ref)) continue;
|
|
302
|
+
unclassified.add(d);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const rows = [...classes.entries()].map(([fact, v]) => ({ fact, classification: v.cls, evidence: v.why.slice(0, 4) }));
|
|
307
|
+
rows.sort((a, b) => (a.fact < b.fact ? -1 : 1));
|
|
308
|
+
const counts = rows.reduce((acc, r) => { acc[r.classification] = (acc[r.classification] || 0) + 1; return acc; }, {});
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
inventory: {
|
|
312
|
+
files: repo.walk("").length,
|
|
313
|
+
units: res.discovery.units.length,
|
|
314
|
+
eligibleUnits: res.discovery.eligible.length,
|
|
315
|
+
entities: graph.entities.length,
|
|
316
|
+
components: graph.entities.filter((e) => e.kind === "Component").length,
|
|
317
|
+
apis: graph.entities.filter((e) => e.kind === "API").length,
|
|
318
|
+
resources: graph.entities.filter((e) => e.kind === "Resource").length,
|
|
319
|
+
relations: graph.relations.length,
|
|
320
|
+
events: graph.interfaces.events.length,
|
|
321
|
+
contracts: contractInventory.length,
|
|
322
|
+
documentationFiles: docInventory.length,
|
|
323
|
+
observations: graph.observations.length,
|
|
324
|
+
verification: graph.verification.length + graph.gates.length,
|
|
325
|
+
},
|
|
326
|
+
facts: rows,
|
|
327
|
+
counts,
|
|
328
|
+
contradictions: [
|
|
329
|
+
...graph.conflicts.filter((c) => c.status === "unresolved").map((c) => ({ kind: c.kind, subject: c.subject, key: c.key, status: c.status, detail: c.detail || null, election: c.election })),
|
|
330
|
+
...docContradictions,
|
|
331
|
+
],
|
|
332
|
+
settledDivergences: graph.conflicts
|
|
333
|
+
.filter((c) => c.status !== "unresolved")
|
|
334
|
+
.map((c) => ({ kind: c.kind, subject: c.subject, key: c.key, status: c.status, basis: c.election.basis, ruleId: c.election.ruleId })),
|
|
335
|
+
documentationContradictions: docContradictions,
|
|
336
|
+
reviewedAmbiguity: [
|
|
337
|
+
...unresolvedEvents.map((pattern) => ({ kind: "EVENT_WITHOUT_PRODUCER", subject: pattern })),
|
|
338
|
+
...ambiguousOwnership.map((ref) => ({ kind: "COMPONENT_OWNERSHIP", subject: ref })),
|
|
339
|
+
...unclaimedContracts.map((p) => ({ kind: "UNCLAIMED_CONTRACT", subject: p })),
|
|
340
|
+
...docContradictions.map((c) => ({ kind: c.kind, subject: c.doc + ": " + c.claim })),
|
|
341
|
+
...(cfg.reviewedOverrides || []).map((o) => ({ kind: "REVIEWED_OVERRIDE", subject: o.subject + " " + o.fact })),
|
|
342
|
+
],
|
|
343
|
+
documentation: {
|
|
344
|
+
findings: docFindings,
|
|
345
|
+
hierarchy: hierarchyDocs,
|
|
346
|
+
orphanDocs: orphanDocs.sort(),
|
|
347
|
+
undocumentedComponents: undocumented.sort(),
|
|
348
|
+
unverifiedComponents: unverified.sort(),
|
|
349
|
+
},
|
|
350
|
+
contracts: { inventory: contractInventory.sort((a, b) => (a.path < b.path ? -1 : 1)), unclaimed: unclaimedContracts.sort() },
|
|
351
|
+
observations: staleObservations,
|
|
352
|
+
unclassifiedDependencies: {
|
|
353
|
+
count: unclassified.size,
|
|
354
|
+
sample: [...unclassified].sort().slice(0, 40),
|
|
355
|
+
// A count a human must triage, stated once rather than as one warning per
|
|
356
|
+
// package: 600 rows of the same decision is not information. The code is
|
|
357
|
+
// in ACCEPTABLE_WARNING_CODES so the *triage* can be recorded with pinned
|
|
358
|
+
// evidence once someone has done it — without which no valid acceptance
|
|
359
|
+
// could ever be written, and the code would be decoration.
|
|
360
|
+
code: "AGENTDOC_EXTERNAL_DEPENDENCY_UNCLASSIFIED",
|
|
361
|
+
subject: null,
|
|
362
|
+
message:
|
|
363
|
+
unclassified.size + " third-party dependencies are not classified as external services. " +
|
|
364
|
+
"Add them under discovery.externalDependencies, or confirm they are not architectural. " +
|
|
365
|
+
"Every npm import is not an external service; saying which ones are is a human decision.",
|
|
366
|
+
},
|
|
367
|
+
unresolvedRefs: graph.conflicts.filter((c) => c.status === "unresolved").map((c) => c.subject + " " + c.key),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function scaffoldProposal(repo, cfg, report) {
|
|
372
|
+
const files = [];
|
|
373
|
+
const docCandidates = repo.walk("").filter((f) => DOC_EXT.test(f) && f.split("/").length <= 3).sort();
|
|
374
|
+
files.push({
|
|
375
|
+
path: "agentdoc/agentdoc.config.yaml",
|
|
376
|
+
action: "create",
|
|
377
|
+
why: "the system has no configuration yet; this is the entry point",
|
|
378
|
+
});
|
|
379
|
+
files.push({ path: "agentdoc/journeys.yaml", action: "create-if-used", why: "journeys are the only cross-component narrative the graph cannot derive" });
|
|
380
|
+
files.push({ path: "agentdoc/domains.yaml", action: "scaffold", why: "domains group systems; propose from module names, confirm with a human" });
|
|
381
|
+
files.push({ path: "agentdoc/systems.yaml", action: "scaffold", why: "systems group components with a shared responsibility" });
|
|
382
|
+
files.push({ path: "agentdoc/apis.yaml", action: "scaffold", why: "one API descriptor per canonical contract" });
|
|
383
|
+
files.push({ path: "agentdoc/resources.yaml", action: "scaffold", why: "one Resource per logical database, dataset, bus or queue" });
|
|
384
|
+
for (const d of docCandidates.slice(0, 200)) {
|
|
385
|
+
files.push({ path: d, action: "review", why: "existing documentation must be linked to a component or declared an orphan" });
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
files,
|
|
389
|
+
summary: {
|
|
390
|
+
toCreate: files.filter((f) => f.action === "create").length,
|
|
391
|
+
toScaffold: files.filter((f) => f.action === "scaffold").length,
|
|
392
|
+
toReview: files.filter((f) => f.action === "review").length,
|
|
393
|
+
// `counts` only carries classifications that are actually present, so an
|
|
394
|
+
// absent key means zero, not undefined. Summing the raw keys produced
|
|
395
|
+
// `null` on the flagship migration target — the one number in the
|
|
396
|
+
// proposal that says what must not be authored.
|
|
397
|
+
mustNotBeAuthored: (report.counts.CONFLICT || 0) + (report.counts.UNRESOLVED || 0),
|
|
398
|
+
},
|
|
399
|
+
rule: "only CONFIRMED and DERIVED facts may be written as authored descriptors; OBSERVED facts go to an ObservationSet; CONFLICT and UNRESOLVED stay in the reviewed-ambiguity list",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function normaliseName(s) {
|
|
404
|
+
return String(s).toLowerCase().replace(/[^a-z0-9]+/g, "").replace(/(api|service|worker|app|module|package|lib|library)$/g, "");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export { expandGlob, isRepoPath };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Evidence classes.
|
|
2
|
+
//
|
|
3
|
+
// The whole point of this system is that "what the repository claims",
|
|
4
|
+
// "what can be derived from code", and "what was actually observed in a live
|
|
5
|
+
// system" are different kinds of claim with different failure modes. Collapsing
|
|
6
|
+
// them is how a catalog starts lying, so the distinction is explicit everywhere:
|
|
7
|
+
// in descriptors, in the graph, in query output, and in the migration report.
|
|
8
|
+
export const EVIDENCE_CLASSES = Object.freeze([
|
|
9
|
+
"AUTHORED",
|
|
10
|
+
"DERIVED",
|
|
11
|
+
"OBSERVED_RUNTIME",
|
|
12
|
+
"EXTERNAL_STANDARD",
|
|
13
|
+
"REVIEWED_OVERRIDE",
|
|
14
|
+
"UNRESOLVED",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
// Confidence is a separate axis from evidence class. It records *how* the claim
|
|
18
|
+
// was obtained, independently of who could vouch for it.
|
|
19
|
+
export const CONFIDENCE = Object.freeze([
|
|
20
|
+
"direct", // read straight from the named authority system
|
|
21
|
+
"deterministic", // reproducible function of committed inputs
|
|
22
|
+
"declared", // asserted by a human-maintained document
|
|
23
|
+
"reviewed", // a reviewer signed off on an interpretation
|
|
24
|
+
"candidate", // heuristic; never allowed to become an elected fact
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// The single most important rule in the system.
|
|
28
|
+
export const CANDIDATE = "candidate";
|
|
29
|
+
|
|
30
|
+
// Map provenance record class -> evidence class, used when materialising
|
|
31
|
+
// relations and indexes.
|
|
32
|
+
export const PROV_TO_EVIDENCE = Object.freeze({
|
|
33
|
+
declared: "AUTHORED",
|
|
34
|
+
reviewed: "REVIEWED_OVERRIDE",
|
|
35
|
+
observed: "DERIVED",
|
|
36
|
+
validated: "DERIVED",
|
|
37
|
+
runtime: "OBSERVED_RUNTIME",
|
|
38
|
+
standard: "EXTERNAL_STANDARD",
|
|
39
|
+
});
|