@event4u/agent-config 17.2.1 → 17.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
  5. package/CHANGELOG.md +167 -193
  6. package/CREDITS.md +2 -2
  7. package/README.md +1 -1
  8. package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  11. package/dist/cli/commands/recordTriggerEval.js +1 -1
  12. package/dist/cli/registry.js +2 -2
  13. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
  14. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  15. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  16. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  17. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
  18. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
  19. package/dist/cli-delegate/cmd_conformance.js +190 -192
  20. package/dist/cli-delegate/cmd_doctor.js +5 -5
  21. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  22. package/dist/cli-delegate/cmd_explain.js +1 -1
  23. package/dist/cli-delegate/cmd_export.js +2 -2
  24. package/dist/cli-delegate/cmd_handoff.js +2 -2
  25. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  26. package/dist/cli-delegate/cmd_prune.js +2 -2
  27. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  28. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  29. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  30. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  31. package/dist/cli-delegate/cmd_sync.js +2 -2
  32. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  33. package/dist/cli-delegate/cmd_update.js +24 -13
  34. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  35. package/dist/cli-delegate/cmd_validate.js +2 -2
  36. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  37. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  38. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  39. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  40. package/dist/discovery/deprecation-report.md +1 -1
  41. package/dist/discovery/discovery-manifest.json +3 -3
  42. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  43. package/dist/discovery/discovery-manifest.summary.md +1 -1
  44. package/dist/discovery/dormancy-report.md +1 -1
  45. package/dist/discovery/orphan-report.md +1 -1
  46. package/dist/discovery/packs.json +2 -2
  47. package/dist/discovery/trust-report.md +1 -1
  48. package/dist/discovery/workspaces.json +2 -2
  49. package/dist/hooks/code_graph_refresh.chunk.cjs +443 -0
  50. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  51. package/dist/hooks/dispatch.js +1001 -938
  52. package/dist/hooks/dispatch.sha256 +1 -1
  53. package/dist/hooks/egress_engine.chunk.cjs +57 -7
  54. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  55. package/dist/hooks/update_notice.chunk.cjs +914 -0
  56. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  57. package/dist/install/install.mjs +6974 -6037
  58. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  59. package/dist/mcp/registry-manifest.json +3 -2
  60. package/dist/mcp/server.json +13 -4
  61. package/dist/mcp/server.mjs +71202 -63282
  62. package/dist/scripts/_lib/egress_rules.js +8 -2
  63. package/dist/scripts/condense.js +18 -15
  64. package/dist/server/routes/discovery.js +1 -1
  65. package/dist/server/routes/install.js +1 -1
  66. package/dist/server/routes/ping.js +1 -1
  67. package/dist/server/routes/schema.js +4 -4
  68. package/dist/server/routes/settings.js +2 -6
  69. package/dist/server/routes/wizard.js +6 -6
  70. package/dist/server/schemas/settings.js +42 -42
  71. package/dist/shared/settingsSurface.js +1 -1
  72. package/dist/shared/userMd/schema.js +1 -1
  73. package/dist/shared/zodJsonSchema.js +47 -0
  74. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  75. package/dist/ui/index.html +1 -1
  76. package/package.json +6 -3
  77. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  78. package/src/config/ci-local-parity.yml +11 -0
  79. package/src/config/continuity-surface.json +9 -2
  80. package/src/config/evaluator-budgets.json +9 -8
  81. package/src/config/gate-violation-baselines.json +3 -3
  82. package/src/config/gitignore-block.txt +6 -5
  83. package/src/config/hook-bundle-budget.json +46 -6
  84. package/src/config/pack-size-budget.json +12 -1
  85. package/src/config/packed-binary-manifest.json +18 -0
  86. package/src/config/sigstore/npm_keys.json +50 -0
  87. package/src/config/sigstore/trust-material.json +9 -0
  88. package/src/config/sigstore/trusted_root.json +126 -0
  89. package/src/config/skill-body-budget.json +7 -1
  90. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  91. package/src/scripts/_cli/cmd_update.ts +48 -20
  92. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  93. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  94. package/src/scripts/_dispatch.bash +4 -3
  95. package/src/scripts/_lib/confusables.ts +0 -19
  96. package/src/scripts/_lib/egress_normalize.ts +53 -23
  97. package/src/scripts/_lib/egress_rules.ts +8 -2
  98. package/src/scripts/_lib/egress_text.ts +5 -3
  99. package/src/scripts/_lib/egress_token.ts +7 -5
  100. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  101. package/src/scripts/_lib/git_common_dir.ts +10 -0
  102. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  103. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  104. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  105. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  106. package/src/scripts/_lib/lineage_map.ts +55 -0
  107. package/src/scripts/_lib/lineage_query.ts +140 -0
  108. package/src/scripts/_lib/lineage_seed.ts +116 -0
  109. package/src/scripts/_lib/lineage_watch.ts +249 -0
  110. package/src/scripts/_lib/link_crypto.ts +8 -4
  111. package/src/scripts/_lib/listing_manifests.ts +85 -0
  112. package/src/scripts/_lib/neighbour_census.ts +1 -0
  113. package/src/scripts/_lib/production_reach.ts +3 -3
  114. package/src/scripts/_lib/project_registry.ts +227 -0
  115. package/src/scripts/_lib/provenance_verify.ts +14 -242
  116. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  117. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  118. package/src/scripts/_lib/source_id.ts +48 -0
  119. package/src/scripts/_lib/source_shape.ts +4 -0
  120. package/src/scripts/_lib/unattended_gates.ts +6 -1
  121. package/src/scripts/adoption_status.ts +231 -27
  122. package/src/scripts/advisories.ts +26 -10
  123. package/src/scripts/ai_council/council_egress.ts +5 -6
  124. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  125. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  126. package/src/scripts/check_dependency_floors.ts +14 -0
  127. package/src/scripts/check_egress_mutations.ts +1 -0
  128. package/src/scripts/check_release_manifest_content.ts +17 -2
  129. package/src/scripts/check_release_pr_shape.ts +5 -0
  130. package/src/scripts/code_graph/build.ts +145 -45
  131. package/src/scripts/code_graph/cli.ts +382 -35
  132. package/src/scripts/code_graph/conventions_default.ts +247 -0
  133. package/src/scripts/code_graph/detect.ts +5 -5
  134. package/src/scripts/code_graph/estate_cli.ts +135 -0
  135. package/src/scripts/code_graph/estate_index.ts +394 -0
  136. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  137. package/src/scripts/code_graph/estate_map.ts +141 -0
  138. package/src/scripts/code_graph/extract.ts +8 -2
  139. package/src/scripts/code_graph/extract_more.ts +241 -2
  140. package/src/scripts/code_graph/graph_budget.ts +158 -0
  141. package/src/scripts/code_graph/graph_home.ts +80 -0
  142. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  143. package/src/scripts/code_graph/loader.ts +10 -2
  144. package/src/scripts/code_graph/map.ts +195 -53
  145. package/src/scripts/code_graph/map_model.ts +113 -20
  146. package/src/scripts/code_graph/map_verify.ts +80 -5
  147. package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
  148. package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
  149. package/src/scripts/code_graph/passes.ts +173 -0
  150. package/src/scripts/code_graph/project_ref.ts +25 -0
  151. package/src/scripts/code_graph/projects_cli.ts +254 -0
  152. package/src/scripts/code_graph/providers/angular.ts +55 -0
  153. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  154. package/src/scripts/code_graph/providers/catalog.ts +299 -0
  155. package/src/scripts/code_graph/providers/django.ts +206 -0
  156. package/src/scripts/code_graph/providers/express.ts +25 -0
  157. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  158. package/src/scripts/code_graph/providers/flask.ts +99 -0
  159. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  160. package/src/scripts/code_graph/providers/index.ts +103 -0
  161. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  162. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  163. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  164. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  165. package/src/scripts/code_graph/providers/rails.ts +204 -0
  166. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  167. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  168. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  169. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  170. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  171. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  172. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  173. package/src/scripts/code_graph/providers/spring.ts +175 -0
  174. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  175. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  176. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  177. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  178. package/src/scripts/code_graph/providers/types.ts +56 -0
  179. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  180. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  181. package/src/scripts/code_graph/query.ts +9 -9
  182. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  183. package/src/scripts/code_graph/refresh_lock.ts +197 -0
  184. package/src/scripts/code_graph/repo_identity.ts +169 -0
  185. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  186. package/src/scripts/code_graph/scip_reader.ts +2 -2
  187. package/src/scripts/code_graph/sqlite_store.ts +31 -28
  188. package/src/scripts/code_graph/types.ts +79 -41
  189. package/src/scripts/code_graph/validate.ts +6 -8
  190. package/src/scripts/condense.ts +15 -15
  191. package/src/scripts/generate_settings_reference.ts +2 -6
  192. package/src/scripts/hook_chunks.mjs +11 -0
  193. package/src/scripts/hook_manifest.json +1 -1
  194. package/src/scripts/hook_manifest.yaml +43 -23
  195. package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
  196. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  197. package/src/scripts/hooks/host_semantics.ts +72 -0
  198. package/src/scripts/hooks/lazy_concern.ts +8 -0
  199. package/src/scripts/hooks/user_notice.ts +105 -0
  200. package/src/scripts/install-hooks.sh +54 -33
  201. package/src/scripts/install.ts +2 -6
  202. package/src/scripts/lint_harvest_provenance.ts +137 -11
  203. package/src/scripts/lint_hook_manifest.ts +2 -0
  204. package/src/scripts/lint_marketplace.ts +33 -0
  205. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  206. package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
  207. package/src/scripts/mcp_server/graph_tools.ts +165 -41
  208. package/src/scripts/network_sources.ts +24 -6
  209. package/src/scripts/release.ts +2 -5
  210. package/src/scripts/release_drill.ts +4 -1
  211. package/src/scripts/release_env.ts +17 -0
  212. package/src/scripts/release_mix_taxonomy.json +3 -0
  213. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  214. package/src/scripts/update_notice_hook.ts +75 -7
  215. package/src/server/schemas/settings.ts +42 -42
  216. package/src/shared/settingsSurface.ts +1 -1
  217. package/src/shared/userMd/schema.ts +1 -1
  218. package/src/shared/zodJsonSchema.ts +41 -0
  219. package/src/vendor/grammars/README.md +8 -6
  220. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  221. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  222. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  223. package/dist/ui/assets/index-w70Ado09.js +0 -23
  224. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  225. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * A repository's identity on this machine (ADR-299, council decision O2).
