@event4u/agent-config 14.21.0 → 14.23.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 (235) 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 -187
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  8. package/dist/agent-src/commands/analyze/repo.md +4 -2
  9. package/dist/agent-src/commands/pr/merge.md +47 -38
  10. package/dist/agent-src/commands/roadmap/next.md +8 -0
  11. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  14. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  15. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  16. package/dist/agent-src/rules/token-efficiency.md +26 -3
  17. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  18. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  19. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  20. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  21. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  23. package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
  24. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  26. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/_cli_entry.js +8 -0
  31. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  32. package/dist/cli-delegate/archival_sweep.js +8 -0
  33. package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
  34. package/dist/cli-delegate/check_update_banner.js +923 -0
  35. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
  36. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  37. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  38. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  39. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  40. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  41. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
  42. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  43. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  44. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  45. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  46. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  47. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  48. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  49. package/dist/cli-delegate/cmd_conformance.js +2 -2
  50. package/dist/cli-delegate/cmd_export.js +2 -2
  51. package/dist/cli-delegate/cmd_handoff.js +45 -13
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  54. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/cli-delegate/dashboard_mode.js +16 -0
  60. package/dist/cli-delegate/gate_budget.js +24 -0
  61. package/dist/cli-delegate/gate_execute.js +25 -0
  62. package/dist/cli-delegate/guarded_baseline.js +18 -0
  63. package/dist/cli-delegate/resume_probe.js +22 -0
  64. package/dist/cli-delegate/roadmap_gates.js +638 -0
  65. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  66. package/dist/cli-delegate/stub_queue.js +47 -0
  67. package/dist/cli-delegate/stubs_due.js +38 -0
  68. package/dist/cli-delegate/ticket_bundles.js +59 -0
  69. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +13 -13
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +1 -1
  74. package/dist/discovery/dormancy-report.md +1 -1
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +2 -2
  77. package/dist/discovery/trust-report.md +1 -1
  78. package/dist/discovery/workspaces.json +2 -2
  79. package/dist/hooks/dispatch.js +1813 -946
  80. package/dist/install/claudeLayerCarriage.js +237 -0
  81. package/dist/install/claudePathsPlan.js +79 -0
  82. package/dist/install/hostLayerFingerprint.js +1 -1
  83. package/dist/install/install.mjs +21 -3
  84. package/dist/install/partitionEligibility.js +231 -117
  85. package/dist/install/ruleLayerPartition.js +10 -7
  86. package/dist/mcp/registry-manifest.json +2 -2
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +10870 -7641
  89. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  90. package/dist/scripts/_lib/router_match.js +243 -0
  91. package/dist/scripts/_lib/rule_injection.js +216 -0
  92. package/dist/scripts/condense.js +52 -45
  93. package/dist/scripts/project_thin_rules.js +139 -19
  94. package/dist/server/schemas/settings.js +16 -3
  95. package/docs/MIGRATION.md +105 -1
  96. package/docs/contracts/settings-classes.md +40 -8
  97. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  98. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  99. package/llms.txt +1 -1
  100. package/package.json +7 -4
  101. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  102. package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  103. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  104. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  105. package/src/agent-src/scripts/stubs_due.ts +4 -18
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  107. package/src/config/agent-settings.template.yml +47 -1
  108. package/src/config/continuity-surface.json +516 -0
  109. package/src/config/estate-count-budget.json +1 -1
  110. package/src/config/evaluator-budgets.json +4 -4
  111. package/src/config/gate-coverage.yml +29 -8
  112. package/src/config/gate-violation-baselines.json +8 -8
  113. package/src/config/gitignore-block.txt +19 -0
  114. package/src/config/hook-token-budget.json +7 -6
  115. package/src/config/host-injection-effect.json +73 -0
  116. package/src/config/packed-binary-manifest.json +33 -0
  117. package/src/config/preamble-payload-budget.json +112 -105
  118. package/src/config/publish-surface.json +4 -2
  119. package/src/config/routing-coverage-seed.json +9 -2
  120. package/src/install/claudeLayerCarriage.ts +273 -0
  121. package/src/install/claudePathsPlan.ts +76 -0
  122. package/src/install/hostLayerFingerprint.ts +1 -1
  123. package/src/install/partitionEligibility.ts +261 -127
  124. package/src/install/ruleLayerPartition.ts +19 -23
  125. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  126. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  127. package/src/scripts/_dispatch.bash +77 -27
  128. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  129. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  130. package/src/scripts/_lib/continuity_slot.ts +295 -0
  131. package/src/scripts/_lib/continuity_surface.ts +296 -0
  132. package/src/scripts/_lib/continuity_writer.ts +253 -0
  133. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  134. package/src/scripts/_lib/hook_settings.ts +86 -1
  135. package/src/scripts/_lib/injection_effect.ts +151 -0
  136. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  137. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  138. package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
  139. package/src/scripts/_lib/pin_resolver.ts +13 -0
  140. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  141. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  142. package/src/scripts/_lib/retired_status.ts +74 -0
  143. package/src/scripts/_lib/session_eol.ts +44 -0
  144. package/src/scripts/_lib/session_index_trust.ts +346 -0
  145. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  146. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  147. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  148. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  149. package/src/scripts/ai_council/api_transport.ts +5 -0
  150. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  151. package/src/scripts/ai_council/prompts.ts +9 -0
  152. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  153. package/src/scripts/audit_mcp_tools.ts +69 -25
  154. package/src/scripts/build_mcp_catalog.ts +24 -1
  155. package/src/scripts/capture_skill_catalogue.ts +24 -11
  156. package/src/scripts/check_candidate_lines.ts +499 -0
  157. package/src/scripts/check_continuity_surface.ts +475 -0
  158. package/src/scripts/check_dependency_floors.ts +13 -6
  159. package/src/scripts/check_estate_count.ts +40 -79
  160. package/src/scripts/check_host_tree_parity.ts +291 -0
  161. package/src/scripts/check_pack_size.ts +262 -23
  162. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  163. package/src/scripts/check_roadmap_trackable.ts +19 -16
  164. package/src/scripts/check_rule_layer_partition.ts +54 -34
  165. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  166. package/src/scripts/check_single_delivery.ts +97 -31
  167. package/src/scripts/check_update_banner.ts +13 -0
  168. package/src/scripts/ci_settle.ts +205 -5
  169. package/src/scripts/code_graph/build.ts +217 -15
  170. package/src/scripts/code_graph/cli.ts +334 -15
  171. package/src/scripts/code_graph/detect.ts +38 -0
  172. package/src/scripts/code_graph/extract.ts +11 -0
  173. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  174. package/src/scripts/code_graph/loader.ts +88 -14
  175. package/src/scripts/code_graph/query.ts +144 -11
  176. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  177. package/src/scripts/code_graph/sqlite_store.ts +262 -6
  178. package/src/scripts/code_graph/types.ts +117 -3
  179. package/src/scripts/code_graph/validate.ts +26 -1
  180. package/src/scripts/code_graph/verbs.ts +590 -0
  181. package/src/scripts/condense.ts +54 -42
  182. package/src/scripts/corpus_manifest.ts +5 -5
  183. package/src/scripts/council_cli.ts +11 -12
  184. package/src/scripts/count_intent_disagreement.ts +426 -0
  185. package/src/scripts/handoff_context_hook.ts +118 -1
  186. package/src/scripts/hook_effect_doctor.ts +82 -1
  187. package/src/scripts/hook_manifest.json +1 -1
  188. package/src/scripts/hook_manifest.yaml +87 -12
  189. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  190. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  191. package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
  192. package/src/scripts/hooks/concern_registry.ts +4 -2
  193. package/src/scripts/hooks/host_semantics.ts +1 -1
  194. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  195. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  196. package/src/scripts/hooks/rule_inject_hook.ts +108 -21
  197. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  198. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  199. package/src/scripts/hot_context_hook.ts +62 -7
  200. package/src/scripts/inbox_source_census.ts +322 -0
  201. package/src/scripts/install-hooks.sh +82 -0
  202. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  203. package/src/scripts/lint_plan_risk_register.ts +13 -17
  204. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  205. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  206. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  207. package/src/scripts/mcp_server/path_util.ts +31 -0
  208. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  209. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  210. package/src/scripts/mcp_server/tools.ts +5 -69
  211. package/src/scripts/media/lib/adapter-contract.md +7 -2
  212. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  213. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  214. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  215. package/src/scripts/model_rule_injection.ts +61 -4
  216. package/src/scripts/project_thin_rules.ts +149 -16
  217. package/src/scripts/rdp_candidates_delta.ts +276 -0
  218. package/src/scripts/rdp_quality_eval.ts +217 -19
  219. package/src/scripts/release_mix_taxonomy.json +3 -2
  220. package/src/scripts/report_host_injection_effect.ts +189 -0
  221. package/src/scripts/report_layer_overlap.ts +41 -0
  222. package/src/scripts/report_skill_menu_census.ts +341 -0
  223. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  224. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  225. package/src/scripts/session_memory_index.ts +81 -8
  226. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  227. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  228. package/src/server/schemas/settings.ts +23 -4
  229. package/src/vendor/grammars/README.md +86 -0
  230. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  231. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  232. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  233. package/src/scripts/_lib/carrier_status.ts +0 -118
  234. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  235. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -32,8 +32,39 @@ import {
32
32
  } from '../_lib/sqlite_guard.js';
