@event4u/agent-config 17.2.1 → 17.3.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 (152) 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 +152 -0
  6. package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
  7. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  8. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  9. package/dist/cli/commands/recordTriggerEval.js +1 -1
  10. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
  11. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  12. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
  13. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
  14. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  15. package/dist/cli-delegate/cmd_conformance.js +20 -14
  16. package/dist/cli-delegate/cmd_doctor.js +3 -3
  17. package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
  18. package/dist/cli-delegate/cmd_explain.js +1 -1
  19. package/dist/cli-delegate/cmd_export.js +2 -2
  20. package/dist/cli-delegate/cmd_prune.js +2 -2
  21. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  22. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  23. package/dist/cli-delegate/cmd_sync.js +2 -2
  24. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  25. package/dist/cli-delegate/cmd_validate.js +2 -2
  26. package/dist/discovery/deprecation-report.md +1 -1
  27. package/dist/discovery/discovery-manifest.json +3 -3
  28. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  29. package/dist/discovery/discovery-manifest.summary.md +1 -1
  30. package/dist/discovery/dormancy-report.md +1 -1
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +2 -2
  33. package/dist/discovery/trust-report.md +1 -1
  34. package/dist/discovery/workspaces.json +2 -2
  35. package/dist/hooks/code_graph_refresh.chunk.cjs +414 -0
  36. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  37. package/dist/hooks/dispatch.js +21 -11
  38. package/dist/hooks/dispatch.sha256 +1 -1
  39. package/dist/hooks/egress_engine.chunk.cjs +1 -1
  40. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  41. package/dist/install/install.mjs +6974 -6037
  42. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  43. package/dist/mcp/registry-manifest.json +3 -2
  44. package/dist/mcp/server.json +13 -4
  45. package/dist/mcp/server.mjs +5561 -2463
  46. package/dist/scripts/_lib/egress_rules.js +8 -2
  47. package/dist/scripts/condense.js +9 -9
  48. package/dist/server/routes/discovery.js +1 -1
  49. package/dist/server/routes/install.js +1 -1
  50. package/dist/server/routes/ping.js +1 -1
  51. package/dist/server/routes/schema.js +4 -4
  52. package/dist/server/routes/settings.js +2 -6
  53. package/dist/server/routes/wizard.js +6 -6
  54. package/dist/server/schemas/settings.js +42 -42
  55. package/dist/shared/settingsSurface.js +1 -1
  56. package/dist/shared/userMd/schema.js +1 -1
  57. package/dist/shared/zodJsonSchema.js +47 -0
  58. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  59. package/dist/ui/index.html +1 -1
  60. package/package.json +3 -3
  61. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  62. package/src/config/ci-local-parity.yml +11 -0
  63. package/src/config/continuity-surface.json +9 -2
  64. package/src/config/gate-violation-baselines.json +3 -3
  65. package/src/config/gitignore-block.txt +6 -5
  66. package/src/config/hook-bundle-budget.json +16 -1
  67. package/src/config/pack-size-budget.json +12 -1
  68. package/src/config/packed-binary-manifest.json +9 -0
  69. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  70. package/src/scripts/_dispatch.bash +1 -1
  71. package/src/scripts/_lib/egress_rules.ts +8 -2
  72. package/src/scripts/_lib/listing_manifests.ts +85 -0
  73. package/src/scripts/_lib/neighbour_census.ts +1 -0
  74. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  75. package/src/scripts/adoption_status.ts +231 -27
  76. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  77. package/src/scripts/check_dependency_floors.ts +7 -0
  78. package/src/scripts/check_release_manifest_content.ts +17 -2
  79. package/src/scripts/check_release_pr_shape.ts +5 -0
  80. package/src/scripts/code_graph/build.ts +80 -44
  81. package/src/scripts/code_graph/cli.ts +260 -16
  82. package/src/scripts/code_graph/conventions_default.ts +228 -0
  83. package/src/scripts/code_graph/extract.ts +5 -2
  84. package/src/scripts/code_graph/extract_more.ts +105 -1
  85. package/src/scripts/code_graph/loader.ts +10 -2
  86. package/src/scripts/code_graph/map.ts +174 -41
  87. package/src/scripts/code_graph/map_model.ts +53 -15
  88. package/src/scripts/code_graph/map_verify.ts +12 -0
  89. package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
  90. package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
  91. package/src/scripts/code_graph/passes.ts +173 -0
  92. package/src/scripts/code_graph/providers/angular.ts +55 -0
  93. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  94. package/src/scripts/code_graph/providers/catalog.ts +290 -0
  95. package/src/scripts/code_graph/providers/django.ts +163 -0
  96. package/src/scripts/code_graph/providers/express.ts +25 -0
  97. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  98. package/src/scripts/code_graph/providers/flask.ts +99 -0
  99. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  100. package/src/scripts/code_graph/providers/index.ts +101 -0
  101. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  102. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  103. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  104. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  105. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  106. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  107. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  108. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  109. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  110. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  111. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  112. package/src/scripts/code_graph/providers/spring.ts +138 -0
  113. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  114. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  115. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  116. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  117. package/src/scripts/code_graph/providers/types.ts +56 -0
  118. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  119. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  120. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  121. package/src/scripts/code_graph/refresh_lock.ts +193 -0
  122. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  123. package/src/scripts/code_graph/scip_reader.ts +2 -2
  124. package/src/scripts/code_graph/sqlite_store.ts +10 -4
  125. package/src/scripts/code_graph/types.ts +75 -41
  126. package/src/scripts/code_graph/validate.ts +6 -8
  127. package/src/scripts/condense.ts +9 -9
  128. package/src/scripts/generate_settings_reference.ts +2 -6
  129. package/src/scripts/hook_chunks.mjs +4 -0
  130. package/src/scripts/hook_manifest.json +1 -1
  131. package/src/scripts/hook_manifest.yaml +24 -7
  132. package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
  133. package/src/scripts/hooks/lazy_concern.ts +4 -0
  134. package/src/scripts/install-hooks.sh +54 -33
  135. package/src/scripts/install.ts +2 -6
  136. package/src/scripts/lint_hook_manifest.ts +2 -0
  137. package/src/scripts/lint_marketplace.ts +33 -0
  138. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  139. package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
  140. package/src/scripts/mcp_server/graph_tools.ts +5 -1
  141. package/src/scripts/release.ts +2 -5
  142. package/src/scripts/release_drill.ts +4 -1
  143. package/src/scripts/release_env.ts +17 -0
  144. package/src/scripts/release_mix_taxonomy.json +3 -0
  145. package/src/server/schemas/settings.ts +42 -42
  146. package/src/shared/settingsSurface.ts +1 -1
  147. package/src/shared/userMd/schema.ts +1 -1
  148. package/src/shared/zodJsonSchema.ts +41 -0
  149. package/src/vendor/grammars/README.md +6 -5
  150. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  151. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  152. package/dist/ui/assets/index-w70Ado09.js +0 -23
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The default convention pass (ADR-298): runs for every project, names no
3
+ * framework, and links only what a string can state about a file.
4
+ *
5
+ * literal-path './x' · 'views/a.html' code --reads--> the one code or template file it names
6
+ * literal-dotted render('a.b') code --renders--> the one template at a/b.<template ext>
7
+ * fs-route pages/a/[id].tsx route --routes-to-> the page module
8
+ *
9
+ * Every edge is INFERRED, drawn only on a single match, and never where an
10
+ * edge between the same two files already exists — an import, a provider's
11
+ * edge or an earlier literal outranks it. The two axes are independent
12
+ * (`types.ts`): the confidence is INFERRED because a convention connects the
13
+ * pair, while the mechanism is a statement — the literal names that exact file
14
+ * — so these edges count as stated for the map and the gate verbs.
15
+ */
16
+ import * as path from 'node:path';
17
+
18
+ import type { SourceFile } from './build.js';
19
+ import type { CodeEdge, CodeNode, ResolvedVia } from './types.js';
20
+ import { EXT_LANG, MAX_FILE_BYTES } from './types.js';
21
+
22
+ /** The text right before a JS/TS module specifier: `from`, `import`, `import(`, `require(`. */
23
+ const IMPORT_POSITION = /(?:\bfrom|\bimport|\bimport\s*\(|\brequire\s*\()\s*$/;
24
+
25
+ export type DefaultMechanism = 'literal-path' | 'literal-dotted' | 'fs-route';
26
+
27
+ /**
28
+ * Which mechanisms run. A mechanism under the 90 % precision bar is switched off
29
+ * here, never the pass (readings: `graph-providers-baseline-2026-10-23.md`).
30
+ * `literal-dotted` measured 3 of 6 on Fixture L — a translation key such as
31
+ * `trans('common.sort')` matches a view of the same name — so dotted names are
32
+ * left to providers, which know which call takes a template.
33
+ */
34
+ export const DEFAULT_MECHANISMS: Readonly<Record<DefaultMechanism, boolean>> = {
35
+ 'literal-path': true,
36
+ 'literal-dotted': false,
37
+ 'fs-route': true,
38
+ };
39
+
40
+ /** Template extensions a dotted name may resolve to; longest first so `.blade.php` wins over `.php`. */
41
+ const TEMPLATE_EXTS = [
42
+ '.blade.php',
43
+ '.handlebars',
44
+ '.mustache',
45
+ '.jinja2',
46
+ '.cshtml',
47
+ '.liquid',
48
+ '.jinja',
49
+ '.razor',
50
+ '.twig',
51
+ '.html',
52
+ '.htm',
53
+ '.hbs',
54
+ '.ejs',
55
+ '.pug',
56
+ '.njk',
57
+ '.erb',
58
+ '.tpl',
59
+ '.j2',
60
+ ];
61
+
62
+ const PAGE_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.vue', '.svelte', '.astro', '.mdx']);
63
+
64
+ export interface DefaultInput {
65
+ files: readonly SourceFile[];
66
+ /** Every path in the project, code or not; the code and doc paths when the build knows no more. */
67
+ paths: readonly string[];
68
+ nodes: readonly CodeNode[];
69
+ edges: readonly CodeEdge[];
70
+ mechanisms?: Readonly<Record<DefaultMechanism, boolean>>;
71
+ }
72
+
73
+ export interface DefaultStats {
74
+ 'literal-path': number;
75
+ 'literal-dotted': number;
76
+ 'fs-route': number;
77
+ }
78
+
79
+ export function templateBase(p: string): string | null {
80
+ for (const ext of TEMPLATE_EXTS) if (p.endsWith(ext) && p.length > ext.length) return p.slice(0, -ext.length);
81
+ return null;
82
+ }
83
+
84
+ /** `pages/a/[id].tsx` → `/a/[id]`; null when the file is no page module. */
85
+ export function fsRoutePath(p: string, pageDirs: ReadonlySet<string>): { verb: string; route: string } | null {
86
+ const ext = path.posix.extname(p);
87
+ if (!PAGE_EXTS.has(ext)) return null;
88
+ const segs = p.split('/');
89
+ const base = segs[segs.length - 1] as string;
90
+ const stem = base.slice(0, -ext.length);
91
+ const clean = (parts: string[]): string =>
92
+ `/${parts.filter((s) => !/^\(.*\)$/.test(s) && !s.startsWith('@') && s !== 'index').join('/')}`.replace(/\/$/, '') || '/';
93
+ const at = (dir: string): number => segs.lastIndexOf(dir, segs.length - 2);
94
+ const app = at('app');
95
+ if (app >= 0 && (stem === 'page' || stem === 'route') && pageDirs.has(segs.slice(0, app + 1).join('/'))) {
96
+ return { verb: stem === 'route' ? 'ROUTE' : 'PAGE', route: clean(segs.slice(app + 1, -1)) };
97
+ }
98
+ const routes = at('routes');
99
+ if (routes >= 1 && segs[routes - 1] === 'src' && /^\+(page|server)$/.test(stem)) {
100
+ return { verb: stem === '+server' ? 'ROUTE' : 'PAGE', route: clean(segs.slice(routes + 1, -1)) };
101
+ }
102
+ const pages = at('pages');
103
+ if (pages >= 0 && !stem.startsWith('_') && pageDirs.has(segs.slice(0, pages + 1).join('/'))) {
104
+ const rest = [...segs.slice(pages + 1, -1), stem];
105
+ return { verb: rest[0] === 'api' ? 'ROUTE' : 'PAGE', route: clean(rest) };
106
+ }
107
+ return null;
108
+ }
109
+
110
+ /** Directories that hold page-shaped modules: an `app/` with a `page.*`, a `pages/` with an `index.*`. */
111
+ function pageDirectories(paths: readonly string[]): Set<string> {
112
+ const dirs = new Set<string>();
113
+ for (const p of paths) {
114
+ const segs = p.split('/');
115
+ const base = segs[segs.length - 1] as string;
116
+ const ext = path.posix.extname(base);
117
+ if (!PAGE_EXTS.has(ext)) continue;
118
+ const stem = base.slice(0, -ext.length);
119
+ for (let i = segs.length - 2; i >= 0; i--) {
120
+ if (segs[i] === 'app' && stem === 'page') dirs.add(segs.slice(0, i + 1).join('/'));
121
+ if (segs[i] === 'pages' && stem === 'index') dirs.add(segs.slice(0, i + 1).join('/'));
122
+ }
123
+ }
124
+ return dirs;
125
+ }
126
+
127
+ export function conventionsDefault(input: DefaultInput): { nodes: CodeNode[]; edges: CodeEdge[]; stats: DefaultStats } {
128
+ const on = input.mechanisms ?? DEFAULT_MECHANISMS;
129
+ const stats: DefaultStats = { 'literal-path': 0, 'literal-dotted': 0, 'fs-route': 0 };
130
+ const nodes: CodeNode[] = [];
131
+ const edges: CodeEdge[] = [];
132
+ const fileNodes = new Set(input.nodes.filter((n) => n.kind === 'file').map((n) => n.id));
133
+ // A literal may name code, a document or a template — never a data file or
134
+ // a dotfile, which would fill the map with what the code merely configures.
135
+ const inventory = new Set([...input.paths.filter((p) => templateBase(p) !== null), ...fileNodes]);
136
+ const nodeIds = new Set(input.nodes.map((n) => n.id));
137
+ const fileOf = new Map(input.nodes.map((n) => [n.id, n.source_file] as const));
138
+ const linked = new Set<string>();
139
+ for (const e of input.edges) {
140
+ const a = fileOf.get(e.source);
141
+ const b = fileOf.get(e.target) ?? (inventory.has(e.target) ? e.target : undefined);
142
+ if (a && b) linked.add(`${a}\0${b}`);
143
+ }
144
+ const ensureFile = (p: string): void => {
145
+ if (nodeIds.has(p)) return;
146
+ nodeIds.add(p);
147
+ nodes.push({ id: p, label: p, kind: 'file', source_file: p, source_location: [] });
148
+ };
149
+ const link = (source: string, target: string, relation: CodeEdge['relation'], via: ResolvedVia & DefaultMechanism): void => {
150
+ const key = `${source}\0${target}`;
151
+ if (source === target || linked.has(key)) return;
152
+ linked.add(key);
153
+ ensureFile(target);
154
+ edges.push({ source, target, relation, confidence: 'INFERRED', resolved_via: via, provider: 'native' });
155
+ stats[via] += 1;
156
+ };
157
+
158
+ // Dotted names: `a/b/c` → the template files whose path ends there.
159
+ const byDotted = new Map<string, string[]>();
160
+ if (on['literal-dotted']) {
161
+ for (const p of input.paths) {
162
+ const base = templateBase(p);
163
+ if (!base) continue;
164
+ const segs = base.split('/');
165
+ for (let i = 0; i <= segs.length - 2; i++) {
166
+ const key = segs.slice(i).join('/');
167
+ byDotted.set(key, [...(byDotted.get(key) ?? []), p]);
168
+ }
169
+ }
170
+ }
171
+
172
+ for (const f of [...input.files].sort((a, b) => (a.path < b.path ? -1 : 1))) {
173
+ if (Buffer.byteLength(f.source, 'utf-8') > MAX_FILE_BYTES) continue;
174
+ const dir = path.posix.dirname(f.path);
175
+ const lang = EXT_LANG[path.posix.extname(f.path).toLowerCase()];
176
+ const infer = lang === 'php' ? ['.php'] : lang === 'python' ? ['.py'] : ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.vue'];
177
+ const jsLike = lang === 'typescript' || lang === 'javascript';
178
+ if (on['literal-path']) {
179
+ // Single and double quotes only: a backtick span is usually a mention in a comment.
180
+ for (const m of f.source.matchAll(/(['"])([^'"`\s\\${}()<>|,;]{3,200})\1/g)) {
181
+ const lit = m[2] as string;
182
+ // A module specifier belongs to the import tier: resolved, it is an
183
+ // `imports` edge already; unresolved, it is an external module — a
184
+ // read in neither case.
185
+ if (jsLike && IMPORT_POSITION.test(f.source.slice(Math.max(0, (m.index as number) - 40), m.index as number))) continue;
186
+ if (lit.includes('://') || lit.startsWith('/') || lit.includes('*') || lit.endsWith('/')) continue;
187
+ const hasExt = /\.[A-Za-z0-9]{1,10}$/.test(path.posix.basename(lit));
188
+ if (!lit.includes('/') && !hasExt) continue;
189
+ const relative = lit.startsWith('./') || lit.startsWith('../');
190
+ const bases = relative ? [path.posix.join(dir, lit)] : [path.posix.normalize(lit), path.posix.join(dir, lit)];
191
+ const hits = new Set<string>();
192
+ for (const b of bases) {
193
+ if (b.startsWith('..')) continue;
194
+ if (inventory.has(b)) hits.add(b);
195
+ if (!hasExt) for (const ext of infer) for (const c of [`${b}${ext}`, `${b}/index${ext}`]) if (inventory.has(c)) hits.add(c);
196
+ }
197
+ if (hits.size === 1) link(f.path, [...hits][0] as string, 'reads', 'literal-path');
198
+ }
199
+ }
200
+ if (on['literal-dotted']) {
201
+ for (const m of f.source.matchAll(/\(\s*(['"])([A-Za-z_][\w-]*(?:\.[A-Za-z_][\w-]*)+)\1/g)) {
202
+ const hits = byDotted.get((m[2] as string).replace(/\./g, '/')) ?? [];
203
+ if (hits.length === 1) link(f.path, hits[0] as string, 'renders', 'literal-dotted');
204
+ }
205
+ }
206
+ }
207
+
208
+ if (on['fs-route']) {
209
+ const pageDirs = pageDirectories(input.paths);
210
+ const seen = new Set<string>();
211
+ // A provider that knows the framework already declared this module's routes.
212
+ const routedFiles = new Set(input.nodes.filter((n) => n.kind === 'route').map((n) => n.source_file));
213
+ for (const p of input.paths) {
214
+ const r = fsRoutePath(p, pageDirs);
215
+ if (!r || routedFiles.has(p)) continue;
216
+ const id = `${p}#route:${r.verb} ${r.route}`;
217
+ // A provider that knows this framework may already have drawn the page.
218
+ if (seen.has(id) || nodeIds.has(id)) continue;
219
+ seen.add(id);
220
+ ensureFile(p);
221
+ nodes.push({ id, label: `${r.verb} ${r.route}`, kind: 'route', source_file: p, source_location: [] });
222
+ edges.push({ source: p, target: id, relation: 'member', confidence: 'EXTRACTED', resolved_via: 'same-file', provider: 'native' });
223
+ edges.push({ source: id, target: p, relation: 'routes-to', confidence: 'INFERRED', resolved_via: 'fs-route', provider: 'native' });
224
+ stats['fs-route'] += 1;
225
+ }
226
+ }
227
+ return { nodes, edges, stats };
228
+ }
@@ -11,7 +11,7 @@
11
11
  * receivers resolved against the parsed class hierarchy.
12
12
  * AMBIGUOUS — dynamic method calls, facades, scoped calls to an unresolved
13
13
  * name: kept with candidates, never dropped. This is the
14
- * expected majority for dynamic-dispatch code (Laravel etc.).
14
+ * expected majority for dynamic-dispatch code (PHP frameworks etc.).
15
15
  */
16
16
  import {
17
17
  type CodeNode,
@@ -20,7 +20,7 @@ import {
20
20
  MAX_FILE_BYTES,
21
21
  type Relation,
22
22
  } from './types.js';
23
- import { extractGo, extractJava, extractPython } from './extract_more.js';
23
+ import { extractCSharp, extractGo, extractJava, extractPython } from './extract_more.js';
24
24
  import { getParser, nodeHasError, type TsNode } from './loader.js';
25
25
 
26
26
  export type CallStyle = 'new' | 'free' | 'this' | 'self' | 'static' | 'parent' | 'scoped' | 'dynamic';
@@ -88,6 +88,8 @@ export interface FileExtract {
88
88
  /** local class id → its declared parent/interface names (2nd-pass inherits). */
89
89
  inherits: Array<{ classId: string; parentName: string }>;
90
90
  parseError: boolean;
91
+ /** C#: the namespaces the file declares, for `using` resolution in the build pass. */
92
+ namespaces?: string[];
91
93
  }
92
94
 
93
95
  export function loc(n: TsNode): number[] {
@@ -700,6 +702,7 @@ export async function extractFile(relPath: string, source: string, lang: Lang):
700
702
  else if (lang === 'python') extractPython(tree.rootNode, relPath, out);
701
703
  else if (lang === 'java') extractJava(tree.rootNode, relPath, out);
702
704
  else if (lang === 'go') extractGo(tree.rootNode, relPath, out);
705
+ else if (lang === 'csharp') extractCSharp(tree.rootNode, relPath, out);
703
706
  else extractTsJs(tree.rootNode, relPath, out);
704
707
  } finally {
705
708
  tree.delete(); // delete the tree, never the cached parser (loader.ts)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Python, Go and Java extraction — the same shape `extract.ts` emits for
2
+ * Python, Go, Java and C# extraction — the same shape `extract.ts` emits for
3
3
  * PHP / TS / JS, so the build pass resolves them with no language branch
4
4
  * beyond module lookup.
5
5
  *
@@ -312,3 +312,107 @@ export function extractGo(root: TsNode, file: string, out: FileExtract): void {
312
312
  };
313
313
  walk(root, file);
314
314
  }
315
+
316
+ /** The name a C# type expression names: `List<User>` → `List`, `A.B` → `B`. */
317
+ function csType(n: TsNode): string {
318
+ if (n.type === 'generic_name') return text(namedChildren(n).find((c) => c.type === 'identifier'));
319
+ if (n.type === 'qualified_name') return lastSegment(n.text.replace(/<.*$/, ''));
320
+ return n.text.replace(/<.*$/, '');
321
+ }
322
+
323
+ export function extractCSharp(root: TsNode, file: string, out: FileExtract): void {
324
+ out.nodes.push({ id: file, label: file, kind: 'file', source_file: file, source_location: [] });
325
+ const TYPES: Record<string, 'class' | 'interface' | 'enum'> = {
326
+ class_declaration: 'class',
327
+ record_declaration: 'class',
328
+ struct_declaration: 'class',
329
+ interface_declaration: 'interface',
330
+ enum_declaration: 'enum',
331
+ };
332
+ const namespaces: string[] = [];
333
+ const walk = (n: TsNode, classId: string | null, scopeId: string, ns = ''): void => {
334
+ const kind = TYPES[n.type];
335
+ if (kind) {
336
+ const nm = nameField(n) ?? '<anon>';
337
+ const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
338
+ out.nodes.push({ id, label: nm, kind, source_file: file, source_location: loc(n) });
339
+ out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
340
+ for (const c of namedChildren(n)) {
341
+ if (c.type !== 'base_list') continue;
342
+ for (const t of namedChildren(c)) {
343
+ if (t.type === 'identifier' || t.type === 'generic_name' || t.type === 'qualified_name') {
344
+ out.inherits.push({ classId: id, parentName: csType(t) });
345
+ }
346
+ }
347
+ }
348
+ for (const c of namedChildren(n)) if (c.type === 'declaration_list') for (const d of namedChildren(c)) walk(d, id, id, ns);
349
+ return;
350
+ }
351
+ switch (n.type) {
352
+ case 'namespace_declaration':
353
+ case 'file_scoped_namespace_declaration': {
354
+ const name = n.childForFieldName('name');
355
+ const inner = ns ? `${ns}.${text(name)}` : text(name);
356
+ if (name && !namespaces.includes(inner)) namespaces.push(inner);
357
+ const body = n.childForFieldName('body') ?? n;
358
+ for (const c of namedChildren(body)) if (!same(c, name)) walk(c, classId, scopeId, inner);
359
+ return;
360
+ }
361
+ case 'using_directive': {
362
+ const parts = namedChildren(n);
363
+ const spec = parts.find((c) => c.type === 'qualified_name' || c.type === 'identifier');
364
+ const target = parts.filter((c) => c.type === 'qualified_name' || c.type === 'identifier').pop();
365
+ if (spec && target) {
366
+ const aliased = parts.some((c) => c.type === 'name_equals') || n.text.includes('=');
367
+ out.rawEdges.push({
368
+ sourceId: file,
369
+ relation: 'imports',
370
+ targetName: lastSegment(target.text),
371
+ confidenceHint: 'EXTRACTED',
372
+ moduleSpecifier: target.text,
373
+ importKind: 'namespace',
374
+ ...(aliased && spec !== target ? { localName: spec.text } : {}),
375
+ });
376
+ }
377
+ return;
378
+ }
379
+ case 'method_declaration':
380
+ case 'constructor_declaration': {
381
+ const nm = nameField(n) ?? '<anon>';
382
+ const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
383
+ out.nodes.push({ id, label: nm, kind: 'method', source_file: file, source_location: loc(n) });
384
+ out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
385
+ for (const c of namedChildren(n)) if (c.type === 'block' || c.type === 'arrow_expression_clause') walk(c, classId, id, ns);
386
+ return;
387
+ }
388
+ case 'invocation_expression': {
389
+ const fn = n.childForFieldName('function');
390
+ const ctx = classId ? { enclosingClassId: classId } : {};
391
+ if (fn && (fn.type === 'identifier' || fn.type === 'generic_name')) {
392
+ out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: csType(fn), confidenceHint: classId ? 'INFERRED' : 'AMBIGUOUS', callStyle: 'this', ...ctx });
393
+ } else if (fn && fn.type === 'member_access_expression') {
394
+ const obj = fn.childForFieldName('expression');
395
+ const nm = csType(fn.childForFieldName('name') ?? fn);
396
+ if (obj && (obj.type === 'this_expression' || obj.type === 'base_expression')) {
397
+ out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: classId ? 'INFERRED' : 'AMBIGUOUS', callStyle: obj.type === 'base_expression' ? 'parent' : 'this', ...ctx });
398
+ } else if (obj && obj.type === 'identifier' && /^[A-Z]/.test(obj.text)) {
399
+ out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: 'AMBIGUOUS', callStyle: 'scoped', scopeName: obj.text, ...ctx });
400
+ } else {
401
+ out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: 'AMBIGUOUS', callStyle: 'dynamic', ...ctx });
402
+ }
403
+ }
404
+ break;
405
+ }
406
+ case 'object_creation_expression': {
407
+ const t = n.childForFieldName('type');
408
+ if (t) out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: csType(t), confidenceHint: 'EXTRACTED', callStyle: 'new' });
409
+ break;
410
+ }
411
+ default:
412
+ break;
413
+ }
414
+ for (const c of namedChildren(n)) walk(c, classId, scopeId, ns);
415
+ };
416
+ walk(root, null, file);
417
+ out.namespaces = namespaces;
418
+ }
@@ -146,16 +146,24 @@ async function _init(): Promise<void> {
146
146
  }
