agent-doc-system 1.2.0 → 1.4.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/CHANGELOG.md +188 -1
- package/README.md +1 -1
- package/adapters/cache.mjs +5 -4
- package/adapters/database.mjs +155 -6
- package/adapters/deploy-config.mjs +3 -2
- package/adapters/dockerfile.mjs +3 -2
- package/adapters/generic.mjs +2 -1
- package/adapters/github-actions.mjs +2 -1
- package/adapters/go.mjs +4 -3
- package/adapters/node.mjs +3 -2
- package/adapters/rust.mjs +3 -0
- package/adapters/turborepo.mjs +3 -2
- package/adapters/typescript.mjs +7 -5
- package/adapters/wrangler.mjs +3 -2
- package/bin/agentdoc.mjs +9 -2
- package/bin/usage.txt +2 -0
- package/core/audit.mjs +7 -5
- package/core/codes.mjs +2 -0
- package/core/compile.mjs +12 -6
- package/core/derive.mjs +4 -3
- package/core/descriptors.mjs +52 -2
- package/core/discover.mjs +3 -2
- package/core/fsx.mjs +174 -42
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +84 -14
- package/core/paths.mjs +71 -0
- package/core/query.mjs +58 -23
- package/core/scaffold.mjs +130 -23
- package/core/sourcescan.mjs +4 -1
- package/docs/ADAPTERS.md +49 -5
- package/docs/BENCHMARK.md +83 -21
- package/docs/CI.md +9 -1
- package/docs/EVALS.md +209 -17
- package/docs/MIGRATION.md +22 -4
- package/docs/PROVENANCE.md +6 -0
- package/docs/QUERY.md +50 -7
- package/docs/SPEC.md +89 -2
- package/docs/ci/github-actions.yml +1 -1
- package/evals/baseline-search.mjs +272 -0
- package/evals/facts.mjs +28 -0
- package/evals/harness.mjs +120 -116
- package/package.json +4 -2
- package/schemas/resource.schema.json +9 -1
- package/skills/agent-doc-system/SKILL.md +2 -1
- package/skills/agent-doc-system/references/cli.md +6 -1
- package/skills/agent-doc-system/references/create-migrate.md +8 -2
- package/templates/ci.yml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,183 @@ refused by the next with `AGENTDOC_COMPILER_MISMATCH` until it is recompiled.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.4.0] - 2026-11-10
|
|
13
|
+
|
|
14
|
+
`impact` now treats a change under a Drizzle migration directory as a change
|
|
15
|
+
to the database it migrates. It reaches every component bound to that
|
|
16
|
+
database, not only the component that holds the file. Any other ORM layout can
|
|
17
|
+
be declared on the database Resource with the new `spec.migrationPaths`.
|
|
18
|
+
Existing `migrations/` and `alembic/versions/` paths give the same answer as
|
|
19
|
+
before.
|
|
20
|
+
|
|
21
|
+
Before you upgrade:
|
|
22
|
+
|
|
23
|
+
- **Recompile committed graphs.** As with every release, a graph compiled by
|
|
24
|
+
1.3.0 is refused with `AGENTDOC_COMPILER_MISMATCH` until you run
|
|
25
|
+
`agentdoc compile` again. Graphs of repositories that enable the `database`
|
|
26
|
+
adapter also gain `derived.migrationDirs` and the provenance behind it.
|
|
27
|
+
- **A Drizzle config whose `out` is not a plain string** (an environment
|
|
28
|
+
variable, an expression, a spread) now raises the warning
|
|
29
|
+
`AGENTDOC_MIGRATION_DIR_UNRESOLVED`. Declare the directory with
|
|
30
|
+
`spec.migrationPaths`, or accept the warning ([#63]).
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- A `logical-database` Resource can declare where its schema migrations live,
|
|
35
|
+
with `spec.migrationPaths` ([#63]). A change under a declared directory is a
|
|
36
|
+
change to that Resource, whichever component owns the file, so `impact`
|
|
37
|
+
reaches every component bound to the database. Paths are whole-segment
|
|
38
|
+
prefixes, not globs: `frontend/apps/web/drizzle` covers `0036_x.sql` and
|
|
39
|
+
`meta/_journal.json`, and never `drizzle-extra/`. A glob, a declaration on
|
|
40
|
+
another Resource type, or one path declared by two Resources fails with the
|
|
41
|
+
new `AGENTDOC_MIGRATION_PATH`; a path outside the repository fails with
|
|
42
|
+
`AGENTDOC_PATH_ESCAPE`, a missing one with `AGENTDOC_PATH_UNRESOLVED`. The
|
|
43
|
+
declaration never creates a binding.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `impact` now classifies a change under a Drizzle migration directory as a
|
|
48
|
+
database change ([#63]). Only `migrations/`, `migration/` and
|
|
49
|
+
`alembic/versions/` used to count, so `impact
|
|
50
|
+
frontend/apps/web/drizzle/0036_email_locale.sql` reported the web component
|
|
51
|
+
alone and missed every other component bound to the same database. The
|
|
52
|
+
`database` adapter now reads Drizzle Kit's `out` from `drizzle.config.ts`
|
|
53
|
+
(`.js`, `.mjs`) as a string literal, without running it, and defaults to
|
|
54
|
+
`./drizzle`. An `out` it cannot read statically is reported with the
|
|
55
|
+
acceptable warning `AGENTDOC_MIGRATION_DIR_UNRESOLVED` instead of being
|
|
56
|
+
guessed. Detected directories, including `migrations` and `alembic/versions`,
|
|
57
|
+
are recorded in the graph as `derived.migrationDirs` with provenance, and
|
|
58
|
+
`impact` classifies from them. A detected directory still reaches a database
|
|
59
|
+
only through the component's existing bindings. The answer for an existing
|
|
60
|
+
`migrations/` or `alembic/versions/` path is unchanged. Committed graphs of
|
|
61
|
+
repositories that enable the `database` adapter gain `migrationDirs` and
|
|
62
|
+
must be recompiled.
|
|
63
|
+
|
|
64
|
+
## [1.3.0] - 2026-11-07
|
|
65
|
+
|
|
66
|
+
A graph compiled on your machine now passes `check` in CI and in a clean
|
|
67
|
+
checkout. Files git ignores no longer count as compiler inputs, so a local
|
|
68
|
+
build that fills an ignored directory no longer changes `inputHash`. A
|
|
69
|
+
component whose descriptor sits at the repository root now owns the root files
|
|
70
|
+
no other component owns, so `query` and `impact` route them. `init` and
|
|
71
|
+
`scaffold --write` no longer write into your product's `docs/`: they follow the
|
|
72
|
+
configured `docs.*` paths, and a new install without a configuration gets its
|
|
73
|
+
skeleton under `agentdoc/docs/`. Releases are now published from CI with npm
|
|
74
|
+
provenance, so `npm audit signatures` can verify that a release was built from
|
|
75
|
+
this repository. The benchmark claim made in the 1.2.0 release notes is
|
|
76
|
+
withdrawn, because its gap came from the evaluation and not from routing.
|
|
77
|
+
|
|
78
|
+
Before you upgrade:
|
|
79
|
+
|
|
80
|
+
- **Recompile committed graphs.** As with every release, a graph compiled by
|
|
81
|
+
1.2.0 is refused with `AGENTDOC_COMPILER_MISMATCH` until you run
|
|
82
|
+
`agentdoc compile` again.
|
|
83
|
+
- **A descriptor or configuration path that points at a git-ignored file now
|
|
84
|
+
fails** with `AGENTDOC_PATH_UNRESOLVED`, as it would in a clean checkout.
|
|
85
|
+
Commit the file or stop ignoring it ([#45]).
|
|
86
|
+
- **New installs write the documentation skeleton to `agentdoc/docs/`**
|
|
87
|
+
instead of `docs/`. A configuration that declares `docs.*` keeps its paths,
|
|
88
|
+
and no existing file is moved ([#59]).
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
|
|
92
|
+
- A deterministic `baseline-search` routing evaluation using working-tree file
|
|
93
|
+
reads and text search, without compiled relations ([#46]). CLI reports compare
|
|
94
|
+
catalog, corrected targeted and search recall/noise/check selectivity per
|
|
95
|
+
scenario, corpus and aggregate; `docs/EVALS.md` documents every heuristic.
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
|
|
99
|
+
- Releases are now published only through a manual GitHub Actions workflow,
|
|
100
|
+
using npm trusted publishing (no token) with provenance, so `npm audit
|
|
101
|
+
signatures` can verify that a release was built from this repository
|
|
102
|
+
([#51]). `RELEASING.md` in the repository describes the process.
|
|
103
|
+
|
|
104
|
+
- **Behaviour change for new installs only:** without a configuration,
|
|
105
|
+
`agentdoc init` (and `scaffold --write`) now writes the documentation
|
|
106
|
+
skeleton to `agentdoc/docs/PRODUCT.md`, `agentdoc/docs/ARCHITECTURE.md`,
|
|
107
|
+
`agentdoc/docs/CONSTRAINTS.md` and `agentdoc/docs/adr/`, and records those
|
|
108
|
+
paths in the `docs:` block of the generated `agentdoc/agentdoc.config.yaml`
|
|
109
|
+
([#59]). It used to write to `docs/`, which in many repositories is the
|
|
110
|
+
product's own user documentation and the source of a published docs site
|
|
111
|
+
(MkDocs, Docusaurus, GitHub Pages). Authored AgentDoc material now lives in
|
|
112
|
+
`agentdoc/`; generated output stays in `.agentdoc/`. Existing configurations
|
|
113
|
+
are unaffected: a configuration that declares `docs.*` keeps its paths,
|
|
114
|
+
`docs/...` included, and no existing file is moved. To keep `docs/` on a new
|
|
115
|
+
install, declare `docs.product`, `docs.architecture`, `docs.constraints` and
|
|
116
|
+
`docs.adrDir` in `agentdoc/agentdoc.config.yaml` before running `init`.
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
|
|
120
|
+
- A component whose descriptor sits at the repository root now owns the files
|
|
121
|
+
no nested component owns ([#50]). Before, `query vite.config.ts` failed with
|
|
122
|
+
`AGENTDOC_REF_UNRESOLVED` and `impact` listed the file in `unmatchedFiles`,
|
|
123
|
+
for both `vite.config.ts` and `./vite.config.ts`. Paths are now compared in
|
|
124
|
+
one canonical repository-relative form (no `./`), by whole path segment
|
|
125
|
+
(`src` never owns `src2/x`), and the longest owning root wins, so nested
|
|
126
|
+
components keep precedence and a contract file still routes to its API. A
|
|
127
|
+
root unit's derived paths (`README.md`, `package.json`, markers, provenance,
|
|
128
|
+
verification config paths) are no longer recorded as `./README.md`, so
|
|
129
|
+
`impact` now recognises them as documentation and compiler inputs. Without a
|
|
130
|
+
root descriptor, root files stay unmatched. Graphs of repositories without a
|
|
131
|
+
root component are unchanged.
|
|
132
|
+
|
|
133
|
+
- `agentdoc scaffold --write` and `agentdoc init` now write the documentation
|
|
134
|
+
skeleton (`PRODUCT.md`, `ARCHITECTURE.md`, `CONSTRAINTS.md`, the ADR README)
|
|
135
|
+
to the configured `docs.product`, `docs.architecture`, `docs.constraints` and
|
|
136
|
+
`docs.adrDir` paths ([#49]). Both used to write to `docs/` even when the
|
|
137
|
+
configuration said otherwise, and `docs/` is often the product's own user
|
|
138
|
+
documentation. The paths are read through the same loader as `compile` and
|
|
139
|
+
`audit`. A role the configuration omits is listed under `notConfigured`
|
|
140
|
+
instead of falling back to `docs/`. An existing file is still skipped unless
|
|
141
|
+
`init --force` is given. A path that leaves the repository, including through
|
|
142
|
+
a symlink, fails with `AGENTDOC_PATH_ESCAPE` before anything is written.
|
|
143
|
+
Without a configuration, `init` uses the `agentdoc/docs/` defaults (see
|
|
144
|
+
[#59] under Changed).
|
|
145
|
+
|
|
146
|
+
- Files git ignores no longer enter the compiler's inputs or `inputHash`
|
|
147
|
+
([#45]). A local build writing into an ignored directory (`astro build`
|
|
148
|
+
filling `.astro/`, a `build/` tree, `*.log`) used to change the hash, so a
|
|
149
|
+
graph compiled after it failed `check` with `AGENTDOC_GRAPH_FRESHNESS` in CI
|
|
150
|
+
and in every clean checkout, while passing on the author's machine. Reads,
|
|
151
|
+
existence probes, listings and discovery now exclude git-ignored paths, with
|
|
152
|
+
git itself as the oracle: tracked files always count, and nested
|
|
153
|
+
`.gitignore`, `.git/info/exclude`, `core.excludesFile` and negations behave
|
|
154
|
+
exactly as in git. A non-ignored untracked file is still an input. Without
|
|
155
|
+
git the previous behaviour (filesystem minus `discovery.generatedDirs`)
|
|
156
|
+
holds. Committed graphs keep their hash. A descriptor or configuration
|
|
157
|
+
that points at a git-ignored file (for example a spec generated into an
|
|
158
|
+
ignored directory before `compile`) now fails with
|
|
159
|
+
`AGENTDOC_PATH_UNRESOLVED`, as it would in a clean checkout: commit the file
|
|
160
|
+
or stop ignoring it. The `git clean -fdX` before
|
|
161
|
+
`compile` documented as a workaround in lbframe/klozeo's `AGENTS.md` is no
|
|
162
|
+
longer needed.
|
|
163
|
+
|
|
164
|
+
- `npm publish` no longer auto-corrects `package.json`: the `bin` path is
|
|
165
|
+
committed in npm's normalized form, `bin/agentdoc.mjs` ([#55]).
|
|
166
|
+
|
|
167
|
+
- All routing evaluation systems and the noise closure now share fact labels
|
|
168
|
+
([#46]). Corrected targeted micro-recall is 100/101 (0.9901), rather than
|
|
169
|
+
the previously reported gap. Search matches catalog recall in 21/30 scenarios;
|
|
170
|
+
measurements and limitations are in `docs/BENCHMARK.md`. Normalized full fact
|
|
171
|
+
counts fall below the existing size floor, so aggregate size reduction is
|
|
172
|
+
reported as unmeasured. Thresholds, scenarios and core routing are unchanged.
|
|
173
|
+
|
|
174
|
+
- **Withdrawn benchmark claim.** The README, `docs/BENCHMARK.md` and the 1.2.0
|
|
175
|
+
release notes reported that routing retrieves 100% of the facts a change
|
|
176
|
+
needs against 58% for a baseline that reads only the touched component, with
|
|
177
|
+
noise under 9% against 40%, and an answer about 70% smaller than the whole
|
|
178
|
+
catalog. The gap came from the evaluation, not from
|
|
179
|
+
routing: the baseline already held almost every expected fact under a
|
|
180
|
+
different label, and it read relations from the compiled graph that an agent
|
|
181
|
+
without AgentDoc does not have. The size figure counted the same component
|
|
182
|
+
once per relation label; deduplicated, every corpus falls below the size
|
|
183
|
+
floor. Both claims are
|
|
184
|
+
removed. A baseline that finds
|
|
185
|
+
relations with search alone is now measured in the benchmark. The
|
|
186
|
+
[correction](docs/BENCHMARK.md#correction-the-fact-recall-comparison-is-withdrawn)
|
|
187
|
+
gives the details.
|
|
188
|
+
|
|
12
189
|
## [1.2.0] - 2026-10-27
|
|
13
190
|
|
|
14
191
|
Rust and Python are now supported at the same level as Node and Go. Before
|
|
@@ -242,7 +419,9 @@ First public release.
|
|
|
242
419
|
for project health.
|
|
243
420
|
- An Agent Skill under `skills/agent-doc-system/`.
|
|
244
421
|
|
|
245
|
-
[Unreleased]: https://github.com/lbframe/agent-doc-system/compare/v1.
|
|
422
|
+
[Unreleased]: https://github.com/lbframe/agent-doc-system/compare/v1.4.0...HEAD
|
|
423
|
+
[1.4.0]: https://github.com/lbframe/agent-doc-system/compare/v1.3.0...v1.4.0
|
|
424
|
+
[1.3.0]: https://github.com/lbframe/agent-doc-system/compare/v1.2.0...v1.3.0
|
|
246
425
|
[1.2.0]: https://github.com/lbframe/agent-doc-system/compare/v1.1.0...v1.2.0
|
|
247
426
|
[1.1.0]: https://github.com/lbframe/agent-doc-system/compare/v1.0.0...v1.1.0
|
|
248
427
|
[1.0.0]: https://github.com/lbframe/agent-doc-system/releases/tag/v1.0.0
|
|
@@ -267,3 +446,11 @@ First public release.
|
|
|
267
446
|
[#40]: https://github.com/lbframe/agent-doc-system/issues/40
|
|
268
447
|
[#41]: https://github.com/lbframe/agent-doc-system/issues/41
|
|
269
448
|
[#43]: https://github.com/lbframe/agent-doc-system/issues/43
|
|
449
|
+
[#45]: https://github.com/lbframe/agent-doc-system/issues/45
|
|
450
|
+
[#46]: https://github.com/lbframe/agent-doc-system/issues/46
|
|
451
|
+
[#49]: https://github.com/lbframe/agent-doc-system/issues/49
|
|
452
|
+
[#50]: https://github.com/lbframe/agent-doc-system/issues/50
|
|
453
|
+
[#51]: https://github.com/lbframe/agent-doc-system/issues/51
|
|
454
|
+
[#55]: https://github.com/lbframe/agent-doc-system/issues/55
|
|
455
|
+
[#59]: https://github.com/lbframe/agent-doc-system/issues/59
|
|
456
|
+
[#63]: https://github.com/lbframe/agent-doc-system/issues/63
|
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ What every adapter derives, and what it deliberately cannot see, is in [docs/ADA
|
|
|
98
98
|
|
|
99
99
|
Measured, reproducible, and with no model in the loop. Method and caveats are in [docs/BENCHMARK.md](docs/BENCHMARK.md).
|
|
100
100
|
|
|
101
|
-
- **The
|
|
101
|
+
- **The checks a change needs, not the whole suite.** Across 30 routing scenarios (Node, Go, Rust, Python), every check the change requires is in the answer, and that answer holds about half of the repository's checks (29% on the largest corpus), not all of them. The thresholds were fixed before the first run and have not moved since.
|
|
102
102
|
- **Seconds to map a real repository.** A fresh `init` → `scaffold` → `compile` takes 1.0–4.6 s on four public repositories, from 141 to 5,142 files (axum, pydantic-ai, full-stack-fastapi-template, create-t3-turbo). No edit was needed beyond choosing each component's placement.
|
|
103
103
|
- **A briefing, not a dump.** A `query` answer is about 100–260 tokens: what the change reaches, what to read, and which checks to run.
|
|
104
104
|
- **The same graph every time.** Compiles are byte-identical, and CI can verify it.
|
package/adapters/cache.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Cache adapter: Redis / in-memory cache client evidence as a derived binding.
|
|
2
2
|
import { Adapter } from "./registry.mjs";
|
|
3
|
+
import { joinPath } from "../core/paths.mjs";
|
|
3
4
|
|
|
4
5
|
const CACHE_HINT = /(@upstash\/redis|ioredis|redis\.Redis|redis|go-redis|redis\.go|valkey|memcache|@memcached)/i;
|
|
5
6
|
|
|
@@ -11,12 +12,12 @@ export class CacheAdapter extends Adapter {
|
|
|
11
12
|
extract(ctx, unit) {
|
|
12
13
|
const { repo, prov, compRef } = ctx;
|
|
13
14
|
const evFile = unit.goModule
|
|
14
|
-
? unit.root
|
|
15
|
+
? joinPath(unit.root, "go.mod")
|
|
15
16
|
: unit.pythonManifest
|
|
16
17
|
? unit.pythonManifest
|
|
17
|
-
: repo.exists(unit.root
|
|
18
|
-
? unit.root
|
|
19
|
-
: unit.root
|
|
18
|
+
: repo.exists(joinPath(unit.root, "package.json"))
|
|
19
|
+
? joinPath(unit.root, "package.json")
|
|
20
|
+
: joinPath(unit.root, "Cargo.toml");
|
|
20
21
|
if (!repo.exists(evFile)) return {};
|
|
21
22
|
const text = repo.readText(evFile);
|
|
22
23
|
if (!CACHE_HINT.test(text)) return {};
|
package/adapters/database.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// library shipped migrations but explicitly did not own a database.
|
|
10
10
|
import { Adapter } from "./registry.mjs";
|
|
11
11
|
import { firstExistingFile, findMigrationDirs } from "../core/sourcescan.mjs";
|
|
12
|
+
import { canonicalPath, joinPath } from "../core/paths.mjs";
|
|
12
13
|
|
|
13
14
|
const ORM_CONFIG = [
|
|
14
15
|
"drizzle.config.ts", "drizzle.config.js", "drizzle.config.mjs",
|
|
@@ -37,8 +38,8 @@ export class DatabaseAdapter extends Adapter {
|
|
|
37
38
|
}
|
|
38
39
|
const alembicDir = migrationDirs.find((d) => d === "alembic/versions" || d.endsWith("/alembic/versions"));
|
|
39
40
|
if (alembicDir) {
|
|
40
|
-
const ini = root
|
|
41
|
-
const pyproject = root
|
|
41
|
+
const ini = joinPath(root, "alembic.ini");
|
|
42
|
+
const pyproject = joinPath(root, "pyproject.toml");
|
|
42
43
|
if (repo.exists(ini)) {
|
|
43
44
|
const text = repo.readText(ini);
|
|
44
45
|
if (/^\s*\[alembic\]/m.test(text) || /^\s*script_location\s*=/m.test(text)) evidencePath = ini;
|
|
@@ -47,6 +48,12 @@ export class DatabaseAdapter extends Adapter {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
const pr = prov.add("observed", evidencePath, "migration-extractor", ["der:" + compRef + ":/bindings"]);
|
|
51
|
+
// Where this unit's migrations live, recorded in the graph so `impact`
|
|
52
|
+
// classifies a change from the graph rather than from a list of names.
|
|
53
|
+
// Authority over a directory, never ownership of a database: these records
|
|
54
|
+
// create no binding.
|
|
55
|
+
const recorded = recordMigrationDirs(ctx, unit, migrationDirs, ormConfig, pr);
|
|
56
|
+
const withDirs = (out) => (recorded.length ? { ...out, migrationDirs: recorded } : out);
|
|
50
57
|
|
|
51
58
|
// A component that both migrates and opens a connection owns the database.
|
|
52
59
|
const opensConnection = hasConnectionEvidence(ctx, unit);
|
|
@@ -65,7 +72,7 @@ export class DatabaseAdapter extends Adapter {
|
|
|
65
72
|
evidenceClass: "DERIVED", confidence: "candidate", provRecs: [pr],
|
|
66
73
|
semantics: "a schema exists here, but which component owns the logical database is unresolved",
|
|
67
74
|
});
|
|
68
|
-
return { migrationAuthority: evidencePath };
|
|
75
|
+
return withDirs({ migrationAuthority: evidencePath });
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
const name = compRef.split("/")[1];
|
|
@@ -88,7 +95,7 @@ export class DatabaseAdapter extends Adapter {
|
|
|
88
95
|
evidenceClass: "DERIVED", confidence: "candidate", provRecs: [pr],
|
|
89
96
|
semantics: "this component opens a database connection, but no authored Resource claims ownership",
|
|
90
97
|
});
|
|
91
|
-
return { migrationAuthority: evidencePath };
|
|
98
|
+
return withDirs({ migrationAuthority: evidencePath });
|
|
92
99
|
}
|
|
93
100
|
ctx.addFact(compRef, "resource.ownership", target.name, {
|
|
94
101
|
evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
|
|
@@ -100,10 +107,10 @@ export class DatabaseAdapter extends Adapter {
|
|
|
100
107
|
const client = ctx.clientKeyForResourceType("logical-database");
|
|
101
108
|
const attrs = client ? { via: "migrations+connection", client } : { via: "migrations+connection" };
|
|
102
109
|
ctx.addEdge("usesResource", compRef, target.ref, attrs, [pr], "DERIVED");
|
|
103
|
-
return {
|
|
110
|
+
return withDirs({
|
|
104
111
|
migrationAuthority: evidencePath,
|
|
105
112
|
bindings: [{ kind: "database", logicalResourceRef: target.ref, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) }],
|
|
106
|
-
};
|
|
113
|
+
});
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -128,3 +135,145 @@ function findDatabaseResource(ctx, name) {
|
|
|
128
135
|
);
|
|
129
136
|
return cands.length === 1 ? cands[0] : null;
|
|
130
137
|
}
|
|
138
|
+
|
|
139
|
+
// ── migration directories ───────────────────────────────────────────────────
|
|
140
|
+
//
|
|
141
|
+
// Two routes, each with its own evidence:
|
|
142
|
+
// - a directory named by convention (`migrations`, `migration`,
|
|
143
|
+
// `alembic/versions`), evidenced by a file inside it;
|
|
144
|
+
// - Drizzle Kit's `out`, evidenced by the config file that states it.
|
|
145
|
+
// Prisma Migrate writes to `prisma/migrations`, which the first route covers.
|
|
146
|
+
function recordMigrationDirs(ctx, unit, conventionDirs, ormConfig, pr) {
|
|
147
|
+
const { repo, prov, compRef } = ctx;
|
|
148
|
+
const subject = "der:" + compRef + ":/migrationDirs";
|
|
149
|
+
const byPath = new Map();
|
|
150
|
+
const add = (dir, via, rec) => {
|
|
151
|
+
if (!byPath.has(dir)) byPath.set(dir, { path: dir, via: new Set(), _provs: new Set() });
|
|
152
|
+
byPath.get(dir).via.add(via);
|
|
153
|
+
byPath.get(dir)._provs.add(rec);
|
|
154
|
+
};
|
|
155
|
+
for (const dir of conventionDirs) {
|
|
156
|
+
// A directory with no file in the input set has nothing a change could
|
|
157
|
+
// touch, and nothing to cite as evidence.
|
|
158
|
+
const file = repo.walkUnit(unit.root, dir)[0];
|
|
159
|
+
if (!file) continue;
|
|
160
|
+
let rec;
|
|
161
|
+
if (file === pr.path) {
|
|
162
|
+
rec = pr;
|
|
163
|
+
pr.subjects.add(subject);
|
|
164
|
+
} else {
|
|
165
|
+
rec = prov.add("observed", file, "migration-extractor", [subject]);
|
|
166
|
+
}
|
|
167
|
+
add(dir, /(^|\/)alembic\/versions$/.test(dir) ? "alembic-versions" : "directory-name", rec);
|
|
168
|
+
}
|
|
169
|
+
if (ormConfig && /(^|\/)drizzle\.config\.[cm]?[jt]s$/.test(ormConfig)) {
|
|
170
|
+
const dir = drizzleOutDir(ctx, unit, ormConfig);
|
|
171
|
+
if (dir) add(dir, "drizzle-config", prov.add("observed", ormConfig, "drizzle-config-extractor", [subject]));
|
|
172
|
+
}
|
|
173
|
+
return [...byPath.values()]
|
|
174
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1))
|
|
175
|
+
.map((d) => ({ path: d.path, via: [...d.via].sort(), provenanceIds: null, _provs: d._provs }));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Drizzle Kit writes migrations to `out`, `./drizzle` when absent. The config
|
|
179
|
+
// is code, and it is never executed: only a string literal is read. Anything
|
|
180
|
+
// else — an expression, an environment variable, a spread that could carry
|
|
181
|
+
// `out`, several `out` keys — is reported and not guessed, because a wrong
|
|
182
|
+
// directory would route migrations to the wrong database silently.
|
|
183
|
+
function drizzleOutDir(ctx, unit, configPath) {
|
|
184
|
+
const { repo, compRef } = ctx;
|
|
185
|
+
const unresolved = (why) => {
|
|
186
|
+
ctx.diagnostics.push({
|
|
187
|
+
severity: "warning",
|
|
188
|
+
code: "AGENTDOC_MIGRATION_DIR_UNRESOLVED",
|
|
189
|
+
subject: compRef,
|
|
190
|
+
refs: [compRef],
|
|
191
|
+
message:
|
|
192
|
+
"Drizzle migration directory in '" + configPath + "' is not recorded: " + why + ". " +
|
|
193
|
+
"Declare it with spec.migrationPaths on the logical-database Resource it migrates",
|
|
194
|
+
paths: [configPath],
|
|
195
|
+
});
|
|
196
|
+
return null;
|
|
197
|
+
};
|
|
198
|
+
const parsed = drizzleOut(repo.readText(configPath));
|
|
199
|
+
if (parsed.error) return unresolved(parsed.error);
|
|
200
|
+
const value = parsed.value ?? "./drizzle";
|
|
201
|
+
// Relative to the directory drizzle-kit runs from, which is the config's own
|
|
202
|
+
// directory in the documented layout (and the unit root here).
|
|
203
|
+
const base = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/")) : ".";
|
|
204
|
+
const dir = value.startsWith("/") ? null : canonicalPath(joinPath(base, value));
|
|
205
|
+
if (!dir || dir === "." || dir === canonicalPath(unit.root)) {
|
|
206
|
+
return unresolved("out " + JSON.stringify(value) + " is not a directory inside the unit at '" + unit.root + "'");
|
|
207
|
+
}
|
|
208
|
+
if (!repo.ownsUnitPath(unit.root, dir)) {
|
|
209
|
+
return unresolved("out " + JSON.stringify(value) + " is outside the unit at '" + unit.root + "'");
|
|
210
|
+
}
|
|
211
|
+
// Not generated yet: nothing to classify. The probe is an input, so the
|
|
212
|
+
// first generated migration invalidates the graph.
|
|
213
|
+
if (!repo.isDir(dir)) return null;
|
|
214
|
+
return dir;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Static read of `out` from a Drizzle config. Comments are removed and string
|
|
218
|
+
// literals set aside first, so neither `// out: "x"` nor a string containing
|
|
219
|
+
// "out:" is mistaken for the key.
|
|
220
|
+
export function drizzleOut(text) {
|
|
221
|
+
const strings = [];
|
|
222
|
+
let code = "";
|
|
223
|
+
for (let i = 0; i < text.length; ) {
|
|
224
|
+
const c = text[i];
|
|
225
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
226
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
230
|
+
const end = text.indexOf("*/", i + 2);
|
|
231
|
+
i = end < 0 ? text.length : end + 2;
|
|
232
|
+
code += " ";
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
236
|
+
let j = i + 1;
|
|
237
|
+
let value = "";
|
|
238
|
+
let closed = false;
|
|
239
|
+
while (j < text.length) {
|
|
240
|
+
if (text[j] === "\\") {
|
|
241
|
+
value += text[j + 1] ?? "";
|
|
242
|
+
j += 2;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (text[j] === c) { closed = true; break; }
|
|
246
|
+
if (c !== "`" && text[j] === "\n") break;
|
|
247
|
+
value += text[j];
|
|
248
|
+
j++;
|
|
249
|
+
}
|
|
250
|
+
if (!closed) return { error: "the config could not be read statically" };
|
|
251
|
+
// A template with a substitution is an expression, not a literal.
|
|
252
|
+
strings.push(c === "`" && value.includes("${") ? null : value);
|
|
253
|
+
code += "\u0000" + (strings.length - 1) + "\u0000";
|
|
254
|
+
i = j + 1;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
code += c;
|
|
258
|
+
i++;
|
|
259
|
+
}
|
|
260
|
+
const hits = [];
|
|
261
|
+
for (const m of code.matchAll(/(?:^|[{,\s])(?:out|\u0000(\d+)\u0000)\s*(:|[,}])/g)) {
|
|
262
|
+
// A quoted key counts only when it says "out".
|
|
263
|
+
if (m[1] !== undefined && strings[Number(m[1])] !== "out") continue;
|
|
264
|
+
hits.push(m);
|
|
265
|
+
}
|
|
266
|
+
if (hits.length === 0) {
|
|
267
|
+
// A spread could bring `out` in from elsewhere; the default is then a guess.
|
|
268
|
+
if (/\.\.\.\s*[A-Za-z_$(]/.test(code)) return { error: "the config spreads another object that may set out" };
|
|
269
|
+
return { value: null };
|
|
270
|
+
}
|
|
271
|
+
if (hits.length > 1) return { error: "out is set more than once" };
|
|
272
|
+
const m = hits[0];
|
|
273
|
+
if (m[2] !== ":") return { error: "out is a shorthand property, not a string literal" };
|
|
274
|
+
const rest = code.slice(m.index + m[0].length);
|
|
275
|
+
const lit = /^\s*\u0000(\d+)\u0000\s*(?:[,}]|$)/.exec(rest);
|
|
276
|
+
const value = lit ? strings[Number(lit[1])] : null;
|
|
277
|
+
if (typeof value !== "string" || value.trim() === "") return { error: "out is not a string literal" };
|
|
278
|
+
return { value };
|
|
279
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Only the file's existence is treated as evidence; no value from these files
|
|
3
3
|
// is ever reported as deployed state.
|
|
4
4
|
import { Adapter } from "./registry.mjs";
|
|
5
|
+
import { joinPath } from "../core/paths.mjs";
|
|
5
6
|
|
|
6
7
|
const DEPLOY_FILES = [
|
|
7
8
|
"fly.toml", "app.yaml", "render.yaml", "railway.json", "vercel.json",
|
|
@@ -16,7 +17,7 @@ export class DeployConfigAdapter extends Adapter {
|
|
|
16
17
|
}
|
|
17
18
|
detect(ctx, root) {
|
|
18
19
|
for (const n of DEPLOY_FILES) {
|
|
19
|
-
if (ctx.repo.exists(root
|
|
20
|
+
if (ctx.repo.exists(joinPath(root, n))) return { path: joinPath(root, n), shape: "deployable" };
|
|
20
21
|
}
|
|
21
22
|
return null;
|
|
22
23
|
}
|
|
@@ -24,7 +25,7 @@ export class DeployConfigAdapter extends Adapter {
|
|
|
24
25
|
const { repo, prov, compRef } = ctx;
|
|
25
26
|
const out = { deployConfigs: [] };
|
|
26
27
|
for (const n of DEPLOY_FILES) {
|
|
27
|
-
const p = unit.root
|
|
28
|
+
const p = joinPath(unit.root, n);
|
|
28
29
|
if (!repo.exists(p)) continue;
|
|
29
30
|
prov.add("observed", p, "deploy-extractor", ["der:" + compRef + ":/artifact"]);
|
|
30
31
|
out.deployConfigs.push(p);
|
package/adapters/dockerfile.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Dockerfile adapter: a Dockerfile is deployable-artifact evidence.
|
|
2
2
|
import { Adapter } from "./registry.mjs";
|
|
3
|
+
import { joinPath } from "../core/paths.mjs";
|
|
3
4
|
|
|
4
5
|
export class DockerfileAdapter extends Adapter {
|
|
5
6
|
static adapterName = "dockerfile";
|
|
@@ -8,7 +9,7 @@ export class DockerfileAdapter extends Adapter {
|
|
|
8
9
|
}
|
|
9
10
|
detect(ctx, root) {
|
|
10
11
|
for (const n of ["Dockerfile", "Dockerfile.prod", "Containerfile"]) {
|
|
11
|
-
if (ctx.repo.exists(root
|
|
12
|
+
if (ctx.repo.exists(joinPath(root, n))) return { path: joinPath(root, n), shape: "deployable" };
|
|
12
13
|
}
|
|
13
14
|
return null;
|
|
14
15
|
}
|
|
@@ -16,7 +17,7 @@ export class DockerfileAdapter extends Adapter {
|
|
|
16
17
|
const { repo, prov, compRef } = ctx;
|
|
17
18
|
const out = { dockerfiles: [] };
|
|
18
19
|
for (const n of ["Dockerfile", "Dockerfile.prod", "Containerfile"]) {
|
|
19
|
-
const p = unit.root
|
|
20
|
+
const p = joinPath(unit.root, n);
|
|
20
21
|
if (!repo.exists(p)) continue;
|
|
21
22
|
prov.add("observed", p, "deploy-extractor", ["der:" + compRef + ":/artifact"]);
|
|
22
23
|
out.dockerfiles.push(p);
|
package/adapters/generic.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { Adapter } from "./registry.mjs";
|
|
8
8
|
import { resolveClientResources } from "./resource-client.mjs";
|
|
9
9
|
import { sourceFiles } from "../core/sourcescan.mjs";
|
|
10
|
+
import { joinPath } from "../core/paths.mjs";
|
|
10
11
|
|
|
11
12
|
// Conservative: a health route is an absolute string literal that looks like a
|
|
12
13
|
// liveness or readiness endpoint. Anything more specific belongs to a
|
|
@@ -50,7 +51,7 @@ export class GenericAdapter extends Adapter {
|
|
|
50
51
|
// Provenance must name a file that exists. The dependency evidence is the
|
|
51
52
|
// manifest when there is one, and otherwise the unit's descriptor.
|
|
52
53
|
const manifest = ["package.json", "go.mod", "Cargo.toml", "pyproject.toml", "pom.xml"]
|
|
53
|
-
.map((n) => unit.root
|
|
54
|
+
.map((n) => joinPath(unit.root, n))
|
|
54
55
|
.find((p) => repo.exists(p)) || ctx.componentEntity().file;
|
|
55
56
|
const clientProv = prov.add("observed", manifest, "client-resolver", ["rel:pending", "der:" + compRef + ":/bindings"]);
|
|
56
57
|
resolveClientResources(ctx, unit, compRef, clientProv);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { Adapter } from "./registry.mjs";
|
|
6
6
|
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
7
|
+
import { ownsPath } from "../core/paths.mjs";
|
|
7
8
|
|
|
8
9
|
export class GitHubActionsAdapter extends Adapter {
|
|
9
10
|
static adapterName = "github-actions";
|
|
@@ -224,7 +225,7 @@ function literalPath(value) {
|
|
|
224
225
|
const p = path.posix.normalize(value);
|
|
225
226
|
return p === ".." || p.startsWith("../") ? null : p.replace(/\/$/, "");
|
|
226
227
|
}
|
|
227
|
-
function within(p, root) { return root
|
|
228
|
+
function within(p, root) { return ownsPath(root, p); }
|
|
228
229
|
function shellQuote(p) { return /^[A-Za-z0-9_./-]+$/.test(p) ? p : "'" + p.replace(/'/g, "'\\''") + "'"; }
|
|
229
230
|
|
|
230
231
|
function scopedTo(unit, args, cwd, filters, units, repo) {
|
package/adapters/go.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Go adapter: go.mod / go.work as unit markers, module graph as build
|
|
2
2
|
// relations, and the go toolchain's own verification commands.
|
|
3
3
|
import { Adapter } from "./registry.mjs";
|
|
4
|
+
import { joinPath } from "../core/paths.mjs";
|
|
4
5
|
|
|
5
6
|
export class GoAdapter extends Adapter {
|
|
6
7
|
static adapterName = "go";
|
|
@@ -26,13 +27,13 @@ export class GoAdapter extends Adapter {
|
|
|
26
27
|
return out;
|
|
27
28
|
}
|
|
28
29
|
detect(ctx, root) {
|
|
29
|
-
const p = root
|
|
30
|
+
const p = joinPath(root, "go.mod");
|
|
30
31
|
if (!ctx.repo.exists(p)) return null;
|
|
31
|
-
return { path: p, shape: ctx.repo.isDir(root
|
|
32
|
+
return { path: p, shape: ctx.repo.isDir(joinPath(root, "cmd")) ? "deployable" : "importable" };
|
|
32
33
|
}
|
|
33
34
|
extract(ctx, unit) {
|
|
34
35
|
const { repo, prov, compRef } = ctx;
|
|
35
|
-
const p = unit.root
|
|
36
|
+
const p = joinPath(unit.root, "go.mod");
|
|
36
37
|
if (!repo.exists(p)) return {};
|
|
37
38
|
const derived = { languages: new Set(["go"]), runtimes: new Set(["go"]) };
|
|
38
39
|
const text = repo.readText(p);
|
package/adapters/node.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Node adapter: package.json as the unit marker and manifest authority.
|
|
2
2
|
import { Adapter } from "./registry.mjs";
|
|
3
|
+
import { joinPath } from "../core/paths.mjs";
|
|
3
4
|
|
|
4
5
|
// A script name maps to a verification tier. Deliberately conservative and
|
|
5
6
|
// project-neutral: a script that does not look like a check is not a check.
|
|
@@ -20,7 +21,7 @@ export class NodeAdapter extends Adapter {
|
|
|
20
21
|
super({ name: "node", version: "1.0.0", kind: "unit-marker" });
|
|
21
22
|
}
|
|
22
23
|
detect(ctx, root) {
|
|
23
|
-
const p = root
|
|
24
|
+
const p = joinPath(root, "package.json");
|
|
24
25
|
if (!ctx.repo.exists(p)) return null;
|
|
25
26
|
let pkg;
|
|
26
27
|
try {
|
|
@@ -34,7 +35,7 @@ export class NodeAdapter extends Adapter {
|
|
|
34
35
|
extract(ctx, unit) {
|
|
35
36
|
const { repo, prov, compRef } = ctx;
|
|
36
37
|
const root = unit.root;
|
|
37
|
-
const manifestPath = root
|
|
38
|
+
const manifestPath = joinPath(root, "package.json");
|
|
38
39
|
if (!repo.exists(manifestPath)) return {};
|
|
39
40
|
const derived = { languages: new Set(["javascript"]), runtimes: new Set(["node"]) };
|
|
40
41
|
const pkg = unit.pkg || repo.readJson(manifestPath);
|
package/adapters/rust.mjs
CHANGED
|
@@ -225,6 +225,9 @@ function childDirectoryNames(repo, dir) {
|
|
|
225
225
|
if (ent.isSymbolicLink() || !ent.isDirectory()) continue;
|
|
226
226
|
if (!ent.name || ent.name.startsWith(".")) continue;
|
|
227
227
|
if (generated.has(ent.name) || WORKSPACE_SKIP_DIRS.has(ent.name)) continue;
|
|
228
|
+
// Each child is probed for Cargo.toml, and a probe is an input: a
|
|
229
|
+
// git-ignored directory must not even be visited (issue #45).
|
|
230
|
+
if (repo.isIgnored && repo.isIgnored(dir ? dir + "/" + ent.name : ent.name)) continue;
|
|
228
231
|
names.push(ent.name);
|
|
229
232
|
}
|
|
230
233
|
names.sort();
|
package/adapters/turborepo.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// verification answer available for a monorepo unit.
|
|
5
5
|
import { Adapter } from "./registry.mjs";
|
|
6
6
|
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
7
|
+
import { isRepoRoot, joinPath } from "../core/paths.mjs";
|
|
7
8
|
|
|
8
9
|
// Whether a declared task is a check is decided by `taskTier` alone, not by a
|
|
9
10
|
// second, narrower pattern. Two lists meant two places to disagree: a task that
|
|
@@ -19,7 +20,7 @@ export class TurborepoAdapter extends Adapter {
|
|
|
19
20
|
super({ name: "turborepo", version: "1.0.0", kind: "ci" });
|
|
20
21
|
}
|
|
21
22
|
detect(ctx, root) {
|
|
22
|
-
const p = root
|
|
23
|
+
const p = joinPath(root, "turbo.json");
|
|
23
24
|
if (!ctx.repo.exists(p)) return null;
|
|
24
25
|
return { path: p, shape: "importable" };
|
|
25
26
|
}
|
|
@@ -122,7 +123,7 @@ function taskTier(t) {
|
|
|
122
123
|
|
|
123
124
|
// A task graph normally lives at the workspace root, not in each package.
|
|
124
125
|
function nearestTurboConfig(repo, root) {
|
|
125
|
-
let dir = root;
|
|
126
|
+
let dir = isRepoRoot(root) ? "" : root;
|
|
126
127
|
for (let i = 0; i < 12; i++) {
|
|
127
128
|
const p = dir ? dir + "/turbo.json" : "turbo.json";
|
|
128
129
|
if (repo.exists(p)) return p;
|