@event4u/agent-config 17.2.1 → 17.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.
Files changed (225) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
  5. package/CHANGELOG.md +167 -193
  6. package/CREDITS.md +2 -2
  7. package/README.md +1 -1
  8. package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  11. package/dist/cli/commands/recordTriggerEval.js +1 -1
  12. package/dist/cli/registry.js +2 -2
  13. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
  14. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  15. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  16. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  17. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
  18. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
  19. package/dist/cli-delegate/cmd_conformance.js +190 -192
  20. package/dist/cli-delegate/cmd_doctor.js +5 -5
  21. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  22. package/dist/cli-delegate/cmd_explain.js +1 -1
  23. package/dist/cli-delegate/cmd_export.js +2 -2
  24. package/dist/cli-delegate/cmd_handoff.js +2 -2
  25. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  26. package/dist/cli-delegate/cmd_prune.js +2 -2
  27. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  28. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  29. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  30. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  31. package/dist/cli-delegate/cmd_sync.js +2 -2
  32. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  33. package/dist/cli-delegate/cmd_update.js +24 -13
  34. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  35. package/dist/cli-delegate/cmd_validate.js +2 -2
  36. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  37. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  38. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  39. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  40. package/dist/discovery/deprecation-report.md +1 -1
  41. package/dist/discovery/discovery-manifest.json +3 -3
  42. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  43. package/dist/discovery/discovery-manifest.summary.md +1 -1
  44. package/dist/discovery/dormancy-report.md +1 -1
  45. package/dist/discovery/orphan-report.md +1 -1
  46. package/dist/discovery/packs.json +2 -2
  47. package/dist/discovery/trust-report.md +1 -1
  48. package/dist/discovery/workspaces.json +2 -2
  49. package/dist/hooks/code_graph_refresh.chunk.cjs +443 -0
  50. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  51. package/dist/hooks/dispatch.js +1001 -938
  52. package/dist/hooks/dispatch.sha256 +1 -1
  53. package/dist/hooks/egress_engine.chunk.cjs +57 -7
  54. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  55. package/dist/hooks/update_notice.chunk.cjs +914 -0
  56. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  57. package/dist/install/install.mjs +6974 -6037
  58. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  59. package/dist/mcp/registry-manifest.json +3 -2
  60. package/dist/mcp/server.json +13 -4
  61. package/dist/mcp/server.mjs +71202 -63282
  62. package/dist/scripts/_lib/egress_rules.js +8 -2
  63. package/dist/scripts/condense.js +18 -15
  64. package/dist/server/routes/discovery.js +1 -1
  65. package/dist/server/routes/install.js +1 -1
  66. package/dist/server/routes/ping.js +1 -1
  67. package/dist/server/routes/schema.js +4 -4
  68. package/dist/server/routes/settings.js +2 -6
  69. package/dist/server/routes/wizard.js +6 -6
  70. package/dist/server/schemas/settings.js +42 -42
  71. package/dist/shared/settingsSurface.js +1 -1
  72. package/dist/shared/userMd/schema.js +1 -1
  73. package/dist/shared/zodJsonSchema.js +47 -0
  74. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  75. package/dist/ui/index.html +1 -1
  76. package/package.json +6 -3
  77. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  78. package/src/config/ci-local-parity.yml +11 -0
  79. package/src/config/continuity-surface.json +9 -2
  80. package/src/config/evaluator-budgets.json +9 -8
  81. package/src/config/gate-violation-baselines.json +3 -3
  82. package/src/config/gitignore-block.txt +6 -5
  83. package/src/config/hook-bundle-budget.json +46 -6
  84. package/src/config/pack-size-budget.json +12 -1
  85. package/src/config/packed-binary-manifest.json +18 -0
  86. package/src/config/sigstore/npm_keys.json +50 -0
  87. package/src/config/sigstore/trust-material.json +9 -0
  88. package/src/config/sigstore/trusted_root.json +126 -0
  89. package/src/config/skill-body-budget.json +7 -1
  90. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  91. package/src/scripts/_cli/cmd_update.ts +48 -20
  92. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  93. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  94. package/src/scripts/_dispatch.bash +4 -3
  95. package/src/scripts/_lib/confusables.ts +0 -19
  96. package/src/scripts/_lib/egress_normalize.ts +53 -23
  97. package/src/scripts/_lib/egress_rules.ts +8 -2
  98. package/src/scripts/_lib/egress_text.ts +5 -3
  99. package/src/scripts/_lib/egress_token.ts +7 -5
  100. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  101. package/src/scripts/_lib/git_common_dir.ts +10 -0
  102. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  103. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  104. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  105. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  106. package/src/scripts/_lib/lineage_map.ts +55 -0
  107. package/src/scripts/_lib/lineage_query.ts +140 -0
  108. package/src/scripts/_lib/lineage_seed.ts +116 -0
  109. package/src/scripts/_lib/lineage_watch.ts +249 -0
  110. package/src/scripts/_lib/link_crypto.ts +8 -4
  111. package/src/scripts/_lib/listing_manifests.ts +85 -0
  112. package/src/scripts/_lib/neighbour_census.ts +1 -0
  113. package/src/scripts/_lib/production_reach.ts +3 -3
  114. package/src/scripts/_lib/project_registry.ts +227 -0
  115. package/src/scripts/_lib/provenance_verify.ts +14 -242
  116. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  117. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  118. package/src/scripts/_lib/source_id.ts +48 -0
  119. package/src/scripts/_lib/source_shape.ts +4 -0
  120. package/src/scripts/_lib/unattended_gates.ts +6 -1
  121. package/src/scripts/adoption_status.ts +231 -27
  122. package/src/scripts/advisories.ts +26 -10
  123. package/src/scripts/ai_council/council_egress.ts +5 -6
  124. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  125. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  126. package/src/scripts/check_dependency_floors.ts +14 -0
  127. package/src/scripts/check_egress_mutations.ts +1 -0
  128. package/src/scripts/check_release_manifest_content.ts +17 -2
  129. package/src/scripts/check_release_pr_shape.ts +5 -0
  130. package/src/scripts/code_graph/build.ts +145 -45
  131. package/src/scripts/code_graph/cli.ts +382 -35
  132. package/src/scripts/code_graph/conventions_default.ts +247 -0
  133. package/src/scripts/code_graph/detect.ts +5 -5
  134. package/src/scripts/code_graph/estate_cli.ts +135 -0
  135. package/src/scripts/code_graph/estate_index.ts +394 -0
  136. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  137. package/src/scripts/code_graph/estate_map.ts +141 -0
  138. package/src/scripts/code_graph/extract.ts +8 -2
  139. package/src/scripts/code_graph/extract_more.ts +241 -2
  140. package/src/scripts/code_graph/graph_budget.ts +158 -0
  141. package/src/scripts/code_graph/graph_home.ts +80 -0
  142. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  143. package/src/scripts/code_graph/loader.ts +10 -2
  144. package/src/scripts/code_graph/map.ts +195 -53
  145. package/src/scripts/code_graph/map_model.ts +113 -20
  146. package/src/scripts/code_graph/map_verify.ts +80 -5
  147. package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
  148. package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
  149. package/src/scripts/code_graph/passes.ts +173 -0
  150. package/src/scripts/code_graph/project_ref.ts +25 -0
  151. package/src/scripts/code_graph/projects_cli.ts +254 -0
  152. package/src/scripts/code_graph/providers/angular.ts +55 -0
  153. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  154. package/src/scripts/code_graph/providers/catalog.ts +299 -0
  155. package/src/scripts/code_graph/providers/django.ts +206 -0
  156. package/src/scripts/code_graph/providers/express.ts +25 -0
  157. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  158. package/src/scripts/code_graph/providers/flask.ts +99 -0
  159. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  160. package/src/scripts/code_graph/providers/index.ts +103 -0
  161. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  162. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  163. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  164. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  165. package/src/scripts/code_graph/providers/rails.ts +204 -0
  166. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  167. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  168. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  169. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  170. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  171. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  172. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  173. package/src/scripts/code_graph/providers/spring.ts +175 -0
  174. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  175. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  176. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  177. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  178. package/src/scripts/code_graph/providers/types.ts +56 -0
  179. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  180. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  181. package/src/scripts/code_graph/query.ts +9 -9
  182. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  183. package/src/scripts/code_graph/refresh_lock.ts +197 -0
  184. package/src/scripts/code_graph/repo_identity.ts +169 -0
  185. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  186. package/src/scripts/code_graph/scip_reader.ts +2 -2
  187. package/src/scripts/code_graph/sqlite_store.ts +31 -28
  188. package/src/scripts/code_graph/types.ts +79 -41
  189. package/src/scripts/code_graph/validate.ts +6 -8
  190. package/src/scripts/condense.ts +15 -15
  191. package/src/scripts/generate_settings_reference.ts +2 -6
  192. package/src/scripts/hook_chunks.mjs +11 -0
  193. package/src/scripts/hook_manifest.json +1 -1
  194. package/src/scripts/hook_manifest.yaml +43 -23
  195. package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
  196. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  197. package/src/scripts/hooks/host_semantics.ts +72 -0
  198. package/src/scripts/hooks/lazy_concern.ts +8 -0
  199. package/src/scripts/hooks/user_notice.ts +105 -0
  200. package/src/scripts/install-hooks.sh +54 -33
  201. package/src/scripts/install.ts +2 -6
  202. package/src/scripts/lint_harvest_provenance.ts +137 -11
  203. package/src/scripts/lint_hook_manifest.ts +2 -0
  204. package/src/scripts/lint_marketplace.ts +33 -0
  205. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  206. package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
  207. package/src/scripts/mcp_server/graph_tools.ts +165 -41
  208. package/src/scripts/network_sources.ts +24 -6
  209. package/src/scripts/release.ts +2 -5
  210. package/src/scripts/release_drill.ts +4 -1
  211. package/src/scripts/release_env.ts +17 -0
  212. package/src/scripts/release_mix_taxonomy.json +3 -0
  213. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  214. package/src/scripts/update_notice_hook.ts +75 -7
  215. package/src/server/schemas/settings.ts +42 -42
  216. package/src/shared/settingsSurface.ts +1 -1
  217. package/src/shared/userMd/schema.ts +1 -1
  218. package/src/shared/zodJsonSchema.ts +41 -0
  219. package/src/vendor/grammars/README.md +8 -6
  220. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  221. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  222. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  223. package/dist/ui/assets/index-w70Ado09.js +0 -23
  224. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  225. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * The estate index: a small, local, user-global table of what each registered
3
+ * project declares, publishes and exports (ADR-299, Phase 4). Never a merged
4
+ * graph — it stores package names, top-level symbol names and the names a
5
+ * project references but does not resolve, and the cross-project edges derived
6
+ * from them. Off by default (council O1): with the registry switch off nothing
7
+ * here runs and no file is written.
8
+ *
9
+ * `runtime/projects/index-v1.sqlite3`. A project's rows are replaced, never
10
+ * appended, on its next refresh. Edges are labelled by how they are known:
11
+ *
12
+ * package A declares a dependency on a package B publishes — a stated fact
13
+ * unique-name A references a name exactly one estate symbol declares — a guess
14
+ * mention A references B's name without declaring it — a guess
15
+ */
16
+ import { createHash } from 'node:crypto';
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+
20
+ import { readRegistry, registryDir, type RegistryOptions, repoIdOf } from '../_lib/project_registry.js';
21
+ import { isSqliteAvailableSync, loadSqliteSync } from '../_lib/sqlite_guard.js';
22
+ import { isTestFile } from './build.js';
23
+ import { type ManifestFacts, manifestFacts } from './estate_manifests.js';
24
+ import { openGraphIndex, sqliteTwinPath, twinIsCurrent } from './sqlite_store.js';
25
+ import type { CodeGraph } from './types.js';
26
+
27
+ export type XVia = 'package' | 'unique-name' | 'mention';
28
+
29
+ export interface GraphFacts {
30
+ exports: Array<{ name: string; kind: string }>;
31
+ imports: Array<{ package: string; uses: number }>;
32
+ refs: string[];
33
+ }
34
+
35
+ export interface XEdge {
36
+ source: string;
37
+ target: string;
38
+ via: XVia;
39
+ detail: string;
40
+ }
41
+
42
+ /** Stored as their index here: a kind name per row would be most of an export row's bytes. */
43
+ const KINDS = ['class', 'interface', 'trait', 'function', 'constant', 'type', 'enum'] as const;
44
+ const EXPORT_KINDS: ReadonlySet<string> = new Set(KINDS);
45
+
46
+ export function indexPath(opts: RegistryOptions = {}): string {
47
+ return path.join(registryDir(opts), 'index-v1.sqlite3');
48
+ }
49
+
50
+ /**
51
+ * Top-level declarations outside test files. The graph records no
52
+ * exportedness (`dead` refuses for the same reason), so a top-level symbol is
53
+ * the closest stated fact; a member (`A::m`) never counts.
54
+ */
55
+ function isTopLevel(id: string, kind: string): boolean {
56
+ return EXPORT_KINDS.has(kind) && !id.includes('::') && id.split('#').length === 2;
57
+ }
58
+
59
+ /** `targets` are distinct edge targets with how many edges point at each. */
60
+ function factsFrom(
61
+ nodes: Iterable<{ id: string; label: string; kind: string | null; source_file: string | null }>,
62
+ targets: Iterable<readonly [string, number]>,
63
+ ): GraphFacts {
64
+ const exports = new Map<string, { name: string; kind: string }>();
65
+ const testFile = new Map<string, boolean>();
66
+ for (const n of nodes) {
67
+ const file = n.source_file ?? '';
68
+ let isTest = testFile.get(file);
69
+ if (isTest === undefined) testFile.set(file, (isTest = isTestFile(file)));
70
+ if (n.kind && !isTest && isTopLevel(n.id, n.kind)) exports.set(n.id, { name: n.label, kind: n.kind });
71
+ }
72
+ const imports = new Map<string, number>();
73
+ const refs = new Set<string>();
74
+ for (const [t, n] of targets) {
75
+ if (t.startsWith('external:')) {
76
+ const hash = t.indexOf('#');
77
+ const pkg = t.slice('external:'.length, hash < 0 ? undefined : hash);
78
+ if (pkg) imports.set(pkg, (imports.get(pkg) ?? 0) + n);
79
+ } else if (t.startsWith('symbol:')) {
80
+ refs.add(t.slice('symbol:'.length));
81
+ }
82
+ }
83
+ return {
84
+ exports: [...exports.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
85
+ imports: [...imports.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([p, uses]) => ({ package: p, uses })),
86
+ refs: [...refs].sort(),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * The graph facts of one cache: from the graph in hand, else from its current
92
+ * SQLite twin (two indexed column reads), else by parsing the JSON.
93
+ */
94
+ export function graphFacts(cachePath: string, graph: CodeGraph | null = null): GraphFacts | null {
95
+ if (graph) {
96
+ const counts = new Map<string, number>();
97
+ for (const e of graph.edges) counts.set(e.target, (counts.get(e.target) ?? 0) + 1);
98
+ return factsFrom(
99
+ graph.nodes.map((n) => ({ id: n.id, label: n.label, kind: n.kind, source_file: n.source_file })),
100
+ counts,
101
+ );
102
+ }
103
+ if (twinIsCurrent(cachePath)) {
104
+ try {
105
+ const { DatabaseSync } = loadSqliteSync('code_graph.estate_index');
106
+ const db = new DatabaseSync(sqliteTwinPath(cachePath), { readOnly: true });
107
+ try {
108
+ const kinds = [...EXPORT_KINDS].map((k) => `'${k}'`).join(', ');
109
+ const nodes = db.prepare(`SELECT id, label, kind, source_file FROM nodes WHERE kind IN (${kinds}) AND instr(id, '::') = 0`).all() as Array<{
110
+ id: string;
111
+ label: string;
112
+ kind: string | null;
113
+ source_file: string | null;
114
+ }>;
115
+ const targets = (
116
+ db
117
+ // Ranges, not LIKE: they use the `edges_target` index.
118
+ .prepare(
119
+ 'SELECT target, COUNT(*) AS n FROM edges WHERE ' +
120
+ "(target >= 'external:' AND target < 'external;') OR (target >= 'symbol:' AND target < 'symbol;') GROUP BY target",
121
+ )
122
+ .all() as Array<{ target: string; n: number }>
123
+ ).map((r) => [r.target, Number(r.n)] as const);
124
+ return factsFrom(nodes, targets);
125
+ } finally {
126
+ db.close();
127
+ }
128
+ } catch {
129
+ /* fall through to the JSON */
130
+ }
131
+ }
132
+ try {
133
+ return graphFacts(cachePath, JSON.parse(fs.readFileSync(cachePath, 'utf-8')) as CodeGraph);
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ // Rows reference a project by its integer `key`, not its 12-hex id: the id
140
+ // repeated on every export row was a third of the file (measured on the 4.4
141
+ // fixture estate), and the size budget is 2 % of the graphs summarised.
142
+ const SCHEMA =
143
+ 'CREATE TABLE IF NOT EXISTS projects (key INTEGER PRIMARY KEY, id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, root TEXT NOT NULL, indexed_at TEXT NOT NULL, facts_key TEXT);' +
144
+ 'CREATE TABLE IF NOT EXISTS declares (project INTEGER NOT NULL, ecosystem TEXT NOT NULL, package TEXT NOT NULL, manifest TEXT NOT NULL);' +
145
+ 'CREATE TABLE IF NOT EXISTS publishes (project INTEGER NOT NULL, ecosystem TEXT NOT NULL, package TEXT NOT NULL);' +
146
+ 'CREATE TABLE IF NOT EXISTS exports (project INTEGER NOT NULL, kind INTEGER NOT NULL, name TEXT NOT NULL);' +
147
+ 'CREATE TABLE IF NOT EXISTS imports (project INTEGER NOT NULL, package TEXT NOT NULL, uses INTEGER NOT NULL);' +
148
+ 'CREATE TABLE IF NOT EXISTS refs (project INTEGER NOT NULL, name TEXT NOT NULL);' +
149
+ 'CREATE TABLE IF NOT EXISTS xedges (source INTEGER NOT NULL, target INTEGER NOT NULL, via TEXT NOT NULL, detail TEXT NOT NULL);';
150
+
151
+ const PER_PROJECT = ['declares', 'publishes', 'exports', 'imports', 'refs'];
152
+
153
+ /** Rebuild `xedges` from the per-project rows; a package edge suppresses both guesses for its pair. */
154
+ const XEDGES_SQL =
155
+ 'DELETE FROM xedges;' +
156
+ "INSERT INTO xedges SELECT DISTINCT d.project, p.project, 'package', d.package FROM declares d " +
157
+ 'JOIN publishes p ON p.package = d.package AND p.ecosystem = d.ecosystem WHERE d.project <> p.project;' +
158
+ "INSERT INTO xedges SELECT DISTINCT r.project, e.project, 'unique-name', r.name FROM refs r JOIN exports e ON e.name = r.name " +
159
+ 'WHERE e.project <> r.project AND r.name IN (SELECT name FROM exports GROUP BY name HAVING COUNT(*) = 1) ' +
160
+ "AND NOT EXISTS (SELECT 1 FROM xedges x WHERE x.source = r.project AND x.target = e.project AND x.via = 'package');" +
161
+ "INSERT INTO xedges SELECT DISTINCT m.project, p.key, 'mention', m.name FROM " +
162
+ '(SELECT project, name FROM refs UNION SELECT project, package FROM imports) m JOIN projects p ON lower(p.name) = lower(m.name) ' +
163
+ "WHERE p.key <> m.project AND NOT EXISTS (SELECT 1 FROM xedges x WHERE x.source = m.project AND x.target = p.key AND x.via = 'package');";
164
+
165
+ type Db = InstanceType<ReturnType<typeof loadSqliteSync>['DatabaseSync']>;
166
+
167
+ function openWritable(opts: RegistryOptions): Db {
168
+ const file = indexPath(opts);
169
+ fs.mkdirSync(path.dirname(file), { recursive: true });
170
+ const { DatabaseSync } = loadSqliteSync('code_graph.estate_index');
171
+ const fresh = !fs.existsSync(file);
172
+ const db = new DatabaseSync(file);
173
+ // Small pages: the index holds names only, and its size budget (4.4) is
174
+ // measured against the project graphs it summarises.
175
+ if (fresh) db.exec('PRAGMA page_size = 1024');
176
+ db.exec('PRAGMA busy_timeout = 2000');
177
+ db.exec(SCHEMA);
178
+ return db;
179
+ }
180
+
181
+ /** Replace one project's rows and re-derive the cross-project edges. */
182
+ export function writeProjectRows(
183
+ id: string,
184
+ root: string,
185
+ facts: ManifestFacts & GraphFacts,
186
+ opts: RegistryOptions = {},
187
+ factsKey: string | null = null,
188
+ ): void {
189
+ const db = openWritable(opts);
190
+ try {
191
+ db.exec('BEGIN IMMEDIATE');
192
+ try {
193
+ db.prepare(
194
+ 'INSERT INTO projects (id, name, root, indexed_at, facts_key) VALUES (?, ?, ?, ?, ?) ' +
195
+ 'ON CONFLICT(id) DO UPDATE SET name = excluded.name, root = excluded.root, indexed_at = excluded.indexed_at, facts_key = excluded.facts_key',
196
+ ).run(id, path.basename(root), root, (opts.now ?? new Date()).toISOString(), factsKey);
197
+ const key = (db.prepare('SELECT key FROM projects WHERE id = ?').get(id) as { key: number }).key;
198
+ for (const t of PER_PROJECT) db.prepare(`DELETE FROM ${t} WHERE project = ?`).run(key);
199
+ const dec = db.prepare('INSERT INTO declares VALUES (?, ?, ?, ?)');
200
+ for (const d of facts.declares) dec.run(key, d.ecosystem, d.package, d.manifest);
201
+ const pub = db.prepare('INSERT INTO publishes VALUES (?, ?, ?)');
202
+ for (const p of facts.publishes) pub.run(key, p.ecosystem, p.package);
203
+ const exp = db.prepare('INSERT INTO exports VALUES (?, ?, ?)');
204
+ for (const e of facts.exports) exp.run(key, KINDS.indexOf(e.kind as (typeof KINDS)[number]), e.name);
205
+ const imp = db.prepare('INSERT INTO imports VALUES (?, ?, ?)');
206
+ for (const i of facts.imports) imp.run(key, i.package, i.uses);
207
+ const ref = db.prepare('INSERT INTO refs VALUES (?, ?)');
208
+ for (const r of facts.refs) ref.run(key, r);
209
+ db.exec(XEDGES_SQL);
210
+ db.exec('COMMIT');
211
+ } catch (e) {
212
+ db.exec('ROLLBACK');
213
+ throw e;
214
+ }
215
+ // Replacing rows leaves free pages; compact only once they are a real share.
216
+ const free = (db.prepare('PRAGMA freelist_count').get() as { freelist_count: number }).freelist_count;
217
+ const pages = (db.prepare('PRAGMA page_count').get() as { page_count: number }).page_count;
218
+ if (free > 32 && free * 4 > pages) db.exec('VACUUM');
219
+ } finally {
220
+ db.close();
221
+ }
222
+ }
223
+
224
+ /** Remove a project from the index (a `projects remove` / `prune`). */
225
+ export function dropProjectRows(id: string, opts: RegistryOptions = {}): void {
226
+ if (!fs.existsSync(indexPath(opts)) || !isSqliteAvailableSync()) return;
227
+ const db = openWritable(opts);
228
+ try {
229
+ const row = db.prepare('SELECT key FROM projects WHERE id = ?').get(id) as { key: number } | undefined;
230
+ if (row) {
231
+ for (const t of PER_PROJECT) db.prepare(`DELETE FROM ${t} WHERE project = ?`).run(row.key);
232
+ db.prepare('DELETE FROM projects WHERE key = ?').run(row.key);
233
+ db.exec(XEDGES_SQL);
234
+ db.exec('VACUUM');
235
+ }
236
+ } finally {
237
+ db.close();
238
+ }
239
+ }
240
+
241
+ /**
242
+ * The refresh hook: index `root` when the owner switched the index on. Returns
243
+ * whether rows were written. Never throws — the index is a convenience and a
244
+ * failure to write it never fails a refresh.
245
+ */
246
+ export function indexAfterRefresh(id: string, root: string, cachePath: string, graph: CodeGraph | null, opts: RegistryOptions = {}): boolean {
247
+ try {
248
+ if (!readRegistry(opts).index || !isSqliteAvailableSync()) return false;
249
+ const manifests = manifestFacts(root);
250
+ // A refresh that changed neither the graph nor a manifest rewrites nothing:
251
+ // the graph's own content checksum plus the manifest rows decide.
252
+ const sum = graphChecksum(cachePath, graph);
253
+ const key = sum === null ? null : createHash('sha256').update(`${sum}\0${root}\0${JSON.stringify(manifests)}`).digest('hex');
254
+ if (key !== null && storedFactsKey(id, opts) === key) return true;
255
+ const g = graphFacts(cachePath, graph);
256
+ if (!g) return false;
257
+ writeProjectRows(id, root, { ...manifests, ...g }, opts, key);
258
+ return true;
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ function graphChecksum(cachePath: string, graph: CodeGraph | null): string | null {
265
+ if (graph) return graph.source_checksum || null;
266
+ const index = openGraphIndex(cachePath);
267
+ if (!index) return null;
268
+ try {
269
+ return index.sourceChecksum || null;
270
+ } finally {
271
+ index.close();
272
+ }
273
+ }
274
+
275
+ function storedFactsKey(id: string, opts: RegistryOptions): string | null {
276
+ const file = indexPath(opts);
277
+ if (!fs.existsSync(file)) return null;
278
+ const { DatabaseSync } = loadSqliteSync('code_graph.estate_index');
279
+ const db = new DatabaseSync(file, { readOnly: true });
280
+ try {
281
+ const row = db.prepare('SELECT facts_key FROM projects WHERE id = ?').get(id) as { facts_key: string | null } | undefined;
282
+ return row?.facts_key ?? null;
283
+ } catch {
284
+ return null;
285
+ } finally {
286
+ db.close();
287
+ }
288
+ }
289
+
290
+ /** A read-only view of the index; `null` when there is none. */
291
+ export function openEstate(opts: RegistryOptions = {}): EstateReader | null {
292
+ const file = indexPath(opts);
293
+ if (!fs.existsSync(file) || !isSqliteAvailableSync()) return null;
294
+ const { DatabaseSync } = loadSqliteSync('code_graph.estate_index');
295
+ return new EstateReader(new DatabaseSync(file, { readOnly: true }));
296
+ }
297
+
298
+ type Answer = { id: string; root: string };
299
+
300
+ /**
301
+ * One row per logical project: clones of one origin are indexed per checkout,
302
+ * so an answer folds them by repo id and lists every checkout root. `facet`
303
+ * keeps rows apart that differ in more than the project (an ecosystem, a kind).
304
+ */
305
+ function byRepo<T extends Answer>(rows: T[], facet: (r: T) => string = () => '', merge: (a: T, b: T) => T = (a) => a): Array<T & { repo_id: string; roots: string[] }> {
306
+ const out = new Map<string, T & { repo_id: string; roots: string[] }>();
307
+ for (const r of rows) {
308
+ const repo_id = repoIdOf(r.id);
309
+ const k = `${repo_id}\0${facet(r)}`;
310
+ const prev = out.get(k);
311
+ if (prev === undefined) out.set(k, { ...r, repo_id, roots: [r.root] });
312
+ else out.set(k, { ...merge(prev, r), repo_id, roots: prev.roots.includes(r.root) ? prev.roots : [...prev.roots, r.root] });
313
+ }
314
+ return [...out.values()];
315
+ }
316
+
317
+ export class EstateReader {
318
+ constructor(private readonly db: Db) {}
319
+
320
+ close(): void {
321
+ this.db.close();
322
+ }
323
+
324
+ projects(): Array<{ id: string; name: string; root: string; indexed_at: string }> {
325
+ return this.db.prepare('SELECT id, name, root, indexed_at FROM projects ORDER BY name, id').all() as Array<{
326
+ id: string;
327
+ name: string;
328
+ root: string;
329
+ indexed_at: string;
330
+ }>;
331
+ }
332
+
333
+ xedges(): XEdge[] {
334
+ return this.db
335
+ .prepare(
336
+ 'SELECT s.id AS source, t.id AS target, x.via, x.detail FROM xedges x JOIN projects s ON s.key = x.source ' +
337
+ 'JOIN projects t ON t.key = x.target ORDER BY s.id, t.id, x.via, x.detail',
338
+ )
339
+ .all() as unknown as XEdge[];
340
+ }
341
+
342
+ /** One project's rows of one table, with the project named by its id and an export's kind by name. */
343
+ rows(table: 'declares' | 'publishes' | 'exports' | 'imports' | 'refs', project: string): Array<Record<string, unknown>> {
344
+ const rows = this.db
345
+ .prepare(`SELECT r.* FROM ${table} r JOIN projects p ON p.key = r.project WHERE p.id = ? ORDER BY 2, 3`)
346
+ .all(project) as Array<Record<string, unknown>>;
347
+ return rows.map((r) => ({
348
+ ...r,
349
+ project,
350
+ ...(table === 'exports' ? { kind: KINDS[r['kind'] as number] ?? 'unknown' } : {}),
351
+ }));
352
+ }
353
+
354
+ /** Projects that declare `pkg` (any ecosystem) or import it in code. */
355
+ projectsUsing(pkg: string): Array<{ id: string; name: string; root: string; declared_in: string[]; imports: number; repo_id: string; roots: string[] }> {
356
+ const rows = this.db
357
+ .prepare(
358
+ 'SELECT p.id, p.name, p.root, ' +
359
+ "(SELECT group_concat(manifest, char(10)) FROM (SELECT DISTINCT manifest FROM declares WHERE project = p.key AND package = ?)) AS declared_in, " +
360
+ '(SELECT COALESCE(SUM(uses), 0) FROM imports WHERE project = p.key AND package = ?) AS imports ' +
361
+ 'FROM projects p WHERE EXISTS (SELECT 1 FROM declares WHERE project = p.key AND package = ?) ' +
362
+ 'OR EXISTS (SELECT 1 FROM imports WHERE project = p.key AND package = ?) ORDER BY p.name, p.id',
363
+ )
364
+ .all(pkg, pkg, pkg, pkg) as Array<{ id: string; name: string; root: string; declared_in: string | null; imports: number }>;
365
+ return byRepo(
366
+ rows.map((r) => ({ ...r, declared_in: r.declared_in ? r.declared_in.split('\n').sort() : [], imports: Number(r.imports) })),
367
+ undefined,
368
+ // Two checkouts of one project count its imports once, not twice.
369
+ (a, b) => ({ ...a, declared_in: [...new Set([...a.declared_in, ...b.declared_in])].sort(), imports: Math.max(a.imports, b.imports) }),
370
+ );
371
+ }
372
+
373
+ /** Projects that reference `name` without resolving it — candidates for a symbol another project declares. */
374
+ referrers(name: string): Array<{ id: string; name: string; root: string; repo_id: string; roots: string[] }> {
375
+ return byRepo(this.db
376
+ .prepare('SELECT DISTINCT p.id, p.name, p.root FROM refs r JOIN projects p ON p.key = r.project WHERE r.name = ? ORDER BY p.name, p.id')
377
+ .all(name) as Array<{ id: string; name: string; root: string }>);
378
+ }
379
+
380
+ /** Projects that publish package `pkg`. */
381
+ publishers(pkg: string): Array<{ id: string; name: string; root: string; ecosystem: string; repo_id: string; roots: string[] }> {
382
+ return byRepo(this.db
383
+ .prepare('SELECT DISTINCT p.id, p.name, p.root, b.ecosystem FROM publishes b JOIN projects p ON p.key = b.project WHERE b.package = ? ORDER BY p.name, p.id')
384
+ .all(pkg) as Array<{ id: string; name: string; root: string; ecosystem: string }>, (r) => r.ecosystem);
385
+ }
386
+
387
+ /** Projects that declare a top-level symbol named `name`. */
388
+ exporters(name: string): Array<{ id: string; name: string; root: string; kind: string; repo_id: string; roots: string[] }> {
389
+ const rows = this.db
390
+ .prepare('SELECT p.id, p.name, p.root, e.kind FROM exports e JOIN projects p ON p.key = e.project WHERE e.name = ? ORDER BY p.name, p.id')
391
+ .all(name) as Array<{ id: string; name: string; root: string; kind: number }>;
392
+ return byRepo(rows.map((r) => ({ ...r, kind: KINDS[r.kind] ?? 'unknown' })), (r) => r.kind);
393
+ }
394
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * What a project's manifests declare and publish, for the estate index
3
+ * (ADR-299, step 4.2). Read from the same manifest set the providers detect a
4
+ * stack by (`readProjectProbe`), plus `Cargo.toml`. Line-level parsing on
5
+ * purpose: the index needs package names, not a resolver, and a malformed
6
+ * manifest costs its own rows rather than the refresh.
7
+ */
8
+ import { execFileSync } from 'node:child_process';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+
12
+ import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
13
+ import { MANIFEST_NAME, readProjectProbe, walkedPath } from './build.js';
14
+
15
+ export type Ecosystem = 'npm' | 'composer' | 'pypi' | 'go' | 'cargo' | 'rubygems';
16
+
17
+ export interface Declared {
18
+ ecosystem: Ecosystem;
19
+ package: string;
20
+ manifest: string;
21
+ }
22
+
23
+ export interface Published {
24
+ ecosystem: Ecosystem;
25
+ package: string;
26
+ }
27
+
28
+ export interface ManifestFacts {
29
+ declares: Declared[];
30
+ publishes: Published[];
31
+ }
32
+
33
+ const PY_NAME = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/;
34
+
35
+ /** PEP 503 normalisation, so `Foo_Bar` and `foo-bar` are one package. */
36
+ function pyName(n: string): string {
37
+ return n.toLowerCase().replace(/[-_.]+/g, '-');
38
+ }
39
+
40
+ function json(text: string): Record<string, unknown> | null {
41
+ try {
42
+ const v = JSON.parse(text) as unknown;
43
+ return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function keys(v: unknown): string[] {
50
+ return v && typeof v === 'object' && !Array.isArray(v) ? Object.keys(v) : [];
51
+ }
52
+
53
+ /** `[section]` → its lines, for the flat TOML tables manifests use. */
54
+ function tomlSections(text: string): Map<string, string[]> {
55
+ const out = new Map<string, string[]>();
56
+ let cur = '';
57
+ out.set(cur, []);
58
+ for (const raw of text.split(/\r?\n/)) {
59
+ const line = raw.replace(/#.*$/, '').trimEnd();
60
+ const head = /^\s*\[\s*([^\]]+?)\s*\]\s*$/.exec(line);
61
+ if (head) {
62
+ cur = head[1] as string;
63
+ if (!out.has(cur)) out.set(cur, []);
64
+ continue;
65
+ }
66
+ out.get(cur)?.push(line);
67
+ }
68
+ return out;
69
+ }
70
+
71
+ function tomlKeys(lines: readonly string[] | undefined): string[] {
72
+ const out: string[] = [];
73
+ for (const l of lines ?? []) {
74
+ const m = /^\s*([A-Za-z0-9_.-]+|"[^"]+")\s*=/.exec(l);
75
+ if (m) out.push((m[1] as string).replace(/^"|"$/g, ''));
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function tomlString(lines: readonly string[] | undefined, key: string): string | null {
81
+ for (const l of lines ?? []) {
82
+ const m = new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']+)["']`).exec(l);
83
+ if (m) return m[1] as string;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /** The quoted entries of a TOML array that may span lines (`dependencies = [ … ]`). */
89
+ function tomlArray(lines: readonly string[] | undefined, key: string): string[] {
90
+ const body = (lines ?? []).join('\n');
91
+ const m = new RegExp(`^\\s*${key}\\s*=\\s*\\[([\\s\\S]*?)\\]`, 'm').exec(body);
92
+ if (!m) return [];
93
+ return [...(m[1] as string).matchAll(/["']([^"']+)["']/g)].map((x) => x[1] as string);
94
+ }
95
+
96
+ function parse(rel: string, text: string, facts: ManifestFacts): void {
97
+ const name = path.posix.basename(rel);
98
+ const dec = (ecosystem: Ecosystem, pkg: string): void => {
99
+ if (pkg) facts.declares.push({ ecosystem, package: pkg, manifest: rel });
100
+ };
101
+ const pub = (ecosystem: Ecosystem, pkg: unknown): void => {
102
+ if (typeof pkg === 'string' && pkg) facts.publishes.push({ ecosystem, package: pkg });
103
+ };
104
+ if (name === 'package.json') {
105
+ const j = json(text);
106
+ if (!j) return;
107
+ pub('npm', j['name']);
108
+ for (const f of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) for (const k of keys(j[f])) dec('npm', k);
109
+ } else if (name === 'composer.json') {
110
+ const j = json(text);
111
+ if (!j) return;
112
+ pub('composer', typeof j['name'] === 'string' ? j['name'].toLowerCase() : null);
113
+ for (const f of ['require', 'require-dev']) {
114
+ for (const k of keys(j[f])) if (k !== 'php' && !k.startsWith('ext-') && !k.startsWith('lib-')) dec('composer', k.toLowerCase());
115
+ }
116
+ } else if (name === 'pyproject.toml') {
117
+ const s = tomlSections(text);
118
+ const own = tomlString(s.get('project'), 'name') ?? tomlString(s.get('tool.poetry'), 'name');
119
+ if (own) pub('pypi', pyName(own));
120
+ for (const d of tomlArray(s.get('project'), 'dependencies')) {
121
+ const m = PY_NAME.exec(d);
122
+ if (m) dec('pypi', pyName(m[1] as string));
123
+ }
124
+ for (const sec of ['tool.poetry.dependencies', 'tool.poetry.dev-dependencies', 'tool.poetry.group.dev.dependencies']) {
125
+ for (const k of tomlKeys(s.get(sec))) if (k !== 'python') dec('pypi', pyName(k));
126
+ }
127
+ } else if (/^requirements[\w.-]*\.txt$/.test(name)) {
128
+ for (const line of text.split(/\r?\n/)) {
129
+ const l = line.replace(/#.*$/, '').trim();
130
+ if (!l || l.startsWith('-')) continue;
131
+ const m = PY_NAME.exec(l);
132
+ if (m) dec('pypi', pyName(m[1] as string));
133
+ }
134
+ } else if (name === 'go.mod') {
135
+ let block = false;
136
+ for (const raw of text.split(/\r?\n/)) {
137
+ const l = raw.replace(/\/\/.*$/, '').trim();
138
+ const mod = /^module\s+(\S+)/.exec(l);
139
+ if (mod) pub('go', mod[1]);
140
+ if (/^require\s*\($/.test(l)) {
141
+ block = true;
142
+ continue;
143
+ }
144
+ if (block && l === ')') {
145
+ block = false;
146
+ continue;
147
+ }
148
+ const req = block ? /^(\S+)\s+\S+/.exec(l) : /^require\s+(\S+)\s+\S+/.exec(l);
149
+ if (req) dec('go', req[1] as string);
150
+ }
151
+ } else if (name === 'Cargo.toml') {
152
+ const s = tomlSections(text);
153
+ pub('cargo', tomlString(s.get('package'), 'name'));
154
+ for (const sec of ['dependencies', 'dev-dependencies', 'build-dependencies']) for (const k of tomlKeys(s.get(sec))) dec('cargo', k);
155
+ } else if (name === 'Gemfile') {
156
+ for (const m of text.matchAll(/^\s*gem\s+["']([^"']+)["']/gm)) dec('rubygems', m[1] as string);
157
+ }
158
+ }
159
+
160
+ function dedupe<T>(rows: T[]): T[] {
161
+ const seen = new Set<string>();
162
+ return rows.filter((r) => {
163
+ const k = JSON.stringify(r);
164
+ if (seen.has(k)) return false;
165
+ seen.add(k);
166
+ return true;
167
+ });
168
+ }
169
+
170
+ const isManifest = (name: string): boolean => name === 'Cargo.toml' || MANIFEST_NAME.test(name);
171
+ const ROOT_MANIFESTS = ['package.json', 'composer.json', 'pyproject.toml', 'requirements.txt', 'go.mod', 'Cargo.toml', 'Gemfile'];
172
+
173
+ /**
174
+ * Manifest paths from git's index, plus an untracked one at the root, then the
175
+ * discovery walk's own directory filter. A refresh already walked the tree
176
+ * once; this keeps the index from walking it a second time (6.4 measured the
177
+ * walk, and then git's untracked scan, as most of the index's cost). `null`
178
+ * outside a repository.
179
+ */
180
+ function manifestPathsFromGit(root: string): string[] | null {
181
+ let out: string;
182
+ try {
183
+ out = execFileSync('git', ['-C', root, 'ls-files', '-z', '--cached'], {
184
+ env: hardenedSpawnEnv(),
185
+ encoding: 'utf-8',
186
+ stdio: ['ignore', 'pipe', 'ignore'],
187
+ maxBuffer: 64 * 1024 * 1024,
188
+ });
189
+ } catch {
190
+ return null;
191
+ }
192
+ const rels = new Set(out.split('\0').filter((rel) => rel && isManifest(path.posix.basename(rel)) && walkedPath(rel)));
193
+ for (const name of ROOT_MANIFESTS) if (fs.existsSync(path.join(root, name))) rels.add(name);
194
+ return [...rels];
195
+ }
196
+
197
+ export function manifestFacts(root: string): ManifestFacts {
198
+ const facts: ManifestFacts = { declares: [], publishes: [] };
199
+ const texts = new Map<string, string>();
200
+ const fromGit = manifestPathsFromGit(root);
201
+ if (fromGit !== null) {
202
+ for (const rel of fromGit) {
203
+ try {
204
+ texts.set(rel, fs.readFileSync(path.join(root, rel), 'utf-8'));
205
+ } catch {
206
+ /* deleted in the working tree: no rows */
207
+ }
208
+ }
209
+ } else {
210
+ const probe = readProjectProbe(root);
211
+ for (const [rel, text] of probe.manifests) texts.set(rel, text);
212
+ for (const rel of probe.paths) {
213
+ if (path.posix.basename(rel) !== 'Cargo.toml') continue;
214
+ try {
215
+ texts.set(rel, fs.readFileSync(path.join(root, rel), 'utf-8'));
216
+ } catch {
217
+ /* unreadable: no rows */
218
+ }
219
+ }
220
+ }
221
+ for (const [rel, text] of [...texts.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) parse(rel, text, facts);
222
+ return { declares: dedupe(facts.declares), publishes: dedupe(facts.publishes) };
223
+ }