gitnexus 1.6.12-rc.20 → 1.6.12-rc.22

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 (29) hide show
  1. package/dist/cli/eval-server.js +13 -1
  2. package/dist/core/ingestion/language-config.d.ts +82 -13
  3. package/dist/core/ingestion/language-config.js +217 -13
  4. package/dist/core/ingestion/languages/typescript/query.d.ts +31 -0
  5. package/dist/core/ingestion/languages/typescript/query.js +7 -1
  6. package/dist/core/ingestion/languages/zig/query.d.ts +26 -0
  7. package/dist/core/ingestion/languages/zig/query.js +61 -1
  8. package/dist/core/ingestion/languages/zig/range-binding.d.ts +8 -0
  9. package/dist/core/ingestion/languages/zig/range-binding.js +24 -0
  10. package/dist/core/ingestion/languages/zig/scope-resolver.d.ts +3 -2
  11. package/dist/core/ingestion/languages/zig/scope-resolver.js +16 -5
  12. package/dist/core/ingestion/languages/zig/this-alias-bindings.d.ts +58 -0
  13. package/dist/core/ingestion/languages/zig/this-alias-bindings.js +176 -0
  14. package/dist/core/ingestion/languages/zig/workspace-static-gating.js +9 -3
  15. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +78 -5
  16. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +262 -7
  17. package/dist/core/ingestion/scope-resolution/pipeline/run.js +5 -1
  18. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +35 -0
  19. package/dist/core/ingestion/scope-resolution/scope/walkers.js +76 -0
  20. package/dist/core/ingestion/scope-resolution/value-ref-edges.d.ts +14 -0
  21. package/dist/core/ingestion/scope-resolution/value-ref-edges.js +14 -0
  22. package/dist/mcp/local/local-backend.d.ts +33 -0
  23. package/dist/mcp/local/local-backend.js +151 -0
  24. package/dist/mcp/tools.js +6 -4
  25. package/dist/storage/parse-cache.js +18 -1
  26. package/package.json +1 -1
  27. package/web/assets/{agent-C2n33ANd.js → agent-DuBBkGz1.js} +35 -35
  28. package/web/assets/{index-CHJoW_P1.js → index-C2pinrQ1.js} +3 -3
  29. package/web/index.html +1 -1
@@ -501,8 +501,20 @@ export function formatImpactResult(result) {
501
501
  }
502
502
  // #1858 — an interface / indirection boundary on the path makes this a lower
503
503
  // bound; surface it so the count is not read as exhaustive.
504
+ //
505
+ // The header names no cause AND asserts no omitted caller, because it cannot
506
+ // know either. DI / dynamic dispatch was the only producer of `lower-bound`
507
+ // when this was written; #3399 added callables named in VALUE position (a
508
+ // registration table, a callback argument), and one of its producers is a
509
+ // probe that could not RUN — `callableValueReferenceBoundaries` hedges on a
510
+ // failed query and says in so many words that whether the symbol is
511
+ // registered is unknown. A header claiming "some callers are not traced"
512
+ // would there assert an omission nothing established, and would contradict
513
+ // the bullet printed directly under it. The bullets carry the cause — they
514
+ // are generated per-cause by `computeEpistemicBoundary` — so the header only
515
+ // has to say the count is a floor.
504
516
  if (result.epistemic === 'lower-bound') {
505
- lines.push('⚠️ Lower bound — unresolved indirection on the path (callers binding via DI / dynamic dispatch are not traced; actual impact may be higher):');
517
+ lines.push('⚠️ Lower bound — impact may be incomplete and actual impact may be higher:');
506
518
  for (const b of result.boundaries || [])
507
519
  lines.push(` • ${b}`);
508
520
  }
