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.
Files changed (117) hide show
  1. package/INSTALL_FOR_AGENTS.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +151 -2
  4. package/adapters/cache.mjs +27 -0
  5. package/adapters/contract-consumers.mjs +56 -0
  6. package/adapters/database.mjs +116 -0
  7. package/adapters/deploy-config.mjs +34 -0
  8. package/adapters/dockerfile.mjs +26 -0
  9. package/adapters/events.mjs +145 -0
  10. package/adapters/generic.mjs +86 -0
  11. package/adapters/github-actions.mjs +106 -0
  12. package/adapters/go.mjs +91 -0
  13. package/adapters/graphql.mjs +28 -0
  14. package/adapters/index.mjs +55 -0
  15. package/adapters/node.mjs +118 -0
  16. package/adapters/object-store.mjs +64 -0
  17. package/adapters/oidc.mjs +56 -0
  18. package/adapters/openapi.mjs +50 -0
  19. package/adapters/pnpm.mjs +42 -0
  20. package/adapters/protobuf.mjs +35 -0
  21. package/adapters/registry.mjs +27 -0
  22. package/adapters/resource-client.mjs +61 -0
  23. package/adapters/turborepo.mjs +135 -0
  24. package/adapters/typescript.mjs +85 -0
  25. package/adapters/wrangler.mjs +206 -0
  26. package/bin/agentdoc.mjs +478 -0
  27. package/bin/usage.txt +26 -0
  28. package/core/acceptances.mjs +69 -0
  29. package/core/audit.mjs +407 -0
  30. package/core/authority/classes.mjs +39 -0
  31. package/core/authority/engine.mjs +243 -0
  32. package/core/authority/facts.mjs +41 -0
  33. package/core/codes.mjs +119 -0
  34. package/core/compile.mjs +881 -0
  35. package/core/contracts.mjs +121 -0
  36. package/core/derive.mjs +102 -0
  37. package/core/descriptors.mjs +486 -0
  38. package/core/determinism.mjs +95 -0
  39. package/core/discover.mjs +147 -0
  40. package/core/facts.mjs +53 -0
  41. package/core/fsx.mjs +342 -0
  42. package/core/graph.mjs +260 -0
  43. package/core/impact.mjs +175 -0
  44. package/core/indexes.mjs +165 -0
  45. package/core/jsonschema.mjs +298 -0
  46. package/core/observations.mjs +129 -0
  47. package/core/provenance.mjs +83 -0
  48. package/core/query.mjs +384 -0
  49. package/core/relations.mjs +286 -0
  50. package/core/scaffold.mjs +322 -0
  51. package/core/secrets.mjs +92 -0
  52. package/core/sourcescan.mjs +88 -0
  53. package/core/toml.mjs +163 -0
  54. package/core/yaml.mjs +475 -0
  55. package/docs/ADAPTERS.md +172 -0
  56. package/docs/AUTHORITY.md +260 -0
  57. package/docs/CI.md +136 -0
  58. package/docs/COMPARISON-KODA.md +164 -0
  59. package/docs/EVALS.md +166 -0
  60. package/docs/MIGRATION.md +193 -0
  61. package/docs/PROVENANCE.md +133 -0
  62. package/docs/QUERY.md +180 -0
  63. package/docs/SPEC.md +339 -0
  64. package/docs/VERIFICATION.md +28 -0
  65. package/docs/ci/generic-ci.sh +28 -0
  66. package/docs/ci/github-actions.yml +35 -0
  67. package/evals/harness.mjs +426 -0
  68. package/evals/scenarios/fixture-a-01-change-api.json +9 -0
  69. package/evals/scenarios/fixture-a-02-change-db.json +9 -0
  70. package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
  71. package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
  72. package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
  73. package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
  74. package/evals/scenarios/fixture-b-04-journey.json +9 -0
  75. package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
  76. package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
  77. package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
  78. package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
  79. package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
  80. package/evals/scenarios/koda-02-change-db-schema.json +20 -0
  81. package/evals/scenarios/koda-03-change-auth.json +21 -0
  82. package/evals/scenarios/koda-04-add-event-producer.json +19 -0
  83. package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
  84. package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
  85. package/evals/scenarios/koda-07-change-shared-package.json +16 -0
  86. package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
  87. package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
  88. package/evals/thresholds.json +11 -0
  89. package/package.json +46 -4
  90. package/schemas/api.schema.json +84 -0
  91. package/schemas/common.schema.json +107 -0
  92. package/schemas/component.schema.json +40 -0
  93. package/schemas/config.schema.json +685 -0
  94. package/schemas/domain.schema.json +14 -0
  95. package/schemas/graph.schema.json +1084 -0
  96. package/schemas/journeys.schema.json +47 -0
  97. package/schemas/observation.schema.json +143 -0
  98. package/schemas/resource.schema.json +28 -0
  99. package/schemas/system.schema.json +26 -0
  100. package/skills/agent-doc-system/SKILL.md +113 -0
  101. package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
  102. package/skills/agent-doc-system/references/cli.md +89 -0
  103. package/skills/agent-doc-system/references/create-migrate.md +101 -0
  104. package/skills/agent-doc-system/references/maintenance.md +52 -0
  105. package/templates/ADR.md +26 -0
  106. package/templates/ARCHITECTURE.md +19 -0
  107. package/templates/CONSTRAINTS.md +17 -0
  108. package/templates/PRODUCT.md +17 -0
  109. package/templates/api.yaml +10 -0
  110. package/templates/ci.yml +31 -0
  111. package/templates/component.yaml +14 -0
  112. package/templates/domain.yaml +6 -0
  113. package/templates/journey.yaml +7 -0
  114. package/templates/observation.yaml +19 -0
  115. package/templates/resource.yaml +8 -0
  116. package/templates/system.yaml +10 -0
  117. package/templates/warning-acceptance.yaml +14 -0
