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.
- package/README.md +1 -1
- package/dist/cli/analyze-config.d.ts +2 -2
- package/dist/cli/analyze-config.js +10 -33
- package/dist/cli/optional-grammars.d.ts +7 -8
- package/dist/cli/optional-grammars.js +9 -19
- package/dist/core/git-ref.d.ts +22 -0
- package/dist/core/git-ref.js +114 -0
- package/dist/core/ingestion/call-extractors/zig-static-gating.d.ts +5 -8
- package/dist/core/ingestion/languages/zig/query.js +7 -8
- package/dist/core/ingestion/languages/zig/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/zig/workspace-static-gating.d.ts +8 -0
- package/dist/core/ingestion/languages/zig/workspace-static-gating.js +76 -0
- package/dist/core/ingestion/parsing-processor.d.ts +22 -6
- package/dist/core/ingestion/parsing-processor.js +38 -19
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +291 -93
- package/dist/core/ingestion/pipeline-phases/parse-round-budget.d.ts +42 -0
- package/dist/core/ingestion/pipeline-phases/parse-round-budget.js +50 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +13 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +1 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +5 -0
- package/dist/core/ingestion/workers/parse-worker.js +14 -6
- package/dist/core/ingestion/workers/worker-pool.d.ts +56 -2
- package/dist/core/ingestion/workers/worker-pool.js +117 -26
- package/dist/core/search/fts-indexes.js +13 -4
- package/dist/core/tree-sitter/parser-loader.js +4 -12
- package/dist/core/tree-sitter/vendored-grammars.d.ts +1 -1
- package/dist/core/tree-sitter/vendored-grammars.js +2 -1
- package/dist/mcp/local/local-backend.js +9 -3
- package/dist/server/analyze-job.d.ts +19 -1
- package/dist/server/analyze-job.js +14 -3
- package/dist/server/analyze-launch.d.ts +14 -0
- package/dist/server/analyze-launch.js +59 -14
- package/dist/server/analyze-worker-ipc.d.ts +6 -5
- package/dist/server/analyze-worker-ipc.js +4 -0
- package/dist/server/api.js +53 -6
- package/dist/server/git-clone.d.ts +38 -5
- package/dist/server/git-clone.js +216 -38
- package/dist/storage/parsedfile-store.js +53 -1
- package/dist/storage/v8-sidecar.d.ts +11 -0
- package/dist/storage/v8-sidecar.js +15 -7
- package/package.json +1 -5
- package/scripts/build-tree-sitter-grammars.cjs +2 -1
- package/vendor/tree-sitter-zig/LICENSE +21 -0
- package/vendor/tree-sitter-zig/README.md +25 -0
- package/vendor/tree-sitter-zig/binding.gyp +29 -0
- package/vendor/tree-sitter-zig/bindings/node/binding.cc +20 -0
- package/vendor/tree-sitter-zig/bindings/node/index.d.ts +28 -0
- package/vendor/tree-sitter-zig/bindings/node/index.js +11 -0
- package/vendor/tree-sitter-zig/grammar.js +900 -0
- package/vendor/tree-sitter-zig/package.json +18 -0
- package/vendor/tree-sitter-zig/prebuilds/SHA256SUMS +6 -0
- package/vendor/tree-sitter-zig/prebuilds/darwin-arm64/tree-sitter-zig.node +0 -0
- package/vendor/tree-sitter-zig/prebuilds/darwin-x64/tree-sitter-zig.node +0 -0
- package/vendor/tree-sitter-zig/prebuilds/linux-arm64/tree-sitter-zig.node +0 -0
- package/vendor/tree-sitter-zig/prebuilds/linux-x64/tree-sitter-zig.node +0 -0
- package/vendor/tree-sitter-zig/prebuilds/win32-arm64/tree-sitter-zig.node +0 -0
- package/vendor/tree-sitter-zig/prebuilds/win32-x64/tree-sitter-zig.node +0 -0
- package/vendor/tree-sitter-zig/src/grammar.json +6570 -0
- package/vendor/tree-sitter-zig/src/node-types.json +3178 -0
- package/vendor/tree-sitter-zig/src/parser.c +169829 -0
- package/vendor/tree-sitter-zig/src/tree_sitter/alloc.h +54 -0
- package/vendor/tree-sitter-zig/src/tree_sitter/array.h +291 -0
- 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)
|
|
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}.
|
|
45
|
-
*
|
|
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}.
|
|
130
|
-
*
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 -
|
|
5
|
-
* loaded from there by absolute path (NEVER copied into
|
|
6
|
-
* core/tree-sitter/vendored-grammars.ts / #2111). Each
|
|
7
|
-
* prebuilds activated via node-gyp-build. All can
|
|
8
|
-
* GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or
|
|
9
|
-
* soft-fail when no prebuild matches the host platform (and a
|
|
10
|
-
* unavailable / not attempted).
|
|
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 -
|
|
5
|
-
* loaded from there by absolute path (NEVER copied into
|
|
6
|
-
* core/tree-sitter/vendored-grammars.ts / #2111). Each
|
|
7
|
-
* prebuilds activated via node-gyp-build. All can
|
|
8
|
-
* GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or
|
|
9
|
-
* soft-fail when no prebuild matches the host platform (and a
|
|
10
|
-
* unavailable / not attempted).
|
|
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: '
|
|
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
|
-
|
|
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
|
-
*
|
|
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");
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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 {
|
|
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
|
|
8
|
-
* so the language module is required lazily and
|
|
9
|
-
* `getZigScopeQuery` throw only when actually invoked without
|
|
10
|
-
* installed. That is safe: the parse pipeline filters `.zig` files
|
|
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
|
|
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
|
|
51
|
-
* results, WITHOUT merging them into the graph.
|
|
52
|
-
* {@link
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
151
|
-
* results, WITHOUT merging them into the graph.
|
|
152
|
-
* {@link
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
|
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
|
|
260
|
+
return perGroup;
|
|
242
261
|
};
|
|
243
262
|
// ============================================================================
|
|
244
263
|
// Public API
|