@event4u/agent-config 14.21.0 → 14.22.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 (181) 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 +198 -0
  5. package/CREDITS.md +37 -0
  6. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  7. package/dist/agent-src/commands/analyze/repo.md +4 -2
  8. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  9. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  10. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  11. package/dist/agent-src/rules/token-efficiency.md +26 -3
  12. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  13. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  14. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  15. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  16. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  17. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  18. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  19. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  20. package/dist/agent-src/templates/agent-settings.md +0 -1
  21. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  22. package/dist/cli-delegate/_cli_entry.js +8 -0
  23. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  24. package/dist/cli-delegate/archival_sweep.js +8 -0
  25. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  26. package/dist/cli-delegate/check_update_banner.js +923 -0
  27. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  28. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  29. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
  30. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  31. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  32. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  33. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  34. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  35. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  36. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  37. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  38. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  39. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  40. package/dist/cli-delegate/cmd_conformance.js +2 -2
  41. package/dist/cli-delegate/cmd_export.js +1 -1
  42. package/dist/cli-delegate/cmd_handoff.js +26 -2
  43. package/dist/cli-delegate/cmd_prune.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  45. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  46. package/dist/cli-delegate/cmd_sync.js +1 -1
  47. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  48. package/dist/cli-delegate/cmd_validate.js +1 -1
  49. package/dist/cli-delegate/dashboard_mode.js +16 -0
  50. package/dist/cli-delegate/gate_budget.js +24 -0
  51. package/dist/cli-delegate/gate_execute.js +25 -0
  52. package/dist/cli-delegate/guarded_baseline.js +18 -0
  53. package/dist/cli-delegate/resume_probe.js +22 -0
  54. package/dist/cli-delegate/roadmap_gates.js +638 -0
  55. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  56. package/dist/cli-delegate/stub_queue.js +47 -0
  57. package/dist/cli-delegate/stubs_due.js +38 -0
  58. package/dist/cli-delegate/ticket_bundles.js +59 -0
  59. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +8 -8
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +1 -1
  64. package/dist/discovery/dormancy-report.md +1 -1
  65. package/dist/discovery/orphan-report.md +1 -1
  66. package/dist/discovery/packs.json +2 -2
  67. package/dist/discovery/trust-report.md +1 -1
  68. package/dist/discovery/workspaces.json +2 -2
  69. package/dist/hooks/dispatch.js +1193 -917
  70. package/dist/install/claudeLayerCarriage.js +237 -0
  71. package/dist/install/claudePathsPlan.js +79 -0
  72. package/dist/install/hostLayerFingerprint.js +1 -1
  73. package/dist/install/install.mjs +6 -0
  74. package/dist/install/partitionEligibility.js +231 -117
  75. package/dist/install/ruleLayerPartition.js +10 -7
  76. package/dist/mcp/registry-manifest.json +2 -2
  77. package/dist/mcp/server.json +2 -2
  78. package/dist/mcp/server.mjs +39219 -38995
  79. package/dist/scripts/condense.js +32 -25
  80. package/docs/MIGRATION.md +49 -1
  81. package/docs/contracts/settings-classes.md +1 -1
  82. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  83. package/package.json +7 -4
  84. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  86. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  87. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  88. package/src/agent-src/scripts/stubs_due.ts +4 -18
  89. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  90. package/src/config/continuity-surface.json +509 -0
  91. package/src/config/estate-count-budget.json +1 -1
  92. package/src/config/gate-coverage.yml +29 -8
  93. package/src/config/gate-violation-baselines.json +8 -8
  94. package/src/config/gitignore-block.txt +19 -0
  95. package/src/config/host-injection-effect.json +73 -0
  96. package/src/config/packed-binary-manifest.json +33 -0
  97. package/src/config/preamble-payload-budget.json +112 -105
  98. package/src/config/publish-surface.json +4 -2
  99. package/src/install/claudeLayerCarriage.ts +273 -0
  100. package/src/install/claudePathsPlan.ts +76 -0
  101. package/src/install/hostLayerFingerprint.ts +1 -1
  102. package/src/install/partitionEligibility.ts +261 -127
  103. package/src/install/ruleLayerPartition.ts +19 -23
  104. package/src/scripts/_dispatch.bash +77 -27
  105. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  106. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  107. package/src/scripts/_lib/continuity_surface.ts +296 -0
  108. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  109. package/src/scripts/_lib/hook_settings.ts +1 -1
  110. package/src/scripts/_lib/injection_effect.ts +151 -0
  111. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  112. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  113. package/src/scripts/_lib/pin_resolver.ts +13 -0
  114. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  115. package/src/scripts/_lib/retired_status.ts +74 -0
  116. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  117. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  118. package/src/scripts/ai_council/api_transport.ts +5 -0
  119. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  120. package/src/scripts/ai_council/prompts.ts +9 -0
  121. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  122. package/src/scripts/capture_skill_catalogue.ts +24 -11
  123. package/src/scripts/check_candidate_lines.ts +499 -0
  124. package/src/scripts/check_continuity_surface.ts +475 -0
  125. package/src/scripts/check_dependency_floors.ts +13 -6
  126. package/src/scripts/check_estate_count.ts +40 -79
  127. package/src/scripts/check_pack_size.ts +262 -23
  128. package/src/scripts/check_roadmap_trackable.ts +19 -16
  129. package/src/scripts/check_rule_layer_partition.ts +54 -34
  130. package/src/scripts/check_single_delivery.ts +97 -31
  131. package/src/scripts/check_update_banner.ts +13 -0
  132. package/src/scripts/code_graph/build.ts +138 -15
  133. package/src/scripts/code_graph/cli.ts +76 -8
  134. package/src/scripts/code_graph/extract.ts +11 -0
  135. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  136. package/src/scripts/code_graph/loader.ts +88 -14
  137. package/src/scripts/code_graph/query.ts +121 -11
  138. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  139. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  140. package/src/scripts/code_graph/types.ts +66 -1
  141. package/src/scripts/code_graph/validate.ts +23 -0
  142. package/src/scripts/condense.ts +34 -22
  143. package/src/scripts/corpus_manifest.ts +5 -5
  144. package/src/scripts/council_cli.ts +11 -12
  145. package/src/scripts/count_intent_disagreement.ts +426 -0
  146. package/src/scripts/handoff_context_hook.ts +79 -0
  147. package/src/scripts/hook_effect_doctor.ts +82 -1
  148. package/src/scripts/hook_manifest.json +1 -1
  149. package/src/scripts/hook_manifest.yaml +64 -12
  150. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  151. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  152. package/src/scripts/hooks/concern_registry.ts +4 -2
  153. package/src/scripts/hooks/host_semantics.ts +1 -1
  154. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  155. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  156. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  157. package/src/scripts/inbox_source_census.ts +322 -0
  158. package/src/scripts/install-hooks.sh +82 -0
  159. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  160. package/src/scripts/lint_plan_risk_register.ts +13 -17
  161. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  162. package/src/scripts/mcp_server/tools.ts +2 -56
  163. package/src/scripts/media/lib/adapter-contract.md +7 -2
  164. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  165. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  166. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  167. package/src/scripts/rdp_candidates_delta.ts +276 -0
  168. package/src/scripts/rdp_quality_eval.ts +217 -19
  169. package/src/scripts/release_mix_taxonomy.json +3 -2
  170. package/src/scripts/report_host_injection_effect.ts +189 -0
  171. package/src/scripts/report_layer_overlap.ts +41 -0
  172. package/src/scripts/report_skill_menu_census.ts +341 -0
  173. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  174. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  175. package/src/vendor/grammars/README.md +61 -0
  176. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  177. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  178. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  179. package/src/scripts/_lib/carrier_status.ts +0 -118
  180. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  181. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -62,6 +62,8 @@ import {
62
62
  query,
63
63
  type QueryResult,
64
64
  } from './query.js';