147
147
  }
148
148
 
149
+ /**
150
+ * Grammars the pinned `tree-sitter-wasms` compiles at another ABI than the rest.
151
+ * The C# grammar ships at ABI 13, which the pinned runtime still loads; the pin
152
+ * stays exact per language, so a drift is still caught.
153
+ */
154
+ export const GRAMMAR_ABI_PIN: Readonly<Partial<Record<Lang, number>>> = { csharp: 13 };
155
+
149
156
  /** Load (and cache) a grammar; asserts the ABI matches the pinned expectation. */
150
157
  export async function loadLanguage(lang: Lang): Promise<TsLanguage> {
151
158
  const cached = _langCache.get(lang);
152
159
  if (cached) return cached;
153
160
  await _init();
154
161
  const language = await _Parser().Language.load(grammarWasmPath(lang));
155
- if (language.version !== EXPECTED_GRAMMAR_ABI) {
162
+ const pinned = GRAMMAR_ABI_PIN[lang] ?? EXPECTED_GRAMMAR_ABI;
163
+ if (language.version !== pinned) {
156
164
  throw new Error(
157
165
  `code-graph: grammar ABI drift for ${lang} — got ${language.version}, ` +
158
- `pinned ${EXPECTED_GRAMMAR_ABI}. Re-pin web-tree-sitter/tree-sitter-wasms ` +
166
+ `pinned ${pinned}. Re-pin web-tree-sitter/tree-sitter-wasms ` +
159
167
  `and re-run the ABI smoke test.`,
160
168
  );
161
169
  }