3
+ *
4
+ * `repoId` is 12 hex of an HMAC-SHA256, keyed by a random key created once
5
+ * under the user-global `runtime/projects/`, over the canonical origin URL —
6
+ * userinfo, tokens, query and fragment removed, host lower-cased, default port
7
+ * dropped, ssh / scp / https forms and a trailing `.git` collapsed. Without a
8
+ * remote the main checkout's realpath stands in. Keyed rather than a plain hash
9
+ * because a hosting URL is dictionary-guessable and may carry a token.
10
+ *
11
+ * A linked worktree keeps its parent's `repoId` and gets an `instance`, so its
12
+ * writable caches never collide with the main checkout's while estate queries
13
+ * still count one project. Two separate clones of one origin share `repoId` too,
14
+ * so `checkout` — keyed over the main checkout's realpath — is what keeps their
15
+ * writable homes apart.
16
+ *
17
+ * File-based like `_lib/git_common_dir.ts`: an inherited `GIT_DIR` would
18
+ * redirect a `git config` subprocess to the wrong repository.
19
+ */
20
+ import { createHmac, randomBytes } from 'node:crypto';
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+
24
+ import { git_common_dir } from '../_lib/git_common_dir.js';
25
+ import { type EnvMap, event4u_root } from '../_lib/user_global_paths.js';
26
+
27
+ export interface RepoIdentity {
28
+ repoId: string;
29
+ /** Set for a linked worktree only. */
30
+ instance: string | null;
31
+ /** 8 hex over the main checkout's realpath: distinct per clone, shared by its worktrees. */
32
+ checkout: string;
33
+ source: 'origin' | 'realpath';
34
+ }
35
+
36
+ const DEFAULT_PORTS: Record<string, string> = { 'ssh:': '22', 'git+ssh:': '22', 'https:': '443', 'http:': '80', 'git:': '9418' };
37
+
38
+ export function projectsDir(env?: EnvMap | null): string {
39
+ return path.join(event4u_root(env), 'runtime', 'projects');
40
+ }
41
+
42
+ /** `dir` overrides the projects directory, for a caller (advisories) that carries its own. */
43
+ export function machineKeyPath(env?: EnvMap | null, dir: string = projectsDir(env)): string {
44
+ return path.join(dir, 'machine.key');
45
+ }
46
+
47
+ /**
48
+ * The machine key, created on first use. Published with a hard link so a
49
+ * concurrent first use either wins or reads the winner's complete key — never
50
+ * an empty file. A key that exists but is malformed throws: replacing it would
51
+ * silently re-key every repository on the machine.
52
+ */
53
+ export function machineKey(env?: EnvMap | null, dir?: string): Buffer {
54
+ const p = machineKeyPath(env, dir);
55
+ const read = (): Buffer => {
56
+ const hex = fs.readFileSync(p, 'utf-8').trim();
57
+ if (!/^[0-9a-f]{64}$/.test(hex)) throw new Error(`malformed machine key: ${p}`);
58
+ return Buffer.from(hex, 'hex');
59
+ };
60
+ if (fs.existsSync(p)) return read();
61
+ fs.mkdirSync(path.dirname(p), { recursive: true });
62
+ const tmp = `${p}.${process.pid}-${randomBytes(4).toString('hex')}.tmp`;
63
+ fs.writeFileSync(tmp, `${randomBytes(32).toString('hex')}\n`, { mode: 0o600 });
64
+ try {
65
+ fs.linkSync(tmp, p);
66
+ } catch (e) {
67
+ if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
68
+ } finally {
69
+ fs.rmSync(tmp, { force: true });
70
+ }
71
+ return read();
72
+ }
73
+
74
+ function trimPath(p: string): string {
75
+ return p
76
+ .replace(/\/{2,}/g, '/')
77
+ .replace(/^\/+|\/+$/g, '')
78
+ .replace(/\.git$/i, '')
79
+ .replace(/\/+$/, '');
80
+ }
81
+
82
+ /**
83
+ * The credential-free form two spellings of one remote share. A relative local
84
+ * path resolves against `base`, the repository root, because the process cwd
85
+ * is wherever the CLI happened to be started.
86
+ */
87
+ export function canonicalOrigin(raw: string, base?: string): string {
88
+ const url = raw.trim();
89
+ const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/\/)(.+)$/.exec(url);
90
+ if (!url.includes('://') && scp && !/^[a-zA-Z]:[\\/]/.test(url)) {
91
+ return `${(scp[1] as string).toLowerCase()}/${trimPath(scp[2] as string)}`;
92
+ }
93
+ try {
94
+ const u = new URL(url);
95
+ if (u.protocol === 'file:') return `file:${trimPath(decodeURIComponent(u.pathname))}`;
96
+ const port = u.port && u.port !== DEFAULT_PORTS[u.protocol] ? `:${u.port}` : '';
97
+ return `${u.hostname.toLowerCase()}${port}/${trimPath(decodeURIComponent(u.pathname))}`;
98
+ } catch {
99
+ return `file:${trimPath(base === undefined ? path.resolve(url) : path.resolve(base, url))}`;
100
+ }
101
+ }
102
+
103
+ /** The `origin` URL, else the first remote's, read from the common git config. */
104
+ export function remoteUrl(root: string): string | null {
105
+ const common = git_common_dir(root);
106
+ if (common === null) return null;
107
+ let text: string;
108
+ try {
109
+ text = fs.readFileSync(path.join(common, 'config'), 'utf-8');
110
+ } catch {
111
+ return null;
112
+ }
113
+ const urls = new Map<string, string>();
114
+ let remote: string | null = null;
115
+ for (const line of text.split(/\r?\n/)) {
116
+ const section = /^\s*\[\s*remote\s+"([^"]+)"\s*\]/.exec(line);
117
+ if (section) {
118
+ remote = section[1] as string;
119
+ continue;
120
+ }
121
+ if (/^\s*\[/.test(line)) {
122
+ remote = null;
123
+ continue;
124
+ }
125
+ const kv = /^\s*url\s*=\s*(.+?)\s*$/.exec(line);
126
+ if (remote !== null && kv && !urls.has(remote)) urls.set(remote, (kv[1] as string).replace(/^"|"$/g, ''));
127
+ }
128
+ return urls.get('origin') ?? urls.values().next().value ?? null;
129
+ }
130
+
131
+ function realpathOr(p: string): string {
132
+ try {
133
+ return fs.realpathSync(p);
134
+ } catch {
135
+ return path.resolve(p);
136
+ }
137
+ }
138
+
139
+ function hmac(key: Buffer, value: string): string {
140
+ return createHmac('sha256', key).update(value).digest('hex');
141
+ }
142
+
143
+ export function repoIdentity(root: string, env?: EnvMap | null, dir?: string): RepoIdentity {
144
+ const key = machineKey(env, dir);
145
+ const real = realpathOr(root);
146
+ const common = git_common_dir(real);
147
+ let linked = false;
148
+ try {
149
+ linked = !fs.statSync(path.join(real, '.git')).isDirectory();
150
+ } catch {
151
+ linked = false;
152
+ }
153
+ const origin = remoteUrl(real);
154
+ const main = common !== null && path.basename(common) === '.git' ? path.dirname(common) : real;
155
+ let source: RepoIdentity['source'] = 'origin';
156
+ let canonical: string;
157
+ if (origin !== null) {
158
+ canonical = `origin:${canonicalOrigin(origin, main)}`;
159
+ } else {
160
+ source = 'realpath';
161
+ canonical = `path:${main}`;
162
+ }
163
+ return {
164
+ repoId: hmac(key, canonical).slice(0, 12),
165
+ instance: linked ? hmac(key, `instance:${real}`).slice(0, 8) : null,
166
+ checkout: hmac(key, `checkout:${main}`).slice(0, 8),
167
+ source,
168
+ };
169
+ }
@@ -108,9 +108,10 @@ function isRecord(v: unknown): v is Record<string, unknown> {
108
108
  return typeof v === 'object' && v !== null && !Array.isArray(v);
109
109
  }