65
+ import { openGraphIndex, sqliteTwinPath } from './sqlite_store.js';
66
+ import type { CodeGraph } from './types.js';
65
67
  import { validateGraph } from './validate.js';
66
68
 
67
69
  const _HERE = fileURLToPath(import.meta.url);
@@ -130,12 +132,33 @@ async function cmdBuild(argv: string[]): Promise<number> {
130
132
  `✅ code-graph ${update ? 'updated' : 'built'} — ${fileCount} files · ${graph.nodes.length} nodes · ${graph.edges.length} edges\n` +
131
133
  ` languages: ${graph.languages.join(', ') || '(none)'} · grammar ABI ${graph.grammar_abi}\n` +
132
134
  ` edges: EXTRACTED ${c.EXTRACTED} · INFERRED ${c.INFERRED} · AMBIGUOUS ${c.AMBIGUOUS}\n` +
135
+ ` resolved_via: ${resolvedViaHistogram(graph)}\n` +
133
136
  (update ? ` incremental: ${reExtracted} re-extracted · ${reused} reused\n` : '') +
134
137
  (out ? ` cache: ${path.relative(REPO_ROOT, out)}\n` : ''),
135
138
  );
136
139
  return 0;
137
140
  }
138
141
 
142
+ /**
143
+ * The `resolved_via` histogram, descending by count.
144
+ *
145
+ * Printed on every build because it is the number that says whether the graph
146
+ * is getting BETTER, and the confidence split does not: 2.3's whole target is
147
+ * moving edges off `name-lookup` onto a real binding, and a reader who cannot
148
+ * see the starting distribution cannot tell whether it moved. Only non-zero
149
+ * buckets appear — a row of zeroes for the four mechanisms nothing emits yet
150
+ * would be noise on every build.
151
+ */
152
+ function resolvedViaHistogram(graph: CodeGraph): string {
153
+ const counts = new Map<string, number>();
154
+ for (const e of graph.edges) counts.set(e.resolved_via, (counts.get(e.resolved_via) ?? 0) + 1);
155
+ if (counts.size === 0) return '(no edges)';
156
+ return [...counts.entries()]
157
+ .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
158
+ .map(([via, n]) => `${via} ${n}`)
159
+ .join(' · ');
160
+ }
161
+
139
162
  function cmdValidate(argv: string[]): number {
140
163
  const p = flag(argv, '--graph') ?? DEFAULT_CACHE;
141
164
  if (!fs.existsSync(p)) {
@@ -150,13 +173,57 @@ function cmdValidate(argv: string[]): number {
150
173
  return 1;
151
174
  }
152
175
  const r = validateGraph(parsed);
153
- if (r.ok) {
154
- process.stdout.write(`✅ graph schema valid: ${path.relative(REPO_ROOT, p)}\n`);
155
- return 0;
176
+ if (!r.ok) {
177
+ process.stderr.write(`❌ graph schema invalid (${r.errors.length}):\n`);
178
+ for (const e of r.errors) process.stderr.write(` ${e}\n`);
179
+ return 1;
180
+ }
181
+
182
+ // Twin ⇔ JSON, by checksum (road-to-a-graph-that-is-shipped 2.1).
183
+ //
184
+ // The schema check above says the JSON is well-formed. It says nothing
185
+ // about the derived store that now ANSWERS most queries, and since 2.1 the
186
+ // twin is read as an index rather than replayed as a blob — so a twin that
187
+ // disagreed with the JSON would produce different answers rather than a
188
+ // parse error. The comparison is `source_checksum`, which is the JSON's own
189
+ // content-addressed cache key, so agreement here means the twin was emitted
190
+ // from THIS graph and not merely from a graph.
191
+ const twinState = twinChecksumState(p, (parsed as CodeGraph).source_checksum);
192
+ const rel = path.relative(REPO_ROOT, p);
193
+ if (twinState.kind === 'mismatch') {
194
+ process.stderr.write(
195
+ `❌ SQLite twin disagrees with the canonical JSON: twin ${twinState.twin} ≠ json ${twinState.json}\n` +
196
+ ' The twin is derived and disposable — delete it and re-run `code-graph build`.\n' +
197
+ ` ${path.relative(REPO_ROOT, sqliteTwinPath(p))}\n`,
198
+ );
199
+ return 1;
200
+ }
201
+ process.stdout.write(`✅ graph schema valid: ${rel} · twin: ${twinState.kind}\n`);
202
+ return 0;
203
+ }
204
+
205
+ /**
206
+ * Compare the twin's stored `source_checksum` against the canonical JSON's.
207
+ *
208
+ * `absent` covers every state in which there is nothing to disagree WITH — no
209
+ * twin, no `node:sqlite`, a stale or corrupt twin the reader would refuse
210
+ * anyway. Those are not validation failures: the JSON is canonical and a
211
+ * missing accelerator changes no answer. Only a twin that is present, fresh,
212
+ * readable AND carrying a different checksum is a real contradiction.
213
+ */
214
+ function twinChecksumState(
215
+ jsonPath: string,
216
+ jsonChecksum: string,
217
+ ): { kind: 'absent' | 'match' } | { kind: 'mismatch'; twin: string; json: string } {
218
+ const index = openGraphIndex(jsonPath);
219
+ if (!index) return { kind: 'absent' };
220
+ try {
221
+ const twin = index.sourceChecksum;
222
+ if (twin && twin !== jsonChecksum) return { kind: 'mismatch', twin, json: jsonChecksum };
223
+ return { kind: 'match' };
224
+ } finally {
225
+ index.close();
156
226
  }
157
- process.stderr.write(`❌ graph schema invalid (${r.errors.length}):\n`);
158
- for (const e of r.errors) process.stderr.write(` ${e}\n`);
159
- return 1;
160
227
  }
161
228
 
162
229
  function resolveGraph(argv: string[]): { g: LoadedGraph; note: string } | { err: string } {
@@ -205,8 +272,9 @@ function changedNodeSeeds(g: LoadedGraph, root: string, ref: string): string[] {
205
272
  } catch {
206
273
  return [];
207
274
  }
208
- const set = new Set(files);
209
- return g.graph.nodes.filter((n) => set.has(n.source_file) && n.kind !== 'file').map((n) => n.id);
275
+ // Served by the `nodes_source_file` index on the SQLite path, so a wide
276
+ // diff does not force the whole node table into memory to answer it.
277
+ return g.idsInFiles(files);
210
278
  }
211
279
 
212
280
  function cmdQuery(kind: 'query' | 'explain' | 'affected' | 'path', argv: string[]): number {
@@ -67,6 +67,16 @@ export interface RawEdge {
67
67
  * searched for the remote one.
68
68
  */
69
69
  localName?: string;
70
+ /**
71
+ * `imports` only, PHP: the FULLY-QUALIFIED name as written (`App\\Svc\\Mailer`).
72
+ *
73
+ * `targetName` carries only the base name, because that is all the
74
+ * repo-wide lookup could use. Composer PSR-4 maps the NAMESPACE to a
75
+ * directory, so it needs the part `baseName()` discards — the same shape as
76
+ * `moduleSpecifier` for TS: the one piece of evidence that says where the
77
+ * name comes from, and it is right there in the source text.
78
+ */
79
+ fqName?: string;
70
80
  }
71
81
 
72
82
  export interface FileExtract {
@@ -137,6 +147,7 @@ function extractPhp(root: TsNode, file: string, out: FileExtract): void {
137
147
  relation: 'imports',
138
148
  targetName: baseName(nameNode.text),
139
149
  confidenceHint: 'EXTRACTED',
150
+ fqName: nameNode.text,
140
151
  });
141
152
  }
142
153
  }
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Report what the PRODUCTION loader makes of each grammar file given as an
4
+ * argument, as one line of JSON on stdout.
5
+ *
6
+ * A separate process because `check_pack_size.main` is synchronous while
7
+ * `Language.load` is not, and because keeping the probe out of the gate's own
8
+ * process means the gate cannot accidentally hold a loaded grammar's emscripten
9
+ * heap for the rest of its run.
10
+ *
11
+ * Never throws for a bad file: an unreadable or invalid grammar comes back as
12
+ * `{ ok: false, error }`, because "the loader refused this" is the ANSWER the
13
+ * eligibility predicate needs, not a crash it has to interpret.
14
+ */
15
+ import process from 'node:process';
16
+
17
+ import { probeGrammarFile } from './loader.js';
18
+
19
+ interface ProbeRow {
20
+ path: string;
21
+ ok: boolean;
22
+ abi?: number;
23
+ grammar_id?: string;
24
+ error?: string;
25
+ }
26
+
27
+ async function main(): Promise<number> {
28
+ const rows: ProbeRow[] = [];
29
+ for (const p of process.argv.slice(2)) {
30
+ try {
31
+ const r = await probeGrammarFile(p);
32
+ rows.push({ path: p, ok: true, abi: r.abi, grammar_id: r.grammar_id });
33
+ } catch (e) {
34
+ rows.push({ path: p, ok: false, error: (e as Error).message });
35
+ }
36
+ }
37
+ process.stdout.write(`${JSON.stringify(rows)}\n`);
38
+ return 0;
39
+ }
40
+
41
+ void main().then((c) => {
42
+ process.exitCode = c;
43
+ });
@@ -2,19 +2,29 @@
2
2
  * WASM tree-sitter loader for the code-graph engine.
3
3
  *
4
4
  * Pinned pair (ABI-verified 2026-07-23): `web-tree-sitter@0.24.7` +
5
- * `tree-sitter-wasms@0.1.13` → grammar ABI 14. The ABI smoke test lives in
6
- * `tests/scripts/code_graph.test.ts`; it runs in this repo because the pair is
7
- * a devDependency. Consumers install neither (see `INSTALL_HINT` below). No
8
- * network, no node-gyp — grammars load from `node_modules` via the resolved
9
- * path.
5
+ * grammar ABI 14. The ABI smoke test lives in
6
+ * `tests/scripts/code_graph.test.ts`.
7
+ *
8
+ * SHIPPED TO CONSUMERS (ADR-259, amended 2026-09-07). `web-tree-sitter` is a
9
+ * runtime `dependency`, and the three loadable grammars are vendored at
10
+ * `src/vendor/grammars/` and listed in `package.json` `files`. So a consumer
11
+ * install resolves both halves with no manual step. `tree-sitter-wasms` stays a
12
+ * devDependency — it is the source the vendored copies are refreshed from, and
13
+ * the fallback below keeps this repo's own dev flow (and any consumer holding
14
+ * the full 36-grammar pack) working unchanged.
15
+ *
16
+ * No network, no node-gyp, and no runtime grammar download — Kill register K1:
17
+ * both lookups below are filesystem reads of already-installed bytes.
10
18
  *
