agent-doc-system 1.2.0 → 1.3.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 +134 -1
- package/README.md +1 -1
- package/adapters/cache.mjs +5 -4
- package/adapters/database.mjs +3 -2
- 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/compile.mjs +7 -5
- package/core/derive.mjs +4 -3
- package/core/descriptors.mjs +3 -2
- package/core/discover.mjs +3 -2
- package/core/fsx.mjs +174 -42
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +11 -4
- 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 +1 -1
- package/docs/BENCHMARK.md +83 -21
- package/docs/CI.md +7 -1
- package/docs/EVALS.md +209 -17
- package/docs/MIGRATION.md +22 -4
- package/docs/PROVENANCE.md +6 -0
- package/docs/QUERY.md +25 -3
- package/docs/SPEC.md +47 -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/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,131 @@ refused by the next with `AGENTDOC_COMPILER_MISMATCH` until it is recompiled.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.3.0] - 2026-11-07
|
|
13
|
+
|
|
14
|
+
A graph compiled on your machine now passes `check` in CI and in a clean
|
|
15
|
+
checkout. Files git ignores no longer count as compiler inputs, so a local
|
|
16
|
+
build that fills an ignored directory no longer changes `inputHash`. A
|
|
17
|
+
component whose descriptor sits at the repository root now owns the root files
|
|
18
|
+
no other component owns, so `query` and `impact` route them. `init` and
|
|
19
|
+
`scaffold --write` no longer write into your product's `docs/`: they follow the
|
|
20
|
+
configured `docs.*` paths, and a new install without a configuration gets its
|
|
21
|
+
skeleton under `agentdoc/docs/`. Releases are now published from CI with npm
|
|
22
|
+
provenance, so `npm audit signatures` can verify that a release was built from
|
|
23
|
+
this repository. The benchmark claim made in the 1.2.0 release notes is
|
|
24
|
+
withdrawn, because its gap came from the evaluation and not from routing.
|
|
25
|
+
|
|
26
|
+
Before you upgrade:
|
|
27
|
+
|
|
28
|
+
- **Recompile committed graphs.** As with every release, a graph compiled by
|
|
29
|
+
1.2.0 is refused with `AGENTDOC_COMPILER_MISMATCH` until you run
|
|
30
|
+
`agentdoc compile` again.
|
|
31
|
+
- **A descriptor or configuration path that points at a git-ignored file now
|
|
32
|
+
fails** with `AGENTDOC_PATH_UNRESOLVED`, as it would in a clean checkout.
|
|
33
|
+
Commit the file or stop ignoring it ([#45]).
|
|
34
|
+
- **New installs write the documentation skeleton to `agentdoc/docs/`**
|
|
35
|
+
instead of `docs/`. A configuration that declares `docs.*` keeps its paths,
|
|
36
|
+
and no existing file is moved ([#59]).
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- A deterministic `baseline-search` routing evaluation using working-tree file
|
|
41
|
+
reads and text search, without compiled relations ([#46]). CLI reports compare
|
|
42
|
+
catalog, corrected targeted and search recall/noise/check selectivity per
|
|
43
|
+
scenario, corpus and aggregate; `docs/EVALS.md` documents every heuristic.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- Releases are now published only through a manual GitHub Actions workflow,
|
|
48
|
+
using npm trusted publishing (no token) with provenance, so `npm audit
|
|
49
|
+
signatures` can verify that a release was built from this repository
|
|
50
|
+
([#51]). `RELEASING.md` in the repository describes the process.
|
|
51
|
+
|
|
52
|
+
- **Behaviour change for new installs only:** without a configuration,
|
|
53
|
+
`agentdoc init` (and `scaffold --write`) now writes the documentation
|
|
54
|
+
skeleton to `agentdoc/docs/PRODUCT.md`, `agentdoc/docs/ARCHITECTURE.md`,
|
|
55
|
+
`agentdoc/docs/CONSTRAINTS.md` and `agentdoc/docs/adr/`, and records those
|
|
56
|
+
paths in the `docs:` block of the generated `agentdoc/agentdoc.config.yaml`
|
|
57
|
+
([#59]). It used to write to `docs/`, which in many repositories is the
|
|
58
|
+
product's own user documentation and the source of a published docs site
|
|
59
|
+
(MkDocs, Docusaurus, GitHub Pages). Authored AgentDoc material now lives in
|
|
60
|
+
`agentdoc/`; generated output stays in `.agentdoc/`. Existing configurations
|
|
61
|
+
are unaffected: a configuration that declares `docs.*` keeps its paths,
|
|
62
|
+
`docs/...` included, and no existing file is moved. To keep `docs/` on a new
|
|
63
|
+
install, declare `docs.product`, `docs.architecture`, `docs.constraints` and
|
|
64
|
+
`docs.adrDir` in `agentdoc/agentdoc.config.yaml` before running `init`.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- A component whose descriptor sits at the repository root now owns the files
|
|
69
|
+
no nested component owns ([#50]). Before, `query vite.config.ts` failed with
|
|
70
|
+
`AGENTDOC_REF_UNRESOLVED` and `impact` listed the file in `unmatchedFiles`,
|
|
71
|
+
for both `vite.config.ts` and `./vite.config.ts`. Paths are now compared in
|
|
72
|
+
one canonical repository-relative form (no `./`), by whole path segment
|
|
73
|
+
(`src` never owns `src2/x`), and the longest owning root wins, so nested
|
|
74
|
+
components keep precedence and a contract file still routes to its API. A
|
|
75
|
+
root unit's derived paths (`README.md`, `package.json`, markers, provenance,
|
|
76
|
+
verification config paths) are no longer recorded as `./README.md`, so
|
|
77
|
+
`impact` now recognises them as documentation and compiler inputs. Without a
|
|
78
|
+
root descriptor, root files stay unmatched. Graphs of repositories without a
|
|
79
|
+
root component are unchanged.
|
|
80
|
+
|
|
81
|
+
- `agentdoc scaffold --write` and `agentdoc init` now write the documentation
|
|
82
|
+
skeleton (`PRODUCT.md`, `ARCHITECTURE.md`, `CONSTRAINTS.md`, the ADR README)
|
|
83
|
+
to the configured `docs.product`, `docs.architecture`, `docs.constraints` and
|
|
84
|
+
`docs.adrDir` paths ([#49]). Both used to write to `docs/` even when the
|
|
85
|
+
configuration said otherwise, and `docs/` is often the product's own user
|
|
86
|
+
documentation. The paths are read through the same loader as `compile` and
|
|
87
|
+
`audit`. A role the configuration omits is listed under `notConfigured`
|
|
88
|
+
instead of falling back to `docs/`. An existing file is still skipped unless
|
|
89
|
+
`init --force` is given. A path that leaves the repository, including through
|
|
90
|
+
a symlink, fails with `AGENTDOC_PATH_ESCAPE` before anything is written.
|
|
91
|
+
Without a configuration, `init` uses the `agentdoc/docs/` defaults (see
|
|
92
|
+
[#59] under Changed).
|
|
93
|
+
|
|
94
|
+
- Files git ignores no longer enter the compiler's inputs or `inputHash`
|
|
95
|
+
([#45]). A local build writing into an ignored directory (`astro build`
|
|
96
|
+
filling `.astro/`, a `build/` tree, `*.log`) used to change the hash, so a
|
|
97
|
+
graph compiled after it failed `check` with `AGENTDOC_GRAPH_FRESHNESS` in CI
|
|
98
|
+
and in every clean checkout, while passing on the author's machine. Reads,
|
|
99
|
+
existence probes, listings and discovery now exclude git-ignored paths, with
|
|
100
|
+
git itself as the oracle: tracked files always count, and nested
|
|
101
|
+
`.gitignore`, `.git/info/exclude`, `core.excludesFile` and negations behave
|
|
102
|
+
exactly as in git. A non-ignored untracked file is still an input. Without
|
|
103
|
+
git the previous behaviour (filesystem minus `discovery.generatedDirs`)
|
|
104
|
+
holds. Committed graphs keep their hash. A descriptor or configuration
|
|
105
|
+
that points at a git-ignored file (for example a spec generated into an
|
|
106
|
+
ignored directory before `compile`) now fails with
|
|
107
|
+
`AGENTDOC_PATH_UNRESOLVED`, as it would in a clean checkout: commit the file
|
|
108
|
+
or stop ignoring it. The `git clean -fdX` before
|
|
109
|
+
`compile` documented as a workaround in lbframe/klozeo's `AGENTS.md` is no
|
|
110
|
+
longer needed.
|
|
111
|
+
|
|
112
|
+
- `npm publish` no longer auto-corrects `package.json`: the `bin` path is
|
|
113
|
+
committed in npm's normalized form, `bin/agentdoc.mjs` ([#55]).
|
|
114
|
+
|
|
115
|
+
- All routing evaluation systems and the noise closure now share fact labels
|
|
116
|
+
([#46]). Corrected targeted micro-recall is 100/101 (0.9901), rather than
|
|
117
|
+
the previously reported gap. Search matches catalog recall in 21/30 scenarios;
|
|
118
|
+
measurements and limitations are in `docs/BENCHMARK.md`. Normalized full fact
|
|
119
|
+
counts fall below the existing size floor, so aggregate size reduction is
|
|
120
|
+
reported as unmeasured. Thresholds, scenarios and core routing are unchanged.
|
|
121
|
+
|
|
122
|
+
- **Withdrawn benchmark claim.** The README, `docs/BENCHMARK.md` and the 1.2.0
|
|
123
|
+
release notes reported that routing retrieves 100% of the facts a change
|
|
124
|
+
needs against 58% for a baseline that reads only the touched component, with
|
|
125
|
+
noise under 9% against 40%, and an answer about 70% smaller than the whole
|
|
126
|
+
catalog. The gap came from the evaluation, not from
|
|
127
|
+
routing: the baseline already held almost every expected fact under a
|
|
128
|
+
different label, and it read relations from the compiled graph that an agent
|
|
129
|
+
without AgentDoc does not have. The size figure counted the same component
|
|
130
|
+
once per relation label; deduplicated, every corpus falls below the size
|
|
131
|
+
floor. Both claims are
|
|
132
|
+
removed. A baseline that finds
|
|
133
|
+
relations with search alone is now measured in the benchmark. The
|
|
134
|
+
[correction](docs/BENCHMARK.md#correction-the-fact-recall-comparison-is-withdrawn)
|
|
135
|
+
gives the details.
|
|
136
|
+
|
|
12
137
|
## [1.2.0] - 2026-10-27
|
|
13
138
|
|
|
14
139
|
Rust and Python are now supported at the same level as Node and Go. Before
|
|
@@ -242,7 +367,8 @@ First public release.
|
|
|
242
367
|
for project health.
|
|
243
368
|
- An Agent Skill under `skills/agent-doc-system/`.
|
|
244
369
|
|
|
245
|
-
[Unreleased]: https://github.com/lbframe/agent-doc-system/compare/v1.
|
|
370
|
+
[Unreleased]: https://github.com/lbframe/agent-doc-system/compare/v1.3.0...HEAD
|
|
371
|
+
[1.3.0]: https://github.com/lbframe/agent-doc-system/compare/v1.2.0...v1.3.0
|
|
246
372
|
[1.2.0]: https://github.com/lbframe/agent-doc-system/compare/v1.1.0...v1.2.0
|
|
247
373
|
[1.1.0]: https://github.com/lbframe/agent-doc-system/compare/v1.0.0...v1.1.0
|
|
248
374
|
[1.0.0]: https://github.com/lbframe/agent-doc-system/releases/tag/v1.0.0
|
|
@@ -267,3 +393,10 @@ First public release.
|
|
|
267
393
|
[#40]: https://github.com/lbframe/agent-doc-system/issues/40
|
|
268
394
|
[#41]: https://github.com/lbframe/agent-doc-system/issues/41
|
|
269
395
|
[#43]: https://github.com/lbframe/agent-doc-system/issues/43
|
|
396
|
+
[#45]: https://github.com/lbframe/agent-doc-system/issues/45
|
|
397
|
+
[#46]: https://github.com/lbframe/agent-doc-system/issues/46
|
|
398
|
+
[#49]: https://github.com/lbframe/agent-doc-system/issues/49
|
|
399
|
+
[#50]: https://github.com/lbframe/agent-doc-system/issues/50
|
|
400
|
+
[#51]: https://github.com/lbframe/agent-doc-system/issues/51
|
|
401
|
+
[#55]: https://github.com/lbframe/agent-doc-system/issues/55
|
|
402
|
+
[#59]: https://github.com/lbframe/agent-doc-system/issues/59
|
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 { 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;
|
|
@@ -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;
|
package/adapters/typescript.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { Adapter } from "./registry.mjs";
|
|
8
8
|
import { parseYamlDocuments } from "../core/yaml.mjs";
|
|
9
|
+
import { joinPath } from "../core/paths.mjs";
|
|
9
10
|
|
|
10
11
|
export class TypeScriptAdapter extends Adapter {
|
|
11
12
|
static adapterName = "typescript";
|
|
@@ -13,13 +14,13 @@ export class TypeScriptAdapter extends Adapter {
|
|
|
13
14
|
super({ name: "typescript", version: "1.0.0", kind: "unit-marker" });
|
|
14
15
|
}
|
|
15
16
|
detect(ctx, root) {
|
|
16
|
-
const p = root
|
|
17
|
+
const p = joinPath(root, "tsconfig.json");
|
|
17
18
|
if (!ctx.repo.exists(p)) return null;
|
|
18
19
|
return { path: p, shape: "importable" };
|
|
19
20
|
}
|
|
20
21
|
extract(ctx, unit) {
|
|
21
22
|
const { repo, prov, compRef } = ctx;
|
|
22
|
-
const p = unit.root
|
|
23
|
+
const p = joinPath(unit.root, "tsconfig.json");
|
|
23
24
|
if (!repo.exists(p)) return {};
|
|
24
25
|
const derived = { languages: new Set(["typescript"]), runtimes: new Set(["node"]) };
|
|
25
26
|
let ts;
|
|
@@ -43,8 +44,9 @@ export class TypeScriptAdapter extends Adapter {
|
|
|
43
44
|
// real HTTP surface and must be visible to the query surface.
|
|
44
45
|
const routes = [];
|
|
45
46
|
for (const base of ["src/app", "app", "src/pages", "pages"]) {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const dir = joinPath(unit.root, base);
|
|
48
|
+
if (!repo.isDir(dir)) continue;
|
|
49
|
+
for (const f of repo.walkUnit(unit.root, dir)) {
|
|
48
50
|
const routePath = routeFor(unit.root, base, f);
|
|
49
51
|
if (!routePath) continue;
|
|
50
52
|
const rpr = prov.add("observed", f, "route-extractor", ["der:" + compRef + ":/routes"]);
|
|
@@ -63,7 +65,7 @@ export class TypeScriptAdapter extends Adapter {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function routeFor(root, base, file) {
|
|
66
|
-
const rel = file.slice((root
|
|
68
|
+
const rel = file.slice(joinPath(root, base).length + 1);
|
|
67
69
|
if (base.endsWith("app")) {
|
|
68
70
|
if (!rel.endsWith("/route.ts") && !rel.endsWith("/route.tsx") && !rel.endsWith("/route.js")) return null;
|
|
69
71
|
const segs = rel.split("/").slice(0, -1);
|
package/adapters/wrangler.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Adapter } from "./registry.mjs";
|
|
12
12
|
import { parse as parseToml } from "../core/toml.mjs";
|
|
13
13
|
import { sourceFiles } from "../core/sourcescan.mjs";
|
|
14
|
+
import { joinPath } from "../core/paths.mjs";
|
|
14
15
|
|
|
15
16
|
const MANIFESTS = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"];
|
|
16
17
|
|
|
@@ -21,13 +22,13 @@ export class WranglerAdapter extends Adapter {
|
|
|
21
22
|
}
|
|
22
23
|
detect(ctx, root) {
|
|
23
24
|
for (const n of MANIFESTS) {
|
|
24
|
-
if (ctx.repo.exists(root
|
|
25
|
+
if (ctx.repo.exists(joinPath(root, n))) return { path: joinPath(root, n), shape: "deployable" };
|
|
25
26
|
}
|
|
26
27
|
return null;
|
|
27
28
|
}
|
|
28
29
|
extract(ctx, unit) {
|
|
29
30
|
const { repo, prov, compRef } = ctx;
|
|
30
|
-
const file = MANIFESTS.map((n) => unit.root
|
|
31
|
+
const file = MANIFESTS.map((n) => joinPath(unit.root, n)).find((p) => repo.exists(p));
|
|
31
32
|
if (!file) return {};
|
|
32
33
|
let toml;
|
|
33
34
|
try {
|
package/bin/agentdoc.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { observationFreshness, stalenessDiagnostics } from "../core/observations
|
|
|
19
19
|
import { assertNoSecretsInText } from "../core/secrets.mjs";
|
|
20
20
|
import { installTemplates } from "../core/scaffold.mjs";
|
|
21
21
|
import { COMMANDS, flagsOf, parseArgs, commandHelp, UsageError } from "./commands.mjs";
|
|
22
|
-
import { loadSchemaBundle, loadConfig } from "../core/descriptors.mjs";
|
|
22
|
+
import { loadSchemaBundle, loadConfig, CONFIG_PATH } from "../core/descriptors.mjs";
|
|
23
23
|
import { execFileSync } from "node:child_process";
|
|
24
24
|
|
|
25
25
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -353,7 +353,14 @@ try {
|
|
|
353
353
|
: {}),
|
|
354
354
|
}, true);
|
|
355
355
|
} else if (cmd === "init") {
|
|
356
|
-
|
|
356
|
+
// An existing configuration is the authority on where documentation lives
|
|
357
|
+
// (issue #49). Load it through the compiler's own loader, so init, scaffold,
|
|
358
|
+
// audit and compile cannot disagree, and so an invalid or escaping docs path
|
|
359
|
+
// is rejected before anything is written.
|
|
360
|
+
const docsConfig = fs.existsSync(path.join(ROOT, CONFIG_PATH))
|
|
361
|
+
? loadConfig(new Repo(ROOT, { noGit: true }), loadSchemaBundle().bundle)
|
|
362
|
+
: null;
|
|
363
|
+
const result = installTemplates(null, null, { write: true, root: ROOT, force: opt("force"), docsConfig });
|
|
357
364
|
out(result, true);
|
|
358
365
|
} else if (cmd === "eval") {
|
|
359
366
|
const which = positional[0] || "routing";
|
package/bin/usage.txt
CHANGED
|
@@ -5,6 +5,8 @@ agentdoc — portable documentation & software-catalog system
|
|
|
5
5
|
agentdoc <command> --help every flag one command accepts (also: agentdoc help <command>)
|
|
6
6
|
|
|
7
7
|
agentdoc init [--force] write a configuration and documentation skeleton
|
|
8
|
+
(docs skeleton goes to the configured docs.* paths;
|
|
9
|
+
agentdoc/docs/ when there is no configuration yet)
|
|
8
10
|
agentdoc audit inventory the repository, classify every fact
|
|
9
11
|
agentdoc scaffold [--write] generate a CREATE-mode skeleton from the audit
|
|
10
12
|
agentdoc validate sources + schemas + semantic checks
|
package/core/audit.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { classifyContractFile } from "./contracts.mjs";
|
|
|
19
19
|
import { ContextIndex } from "./query.mjs";
|
|
20
20
|
import { expandGlob, isRepoPath } from "./fsx.mjs";
|
|
21
21
|
import { isNormative, NORMATIVE_SEMANTICS_PREFIX } from "./normative.mjs";
|
|
22
|
+
import { isRepoRoot, longestOwner } from "./paths.mjs";
|
|
22
23
|
|
|
23
24
|
const DOC_EXT = /\.(md|mdx|adoc|rst|txt)$/;
|
|
24
25
|
|
|
@@ -143,11 +144,12 @@ export function audit(repo, cfg, res) {
|
|
|
143
144
|
configured.push({ role, path: dir, configured: false });
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
// The most specific unit root containing the path. A root unit is left out
|
|
148
|
+
// on purpose: it contains the whole repository, so counting it would make
|
|
149
|
+
// every repository-wide hierarchy document "component-scoped" and the
|
|
150
|
+
// distinction this check draws would disappear.
|
|
151
|
+
const unitRoots = [...new Set(res.discovery.units.map((u) => u.root))].filter((r) => !isRepoRoot(r));
|
|
152
|
+
const insideComponent = (p) => longestOwner(unitRoots, p);
|
|
151
153
|
|
|
152
154
|
// The audit reports; it does not gate. These are therefore collected as audit
|
|
153
155
|
// findings on the report rather than pushed into the compile-time diagnostics,
|
package/core/compile.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./normative.mjs";
|
|
25
25
|
import { loadAdapters } from "../adapters/index.mjs";
|
|
26
26
|
import { loadCargoUnit, analyzeCargoDeps, rustIdentsInSource } from "../adapters/rust.mjs";
|
|
27
|
+
import { joinPath, ownsPath } from "./paths.mjs";
|
|
27
28
|
import {
|
|
28
29
|
COMPILER_NAME, COMPILER_VERSION, GRAPH_SCHEMA_VERSION,
|
|
29
30
|
assembleGraph, serializeGraph, finalizeSerialization, subjectResolver,
|
|
@@ -300,7 +301,7 @@ export function compile(root, opts = {}) {
|
|
|
300
301
|
// every graph that does not enable python.
|
|
301
302
|
const pythonOn = adapters.some((a) => a.name === "python");
|
|
302
303
|
for (const unit of discovery.eligible) {
|
|
303
|
-
const pkgPath = unit.root
|
|
304
|
+
const pkgPath = joinPath(unit.root, "package.json");
|
|
304
305
|
if (repo.exists(pkgPath)) {
|
|
305
306
|
try {
|
|
306
307
|
const pkg = repo.readJson(pkgPath);
|
|
@@ -310,7 +311,7 @@ export function compile(root, opts = {}) {
|
|
|
310
311
|
res.errors.push(new AgentDocError(CODES.SCHEMA, "package.json is not valid JSON: " + pkgPath, { path: pkgPath }));
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
|
-
const gomodPath = unit.root
|
|
314
|
+
const gomodPath = joinPath(unit.root, "go.mod");
|
|
314
315
|
if (repo.exists(gomodPath)) {
|
|
315
316
|
const m = /^module\s+(\S+)/m.exec(repo.readText(gomodPath));
|
|
316
317
|
unit.goModule = m ? m[1] : null;
|
|
@@ -398,7 +399,7 @@ export function compile(root, opts = {}) {
|
|
|
398
399
|
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
399
400
|
}
|
|
400
401
|
if (unit.goModule) {
|
|
401
|
-
const gomod = repo.exists(unit.root
|
|
402
|
+
const gomod = repo.exists(joinPath(unit.root, "go.mod")) ? repo.readText(joinPath(unit.root, "go.mod")) : "";
|
|
402
403
|
const goReqs = [...gomod.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)].map((m) => m[1]);
|
|
403
404
|
productionDependencies.set(unit.component.ref, [...(productionDependencies.get(unit.component.ref) || []), ...goReqs]);
|
|
404
405
|
for (const m of gomod.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)) {
|
|
@@ -796,14 +797,15 @@ export function compile(root, opts = {}) {
|
|
|
796
797
|
...schemaInputs,
|
|
797
798
|
...adapters.map((a) => ADAPTER_VERSION_INPUT_PREFIX + a.name + "@" + a.version),
|
|
798
799
|
]);
|
|
799
|
-
|
|
800
|
+
// The same git status snapshot the ignore oracle answered from (fsx.mjs).
|
|
801
|
+
const dirtySet = new Set(repo.dirtyPaths({ snapshot: true }).filter((p) => p !== graphPath));
|
|
800
802
|
const walkedDirs = [...repo.walks.keys()];
|
|
801
803
|
const dirty = [...dirtySet].some(
|
|
802
804
|
(p) =>
|
|
803
805
|
repo.readPaths.has(p) ||
|
|
804
806
|
repo.probes.has(p) ||
|
|
805
807
|
repo.listings.has(p) ||
|
|
806
|
-
walkedDirs.some((d) => (d
|
|
808
|
+
walkedDirs.some((d) => ownsPath(d, p))
|
|
807
809
|
);
|
|
808
810
|
graph.source = { commit, dirty, inputHash };
|
|
809
811
|
|
package/core/derive.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { assertRepoPath } from "./fsx.mjs";
|
|
9
9
|
import { CODES } from "./codes.mjs";
|
|
10
|
+
import { joinPath } from "./paths.mjs";
|
|
10
11
|
|
|
11
12
|
const DEFAULT_DOC_CANDIDATES = ["README.md", "ARCHITECTURE.md", "AGENTS.md", "CONTRIBUTING.md", "DESIGN.md", "OVERVIEW.md"];
|
|
12
13
|
const DEFAULT_CONSTRAINT_CANDIDATES = ["CONSTRAINTS.md", "INVARIANTS.md"];
|
|
@@ -33,15 +34,15 @@ export function genericFacts(ctx, unit) {
|
|
|
33
34
|
const constraints = [];
|
|
34
35
|
const runbooks = [];
|
|
35
36
|
for (const n of docNames) {
|
|
36
|
-
const p = root
|
|
37
|
+
const p = joinPath(root, n);
|
|
37
38
|
if (repo.exists(p)) { docs.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/docs"]); }
|
|
38
39
|
}
|
|
39
40
|
for (const n of constraintNames) {
|
|
40
|
-
const p = root
|
|
41
|
+
const p = joinPath(root, n);
|
|
41
42
|
if (repo.exists(p)) { constraints.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/constraints"]); }
|
|
42
43
|
}
|
|
43
44
|
for (const n of runbookNames) {
|
|
44
|
-
const p = root
|
|
45
|
+
const p = joinPath(root, n);
|
|
45
46
|
if (repo.exists(p)) { runbooks.push(p); prov.add("observed", p, "docs-extractor", ["der:" + compRef + ":/runbooks"]); }
|
|
46
47
|
}
|
|
47
48
|
const spec = ctx.componentEntity().doc.spec || {};
|
package/core/descriptors.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { parseYamlDocuments } from "./yaml.mjs";
|
|
|
20
20
|
import { scanForSecrets } from "./secrets.mjs";
|
|
21
21
|
import { formatErrors } from "./jsonschema.mjs";
|
|
22
22
|
import { SchemaBundle } from "./jsonschema.mjs";
|
|
23
|
+
import { joinPath, ownsPath } from "./paths.mjs";
|
|
23
24
|
|
|
24
25
|
export const KINDS = Object.freeze(["Domain", "System", "Component", "API", "Resource"]);
|
|
25
26
|
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -177,7 +178,7 @@ function descriptorGlobs(repo, cfg) {
|
|
|
177
178
|
// glob — its descriptor lives at a fixed path and must be loaded even when
|
|
178
179
|
// no componentDescriptors glob happens to cover that directory.
|
|
179
180
|
for (const r of cfg.discovery.supplementalRoots || []) {
|
|
180
|
-
const f = String(r)
|
|
181
|
+
const f = joinPath(String(r), cfg.discovery.componentDescriptorName);
|
|
181
182
|
if (repo.exists(f)) componentFiles.add(f);
|
|
182
183
|
}
|
|
183
184
|
const centralFiles = new Set();
|
|
@@ -473,7 +474,7 @@ export function loadReviewedOverrides(repo, cfg, refs, byRef) {
|
|
|
473
474
|
: null;
|
|
474
475
|
for (const ev of o.evidence) {
|
|
475
476
|
const p = ev.path;
|
|
476
|
-
const scoped = p === o.contractRef ||
|
|
477
|
+
const scoped = p === o.contractRef || ownsPath(subjectRoot, p) || (targetRoot !== null && ownsPath(targetRoot, p));
|
|
477
478
|
if (!scoped) {
|
|
478
479
|
throw new AgentDocError(
|
|
479
480
|
CODES.RELATION_KIND,
|