33
33
  import type { CodeEdge, CodeGraph, CodeNode } from './types.js';
34
34
 
35
- /** Bump on ANY table/column change — a mismatch sends readers to the JSON. */
36
- export const GRAPH_STORE_VERSION = 1;
35
+ /**
36
+ * Bump on ANY table/column change — a mismatch sends readers to the JSON.
37
+ *
38
+ * 1 → 2 (2.1): `edges` gained `resolved_via` and `provider`; `nodes` gained
39
+ * indexes on `label` and `source_file`; `meta` gained `node_count` /
40
+ * `edge_count`. A v1 twin has none of them, and the indexed read path below
41
+ * would silently answer from missing columns.
42
+ *
43
+ * 2 → 3 (2.2): those two columns are now REQUIRED on every edge. A v2 twin has
44
+ * the columns but may have written them NULL, from a graph built before the
45
+ * fields existed — which reads back as a valid edge carrying a fabricated
46
+ * mechanism. Refusing it costs nothing, because the twin is derived and the
47
+ * next build re-emits it.
48
+ *
49
+ * 3 → 4 (3.2): the `Relation` union gained `tests`, so a v3 twin carries no
50
+ * `tests` rows for a graph whose JSON has them. No COLUMN changed, which is
51
+ * precisely why the bump is needed rather than optional: a v3 twin would read
52
+ * back cleanly, answer `tests-for` with the empty set, and the caller could not
53
+ * tell "nothing tests this" from "this twin predates the relation". An index
54
+ * that answers a question it cannot answer is worse than one that refuses.
55
+ */
56
+ export const GRAPH_STORE_VERSION = 4;
57
+
58
+ /**
59
+ * Edge count at or above which the indexed read path replaces the blob path.
60
+ *
61
+ * A DECLARED threshold, not a measured crossover — nothing establishes that
62
+ * 50k is where the two paths actually cross. Below it, building the in-memory
63
+ * Maps is cheap enough that the extra SQL round-trips per hop are not worth
64
+ * paying; above it, `JSON.parse` plus two `Map`s over every edge is the
65
+ * dominant cost and it is paid before the first question is answered.
66
+ */
67
+ export const INDEXED_READ_MIN_EDGES = 50_000;
37
68
 