11
19
  * web-tree-sitter 0.24.x is CJS with a single default export (the Parser
12
20
  * class) and the classic `Parser.Language.load()` API; we reach it through
13
21
  * `createRequire` so this ESM module stays portable under the repo's tsx
14
22
  * runner.
15
23
  */
24
+ import * as fs from 'node:fs';
16
25
  import { createRequire } from 'node:module';
17
26
  import * as path from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
18
28
 
19
29
  import { EXPECTED_GRAMMAR_ABI, GRAMMAR_WASM, type Lang } from './types.js';
20
30
 
@@ -53,22 +63,35 @@ interface ParserStatic {
53
63
  }
54
64
 
55
65
  /**
56
- * The parser pair is a devDependency, not a runtime one: this repo's tests still
57
- * exercise the loader, while a consumer install resolves neither. The engine is
58
- * permanently `enabled: false` (measured recall 0.365 vs grep 0.797), so
59
- * shipping ~51 MB of WASM to every consumer for a path none of them can reach
60
- * was pure install cost. A consumer re-enabling the engine installs the
61
- * ABI-locked pair themselves — this message carries the pin, because
62
- * `check_dependency_floors` only scans the `dependencies` block.
66
+ * Reached only when BOTH grammar sources are missing — the vendored set that
67
+ * ships in the tarball and the devDependency pack. That is a broken or
68
+ * partially-extracted install, not the ordinary consumer state, so the message
69
+ * says how to repair rather than how to opt in.
70
+ *
71
+ * It still carries the exact pin because the pair is ABI-coupled and
72
+ * `check_dependency_floors` reads only version ranges, never this string.
63
73
  */
