agent-doc-system 0.0.0-stage → 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.
Files changed (124) hide show
  1. package/INSTALL_FOR_AGENTS.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +98 -2
  4. package/adapters/ansible.mjs +306 -0
  5. package/adapters/cache.mjs +27 -0
  6. package/adapters/contract-consumers.mjs +56 -0
  7. package/adapters/database.mjs +116 -0
  8. package/adapters/deploy-config.mjs +34 -0
  9. package/adapters/dockerfile.mjs +26 -0
  10. package/adapters/events.mjs +145 -0
  11. package/adapters/generic.mjs +86 -0
  12. package/adapters/github-actions.mjs +106 -0
  13. package/adapters/go.mjs +91 -0
  14. package/adapters/graphql.mjs +28 -0
  15. package/adapters/index.mjs +56 -0
  16. package/adapters/node.mjs +118 -0
  17. package/adapters/object-store.mjs +64 -0
  18. package/adapters/oidc.mjs +56 -0
  19. package/adapters/openapi.mjs +50 -0
  20. package/adapters/pnpm.mjs +42 -0
  21. package/adapters/protobuf.mjs +35 -0
  22. package/adapters/registry.mjs +27 -0
  23. package/adapters/resource-client.mjs +61 -0
  24. package/adapters/turborepo.mjs +135 -0
  25. package/adapters/typescript.mjs +85 -0
  26. package/adapters/wrangler.mjs +206 -0
  27. package/bin/agentdoc.mjs +494 -0
  28. package/bin/commands.mjs +169 -0
  29. package/bin/usage.txt +28 -0
  30. package/core/acceptances.mjs +69 -0
  31. package/core/audit.mjs +456 -0
  32. package/core/authority/classes.mjs +39 -0
  33. package/core/authority/engine.mjs +266 -0
  34. package/core/authority/facts.mjs +41 -0
  35. package/core/codes.mjs +126 -0
  36. package/core/compile.mjs +1018 -0
  37. package/core/contracts.mjs +121 -0
  38. package/core/derive.mjs +102 -0
  39. package/core/descriptors.mjs +489 -0
  40. package/core/determinism.mjs +95 -0
  41. package/core/discover.mjs +147 -0
  42. package/core/facts.mjs +53 -0
  43. package/core/fsx.mjs +348 -0
  44. package/core/graph.mjs +260 -0
  45. package/core/impact.mjs +175 -0
  46. package/core/indexes.mjs +165 -0
  47. package/core/jsonschema.mjs +298 -0
  48. package/core/normative.mjs +171 -0
  49. package/core/observations.mjs +129 -0
  50. package/core/provenance.mjs +83 -0
  51. package/core/query.mjs +384 -0
  52. package/core/relations.mjs +286 -0
  53. package/core/scaffold.mjs +322 -0
  54. package/core/secrets.mjs +92 -0
  55. package/core/sourcescan.mjs +88 -0
  56. package/core/toml.mjs +163 -0
  57. package/core/yaml.mjs +477 -0
  58. package/docs/ADAPTERS.md +207 -0
  59. package/docs/AUTHORITY.md +269 -0
  60. package/docs/CI.md +138 -0
  61. package/docs/COMPARISON-KODA.md +164 -0
  62. package/docs/EVALS.md +166 -0
  63. package/docs/MIGRATION.md +261 -0
  64. package/docs/PROVENANCE.md +133 -0
  65. package/docs/QUERY.md +180 -0
  66. package/docs/SPEC.md +339 -0
  67. package/docs/VERIFICATION.md +28 -0
  68. package/docs/assets/readme/agentdoc-hero.webp +0 -0
  69. package/docs/assets/readme/agentdoc-in-action.webp +0 -0
  70. package/docs/assets/readme/how-agentdoc-works.webp +0 -0
  71. package/docs/assets/readme/why-agentdoc.webp +0 -0
  72. package/docs/ci/generic-ci.sh +28 -0
  73. package/docs/ci/github-actions.yml +35 -0
  74. package/evals/harness.mjs +426 -0
  75. package/evals/scenarios/fixture-a-01-change-api.json +9 -0
  76. package/evals/scenarios/fixture-a-02-change-db.json +9 -0
  77. package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
  78. package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
  79. package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
  80. package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
  81. package/evals/scenarios/fixture-b-04-journey.json +9 -0
  82. package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
  83. package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
  84. package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
  85. package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
  86. package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
  87. package/evals/scenarios/koda-02-change-db-schema.json +20 -0
  88. package/evals/scenarios/koda-03-change-auth.json +21 -0
  89. package/evals/scenarios/koda-04-add-event-producer.json +19 -0
  90. package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
  91. package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
  92. package/evals/scenarios/koda-07-change-shared-package.json +16 -0
  93. package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
  94. package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
  95. package/evals/thresholds.json +11 -0
  96. package/package.json +46 -4
  97. package/schemas/api.schema.json +84 -0
  98. package/schemas/common.schema.json +147 -0
  99. package/schemas/component.schema.json +44 -0
  100. package/schemas/config.schema.json +801 -0
  101. package/schemas/domain.schema.json +14 -0
  102. package/schemas/graph.schema.json +1084 -0
  103. package/schemas/journeys.schema.json +47 -0
  104. package/schemas/observation.schema.json +143 -0
  105. package/schemas/resource.schema.json +28 -0
  106. package/schemas/system.schema.json +26 -0
  107. package/skills/agent-doc-system/SKILL.md +117 -0
  108. package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
  109. package/skills/agent-doc-system/references/cli.md +97 -0
  110. package/skills/agent-doc-system/references/create-migrate.md +113 -0
  111. package/skills/agent-doc-system/references/maintenance.md +52 -0
  112. package/templates/ADR.md +26 -0
  113. package/templates/ARCHITECTURE.md +19 -0
  114. package/templates/CONSTRAINTS.md +17 -0
  115. package/templates/PRODUCT.md +17 -0
  116. package/templates/api.yaml +10 -0
  117. package/templates/ci.yml +31 -0
  118. package/templates/component.yaml +14 -0
  119. package/templates/domain.yaml +6 -0
  120. package/templates/journey.yaml +7 -0
  121. package/templates/observation.yaml +19 -0
  122. package/templates/resource.yaml +8 -0
  123. package/templates/system.yaml +10 -0
  124. package/templates/warning-acceptance.yaml +14 -0