@@ -0,0 +1,60 @@
1
+ # Install AgentDoc — instructions for an AI coding agent
2
+
3
+ This document has exactly one job: get the `agentdoc` CLI installed and verified
4
+ on the current machine. It is **not** a project workflow. Do not look for a
5
+ project root, do not run `init`, `audit`, `scaffold`, or `check`, do not create
6
+ `.agentdoc/` or `agentdoc/`, and do not modify any software project. The task
7
+ ends when the CLI is installed and verified.
8
+
9
+ ## 1. Inspect the environment
10
+
11
+ ```bash
12
+ uname -a # OS and architecture
13
+ echo "$SHELL" # current shell
14
+ node --version # is Node present, and which major
15
+ npm --version # is npm present
16
+ command -v git # is git present (needed to install from GitHub)
17
+ ```
18
+
19
+ ## 2. Ensure a supported Node
20
+
21
+ AgentDoc requires **Node 22 or newer** (22, 24 and 26 are the tested
22
+ baselines). If `node` is missing or older than 22, install or switch to a
23
+ supported release using whatever the machine already has — `nvm`, `fnm`,
24
+ `mise`, `brew`, or the package manager — then re-check `node --version`.
25
+
26
+ If no Node toolchain exists and none can be installed without a human (for
27
+ example a managed machine), stop and report that prerequisite.
28
+
29
+ ## 3. Install or upgrade the CLI
30
+
31
+ Canonical install, from the AgentDoc GitHub repository:
32
+
33
+ ```bash
34
+ npm install -g github:lbframe/agent-doc-system
35
+ ```
36
+
37
+ The command is idempotent: run it again to upgrade to the latest `main`.
38
+ If a specific ref is required, append it: `github:lbframe/agent-doc-system#<ref>`.
39
+
40
+ The package has zero runtime dependencies. If `npm install -g` fails on
41
+ permissions, do **not** use `sudo`; fix the npm global prefix instead
42
+ (`npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to `PATH`),
43
+ or report the blocker.
44
+
45
+ ## 4. Verify the binary resolves
46
+
47
+ ```bash
48
+ command -v agentdoc # must print a path
49
+ agentdoc --version # must print a version
50
+ agentdoc doctor # machine self-check; must exit 0
51
+ ```
52
+
53
+ `agentdoc doctor` reports the Node version, the schema bundle and the compiler
54
+ version, and fails if the installation itself is unhealthy. It never requires a
55
+ project — run it from anywhere.
56
+
57
+ ## 5. Report and stop
58
+
59
+ Report the installed version and that `doctor` passed. The installation task is
60
+ complete. Do not proceed into any repository, catalog, or migration work.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lbframe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,152 @@
1
- # Temporary Holding Version
1
+ # agentdoc — portable documentation & software-catalog system
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Zero runtime dependencies. One Node runtime. AgentDoc compiles a repository
4
+ into one deterministic graph and routes a path or an entity to the minimum
5
+ context needed to change it safely.
6
+
7
+ It exists to answer one question cheaply and correctly, for an AI coding agent
8
+ or a human reviewer:
9
+
10
+ > I am about to change *this*. What do I need to know, what must I keep
11
+ > consistent, and which command proves it?
12
+
13
+ It does that while keeping three kinds of claim apart — what the repository
14
+ *claims*, what can be *derived* from code, and what was *observed* running —
15
+ and surfacing their disagreements as conflicts instead of guessing.
16
+
17
+ ## Install the CLI
18
+
19
+ The CLI is installed once per machine. Requires Node 22 or newer (22, 24 and
20
+ 26 are the tested baselines).
21
+
22
+ ```bash
23
+ npm install -g github:lbframe/agent-doc-system
24
+
25
+ agentdoc --version
26
+ agentdoc doctor # machine self-check
27
+ ```
28
+
29
+ Unpinned installs track `main`. For reproducible CI, pin a tag or commit:
30
+ `github:lbframe/agent-doc-system#v1.0.0`.
31
+
32
+ The install footprint is the CLI only — `bin/`, `core/`, `adapters/`,
33
+ `schemas/`, `templates/`, `evals/`, docs and the agent skill. Nothing is ever
34
+ copied into a project that uses AgentDoc.
35
+
36
+ ### Have an AI agent install it for you
37
+
38
+ Paste this into a capable coding agent:
39
+
40
+ ```text
41
+ Install AgentDoc on this machine from:
42
+ https://github.com/lbframe/agent-doc-system
43
+
44
+ Read INSTALL_FOR_AGENTS.md from that repository and follow it completely.
45
+
46
+ Do not initialize, inspect, migrate, or modify any software project as part of
47
+ this installation task.
48
+
49
+ When finished, verify the AgentDoc CLI and report the installed version.
50
+ ```
51
+
52
+ [`INSTALL_FOR_AGENTS.md`](INSTALL_FOR_AGENTS.md) contains the procedure.
53
+
54
+ ## Use it in a project
55
+
56
+ From the repository root:
57
+
58
+ ```bash
59
+ cd my-project
60
+ agentdoc doctor --project # is there a catalog here, and is it healthy
61
+ agentdoc audit # inventory the repo, classify every fact
62
+ ```
63
+
64
+ Then follow the workflow the audit implies:
65
+
66
+ | workflow | when | entry point |
67
+ |---|---|---|
68
+ | CREATE | no catalog exists | `agentdoc init`, then `agentdoc scaffold --write` |
69
+ | MIGRATE | established repo, scattered or stale docs | `agentdoc audit`, work the report |
70
+ | VALIDATE | before committing, in CI | `agentdoc validate` → `compile` → `check` |
71
+ | QUERY | before and during a change | `agentdoc query <path-or-ref>` |
72
+ | MAINTAIN | after a change | `agentdoc impact <paths>` |
73
+
74
+ AgentDoc writes **project data only**: `agentdoc/` (configuration and
75
+ descriptors you own), `docs/` skeleton files, `.agentdoc/graph.json` (the
76
+ compiled artifact), and a CI workflow. The CLI source is never vendored into
77
+ your repository.
78
+
79
+ The two directories differ in ownership, not just punctuation:
80
+
81
+ - `agentdoc/` is **authored** — configuration, descriptors and observation
82
+ sets. Everything in it is a claim you wrote and a reviewer can diff.
83
+ - `.agentdoc/` is **generated** — the compiled graph and other build
84
+ artifacts. Never edit it by hand; `compile` rewrites it and `check` rejects
85
+ a stale or edited one.
86
+
87
+ ## Use it with an AI agent
88
+
89
+ The agent skill is [`skills/agent-doc-system/`](skills/agent-doc-system/SKILL.md).
90
+ Install that directory as a skill in your agent harness. It teaches the agent
91
+ when to use the CLI, the evidence-class model, the working loop, and the
92
+ integrity rules — with deeper material under `skills/agent-doc-system/references/`.
93
+
94
+ ## What it is built on
95
+
96
+ Most repositories conflate three different kinds of claim, and the confusion
97
+ is the single largest source of wrong agent behaviour:
98
+
99
+ | what | who says it | how it fails |
100
+ |---|---|---|
101
+ | what the repository **claims** | authored docs, config, manifests | drifts, is aspirational, is not tested |
102
+ | what can be **derived** from code | imports, manifests, contracts | heuristic, incomplete, wrong about intent |
103
+ | what was **observed** in a running system | schedulers, bindings, live APIs | goes stale, disagrees with intent |
104
+
105
+ A claim carries an evidence class, a confidence, and a pointer to its evidence.
106
+ When two classes disagree about the same fact, the disagreement is **surfaced
107
+ as a conflict**, not reconciled. There is no global precedence order, because
108
+ authority depends on the fact: the committed manifest is authoritative for
109
+ *intent*, the live scheduler is authoritative for *what fires*, and the
110
+ canonical contract is authoritative for the *desired wire surface* while a
111
+ live probe is authoritative for *what is deployed today*.
112
+
113
+ The default is fail-closed. A fact with no governing authority rule and
114
+ disagreeing evidence is an error, and no gate passes while it stands.
115
+
116
+ ## Documentation
117
+
118
+ | file | covers |
119
+ |---|---|
120
+ | [`skills/agent-doc-system/SKILL.md`](skills/agent-doc-system/SKILL.md) | how an agent uses the system |
121
+ | [INSTALL_FOR_AGENTS.md](INSTALL_FOR_AGENTS.md) | machine installation, for an AI agent |
122
+ | [docs/SPEC.md](docs/SPEC.md) | the normative specification: entities, graph, determinism |
123
+ | [docs/AUTHORITY.md](docs/AUTHORITY.md) | evidence classes, conflict semantics, elections |
124
+ | [docs/PROVENANCE.md](docs/PROVENANCE.md) | evidence records, pointers, secret handling |
125
+ | [docs/QUERY.md](docs/QUERY.md) | the context router, budgets, freshness refusal |
126
+ | [docs/MIGRATION.md](docs/MIGRATION.md) | CREATE and MIGRATE workflows |
127
+ | [docs/CI.md](docs/CI.md) | the drift gate and its failure modes |
128
+ | [docs/ADAPTERS.md](docs/ADAPTERS.md) | writing an adapter, and what must never live in one |
129
+ | [docs/EVALS.md](docs/EVALS.md) | the routing benchmark and pre-registered thresholds |
130
+ | [docs/COMPARISON-KODA.md](docs/COMPARISON-KODA.md) | measured comparison against the reference corpus |
131
+
132
+ ## Fixtures and the golden corpus
133
+
134
+ - `fixtures/a-node-ts-postgres` — single Node/TypeScript service, REST, Postgres, GitHub Actions.
135
+ - `fixtures/b-go-multi-service` — multi-service Go repository, protobuf contracts, shared library, resources.
136
+ - `fixtures/c-alt-monorepo` — deliberately *not* `apps/`+`packages/`+`workers/`, mixed runtimes, inconsistent pre-existing docs.
137
+ - `examples/koda` — the profile that represents a large, mature, TypeScript+Go monorepo, including the runtime-observation conflicts that motivated the authority model.
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ node --test tests/*.test.mjs # unit, adversarial and mode tests
143
+ node evals/run-all.mjs # aggregate routing evaluation (gate)
144
+ npm run selfcheck # agentdoc doctor, from the source checkout
145
+ ```
146
+
147
+ ## Licence of the design
148
+
149
+ The architecture, the fail-closed discipline, the provenance model and the
150
+ warning-acceptance mechanism are extracted from a production catalog system
151
+ that proved them at scale. The namespace, schemas, adapters, CLI and authority
152
+ model here are new and neutral.
@@ -0,0 +1,27 @@
1
+ // Cache adapter: Redis / in-memory cache client evidence as a derived binding.
2
+ import { Adapter } from "./registry.mjs";
3
+
4
+ const CACHE_HINT = /(@upstash\/redis|ioredis|redis|go-redis|redis\.go|valkey|memcache|@memcached)/i;
5
+
6
+ export class CacheAdapter extends Adapter {
7
+ static adapterName = "cache";
8
+ constructor() {
9
+ super({ name: "cache", version: "1.0.0", kind: "source" });
10
+ }
11
+ extract(ctx, unit) {
12
+ const { repo, prov, compRef } = ctx;
13
+ const evFile = unit.goModule ? unit.root + "/go.mod" : unit.root + "/package.json";
14
+ if (!repo.exists(evFile)) return {};
15
+ const text = repo.readText(evFile);
16
+ if (!CACHE_HINT.test(text)) return {};
17
+ const pr = prov.add("observed", evFile, "binding-extractor", ["der:" + compRef + ":/bindings"]);
18
+ const resources = ctx.sources.entities.filter((e) => e.kind === "Resource" && e.doc.spec.type === "cache");
19
+ if (resources.length === 1) {
20
+ const client = ctx.clientKeyForResourceType("cache");
21
+ const attrs = client ? { via: "cache-client", client } : { via: "cache-client" };
22
+ ctx.addEdge("usesResource", compRef, resources[0].ref, attrs, [pr], "DERIVED");
23
+ return { bindings: [{ kind: "cache", logicalResourceRef: resources[0].ref, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) }] };
24
+ }
25
+ return { bindings: [{ kind: "cache", resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) }] };
26
+ }
27
+ }
@@ -0,0 +1,56 @@
1
+ // Shared contract-consumer resolution.
2
+ //
3
+ // A cross-component boundary is a contract plus its consumers. Consumers are
4
+ // discovered deterministically: a unit that references the canonical contract
5
+ // file from a source or build-configuration file, other than the provider's own
6
+ // unit, consumes that contract. Documentation mentions are excluded so a doc
7
+ // that quotes a contract path cannot manufacture a dependency edge.
8
+ import path from "node:path";
9
+
10
+ const CODE_EXT = /\.(ts|tsx|js|jsx|mts|cts|mjs|cjs|go|py|rb|java|kt|rs|ex|exs|cs|php|swift|gradle|proto|graphql|sql)$/;
11
+ const CONFIG_NAMES = new Set([
12
+ "package.json", "go.mod", "buf.gen.yaml", "buf.yaml", "buf.work.yaml",
13
+ "codegen.yaml", "codegen.yml", "openapi-ts.config.ts", "openapi-ts.config.js",
14
+ "oapi-codegen.yaml", "oapi-codegen.yml", "turbo.json", "Makefile", "justfile",
15
+ "Taskfile.yml", "build.gradle", "build.gradle.kts", "pom.xml",
16
+ ]);
17
+ const DOC_EXT = /\.(md|mdx|txt|adoc|rst)$/;
18
+
19
+ export function contractReferenceSites(repo, unit) {
20
+ const out = new Map(); // file -> true
21
+ for (const f of repo.walk(unit.root)) {
22
+ const base = path.posix.basename(f);
23
+ if (DOC_EXT.test(f)) continue;
24
+ if (f.includes("/docs/") || f.startsWith("docs/")) continue;
25
+ if (!CODE_EXT.test(f) && !CONFIG_NAMES.has(base)) continue;
26
+ out.set(f, true);
27
+ }
28
+ return out;
29
+ }
30
+
31
+ // Resolve consumers for a contract file. Returns Map<componentRef, string[]>.
32
+ // The per-unit file inventory is built once and cached on the context, so a
33
+ // repository with many contracts does not re-walk every unit per contract.
34
+ export function resolveContractConsumers(ctx, contractRef, providerRef) {
35
+ if (!ctx.referenceIndex) {
36
+ const perUnit = new Map();
37
+ for (const unit of ctx.discovery.eligible) {
38
+ if (!unit.component) continue;
39
+ perUnit.set(unit.component.ref, [...contractReferenceSites(ctx.repo, unit).keys()].sort());
40
+ }
41
+ ctx.referenceIndex = perUnit;
42
+ }
43
+ const out = new Map();
44
+ for (const [ref, files] of ctx.referenceIndex) {
45
+ if (ref === providerRef) continue;
46
+ const hits = files.filter((f) => ctx.repo.readText(f).includes(contractRef));
47
+ if (hits.length) out.set(ref, hits);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ // A unit that names the contract in its own build config is a stronger signal
53
+ // than a stray mention in source.
54
+ export function isBuildConfig(file) {
55
+ return CONFIG_NAMES.has(path.posix.basename(file));
56
+ }
@@ -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
+ }