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,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
|
-
#
|
|
1
|
+
# agentdoc — portable documentation & software-catalog system
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|