@@ -0,0 +1,116 @@
1
+ // Database adapter: migration and schema evidence.
2
+ //
3
+ // Two distinct outcomes, deliberately kept apart:
4
+ // - an authored logical-database Resource that matches → a resolved binding
5
+ // and a uses-resource relation;
6
+ // - no match → a *candidate* fact plus a warning. Migration authority alone
7
+ // does not establish database ownership, so the system refuses to invent
8
+ // the edge. This mirrors a real review in the reference project where a
9
+ // library shipped migrations but explicitly did not own a database.
10
+ import { Adapter } from "./registry.mjs";
11
+ import { firstExistingFile, findMigrationDirs } from "../core/sourcescan.mjs";
12
+
13
+ const ORM_CONFIG = [
14
+ "drizzle.config.ts", "drizzle.config.js", "drizzle.config.mjs",
15
+ "prisma/schema.prisma", "schema.prisma",
16
+ "knexfile.js", "knexfile.ts", "sequelize.config.js", "ormconfig.json",
17
+ ];
18
+
19
+ export class DatabaseAdapter extends Adapter {
20
+ static adapterName = "database";
21
+ constructor() {
22
+ super({ name: "database", version: "1.0.0", kind: "source" });
23
+ }
24
+ extract(ctx, unit) {
25
+ const { repo, prov, compRef } = ctx;
26
+ const root = unit.root;
27
+ const migrationDirs = findMigrationDirs(repo, root);
28
+ const ormConfig = firstExistingFile(repo, root, ORM_CONFIG);
29
+ if (!migrationDirs.length && !ormConfig) return {};
30
+
31
+ let evidencePath = ormConfig;
32
+ if (migrationDirs.length) {
33
+ evidencePath = repo.walk(migrationDirs[0])[0] || migrationDirs[0];
34
+ }
35
+ const pr = prov.add("observed", evidencePath, "migration-extractor", ["der:" + compRef + ":/bindings"]);
36
+
37
+ // A component that both migrates and opens a connection owns the database.
38
+ const opensConnection = hasConnectionEvidence(ctx, unit);
39
+ if (!opensConnection) {
40
+ ctx.diagnostics.push({
41
+ severity: "warning",
42
+ code: "AGENTDOC_RESOURCE_CANDIDATE",
43
+ subject: compRef,
44
+ refs: [compRef],
45
+ message:
46
+ "migration or schema authority under '" + root + "' has no matching authored logical-database Resource, " +
47
+ "and no connection-pool evidence: migration authority alone does not establish database ownership",
48
+ paths: [evidencePath],
49
+ });
50
+ ctx.addFact(compRef, "resource.ownership", null, {
51
+ evidenceClass: "DERIVED", confidence: "candidate", provRecs: [pr],
52
+ semantics: "a schema exists here, but which component owns the logical database is unresolved",
53
+ });
54
+ return { migrationAuthority: evidencePath };
55
+ }
56
+
57
+ const name = compRef.split("/")[1];
58
+ const target = ctx.sources.byKindName.get("Resource/" + name + "-postgres")
59
+ || ctx.sources.byKindName.get("Resource/" + name + "-database")
60
+ || ctx.sources.byKindName.get("Resource/" + name + "-db")
61
+ || findDatabaseResource(ctx, name);
62
+ if (!target) {
63
+ ctx.diagnostics.push({
64
+ severity: "warning",
65
+ code: "AGENTDOC_RESOURCE_CANDIDATE",
66
+ subject: compRef,
67
+ refs: [compRef],
68
+ message:
69
+ "component '" + name + "' has migration and connection authority but no authored logical-database Resource; " +
70
+ "declare one or record why the database is owned elsewhere",
71
+ paths: [evidencePath],
72
+ });
73
+ ctx.addFact(compRef, "resource.ownership", null, {
74
+ evidenceClass: "DERIVED", confidence: "candidate", provRecs: [pr],
75
+ semantics: "this component opens a database connection, but no authored Resource claims ownership",
76
+ });
77
+ return { migrationAuthority: evidencePath };
78
+ }
79
+ ctx.addFact(compRef, "resource.ownership", target.name, {
80
+ evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
81
+ semantics: "migration and connection authority in this root resolve to the authored logical database",
82
+ });
83
+ // Migrations and a connection string are the same dependency a declared
84
+ // client matcher would find, so the edge carries the same instance key and
85
+ // the two routes merge into one fact.
86
+ const client = ctx.clientKeyForResourceType("logical-database");
87
+ const attrs = client ? { via: "migrations+connection", client } : { via: "migrations+connection" };
88
+ ctx.addEdge("usesResource", compRef, target.ref, attrs, [pr], "DERIVED");
89
+ return {
90
+ migrationAuthority: evidencePath,
91
+ bindings: [{ kind: "database", logicalResourceRef: target.ref, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) }],
92
+ };
93
+ }
94
+ }
95
+
96
+ const POOL_HINT = /(createPool|new Pool|pg\.Pool|psycopg2?\.connect|sql\.Open|mysql\.createConnection|Pool\(|DataSource|prisma\.(datasource|client)|DATABASE_URL|DB_DSN)/;
97
+
98
+ function hasConnectionEvidence(ctx, unit) {
99
+ for (const f of ctx.repo.walk(unit.root)) {
100
+ if (/\.(ts|tsx|js|mjs|go|py|rb|java|kt|rs|ex|cs|php)$/.test(f)) {
101
+ if (/(^|\/)(tests?|__tests__)\//.test(f)) continue;
102
+ if (POOL_HINT.test(ctx.repo.readText(f))) return true;
103
+ }
104
+ }
105
+ return false;
106
+ }
107
+
108
+ // A Resource whose name starts with the component name and whose type is a
109
+ // database. A deterministic naming convention, not a guess: an ambiguous match
110
+ // is left unresolved.
111
+ function findDatabaseResource(ctx, name) {
112
+ const cands = ctx.sources.entities.filter(
113
+ (e) => e.kind === "Resource" && e.doc.spec.type === "logical-database" && e.name.startsWith(name)
114
+ );
115
+ return cands.length === 1 ? cands[0] : null;
116
+ }
@@ -0,0 +1,34 @@
1
+ // Deployment-config adapter: platform manifests that make a root deployable.
2
+ // Only the file's existence is treated as evidence; no value from these files
3
+ // is ever reported as deployed state.
4
+ import { Adapter } from "./registry.mjs";
5
+
6
+ const DEPLOY_FILES = [
7
+ "fly.toml", "app.yaml", "render.yaml", "railway.json", "vercel.json",
8
+ "netlify.toml", "heroku.yml", "Procfile", "service.yaml", "k8s/deployment.yaml",
9
+ "chart/Chart.yaml", "serverless.yml", "serverless.yaml", "ansible.cfg",
10
+ ];
11
+
12
+ export class DeployConfigAdapter extends Adapter {
13
+ static adapterName = "deploy-config";
14
+ constructor() {
15
+ super({ name: "deploy-config", version: "1.0.0", kind: "unit-marker" });
16
+ }
17
+ detect(ctx, root) {
18
+ for (const n of DEPLOY_FILES) {
19
+ if (ctx.repo.exists(root + "/" + n)) return { path: root + "/" + n, shape: "deployable" };
20
+ }
21
+ return null;
22
+ }
23
+ extract(ctx, unit) {
24
+ const { repo, prov, compRef } = ctx;
25
+ const out = { deployConfigs: [] };
26
+ for (const n of DEPLOY_FILES) {
27
+ const p = unit.root + "/" + n;
28
+ if (!repo.exists(p)) continue;
29
+ prov.add("observed", p, "deploy-extractor", ["der:" + compRef + ":/artifact"]);
30
+ out.deployConfigs.push(p);
31
+ }
32
+ return out;
33
+ }
34
+ }
@@ -0,0 +1,26 @@
1
+ // Dockerfile adapter: a Dockerfile is deployable-artifact evidence.
2
+ import { Adapter } from "./registry.mjs";
3
+
4
+ export class DockerfileAdapter extends Adapter {
5
+ static adapterName = "dockerfile";
6
+ constructor() {
7
+ super({ name: "dockerfile", version: "1.0.0", kind: "unit-marker" });
8
+ }
9
+ detect(ctx, root) {
10
+ for (const n of ["Dockerfile", "Dockerfile.prod", "Containerfile"]) {
11
+ if (ctx.repo.exists(root + "/" + n)) return { path: root + "/" + n, shape: "deployable" };
12
+ }
13
+ return null;
14
+ }
15
+ extract(ctx, unit) {
16
+ const { repo, prov, compRef } = ctx;
17
+ const out = { dockerfiles: [] };
18
+ for (const n of ["Dockerfile", "Dockerfile.prod", "Containerfile"]) {
19
+ const p = unit.root + "/" + n;
20
+ if (!repo.exists(p)) continue;
21
+ prov.add("observed", p, "deploy-extractor", ["der:" + compRef + ":/artifact"]);
22
+ out.dockerfiles.push(p);
23
+ }
24
+ return out;
25
+ }
26
+ }
@@ -0,0 +1,145 @@
1
+ // Event subject adapter.
2
+ //
3
+ // Discovers event subjects and their producers and consumers from source,
4
+ // filtered by the project's declared event subject prefixes. Two things matter
5
+ // here:
6
+ //
7
+ // 1. No hard-coded project vocabulary. Koda's `koda.` prefix became a
8
+ // configuration value; without prefixes the extractor falls back to a
9
+ // conservative three-or-more-segment shape, which is what subject-based
10
+ // brokers actually use.
11
+ // 2. Producer and consumer roles are evidence, not assumption. A file that
12
+ // merely mentions a subject is neither.
13
+ import { Adapter } from "./registry.mjs";
14
+ import { sourceFiles as sourceFilesOf } from "../core/sourcescan.mjs";
15
+
16
+ const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts|mjs|cjs|go|py|rb|java|kt|rs|ex|exs|cs|php)$/;
17
+ const TEST_PATH = /(^|\/)(tests?|__tests__|spec)\//;
18
+ const TEST_FILE = /\.(test|spec)\./;
19
+
20
+ // A subject-shaped token: three or more dot-separated lowercase segments, with
21
+ // optional trailing wildcards. Two-segment values are usually scopes, not
22
+ // subjects, and are excluded for exactly that reason.
23
+ // A subject is three or more dot-separated segments: `orders.created.v1`. A
24
+ // declared family is two or more segments plus a wildcard: `orders.created.*`.
25
+ // Both are real and both are indexed. A bare `orders.*` is NOT accepted: a
26
+ // wildcard at the top level is a catch-all, not a subject, and letting it in
27
+ // would fold every subject in the system into one meaningless family.
28
+ const SUBJECT_TOKEN = /["'`]([a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)+(?:[.*>])?|[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)+\.(?:\*|>))["'`]/g;
29
+ // A named subject constant: its name is what a producer file actually mentions,
30
+ // while the pattern lives in another unit. Resolving the name is what lets a
31
+ // component that publishes "LESSON_PUBLISHED" be credited with producing the
32
+ // subject the shared package defines.
33
+ const SUBJECT_CONST = /\b([A-Z][A-Z0-9_]*_SUBJECT)\b\s*[=:]\s*["'`]([a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+){2,}(?:[.*>])?)["'`]/g;
34
+ // Any identifier bound to a subject literal, in any naming style. A producer
35
+ // file usually references the constant by name; the literal itself lives in the
36
+ // shared unit that defines the vocabulary.
37
+ const SUBJECT_DECL = /\b([A-Za-z_$][\w$]*)\b\s*(?::\s*string\s*)?=\s*["'`]([a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+){2,}(?:[.*>])?)["'`]/g;
38
+ const CONST_REF = /\b([A-Za-z_$][\w$]*)\b/g;
39
+
40
+ const PUBLISH_HINT = /\b(publish|emit|enqueue|produce|outbox|Publish\(|Emit\()/;
41
+ const CONSUME_HINT = /\b(subscribe|consume|onMessage|handler|WithFilterSubject|FilterSubjects|busloop|subscriber|ProcessBatch|dlq|DLQ|ack|Nak\()/;
42
+
43
+ export class EventsAdapter extends Adapter {
44
+ static adapterName = "events";
45
+ constructor() {
46
+ super({ name: "events", version: "1.0.0", kind: "source" });
47
+ }
48
+ extract(ctx, unit) {
49
+ const { repo, prov, compRef } = ctx;
50
+ const prefixes = (ctx.cfg.discovery.eventPatternPrefixes || []).filter(Boolean);
51
+ const contract = ctx.primaryEventContract();
52
+ if (!contract) return {};
53
+ // Constants declared anywhere in the repository, and which of those
54
+ // declarations this unit can actually see (itself plus what it depends on).
55
+ const consts = new Map();
56
+ const collectDecls = (root) => {
57
+ for (const f of sourceFilesOf(ctx.repo, root)) {
58
+ const text = ctx.repo.readText(f);
59
+ for (const m of text.matchAll(SUBJECT_DECL)) consts.set(m[1], m[2]);
60
+ for (const m of text.matchAll(SUBJECT_CONST)) consts.set(m[1], m[2]);
61
+ }
62
+ };
63
+ collectDecls(unit.root);
64
+ // A unit can only be credited with a subject it can see: its own sources,
65
+ // plus those of the units it declares a dependency on, transitively. The
66
+ // dependency map is built from manifests before extraction, so this does
67
+ // not depend on adapter execution order.
68
+ const visible = ctx.transitiveDependencies(compRef);
69
+ for (const other of ctx.discovery.eligible) {
70
+ if (other.component && visible.has(other.component.ref) && other.component.ref !== compRef) collectDecls(other.root);
71
+ }
72
+ const families = new Set();
73
+ for (const f of repo.walk(unit.root)) {
74
+ if (!SRC_EXT.test(f)) continue;
75
+ if (TEST_PATH.test(f) || TEST_FILE.test(f)) continue;
76
+ const text = repo.readText(f);
77
+ const publish = PUBLISH_HINT.test(text);
78
+ const consume = CONSUME_HINT.test(text);
79
+ if (!publish && !consume) continue;
80
+ const found = new Set();
81
+ for (const m of text.matchAll(SUBJECT_TOKEN)) {
82
+ const s = m[1];
83
+ if (prefixes.length && !prefixes.some((p) => s.startsWith(p))) continue;
84
+ found.add(s);
85
+ }
86
+ for (const m of text.matchAll(CONST_REF)) {
87
+ const pattern = consts.get(m[1]);
88
+ if (pattern && pattern !== m[0] && (!prefixes.length || prefixes.some((p) => pattern.startsWith(p)))) found.add(pattern);
89
+ }
90
+ for (const s of found) {
91
+ if (s.endsWith(".*") || s.endsWith(".>")) families.add(s);
92
+ }
93
+ for (const s of found) {
94
+ const pr = prov.add("observed", f, "event-extractor", ["evt:" + s]);
95
+ if (publish) ctx.addEvent(s, "producer", compRef, pr, contract);
96
+ if (consume) ctx.addEvent(s, "consumer", compRef, pr, contract);
97
+ }
98
+ }
99
+ // The family set is only needed to fold literals; the fold itself happens in
100
+ // the index builder, where the contract and the resolution state are known.
101
+ void families;
102
+ return {};
103
+ }
104
+ // A declared event catalog is a strong, human-maintained statement of which
105
+ // component subscribes to which subjects. The catalog file is the contract.
106
+ contracts(ctx) {
107
+ const out = [];
108
+ for (const ec of ctx.cfg.discovery.eventContracts || []) {
109
+ // A contract may be a file, a directory, or a module prefix.
110
+ const files = ctx.repo.filesWithPrefix(ec.path);
111
+ if (!files.length) {
112
+ ctx.errors.push({
113
+ code: "AGENTDOC_PATH_UNRESOLVED",
114
+ message: "declared event contract path does not resolve: " + ec.path,
115
+ path: ec.path,
116
+ });
117
+ continue;
118
+ }
119
+ const patterns = ec.format === "code" ? harvestFromCode(ctx, ec, files) : Object.keys(safeJson(ctx, ec));
120
+ // Provenance must name a real file; the contract may be a directory or a
121
+ // module prefix.
122
+ const evidenceFile = files.find((f) => SRC_EXT.test(f) && !/\.(test|spec)\./.test(f)) || files[0];
123
+ const pr = ctx.prov.add("declared", evidenceFile, "event-catalog-extractor", patterns.map((p) => "evt:" + p));
124
+ for (const p of patterns.sort()) ctx.addEvent(p, "consumer", ec.consumer, pr, { format: ec.format === "code" ? "code" : "declared-catalog", ref: ec.path });
125
+ }
126
+ return out;
127
+ }
128
+ }
129
+
130
+ function safeJson(ctx, ec) {
131
+ try {
132
+ return ctx.repo.readJson(ec.path);
133
+ } catch {
134
+ return {};
135
+ }
136
+ }
137
+
138
+ function harvestFromCode(ctx, ec, files) {
139
+ const out = new Set();
140
+ for (const f of files) {
141
+ if (!SRC_EXT.test(f)) continue;
142
+ for (const m of ctx.repo.readText(f).matchAll(SUBJECT_TOKEN)) out.add(m[1]);
143
+ }
144
+ return [...out];
145
+ }
@@ -0,0 +1,86 @@
1
+ // Generic adapter: language- and toolchain-agnostic facts only.
2
+ //
3
+ // Nothing here knows a language, a package manager, a provider or a folder
4
+ // name. It contributes the two things any repository can be said to have:
5
+ // conventional documentation (handled in core/derive.mjs) and health contract
6
+ // route literals found in source.
7
+ import { Adapter } from "./registry.mjs";
8
+ import { resolveClientResources } from "./resource-client.mjs";
9
+ import { sourceFiles } from "../core/sourcescan.mjs";
10
+
11
+ // Conservative: a health route is an absolute string literal that looks like a
12
+ // liveness or readiness endpoint. Anything more specific belongs to a
13
+ // framework adapter.
14
+ const HEALTH_LITERAL = /["'`](\/(?:healthz|health|livez|readyz|ready|ping|status))["'`]/;
15
+
16
+ export class GenericAdapter extends Adapter {
17
+ static adapterName = "generic";
18
+ constructor() {
19
+ super({ name: "generic", version: "1.0.0", kind: "source" });
20
+ }
21
+
22
+ extract(ctx, unit) {
23
+ const { repo, prov, compRef } = ctx;
24
+ const derived = { healthContracts: [] };
25
+ const seen = new Set();
26
+ const ports = new Set();
27
+ const portFiles = new Map();
28
+ const allSrc = sourceFiles(repo, unit.root, { test: false });
29
+ for (const f of allSrc) {
30
+ const text = repo.readText(f);
31
+ const pm = /ListenAndServe\(\s*":(\d{2,5})"|\bPORT\s*=\s*"?(\d{2,5})"?|\bport:\s*(\d{2,5})\b/.exec(text);
32
+ if (pm) {
33
+ const p = pm[1] || pm[2] || pm[3];
34
+ ports.add(p);
35
+ if (!portFiles.has(p)) portFiles.set(p, f);
36
+ }
37
+ const m = HEALTH_LITERAL.exec(text);
38
+ if (!m) continue;
39
+ const route = m[1];
40
+ if (seen.has(route)) continue;
41
+ seen.add(route);
42
+ const pr = prov.add("observed", f, "health-extractor", ["der:" + compRef + ":/healthContracts"]);
43
+ derived.healthContracts.push({ path: f, route, semantics: "liveness", provenanceIds: null, _provs: new Set([pr]) });
44
+ }
45
+ if (derived.healthContracts.length > 1) derived.healthContracts.sort((a, b) => (a.route < b.route ? -1 : 1));
46
+ // A port is only a fact when the source states exactly one. Two literals
47
+ // mean the component binds more than one, and a single number would be a
48
+ // confident half-truth.
49
+ // Declared external services front Resources; resolve those bindings.
50
+ // Provenance must name a file that exists. The dependency evidence is the
51
+ // manifest when there is one, and otherwise the unit's descriptor.
52
+ const manifest = ["package.json", "go.mod", "Cargo.toml", "pyproject.toml", "pom.xml"]
53
+ .map((n) => unit.root + "/" + n)
54
+ .find((p) => repo.exists(p)) || ctx.componentEntity().file;
55
+ const clientProv = prov.add("observed", manifest, "client-resolver", ["rel:pending", "der:" + compRef + ":/bindings"]);
56
+ resolveClientResources(ctx, unit, compRef, clientProv);
57
+
58
+ // Capability detectors are declared, not hard-coded: a manifest cannot tell
59
+ // you a component can use a browser API, but a source literal can.
60
+ for (const cap of (ctx.cfg.discovery.capabilities || [])) {
61
+ let re;
62
+ try {
63
+ re = new RegExp(cap.match);
64
+ } catch {
65
+ continue;
66
+ }
67
+ for (const f of allSrc) {
68
+ if (!re.test(repo.readText(f))) continue;
69
+ const cpr = prov.add("observed", f, "capability-extractor", ["cap:" + compRef + "|" + cap.name]);
70
+ ctx.addCapability(compRef, cap.name, cpr, "DERIVED");
71
+ break;
72
+ }
73
+ }
74
+
75
+ if (ports.size === 1) {
76
+ const p = [...ports][0];
77
+ const pr = prov.add("observed", portFiles.get(p) || ctx.componentEntity().file, "port-extractor", ["der:" + compRef + ":/port"]);
78
+ derived.port = Number(p);
79
+ ctx.addFact(compRef, "component.port", Number(p), {
80
+ evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
81
+ semantics: "the single TCP port the source binds a listener to",
82
+ });
83
+ }
84
+ return derived;
85
+ }
86
+ }
@@ -0,0 +1,106 @@
1
+ // GitHub Actions adapter.
2
+ //
3
+ // CI is a verification authority: a workflow that names a component's paths is
4
+ // the check that must be run for that component. Recording the mapping means
5
+ // an agent changing a component is told which gate covers it, instead of
6
+ // guessing or running everything.
7
+ import { Adapter } from "./registry.mjs";
8
+ import { parseYamlDocuments } from "../core/yaml.mjs";
9
+
10
+ export class GitHubActionsAdapter extends Adapter {
11
+ static adapterName = "github-actions";
12
+ constructor() {
13
+ super({ name: "github-actions", version: "1.0.0", kind: "ci" });
14
+ }
15
+ // No detect(): a workflow directory is not a unit marker. It is not evidence
16
+ // of an importable or deployable artifact, and a marker path is hashed as a
17
+ // file for provenance, so a directory marker can only fail. GitHub runs only
18
+ // the repository-root .github/workflows; a nested one (vendored upstream, a
19
+ // subtree) is inert and is ignored here too.
20
+ contracts(ctx) {
21
+ const dir = ".github/workflows";
22
+ if (!ctx.repo.isDir(dir)) return [];
23
+ const out = [];
24
+ const files = ctx.repo.walk(dir).filter((f) => /\.ya?ml$/.test(f)).sort();
25
+ const units = ctx.discovery.eligible.filter((u) => u.component);
26
+ for (const f of files) {
27
+ let text = "";
28
+ try {
29
+ text = ctx.repo.readText(f);
30
+ } catch {
31
+ continue;
32
+ }
33
+ let doc = null;
34
+ try {
35
+ doc = parseYamlDocuments(text, f)[0]?.doc;
36
+ } catch {
37
+ doc = null;
38
+ }
39
+ const steps = [];
40
+ if (doc && doc.jobs) {
41
+ for (const [jobName, job] of Object.entries(doc.jobs)) {
42
+ for (const step of (job && job.steps) || []) {
43
+ const run = [step.run, step.uses].filter(Boolean).join(" ");
44
+ if (!run) continue;
45
+ steps.push({ job: jobName, run: String(run) });
46
+ }
47
+ }
48
+ }
49
+ const covered = new Set();
50
+ for (const u of units) {
51
+ if (text.includes(u.root + "/") || (u.pkgName && text.includes(u.pkgName))) covered.add(u.component.ref);
52
+ }
53
+ const pr = ctx.prov.add("observed", f, "ci-extractor", []);
54
+ const subjects = [];
55
+ steps.forEach((s, i) => {
56
+ if (!/[a-z0-9]/i.test(s.run)) return;
57
+ const command = s.run.trim().slice(0, 600);
58
+ // A step that installs dependencies or checks out code is not a check.
59
+ // Recording it as one would bury the commands that actually gate.
60
+ const tier = ciTier(command);
61
+ if (!tier) return;
62
+ const id = "ci:" + f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + s.job + ":" + i;
63
+ if (covered.size) {
64
+ ctx.addVerification({
65
+ id,
66
+ componentRefs: [...covered].sort(),
67
+ tier,
68
+ command,
69
+ configPaths: [f],
70
+ provRecs: [pr],
71
+ });
72
+ subjects.push("ver:" + id);
73
+ } else {
74
+ // A gate that names no component is repository-wide: it applies to
75
+ // everything, so it is recorded once as a gate rather than
76
+ // duplicated onto every component.
77
+ // The graph id must not itself start with the provenance prefix, or
78
+ // the subject key becomes ambiguous. "repo-gate:" is unambiguous.
79
+ const gid = "repo-gate:" + f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + s.job + ":" + i;
80
+ ctx.addGate({ id: gid, tier: ciTier(command), command, configPaths: [f], provRecs: [pr] });
81
+ subjects.push("gate:" + gid);
82
+ }
83
+ });
84
+ for (const s of subjects) pr.subjects.add(s);
85
+ void steps;
86
+ void covered;
87
+ }
88
+ return out;
89
+ }
90
+ }
91
+
92
+ // Returns null for a step that is not a check. Install, checkout, cache and
93
+ // upload steps gate nothing and would only add noise to a routing surface.
94
+ const NOT_A_CHECK = /^\s*(?:-\s*)?(?:uses:\s*actions\/(?:checkout|cache|upload|download|setup-|artifact)|run:\s*(?:\w*\s*)?(?:pnpm|npm|yarn|bun)\s+(?:i|install|ci)\b)/i;
95
+
96
+ function ciTier(run) {
97
+ if (NOT_A_CHECK.test(run.trim())) return null;
98
+ if (/^uses:/.test(run.trim()) && !/check|lint|test|build|audit|scan/i.test(run)) return null;
99
+ if (/e2e|playwright|cypress/.test(run)) return "e2e";
100
+ if (/contract|openapi|api:check|drift|catalog/.test(run)) return "contract";
101
+ if (/integration/.test(run)) return "integration";
102
+ if (/lint|typecheck|type-check|golangci|vet/.test(run)) return "static";
103
+ if (/\btest\b|pytest|vitest|jest|go test/.test(run)) return "unit";
104
+ if (/\bbuild\b|tsc|compile/.test(run)) return "build";
105
+ return null;
106
+ }
@@ -0,0 +1,91 @@
1
+ // Go adapter: go.mod / go.work as unit markers, module graph as build
2
+ // relations, and the go toolchain's own verification commands.
3
+ import { Adapter } from "./registry.mjs";
4
+
5
+ export class GoAdapter extends Adapter {
6
+ static adapterName = "go";
7
+ constructor() {
8
+ super({ name: "go", version: "1.0.0", kind: "unit-marker" });
9
+ }
10
+ roots(ctx) {
11
+ const out = [];
12
+ const ws = "go.work";
13
+ if (!ctx.repo.exists(ws)) return out;
14
+ const text = ctx.repo.readText(ws);
15
+ const block = /use\s*\(([^)]*)\)/s.exec(text);
16
+ const candidates = block
17
+ ? block[1].split("\n").map((l) => l.replace(/\/\/.*$/, "").trim()).filter(Boolean)
18
+ : [...text.matchAll(/^\s*use\s+(\S+)/gm)].map((m) => m[1]);
19
+ for (const raw of candidates) {
20
+ const d = raw.replace(/^\.\//, "").replace(/\/$/, "");
21
+ if (!d || d.startsWith("//") || d.includes("..")) continue;
22
+ if (!ctx.repo.isDir(d)) continue;
23
+ if (!ctx.repo.exists(d + "/go.mod")) continue;
24
+ out.push({ root: d, deployable: ctx.repo.isDir(d + "/cmd"), importable: !ctx.repo.isDir(d + "/cmd") });
25
+ }
26
+ return out;
27
+ }
28
+ detect(ctx, root) {
29
+ const p = root + "/go.mod";
30
+ if (!ctx.repo.exists(p)) return null;
31
+ return { path: p, shape: ctx.repo.isDir(root + "/cmd") ? "deployable" : "importable" };
32
+ }
33
+ extract(ctx, unit) {
34
+ const { repo, prov, compRef } = ctx;
35
+ const p = unit.root + "/go.mod";
36
+ if (!repo.exists(p)) return {};
37
+ const derived = { languages: new Set(["go"]), runtimes: new Set(["go"]) };
38
+ const text = repo.readText(p);
39
+ const pr = prov.add("observed", p, "gomod-extractor", ["der:" + compRef + ":/runtimes", "der:" + compRef + ":/languages"]);
40
+
41
+ for (const m of text.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)) {
42
+ const mod = m[1];
43
+ const target = ctx.unitByGoModule.get(mod);
44
+ if (target && target.component && target.component.ref !== compRef) {
45
+ ctx.addEdge("buildDependsOn", compRef, target.component.ref, { via: "go-module" }, [pr], "DERIVED");
46
+ } else {
47
+ for (const x of ctx.externalMatchers()) {
48
+ if (!x.re.test(mod)) continue;
49
+ ctx.addExternal(compRef, x.name, x.mechanism, x.role, pr, "DERIVED");
50
+ }
51
+ }
52
+ }
53
+ // Workspace members depended on by import alone. A Go workspace member can
54
+ // have no require line at all, so the import graph is the only evidence.
55
+ for (const importPath of ctx.importIndex.goPathsByUnit.get(unit) || []) {
56
+ for (const [mod, target] of ctx.unitByGoModule) {
57
+ if (target === unit || !target.component) continue;
58
+ if (importPath !== mod && importPath.startsWith(mod + "/")) {
59
+ ctx.addEdge("buildDependsOn", compRef, target.component.ref, { via: "go-import" }, [pr], "DERIVED");
60
+ }
61
+ }
62
+ }
63
+
64
+ const hasTests = repo.walk(unit.root).some((f) => /_test\.go$/.test(f));
65
+ ctx.addVerification({
66
+ id: compRef + ":go-test",
67
+ componentRefs: [compRef],
68
+ tier: "unit",
69
+ command: "go test ./...",
70
+ configPaths: [p],
71
+ provRecs: [pr],
72
+ });
73
+ ctx.addVerification({
74
+ id: compRef + ":go-vet",
75
+ componentRefs: [compRef],
76
+ tier: "static",
77
+ command: "go vet ./...",
78
+ configPaths: [p],
79
+ provRecs: [pr],
80
+ });
81
+ if (hasTests) derived.hasTests = true;
82
+ derived.goModule = unit.goModule;
83
+ return derived;
84
+ }
85
+ }
86
+
87
+ export function goExternalName(mod) {
88
+ const parts = mod.split("/");
89
+ const tail = parts[parts.length - 1].replace(/\.v\d+$/, "");
90
+ return (tail || parts[0]).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "go-module";
91
+ }
@@ -0,0 +1,28 @@
1
+ // GraphQL adapter: SDL as the canonical contract plus deterministic consumer
2
+ // resolution.
3
+ import { Adapter } from "./registry.mjs";
4
+ import { resolveContractConsumers, isBuildConfig } from "./contract-consumers.mjs";
5
+
6
+ export class GraphQLAdapter extends Adapter {
7
+ static adapterName = "graphql";
8
+ constructor() {
9
+ super({ name: "graphql", version: "1.0.0", kind: "contract" });
10
+ }
11
+ contracts(ctx) {
12
+ const out = [];
13
+ for (const api of ctx.sources.entities.filter((e) => e.kind === "API" && ["graphql", "graphql-sdl"].includes(e.doc.spec.type))) {
14
+ const ref = api.doc.spec.contract.ref;
15
+ const text = ctx.repo.readText(ref);
16
+ const types = [...text.matchAll(/^\s*(?:type|interface|input|enum|scalar|union)\s+([A-Za-z_][A-Za-z0-9_]*)/gm)].map((m) => m[1]);
17
+ const queries = [...text.matchAll(/^\s*(?:extend\s+)?type\s+Query\s*\{([\s\S]*?)\n\}/gm)]
18
+ .flatMap((m) => [...m[1].matchAll(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[(:]/gm)].map((x) => x[1]));
19
+ ctx.addContract({ apiRef: api.ref, ref, types: [...new Set(types)].sort(), queries: [...new Set(queries)].sort() });
20
+ const consumers = resolveContractConsumers(ctx, ref, api.doc.spec.provider);
21
+ for (const [consumerRef, files] of consumers) {
22
+ const provs = files.map((f) => ctx.prov.add(isBuildConfig(f) ? "validated" : "observed", f, "api-contract-extractor", ["rel:pending"]));
23
+ ctx.addEdge("consumesApi", consumerRef, api.ref, { via: files.some(isBuildConfig) ? "codegen" : "source-reference" }, provs, "DERIVED");
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ }