@event4u/agent-config 17.3.0 → 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 (137) 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/CHANGELOG.md +164 -342
  5. package/CREDITS.md +2 -2
  6. package/README.md +1 -1
  7. package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
  8. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  9. package/dist/cli/registry.js +2 -2
  10. package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
  11. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  12. package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
  13. package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
  14. package/dist/cli-delegate/cmd_conformance.js +175 -183
  15. package/dist/cli-delegate/cmd_doctor.js +5 -5
  16. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  17. package/dist/cli-delegate/cmd_explain.js +1 -1
  18. package/dist/cli-delegate/cmd_handoff.js +2 -2
  19. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  20. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  21. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  22. package/dist/cli-delegate/cmd_update.js +24 -13
  23. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  24. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  25. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  26. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  27. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +3 -3
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
  38. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
  39. package/dist/hooks/dispatch.js +982 -929
  40. package/dist/hooks/dispatch.sha256 +1 -1
  41. package/dist/hooks/egress_engine.chunk.cjs +56 -6
  42. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  43. package/dist/hooks/update_notice.chunk.cjs +914 -0
  44. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  45. package/dist/mcp/registry-manifest.json +1 -1
  46. package/dist/mcp/server.json +2 -2
  47. package/dist/mcp/server.mjs +71259 -66437
  48. package/dist/scripts/condense.js +9 -6
  49. package/package.json +4 -1
  50. package/src/config/evaluator-budgets.json +9 -8
  51. package/src/config/gate-violation-baselines.json +2 -2
  52. package/src/config/hook-bundle-budget.json +31 -6
  53. package/src/config/packed-binary-manifest.json +9 -0
  54. package/src/config/sigstore/npm_keys.json +50 -0
  55. package/src/config/sigstore/trust-material.json +9 -0
  56. package/src/config/sigstore/trusted_root.json +126 -0
  57. package/src/config/skill-body-budget.json +7 -1
  58. package/src/scripts/_cli/cmd_update.ts +48 -20
  59. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  60. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  61. package/src/scripts/_dispatch.bash +3 -2
  62. package/src/scripts/_lib/confusables.ts +0 -19
  63. package/src/scripts/_lib/egress_normalize.ts +53 -23
  64. package/src/scripts/_lib/egress_text.ts +5 -3
  65. package/src/scripts/_lib/egress_token.ts +7 -5
  66. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  67. package/src/scripts/_lib/git_common_dir.ts +10 -0
  68. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  69. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  70. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  71. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  72. package/src/scripts/_lib/lineage_map.ts +55 -0
  73. package/src/scripts/_lib/lineage_query.ts +140 -0
  74. package/src/scripts/_lib/lineage_seed.ts +116 -0
  75. package/src/scripts/_lib/lineage_watch.ts +249 -0
  76. package/src/scripts/_lib/link_crypto.ts +8 -4
  77. package/src/scripts/_lib/production_reach.ts +3 -3
  78. package/src/scripts/_lib/project_registry.ts +227 -0
  79. package/src/scripts/_lib/provenance_verify.ts +14 -242
  80. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  81. package/src/scripts/_lib/source_id.ts +48 -0
  82. package/src/scripts/_lib/source_shape.ts +4 -0
  83. package/src/scripts/_lib/unattended_gates.ts +6 -1
  84. package/src/scripts/advisories.ts +26 -10
  85. package/src/scripts/ai_council/council_egress.ts +5 -6
  86. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  87. package/src/scripts/check_dependency_floors.ts +7 -0
  88. package/src/scripts/check_egress_mutations.ts +1 -0
  89. package/src/scripts/code_graph/build.ts +67 -3
  90. package/src/scripts/code_graph/cli.ts +131 -28
  91. package/src/scripts/code_graph/conventions_default.ts +20 -1
  92. package/src/scripts/code_graph/detect.ts +5 -5
  93. package/src/scripts/code_graph/estate_cli.ts +135 -0
  94. package/src/scripts/code_graph/estate_index.ts +394 -0
  95. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  96. package/src/scripts/code_graph/estate_map.ts +141 -0
  97. package/src/scripts/code_graph/extract.ts +4 -1
  98. package/src/scripts/code_graph/extract_more.ts +137 -2
  99. package/src/scripts/code_graph/graph_budget.ts +158 -0
  100. package/src/scripts/code_graph/graph_home.ts +80 -0
  101. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  102. package/src/scripts/code_graph/map.ts +22 -13
  103. package/src/scripts/code_graph/map_model.ts +67 -12
  104. package/src/scripts/code_graph/map_verify.ts +68 -5
  105. package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
  106. package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
  107. package/src/scripts/code_graph/project_ref.ts +25 -0
  108. package/src/scripts/code_graph/projects_cli.ts +254 -0
  109. package/src/scripts/code_graph/providers/catalog.ts +14 -5
  110. package/src/scripts/code_graph/providers/django.ts +52 -9
  111. package/src/scripts/code_graph/providers/index.ts +2 -0
  112. package/src/scripts/code_graph/providers/rails.ts +204 -0
  113. package/src/scripts/code_graph/providers/spring.ts +38 -1
  114. package/src/scripts/code_graph/query.ts +9 -9
  115. package/src/scripts/code_graph/refresh_lock.ts +12 -8
  116. package/src/scripts/code_graph/repo_identity.ts +169 -0
  117. package/src/scripts/code_graph/sqlite_store.ts +22 -25
  118. package/src/scripts/code_graph/types.ts +6 -2
  119. package/src/scripts/condense.ts +6 -6
  120. package/src/scripts/hook_chunks.mjs +7 -0
  121. package/src/scripts/hook_manifest.json +1 -1
  122. package/src/scripts/hook_manifest.yaml +20 -17
  123. package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
  124. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  125. package/src/scripts/hooks/host_semantics.ts +72 -0
  126. package/src/scripts/hooks/lazy_concern.ts +4 -0
  127. package/src/scripts/hooks/user_notice.ts +105 -0
  128. package/src/scripts/lint_harvest_provenance.ts +137 -11
  129. package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
  130. package/src/scripts/mcp_server/graph_tools.ts +161 -41
  131. package/src/scripts/network_sources.ts +24 -6
  132. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  133. package/src/scripts/update_notice_hook.ts +75 -7
  134. package/src/vendor/grammars/README.md +7 -6
  135. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  136. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  137. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `code-graph estate …` — questions about every registered project at once,