38
69
  /** Derived twin path for a JSON cache path.
39
70
  * cache-invalidation: versioned via PRAGMA user_version (GRAPH_STORE_VERSION), not the filename. */
@@ -70,15 +101,25 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
70
101
  db.exec(
71
102
  'CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);' +
72
103
  'CREATE TABLE nodes (id TEXT PRIMARY KEY, label TEXT NOT NULL, kind TEXT, source_file TEXT, l0 INTEGER, l1 INTEGER, l2 INTEGER, l3 INTEGER);' +
73
- 'CREATE TABLE edges (source TEXT NOT NULL, target TEXT NOT NULL, relation TEXT NOT NULL, confidence TEXT NOT NULL, candidates TEXT);' +
104
+ 'CREATE TABLE edges (source TEXT NOT NULL, target TEXT NOT NULL, relation TEXT NOT NULL, confidence TEXT NOT NULL, candidates TEXT, ambiguity_reason TEXT, resolved_via TEXT, provider TEXT);' +
74
105
  'CREATE INDEX edges_source ON edges(source);' +
75
- 'CREATE INDEX edges_target ON edges(target);',
106
+ 'CREATE INDEX edges_target ON edges(target);' +
107
+ // Seed resolution and the changed-file seed set are the two
108
+ // reads that would otherwise scan every node. `id` needs no
109
+ // index — it is the PRIMARY KEY.
110
+ 'CREATE INDEX nodes_label ON nodes(label);' +
111
+ 'CREATE INDEX nodes_source_file ON nodes(source_file);',
76
112
  );
77
113
  const meta = db.prepare('INSERT INTO meta (key, value) VALUES (?, ?)');
78
114
  meta.run('source_checksum', graph.source_checksum);
79
115
  meta.run('json_size', String(stat.size));
80
116
  meta.run('json_mtime_ms', String(stat.mtimeMs));
81
117
  meta.run('graph_json', serialized);
