agent-doc-system 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +97 -112
  3. package/adapters/ansible.mjs +306 -0
  4. package/adapters/cache.mjs +8 -2
  5. package/adapters/contract-consumers.mjs +1 -1
  6. package/adapters/database.mjs +18 -4
  7. package/adapters/events.mjs +1 -1
  8. package/adapters/github-actions.mjs +244 -85
  9. package/adapters/go.mjs +1 -1
  10. package/adapters/index.mjs +5 -2
  11. package/adapters/object-store.mjs +2 -2
  12. package/adapters/python.mjs +1142 -0
  13. package/adapters/resource-client.mjs +1 -1
  14. package/adapters/rust.mjs +987 -0
  15. package/adapters/typescript.mjs +1 -1
  16. package/bin/agentdoc.mjs +60 -44
  17. package/bin/commands.mjs +169 -0
  18. package/bin/usage.txt +5 -3
  19. package/core/audit.mjs +55 -4
  20. package/core/authority/engine.mjs +27 -4
  21. package/core/codes.mjs +11 -0
  22. package/core/compile.mjs +263 -13
  23. package/core/dependents.mjs +42 -0
  24. package/core/descriptors.mjs +5 -2
  25. package/core/discover.mjs +60 -9
  26. package/core/fsx.mjs +27 -4
  27. package/core/graph.mjs +1 -1
  28. package/core/impact.mjs +55 -44
  29. package/core/normative.mjs +171 -0
  30. package/core/query.mjs +18 -8
  31. package/core/scaffold.mjs +87 -22
  32. package/core/sourcescan.mjs +11 -2
  33. package/core/toml.mjs +463 -140
  34. package/core/yaml.mjs +5 -3
  35. package/docs/ADAPTERS.md +232 -7
  36. package/docs/AUTHORITY.md +10 -1
  37. package/docs/BENCHMARK.md +68 -0
  38. package/docs/CI.md +3 -1
  39. package/docs/EVALS.md +33 -2
  40. package/docs/MIGRATION.md +76 -6
  41. package/docs/QUERY.md +53 -4
  42. package/docs/SPEC.md +22 -0
  43. package/docs/assets/readme/agentdoc-hero.webp +0 -0
  44. package/docs/assets/readme/agentdoc-in-action.webp +0 -0
  45. package/docs/assets/readme/how-agentdoc-works.webp +0 -0
  46. package/docs/assets/readme/why-agentdoc.webp +0 -0
  47. package/docs/ci/github-actions.yml +1 -1
  48. package/evals/harness.mjs +32 -6
  49. package/evals/scenarios/fixture-b-03-change-shared-lib.json +8 -1
  50. package/evals/scenarios/fixture-c-03-shared-vocabulary.json +7 -1
  51. package/evals/scenarios/fixture-e-01-change-shared-lib.json +14 -0
  52. package/evals/scenarios/fixture-e-02-change-manifest.json +9 -0
  53. package/evals/scenarios/fixture-e-03-local-module.json +9 -0
  54. package/evals/scenarios/fixture-e-04-ambiguous-crate.json +14 -0
  55. package/evals/scenarios/fixture-f-01-change-shared-lib.json +9 -0
  56. package/evals/scenarios/fixture-f-02-change-manifest.json +9 -0
  57. package/evals/scenarios/fixture-f-03-shadowed-import.json +9 -0
  58. package/evals/scenarios/fixture-f-04-nested-workspace.json +9 -0
  59. package/evals/scenarios/fixture-f-05-alembic-database.json +9 -0
  60. package/evals/scenarios/fixture-f-06-unittest-job.json +9 -0
  61. package/package.json +2 -1
  62. package/schemas/common.schema.json +40 -0
  63. package/schemas/component.schema.json +5 -1
  64. package/schemas/config.schema.json +117 -1
  65. package/skills/agent-doc-system/SKILL.md +5 -1
  66. package/skills/agent-doc-system/references/cli.md +12 -4
  67. package/skills/agent-doc-system/references/create-migrate.md +28 -6
  68. package/templates/ci.yml +1 -1
