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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Contract validation and indexing.
|
|
2
|
+
//
|
|
3
|
+
// Every durable cross-component boundary should have exactly one canonical
|
|
4
|
+
// machine-readable contract, and the Catalog points at it rather than
|
|
5
|
+
// duplicating it. The default dialect is OpenAPI 3.1 for JSON HTTP, but a
|
|
6
|
+
// protocol's native schema is equally valid: OIDC discovery, GraphQL SDL,
|
|
7
|
+
// protobuf, AsyncAPI.
|
|
8
|
+
import { parseYamlDocuments } from "./yaml.mjs";
|
|
9
|
+
import { AgentDocError, CODES } from "./codes.mjs";
|
|
10
|
+
|
|
11
|
+
const CONTRACTS = {
|
|
12
|
+
openapi: {
|
|
13
|
+
validate(text, fail) {
|
|
14
|
+
let root = null;
|
|
15
|
+
try {
|
|
16
|
+
const docs = parseYamlDocuments(text, "");
|
|
17
|
+
root = docs[0] && docs[0].doc;
|
|
18
|
+
} catch {
|
|
19
|
+
try {
|
|
20
|
+
root = JSON.parse(text);
|
|
21
|
+
} catch {
|
|
22
|
+
fail("not parseable as YAML or JSON");
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!root || typeof root !== "object") { fail("root is not a mapping"); return null; }
|
|
27
|
+
if (typeof root.openapi !== "string") fail("missing 'openapi' version field");
|
|
28
|
+
else if (!/^3\.[01]/.test(root.openapi)) fail("openapi must be 3.0 or 3.1, got " + root.openapi);
|
|
29
|
+
if (!root.paths || typeof root.paths !== "object") fail("missing 'paths' object");
|
|
30
|
+
if (root.components === undefined) fail("missing 'components' object");
|
|
31
|
+
if (typeof root.info?.title !== "string") fail("missing 'info.title'");
|
|
32
|
+
return { openapi: root.openapi, title: root.info?.title || null, operations: countOps(root.paths) };
|
|
33
|
+
},
|
|
34
|
+
defRe: /\.(openapi)\.(ya?ml|json)$/,
|
|
35
|
+
},
|
|
36
|
+
asyncapi: {
|
|
37
|
+
validate(text, fail) {
|
|
38
|
+
let root = null;
|
|
39
|
+
try {
|
|
40
|
+
const docs = parseYamlDocuments(text, "");
|
|
41
|
+
root = docs[0] && docs[0].doc;
|
|
42
|
+
} catch {
|
|
43
|
+
try { root = JSON.parse(text); } catch { fail("not parseable as YAML or JSON"); return null; }
|
|
44
|
+
}
|
|
45
|
+
if (!root || root.asyncapi === undefined) { fail("missing 'asyncapi' field"); return null; }
|
|
46
|
+
if (!root.channels || typeof root.channels !== "object") fail("missing 'channels' object");
|
|
47
|
+
return { asyncapi: root.asyncapi, title: root.info?.title || null, channels: Object.keys(root.channels).length };
|
|
48
|
+
},
|
|
49
|
+
defRe: /\.(asyncapi)\.(ya?ml|json)$/,
|
|
50
|
+
},
|
|
51
|
+
protobuf: {
|
|
52
|
+
validate(text, fail) {
|
|
53
|
+
if (!/^\s*(syntax|edition)\s*=/m.test(text)) { fail("missing syntax or edition declaration"); return null; }
|
|
54
|
+
const services = [...text.matchAll(/^\s*service\s+([A-Za-z0-9_]+)/gm)].map((m) => m[1]);
|
|
55
|
+
if (!services.length) fail("no service definitions found");
|
|
56
|
+
return { services };
|
|
57
|
+
},
|
|
58
|
+
defRe: /\.proto$/,
|
|
59
|
+
},
|
|
60
|
+
graphql: {
|
|
61
|
+
validate(text, fail) {
|
|
62
|
+
if (!/(type|schema|interface|input|enum|scalar)\s+[A-Za-z_]/.test(text)) { fail("no GraphQL type definitions found"); return null; }
|
|
63
|
+
return { types: [...text.matchAll(/^\s*(?:type|interface|input|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/gm)].map((m) => m[1]) };
|
|
64
|
+
},
|
|
65
|
+
defRe: /\.graphqls?$/,
|
|
66
|
+
},
|
|
67
|
+
"graphql-sdl": {
|
|
68
|
+
validate(text, fail) {
|
|
69
|
+
if (!/(type|schema|interface|input|enum|scalar)\s+[A-Za-z_]/.test(text)) { fail("no GraphQL type definitions found"); return null; }
|
|
70
|
+
return null;
|
|
71
|
+
},
|
|
72
|
+
defRe: /\.graphqls?$/,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function countOps(paths) {
|
|
77
|
+
let n = 0;
|
|
78
|
+
for (const v of Object.values(paths || {})) {
|
|
79
|
+
if (v && typeof v === "object") n += Object.keys(v).filter((k) => ["get", "put", "post", "delete", "options", "head", "patch", "trace"].includes(k)).length;
|
|
80
|
+
}
|
|
81
|
+
return n;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function validateContractFile(repo, bundle, api) {
|
|
85
|
+
const type = api.doc.spec.type;
|
|
86
|
+
const ref = api.doc.spec.contract?.ref;
|
|
87
|
+
if (!ref) return;
|
|
88
|
+
if (type === "oidc") return; // the canonical authority is the published standard, not a file
|
|
89
|
+
const spec = CONTRACTS[type];
|
|
90
|
+
const text = repo.readText(ref);
|
|
91
|
+
const fail = (msg) => {
|
|
92
|
+
throw new AgentDocError(
|
|
93
|
+
CODES.CONTRACT_SYNTAX,
|
|
94
|
+
"contract " + ref + " is not a valid " + type + " definition: " + msg,
|
|
95
|
+
{ path: ref, ref: api.ref }
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
if (!spec) throw new AgentDocError(CODES.CONTRACT_FORM, "unsupported contract type " + type, { path: ref, ref: api.ref });
|
|
99
|
+
const summary = spec.validate(text, fail);
|
|
100
|
+
void bundle;
|
|
101
|
+
api.contractSummary = summary;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function contractDefRegex() {
|
|
105
|
+
const res = [];
|
|
106
|
+
for (const s of new Set(Object.values(CONTRACTS).map((c) => c.defRe.source))) res.push(s);
|
|
107
|
+
return new RegExp("(" + res.join("|") + ")");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Contract files matching a declared type. Used by the audit to point at
|
|
111
|
+
// contract-shaped files that no API claims.
|
|
112
|
+
export function classifyContractFile(rel) {
|
|
113
|
+
for (const [type, spec] of Object.entries(CONTRACTS)) {
|
|
114
|
+
if (spec.defRe.test(rel)) return type;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function contractTypes() {
|
|
120
|
+
return Object.keys(CONTRACTS);
|
|
121
|
+
}
|
package/core/derive.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Generic derivation: per-unit facts that hold for any language or toolchain.
|
|
2
|
+
//
|
|
3
|
+
// Everything technology-specific is delegated to adapters. This file owns only
|
|
4
|
+
// facts that can be read from a directory listing alone: the conventional docs
|
|
5
|
+
// that exist, the unit's deployable/importable shape, and the ownership of the
|
|
6
|
+
// source root itself.
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { assertRepoPath } from "./fsx.mjs";
|
|
9
|
+
import { CODES } from "./codes.mjs";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_DOC_CANDIDATES = ["README.md", "ARCHITECTURE.md", "AGENTS.md", "CONTRIBUTING.md", "DESIGN.md", "OVERVIEW.md"];
|
|
12
|
+
const DEFAULT_CONSTRAINT_CANDIDATES = ["CONSTRAINTS.md", "INVARIANTS.md"];
|
|
13
|
+
const DEFAULT_RUNBOOK_CANDIDATES = ["RUNBOOK.md", "OPERATIONS.md", "ONCALL.md"];
|
|
14
|
+
|
|
15
|
+
export function genericFacts(ctx, unit) {
|
|
16
|
+
const { repo, cfg, prov, compRef } = ctx;
|
|
17
|
+
const root = unit.root;
|
|
18
|
+
const derived = { sourcePath: root };
|
|
19
|
+
|
|
20
|
+
// ── artifact shape ────────────────────────────────────────────────────────
|
|
21
|
+
const shapeProv = unit.markers.map((m) => prov.add("observed", m.path, "discovery:" + m.adapter, ["der:" + compRef + ":/artifact"]));
|
|
22
|
+
prov.add("observed", ctx.descriptorPath(unit), "descriptor-locator", ["der:" + compRef + ":/sourcePath"]);
|
|
23
|
+
if (shapeProv.length) {
|
|
24
|
+
ctx.addFact(compRef, "component.deployable", unit.deployable, { evidenceClass: "DERIVED", confidence: "deterministic", provRecs: shapeProv });
|
|
25
|
+
ctx.addFact(compRef, "component.importable", unit.importable, { evidenceClass: "DERIVED", confidence: "deterministic", provRecs: shapeProv });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── conventional docs ─────────────────────────────────────────────────────
|
|
29
|
+
const docNames = cfg.discovery.docCandidates || DEFAULT_DOC_CANDIDATES;
|
|
30
|
+
const constraintNames = cfg.discovery.constraintCandidates || DEFAULT_CONSTRAINT_CANDIDATES;
|
|
31
|
+
const runbookNames = cfg.discovery.runbookCandidates || DEFAULT_RUNBOOK_CANDIDATES;
|
|
32
|
+
const docs = [];
|
|
33
|
+
const constraints = [];
|
|
34
|
+
const runbooks = [];
|
|
35
|
+
for (const n of docNames) {
|
|
36
|
+
const p = root + "/" + n;
|
|
37
|
+
if (repo.exists(p)) { docs.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/docs"]); }
|
|
38
|
+
}
|
|
39
|
+
for (const n of constraintNames) {
|
|
40
|
+
const p = root + "/" + n;
|
|
41
|
+
if (repo.exists(p)) { constraints.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/constraints"]); }
|
|
42
|
+
}
|
|
43
|
+
for (const n of runbookNames) {
|
|
44
|
+
const p = root + "/" + n;
|
|
45
|
+
if (repo.exists(p)) { runbooks.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/runbooks"]); }
|
|
46
|
+
}
|
|
47
|
+
const spec = ctx.componentEntity().doc.spec || {};
|
|
48
|
+
for (const p of spec.context?.docs || []) { docs.push(p); prov.add("declared", ctx.descriptorPath(unit), "descriptor-reader", ["der:" + compRef + ":/docs"]); }
|
|
49
|
+
for (const p of spec.context?.constraints || []) { constraints.push(p); prov.add("declared", ctx.descriptorPath(unit), "descriptor-reader", ["der:" + compRef + ":/constraints"]); }
|
|
50
|
+
for (const p of spec.context?.runbooks || []) { runbooks.push(p); prov.add("declared", ctx.descriptorPath(unit), "descriptor-reader", ["der:" + compRef + ":/runbooks"]); }
|
|
51
|
+
|
|
52
|
+
const uniq = (a) => [...new Set(a)].sort();
|
|
53
|
+
derived.docs = uniq(docs);
|
|
54
|
+
derived.constraints = uniq(constraints);
|
|
55
|
+
derived.runbooks = uniq(runbooks);
|
|
56
|
+
derived.markers = unit.markers.map((m) => ({ adapter: m.adapter, path: m.path })).sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
57
|
+
|
|
58
|
+
return derived;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The declared component type must be corroborated by the artifact shape the
|
|
62
|
+
// repository actually shows. Preserved from the reference implementation: an
|
|
63
|
+
// authored claim that a library is deployable is a documentation bug.
|
|
64
|
+
const DEPLOYABLE_TYPES = ["service", "worker", "job", "web-app", "static-site", "infrastructure"];
|
|
65
|
+
const IMPORTABLE_TYPES = ["library", "contract-library", "cli"];
|
|
66
|
+
|
|
67
|
+
export function checkArtifactType(entity, unit) {
|
|
68
|
+
const t = entity.doc.spec.type;
|
|
69
|
+
const out = [];
|
|
70
|
+
if (DEPLOYABLE_TYPES.includes(t) && !unit.deployable) {
|
|
71
|
+
out.push({
|
|
72
|
+
severity: "error",
|
|
73
|
+
code: CODES.ARTIFACT_TYPE,
|
|
74
|
+
message: "component '" + entity.name + "' declares type '" + t + "' but no deployable artifact evidence was found in its source root",
|
|
75
|
+
refs: [entity.ref],
|
|
76
|
+
paths: [entity.file],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (IMPORTABLE_TYPES.includes(t) && !unit.importable) {
|
|
80
|
+
out.push({
|
|
81
|
+
severity: "error",
|
|
82
|
+
code: CODES.ARTIFACT_TYPE,
|
|
83
|
+
message: "component '" + entity.name + "' declares type '" + t + "' but no importable artifact evidence was found in its source root",
|
|
84
|
+
refs: [entity.ref],
|
|
85
|
+
paths: [entity.file],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (["library", "contract-library", "cli"].includes(t) && unit.deployable && !unit.importable) {
|
|
89
|
+
out.push({
|
|
90
|
+
severity: "warning",
|
|
91
|
+
code: CODES.ARTIFACT_TYPE,
|
|
92
|
+
message: "component '" + entity.name + "' declares a library type but the root also shows deployable evidence; verify the authored type",
|
|
93
|
+
refs: [entity.ref],
|
|
94
|
+
paths: [entity.file],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function assertContextPaths(paths, file, ref) {
|
|
101
|
+
for (const p of paths) assertRepoPath(p, file);
|
|
102
|
+
}
|