118
+ // Counts live in meta so a reader can pick its path from one row
119
+ // instead of a COUNT(*) over the table it is deciding whether to
120
+ // read at all.
121
+ meta.run('node_count', String(graph.nodes.length));
122
+ meta.run('edge_count', String(graph.edges.length));
82
123
  const insNode = db.prepare(
83
124
  'INSERT INTO nodes (id, label, kind, source_file, l0, l1, l2, l3) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
84
125
  );
@@ -96,10 +137,20 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
96
137
  );
97
138
  }
98
139
  const insEdge = db.prepare(
99
- 'INSERT INTO edges (source, target, relation, confidence, candidates) VALUES (?, ?, ?, ?, ?)',
140
+ 'INSERT INTO edges (source, target, relation, confidence, candidates, ambiguity_reason, resolved_via, provider) ' +
141
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
100
142
  );
101
143
  for (const e of graph.edges) {
102
- insEdge.run(e.source, e.target, e.relation, e.confidence, e.candidates ? JSON.stringify(e.candidates) : null);
144
+ insEdge.run(
145
+ e.source,
146
+ e.target,
147
+ e.relation,
148
+ e.confidence,
149
+ e.candidates ? JSON.stringify(e.candidates) : null,
150
+ e.ambiguity_reason ?? null,
151
+ e.resolved_via,
152
+ e.provider,
153
+ );
103
154
  }
104
155
  stampUserVersion(db, GRAPH_STORE_VERSION);
105
156
  } finally {
@@ -150,6 +201,211 @@ export function loadSerializedFromTwin(jsonPath: string): string | null {
150
201
  }
151
202
  }
152
203
 
