gitnexus 1.6.11 → 1.6.12-rc.10

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 (63) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/analyze-config.d.ts +2 -2
  3. package/dist/cli/analyze-config.js +10 -33
  4. package/dist/cli/optional-grammars.d.ts +7 -8
  5. package/dist/cli/optional-grammars.js +9 -19
  6. package/dist/core/git-ref.d.ts +22 -0
  7. package/dist/core/git-ref.js +114 -0
  8. package/dist/core/ingestion/call-extractors/zig-static-gating.d.ts +5 -8
  9. package/dist/core/ingestion/languages/zig/query.js +7 -8
  10. package/dist/core/ingestion/languages/zig/scope-resolver.js +2 -0
  11. package/dist/core/ingestion/languages/zig/workspace-static-gating.d.ts +8 -0
  12. package/dist/core/ingestion/languages/zig/workspace-static-gating.js +76 -0
  13. package/dist/core/ingestion/parsing-processor.d.ts +22 -6
  14. package/dist/core/ingestion/parsing-processor.js +38 -19
  15. package/dist/core/ingestion/pipeline-phases/parse-impl.js +291 -93
  16. package/dist/core/ingestion/pipeline-phases/parse-round-budget.d.ts +42 -0
  17. package/dist/core/ingestion/pipeline-phases/parse-round-budget.js +50 -0
  18. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +13 -0
  19. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +1 -0
  20. package/dist/core/ingestion/scope-resolution/pipeline/run.js +5 -0
  21. package/dist/core/ingestion/workers/parse-worker.js +14 -6
  22. package/dist/core/ingestion/workers/worker-pool.d.ts +56 -2
  23. package/dist/core/ingestion/workers/worker-pool.js +117 -26
  24. package/dist/core/search/fts-indexes.js +13 -4
  25. package/dist/core/tree-sitter/parser-loader.js +4 -12
  26. package/dist/core/tree-sitter/vendored-grammars.d.ts +1 -1
  27. package/dist/core/tree-sitter/vendored-grammars.js +2 -1
  28. package/dist/mcp/local/local-backend.js +9 -3
  29. package/dist/server/analyze-job.d.ts +19 -1
  30. package/dist/server/analyze-job.js +14 -3
  31. package/dist/server/analyze-launch.d.ts +14 -0
  32. package/dist/server/analyze-launch.js +59 -14
  33. package/dist/server/analyze-worker-ipc.d.ts +6 -5
  34. package/dist/server/analyze-worker-ipc.js +4 -0
  35. package/dist/server/api.js +53 -6
  36. package/dist/server/git-clone.d.ts +38 -5
  37. package/dist/server/git-clone.js +216 -38
  38. package/dist/storage/parsedfile-store.js +53 -1
  39. package/dist/storage/v8-sidecar.d.ts +11 -0
  40. package/dist/storage/v8-sidecar.js +15 -7
  41. package/package.json +1 -5
  42. package/scripts/build-tree-sitter-grammars.cjs +2 -1
  43. package/vendor/tree-sitter-zig/LICENSE +21 -0
  44. package/vendor/tree-sitter-zig/README.md +25 -0
  45. package/vendor/tree-sitter-zig/binding.gyp +29 -0
  46. package/vendor/tree-sitter-zig/bindings/node/binding.cc +20 -0
  47. package/vendor/tree-sitter-zig/bindings/node/index.d.ts +28 -0
  48. package/vendor/tree-sitter-zig/bindings/node/index.js +11 -0
  49. package/vendor/tree-sitter-zig/grammar.js +900 -0
  50. package/vendor/tree-sitter-zig/package.json +18 -0
  51. package/vendor/tree-sitter-zig/prebuilds/SHA256SUMS +6 -0
  52. package/vendor/tree-sitter-zig/prebuilds/darwin-arm64/tree-sitter-zig.node +0 -0
  53. package/vendor/tree-sitter-zig/prebuilds/darwin-x64/tree-sitter-zig.node +0 -0
  54. package/vendor/tree-sitter-zig/prebuilds/linux-arm64/tree-sitter-zig.node +0 -0
  55. package/vendor/tree-sitter-zig/prebuilds/linux-x64/tree-sitter-zig.node +0 -0
  56. package/vendor/tree-sitter-zig/prebuilds/win32-arm64/tree-sitter-zig.node +0 -0
  57. package/vendor/tree-sitter-zig/prebuilds/win32-x64/tree-sitter-zig.node +0 -0
  58. package/vendor/tree-sitter-zig/src/grammar.json +6570 -0
  59. package/vendor/tree-sitter-zig/src/node-types.json +3178 -0
  60. package/vendor/tree-sitter-zig/src/parser.c +169829 -0
  61. package/vendor/tree-sitter-zig/src/tree_sitter/alloc.h +54 -0
  62. package/vendor/tree-sitter-zig/src/tree_sitter/array.h +291 -0
  63. package/vendor/tree-sitter-zig/src/tree_sitter/parser.h +266 -0