3
+ * answered from the estate index alone (ADR-299, step 5.3). No project graph
4
+ * is opened: an estate answer is as current as each project's last refresh,
5
+ * and says when it was indexed.
6
+ *
7
+ * estate where-used <package|symbol> who declares, imports, publishes or references it
8
+ * estate projects-using <package> projects that declare or import a package
9
+ * estate advisory-impact [--package P] advisory packages × the projects that use them
10
+ *
11
+ * `--json` prints the answer as JSON.
12
+ */
13
+ import { readState } from '../_lib/advisory_notice.js';
14
+ import { type EstateReader, openEstate } from './estate_index.js';
15
+
16
+ const NO_INDEX =
17
+ 'no estate index — switch it on with `code-graph projects index on`, then build or refresh each project once';
18
+
19
+ export interface WhereUsed {
20
+ name: string;
21
+ package_users: ReturnType<EstateReader['projectsUsing']>;
22
+ publishers: ReturnType<EstateReader['publishers']>;
23
+ declared_by: ReturnType<EstateReader['exporters']>;
24
+ referenced_by: ReturnType<EstateReader['referrers']>;
25
+ }
26
+
27
+ export interface AdvisoryImpact {
28
+ package: string;
29
+ advisories: string[];
30
+ projects: ReturnType<EstateReader['projectsUsing']>;
31
+ }
32
+
33
+ export function whereUsed(db: EstateReader, name: string): WhereUsed {
34
+ return {
35
+ name,
36
+ package_users: db.projectsUsing(name),
37
+ publishers: db.publishers(name),
38
+ declared_by: db.exporters(name),
39
+ referenced_by: db.referrers(name),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Advisory packages from each indexed project's own advisory cache (or the one
45
+ * `--package` names), joined with the index. A project that imports the
46
+ * package in code is the one an advisory reaches first; `imports` says so.
47
+ */
48
+ export function advisoryImpact(db: EstateReader, only: string | null): AdvisoryImpact[] {
49
+ const ids = new Map<string, Set<string>>();
50
+ if (only !== null) ids.set(only, new Set());
51
+ else {
52
+ for (const p of db.projects()) {
53
+ for (const f of readState(p.root)?.findings ?? []) {
54
+ if (!ids.has(f.package)) ids.set(f.package, new Set());
55
+ ids.get(f.package)?.add(f.id);
56
+ }
57
+ }
58
+ }
59
+ return [...ids.entries()]
60
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
61
+ .map(([pkg, adv]) => ({ package: pkg, advisories: [...adv].sort(), projects: db.projectsUsing(pkg) }))
62
+ .filter((r) => only !== null || r.projects.length > 0);
63
+ }
64
+
65
+ function flagValue(argv: string[], name: string): string | null {
66
+ const i = argv.indexOf(name);
67
+ return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
68
+ }
69
+
70
+ function line(p: { id: string; root: string }, extra = ''): string {
71
+ return ` ${p.id} ${p.root}${extra}\n`;
72
+ }
73
+
74
+ export function estateMain(argv: string[]): number {
75
+ const [verb, ...rest] = argv;
76
+ const json = rest.includes('--json');
77
+ const positional = rest.filter((a, i) => !a.startsWith('--') && rest[i - 1] !== '--package');
78
+ if (verb !== 'where-used' && verb !== 'projects-using' && verb !== 'advisory-impact') {
79
+ process.stderr.write('usage: code-graph estate where-used <name>|projects-using <package>|advisory-impact [--package P] [--json]\n');
80
+ return 2;
81
+ }
82
+ if (verb !== 'advisory-impact' && !positional[0]) {
83
+ process.stderr.write(`usage: code-graph estate ${verb} <name> [--json]\n`);
84
+ return 2;
85
+ }
86
+ const db = openEstate();
87
+ if (!db) {
88
+ process.stdout.write(`${NO_INDEX}\n`);
89
+ return 1;
90
+ }
91
+ try {
92
+ const name = positional[0] as string;
93
+ if (verb === 'projects-using') {
94
+ const rows = db.projectsUsing(name);
95
+ if (json) process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
96
+ else {
97
+ process.stdout.write(`projects using ${name} (${rows.length}):\n`);
98
+ for (const r of rows) process.stdout.write(line(r, `${r.declared_in.length ? ` · declared in ${r.declared_in.join(', ')}` : ''}${r.imports ? ` · imported ${r.imports}×` : ''}`));
99
+ }
100
+ return 0;
101
+ }
102
+ if (verb === 'where-used') {
103
+ const w = whereUsed(db, name);
104
+ if (json) {
105
+ process.stdout.write(`${JSON.stringify(w, null, 2)}\n`);
106
+ return 0;
107
+ }
108
+ const section = (title: string, rows: Array<{ id: string; root: string }>): void => {
109
+ if (!rows.length) return;
110
+ process.stdout.write(`${title} (${rows.length}):\n`);
111
+ for (const r of rows) process.stdout.write(line(r));
112
+ };
113
+ section(`declare or import package ${name}`, w.package_users);
114
+ section(`publish package ${name}`, w.publishers);
115
+ section(`declare a top-level symbol ${name}`, w.declared_by);
116
+ section(`reference ${name} without resolving it (a guess)`, w.referenced_by);
117
+ if (!w.package_users.length && !w.publishers.length && !w.declared_by.length && !w.referenced_by.length) {
118
+ process.stdout.write(`no registered project uses ${name}\n`);
119
+ }
120
+ return 0;
121
+ }
122
+ const impact = advisoryImpact(db, flagValue(rest, '--package'));
123
+ if (json) process.stdout.write(`${JSON.stringify(impact, null, 2)}\n`);
124
+ else {
125
+ if (!impact.length) process.stdout.write('no advisory package is used by a registered project\n');
126
+ for (const r of impact) {
127
+ process.stdout.write(`${r.package}${r.advisories.length ? ` — ${r.advisories.join(', ')}` : ''}:\n`);
128
+ for (const p of r.projects) process.stdout.write(line(p, p.imports ? ` · imported ${p.imports}×` : ' · declared only'));
129
+ }
130
+ }
131
+ return 0;
132
+ } finally {
133
+ db.close();
134
+ }
135
+ }
@@ -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
+ }