@@ -71,13 +71,24 @@ export interface SwiftPackageConfig {
71
71
  /** Zig package config parsed from build.zig.zon and the root build.zig */
72
72
  export interface ZigBuildZonConfig {
73
73
  /**
74
- * Map of dependency name -> the raw `.path = "..."` value, exactly as
75
- * written in build.zig.zon (relative to the repo root, and possibly
76
- * escaping it: `../local_dep`). Consumers normalize — see
77
- * `normalizeZigDepPath` below, which rejects absolute
78
- * and repo-escaping values. `.url`-based deps cannot be resolved to a
79
- * repo-local file (they unpack into a build cache outside the repo) and so
80
- * are not included here.
74
+ * Map of dependency name -> the dep's directory, in one of two spellings
75
+ * depending on which package this config describes:
76
+ *
77
+ * - ROOT package (`pkg === ''`): the raw `.path = "..."` value, exactly as
78
+ * written in build.zig.zon (relative to the repo root, and possibly
79
+ * escaping it: `../local_dep`). This is what `parseZigBuildZon` promises
80
+ * and what its tests pin.
81
+ * - NESTED package: repo-relative and already normalized, because a nested
82
+ * package's `.path` is written relative to ITS directory and means
83
+ * nothing against the repo-relative keys consumers match on
84
+ * (`packages/app`'s `../core` is stored as `packages/core`). A dep
85
+ * escaping the REPO root is dropped rather than stored.
86
+ *
87
+ * Either spelling is safe to hand to `normalizeZigDepPath` below — it rejects
88
+ * absolute and repo-escaping values and is idempotent on an already
89
+ * normalized one, which is what `resolveZigImportInternal` relies on.
90
+ * `.url`-based deps cannot be resolved to a repo-local file (they unpack into
91
+ * a build cache outside the repo) and so are not included here.
81
92
  */
82
93
  pathDeps: Map<string, string>;
83
94
  /**
@@ -116,6 +127,27 @@ export interface ZigBuildZonConfig {
116
127
  */
117
128
  buildModules?: readonly ZigBuildModule[];
118
129
  }
130
+ /**
131
+ * One Zig build package: the directory whose `build.zig` / `build.zig.zon`
132
+ * declare the config, and that config with every path REPO-relative.
133
+ *
134
+ * A Zig module's import table is declared by the `build.zig` of the package it
135
+ * belongs to, so a repo holding several packages holds several import tables —
136
+ * the same shape a TypeScript monorepo has with a `tsconfig.json` per package.
137
+ */
138
+ export interface ZigPackageScope {
139
+ /** Repo-relative directory the package governs (`''` for the repo root). */
140
+ readonly dir: string;
141
+ readonly config: ZigBuildZonConfig;
142
+ }
143
+ /**
144
+ * Every Zig build package in the repo, indexed so the nearest one to a file
145
+ * wins — the `TsconfigIndex` analogue, and for the same reason.
146
+ */
147
+ export interface ZigWorkspaceIndex {
148
+ /** Deepest-first, so the first `dir` that prefixes a file path governs it. */
149
+ readonly packages: readonly ZigPackageScope[];
150
+ }
119
151
  /** One build module of the root `build.zig` — see `ZigBuildZonConfig.buildModules`. */
120
152
  export interface ZigBuildModule {
121
153
  /** The `addModule("<name>", …)` name; absent for `createModule` bindings
@@ -189,14 +221,51 @@ export declare function loadSwiftPackageConfig(repoRoot: string): Promise<SwiftP
189
221
  * string literals — a commented-out `.path` or a `}` inside a comment
190
222
  * or string cannot declare a dep or truncate the block.
191
223
  */
192
- export declare function loadZigBuildConfig(repoRoot: string): Promise<ZigBuildZonConfig | null>;
224
+ export declare function loadZigBuildConfig(repoRoot: string, packageDir?: string): Promise<ZigBuildZonConfig | null>;
193
225
  /**
194
- * Normalize a `.path` value from build.zig.zon into a repo-relative form.
195
- * Returns null for paths that escape the repo root (start with `..`) or
196
- * are absolute — those point to files we don't index. `.` / `./` normalize
197
- * to the empty string (the repo root itself). Shared with the import
198
- * resolver so both sides agree on which deps are in-repo.
226
+ * The Zig build package governing `filePath` — the nearest one at or above it.
227
+ *
228
+ * A Zig module's import table is declared by the `build.zig` of the package the
229
+ * file belongs to, so the nearest enclosing package is the faithful reading of
230
+ * `@import("name")` at that site, exactly as `tsconfigFor` reads a non-relative
231
+ * specifier against the nearest enclosing project.
232
+ *
233
+ * There is deliberately NO fall-through to an enclosing package when the nearest
234
+ * one does not bind the name. Falling through is how a vendored dependency's
235
+ * `@import("config")` silently resolved to the outer repo's `config` module —
236
+ * the same failure `loadTsconfigIndex` documents for a package whose own
237
+ * tsconfig declares no `baseUrl`, and the same failure the per-module import
238
+ * tables in `resolveZigImportInternal` already exist to prevent one level down.
239
+ */
240
+ export declare function zigPackageFor(index: ZigWorkspaceIndex | null | undefined, filePath: string): ZigBuildZonConfig | null;
241
+ /**
242
+ * Load every Zig build package in the repo, nearest-first.
243
+ *
244
+ * Called with no `packageDir` — which is how every call site read it before
245
+ * this function existed — `loadZigBuildConfig` reads the ROOT `build.zig` /
246
+ * `build.zig.zon` and nothing else. That is the whole configuration of a
247
+ * single-package repo and none of the configuration of a monorepo: a repo
248
+ * laying its packages out as `packages/<name>/build.zig` has no root build
249
+ * files at all, so the loader answers `null` and EVERY bare
250
+ * `@import("<module>")` in it goes unresolved — cross-file resolution silently
251
+ * degrades to relative imports only. Measured on a two-package fixture:
252
+ * `config = null`, `@import("core")` → `null`.
253
+ *
254
+ * The loader itself is not root-bound any more: this function is what supplies
255
+ * it a `packageDir`, one per package below.
256
+ *
257
+ * So the packages are discovered the way tsconfigs are (`findTsconfigFiles`):
258
+ * one bounded breadth-first walk that skips the hardcoded ignore set, then
259
+ * deepest-first ordering so `zigPackageFor` can take the first match.
260
+ *
261
+ * Called from `ScopeResolver.loadResolutionConfig`, which the orchestrator runs
262
+ * once per LANGUAGE workspace pass — so the walk happens only for repos that
263
+ * actually contain Zig. `loadImportConfigs`, which runs unconditionally for
264
+ * every repo, keeps calling `loadZigBuildConfig` for the root package alone;
265
+ * that is the same split TypeScript already has between the cheap
266
+ * `loadTsconfigPaths` and the repo-walking `loadTsconfigIndex`.
199
267
  */
268
+ export declare function loadZigWorkspaceIndex(repoRoot: string): Promise<ZigWorkspaceIndex | null>;
200
269
  export declare function normalizeZigDepPath(depPath: string): string | null;
201
270
  /**
202
271
  * The `root_source_file` paths a `build.zig` declares, dep-relative, with the
@@ -3,6 +3,7 @@ import { createReadStream } from 'fs';
3
3
  import { createInterface } from 'readline';
4
4
  import path from 'path';
5
5
  import { isDev } from './utils/env.js';
6
+ import { isHardcodedIgnoredDirectoryAtPath } from '../../config/ignore-service.js';
6
7
  import { mapConcurrent } from '../../lib/utils.js';
7
8
  import { logger } from '../logger.js';
8
9
  function normalizeComposerDirectory(baseDir, directory) {
@@ -475,10 +476,18 @@ export async function loadSwiftPackageConfig(repoRoot) {
475
476
  * string literals — a commented-out `.path` or a `}` inside a comment
476
477
  * or string cannot declare a dep or truncate the block.
477
478
  */
478
- export async function loadZigBuildConfig(repoRoot) {
479
+ export async function loadZigBuildConfig(repoRoot, packageDir = '') {
480
+ // Every path this function returns is REPO-relative, because that is the
481
+ // keyspace `allFilePaths` uses. The parsers below answer package-relative, so
482
+ // a nested package rebases them through `inPackage`. For the root package
483
+ // (`packageDir === ''`) the prefix is empty and every value is byte-identical
484
+ // to what this function returned before nested packages existed.
485
+ const pkg = packageDir === '' ? '' : `${packageDir}/`;
486
+ const inPackage = (relToPackage) => `${pkg}${relToPackage}`;
487
+ const packageFile = (name) => path.join(repoRoot, packageDir, name);
479
488
  let config = null;
480
489
  try {
481
- const raw = await fs.readFile(path.join(repoRoot, 'build.zig.zon'), 'utf-8');
490
+ const raw = await fs.readFile(packageFile('build.zig.zon'), 'utf-8');
482
491
  config = parseZigBuildZon(raw);
483
492
  }
484
493
  catch {
@@ -490,10 +499,11 @@ export async function loadZigBuildConfig(repoRoot) {
490
499
  let rootModules;
491
500
  let rootBuildZig = null;
492
501
  try {
493
- rootBuildZig = await fs.readFile(path.join(repoRoot, 'build.zig'), 'utf-8');
502
+ rootBuildZig = await fs.readFile(packageFile('build.zig'), 'utf-8');
494
503
  const parsed = parseZigRootModules(rootBuildZig);
495
- if (parsed.size > 0)
496
- rootModules = parsed;
504
+ if (parsed.size > 0) {
505
+ rootModules = new Map(Array.from(parsed, ([name, root]) => [name, inPackage(root)]));
506
+ }
497
507
  }
498
508
  catch {
499
509
  // No root build.zig — nothing to declare.
@@ -502,7 +512,7 @@ export async function loadZigBuildConfig(repoRoot) {
502
512
  if (rootBuildZig === null)
503
513
  return null;
504
514
  // No zon: no path deps, so `dep.module(…)` operands resolve to nothing.
505
- const buildModules = parseZigBuildModules(rootBuildZig);
515
+ const buildModules = rebaseZigBuildModules(parseZigBuildModules(rootBuildZig), inPackage);
506
516
  if (!rootModules && buildModules.length === 0)
507
517
  return null;
508
518
  return {
@@ -519,10 +529,25 @@ export async function loadZigBuildConfig(repoRoot) {
519
529
  // Per path dep: the modules its build.zig NAMES (`addModule("core", …)`),
520
530
  // repo-relative — what a root-build.zig `dep.module("core")` operand means.
521
531
  const depModules = new Map();
532
+ // A nested package's `.path` values are written relative to ITS directory, so
533
+ // they are rebased here and stored repo-relative; `resolveZigImportInternal`
534
+ // then reads them through the same `normalizeZigDepPath`, which is idempotent
535
+ // on an already-normalized value. A dep that escapes the REPO root (not merely
536
+ // the package) resolves to nothing and is dropped. The root package keeps its
537
+ // raw spelling, which is what `parseZigBuildZon` promises and its tests pin.
538
+ const pathDeps = pkg === '' ? config.pathDeps : new Map();
522
539
  for (const [depName, depPath] of config.pathDeps) {
523
- const rel = normalizeZigDepPath(depPath);
540
+ // Asked of the value AS WRITTEN, before the package prefix goes on: an
541
+ // absolute `.path` points outside the repository whichever package declared
542
+ // it, and prefixing hides that from `normalizeZigDepPath`. See
543
+ // `isAbsoluteZigDepPath`.
544
+ if (isAbsoluteZigDepPath(depPath))
545
+ continue;
546
+ const rel = normalizeZigDepPath(`${pkg}${depPath}`);
524
547
  if (rel === null)
525
548
  continue;
549
+ if (pkg !== '')
550
+ pathDeps.set(depName, rel);
526
551
  let buildZig;
527
552
  try {
528
553
  buildZig = await fs.readFile(path.join(repoRoot, rel, 'build.zig'), 'utf-8');
@@ -542,14 +567,169 @@ export async function loadZigBuildConfig(repoRoot) {
542
567
  if (named.size > 0)
543
568
  depModules.set(depName, named);
544
569
  }
545
- const buildModules = rootBuildZig === null ? [] : parseZigBuildModules(rootBuildZig, depModules);
570
+ const buildModules = rootBuildZig === null
571
+ ? []
572
+ : rebaseZigBuildModules(parseZigBuildModules(rootBuildZig, depModules), inPackage, depModules);
546
573
  return {
547
574
  ...config,
575
+ pathDeps,
548
576
  ...(moduleRoots.size > 0 ? { moduleRoots } : {}),
549
577
  ...(rootModules ? { rootModules } : {}),
550
578
  ...(buildModules.length > 0 ? { buildModules } : {}),
551
579
  };
552
580
  }
581
+ /**
582
+ * Rebase a package's own build modules to repo-relative paths.
583
+ *
584
+ * `parseZigBuildModules` answers package-relative for everything it read out of
585
+ * the `build.zig` it was handed, with one exception: an alias resolved through
586
+ * `depModules` (`addImport("api", dep.module("core"))`) is already repo-relative,
587
+ * because `depModules` was built that way. Prefixing that a second time would
588
+ * point the alias at a path no file has. The already-repo-relative values are
589
+ * therefore identified by membership in `depModules`, not guessed at from their
590
+ * shape.
591
+ */
592
+ function rebaseZigBuildModules(modules, inPackage, depModules) {
593
+ if (inPackage('') === '')
594
+ return [...modules];
595
+ const fromDep = new Set();
596
+ for (const named of depModules?.values() ?? [])
597
+ for (const root of named.values())
598
+ fromDep.add(root);
599
+ return modules.map((mod) => ({
600
+ ...(mod.name !== undefined ? { name: mod.name } : {}),
601
+ root: inPackage(mod.root),
602
+ imports: new Map(Array.from(mod.imports, ([alias, root]) => [
603
+ alias,
604
+ fromDep.has(root) ? root : inPackage(root),
605
+ ])),
606
+ }));
607
+ }
608
+ /** Bounds for the package walk, mirroring the tsconfig scan. */
609
+ const ZIG_SCAN_MAX_DIRS = 20_000;
610
+ const ZIG_SCAN_MAX_DEPTH = 24;
611
+ /**
612
+ * The Zig build package governing `filePath` — the nearest one at or above it.
613
+ *
614
+ * A Zig module's import table is declared by the `build.zig` of the package the
615
+ * file belongs to, so the nearest enclosing package is the faithful reading of
616
+ * `@import("name")` at that site, exactly as `tsconfigFor` reads a non-relative
617
+ * specifier against the nearest enclosing project.
618
+ *
619
+ * There is deliberately NO fall-through to an enclosing package when the nearest
620
+ * one does not bind the name. Falling through is how a vendored dependency's
621
+ * `@import("config")` silently resolved to the outer repo's `config` module —
622
+ * the same failure `loadTsconfigIndex` documents for a package whose own
623
+ * tsconfig declares no `baseUrl`, and the same failure the per-module import
624
+ * tables in `resolveZigImportInternal` already exist to prevent one level down.
625
+ */
626
+ export function zigPackageFor(index, filePath) {
627
+ if (index === null || index === undefined)
628
+ return null;
629
+ for (const scope of index.packages) {
630
+ if (scope.dir === '')
631
+ return scope.config;
632
+ if (filePath.startsWith(`${scope.dir}/`))
633
+ return scope.config;
634
+ }
635
+ return null;
636
+ }
637
+ /**
638
+ * Load every Zig build package in the repo, nearest-first.
639
+ *
640
+ * Called with no `packageDir` — which is how every call site read it before
641
+ * this function existed — `loadZigBuildConfig` reads the ROOT `build.zig` /
642
+ * `build.zig.zon` and nothing else. That is the whole configuration of a
643
+ * single-package repo and none of the configuration of a monorepo: a repo
644
+ * laying its packages out as `packages/<name>/build.zig` has no root build
645
+ * files at all, so the loader answers `null` and EVERY bare
646
+ * `@import("<module>")` in it goes unresolved — cross-file resolution silently
647
+ * degrades to relative imports only. Measured on a two-package fixture:
648
+ * `config = null`, `@import("core")` → `null`.
649
+ *
650
+ * The loader itself is not root-bound any more: this function is what supplies
651
+ * it a `packageDir`, one per package below.
652
+ *
653
+ * So the packages are discovered the way tsconfigs are (`findTsconfigFiles`):
654
+ * one bounded breadth-first walk that skips the hardcoded ignore set, then
655
+ * deepest-first ordering so `zigPackageFor` can take the first match.
656
+ *
657
+ * Called from `ScopeResolver.loadResolutionConfig`, which the orchestrator runs
658
+ * once per LANGUAGE workspace pass — so the walk happens only for repos that
659
+ * actually contain Zig. `loadImportConfigs`, which runs unconditionally for
660
+ * every repo, keeps calling `loadZigBuildConfig` for the root package alone;
661
+ * that is the same split TypeScript already has between the cheap
662
+ * `loadTsconfigPaths` and the repo-walking `loadTsconfigIndex`.
663
+ */
664
+ export async function loadZigWorkspaceIndex(repoRoot) {
665
+ const dirs = await findZigPackageDirs(repoRoot);
666
+ if (dirs.length === 0)
667
+ return null;
668
+ const packages = [];
669
+ for (const dir of dirs) {
670
+ const config = await loadZigBuildConfig(repoRoot, dir);
671
+ // A `build.zig` that declares no module and no path dep contributes nothing
672
+ // a lookup could answer with. Keeping it as an empty scope would be worse
673
+ // than dropping it: it would shadow an enclosing package that DOES declare
674
+ // the name, and answer nothing in its place.
675
+ if (config !== null)
676
+ packages.push({ dir, config });
677
+ }
678
+ if (packages.length === 0)
679
+ return null;
680
+ // Deepest first, so `zigPackageFor` takes the most specific package rather
681
+ // than whichever the walk reached first.
682
+ packages.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
683
+ return { packages };
684
+ }
685
+ /** Repo-relative directories holding a `build.zig` and/or a `build.zig.zon`. */
686
+ async function findZigPackageDirs(repoRoot) {
687
+ const found = [];
688
+ const queue = [{ dir: repoRoot, depth: 0 }];
689
+ // A HEAD INDEX rather than `queue.shift()`. The queue is pushed to while it is
690
+ // drained, which keeps the array in a mode where `shift()` memmoves the whole
691
+ // remainder instead of taking V8's left-trimming fast path — so the walk is
692
+ // quadratic in the frontier, and `ZIG_SCAN_MAX_DIRS` is the bound on how bad
693
+ // that gets. Measured at that bound (20,000 dequeues): 53 ms at fan-out 4 and
694
+ // 81 ms at fan-out 20, against 0.8 ms here — 66-106x, paid before any config
695
+ // is read. Memory is unchanged: entries were already retained by the pushes,
696
+ // `shift()` only dropped the head.
697
+ let queueHead = 0;
698
+ let dirsScanned = 0;
699
+ while (queueHead < queue.length && dirsScanned < ZIG_SCAN_MAX_DIRS) {
700
+ const { dir, depth } = queue[queueHead++];
701
+ dirsScanned++;
702
+ let entries;
703
+ try {
704
+ entries = await fs.readdir(dir, { withFileTypes: true });
705
+ }
706
+ catch {
707
+ continue;
708
+ }
709
+ let isPackage = false;
710
+ for (const entry of entries) {
711
+ if (entry.isDirectory()) {
712
+ const childDir = path.join(dir, entry.name);
713
+ if (isHardcodedIgnoredDirectoryAtPath(repoRoot, childDir))
714
+ continue;
715
+ if (depth < ZIG_SCAN_MAX_DEPTH)
716
+ queue.push({ dir: childDir, depth: depth + 1 });
717
+ continue;
718
+ }
719
+ if (!entry.isFile())
720
+ continue;
721
+ // Either marker declares a package: a `build.zig` with no zon still names
722
+ // modules, and a zon with no build.zig still names path deps.
723
+ if (entry.name === 'build.zig' || entry.name === 'build.zig.zon')
724
+ isPackage = true;
725
+ }
726
+ if (isPackage) {
727
+ const rel = path.relative(repoRoot, dir).split(path.sep).join('/');
728
+ found.push(rel === '.' || rel === '' ? '' : rel);
729
+ }
730
+ }
731
+ return found;
732
+ }
553
733
  /**
554
734
  * Normalize a `.path` value from build.zig.zon into a repo-relative form.
555
735
  * Returns null for paths that escape the repo root (start with `..`) or
@@ -557,13 +737,37 @@ export async function loadZigBuildConfig(repoRoot) {
557
737
  * to the empty string (the repo root itself). Shared with the import
558
738
  * resolver so both sides agree on which deps are in-repo.
559
739
  */
740
+ /**
741
+ * Does this `.path` value point outside the repository BY ITS SPELLING —
742
+ * POSIX absolute (`/dep`), Windows drive-qualified (`C:\dep`, `C:/dep`),
743
+ * root-relative (`\dep`) or UNC (`\\server\share`)?
744
+ *
745
+ * Separators are normalized first so every Windows spelling is visible to the
746
+ * one test. Exported-in-spirit rather than inlined because it must be asked in
747
+ * TWO places and the two must not drift: `normalizeZigDepPath` asks it of the
748
+ * value it is given, and `loadZigBuildConfig` asks it of a NESTED package's
749
+ * value BEFORE prefixing the package directory. That second call is the whole
750
+ * point — prefixing turns `/dep` into `packages/app//dep`, which is relative by
751
+ * inspection, so the check inside `normalizeZigDepPath` no longer sees an
752
+ * absolute path and the empty segment is simply dropped, mapping an
753
+ * out-of-repo dependency onto a real in-repo directory if one happens to exist.
754
+ *
755
+ * `path.posix.join` is NOT a substitute: it strips the leading slash too
756
+ * (`join('packages/app/', '/dep')` is `packages/app/dep`), so it produces the
757
+ * same fabricated path without ever rejecting anything.
758
+ *
759
+ * A `..` prefix is deliberately NOT handled here. `../core` escapes the
760
+ * package but not necessarily the repo, and rebasing it is exactly what the
761
+ * nested-package branch exists to do; `normalizeZigDepPath` rejects the ones
762
+ * that still escape the ROOT after rebasing.
763
+ */
764
+ function isAbsoluteZigDepPath(depPath) {
765
+ const normalized = depPath.replace(/\\/g, '/');
766
+ return normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized);
767
+ }
560
768
  export function normalizeZigDepPath(depPath) {
561
- // Normalize separators BEFORE the absolute check so every Windows spelling
562
- // is visible to it: POSIX (`/x`), drive (`C:\x`, `C:/x`), root-relative
563
- // (`\x` → `/x`) and UNC (`\\server\share` → `//server/share`) paths all
564
- // point outside the repository.
565
769
  const normalized = depPath.replace(/\\/g, '/');
566
- if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized))
770
+ if (isAbsoluteZigDepPath(depPath))
567
771
  return null;
568
772
  const parts = [];
569
773
  for (const part of normalized.split('/')) {
@@ -51,6 +51,37 @@
51
51
  */
52
52
  import Parser from 'tree-sitter';
53
53
  export declare const TYPESCRIPT_SCOPE_QUERY: string;
54
+ /**
55
+ * JSX-only query suffix. Appended to the base query when compiling
56
+ * against the TSX grammar; NOT compiled against the plain TS grammar
57
+ * (which has no \`jsx_*\` node types and would reject these patterns).
58
+ *
59
+ * Why JSX as a CALLS edge: \`<Foo />\` is syntactic sugar for \`Foo(props)\`
60
+ * and the React component is invoked by the renderer, so for blast-radius
61
+ * (\`gitnexus_impact("Badge", direction: "upstream")\`) and call-graph
62
+ * (\`gitnexus_context("Foo")\`) purposes JSX usage IS a call. Routing
63
+ * through \`@reference.call.free\` / \`@reference.call.member\` makes the
64
+ * downstream caller-walk + edge-emission paths handle JSX uniformly with
65
+ * ordinary call expressions — no new edge type, no schema changes.
66
+ *
67
+ * Identifier-only JSX is filtered to PascalCase via \`(#match? ... "^[A-Z]")\`
68
+ * so \`<div>\`, \`<span>\`, \`<button>\` and other native HTML elements (which
69
+ * by JSX convention start lowercase) don't emit edges to nonexistent
70
+ * "div" / "span" symbols. Member-form JSX (\`<Foo.Bar />\`) is always a
71
+ * component (HTML element names can't contain dots), so no predicate
72
+ * filter is applied there.
73
+ *
74
+ * Both \`jsx_self_closing_element\` (\`<Foo />\`) and \`jsx_opening_element\`
75
+ * (\`<Foo>...</Foo>\`) emit; the closing tag is intentionally NOT captured —
76
+ * each JSX element should emit exactly one CALLS edge per use site.
77
+ */
78
+ /**
79
+ * Exported alongside `TYPESCRIPT_SCOPE_QUERY` so
80
+ * `value-ref-dispatchability.test.ts` checks the whole query a `.tsx` file is
81
+ * analyzed with. Checking the base alone would miss a `value-ref` rule added
82
+ * here. Not part of the provider surface — use `getTsScopeQuery`.
83
+ */
84
+ export declare const TSX_JSX_QUERY_SUFFIX = "\n;; <Foo />\n((jsx_self_closing_element\n name: (identifier) @reference.name) @reference.call.free\n (#match? @reference.name \"^[A-Z]\"))\n\n;; <Foo> ... </Foo> (paired form \u2014 match the opening tag only)\n((jsx_opening_element\n name: (identifier) @reference.name) @reference.call.free\n (#match? @reference.name \"^[A-Z]\"))\n\n;; <Foo.Bar /> / <Container.Section.Title /> \u2014 namespaced JSX\n(jsx_self_closing_element\n name: (member_expression\n object: (_) @reference.receiver\n property: (property_identifier) @reference.name)) @reference.call.member\n\n(jsx_opening_element\n name: (member_expression\n object: (_) @reference.receiver\n property: (property_identifier) @reference.name)) @reference.call.member\n";
54
85
  /**
55
86
  * Return the right tree-sitter parser for `filePath` (or the TS parser
56
87
  * when no path is given — the legacy callsite shape).
@@ -1338,7 +1338,13 @@ export const TYPESCRIPT_SCOPE_QUERY = `
1338
1338
  * (\`<Foo>...</Foo>\`) emit; the closing tag is intentionally NOT captured —
1339
1339
  * each JSX element should emit exactly one CALLS edge per use site.
1340
1340
  */
1341
- const TSX_JSX_QUERY_SUFFIX = `
1341
+ /**
1342
+ * Exported alongside `TYPESCRIPT_SCOPE_QUERY` so
1343
+ * `value-ref-dispatchability.test.ts` checks the whole query a `.tsx` file is
1344
+ * analyzed with. Checking the base alone would miss a `value-ref` rule added
1345
+ * here. Not part of the provider surface — use `getTsScopeQuery`.
1346
+ */
1347
+ export const TSX_JSX_QUERY_SUFFIX = `
1342
1348
  ;; <Foo />
1343
1349
  ((jsx_self_closing_element
1344
1350
  name: (identifier) @reference.name) @reference.call.free
@@ -1,3 +1,29 @@
1
1
  import Parser from 'tree-sitter';
2
+ /**
3
+ * Zig scope-resolution query (RFC #909 Ring 3).
4
+ *
5
+ * The grammar is vendored (`vendor/tree-sitter-zig`) and may be absent on a
6
+ * platform without a prebuild, so the language module is required lazily and
7
+ * `getZigParser` / `getZigScopeQuery` throw only when actually invoked without
8
+ * the grammar installed. That is safe: the parse pipeline filters `.zig` files
9
+ * through `parser-loader.isLanguageAvailable` before any scope extraction runs.
10
+ *
11
+ * Zig specifics encoded here:
12
+ * - Containers (struct/enum/union/opaque) are anonymous nodes bound by the
13
+ * enclosing `variable_declaration`; declarations capture the binding
14
+ * identifier from the wrapper.
15
+ * - `@import` is a builtin call, not import-statement syntax; the
16
+ * `#eq?` predicate keeps other builtins (@sizeOf, @as, …) out.
17
+ * - A plain `(variable_declaration (identifier))` rule would also match
18
+ * container and import bindings — `emitZigScopeCaptures` filters those
19
+ * groups out so a name binds exactly once.
20
+ */
21
+ /**
22
+ * Exported for `value-ref-dispatchability.test.ts`, which reads every language's
23
+ * scope query to enforce the keyed/unkeyed partition that
24
+ * `callableValueReferenceBoundaries`' dispatch exclusion depends on. Not part of
25
+ * the provider surface — nothing else should import it.
26
+ */
27
+ export declare const ZIG_SCOPE_QUERY = "\n;; Scopes\n(source_file) @scope.module\n(struct_declaration) @scope.class\n(enum_declaration) @scope.class\n(union_declaration) @scope.class\n(opaque_declaration) @scope.class\n(function_declaration) @scope.function\n(test_declaration) @scope.function\n(block) @scope.block\n\n;; Declarations \u2014 functions (relabeled @declaration.method inside containers\n;; by emitZigScopeCaptures, mirroring the provider's labelOverride)\n(function_declaration\n name: (identifier) @declaration.name) @declaration.function\n\n;; Declarations \u2014 named tests. Same naming rule as ZIG_QUERIES: the string\n;; node WITH quotes, so the def joins the graph node and never collides with\n;; a same-named fn. Anonymous / decl-form tests are scopes without a def.\n(test_declaration\n (string) @declaration.name) @declaration.function\n\n;; Declarations \u2014 containers. The binding name lives on the wrapper\n;; variable_declaration, but the ANCHOR is the container node itself so its\n;; range equals the @scope.class range: the extractor then attaches the def\n;; to the class scope (walkers.populateClassOwnedMembers expects the\n;; class-like def among the class scope's ownedDefs) and auto-hoists the\n;; name binding to the parent scope. Keyword-gated like the ordinary\n;; binding rules: a keyword-less `x = struct {\u2026};` is an assignment\n;; (tree-sitter-zig 1.1.2 reuses variable_declaration), not a container def.\n(variable_declaration\n \"const\" . (identifier) @declaration.name\n (struct_declaration) @declaration.struct)\n(variable_declaration\n \"var\" . (identifier) @declaration.name\n (struct_declaration) @declaration.struct)\n(variable_declaration\n \"const\" . (identifier) @declaration.name\n (enum_declaration) @declaration.enum)\n(variable_declaration\n \"var\" . (identifier) @declaration.name\n (enum_declaration) @declaration.enum)\n(variable_declaration\n \"const\" . (identifier) @declaration.name\n (union_declaration) @declaration.union)\n(variable_declaration\n \"var\" . (identifier) @declaration.name\n (union_declaration) @declaration.union)\n;; opaque {} is a fieldless container that may own methods \u2014 Struct, as in\n;; ZIG_QUERIES (see the rationale there).\n(variable_declaration\n \"const\" . (identifier) @declaration.name\n (opaque_declaration) @declaration.struct)\n(variable_declaration\n \"var\" . (identifier) @declaration.name\n (opaque_declaration) @declaration.struct)\n\n;; Declarations \u2014 generic type constructors. `fn List(comptime T: type) type\n;; { return struct {\u2026}; }` is Zig's only spelling of a generic type; the\n;; returned container is anonymous in the grammar but every reader (and every\n;; caller: `List(u8)`) names it after the function. Anchor on the container\n;; so the def sits in its own class scope; the name binding is hoisted to the\n;; MODULE scope by `zigBindingScopeFor` (not the fn body, where the name\n;; would be invisible to callers) and coexists with the Function def of the\n;; same name \u2014 `List` really is both a callable and a type.\n((function_declaration\n name: (identifier) @declaration.name\n type: (builtin_type) @_ret\n body: (block (expression_statement (return_expression\n (struct_declaration) @declaration.struct))))\n (#eq? @_ret \"type\"))\n((function_declaration\n name: (identifier) @declaration.name\n type: (builtin_type) @_ret\n body: (block (expression_statement (return_expression\n (union_declaration) @declaration.union))))\n (#eq? @_ret \"type\"))\n((function_declaration\n name: (identifier) @declaration.name\n type: (builtin_type) @_ret\n body: (block (expression_statement (return_expression\n (enum_declaration) @declaration.enum))))\n (#eq? @_ret \"type\"))\n\n;; Declarations \u2014 container fields (struct fields, enum/union variants).\n;; The #not-eq? guard drops the MISSING placeholder identifier tree-sitter-zig\n;; recovers for an empty container body (see ZIG_QUERIES). The optional\n;; `type:` (absent on enum variants) is captured as @declaration.field-type:\n;; `emitZigScopeCaptures` turns it into a @type-binding.field on the\n;; container's Class scope so `self.session.name()` can walk the field's\n;; type (Rust/Go parity \u2014 see the F5 block in captures.ts).\n((container_field\n name: (identifier) @declaration.name\n type: (_)? @declaration.field-type) @declaration.field\n (#not-eq? @declaration.name \"\"))\n\n;; Declarations \u2014 const/var bindings (import/container groups filtered in TS).\n;; The `.` anchor pins the FIRST named child: without it the pattern also\n;; matched the initializer of `const first = target;`, minting a phantom\n;; local named `target` that shadowed the real callee for every later\n;; reference in the block. The literal keyword is load-bearing too:\n;; tree-sitter-zig 1.1.2 parses statement assignments (`x = 5;`, `x += 1;`,\n;; `_ = expr;`) as `variable_declaration` WITHOUT a keyword child, and\n;; without the keyword every assignment and every discard minted a phantom\n;; local (one `_` per statement).\n(variable_declaration\n \"const\" . (identifier) @declaration.name) @declaration.variable\n(variable_declaration\n \"var\" . (identifier) @declaration.name) @declaration.variable\n\n;; Imports \u2014 const x = @import(\"...\") / var x = @import(\"...\"). Keyword-gated\n;; like every binding rule: a keyword-less `x = @import(\"...\")` is a\n;; statement (see the side-effect rule below), not a binding.\n(variable_declaration\n \"const\" . (identifier) @import.name\n (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.statement\n(variable_declaration\n \"var\" . (identifier) @import.name\n (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.statement\n\n;; Imports \u2014 const X = @import(\"...\").X : a NAMED import of one member. The\n;; local name is whatever the user chose (`const Alloc = @import(\"std\").mem;`\n;; is a rename), the imported name is the member. A deeper chain\n;; (`@import(\"std\").mem.Allocator`, `@import(\"lib.zig\").B.work`) is matched\n;; by the second rule below but is NOT bound as a named import of the\n;; innermost member: that discarded the written owner (`B`) and let a\n;; same-named `A.work` answer first. `emitZigScopeCaptures` binds the module\n;; under the builtin's text instead and rewrites the alias's use sites to the\n;; full path (`collectZigDeepAliases`, PR #1432 review 8.4).\n(variable_declaration\n \"const\" . (identifier) @import.name\n (field_expression\n object: (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n member: (identifier) @import.imported)\n (#eq? @_builtin \"@import\")) @import.statement\n(variable_declaration\n \"const\" . (identifier) @import.name\n (field_expression\n object: (field_expression\n object: (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source)))\n member: (identifier) @import.imported)\n (#eq? @_builtin \"@import\")) @import.statement\n\n;; Imports \u2014 a keyword-less `<ident> = @import(\"...\");` statement\n;; (`_ = @import(\"all_tests.zig\");` in a test block, the refAllDecls\n;; idiom): tree-sitter-zig reuses `variable_declaration` for assignments, so\n;; the shape is a declaration minus the keyword. It references the file\n;; without binding a name \u2014 a side-effect import. Tree-sitter queries cannot\n;; say \"no keyword child\", so this rule matches the keyword-bearing shapes\n;; too; `emitZigScopeCaptures` keeps it only when `isZigKeywordDeclaration`\n;; is false (the keyword shapes are the binding rules above).\n(variable_declaration\n . (identifier)\n (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.side-effect\n\n;; Aliases of a namespace member \u2014 const Counter = counter.Counter; where\n;; `counter` is an @import binding of THIS file. The query cannot know which\n;; identifiers are import bindings, so it captures every one-level member\n;; alias and `emitZigScopeCaptures` promotes the ones whose object is a\n;; known @import to a named import (same fact as `const Counter =\n;; @import(\"counter.zig\").Counter;`); the rest stay ordinary variables.\n;; Only the ONE-level shape is promoted; a deeper chain (`lib.B.work`,\n;; `std.mem.Allocator`) is a deep alias \u2014 a Const whose use sites are\n;; rewritten to the written owner path (see the import rule above, 8.4).\n(variable_declaration\n \"const\" . (identifier) @alias.name\n (field_expression\n object: (identifier) @alias.namespace\n member: (identifier) @alias.member) .) @alias.statement\n(variable_declaration\n \"const\" . (identifier) @alias.name\n (field_expression\n object: (field_expression\n object: (identifier) @alias.namespace)\n member: (identifier) @alias.member) .) @alias.statement\n\n;; Imports \u2014 pub usingnamespace @import(\"...\"); : every pub decl of the target\n;; becomes a decl of this container (removed from the language in 0.15, still\n;; everywhere in 0.11\u20130.14 code). Modelled as a wildcard import.\n(using_namespace_declaration\n (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.wildcard\n\n;; Imports \u2014 `@import(\"...\")` in ANY other position: a tuple element\n;; (`pub const Interfaces = .{ @import(\"a.zig\"), @import(\"b.zig\") }`, the\n;; JS-API registration table), a call argument (`event.is(@import(\"x.zig\"))`),\n;; a comparison operand (`T == @import(\"x.zig\").T`), the receiver of a\n;; member call (`try @import(\"dump.zig\").root(...)`), a 3-deep member chain\u2026\n;; Every one of them is a file dependency; only the const/var/usingnamespace\n;; shapes above bind a name. This rule matches EVERY `@import` builtin, the\n;; bound shapes included \u2014 `emitZigScopeCaptures` drops the matches whose\n;; string node a binding rule (or the keyword-less side-effect rule) already\n;; claimed, so a bound import is never doubled, and emits the rest as\n;; side-effect imports (file edge, no binding) \u2014 except the member-call\n;; receiver, which becomes a namespace import keyed by its own source text so\n;; the call resolves into the imported module (see the emitter).\n((builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.inline\n\n;; Type bindings \u2014 parameter annotations (incl. self: *T receivers)\n(parameter\n name: (identifier) @type-binding.name\n type: (_) @type-binding.type) @type-binding.parameter\n\n;; Type bindings \u2014 constructor inference: const p = T{ ... }. Keyword-gated\n;; like every binding rule: a keyword-less `p = T{ ... };` is a\n;; re-assignment (same node type in tree-sitter-zig 1.1.2), and Zig's static\n;; typing means `p` already carries its type from its declaration\n;; (annotation, constructor or inferred value) \u2014 the assignment declares\n;; nothing, and `_ = T{ ... };` must not bind `_`.\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (struct_initializer\n (identifier) @type-binding.type)) @type-binding.constructor\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (struct_initializer\n (identifier) @type-binding.type)) @type-binding.constructor\n\n;; Type bindings \u2014 qualified constructor: const p = mod.T{ ... }. The whole\n;; field_expression is captured so the dotted text \"mod.T\" survives \u2014\n;; receiver dispatch resolves the namespace prefix through the import\n;; binding (emitReceiverBoundCalls Case 3).\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (struct_initializer\n (field_expression) @type-binding.type)) @type-binding.constructor\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (struct_initializer\n (field_expression) @type-binding.type)) @type-binding.constructor\n\n;; Type bindings \u2014 generic instantiation literal: const l = List(u8){ ... }.\n;; The callee is the type constructor; `normalizeZigTypeName` drops the\n;; comptime argument list so `List(u8)` looks up `List`.\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (struct_initializer\n (call_expression) @type-binding.type)) @type-binding.constructor\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (struct_initializer\n (call_expression) @type-binding.type)) @type-binding.constructor\n\n;; Type bindings \u2014 declared type: var x: T = \u2026; const x: T = .init(\u2026);\n;; The annotation is the ONLY type source for `= undefined` and for 0.14+\n;; decl literals (`.init`, `.empty`), which are the idiomatic\n;; constructors in current std. Ranked below constructor inference by the\n;; shared resolver (source 'annotation'), so a literal on the right still\n;; wins when both are present.\n(variable_declaration\n . (identifier) @type-binding.name\n type: (_) @type-binding.type) @type-binding.annotation\n\n;; Type bindings \u2014 value inference (F6): const t = <value>; where <value> is a\n;; call (`Counter.init()`, `makeThing()`, `node.asElement()`), possibly\n;; wrapped in `try` / `catch \u2026` / `\u2026 orelse \u2026` / parentheses \u2014 the shape of\n;; nearly every Zig constructor call (`const p = try Page.init(\u2026)`). The\n;; query only pins the declaration; `emitZigScopeCaptures` unwraps the\n;; wrappers and rewrites `@type-binding.type` to the type source (see\n;; `zigCallReturnTypeOf`). Keyword-gated: `_ = e.top();` is an assignment\n;; (same node type), not a binding of `_`. Rust's twin is\n;; `let x = Foo::new()` / `let x = foo().await`.\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (_) @type-binding.value .) @type-binding.call-return\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (_) @type-binding.value .) @type-binding.call-return\n\n;; Type bindings \u2014 return-type annotation (F6): `fn make() !*Thing` binds\n;; `make \u21A6 Thing` in the enclosing scope (Module for free fns, the container's\n;; Class scope for methods \u2014 that is where the compound resolver reads a\n;; method's return type for `node.asElement()`), so `const t = makeThing()`\n;; chains to `Thing`. Rust: `(function_item \u2026 return_type:) @type-binding.return`.\n;; `emitZigScopeCaptures` drops builtin / `type` returns (a `List \u21A6 type`\n;; binding would hijack the `List(u8){}` constructor chain).\n(function_declaration\n name: (identifier) @type-binding.name\n type: (_) @type-binding.type) @type-binding.return\n\n;; Type bindings \u2014 aliases (F5 field-access aliases + F7 type aliases):\n;; `const page = self.page;` (F5: the RHS path is kept verbatim as the\n;; \"type\", the compound resolver's member-alias branch re-resolves it as a\n;; receiver chain \u2014 head `self` \u2192 class \u2192 field type), `const LocalAlias = Local;`,\n;; `const Proto = HtmlElement;`, `const T2 = Thing;` (alias of an alias /\n;; import), `const B = util.List(u8);` (an INSTANTIATED generic type\n;; constructor). Zig has no `type X = Y` syntax \u2014 a type alias is a const\n;; whose value is a type expression, and it stays a Const in the graph. What\n;; must change is the scope side: bind the alias NAME to the value's type\n;; text (Rust's `let x = y` / JS's `const B = Foo` `@type-binding.alias`,\n;; source 'assignment-inferred'), so `LocalAlias.mk()` types through Case 4,\n;; `B.init()` / `x: B` / `B{}` through Case 3 once `normalizeZigTypeName`\n;; drops the comptime arguments (`util.List(u8)` \u2192 `util.List`), and every\n;; binding that names the alias (`var l = LocalAlias.mk()`, `var x: B`) is\n;; chained to the target by the shared `followChainedRef` /\n;; `followChainPostFinalize`. The identifier / member shapes take `var` too:\n;; `var node = orig_node;` is the same value alias as Rust's `let x = y`\n;; and chains to the type of `orig_node` (a Zig type is comptime and never\n;; `var`, so the type-alias reading only ever applies to `const`). The\n;; call shape is `const`-only and kept only when the callee's last\n;; identifier is TitleCase \u2014 see `emitZigScopeCaptures` (a value call\n;; `const t = util.makeThing()` belongs to the call-return rules above and\n;; must not receive a competing binding). A promoted namespace-member alias\n;; (`const Counter = counter.Counter;` \u2192 named import) is skipped there too:\n;; the import binding already carries the type.\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (identifier) @type-binding.type .) @type-binding.alias\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (identifier) @type-binding.type .) @type-binding.alias\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (field_expression) @type-binding.type .) @type-binding.alias\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (field_expression) @type-binding.type .) @type-binding.alias\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (call_expression) @type-binding.type .) @type-binding.alias\n\n;; References \u2014 VALUE positions (#3399): a callable named where a value is\n;; expected rather than where a callee is. Zig's JS bridge is built entirely\n;; out of this shape \u2014\n;;\n;; pub const namespaceURI = bridge.accessor(Element.getNamespaceUri, null, .{});\n;;\n;; \u2014 2,047 such declarations across 257 files in lightpanda-io/browser, the\n;; project's whole JS\u2194Zig surface, and NONE of them reached the graph: Zig\n;; emitted no `value-ref` capture at all, so a public DOM accessor's only\n;; recorded callers were the two internal ones and `impact` called that `exact`.\n;;\n;; These become reference-class USES edges through the existing\n;; `mapReferenceKindToEdgeType` mapping \u2014 a registration is not an invocation\n;; (Kythe `ref` vs `ref/call`; Joern `METHOD_REF`) \u2014 resolved by the\n;; property-dispatch pass, which keeps ONLY callable targets. That callable gate\n;; is what makes these deliberately broad rules safe: `js.Bridge(Element)` and\n;; `register(count)` match too, and emit nothing, exactly as TypeScript's\n;; `{ port: DEFAULT_PORT }` does.\n;;\n;; No `@reference.property-key` is attached: Zig has no object-literal key to\n;; dispatch through, so these register a reference and never synthesize CALLS.\n;; The terminal invoke \u2014 `Accessor.init` \u2192 a struct field \u2192 `Factory.zig`'s\n;; `inline for`/`@typeInfo` \u2192 `@call(.auto, func, args)` \u2014 needs comptime\n;; evaluation and is deliberately NOT modelled; `impact` reports the shortfall\n;; as `epistemic: \"lower-bound\"` instead of pretending to certainty.\n\n;; Call ARGUMENTS. In tree-sitter-zig arguments are direct children of\n;; `call_expression`, NOT wrapped in an `arguments` node (only builtins have\n;; one), so the callee has to be consumed explicitly by `function:` \u2014 without\n;; that binding the same rule also matches the callee of `foo(bar)` and mints a\n;; USES edge duplicating the call.\n(call_expression\n function: (_)\n (identifier) @reference.name @reference.value-ref)\n\n;; Qualified argument \u2014 `bridge.accessor(Element.getNamespaceUri, \u2026)`. The\n;; RECEIVER is captured alongside the member so the site carries the owner it\n;; was written with; `@reference.name` stays the member, which is the name the\n;; scope walk resolves.\n(call_expression\n function: (_)\n (field_expression\n object: (_) @reference.receiver\n member: (identifier) @reference.name) @reference.value-ref)\n\n;; Const binding initialiser \u2014 `pub const defaultHandler = onReset;`. Both\n;; anchors are load-bearing: the leading `.` pins the bound name to the first\n;; named child (see the declaration rules above), and the trailing `.` keeps the\n;; initializer as the LAST child, so a `const x: T = y` annotation shape cannot\n;; put the TYPE in value position.\n(variable_declaration\n \"const\" . (identifier)\n (identifier) @reference.name @reference.value-ref .)\n\n;; References \u2014 free calls: foo(...)\n(call_expression\n function: (identifier) @reference.name) @reference.call.free\n\n;; References \u2014 member calls: obj.method(...) / mod.fn(...)\n(call_expression\n function: (field_expression\n object: (_) @reference.receiver\n member: (identifier) @reference.name)) @reference.call.member\n\n;; References \u2014 constructor uses: T{ ... }\n(struct_initializer\n (identifier) @reference.name) @reference.call.constructor\n\n;; References \u2014 qualified constructor uses: mod.T{ ... } / hub.sub.T{ ... }\n;; Captured with the RECEIVER, not as a free constructor with a raw qualified\n;; name, on purpose: the free-call fallback resolves a qualified constructor by\n;; its simple tail, and a workspace-unique `Thing` then answers for\n;; `other.Thing{}` whichever module the source named (measured: `c.Thing{}`\n;; with no `Thing` in c.zig bound to a.zig's). With the receiver the site goes\n;; through the receiver-bound namespace case, which resolves the member inside\n;; the module the receiver is bound to \u2014 the same path `mod.fn()` takes \u2014 so\n;; `a.Thing{}` and `b.Thing{}` each bind their own file and `std.Thread.Mutex{}`\n;; binds nothing even when a local `Mutex` exists.\n(struct_initializer\n (field_expression\n object: (_) @reference.receiver\n member: (identifier) @reference.name)) @reference.call.constructor\n\n;; References \u2014 generic instantiation literals: List(u8){ ... } /\n;; lists.List(u8){ ... }. The type head is a call_expression \u2014 the\n;; instantiation of the type constructor \u2014 which neither constructor rule\n;; above matches, so the OUTER aggregate event had no site: only the inner\n;; `List(u8)` call (a free / member call reference on the call node) reached\n;; the graph (PR #1432 review, 8.11). Two sites on two anchors: the call\n;; (an invocation of `List`) and this initializer (a construction of the\n;; container `List` returns, marked `(constructor)`). The receiver form goes\n;; through the same namespace path as `mod.T{}`.\n(struct_initializer\n (call_expression\n function: (identifier) @reference.name)) @reference.call.constructor\n(struct_initializer\n (call_expression\n function: (field_expression\n object: (_) @reference.receiver\n member: (identifier) @reference.name))) @reference.call.constructor\n";
2
28
  export declare function getZigParser(): Parser;
3
29
  export declare function getZigScopeQuery(): Parser.Query;
@@ -19,7 +19,13 @@ import { requireVendoredGrammar } from '../../../tree-sitter/vendored-grammars.j
19
19
  * container and import bindings — `emitZigScopeCaptures` filters those
20
20
  * groups out so a name binds exactly once.
21
21
  */
22
- const ZIG_SCOPE_QUERY = `
22
+ /**
23
+ * Exported for `value-ref-dispatchability.test.ts`, which reads every language's
24
+ * scope query to enforce the keyed/unkeyed partition that
25
+ * `callableValueReferenceBoundaries`' dispatch exclusion depends on. Not part of
26
+ * the provider surface — nothing else should import it.
27
+ */
28
+ export const ZIG_SCOPE_QUERY = `
23
29
  ;; Scopes
24
30
  (source_file) @scope.module
25
31
  (struct_declaration) @scope.class
@@ -358,6 +364,60 @@ const ZIG_SCOPE_QUERY = `
358
364
  "const" . (identifier) @type-binding.name
359
365
  (call_expression) @type-binding.type .) @type-binding.alias
360
366
 
367
+ ;; References — VALUE positions (#3399): a callable named where a value is
368
+ ;; expected rather than where a callee is. Zig's JS bridge is built entirely
369
+ ;; out of this shape —
370
+ ;;
371
+ ;; pub const namespaceURI = bridge.accessor(Element.getNamespaceUri, null, .{});
372
+ ;;
373
+ ;; — 2,047 such declarations across 257 files in lightpanda-io/browser, the
374
+ ;; project's whole JS↔Zig surface, and NONE of them reached the graph: Zig
375
+ ;; emitted no \`value-ref\` capture at all, so a public DOM accessor's only
376
+ ;; recorded callers were the two internal ones and \`impact\` called that \`exact\`.
377
+ ;;
378
+ ;; These become reference-class USES edges through the existing
379
+ ;; \`mapReferenceKindToEdgeType\` mapping — a registration is not an invocation
380
+ ;; (Kythe \`ref\` vs \`ref/call\`; Joern \`METHOD_REF\`) — resolved by the
381
+ ;; property-dispatch pass, which keeps ONLY callable targets. That callable gate
382
+ ;; is what makes these deliberately broad rules safe: \`js.Bridge(Element)\` and
383
+ ;; \`register(count)\` match too, and emit nothing, exactly as TypeScript's
384
+ ;; \`{ port: DEFAULT_PORT }\` does.
385
+ ;;
386
+ ;; No \`@reference.property-key\` is attached: Zig has no object-literal key to
387
+ ;; dispatch through, so these register a reference and never synthesize CALLS.
388
+ ;; The terminal invoke — \`Accessor.init\` → a struct field → \`Factory.zig\`'s
389
+ ;; \`inline for\`/\`@typeInfo\` → \`@call(.auto, func, args)\` — needs comptime
390
+ ;; evaluation and is deliberately NOT modelled; \`impact\` reports the shortfall
391
+ ;; as \`epistemic: "lower-bound"\` instead of pretending to certainty.
392
+
393
+ ;; Call ARGUMENTS. In tree-sitter-zig arguments are direct children of
394
+ ;; \`call_expression\`, NOT wrapped in an \`arguments\` node (only builtins have
395
+ ;; one), so the callee has to be consumed explicitly by \`function:\` — without
396
+ ;; that binding the same rule also matches the callee of \`foo(bar)\` and mints a
397
+ ;; USES edge duplicating the call.
398
+ (call_expression
399
+ function: (_)
400
+ (identifier) @reference.name @reference.value-ref)
401
+
402
+ ;; Qualified argument — \`bridge.accessor(Element.getNamespaceUri, …)\`. The
403
+ ;; RECEIVER is captured alongside the member so the site carries the owner it
404
+ ;; was written with; \`@reference.name\` stays the member, which is the name the
405
+ ;; scope walk resolves.
406
+ (call_expression
407
+ function: (_)
408
+ (field_expression
409
+ object: (_) @reference.receiver
410
+ member: (identifier) @reference.name) @reference.value-ref)
411
+
412
+ ;; Const binding initialiser — \`pub const defaultHandler = onReset;\`. Both
413
+ ;; anchors are load-bearing: the leading \`.\` pins the bound name to the first
414
+ ;; named child (see the declaration rules above), and the trailing \`.\` keeps the
415
+ ;; initializer as the LAST child, so a \`const x: T = y\` annotation shape cannot
416
+ ;; put the TYPE in value position.
417
+ (variable_declaration
418
+ "const" . (identifier)
419
+ (identifier) @reference.name @reference.value-ref .)
420
+
361
421
  ;; References — free calls: foo(...)
362
422
  (call_expression
363
423
  function: (identifier) @reference.name) @reference.call.free