package/README.md CHANGED
@@ -631,7 +631,7 @@ results until you re-run `gitnexus analyze --repair-fts` from a shell where the
631
631
 
632
632
  ### Installation fails with native module errors
633
633
 
634
- Some optional language grammars (Dart, Proto, Swift, Kotlin) require native compilation. If they fail, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before installing. Zig (`@tree-sitter-grammars/tree-sitter-zig`, an npm `optionalDependency`) behaves the same way: if its native binding fails to install, `.zig` files are skipped.
634
+ Some optional language grammars (Dart, Proto, Swift, Kotlin, Zig) ship vendored native prebuilds. If a prebuild is missing and a source build is not possible, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before installing.
635
635
 
636
636
  If `npm install -g gitnexus` fails on native modules:
637
637
 
@@ -41,8 +41,8 @@ export declare class GitNexusRcError extends Error {
41
41
  }
42
42
  /**
43
43
  * Validate a user-supplied branch name (from CLI or `.gitnexusrc`). Returns the
44
- * trimmed name or throws {@link GitNexusRcError}. Conservative but accepts the
45
- * shapes real branches use (`feature/foo-bar`, `release/1.2`, `develop`).
44
+ * trimmed name or throws {@link GitNexusRcError}. Rules live in
45
+ * `core/git-ref.ts`; this wrapper keeps the CLI / `.gitnexusrc` error type.
46
46
  */
47
47
  export declare function validateBranchName(value: string, source: string): string;
48
48
  /**
@@ -30,11 +30,10 @@
30
30
  import fs from 'node:fs';
31
31
  import path from 'node:path';
32
32
  import { readRepoControlFile } from '../config/repo-control-file.js';
33
+ import { InvalidBranchError, validateBranchName as validateBranchNameCore, } from '../core/git-ref.js';
33
34
  export const GITNEXUS_RC_FILENAME = '.gitnexusrc';
34
35
  /** Final fallback when no branch is configured or detectable. */
35
36
  export const DEFAULT_BRANCH_FALLBACK = 'main';
36
- /** Git refs longer than this are almost certainly a mistake / injection attempt. */
37
- const BRANCH_MAX_LENGTH = 255;
38
37
  /**
39
38
  * Thrown for any `.gitnexusrc` problem (missing-file is NOT an error — it
40
39
  * returns `undefined`). The message is user-facing and names the file so the
@@ -126,41 +125,19 @@ const assertNoHiddenChars = (value, source) => {
126
125
  };
127
126
  /**
128
127
  * Validate a user-supplied branch name (from CLI or `.gitnexusrc`). Returns the
129
- * trimmed name or throws {@link GitNexusRcError}. Conservative but accepts the
130
- * shapes real branches use (`feature/foo-bar`, `release/1.2`, `develop`).
128
+ * trimmed name or throws {@link GitNexusRcError}. Rules live in
129
+ * `core/git-ref.ts`; this wrapper keeps the CLI / `.gitnexusrc` error type.
131
130
  */