64
74
  const INSTALL_HINT =
65
- 'the code-graph engine needs its parser pair, which this package no longer ships to consumers ' +
66
- '(the engine is disabled by a recorded null result). Install the ABI-locked pair:\n' +
75
+ 'the code-graph engine could not find its grammars. They ship with this package at ' +
76
+ '`src/vendor/grammars/`, so this usually means an incomplete install — reinstall the ' +
77
+ 'package first. To supply them manually, install the ABI-locked pack:\n' +
67
78
  ' npm i web-tree-sitter@0.24.7 tree-sitter-wasms@0.1.13\n' +
68
79
  'Both versions are exact on purpose — the pair is ABI-coupled (grammar ABI 14) ' +
69
80
  'and must move together.';
70
81
 
82
+ /**
83
+ * Directory holding the grammars that ship inside the package.
84
+ *
85
+ * `../../..` from `src/scripts/code_graph/` is the package root — the same
86
+ * walk `_lib/agent_settings.ts` uses from `src/scripts/_lib/`. A wrong answer
87
+ * here is not a failure: `grammarWasmPath` only uses this path when the file is
88
+ * actually present, and otherwise falls through to the devDependency pack,
89
+ * which is exactly today's behaviour.
90
+ */
91
+ const VENDORED_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'vendor', 'grammars');
92
+
71
93
  let _wasmDir: string | null = null;