204
+ /**
205
+ * A per-node / per-edge read API over the twin — the thing this module did not
206
+ * export before (road-to-a-graph-that-is-shipped D7).
207
+ *
208
+ * Every export it had took a PATH and returned the whole graph: the serialized
209
+ * blob, or a pair of counts. So "the query path prefers SQLite" was true and
210
+ * bought nothing — `loadSerializedFromTwin` hands back the entire
211
+ * serialization, and the caller then parses it and builds a `Map` over every
212
+ * node and every edge. The twin was a cheaper blob transport, never an index.
213
+ *
214
+ * These reads take an id, a label, or a file, and touch only the rows that
215
+ * answer. Statements are prepared once per handle because a BFS calls
216
+ * `edgesTo` once per frontier node, and re-preparing per hop would hand back
217
+ * the constant factor the index just removed.
218
+ *
219
+ * The handle owns an open database and MUST be closed. Every method is safe
220
+ * after `close()` only in the sense that it will throw — callers hold it for
221
+ * the duration of one query and release it.
222
+ */
223
+ export interface GraphIndex {
224
+ readonly nodeCount: number;
225
+ readonly edgeCount: number;
226
+ readonly sourceChecksum: string;
227
+ getNode(id: string): CodeNode | undefined;
228
+ hasNode(id: string): boolean;
229
+ /** Out-edges: everything `id` points at. */
230
+ edgesFrom(id: string): CodeEdge[];
231
+ /** In-edges: everything pointing at `id` — the reverse-BFS direction. */
232
+ edgesTo(id: string): CodeEdge[];
233
+ /** Exact-label resolution, served by `nodes_label`. */
234
+ idsByLabel(label: string, limit: number): string[];
235
+ /** Non-file nodes declared in any of `files` — the changed-seed lookup. */
236
+ idsInFiles(files: readonly string[]): string[];
237
+ /** Every node id, sorted. A whole-table read, for `dead` only — see
238
+ * `LoadedGraph.allNodeIds`. */
239
+ allNodeIds(): string[];
240
+ /** Every node's id + searchable text. The ONLY whole-table read, and it
241
+ * exists because a BM25 fallback genuinely needs the corpus. Callers reach
242
+ * it only after an exact id AND an exact label both missed. */
243
+ lexicalCorpus(): { id: string; text: string }[];
244
+ close(): void;
245
+ }
246
+
247
+ interface NodeRow {
248
+ id: string;
249
+ label: string;
250
+ kind: string | null;
251
+ source_file: string | null;
252
+ l0: number | null;
253
+ l1: number | null;
254
+ l2: number | null;
255
+ l3: number | null;
256
+ }
257
+
258
+ interface EdgeRow {
259
+ source: string;
260
+ target: string;
261
+ relation: string;
262
+ confidence: string;
263
+ candidates: string | null;
264
+ ambiguity_reason: string | null;
265
+ resolved_via: string | null;
266
+ provider: string | null;
267
+ }
268
+
269
+ function toNode(r: NodeRow): CodeNode {
270
+ const loc = [r.l0, r.l1, r.l2, r.l3].filter((v): v is number => typeof v === 'number');
271
+ return {
272
+ id: r.id,
273
+ label: r.label,
274
+ kind: (r.kind ?? 'file') as CodeNode['kind'],
275
+ source_file: r.source_file ?? '',
276
+ source_location: loc,
277
+ };
278
+ }
279
+
280
+ function toEdge(r: EdgeRow): CodeEdge {
281
+ const e: CodeEdge = {
282
+ source: r.source,
283
+ target: r.target,
284
+ relation: r.relation as CodeEdge['relation'],
285
+ confidence: r.confidence as CodeEdge['confidence'],
286
+ // The SQL columns are nullable, the TypeScript fields are not. A null
287
+ // can only come from a twin emitted before these fields existed, which
288
+ // GRAPH_STORE_VERSION already refuses — so this coalesce is unreachable
289
+ // in a healthy tree and is written to fail READABLE rather than to
290
+ // fabricate: `name-lookup` is the weakest mechanism, so a mislabelled
291
+ // edge is under-trusted rather than over-trusted by Phase 3's filter.
292
+ resolved_via: (r.resolved_via ?? 'name-lookup') as CodeEdge['resolved_via'],
293
+ provider: (r.provider ?? 'native') as CodeEdge['provider'],
294
+ };
295
+ if (r.candidates) e.candidates = JSON.parse(r.candidates) as string[];
296
+ if (r.ambiguity_reason === 'receiver-unknown' || r.ambiguity_reason === 'hierarchy-unresolved') {
297
+ e.ambiguity_reason = r.ambiguity_reason;
298
+ }
299
+ return e;
300
+ }
301
+
302
+ /**
303
+ * Open the twin for INDEXED reads, or null when it cannot serve them.
304
+ *
305
+ * Null on exactly the conditions `loadSerializedFromTwin` already refuses on —
306
+ * absent, stale against the JSON's size/mtime, corrupt, version-mismatched, no
307
+ * `node:sqlite` — so the two paths agree about when the twin is trustworthy
308
+ * and a caller never has to ask twice.
309
+ */
310
+ export function openGraphIndex(jsonPath: string): GraphIndex | null {
311
+ if (!isSqliteAvailableSync()) return null;
312
+ const twin = sqliteTwinPath(jsonPath);
313
+ if (!fs.existsSync(twin)) return null;
314
+ let stat: fs.Stats;
315
+ try {
316
+ stat = fs.statSync(jsonPath);
317
+ } catch {
318
+ return null;
319
+ }
320
+ try {
321
+ const { DatabaseSync } = loadSqliteSync('code_graph.sqlite_store');
322
+ const db = new DatabaseSync(twin, { readOnly: true });
323
+ try {
324
+ if (readUserVersion(db) !== GRAPH_STORE_VERSION) {
325
+ db.close();
326
+ return null;
327
+ }
328
+ const rows = db.prepare('SELECT key, value FROM meta').all() as { key: string; value: string }[];
329
+ const meta = new Map(rows.map((r) => [r.key, r.value]));
330
+ if (meta.get('json_size') !== String(stat.size) || meta.get('json_mtime_ms') !== String(stat.mtimeMs)) {
331
+ db.close();
332
+ return null;
333
+ }
334
+ const nodeCount = Number(meta.get('node_count') ?? NaN);
335
+ const edgeCount = Number(meta.get('edge_count') ?? NaN);
336
+ if (!Number.isFinite(nodeCount) || !Number.isFinite(edgeCount)) {
337
+ // A v2 twin always carries both. Missing means something wrote
338
+ // this file that is not this module, and guessing the counts
339
+ // would be worse than declining.
340
+ db.close();
341
+ return null;
342
+ }
343
+
344
+ const qNode = db.prepare('SELECT * FROM nodes WHERE id = ?');
345
+ const qFrom = db.prepare('SELECT * FROM edges WHERE source = ?');
346
+ const qTo = db.prepare('SELECT * FROM edges WHERE target = ?');
347
+ const qLabel = db.prepare('SELECT id FROM nodes WHERE label = ? LIMIT ?');
348
+
349
+ return {
350
+ nodeCount,
351
+ edgeCount,
352
+ sourceChecksum: meta.get('source_checksum') ?? '',
353
+ getNode(id) {
354
+ const r = qNode.get(id) as unknown as NodeRow | undefined;
355
+ return r ? toNode(r) : undefined;
356
+ },
357
+ hasNode(id) {
358
+ return (qNode.get(id) as unknown as NodeRow | undefined) !== undefined;
359
+ },
360
+ edgesFrom(id) {
361
+ return (qFrom.all(id) as unknown as EdgeRow[]).map(toEdge);
362
+ },
363
+ edgesTo(id) {
364
+ return (qTo.all(id) as unknown as EdgeRow[]).map(toEdge);
365
+ },
366
+ idsByLabel(labelText, limit) {
367
+ return (qLabel.all(labelText, limit) as { id: string }[]).map((r) => r.id);
368
+ },
369
+ idsInFiles(files) {
370
+ if (files.length === 0) return [];
371
+ // Chunked because SQLite caps bound parameters (999 on the
372
+ // conservative default build) and a changed-file set from a
373
+ // wide diff can exceed it.
374
+ const out: string[] = [];
375
+ const CHUNK = 400;
376
+ for (let i = 0; i < files.length; i += CHUNK) {
377
+ const slice = files.slice(i, i + CHUNK);
378
+ const marks = slice.map(() => '?').join(',');
379
+ const stmt = db.prepare(
380
+ `SELECT id FROM nodes WHERE kind != 'file' AND source_file IN (${marks})`,
381
+ );
382
+ for (const r of stmt.all(...slice) as { id: string }[]) out.push(r.id);
383
+ }
384
+ return out;
385
+ },
386
+ allNodeIds() {
387
+ return (db.prepare('SELECT id FROM nodes ORDER BY id').all() as { id: string }[]).map(
388
+ (r) => r.id,
389
+ );
390
+ },
391
+ lexicalCorpus() {
392
+ return (db.prepare('SELECT id, label FROM nodes').all() as { id: string; label: string }[]).map(
393
+ (r) => ({ id: r.id, text: `${r.label} ${r.id}` }),
394
+ );
395
+ },
396
+ close() {
397
+ db.close();
398
+ },
399
+ };
400
+ } catch (err) {
401
+ db.close();
402
+ throw err;
403
+ }
404
+ } catch {
405
+ return null;
406
+ }
407
+ }
408
+
153
409
  /** Structured node/edge access for future in-process consumers (kept minimal:
154
410
  * the loader above guarantees byte-identical answers; these are for tooling). */