132
131
  export function validateBranchName(value, source) {
133
- const trimmed = value.trim();
134
- if (!trimmed) {
135
- throw new GitNexusRcError(`${source}: branch name must not be empty.`);
136
- }
137
- if (trimmed.length > BRANCH_MAX_LENGTH) {
138
- throw new GitNexusRcError(`${source}: branch name is too long (max ${BRANCH_MAX_LENGTH}).`);
139
- }
140
- assertNoHiddenChars(trimmed, source);
141
- if (/\s/.test(trimmed)) {
142
- throw new GitNexusRcError(`${source}: branch name must not contain whitespace.`);
143
- }
144
- // git ref-name rules (subset): reject characters git itself forbids in refs.
145
- if (/[~^:?*[\\]/.test(trimmed)) {
146
- throw new GitNexusRcError(`${source}: branch name contains characters not allowed in a git ref (~ ^ : ? * [ \\).`);
147
- }
148
- if (trimmed.startsWith('-')) {
149
- throw new GitNexusRcError(`${source}: branch name must not start with "-".`);
150
- }
151
- if (trimmed.includes('..')) {
152
- throw new GitNexusRcError(`${source}: branch name must not contain "..".`);
132
+ try {
133
+ return validateBranchNameCore(value, source);
153
134
  }
154
- // Git permits a backtick in a ref, but the branch is embedded inside a
155
- // Markdown inline-code span in the generated AGENTS.md/CLAUDE.md regression
156
- // example, where a backtick would close the span early and let the rest of
157
- // the template render as instruction text. Reject it at this single
158
- // chokepoint so all three tiers (CLI flag, .gitnexusrc, auto-detect via
159
- // sanitizeDetectedBranch) are covered (#1996 tri-review P1).
160
- if (trimmed.includes('`')) {
161
- throw new GitNexusRcError(`${source}: branch name must not contain a backtick (it would break the generated Markdown).`);
135
+ catch (err) {
136
+ if (err instanceof InvalidBranchError) {
137
+ throw new GitNexusRcError(err.message);
138
+ }
139
+ throw err;
162
140
  }
163
- return trimmed;
164
141
  }
165
142
  /**
166
143
  * Best-effort validation for an auto-detected branch (from git). Never throws —
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Optional grammar availability check.
3
3
  *
4
- * tree-sitter-dart, -proto, -swift, and -kotlin are vendored under vendor/ and
5
- * loaded from there by absolute path (NEVER copied into node_modules — see
6
- * core/tree-sitter/vendored-grammars.ts / #2111). Each ships committed platform
7
- * prebuilds activated via node-gyp-build. All can be skipped via
8
- * GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
9
- * soft-fail when no prebuild matches the host platform (and a source build was
10
- * unavailable / not attempted). tree-sitter-zig is the one npm-installed
11
- * optionalDependency in the list; its `probe` overrides the vendored load.
4
+ * tree-sitter-dart, -proto, -swift, -kotlin, and -zig are vendored under
5
+ * vendor/ and loaded from there by absolute path (NEVER copied into
6
+ * node_modules — see core/tree-sitter/vendored-grammars.ts / #2111). Each
7
+ * ships committed platform prebuilds activated via node-gyp-build. All can
8
+ * be skipped via GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or
9
+ * can silently soft-fail when no prebuild matches the host platform (and a
10
+ * source build was unavailable / not attempted).
12
11
  *
13
12
  * Either path produces the same observable: the .node binding is absent
14
13
  * at runtime. This helper detects that condition and surfaces a single
@@ -1,26 +1,23 @@
1
1
  /**
2
2
  * Optional grammar availability check.
3
3
  *
4
- * tree-sitter-dart, -proto, -swift, and -kotlin are vendored under vendor/ and
5
- * loaded from there by absolute path (NEVER copied into node_modules — see
6
- * core/tree-sitter/vendored-grammars.ts / #2111). Each ships committed platform
7
- * prebuilds activated via node-gyp-build. All can be skipped via
8
- * GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
9
- * soft-fail when no prebuild matches the host platform (and a source build was
10
- * unavailable / not attempted). tree-sitter-zig is the one npm-installed
11
- * optionalDependency in the list; its `probe` overrides the vendored load.
4
+ * tree-sitter-dart, -proto, -swift, -kotlin, and -zig are vendored under
5
+ * vendor/ and loaded from there by absolute path (NEVER copied into
6
+ * node_modules — see core/tree-sitter/vendored-grammars.ts / #2111). Each
7
+ * ships committed platform prebuilds activated via node-gyp-build. All can
8
+ * be skipped via GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or
9
+ * can silently soft-fail when no prebuild matches the host platform (and a
10
+ * source build was unavailable / not attempted).
12
11
  *
13
12
  * Either path produces the same observable: the .node binding is absent
14
13
  * at runtime. This helper detects that condition and surfaces a single
15
14
  * stderr line per missing grammar so users learn why .dart/.proto/.swift/.kt/.zig
16
15
  * support is unavailable instead of silently getting a degraded index.
17
16
  */
18
- import { createRequire } from 'node:module';
19
17
  import { SupportedLanguages } from '../_shared/index.js';
20
18
  import { isGrammarRuntimeSkipped } from '../core/tree-sitter/parser-loader.js';
21
19
  import { requireVendoredGrammar } from '../core/tree-sitter/vendored-grammars.js';
22
20
  import { cliWarn } from './cli-message.js';
23
- const _require = createRequire(import.meta.url);
24
21
  const OPTIONAL_GRAMMARS = [
25
22
  {
26
23
  name: 'tree-sitter-dart',
@@ -43,11 +40,9 @@ const OPTIONAL_GRAMMARS = [
43
40
  },
44
41
  {
45
42
  name: 'tree-sitter-zig',
46
- pkg: '@tree-sitter-grammars/tree-sitter-zig',
43
+ pkg: 'tree-sitter-zig',
47
44
  extensions: ['.zig'],
48
45
  language: SupportedLanguages.Zig,
49
- // npm optionalDependency, not vendored — probe via plain require.
50
- probe: () => _require('@tree-sitter-grammars/tree-sitter-zig'),
51
46
  },
52
47
  ];
53
48
  /**
@@ -84,12 +79,7 @@ export function detectMissingOptionalGrammars() {
84
79
  continue;
85
80
  }
86
81
  try {
87
- if (g.probe !== undefined) {
88
- g.probe();
89
- }
90
- else {
91
- requireVendoredGrammar(g.pkg);
92
- }
82
+ requireVendoredGrammar(g.pkg);
93
83
  }
94
84
  catch (err) {
95
85
  const code = err?.code;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Git ref-name validation used by both the CLI and the HTTP analyze route.
3
+ *
4
+ * Lives in `core/` so `server/api.ts` does not import `cli/analyze-config`
5
+ * (that import closed a cli → server → cli cycle: `cli/serve.ts` already
6
+ * imports `createServer`). The CLI keeps a thin wrapper that rethrows
7
+ * {@link InvalidBranchError} as `GitNexusRcError`.
8
+ */
9
+ /**
10
+ * Thrown when a user-supplied branch name fails {@link validateBranchName}.
11
+ * Callers at a product boundary map this to their own error type (CLI:
12
+ * `GitNexusRcError`; HTTP: 400).
13
+ */
14
+ export declare class InvalidBranchError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ /**
18
+ * Validate a user-supplied branch name. Returns the trimmed name or throws
19
+ * {@link InvalidBranchError}. Conservative but accepts the shapes real
20
+ * branches use (`feature/foo-bar`, `release/1.2`, `develop`).
21
+ */
22
+ export declare function validateBranchName(value: string, source: string): string;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Git ref-name validation used by both the CLI and the HTTP analyze route.
3
+ *
4
+ * Lives in `core/` so `server/api.ts` does not import `cli/analyze-config`
5
+ * (that import closed a cli → server → cli cycle: `cli/serve.ts` already
6
+ * imports `createServer`). The CLI keeps a thin wrapper that rethrows
7
+ * {@link InvalidBranchError} as `GitNexusRcError`.
8
+ */
9
+ /** Git refs longer than this are almost certainly a mistake / injection attempt. */
10
+ const BRANCH_MAX_LENGTH = 255;
11
+ /**
12
+ * Thrown when a user-supplied branch name fails {@link validateBranchName}.
13
+ * Callers at a product boundary map this to their own error type (CLI:
14
+ * `GitNexusRcError`; HTTP: 400).
15
+ */
16
+ export class InvalidBranchError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'InvalidBranchError';
20
+ }
21
+ }
22
+ /**
23
+ * Reject control characters and hidden / bidirectional Unicode in a string
24
+ * value. These have no legitimate place in a branch name and would otherwise
25
+ * let a committed config or HTTP body smuggle invisible controls into
26
+ * generated AGENTS.md / CLAUDE.md content.
27
+ */
28
+ const isHiddenOrControl = (codePoint) => codePoint < 0x20 ||
29
+ codePoint === 0x7f ||
30
+ (codePoint >= 0x200b && codePoint <= 0x200f) || // zero-width + LRM/RLM
31
+ (codePoint >= 0x202a && codePoint <= 0x202e) || // bidi embeddings/overrides
32
+ (codePoint >= 0x2060 && codePoint <= 0x2064) || // word-joiner + invisible math
33
+ (codePoint >= 0x2066 && codePoint <= 0x206f) || // bidi isolates + deprecated
34
+ codePoint === 0xfeff; // BOM / zero-width no-break space
35
+ const assertNoHiddenChars = (value, source) => {
36
+ for (const ch of value) {
37
+ const cp = ch.codePointAt(0);
38
+ if (cp !== undefined && isHiddenOrControl(cp)) {
39
+ throw new InvalidBranchError(`${source}: value contains control or hidden/bidirectional characters, which are not allowed.`);
40
+ }
41
+ }
42
+ };
43
+ /**
44
+ * Validate a user-supplied branch name. Returns the trimmed name or throws
45
+ * {@link InvalidBranchError}. Conservative but accepts the shapes real
46
+ * branches use (`feature/foo-bar`, `release/1.2`, `develop`).
47
+ */
48
+ export function validateBranchName(value, source) {
49
+ const trimmed = value.trim();
50
+ if (!trimmed) {
51
+ throw new InvalidBranchError(`${source}: branch name must not be empty.`);
52
+ }
53
+ if (trimmed.length > BRANCH_MAX_LENGTH) {
54
+ throw new InvalidBranchError(`${source}: branch name is too long (max ${BRANCH_MAX_LENGTH}).`);
55
+ }
56
+ assertNoHiddenChars(trimmed, source);
57
+ if (/\s/.test(trimmed)) {
58
+ throw new InvalidBranchError(`${source}: branch name must not contain whitespace.`);
59
+ }
60
+ // git ref-name rules (subset): reject characters git itself forbids in refs.
61
+ if (/[~^:?*[\\]/.test(trimmed)) {
62
+ throw new InvalidBranchError(`${source}: branch name contains characters not allowed in a git ref (~ ^ : ? * [ \\).`);
63
+ }
64
+ if (trimmed.startsWith('-')) {
65
+ throw new InvalidBranchError(`${source}: branch name must not start with "-".`);
66
+ }
67
+ // Force-refspec prefix (`git fetch origin +main` / `+refs/heads/main:…`).
68
+ // Rejected here so neither the CLI nor HTTP can pass a force-update refspec
69
+ // through as a "branch" (#3199 review, defense in depth).
70
+ if (trimmed.startsWith('+')) {
71
+ throw new InvalidBranchError(`${source}: branch name must not start with "+".`);
72
+ }
73
+ // The symbolic ref HEAD (case-sensitive). A repo can have a branch named
74
+ // `head`; git itself treats only `HEAD` as the current-commit alias.
75
+ if (trimmed === 'HEAD') {
76
+ throw new InvalidBranchError(`${source}: branch name must not be "HEAD".`);
77
+ }
78
+ if (trimmed.includes('..')) {
79
+ throw new InvalidBranchError(`${source}: branch name must not contain "..".`);
80
+ }
81
+ // The remaining `git check-ref-format` rules. Without these the validator
82
+ // accepted refs git itself refuses (`feature.lock`, `/feature`, `feature/`,
83
+ // `feature//next`, `@`, `.hidden`), so the failure surfaced later from the
84
+ // git subprocess instead of here. No real branch can violate them — git
85
+ // could not have created one — so nothing that works today starts failing.
86
+ if (trimmed.endsWith('.lock') || trimmed.split('/').some((part) => part.endsWith('.lock'))) {
87
+ throw new InvalidBranchError(`${source}: branch name must not end with ".lock".`);
88
+ }
89
+ if (trimmed.startsWith('/') || trimmed.endsWith('/')) {
90
+ throw new InvalidBranchError(`${source}: branch name must not start or end with "/".`);
91
+ }
92
+ if (trimmed.includes('//')) {
93
+ throw new InvalidBranchError(`${source}: branch name must not contain consecutive slashes.`);
94
+ }
95
+ if (trimmed === '@') {
96
+ throw new InvalidBranchError(`${source}: branch name must not be the single character "@".`);
97
+ }
98
+ if (trimmed.includes('@{')) {
99
+ throw new InvalidBranchError(`${source}: branch name must not contain "@{".`);
100
+ }
101
+ if (trimmed.endsWith('.') || trimmed.split('/').some((part) => part.startsWith('.'))) {
102
+ throw new InvalidBranchError(`${source}: branch name must not end with "." or have a path component starting with ".".`);
103
+ }
104
+ // Git permits a backtick in a ref, but the branch is embedded inside a
105
+ // Markdown inline-code span in the generated AGENTS.md/CLAUDE.md regression
106
+ // example, where a backtick would close the span early and let the rest of
107
+ // the template render as instruction text. Reject it at this single
108
+ // chokepoint so all three tiers (CLI flag, .gitnexusrc, auto-detect via
109
+ // sanitizeDetectedBranch) are covered (#1996 tri-review P1).
110
+ if (trimmed.includes('`')) {
111
+ throw new InvalidBranchError(`${source}: branch name must not contain a backtick (it would break the generated Markdown).`);
112
+ }
113
+ return trimmed;
114
+ }
@@ -11,17 +11,14 @@
11
11
  * Conservative by design: we only tag an edge when we can prove the
12
12
  * gating expression evaluates to `false`. Anything ambiguous → live.
13
13
  *
14
- * Scope of v1:
14
+ * Supported scope:
15
15
  *
16
16
  * (a) **File-local** consts (`pub const FOO = false;`, plus const-to-const
17
17
  * aliases up to 5 hops), built once per file by `buildZigBoolConstMap`.
18
- * (b) **Cross-file** (`const cfg = @import("./cfg.zig"); if (cfg.FOO)`) is
19
- * NOT resolved yet. The evaluator keeps the seam for it (`importAliases`
20
- * + `lookupBoolsForPath`, consumed by the `field_expression` case), but
21
- * the only caller passes an empty alias map and a lookup that always
22
- * returns `undefined`, because the capture emitter runs in the parse
23
- * worker and sees only the current file. Tracked in #3162. Until then
24
- * every `cfg.FOO` condition folds to unknown, i.e. live.
18
+ * (b) **Cross-file** direct imports (`const cfg = @import("./cfg.zig");
19
+ * if (cfg.FOO)`) are enriched after per-file extraction. The workspace
20
+ * caller supplies `importAliases` and `lookupBoolsForPath`; the parse
21
+ * worker still uses empty/undefined inputs and remains file-local.
25
22
  *
26
23
  * Also out of scope: multi-hop member access (`cfg.sub.FOO`), re-exported
27
24
  * consts, runtime-evaluated bools (`const FOO = computeIt();`), and
@@ -1,14 +1,13 @@
1
1
  import Parser from 'tree-sitter';
2
- import { createRequire } from 'node:module';
3
- const _require = createRequire(import.meta.url);
2
+ import { requireVendoredGrammar } from '../../../tree-sitter/vendored-grammars.js';
4
3
  /**
5
4
  * Zig scope-resolution query (RFC #909 Ring 3).
6
5
  *
7
- * The grammar is an optionalDependency (`@tree-sitter-grammars/tree-sitter-zig`),
8
- * so the language module is required lazily and `getZigParser` /
9
- * `getZigScopeQuery` throw only when actually invoked without the grammar
10
- * installed. That is safe: the parse pipeline filters `.zig` files through
11
- * `parser-loader.isLanguageAvailable` before any scope extraction runs.
6
+ * The grammar is vendored (`vendor/tree-sitter-zig`) and may be absent on a
7
+ * platform without a prebuild, so the language module is required lazily and
8
+ * `getZigParser` / `getZigScopeQuery` throw only when actually invoked without
9
+ * the grammar installed. That is safe: the parse pipeline filters `.zig` files
10
+ * through `parser-loader.isLanguageAvailable` before any scope extraction runs.
12
11
  *
13
12
  * Zig specifics encoded here:
14
13
  * - Containers (struct/enum/union/opaque) are anonymous nodes bound by the
@@ -409,7 +408,7 @@ const ZIG_SCOPE_QUERY = `
409
408
  let _parser = null;
410
409
  let _query = null;
411
410
  function getZigLanguage() {
412
- return _require('@tree-sitter-grammars/tree-sitter-zig');
411
+ return requireVendoredGrammar('tree-sitter-zig');
413
412
  }
414
413
  export function getZigParser() {
415
414
  if (_parser === null) {
@@ -16,6 +16,7 @@ import { resolveZigImportInternal } from '../../import-resolvers/zig.js';
16
16
  import { zigProvider } from '../zig.js';
17
17
  import { expandZigWildcardNames, zigArityCompatibility, zigMergeBindings } from './index.js';
18
18
  import { populateZigRangeBindings } from './range-binding.js';
19
+ import { populateZigWorkspaceStaticGating } from './workspace-static-gating.js';
19
20
  export const zigScopeResolver = {
20
21
  language: SupportedLanguages.Zig,
21
22
  languageProvider: zigProvider,
@@ -48,6 +49,7 @@ export const zigScopeResolver = {
48
49
  arityCompatibility: zigArityCompatibility,
49
50
  buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
50
51
  populateOwners: (parsed) => populateClassOwnedMembers(parsed),
52
+ populateWorkspaceReferences: populateZigWorkspaceStaticGating,
51
53
  // Payload captures — `for (items) |it|`, `if (opt) |v|`, `while (it.next())
52
54
  // |x|` — typed from the subject's binding after finalize (F6).
53
55
  populateRangeBindings: populateZigRangeBindings,
@@ -0,0 +1,8 @@
1
+ import type { ParsedFile } from '../../../../_shared/index.js';
2
+ export declare function populateZigWorkspaceStaticGating(parsedFiles: ParsedFile[], ctx: {
3
+ readonly fileContents: ReadonlyMap<string, string>;
4
+ readonly treeCache?: {
5
+ get(filePath: string): unknown;
6
+ };
7
+ readonly resolutionConfig?: unknown;
8
+ }): void;
@@ -0,0 +1,76 @@
1
+ import { getTreeSitterBufferSize } from '../../constants.js';
2
+ import { resolveZigImportInternal } from '../../import-resolvers/zig.js';
3
+ import { buildZigBoolConstMap, collectZigStaticGatedRanges, isPositionStaticGated, } from '../../call-extractors/zig-static-gating.js';
4
+ import { parseSourceSafe, ParseTimeoutError } from '../../../tree-sitter/safe-parse.js';
5
+ import { getZigParser } from './query.js';
6
+ export function populateZigWorkspaceStaticGating(parsedFiles, ctx) {
7
+ const parser = getZigParser();
8
+ const trees = new Map();
9
+ const bools = new Map();
10
+ for (const parsed of parsedFiles) {
11
+ const source = ctx.fileContents.get(parsed.filePath);
12
+ if (source === undefined)
13
+ continue;
14
+ let tree = ctx.treeCache?.get(parsed.filePath);
15
+ if (tree === undefined) {
16
+ try {
17
+ tree = parseSourceSafe(parser, source, undefined, {
18
+ bufferSize: getTreeSitterBufferSize(source),
19
+ });
20
+ }
21
+ catch (err) {
22
+ if (err instanceof ParseTimeoutError)
23
+ continue;
24
+ throw err;
25
+ }
26
+ }
27
+ trees.set(parsed.filePath, tree);
28
+ bools.set(parsed.filePath, buildZigBoolConstMap(tree.rootNode));
29
+ }
30
+ const knownPaths = new Set(trees.keys());
31
+ for (const [index, parsed] of parsedFiles.entries()) {
32
+ const tree = trees.get(parsed.filePath);
33
+ if (tree === undefined)
34
+ continue;
35
+ const aliases = collectImportAliases(tree, parsed.filePath, knownPaths, ctx.resolutionConfig);
36
+ if (aliases.size === 0)
37
+ continue;
38
+ const ranges = collectZigStaticGatedRanges(tree.rootNode, bools.get(parsed.filePath) ?? new Map(), aliases, (filePath) => bools.get(filePath));
39
+ if (ranges.length === 0)
40
+ continue;
41
+ const next = parsed.referenceSites.map((site) => site.kind === 'call' &&
42
+ site.staticGated !== true &&
43
+ isPositionStaticGated(site.atRange.startLine, site.atRange.startCol, ranges)
44
+ ? { ...site, staticGated: true }
45
+ : site);
46
+ parsedFiles[index] = Object.freeze({ ...parsed, referenceSites: Object.freeze(next) });
47
+ }
48
+ }
49
+ function collectImportAliases(tree, fromFile, knownPaths, resolutionConfig) {
50
+ const candidates = new Map();
51
+ const declarationCounts = new Map();
52
+ for (const decl of tree.rootNode.descendantsOfType('variable_declaration')) {
53
+ const names = decl.namedChildren.filter((node) => node.type === 'identifier');
54
+ const binding = names[0]?.text;
55
+ if (binding === undefined)
56
+ continue;
57
+ declarationCounts.set(binding, (declarationCounts.get(binding) ?? 0) + 1);
58
+ const builtin = decl.namedChildren.find((node) => node.type === 'builtin_function' && node.text.startsWith('@import('));
59
+ const raw = builtin?.descendantsOfType('string').at(0)?.text;
60
+ if (raw === undefined)
61
+ continue;
62
+ const specifier = raw.replace(/^['"]|['"]$/g, '');
63
+ const target = resolveZigImportInternal(fromFile, specifier, knownPaths, resolutionConfig);
64
+ if (target !== null)
65
+ candidates.set(binding, target);
66
+ }
67
+ const aliases = new Map();
68
+ for (const [binding, target] of candidates) {
69
+ // Alias lookup below is name-based rather than position-aware. If a name
70
+ // is redeclared in another lexical scope, fail open instead of applying
71
+ // either module's constants to every use of that spelling.
72
+ if (declarationCounts.get(binding) === 1)
73
+ aliases.set(binding, target);
74
+ }
75
+ return aliases;
76
+ }
@@ -3,6 +3,7 @@ import type { SymbolTableWriter } from './model/index.js';
3
3
  import { type ExportedTypeMap } from './call-processor.js';
4
4
  import type { ParsedFile } from '../../_shared/index.js';
5
5
  import { WorkerPool } from './workers/worker-pool.js';
6
+ import type { DispatchGroup } from './workers/worker-pool.js';
6
7
  import type { ParseWorkerResult, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedModuleConstants, ExtractedToolDef, FileScopeBindings, ExtractedORMQuery, FetchWrapperDef } from './workers/parse-worker.js';
7
8
  import type { ExtractedRouterConstructorPrefix, ExtractedRouterImport, ExtractedRouterInclude, ExtractedRouterModuleAlias } from './route-extractors/fastapi-router-bindings.js';
8
9
  import type { SharedSpringType } from './route-extractors/spring-shared.js';
@@ -47,12 +48,12 @@ export interface WorkerExtractedData {
47
48
  */
48
49
  export declare const mergeChunkResults: (graph: KnowledgeGraph, symbolTable: SymbolTableWriter, chunkResults: readonly ParseWorkerResult[], exportedTypeMap?: ExportedTypeMap) => WorkerExtractedData;
49
50
  /**
50
- * Dispatch a chunk's files to the worker pool and return the RAW per-worker
51
- * results, WITHOUT merging them into the graph. Split out from
52
- * {@link processParsing} so the parse loop can overlap one chunk's
53
- * merge (main-thread, via {@link mergeChunkResults}) with the NEXT chunk's
54
- * worker parse the merge is the only remaining serial main-thread step once
55
- * ParsedFile serialization moved into the workers (#worker-idle pipelining).
51
+ * Dispatch ONE chunk's files to the worker pool and return the RAW per-worker
52
+ * results, WITHOUT merging them into the graph. A thin single-group wrapper
53
+ * over {@link dispatchChunkParseRound}, used by {@link processParsing}'s
54
+ * one-shot path. The chunk-to-chunk overlap this once described now lives in
55
+ * `parse-impl.ts` at ROUND granularity (`startRound` / `drainRound` /
56
+ * `closeRound`), which batches several chunks into one dispatch.
56
57
  * Returns `[]` for an all-unparseable chunk (the caller merges `[]` → empty).
57
58
  */
58
59
  export declare const dispatchChunkParse: (files: {
@@ -67,6 +68,21 @@ outRawResults?: ParseWorkerResult[],
67
68
  * (#2038). `undefined` ⇒ no durable write (tests / no-cache path).
68
69
  */
69
70
  chunkHash?: string) => Promise<ParseWorkerResult[]>;
71
+ /**
72
+ * Dispatch SEVERAL parse-cache chunks as one pool round and return their raw
73
+ * results, one array per input group in input order.
74
+ *
75
+ * `WorkerPool.dispatch` is a barrier, so one round-trip per chunk leaves most
76
+ * slots idle whenever a chunk is smaller than the pool — which stable
77
+ * `(language, hash(path) % 128)` packs usually are. Batching chunks into one
78
+ * `dispatchGroups` call removes those barriers; jobs are still cut at chunk
79
+ * boundaries, so every result stays attributable to the chunk whose cache key
80
+ * owns it.
81
+ */
82
+ export declare const dispatchChunkParseRound: (groups: ReadonlyArray<DispatchGroup<{
83
+ path: string;
84
+ content: string;
85
+ }>>, workerPool: WorkerPool, onFileProgress?: FileProgressCallback) => Promise<ParseWorkerResult[][]>;
70
86
  export declare const processParsing: (graph: KnowledgeGraph, files: {
71
87
  path: string;
72
88
  content: string;
@@ -147,12 +147,12 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
147
147
  };
148
148
  };
149
149
  /**
150
- * Dispatch a chunk's files to the worker pool and return the RAW per-worker
151
- * results, WITHOUT merging them into the graph. Split out from
152
- * {@link processParsing} so the parse loop can overlap one chunk's
153
- * merge (main-thread, via {@link mergeChunkResults}) with the NEXT chunk's
154
- * worker parse the merge is the only remaining serial main-thread step once
155
- * ParsedFile serialization moved into the workers (#worker-idle pipelining).
150
+ * Dispatch ONE chunk's files to the worker pool and return the RAW per-worker
151
+ * results, WITHOUT merging them into the graph. A thin single-group wrapper
152
+ * over {@link dispatchChunkParseRound}, used by {@link processParsing}'s
153
+ * one-shot path. The chunk-to-chunk overlap this once described now lives in
154
+ * `parse-impl.ts` at ROUND granularity (`startRound` / `drainRound` /
155
+ * `closeRound`), which batches several chunks into one dispatch.
156
156
  * Returns `[]` for an all-unparseable chunk (the caller merges `[]` → empty).
157
157
  */
158
158
  export const dispatchChunkParse = async (files, workerPool, onFileProgress,
@@ -164,23 +164,42 @@ outRawResults,
164
164
  * (#2038). `undefined` ⇒ no durable write (tests / no-cache path).
165
165
  */
166
166
  chunkHash) => {
167
- const parseableFiles = [];
168
- for (const file of files) {
169
- const lang = getLanguageFromFilename(file.path);
170
- if (lang)
171
- parseableFiles.push({ path: file.path, content: file.content });
172
- }
173
- if (parseableFiles.length === 0)
174
- return [];
175
- const total = files.length;
176
- const chunkResults = await workerPool.dispatch(parseableFiles, (filesProcessed) => {
177
- onFileProgress?.(Math.min(filesProcessed, total), total, 'Parsing...');
178
- }, chunkHash);
167
+ const [chunkResults = []] = await dispatchChunkParseRound([{ items: files, chunkHash }], workerPool, onFileProgress);
179
168
  // Capture raw results for the incremental parse cache before merging.
180
169
  if (outRawResults) {
181
170
  for (const r of chunkResults)
182
171
  outRawResults.push(r);
183
172
  }
173
+ return chunkResults;
174
+ };
175
+ /**
176
+ * Dispatch SEVERAL parse-cache chunks as one pool round and return their raw
177
+ * results, one array per input group in input order.
178
+ *
179
+ * `WorkerPool.dispatch` is a barrier, so one round-trip per chunk leaves most
180
+ * slots idle whenever a chunk is smaller than the pool — which stable
181
+ * `(language, hash(path) % 128)` packs usually are. Batching chunks into one
182
+ * `dispatchGroups` call removes those barriers; jobs are still cut at chunk
183
+ * boundaries, so every result stays attributable to the chunk whose cache key
184
+ * owns it.
185
+ */
186
+ export const dispatchChunkParseRound = async (groups, workerPool, onFileProgress) => {
187
+ const dispatchGroups = groups.map((group) => {
188
+ const items = [];
189
+ for (const file of group.items) {
190
+ const lang = getLanguageFromFilename(file.path);
191
+ if (lang)
192
+ items.push({ path: file.path, content: file.content });
193
+ }
194
+ return { items, chunkHash: group.chunkHash };
195
+ });
196
+ const total = groups.reduce((sum, group) => sum + group.items.length, 0);
197
+ if (dispatchGroups.every((group) => group.items.length === 0))
198
+ return groups.map(() => []);
199
+ const perGroup = await workerPool.dispatchGroups(dispatchGroups, (filesProcessed) => {
200
+ onFileProgress?.(Math.min(filesProcessed, total), total, 'Parsing...');
201
+ });
202
+ const chunkResults = perGroup.flat();
184
203
  // Skipped-language telemetry (worker output, independent of the merge).
185
204
  const skippedLanguages = new Map();
186
205
  for (const result of chunkResults) {
@@ -238,7 +257,7 @@ chunkHash) => {
238
257
  logger.warn(` Sanitized ${skippedPaths.length} file(s) with non-serializable parse output: ${shown}${more}`);
239
258
  }
240
259
  onFileProgress?.(total, total, 'done');
241
- return chunkResults;
260
+ return perGroup;
242
261
  };
243
262
  // ============================================================================
244
263
  // Public API