94
+ /** The devDependency grammar pack — the fallback source, and this repo's own. */
72
95
  function wasmDir(): string {
73
96
  if (_wasmDir === null) {
74
97
  let pkgJson: string;
@@ -86,7 +109,18 @@ let _initialized = false;
86
109
  const _langCache = new Map<Lang, TsLanguage>();
87
110
  const _parserCache = new Map<Lang, TsParser>();
88
111
 
112
+ /**
113
+ * Absolute path to a grammar, vendored set first.
114
+ *
115
+ * Order is load-bearing: the vendored set is what a consumer actually has, and
116
+ * it is ABI-pinned by the same change that wrote it. The devDependency pack is
117
+ * second so this repo keeps working when `src/vendor/grammars/` has not been
118
+ * refreshed, and so a consumer holding the full 36-grammar pack can reach a
119
+ * grammar the vendored three do not cover.
120
+ */
89
121
  export function grammarWasmPath(lang: Lang): string {
122
+ const vendored = path.join(VENDORED_DIR, GRAMMAR_WASM[lang]);
123
+ if (fs.existsSync(vendored)) return vendored;
90
124
  return path.join(wasmDir(), GRAMMAR_WASM[lang]);
91
125
  }
92
126
 
@@ -150,3 +184,43 @@ export async function getParser(lang: Lang): Promise<TsParser> {
150
184
  export function nodeHasError(n: TsNode): boolean {
151
185
  return typeof n.hasError === 'function' ? n.hasError() : n.hasError;
152
186
  }
187
+
188
+ /**
189
+ * Load an ARBITRARY grammar file through the production loader and report what
190
+ * it actually is.
191
+ *
192
+ * Exists for `check_pack_size`'s binary-eligibility predicate, and the "through
193
+ * the production loader" part is the whole point: the gate must not re-implement
194
+ * grammar loading, because a second implementation could accept bytes the engine
195
+ * rejects — which is precisely the gap that would let a broken grammar ship
196
+ * while the gate reported it valid. Same `_Parser()` instance, same
197
+ * `Language.load`, same ABI reading as `loadLanguage`.
198
+ *
199
+ * `grammar_id` comes from the module's exported `tree_sitter_<id>` symbol rather
200
+ * than from the `Language` object, because web-tree-sitter 0.24's `Language`
201
+ * exposes `version` and no name. `WebAssembly.Module.exports` is the runtime's
202
+ * own parser, so the id is read rather than guessed.
203
+ *
204
+ * Throws on anything it cannot establish — a caller that wants a verdict rather
205
+ * than an exception should catch. Never silently returns a partial answer.
206
+ */
207
+ export async function probeGrammarFile(wasmPath: string): Promise<{ abi: number; grammar_id: string }> {
208
+ const bytes = fs.readFileSync(wasmPath);
209
+ const mod = new WebAssembly.Module(bytes);
210
+ // A grammar module exports `tree_sitter_<id>` plus, usually, a family of
211
+ // `tree_sitter_<id>_external_scanner_*` helpers — measured: 6 such exports
212
+ // on php, 7 on typescript and javascript. The language id is the one
213
+ // WITHOUT the scanner suffix, and filtering for it leaves exactly one.
214
+ const ids = WebAssembly.Module.exports(mod)
215
+ .map((e) => e.name)
216
+ .filter((n) => n.startsWith('tree_sitter_') && !n.includes('_external_scanner_'))
217
+ .map((n) => n.slice('tree_sitter_'.length));
218
+ if (ids.length !== 1) {
219
+ throw new Error(
220
+ `grammar probe: expected exactly one tree_sitter_* export in ${wasmPath}, found ${String(ids.length)}`,
221
+ );
222
+ }
223
+ await _init();
224
+ const language = await _Parser().Language.load(wasmPath);
225
+ return { abi: language.version, grammar_id: ids[0] as string };
226
+ }
@@ -9,22 +9,114 @@ import * as fs from 'node:fs';
9
9
 
10
10
  import { LexicalIndex, tokenize } from '../_lib/lexical_index.js';
11
11
  import { sanitizeLabel } from './sanitize.js';
12
- import { emitSqliteTwin, loadSerializedFromTwin } from './sqlite_store.js';
13
- import type { CodeEdge, CodeGraph } from './types.js';
12
+ import {
13
+ emitSqliteTwin,
14
+ type GraphIndex,
15
+ INDEXED_READ_MIN_EDGES,
16
+ loadSerializedFromTwin,
17
+ openGraphIndex,
18
+ } from './sqlite_store.js';
19
+ import type { CodeEdge, CodeGraph, CodeNode } from './types.js';
14
20
  import { validateGraph } from './validate.js';
15
21
 
16
22
  const APPROX_CHARS_PER_TOKEN = 4;
17
23
 
24
+ /**
25
+ * The node/adjacency reads every verb performs.
26
+ *
27
+ * Declared as interfaces rather than `Map` so one set of verbs runs over two
28
+ * stores. `Map` satisfies both structurally, so the in-memory path is unchanged
29
+ * and needed no edits; the SQLite path supplies objects with the same two
30
+ * methods that hit an index instead of a heap.
31
+ */
32
+ export interface NodeLookup {
33
+ get(id: string): CodeNode | undefined;
34
+ has(id: string): boolean;
35
+ }
36
+ export interface AdjacencyLookup {
37
+ get(id: string): CodeEdge[] | undefined;
38
+ }
39
+
18
40
  export interface LoadedGraph {
19
- graph: CodeGraph;
41
+ /**
42
+ * The whole deserialized graph — `null` on the indexed path, where not
43
+ * materializing it is the entire point. Nothing in this module reads it;
44
+ * the two callers that used to (`resolveSeedsTiered`, the CLI's
45
+ * changed-file seeds) go through `idsByLabel` / `idsInFiles` instead, which
46
+ * answer the same questions from an index.
47
+ */
48
+ graph: CodeGraph | null;
20
49
  source: string; // attribution path
21
- byId: Map<string, CodeGraph['nodes'][number]>;
22
- out: Map<string, CodeEdge[]>;
23
- in: Map<string, CodeEdge[]>;
24
- lex: LexicalIndex;
50
+ byId: NodeLookup;
51
+ out: AdjacencyLookup;
52
+ in: AdjacencyLookup;
53
+ /** Exact-label resolution. */
54
+ idsByLabel(label: string, limit: number): string[];
55
+ /** Non-file nodes declared in any of `files`. */
56
+ idsInFiles(files: readonly string[]): string[];
57
+ /**
58
+ * BM25 fallback corpus, LAZY.
59
+ *
60
+ * A function rather than a field because on the indexed path building it
61
+ * reads every node — the one whole-table scan left — and a seed that
62
+ * resolves by exact id or exact label must never pay for it. On the
63
+ * in-memory path the corpus is already in hand and this just returns it.
64
+ */
65
+ lex(): LexicalIndex;
66
+ /** Release any handle the store holds. No-op for the in-memory path. */
67
+ close(): void;
68
+ }
69
+
70
+ /**
71
+ * A `LoadedGraph` backed by the SQLite index — no `JSON.parse`, no whole-graph
72
+ * `Map`.
73
+ *
74
+ * The adjacency lookups are per-call SQL against `edges_source` / `edges_target`
75
+ * rather than a materialized map, which is the difference the threshold buys: a
76
+ * 2-hop `affected` touches the rows for its frontier, not every edge in the
77
+ * repository. Node reads are memoized per handle because a BFS asks for the
78
+ * same label repeatedly while rendering edge lines, and that repetition is
79
+ * cheap to remove and expensive to leave.
80
+ */
81
+ function indexBackedGraph(index: GraphIndex, source: string): LoadedGraph {
82
+ const nodeCache = new Map<string, CodeNode | undefined>();
83
+ const readNode = (id: string): CodeNode | undefined => {
84
+ if (nodeCache.has(id)) return nodeCache.get(id);
85
+ const n = index.getNode(id);
86
+ nodeCache.set(id, n);
87
+ return n;
88
+ };
89
+ let lexical: LexicalIndex | null = null;
90
+ return {
91
+ graph: null,
92
+ source,
93
+ byId: { get: readNode, has: (id) => readNode(id) !== undefined },
94
+ out: { get: (id) => index.edgesFrom(id) },
95
+ in: { get: (id) => index.edgesTo(id) },
96
+ idsByLabel: (labelText, limit) => index.idsByLabel(labelText, limit),
97
+ idsInFiles: (files) => index.idsInFiles(files),
98
+ lex: () => {
99
+ lexical ??= new LexicalIndex(index.lexicalCorpus());
100
+ return lexical;
101
+ },
102
+ close: () => index.close(),
103
+ };
25
104
  }
26
105
 
27
106
  export function loadGraph(graphPath: string, source = graphPath): LoadedGraph {
107
+ // Above the threshold, read through the INDEX: the twin's rows answer
108
+ // per-node and per-edge questions directly, so neither `JSON.parse` nor the
109
+ // two whole-graph `Map`s below ever run. This is the D6 repair — the twin
110
+ // used to be preferred and still returned the entire serialization, which
111
+ // made it a cheaper blob transport rather than an index.
112
+ const index = openGraphIndex(graphPath);
113
+ if (index) {
114
+ if (index.edgeCount >= INDEXED_READ_MIN_EDGES) return indexBackedGraph(index, source);
115
+ // Small graph: the in-memory path is cheaper. Release the handle rather
116
+ // than leaking a database for the life of the process.
117
+ index.close();
118
+ }
119
+
28
120
  // Prefer the derived SQLite twin (ADR-129): checksum/stat-verified against
29
121
  // the canonical JSON, byte-identical content, ~1/90 the load cost at
30
122
  // consumer scale. Fallback = parse the JSON — and best-effort re-emit the
@@ -51,7 +143,23 @@ export function loadGraph(graphPath: string, source = graphPath): LoadedGraph {
51
143
  add(inM, e.target, e);
52
144
  }
53
145
  const lex = new LexicalIndex(graph.nodes.map((n) => ({ id: n.id, text: `${n.label} ${n.id}` })));
54
- return { graph, source, byId, out: outM, in: inM, lex };
146
+ return {
147
+ graph,
148
+ source,
149
+ byId,
150
+ out: outM,
151
+ in: inM,
152
+ idsByLabel: (labelText, limit) =>
153
+ graph.nodes.filter((n) => n.label === labelText).slice(0, limit).map((n) => n.id),
154
+ idsInFiles: (files) => {
155
+ const set = new Set(files);
156
+ return graph.nodes.filter((n) => set.has(n.source_file) && n.kind !== 'file').map((n) => n.id);
157
+ },
158
+ lex: () => lex,
159
+ close: () => {
160
+ /* nothing held */
161
+ },
162
+ };
55
163
  }
56
164
 
57
165
  interface SeedResolution {
@@ -64,9 +172,11 @@ interface SeedResolution {
64
172
  /** Resolve a free-text seed to node ids: exact id → exact label → BM25. */
65
173
  function resolveSeedsTiered(g: LoadedGraph, seed: string, limit = 5): SeedResolution {
66
174
  if (g.byId.has(seed)) return { ids: [seed], weak: false };
67
- const exactLabel = g.graph.nodes.filter((n) => n.label === seed).map((n) => n.id);
68
- if (exactLabel.length) return { ids: exactLabel.slice(0, limit), weak: false };
69
- const ranked = g.lex.rank(tokenize(seed)).filter((r) => r.score > 0);
175
+ const exactLabel = g.idsByLabel(seed, limit);
176
+ if (exactLabel.length) return { ids: exactLabel, weak: false };
177
+ // Only here does the BM25 corpus get built — the one whole-table read on
178
+ // the indexed path, and unreachable when the seed resolved exactly.
179
+ const ranked = g.lex().rank(tokenize(seed)).filter((r) => r.score > 0);
70
180
  return { ids: ranked.slice(0, limit).map((r) => r.id), weak: true };
71
181
  }
72
182