155
411
  export function twinCounts(jsonPath: string): { nodes: number; edges: number } | null {
@@ -14,8 +14,97 @@ export type Lang = 'php' | 'typescript' | 'javascript';
14
14
  /** Same scale as `discovery_graph.ts::EdgeConfidence` (reused, not forked). */
15
15
  export type EdgeConfidence = 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS';
16
16
 
17
- /** Relations the extractor emits. */
18
- export type Relation = 'calls' | 'imports' | 'uses' | 'inherits' | 'member';
17
+ /**
18
+ * Relations the graph carries.
19
+ *
20
+ * `calls` / `imports` / `uses` / `inherits` / `member` come from the extractor.
21
+ * `tests` is DERIVED in the build pass (`road-to-a-graph-that-is-shipped` 3.2):
22
+ * a test file that imports an in-repo symbol from a non-test file is asserted
23
+ * to test it, `resolved_via: 'test-import'`, `confidence: 'INFERRED'`. The two
24
+ * halves of that are deliberately split across the two axes — the import is a
25
+ * syntactic fact, so it is EXTRACTED evidence, while "this test tests that
26
+ * subject" is an inference from a naming convention, which is what INFERRED
27
+ * says. Deriving it in `buildGraph` rather than in the extractor keeps the
28
+ * extractor per-file and pure: the predicate needs only the two paths, and no
29
+ * grammar can see whether a file is a test.
30
+ */
31
+ export type Relation = 'calls' | 'imports' | 'uses' | 'inherits' | 'member' | 'tests';
32
+
33
+ /**
34
+ * HOW an edge's target was arrived at — the mechanism, not the confidence.
35
+ *
36
+ * The two axes are independent and conflating them is what this field fixes.
37
+ * `confidence` says how much to trust the target; `resolved_via` says what
38
+ * produced it, which is the axis a consumer filters on. Phase 3's verbs accept
39
+ * an edge when `resolved_via` is not in `{name-lookup, dynamic}` — a rule that
40
+ * cannot be expressed in the confidence scale at all, because a `name-lookup`
41
+ * hit and a same-file hit can both be INFERRED.
42
+ *
43
+ * Four of the eight occur today, and saying which is the point of an enum that
44
+ * is wider than its current population:
45
+ *
46
+ * · `same-file` — a local declaration in the using file. The language's own
47
+ * shadowing rule makes this the strongest binding there is.
48
+ * · `import-specifier` — an import/`use` binding: the file states where the
49
+ * name comes from.
50
+ * · `name-lookup` — the repo-wide same-name table, or an attempted lookup that
51
+ * found nothing and left a `symbol:` pseudo-target. A guess either way, and
52
+ * the value that says so.
53
+ * · `dynamic` — a receiver the extractor cannot type (`$obj->m()`, an
54
+ * unresolved `C::m()`), or a hierarchy rooted outside the repository.
55
+ *
56
+ * The remaining four are declared and currently unemitted: `path-alias` and
57
+ * `psr4` arrive with the resolution tiers in step 2.3, and `route-table` /
58
+ * `test-import` with the relations Phase 3 adds. They are in the union now so
59
+ * that adding them later is a build change rather than a schema change.
60
+ */
61
+ export type ResolvedVia =
62
+ | 'same-file'
63
+ | 'import-specifier'
64
+ | 'path-alias'
65
+ | 'psr4'
66
+ | 'route-table'
67
+ | 'test-import'
68
+ | 'name-lookup'
69
+ | 'dynamic';
70
+
71
+ /**
72
+ * WHICH engine produced the edge.
73
+ *
74
+ * One value, and the field exists anyway: an edge with no provider is
75
+ * indistinguishable from an edge whose provider nobody recorded, and the whole
76
+ * point of a mixed graph — should one ever exist — is being able to tell them
77
+ * apart. Kill register K2 forbids an external provider entering `src/`, so this
78
+ * stays a single-member union until a decision changes that.
79
+ */
80
+ export type EdgeProvider = 'native';
81
+
82
+ /**
83
+ * The two mechanisms that are GUESSES rather than statements.
84
+ *
85
+ * `name-lookup` is the repo-wide same-name table (or a lookup that found
86
+ * nothing); `dynamic` is a receiver the extractor cannot type. Everything else
87
+ * names where the target came from.
88
+ *
89
+ * Declared HERE, beside `ResolvedVia`, rather than in the query tier that first
90
+ * needed it — because the BUILD pass needs it too, and the reason is a defect an
91
+ * independent review demonstrated: a derived edge stamped with a trustworthy
92
+ * mechanism while its own evidence was a guess LAUNDERS the guess. A PHP
93
+ * `use Two\Mailer` with no `composer.json` binds by base name, so the `imports`
94
+ * edge is `name-lookup` and may point at `One\Mailer`; deriving a `tests` edge
95
+ * from it and tagging that `test-import` made a wrong target trustworthy to
96
+ * every consumer of the filter. One set, two readers, and the build pass can no
97
+ * longer disagree with the query tier about what a guess is.
98
+ */
99
+ export const GUESS_RESOLVED_VIA: ReadonlySet<ResolvedVia> = new Set<ResolvedVia>([
100
+ 'name-lookup',
101
+ 'dynamic',
102
+ ]);
103
+
104
+ /** Does this edge's target rest on a stated fact rather than a guess? */
105
+ export function isStatedResolution(resolved_via: ResolvedVia): boolean {
106
+ return !GUESS_RESOLVED_VIA.has(resolved_via);
107
+ }
19
108
 
20
109
  export interface CodeNode {
21
110
  /** Path-qualified from day one: `<relpath>#<symbol>` (collision-free). */
@@ -45,6 +134,14 @@ export interface CodeEdge {
45
134
  target: string;
46
135
  relation: Relation;
47
136
  confidence: EdgeConfidence;
137
+ /**
138
+ * REQUIRED. Not optional, so "no edge lacks either field" is a property the
139
+ * type system enforces at every construction site rather than a rule a
140
+ * linter has to re-check afterwards.
141
+ */
142
+ resolved_via: ResolvedVia;
143
+ /** REQUIRED, for the same reason. */
144
+ provider: EdgeProvider;
48
145
  /**
49
146
  * Present only on AMBIGUOUS / multi-candidate edges: the resolved-target
50
147
  * candidates by id. An AMBIGUOUS edge is "correct" when the true target is
@@ -105,6 +202,23 @@ export interface CodeGraph {
105
202
  }
106
203
 
107
204
  /**
205
+ * Bumped 3 → 4 by `road-to-a-graph-that-is-shipped` 3.2: the `Relation` union
206
+ * gained `tests`, and the build pass now derives those edges. A cached sidecar
207
+ * written at v3 carries per-file extracts that are still correct — `tests` is
208
+ * derived from `imports`, not extracted — but `readSidecar` refuses a version
209
+ * mismatch anyway, and that refusal is the right conservative default: the
210
+ * alternative is a version check that has to reason about WHICH schema changes
211
+ * a sidecar survives, which is a rule that rots silently the first time someone
212
+ * adds an extractor field. A full re-extraction is the cost of being sure.
213
+ *
214
+ * Bumped 2 → 3 by `road-to-a-graph-that-is-shipped` 2.2: every `CodeEdge` now
215
+ * carries `resolved_via` and `provider`. A cached sidecar written at v2 has
216
+ * neither, so `--update` would reuse untagged edges beside tagged ones and
217
+ * produce a graph whose `resolved_via` histogram silently under-counts —
218
+ * exactly the mixed-graph failure the previous bump describes. `readSidecar`
219
+ * refuses a version mismatch, which is what makes that impossible rather than
220
+ * merely unlikely.
221
+ *
108
222
  * Bumped 1 → 2 by `road-to-the-graph-that-lies-confidently`: `CodeNode.kind`
109
223
  * gained three members, `CodeGraph` gained `suppressed_edge_counts`, and the
110
224
  * per-file extract sidecar gained the import specifier. A cached sidecar
@@ -113,7 +227,7 @@ export interface CodeGraph {
113
227
  * file — a silently mixed graph. `readSidecar` refuses a version mismatch, so
114
228
  * the bump is what makes that impossible rather than merely unlikely.
115
229
  */
116
- export const SCHEMA_VERSION = 2;
230
+ export const SCHEMA_VERSION = 4;
117
231
 
118
232
  /** Deterministic per-file byte cap — files above this become a SKIPPED node. */
119
233
  export const MAX_FILE_BYTES = 1_000_000;
@@ -4,8 +4,23 @@
4
4
  */
5
5
  import type { CodeGraph } from './types.js';
6
6
 
7
- const RELATIONS = new Set(['calls', 'imports', 'uses', 'inherits', 'member']);
7
+ /** Mirrors `Relation` in types.ts. `tests` is derived by the build pass (3.2),
8
+ * not extracted, and is validated on exactly the same terms as the rest. */
9
+ const RELATIONS = new Set(['calls', 'imports', 'uses', 'inherits', 'member', 'tests']);
8
10
  const CONFIDENCES = new Set(['EXTRACTED', 'INFERRED', 'AMBIGUOUS']);
11
+ /** Mirrors `ResolvedVia` in types.ts — the full union, including the four
12
+ * values no build emits yet (2.3 and Phase 3 add them). */
13
+ const RESOLVED_VIA = new Set([
14
+ 'same-file',
15
+ 'import-specifier',
16
+ 'path-alias',
17
+ 'psr4',
18
+ 'route-table',
19
+ 'test-import',
20
+ 'name-lookup',
21
+ 'dynamic',
22
+ ]);
23
+ const PROVIDERS = new Set(['native']);
9
24
  const KINDS = new Set([
10
25
  'file',
11
26
  'class',
@@ -57,6 +72,16 @@ export function validateGraph(g: unknown): ValidateResult {
57
72
  if (!RELATIONS.has(ee['relation'] as string)) push(`edge[${i}].relation invalid: ${String(ee['relation'])}`);
58
73
  if (!CONFIDENCES.has(ee['confidence'] as string))
59
74
  push(`edge[${i}].confidence invalid: ${String(ee['confidence'])}`);
75
+ // Both are REQUIRED (2.2). Checked as presence-and-membership rather
76
+ // than membership alone, because an absent field and a misspelled one
77
+ // are different defects: absent means the build did not tag the edge,
78
+ // misspelled means the taxonomy drifted.
79
+ if (ee['resolved_via'] === undefined) push(`edge[${i}].resolved_via is required`);
80
+ else if (!RESOLVED_VIA.has(ee['resolved_via'] as string))
81
+ push(`edge[${i}].resolved_via invalid: ${String(ee['resolved_via'])}`);
82
+ if (ee['provider'] === undefined) push(`edge[${i}].provider is required`);
83
+ else if (!PROVIDERS.has(ee['provider'] as string))
84
+ push(`edge[${i}].provider invalid: ${String(ee['provider'])}`);
60
85
  // a source must be a known node; targets may be unresolved `symbol:` refs
61
86
  if (typeof ee['source'] === 'string' && !ids.has(ee['source'] as string))
62
87
  push(`edge[${i}].source is not a known node: ${String(ee['source'])}`);