110
110
 
111
- /** Normalise a repo-relative path fragment: no leading `./`, no trailing `/`. */
111
+ /** Normalise a repo-relative path fragment: no leading `./`, no trailing `/`, and `.` is the root (`''`). */
112
112
  function normalizeFragment(p: string): string {
113
- return p.replace(/^\.\//, '').replace(/\/+$/, '');
113
+ const s = p.replace(/^(\.\/)+/, '').replace(/\/+$/, '');
114
+ return s === '.' ? '' : s;
114
115
  }
115
116
 
116
117
  /**
@@ -119,14 +120,19 @@ function normalizeFragment(p: string): string {
119
120
  * `baseUrl` is resolved relative to the tsconfig's own directory, which for a
120
121
  * root tsconfig is the repo root — so `baseUrl: "./src"` plus `"@shared/*":
121
122
  * ["shared/*"]` yields the repo-relative prefix `src/shared/`.
123
+ *
124
+ * A `baseUrl` also resolves every other non-relative specifier against
125
+ * itself, which TypeScript does after the `paths` entries: `baseUrl: "."`
126
+ * makes `components/cart` the file `components/cart.tsx`. That fallback is the
127
+ * last rule, an empty wildcard prefix, so a `paths` entry always wins.
122
128
  */
123
129
  export function aliasRulesFrom(tsconfig: unknown): AliasRule[] {
124
130
  if (!isRecord(tsconfig)) return [];
125
131
  const co = tsconfig['compilerOptions'];
126
132
  if (!isRecord(co)) return [];
127
- const paths = co['paths'];
128
- if (!isRecord(paths)) return [];
129
- const baseUrl = typeof co['baseUrl'] === 'string' ? normalizeFragment(co['baseUrl']) : '';
133
+ const paths = isRecord(co['paths']) ? co['paths'] : {};
134
+ const hasBaseUrl = typeof co['baseUrl'] === 'string';
135
+ const baseUrl = hasBaseUrl ? normalizeFragment(co['baseUrl'] as string) : '';
130
136
  const rules: AliasRule[] = [];
131
137
  for (const [pattern, valueRaw] of Object.entries(paths)) {
132
138
  if (!Array.isArray(valueRaw)) continue;
@@ -136,7 +142,8 @@ export function aliasRulesFrom(tsconfig: unknown): AliasRule[] {
136
142
  for (const v of valueRaw) {
137
143
  if (typeof v !== 'string') continue;
138
144
  const tgt = wildcard ? (v.endsWith('*') ? v.slice(0, -1) : v) : v;
139
- const joined = baseUrl ? `${baseUrl}/${normalizeFragment(tgt)}` : normalizeFragment(tgt);
145
+ const frag = normalizeFragment(tgt);
146
+ const joined = baseUrl && frag ? `${baseUrl}/${frag}` : baseUrl || frag;
140
147
  targets.push(joined.replace(/\/+$/, ''));
141
148
  }
142
149
  if (targets.length) rules.push({ prefix, wildcard, targets });
@@ -144,6 +151,7 @@ export function aliasRulesFrom(tsconfig: unknown): AliasRule[] {
144
151
  // Longest prefix first: `@shared/deep/` must beat `@shared/` when both are
145
152
  // declared, which is TypeScript's own rule and not a preference.
146
153
  rules.sort((a, b) => b.prefix.length - a.prefix.length);
154
+ if (hasBaseUrl) rules.push({ prefix: '', wildcard: true, targets: [baseUrl] });
147
155
  return rules;
148
156
  }
149
157
 
@@ -196,9 +204,13 @@ export function resolveAlias(spec: string, config: ResolutionConfig, fileIds: Re
196
204
  for (const rule of config.aliases) {
197
205
  if (rule.wildcard) {
198
206
  if (!spec.startsWith(rule.prefix)) continue;
207
+ // The baseUrl fallback never claims a relative or absolute specifier.
208
+ if (rule.prefix === '' && /^[./]/.test(spec)) continue;
199
209
  const rest = spec.slice(rule.prefix.length);
200
210
  for (const t of rule.targets) {
201
- const hit = firstExisting(rest ? `${t}/${rest}` : t, fileIds);
211
+ const candidate = rest && t ? `${t}/${rest}` : rest || t;
212
+ if (!candidate) continue;
213
+ const hit = firstExisting(candidate, fileIds);
202
214
  if (hit) return hit;
203
215
  }
204
216
  } else {
@@ -14,7 +14,7 @@
14
14
  * still reads. A malformed buffer throws; the caller treats that as "no SCIP".
15
15
  */
16
16
  import type { CodeEdge, CodeNode } from './types.js';
17
- import { GUESS_RESOLVED_VIA } from './types.js';
17
+ import { isStatedResolution } from './types.js';
18
18
 
19
19
  export interface ScipOccurrence {
20
20
  /** 0-based start line. */
@@ -192,7 +192,7 @@ export function supersededByScip(scip: readonly CodeEdge[], nodes: readonly Code
192
192
  named.set(e.source, set);
193
193
  }
194
194
  return (e: CodeEdge): boolean => {
195
- if (!GUESS_RESOLVED_VIA.has(e.resolved_via)) return false;
195
+ if (isStatedResolution(e.resolved_via)) return false;
196
196
  const set = named.get(e.source);
197
197
  if (!set) return false;
198
198
  const target = label.get(e.target) ?? e.target.replace(/^symbol:/, '');
@@ -52,8 +52,11 @@ import type { CodeEdge, CodeGraph, CodeNode } from './types.js';
52
52
  * back cleanly, answer `tests-for` with the empty set, and the caller could not
53
53
  * tell "nothing tests this" from "this twin predates the relation". An index
54
54
  * that answers a question it cannot answer is worse than one that refuses.
55
+ *
56
+ * 4 → 5 (`road-to-graph-providers-for-every-stack` 2.1): `edges` gained
57
+ * `provider_id`, the convention provider that drew a convention edge.
55
58
  */
56
- export const GRAPH_STORE_VERSION = 4;
59
+ export const GRAPH_STORE_VERSION = 6;
57
60
 
58
61
  /**
59
62
  * Edge count at or above which the indexed read path replaces the blob path.
@@ -78,11 +81,10 @@ export function sqliteTwinPath(jsonPath: string): string {
78
81
  * JSON path remains the source of truth either way.
79
82
  *
80
83
  * Storage model: nodes/edges land in real tables (queryable, and the FTS-free
81
- * structure the rollback triggers measure), while `meta.graph_json` carries
82
- * the byte-exact canonical serialization so the loader can guarantee
83
- * byte-identical answers without re-deriving key order.
84
+ * structure the rollback triggers measure). The JSON is the only full copy;
85
+ * the twin is a disposable, rebuildable projection of it.
84
86
  */
85
- export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: string): boolean {
87
+ export function emitSqliteTwin(graph: CodeGraph, jsonPath: string): boolean {
86
88
  if (!isSqliteAvailableSync()) return false;
87
89
  const twin = sqliteTwinPath(jsonPath);
88
90
  const tmp = `${twin}.tmp-${process.pid}`;
@@ -101,7 +103,7 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
101
103
  db.exec(
102
104
  'CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);' +
103
105
  'CREATE TABLE nodes (id TEXT PRIMARY KEY, label TEXT NOT NULL, kind TEXT, source_file TEXT, l0 INTEGER, l1 INTEGER, l2 INTEGER, l3 INTEGER);' +
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);' +
106
+ '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, provider_id TEXT);' +
105
107
  'CREATE INDEX edges_source ON edges(source);' +
106
108
  'CREATE INDEX edges_target ON edges(target);' +
107
109
  // Seed resolution and the changed-file seed set are the two
@@ -117,7 +119,6 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
117
119
  meta.run('source_checksum', graph.source_checksum);
118
120
  meta.run('json_size', String(stat.size));
119
121
  meta.run('json_mtime_ms', String(stat.mtimeMs));
120
- meta.run('graph_json', serialized);
121
122
  // Counts live in meta so a reader can pick its path from one row
122
123
  // instead of a COUNT(*) over the table it is deciding whether to
123
124
  // read at all.
@@ -140,8 +141,8 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
140
141
  );
141
142
  }
142
143
  const insEdge = db.prepare(
143
- 'INSERT INTO edges (source, target, relation, confidence, candidates, ambiguity_reason, resolved_via, provider) ' +
144
- 'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
144
+ 'INSERT INTO edges (source, target, relation, confidence, candidates, ambiguity_reason, resolved_via, provider, provider_id) ' +
145
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
145
146
  );
146
147
  for (const e of graph.edges) {
147
148
  insEdge.run(
@@ -153,6 +154,7 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
153
154
  e.ambiguity_reason ?? null,
154
155
  e.resolved_via,
155
156
  e.provider,
157
+ e.provider_id ?? null,
156
158
  );
157
159
  }
158
160
  db.exec('COMMIT');
@@ -172,39 +174,38 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
172
174
  }
173
175
 
174
176
  /**
175
- * Load the canonical serialization from a FRESH SQLite twin, or null when the
176
- * twin is absent / stale (JSON changed since emission) / corrupt /
177
- * version-mismatched / unsupported runtime. Callers fall back to reading the
178
- * JSON — and best-effort re-emit the twin from it (zero-touch rebuild).
177
+ * Whether the twin beside `jsonPath` mirrors it: present, the current store
178
+ * version, and recorded against the JSON's present size and mtime. A corrupt
179
+ * twin is a non-event — it is dropped so the next load re-emits it.
180
+ *
181
+ * The twin used to carry the whole serialized graph a second time and this
182
+ * returned it; that blob was the same bytes as the JSON and doubled the twin
183
+ * (road-to-a-graph-that-follows-you D2). The JSON is the only full copy now.
179
184
  */
180
- export function loadSerializedFromTwin(jsonPath: string): string | null {
181
- if (!isSqliteAvailableSync()) return null;
185
+ export function twinIsCurrent(jsonPath: string): boolean {
186
+ if (!isSqliteAvailableSync()) return false;
182
187
  const twin = sqliteTwinPath(jsonPath);
183
- if (!fs.existsSync(twin)) return null;
188
+ if (!fs.existsSync(twin)) return false;
184
189
  let stat: fs.Stats;
185
190
  try {
186
191
  stat = fs.statSync(jsonPath);
187
192
  } catch {
188
- return null; // canonical JSON gone — the twin has no truth to mirror
193
+ return false; // canonical JSON gone — the twin has no truth to mirror
189
194
  }
190
195
  try {
191
196
  const { DatabaseSync } = loadSqliteSync('code_graph.sqlite_store');
192
197
  const db = new DatabaseSync(twin, { readOnly: true });
193
198
  try {
194
- if (readUserVersion(db) !== GRAPH_STORE_VERSION) return null;
199
+ if (readUserVersion(db) !== GRAPH_STORE_VERSION) return false;
195
200
  const rows = db.prepare('SELECT key, value FROM meta').all() as { key: string; value: string }[];
196
201
  const meta = new Map(rows.map((r) => [r.key, r.value]));
197
- if (meta.get('json_size') !== String(stat.size)) return null;
198
- if (meta.get('json_mtime_ms') !== String(stat.mtimeMs)) return null;
199
- const serialized = meta.get('graph_json');
200
- return serialized && serialized.length > 0 ? serialized : null;
202
+ return meta.get('json_size') === String(stat.size) && meta.get('json_mtime_ms') === String(stat.mtimeMs);
201
203
  } finally {
202
204
  db.close();
203
205
  }
204
206
  } catch {
205
- // Corrupt twin is a non-event: drop it so the next build re-emits.
206
207
  fs.rmSync(twin, { force: true });
207
- return null;
208
+ return false;
208
209
  }
209
210
  }
210
211
 
@@ -214,9 +215,9 @@ export function loadSerializedFromTwin(jsonPath: string): string | null {
214
215
  *
215
216
  * Every export it had took a PATH and returned the whole graph: the serialized
216
217
  * blob, or a pair of counts. So "the query path prefers SQLite" was true and
217
- * bought nothing — `loadSerializedFromTwin` hands back the entire
218
- * serialization, and the caller then parses it and builds a `Map` over every
219
- * node and every edge. The twin was a cheaper blob transport, never an index.
218
+ * bought nothing — the loader handed back the entire serialization, and the
219
+ * caller then parsed it and built a `Map` over every node and every edge. The
220
+ * twin was a cheaper blob transport, never an index.
220
221
  *
221
222
  * These reads take an id, a label, or a file, and touch only the rows that
222
223
  * answer. Statements are prepared once per handle because a BFS calls
@@ -271,6 +272,7 @@ interface EdgeRow {
271
272
  ambiguity_reason: string | null;
272
273
  resolved_via: string | null;
273
274
  provider: string | null;
275
+ provider_id: string | null;
274
276
  }
275
277
 
276
278
  function toNode(r: NodeRow): CodeNode {
@@ -303,13 +305,14 @@ function toEdge(r: EdgeRow): CodeEdge {
303
305
  if (r.ambiguity_reason === 'receiver-unknown' || r.ambiguity_reason === 'hierarchy-unresolved') {
304
306
  e.ambiguity_reason = r.ambiguity_reason;
305
307
  }
308
+ if (r.provider_id) e.provider_id = r.provider_id;
306
309
  return e;
307
310
  }
308
311
 
309
312
  /**
310
313
  * Open the twin for INDEXED reads, or null when it cannot serve them.
311
314
  *
312
- * Null on exactly the conditions `loadSerializedFromTwin` already refuses on —
315
+ * Null on exactly the conditions `twinIsCurrent` refuses on —
313
316
  * absent, stale against the JSON's size/mtime, corrupt, version-mismatched, no
314
317
  * `node:sqlite` — so the two paths agree about when the twin is trustworthy
315
318
  * and a caller never has to ask twice.
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  /** Launch set — the stacks this suite's consumers actually run. */
12
- export type Lang = 'php' | 'typescript' | 'javascript' | 'python' | 'go' | 'java';
12
+ export type Lang = 'php' | 'typescript' | 'javascript' | 'python' | 'go' | 'java' | 'csharp' | 'ruby';
13
13
 
14
14
  /** Same scale as `discovery_graph.ts::EdgeConfidence` (reused, not forked). */
15
15
  export type EdgeConfidence = 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS';
@@ -29,11 +29,12 @@ export type EdgeConfidence = 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS';
29
29
  * grammar can see whether a file is a test.
30
30
  */
31
31
  /**
32
- * The last seven are the framework links the Laravel overlay reads by rule
33
- * (`laravel_overlay.ts`): a route to its handler, a method to the view it
34
- * renders, a view to the view it includes, code to the translation or config
35
- * file it reads, a migration to its table, a model to its table, a model to
36
- * the model an Eloquent relation names.
32
+ * `routes-to` … `injects` are the convention links a registered provider reads
33
+ * by rule (`providers/`, ADR-298): a route to its handler, code to the template
34
+ * it renders, a template to the template it includes, code to the translation
35
+ * or config file it reads, a migration to its table, a model to its table, a
36
+ * model to the model a relation names, a class to the service a container
37
+ * injects into it. The vocabulary is fixed and names no framework.
37
38
  */
38
39
  export type Relation =
39
40
  | 'calls'
@@ -49,6 +50,7 @@ export type Relation =
49
50
  | 'migrates'
50
51
  | 'persists'
51
52
  | 'relates'
53
+ | 'injects'
52
54
  /** A Markdown document links to, or names, the file (`docs_layer.ts`). */
53
55
  | 'documents';
54
56
 
@@ -86,14 +88,24 @@ export type ResolvedVia =
86
88
  | 'path-alias'
87
89
  | 'psr4'
88
90
  | 'route-table'
89
- /** A Blade directive, a `view()` / `__()` / `config()` call, or a Markdown link naming a file. */
91
+ /** A template directive, a render / translation / config call naming a file, or a Markdown link. */
90
92
  | 'directive-scan'
91
- /** A `Schema::create|table` call, or a model whose table one of them creates. */
93
+ /** A schema or migration call creating a table, or a model mapped to such a table. */
92
94
  | 'schema-call'
93
- /** `$this->hasMany(Other::class)` and its siblings inside a model. */
94
- | 'eloquent-relation'
95
+ /** An ORM relation declared inside a model, naming the related model. */
96
+ | 'orm-relation'
97
+ /** A dependency-injection declaration naming the injected class. */
98
+ | 'di-binding'
99
+ /** A string literal equal to exactly one file's path (the default pass). */
100
+ | 'literal-path'
101
+ /** A dotted name equal to exactly one template's path (the default pass). */
102
+ | 'literal-dotted'
103
+ /** A page module whose path is its route (the default pass). */
104
+ | 'fs-route'
95
105
  /** Stated by a compiler-grade SCIP index the project ships (`scip_reader.ts`). */
96
106
  | 'compiler-index'
107
+ /** C#: the one type of that name in the namespaces the file can see (its own, their parents, its `using`s). */
108
+ | 'namespace-scope'
97
109
  | 'test-import'
98
110
  | 'name-lookup'
99
111
  | 'dynamic'
@@ -131,33 +143,40 @@ export type EdgeProvider =
131
143
  | 'scip';
132
144
 
133
145
  /**
134
- * The two mechanisms that are GUESSES rather than statements.
146
+ * Every mechanism, classified as a statement (the source names the target) or
147
+ * a guess. Trust is this ALLOWLIST (ADR-298): it replaced a denylist of guesses,
148
+ * under which a new mechanism was trusted merely because nobody had listed it —
149
+ * wrong once several providers emit edges. A string that is no member at all
150
+ * (an older or foreign graph) is a guess. The type below fails to compile when a
151
+ * `ResolvedVia` member is in neither list.
135
152
  *
136
- * `name-lookup` is the repo-wide same-name table (or a lookup that found
137
- * nothing); `dynamic` is a receiver the extractor cannot type. Everything else
138
- * names where the target came from.
139
- *
140
- * Declared HERE, beside `ResolvedVia`, rather than in the query tier that first
141
- * needed it — because the BUILD pass needs it too, and the reason is a defect an
142
- * independent review demonstrated: a derived edge stamped with a trustworthy
143
- * mechanism while its own evidence was a guess LAUNDERS the guess. A PHP
144
- * `use Two\Mailer` with no `composer.json` binds by base name, so the `imports`
145
- * edge is `name-lookup` and may point at `One\Mailer`; deriving a `tests` edge
146
- * from it and tagging that `test-import` made a wrong target trustworthy to
147
- * every consumer of the filter. One set, two readers, and the build pass can no
148
- * longer disagree with the query tier about what a guess is.
149
- *
150
- * The accepted-edge filter is a DENYLIST, so a new value is accepted by
151
- * default — which is exactly backwards for `foreign`. It must be listed here
152
- * or a consumer's graph would silently answer the gate verbs as if this
153
- * package had resolved its edges. (Kept out of the literal: esbuild carries a
154
- * comment inside an expression into the hook bundle.)
153
+ * The guesses: `name-lookup` is the repo-wide same-name table (or a lookup that
154
+ * found nothing); `dynamic` is a receiver the extractor cannot type; `foreign`
155
+ * was read from another tool's graph and only mapped. The BUILD pass needs the
156
+ * split as much as the query tier: a derived edge stamped with a trustworthy
157
+ * mechanism while its own evidence was a guess LAUNDERS the guess — a PHP
158
+ * `use Two\Mailer` with no `composer.json` binds by base name, and a `tests`
159
+ * edge derived from it made a wrong target trustworthy to every consumer.
155
160
  */
156
- export const GUESS_RESOLVED_VIA: ReadonlySet<ResolvedVia> = new Set<ResolvedVia>(['name-lookup', 'dynamic', 'foreign']);
161
+ // One space-separated string per list: this module ships in the hook bundle,
162
+ // where an array literal costs a line per entry. `Words` splits it in the type.
163
+ const STATED_WORDS =
164
+ 'same-file import-specifier path-alias psr4 route-table directive-scan schema-call orm-relation di-binding compiler-index namespace-scope test-import literal-path literal-dotted fs-route' as const;
165
+ const GUESS_WORDS = 'name-lookup dynamic foreign' as const;
166
+ type Words<S extends string> = S extends `${infer H} ${infer T}` ? H | Words<T> : S;
167
+ /** Mechanisms in neither list; must be `never` (a test asserts it). */
168
+ export type UnclassifiedResolvedVia = Exclude<ResolvedVia, Words<typeof STATED_WORDS> | Words<typeof GUESS_WORDS>>;
169
+ /** Words that are no mechanism at all; must be `never` too. */
170
+ export type UnknownTrustWord = Exclude<Words<typeof STATED_WORDS> | Words<typeof GUESS_WORDS>, ResolvedVia>;
171
+ /** Fails to compile while either list is incomplete or carries a stray word. */
172
+ export type TrustListsComplete = [UnclassifiedResolvedVia | UnknownTrustWord] extends [never] ? true : never;
173
+ export const TRUST_LISTS_COMPLETE: TrustListsComplete = true;
174
+ export const STATED_RESOLVED_VIA: ReadonlySet<string> = new Set(STATED_WORDS.split(' '));
175
+ export const GUESS_RESOLVED_VIA: ReadonlySet<ResolvedVia> = new Set(GUESS_WORDS.split(' ') as ResolvedVia[]);
157
176
 
158
- /** Does this edge's target rest on a stated fact rather than a guess? */
177
+ /** Does this edge's target rest on a stated fact? Anything not listed as stated is a guess. */
159
178
  export function isStatedResolution(resolved_via: ResolvedVia): boolean {
160
- return !GUESS_RESOLVED_VIA.has(resolved_via);
179
+ return STATED_RESOLVED_VIA.has(resolved_via);
161
180
  }
162
181
 
163
182
  export interface CodeNode {
@@ -188,9 +207,9 @@ export interface CodeNode {
188
207
  | 'type'
189
208
  | 'enum'
190
209
  | 'skipped'
191
- /** A declared route, `<VERB> <path>` (Laravel overlay). */
210
+ /** A declared route or dispatched entry, `<VERB> <path>` (a convention provider). */
192
211
  | 'route'
193
- /** A database table a migration creates (Laravel overlay). */
212
+ /** A database table a migration or schema creates (a convention provider). */
194
213
  | 'table';
195
214
  source_file: string;
196
215
  /** 1-based [startLine, startCol, endLine, endCol]; [] for synthetic nodes. */
@@ -240,6 +259,14 @@ export interface CodeEdge {
240
259
  * target.
241
260
  */
242
261
  ambiguity_reason?: 'receiver-unknown' | 'hierarchy-unresolved';
262
+ /** The convention provider that drew this edge (ADR-298); absent on every other edge. */
263
+ provider_id?: string;
264
+ }
265
+
266
+ /** A convention provider active in this build, with the package roots it applied to. */
267
+ export interface ActiveProviderRecord {
268
+ id: string;
269
+ roots: string[];
243
270
  }
244
271
 
245
272
  export interface CodeGraph {
@@ -267,13 +294,15 @@ export interface CodeGraph {
267
294
  suppressed_edge_counts: {
268
295
  dynamic_no_candidate: number;
269
296
  /**
270
- * A fluent call on Laravel's route builder (`->middleware()`,
271
- * `->prefix()`) that name lookup had bound to an unrelated in-repo
272
- * method of the same name. Present only when it is non-zero, so a
273
- * graph without routes keeps its bytes.
297
+ * Base edges a convention provider proved false and removed, per
298
+ * provider id — e.g. a fluent route-builder call that name lookup had
299
+ * bound to an unrelated in-repo method of the same name. Present only
300
+ * when some count is non-zero.
274
301
  */
275
- route_builder_call?: number;
302
+ contested_by_provider?: Record<string, number>;
276
303
  };
304
+ /** Convention providers active in this build; present only when one was. */
305
+ providers?: ActiveProviderRecord[];
277
306
  nodes: CodeNode[];
278
307
  edges: CodeEdge[];
279
308
  }
@@ -307,8 +336,13 @@ export interface CodeGraph {
307
336
  * Bumped 4 → 5 by `road-to-a-map-that-draws-itself` 1.3: same-file
308
337
  * same-name declarations now carry `@2`, `@3` suffixes, so a cache written
309
338
  * at v4 holds ids a v5 build would not produce.
339
+ *
340
+ * Bumped 5 → 6 by `road-to-graph-providers-for-every-stack` 2.1: framework
341
+ * names left the schema (`orm-relation`, `contested_by_provider`), provider
342
+ * edges carry `provider_id`, the graph records its active providers, and the
343
+ * vocabulary gained `injects` / `di-binding`.
310
344
  */
311
- export const SCHEMA_VERSION = 5;
345
+ export const SCHEMA_VERSION = 6;
312
346
 
313
347
  /** Deterministic per-file byte cap — files above this become a SKIPPED node. */
314
348
  export const MAX_FILE_BYTES = 1_000_000;
@@ -329,6 +363,8 @@ export const EXT_LANG: Readonly<Record<string, Lang>> = {
329
363
  '.py': 'python',
330
364
  '.go': 'go',
331
365
  '.java': 'java',
366
+ '.cs': 'csharp',
367
+ '.rb': 'ruby',
332
368
  '.vue': 'typescript',
333
369
  };
334
370
 
@@ -340,6 +376,8 @@ export const GRAMMAR_WASM: Readonly<Record<Lang, string>> = {
340
376
  python: 'tree-sitter-python.wasm',
341
377
  go: 'tree-sitter-go.wasm',
342
378
  java: 'tree-sitter-java.wasm',
379
+ csharp: 'tree-sitter-c_sharp.wasm',
380
+ ruby: 'tree-sitter-ruby.wasm',
343
381
  };
344
382
 
345
383
  /** The single ABI version the pinned grammar set is built against. */
@@ -2,7 +2,7 @@
2
2
  * Schema gate — validate a graph.json (native cache or consumer-shipped)
3
3
  * before any consumer trusts it. Pure, no IO.
4
4
  */
5
- import type { CodeGraph } from './types.js';
5
+ import { type CodeGraph, GUESS_RESOLVED_VIA, STATED_RESOLVED_VIA } from './types.js';
6
6
 
7
7
  /** One space-separated string per vocabulary: this module ships in the hook bundle, paid on every dispatch. */
8
8
  const words = (s: string): ReadonlySet<string> => new Set(s.split(' '));
@@ -10,15 +10,11 @@ const words = (s: string): ReadonlySet<string> => new Set(s.split(' '));
10
10
  /** Mirrors `Relation` in types.ts. `tests` is derived by the build pass (3.2),
11
11
  * not extracted, and is validated on exactly the same terms as the rest. */
12
12
  const RELATIONS = words(
13
- 'calls imports uses inherits member tests routes-to renders includes reads migrates persists relates documents',
13
+ 'calls imports uses inherits member tests routes-to renders includes reads migrates persists relates injects documents',
14
14
  );
15
15
  const CONFIDENCES = new Set(['EXTRACTED', 'INFERRED', 'AMBIGUOUS']);
16
- /** Mirrors `ResolvedVia` in types.ts — the full union, including the four
17
- * values no build emits yet (2.3 and Phase 3 add them). */
18
- // `foreign`: adapted from a consumer's own graph file; see `foreign.ts`.
19
- const RESOLVED_VIA = words(
20
- 'same-file import-specifier path-alias psr4 route-table directive-scan schema-call eloquent-relation compiler-index test-import name-lookup dynamic foreign',
21
- );
16
+ /** Every `ResolvedVia` member: the trust lists in types.ts, so the two cannot drift. */
17
+ const RESOLVED_VIA: ReadonlySet<string> = new Set([...STATED_RESOLVED_VIA, ...GUESS_RESOLVED_VIA]);
22
18
  const PROVIDERS = new Set(['native', 'foreign', 'scip']);
23
19
  const KINDS = words('file class interface trait function method constant type enum skipped route table');
24
20
 
@@ -74,6 +70,8 @@ export function validateGraph(g: unknown): ValidateResult {
74
70
  if (ee['provider'] === undefined) push(`edge[${i}].provider is required`);
75
71
  else if (!PROVIDERS.has(ee['provider'] as string))
76
72
  push(`edge[${i}].provider invalid: ${String(ee['provider'])}`);
73
+ if (ee['provider_id'] !== undefined && typeof ee['provider_id'] !== 'string')
74
+ push(`edge[${i}].provider_id must be a string`);
77
75
  // a source must be a known node; targets may be unresolved `symbol:` refs
78
76
  if (typeof ee['source'] === 'string' && !ids.has(ee['source'] as string))
79
77
  push(`edge[${i}].source is not a known node: ${String(ee['source'])}`);