@@ -0,0 +1,306 @@
1
+ // Ansible adapter: what host provisioning actually installs.
2
+ //
3
+ // Reads the playbooks under a unit root without running anything and without
4
+ // evaluating a single Jinja expression. From every task reachable from a
5
+ // playbook (plays, roles, includes, blocks) it records the host paths the
6
+ // task creates and the services it manages. That inventory is the DERIVED side
7
+ // of a normative requirement whose `satisfiedBy` names this adapter and whose
8
+ // `expects` names a path or a service:
9
+ //
10
+ // provided a reachable, unconditional task installs it deterministic
11
+ // absent no reachable task does, and every task is transparent deterministic
12
+ //
13
+ // Anything weaker is a candidate, which the authority engine never elects: a
14
+ // matching task behind a `when`, or an absence claimed over a tree that holds
15
+ // an opaque task (a shell command, a package install, a templated path, a role
16
+ // that is not in the repository). An absence is only a fact when the adapter
17
+ // could have seen the opposite.
18
+ import { Adapter } from "./registry.mjs";
19
+ import { parseYamlDocuments } from "../core/yaml.mjs";
20
+ import { AgentDocError } from "../core/codes.mjs";
21
+
22
+ // Modules that create the path they name, and the arguments naming it.
23
+ const PATH_MODULES = {
24
+ copy: ["dest"],
25
+ template: ["dest"],
26
+ unarchive: ["dest", "creates"],
27
+ get_url: ["dest"],
28
+ git: ["dest"],
29
+ synchronize: ["dest"],
30
+ pip: ["virtualenv"],
31
+ };
32
+ // `file` creates only for these states; `lineinfile`/`blockinfile` only with
33
+ // `create: true`.
34
+ const FILE_CREATING_STATES = new Set(["directory", "touch", "link", "hard"]);
35
+ const SERVICE_MODULES = new Set(["systemd", "systemd_service", "service"]);
36
+ // Commands can do anything; their `creates` is the one thing they promise.
37
+ const COMMAND_MODULES = new Set(["command", "shell", "script", "raw"]);
38
+ // Modules that change no file the adapter would be asked about.
39
+ const INERT_MODULES = new Set([
40
+ "debug", "set_fact", "assert", "fail", "meta", "pause", "wait_for", "stat", "ping", "setup",
41
+ "include_vars", "group_by", "add_host", "user", "group", "uri", "slurp", "fetch", "set_stats",
42
+ ]);
43
+ // Task keywords, i.e. everything on a task that is not its module.
44
+ const TASK_KEYWORDS = new Set([
45
+ "name", "when", "loop", "with_items", "with_dict", "with_fileglob", "loop_control", "register", "notify",
46
+ "become", "become_user", "become_method", "tags", "vars", "environment", "args", "ignore_errors", "changed_when",
47
+ "failed_when", "until", "retries", "delay", "no_log", "delegate_to", "run_once", "check_mode", "diff",
48
+ "listen", "timeout", "throttle", "any_errors_fatal", "module_defaults", "collections", "connection",
49
+ ]);
50
+ const INCLUDES = new Set(["include_tasks", "import_tasks", "include"]);
51
+ const ROLE_INCLUDES = new Set(["include_role", "import_role"]);
52
+
53
+ export class AnsibleAdapter extends Adapter {
54
+ static adapterName = "ansible";
55
+ constructor() {
56
+ super({ name: "ansible", version: "1.0.0", kind: "source" });
57
+ }
58
+
59
+ // A unit that holds a playbook is a deployable infrastructure unit.
60
+ detect(ctx, root) {
61
+ const pb = playbooks(ctx.repo, root);
62
+ return pb.length ? { path: pb[0].path, shape: "deployable" } : null;
63
+ }
64
+
65
+ extract(ctx, unit) {
66
+ const inv = inventory(ctx.repo, unit.root);
67
+ if (!inv.files.length) return {};
68
+ for (const r of ctx.requirements || []) {
69
+ if (r.subject !== ctx.compRef || !r.satisfiedBy.includes("ansible") || !r.expects) continue;
70
+ const verdict = judge(inv, r.expects);
71
+ const files = verdict.files.length ? verdict.files : inv.files;
72
+ ctx.addFact(r.subject, r.key, verdict.value, {
73
+ evidenceClass: "DERIVED",
74
+ confidence: verdict.certain ? "deterministic" : "candidate",
75
+ provRecs: files.map((f) => ctx.prov.add("observed", f, "ansible-extractor", ["ass:pending"])),
76
+ semantics: "ansible: " + verdict.why,
77
+ });
78
+ }
79
+ return {
80
+ provisioning: {
81
+ installs: inv.installs.map((i) => ({ path: i.path, file: i.file, task: i.task, conditional: i.conditional })),
82
+ services: inv.services.map((s) => ({ name: s.name, file: s.file, task: s.task, conditional: s.conditional })),
83
+ opaque: inv.opaque,
84
+ },
85
+ };
86
+ }
87
+ }
88
+
89
+ function judge(inv, expects) {
90
+ const target = expects.installs || expects.service;
91
+ const what = expects.installs ? "path " + target : "service " + target;
92
+ const hits = expects.installs
93
+ ? inv.installs.filter((i) => i.path === target || i.path.startsWith(target + "/"))
94
+ : inv.services.filter((s) => s.name === target || s.name === target + ".service");
95
+ const sure = hits.filter((h) => !h.conditional);
96
+ if (sure.length) {
97
+ return { value: "provided", certain: true, files: uniq(sure.map((h) => h.file)), why: what + " installed by " + describe(sure[0]) };
98
+ }
99
+ if (hits.length) {
100
+ return { value: "provided", certain: false, files: uniq(hits.map((h) => h.file)), why: what + " installed only conditionally by " + describe(hits[0]) };
101
+ }
102
+ if (inv.opaque.length) {
103
+ return {
104
+ value: "absent", certain: false, files: [],
105
+ why: "no task installs " + what + ", but absence cannot be proven: " + inv.opaque.slice(0, 3).join("; ") +
106
+ (inv.opaque.length > 3 ? " (+" + (inv.opaque.length - 3) + " more)" : ""),
107
+ };
108
+ }
109
+ return { value: "absent", certain: true, files: [], why: "no reachable task installs " + what };
110
+ }
111
+
112
+ function describe(h) {
113
+ return h.file + (h.task ? " (" + h.task + ")" : "");
114
+ }
115
+
116
+ function uniq(a) {
117
+ return [...new Set(a)].sort();
118
+ }
119
+
120
+ function parse(repo, file) {
121
+ try {
122
+ return parseYamlDocuments(repo.readText(file), file, { allowSequenceRoot: true }).map((d) => d.doc);
123
+ } catch (e) {
124
+ if (e instanceof AgentDocError) return null;
125
+ throw e;
126
+ }
127
+ }
128
+
129
+ function isYaml(f) {
130
+ return /\.ya?ml$/.test(f);
131
+ }
132
+
133
+ function playbooks(repo, root) {
134
+ const out = [];
135
+ for (const f of repo.walkUnit(root)) {
136
+ if (!isYaml(f) || /(^|\/)(tasks|handlers|vars|defaults|meta|group_vars|host_vars|templates|files)\//.test(f.slice(root.length))) continue;
137
+ const docs = parse(repo, f);
138
+ if (!docs) continue;
139
+ const plays = docs.flatMap((d) => (Array.isArray(d) ? d : []));
140
+ if (plays.length && plays.every((p) => p && typeof p === "object" && (p.hosts !== undefined || p.import_playbook !== undefined))) {
141
+ out.push({ path: f, plays });
142
+ }
143
+ }
144
+ return out.sort((a, b) => (a.path < b.path ? -1 : 1));
145
+ }
146
+
147
+ // Every task reachable from a playbook under `root`, walked in a fixed order.
148
+ function inventory(repo, root) {
149
+ const inv = { files: new Set(), installs: [], services: [], opaque: [] };
150
+ const seen = new Set();
151
+ const opaque = (file, task, why) => inv.opaque.push(file + (task ? " (" + task + ")" : "") + ": " + why);
152
+
153
+ const dirOf = (p) => p.split("/").slice(0, -1).join("/");
154
+ const join = (dir, rel) => {
155
+ const parts = (dir ? dir.split("/") : []).concat(rel.split("/"));
156
+ const out = [];
157
+ for (const s of parts) {
158
+ if (s === "" || s === ".") continue;
159
+ if (s === "..") { if (!out.length) return null; out.pop(); } else out.push(s);
160
+ }
161
+ return out.join("/");
162
+ };
163
+ const templated = (v) => typeof v === "string" && /\{\{|\{%/.test(v);
164
+
165
+ const findRole = (name, fromDir) => {
166
+ for (const base of [fromDir, root]) {
167
+ for (const ext of ["yml", "yaml"]) {
168
+ const p = join(base, "roles/" + name + "/tasks/main." + ext);
169
+ if (p && repo.exists(p)) return p;
170
+ }
171
+ }
172
+ return null;
173
+ };
174
+
175
+ const walkFile = (file, conditional, kind) => {
176
+ const key = file + "|" + conditional + "|" + kind;
177
+ if (seen.has(key)) return;
178
+ seen.add(key);
179
+ inv.files.add(file);
180
+ const docs = parse(repo, file);
181
+ if (!docs) { opaque(file, null, "not parseable as YAML"); return; }
182
+ const list = docs.flatMap((d) => (Array.isArray(d) ? d : []));
183
+ if (kind === "playbook") for (const p of list) walkPlay(file, p, conditional);
184
+ else walkTasks(file, list, conditional);
185
+ };
186
+
187
+ const walkPlay = (file, play, conditional) => {
188
+ if (!play || typeof play !== "object") return;
189
+ if (play.import_playbook !== undefined) {
190
+ const ref = String(play.import_playbook);
191
+ const p = !templated(ref) && join(dirOf(file), ref);
192
+ if (p && repo.exists(p)) walkFile(p, conditional || play.when !== undefined, "playbook");
193
+ else opaque(file, null, "imported playbook not resolvable in the repository: " + ref);
194
+ return;
195
+ }
196
+ const cond = conditional || play.when !== undefined;
197
+ for (const section of ["pre_tasks", "roles", "tasks", "post_tasks", "handlers"]) {
198
+ const items = play[section];
199
+ if (!Array.isArray(items)) continue;
200
+ if (section === "roles") {
201
+ for (const r of items) {
202
+ const name = typeof r === "string" ? r : r && (r.role || r.name);
203
+ walkRole(file, String(name), cond || (r && typeof r === "object" && r.when !== undefined));
204
+ }
205
+ } else {
206
+ walkTasks(file, items, cond);
207
+ }
208
+ }
209
+ };
210
+
211
+ const walkRole = (file, name, conditional) => {
212
+ const main = !templated(name) && findRole(name, dirOf(file));
213
+ if (main) walkFile(main, conditional, "tasks");
214
+ else opaque(file, null, "role not found in the repository: " + name);
215
+ };
216
+
217
+ const walkTasks = (file, tasks, conditional) => {
218
+ for (const t of tasks) {
219
+ if (!t || typeof t !== "object" || Array.isArray(t)) continue;
220
+ const cond = conditional || t.when !== undefined;
221
+ const label = typeof t.name === "string" ? t.name : null;
222
+ if (Array.isArray(t.block)) {
223
+ walkTasks(file, t.block, cond);
224
+ // A rescue runs only when the block failed: never an unconditional install.
225
+ for (const k of ["rescue", "always"]) if (Array.isArray(t[k])) walkTasks(file, t[k], cond || k === "rescue");
226
+ continue;
227
+ }
228
+ const modKey = Object.keys(t).find((k) => !TASK_KEYWORDS.has(k));
229
+ if (!modKey) continue;
230
+ const mod = modKey.split(".").pop();
231
+ const raw = t[modKey];
232
+ const args = { ...parseFreeForm(raw), ...(t.args && typeof t.args === "object" ? t.args : {}) };
233
+ // A loop runs the module once per item with a templated argument; the
234
+ // adapter does not expand it.
235
+ const looped = t.loop !== undefined || Object.keys(t).some((k) => k.startsWith("with_"));
236
+
237
+ if (INCLUDES.has(mod)) {
238
+ const ref = typeof raw === "string" ? raw.split(/\s+/)[0] : args.file;
239
+ const p = ref && !templated(ref) && join(dirOf(file), String(ref));
240
+ if (p && repo.exists(p)) walkFile(p, cond, "tasks");
241
+ else opaque(file, label, "included task file not resolvable in the repository: " + ref);
242
+ continue;
243
+ }
244
+ if (ROLE_INCLUDES.has(mod)) {
245
+ walkRole(file, String(args.name), cond);
246
+ continue;
247
+ }
248
+ const record = (list, entry) => list.push({ ...entry, file, task: label, conditional: cond });
249
+ if (PATH_MODULES[mod] || mod === "file" || mod === "lineinfile" || mod === "blockinfile") {
250
+ let names = PATH_MODULES[mod] || ["path", "dest", "name"];
251
+ if (mod === "file" && !FILE_CREATING_STATES.has(String(args.state || "file"))) names = [];
252
+ if ((mod === "lineinfile" || mod === "blockinfile") && !isTrue(args.create)) names = [];
253
+ for (const n of names) {
254
+ const v = args[n];
255
+ if (v === undefined) continue;
256
+ if (templated(v) || looped) { opaque(file, label, mod + " " + n + " is templated: " + v); continue; }
257
+ record(inv.installs, { path: normalisePath(String(v)) });
258
+ }
259
+ continue;
260
+ }
261
+ if (SERVICE_MODULES.has(mod)) {
262
+ const n = args.name;
263
+ if (n === undefined) continue;
264
+ if (templated(n) || looped) opaque(file, label, mod + " name is templated: " + n);
265
+ else record(inv.services, { name: String(n) });
266
+ continue;
267
+ }
268
+ if (COMMAND_MODULES.has(mod)) {
269
+ if (args.creates !== undefined && !templated(args.creates) && !looped) {
270
+ record(inv.installs, { path: normalisePath(String(args.creates)) });
271
+ } else {
272
+ opaque(file, label, mod + " task without a literal `creates`");
273
+ }
274
+ continue;
275
+ }
276
+ if (INERT_MODULES.has(mod)) continue;
277
+ opaque(file, label, "module " + modKey + " is not modeled and may change the host");
278
+ }
279
+ };
280
+
281
+ for (const pb of playbooks(repo, root)) walkFile(pb.path, false, "playbook");
282
+ const byPath = (a, b) => (a.path + a.file < b.path + b.file ? -1 : a.path + a.file > b.path + b.file ? 1 : 0);
283
+ return {
284
+ files: [...inv.files].sort(),
285
+ installs: inv.installs.sort(byPath),
286
+ services: inv.services.sort((a, b) => (a.name + a.file < b.name + b.file ? -1 : 1)),
287
+ opaque: [...new Set(inv.opaque)].sort(),
288
+ };
289
+ }
290
+
291
+ // `file: path=/a state=directory` — the key=value shorthand.
292
+ function parseFreeForm(raw) {
293
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
294
+ if (typeof raw !== "string") return {};
295
+ const out = {};
296
+ for (const m of raw.matchAll(/(\w+)=("[^"]*"|'[^']*'|\S+)/g)) out[m[1]] = m[2].replace(/^["']|["']$/g, "");
297
+ return out;
298
+ }
299
+
300
+ function isTrue(v) {
301
+ return v === true || /^(yes|true)$/i.test(String(v));
302
+ }
303
+
304
+ function normalisePath(p) {
305
+ return p.length > 1 ? p.replace(/\/+$/, "") : p;
306
+ }
@@ -1,7 +1,7 @@
1
1
  // Cache adapter: Redis / in-memory cache client evidence as a derived binding.
2
2
  import { Adapter } from "./registry.mjs";
3
3
 
4
- const CACHE_HINT = /(@upstash\/redis|ioredis|redis|go-redis|redis\.go|valkey|memcache|@memcached)/i;
4
+ const CACHE_HINT = /(@upstash\/redis|ioredis|redis\.Redis|redis|go-redis|redis\.go|valkey|memcache|@memcached)/i;
5
5
 
6
6
  export class CacheAdapter extends Adapter {
7
7
  static adapterName = "cache";
@@ -10,7 +10,13 @@ export class CacheAdapter extends Adapter {
10
10
  }
11
11
  extract(ctx, unit) {
12
12
  const { repo, prov, compRef } = ctx;
13
- const evFile = unit.goModule ? unit.root + "/go.mod" : unit.root + "/package.json";
13
+ const evFile = unit.goModule
14
+ ? unit.root + "/go.mod"
15
+ : unit.pythonManifest
16
+ ? unit.pythonManifest
17
+ : repo.exists(unit.root + "/package.json")
18
+ ? unit.root + "/package.json"
19
+ : unit.root + "/Cargo.toml";
14
20
  if (!repo.exists(evFile)) return {};
15
21
  const text = repo.readText(evFile);
16
22
  if (!CACHE_HINT.test(text)) return {};
@@ -18,7 +18,7 @@ const DOC_EXT = /\.(md|mdx|txt|adoc|rst)$/;
18
18
 
19
19
  export function contractReferenceSites(repo, unit) {
20
20
  const out = new Map(); // file -> true
21
- for (const f of repo.walk(unit.root)) {
21
+ for (const f of repo.walkUnit(unit.root)) {
22
22
  const base = path.posix.basename(f);
23
23
  if (DOC_EXT.test(f)) continue;
24
24
  if (f.includes("/docs/") || f.startsWith("docs/")) continue;
@@ -15,6 +15,9 @@ const ORM_CONFIG = [
15
15
  "prisma/schema.prisma", "schema.prisma",
16
16
  "knexfile.js", "knexfile.ts", "sequelize.config.js", "ormconfig.json",
17
17
  ];
18
+ // Probed only for a Cargo package. Adding it to the list above would record
19
+ // a new existence probe on every non-Rust unit and change those graphs.
20
+ const RUST_ORM_CONFIG = ["diesel.toml"];
18
21
 
19
22
  export class DatabaseAdapter extends Adapter {
20
23
  static adapterName = "database";
@@ -25,12 +28,23 @@ export class DatabaseAdapter extends Adapter {
25
28
  const { repo, prov, compRef } = ctx;
26
29
  const root = unit.root;
27
30
  const migrationDirs = findMigrationDirs(repo, root);
28
- const ormConfig = firstExistingFile(repo, root, ORM_CONFIG);
31
+ const ormConfig = firstExistingFile(repo, root, unit.cargoPackage ? ORM_CONFIG.concat(RUST_ORM_CONFIG) : ORM_CONFIG);
29
32
  if (!migrationDirs.length && !ormConfig) return {};
30
33
 
31
34
  let evidencePath = ormConfig;
32
35
  if (migrationDirs.length) {
33
- evidencePath = repo.walk(migrationDirs[0])[0] || migrationDirs[0];
36
+ evidencePath = repo.walkUnit(root, migrationDirs[0])[0] || migrationDirs[0];
37
+ }
38
+ const alembicDir = migrationDirs.find((d) => d === "alembic/versions" || d.endsWith("/alembic/versions"));
39
+ if (alembicDir) {
40
+ const ini = root + "/alembic.ini";
41
+ const pyproject = root + "/pyproject.toml";
42
+ if (repo.exists(ini)) {
43
+ const text = repo.readText(ini);
44
+ if (/^\s*\[alembic\]/m.test(text) || /^\s*script_location\s*=/m.test(text)) evidencePath = ini;
45
+ } else if (repo.exists(pyproject) && /\[tool\.alembic\]/.test(repo.readText(pyproject))) {
46
+ evidencePath = pyproject;
47
+ }
34
48
  }
35
49
  const pr = prov.add("observed", evidencePath, "migration-extractor", ["der:" + compRef + ":/bindings"]);
36
50
 
@@ -93,10 +107,10 @@ export class DatabaseAdapter extends Adapter {
93
107
  }
94
108
  }
95
109
 
96
- const POOL_HINT = /(createPool|new Pool|pg\.Pool|psycopg2?\.connect|sql\.Open|mysql\.createConnection|Pool\(|DataSource|prisma\.(datasource|client)|DATABASE_URL|DB_DSN)/;
110
+ const POOL_HINT = /(createPool|new Pool|pg\.Pool|PgPool|psycopg2?\.connect|asyncpg\.(?:connect|create_pool)|sql\.Open|mysql\.createConnection|Pool\(|DataSource|prisma\.(datasource|client)|DATABASE_URL|DB_DSN|sqlx::migrate!|tokio_postgres|tokio-postgres|deadpool|sea_orm|sea-orm|diesel::|create_engine\s*\(|\bDATABASES\s*=)/;
97
111
 
98
112
  function hasConnectionEvidence(ctx, unit) {
99
- for (const f of ctx.repo.walk(unit.root)) {
113
+ for (const f of ctx.repo.walkUnit(unit.root)) {
100
114
  if (/\.(ts|tsx|js|mjs|go|py|rb|java|kt|rs|ex|cs|php)$/.test(f)) {
101
115
  if (/(^|\/)(tests?|__tests__)\//.test(f)) continue;
102
116
  if (POOL_HINT.test(ctx.repo.readText(f))) return true;
@@ -70,7 +70,7 @@ export class EventsAdapter extends Adapter {
70
70
  if (other.component && visible.has(other.component.ref) && other.component.ref !== compRef) collectDecls(other.root);
71
71
  }
72
72
  const families = new Set();
73
- for (const f of repo.walk(unit.root)) {
73
+ for (const f of repo.walkUnit(unit.root)) {
74
74
  if (!SRC_EXT.test(f)) continue;
75
75
  if (TEST_PATH.test(f) || TEST_FILE.test(f)) continue;
76
76
  const text = repo.readText(f);