gitnexus 1.6.7 → 1.6.8-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 (82) hide show
  1. package/README.md +20 -0
  2. package/dist/cli/analyze-config.js +39 -0
  3. package/dist/cli/analyze.d.ts +16 -0
  4. package/dist/cli/analyze.js +34 -6
  5. package/dist/cli/clean.d.ts +1 -0
  6. package/dist/cli/clean.js +43 -1
  7. package/dist/cli/eval-server.js +43 -0
  8. package/dist/cli/help-i18n.js +6 -0
  9. package/dist/cli/i18n/en.d.ts +11 -0
  10. package/dist/cli/i18n/en.js +11 -0
  11. package/dist/cli/i18n/resources.d.ts +22 -0
  12. package/dist/cli/i18n/zh-CN.d.ts +11 -0
  13. package/dist/cli/i18n/zh-CN.js +11 -0
  14. package/dist/cli/index.js +9 -0
  15. package/dist/cli/list.js +12 -0
  16. package/dist/cli/optional-grammars.d.ts +6 -8
  17. package/dist/cli/optional-grammars.js +8 -11
  18. package/dist/cli/status.js +26 -5
  19. package/dist/cli/tool.d.ts +5 -0
  20. package/dist/cli/tool.js +5 -0
  21. package/dist/core/embeddings/embedder.js +4 -0
  22. package/dist/core/embeddings/embedding-pipeline.js +27 -16
  23. package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
  24. package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
  25. package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
  26. package/dist/core/group/extractors/http-patterns/java.js +3 -50
  27. package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
  28. package/dist/core/group/extractors/include-extractor.js +7 -7
  29. package/dist/core/ingestion/language-provider.d.ts +25 -2
  30. package/dist/core/ingestion/languages/c-cpp.js +11 -4
  31. package/dist/core/ingestion/languages/java.js +3 -0
  32. package/dist/core/ingestion/languages/kotlin/query.js +3 -2
  33. package/dist/core/ingestion/languages/kotlin.js +5 -1
  34. package/dist/core/ingestion/parsing-processor.js +21 -0
  35. package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
  36. package/dist/core/ingestion/pipeline.d.ts +9 -0
  37. package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
  38. package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
  39. package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
  40. package/dist/core/ingestion/route-extractors/spring.js +136 -0
  41. package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
  42. package/dist/core/ingestion/workers/clone-safety.js +465 -0
  43. package/dist/core/ingestion/workers/parse-worker.d.ts +10 -0
  44. package/dist/core/ingestion/workers/parse-worker.js +27 -52
  45. package/dist/core/ingestion/workers/post-result.d.ts +22 -0
  46. package/dist/core/ingestion/workers/post-result.js +87 -0
  47. package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
  48. package/dist/core/ingestion/workers/result-merge.js +43 -0
  49. package/dist/core/ingestion/workers/worker-pool.d.ts +6 -0
  50. package/dist/core/ingestion/workers/worker-pool.js +28 -14
  51. package/dist/core/lbug/lbug-adapter.d.ts +19 -0
  52. package/dist/core/lbug/lbug-adapter.js +56 -1
  53. package/dist/core/run-analyze.d.ts +45 -0
  54. package/dist/core/run-analyze.js +175 -24
  55. package/dist/core/tree-sitter/parser-loader.js +5 -4
  56. package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
  57. package/dist/core/tree-sitter/vendored-grammars.js +57 -0
  58. package/dist/mcp/core/embedder.js +4 -0
  59. package/dist/mcp/local/local-backend.d.ts +55 -2
  60. package/dist/mcp/local/local-backend.js +363 -25
  61. package/dist/mcp/tools.js +32 -0
  62. package/dist/server/analyze-worker-ipc.d.ts +58 -0
  63. package/dist/server/analyze-worker-ipc.js +16 -0
  64. package/dist/server/analyze-worker.js +7 -1
  65. package/dist/storage/branch-index.d.ts +52 -0
  66. package/dist/storage/branch-index.js +65 -0
  67. package/dist/storage/git.d.ts +11 -0
  68. package/dist/storage/git.js +28 -0
  69. package/dist/storage/parse-cache.js +3 -0
  70. package/dist/storage/repo-manager.d.ts +57 -2
  71. package/dist/storage/repo-manager.js +132 -18
  72. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +24 -4
  73. package/hooks/claude/gitnexus-hook.cjs +18 -3
  74. package/package.json +2 -2
  75. package/scripts/assert-publish-grammar-coverage.cjs +31 -0
  76. package/scripts/build-tree-sitter-grammars.cjs +16 -10
  77. package/vendor/tree-sitter-c/package.json +1 -1
  78. package/vendor/tree-sitter-dart/package.json +1 -1
  79. package/vendor/tree-sitter-kotlin/package.json +1 -1
  80. package/vendor/tree-sitter-proto/package.json +1 -1
  81. package/vendor/tree-sitter-swift/package.json +1 -1
  82. package/scripts/materialize-vendor-grammars.cjs +0 -97
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Structured-clone safety for the worker result boundary (#2112).
3
+ *
4
+ * A parse worker delivers its accumulated result to the main thread via
5
+ * `parentPort.postMessage(...)`. Node serializes that payload with the
6
+ * structured-clone algorithm SYNCHRONOUSLY on the worker thread, and it
7
+ * THROWS a `DataCloneError` the instant it meets a value it can't serialize —
8
+ * a function, a symbol, a Promise, a WeakMap, etc. The reporter of #2112 hit
9
+ * exactly this: a node record whose `properties` carried an own-enumerable
10
+ * value pointing at a native function (`function toString() { [native code] }
11
+ * could not be cloned`). One such value aborted the entire parse phase,
12
+ * because the worker re-posts the throw as `{type:'error'}` which the pool
13
+ * counts as a worker death — and under `GITNEXUS_WORKER_POOL_SIZE=1` the same
14
+ * graph re-throws on every respawn until the slot's budget is exhausted.
15
+ *
16
+ * This module is the safety net. It runs ONLY after a real clone failure on
17
+ * the fast-path post (zero overhead on healthy runs), and rewrites the
18
+ * boundary-crossing arrays so the result becomes cloneable: a non-cloneable
19
+ * value inside a plain extraction record is dropped (the record is otherwise
20
+ * kept — strictly-missing data, never wrong), and a `ParsedFile` that can't be
21
+ * made cloneable is dropped whole so scope-resolution re-derives it on the
22
+ * main thread (where there is no clone boundary) with intact edge data.
23
+ *
24
+ * Language-neutral by construction: it keys on value shape and field name
25
+ * only, never on a language (AGENTS.md shared-pipeline rule). The strip
26
+ * semantics mirror what the store path's `JSON.stringify` already silently
27
+ * drops, so store / no-store / cold / warm runs converge on the same graph.
28
+ */
29
+ /** A file whose parse result was sanitized or dropped at the clone boundary. */
30
+ export interface SkippedPath {
31
+ /** Best-effort source path of the offending record (or `(unknown)`). */
32
+ path: string;
33
+ /** Human-readable reason, e.g. "dropped 1 non-serializable value from nodes". */
34
+ reason: string;
35
+ }
36
+ /**
37
+ * True iff `value` survives Node's structured-clone algorithm (the same
38
+ * algorithm `postMessage` uses). This is the authoritative probe — it matches
39
+ * the real failure exactly, including Map/Set/Date/RegExp/TypedArray support,
40
+ * so it never false-positives on the `Scope` Maps that clone fine.
41
+ */
42
+ export declare function isStructuredCloneable(value: unknown): boolean;
43
+ /** The leaf values the structured-clone algorithm copies verbatim. */
44
+ type CloneablePrimitive = undefined | null | boolean | number | bigint | string;
45
+ /**
46
+ * Maps `T` to itself when every value reachable from it is structured-clone
47
+ * safe, and to a type containing `never` at the first offending property
48
+ * otherwise. A function or symbol — the values `postMessage` rejects — becomes
49
+ * `never`, so a struct carrying one is no longer assignable to its own
50
+ * `Cloneable<T>` and `assertCloneable` rejects it, naming the bad key.
51
+ *
52
+ * Implemented as a homomorphic mapped type (`{ [K in keyof T]: … }`) so it
53
+ * preserves `interface` shapes and `readonly` modifiers and works WITHOUT
54
+ * requiring the payload types to carry an index signature — sidestepping the
55
+ * "closed interface is not assignable to a recursive index-signature type" wall
56
+ * that blocked the value-typed-`Cloneable` approach (#2143). `Map`/`Set`/array
57
+ * containers recurse into their element types; `Date`/`RegExp` are clone-safe
58
+ * leaves.
59
+ */
60
+ /** True iff `T` is `any` (the canonical `IsAny` probe: only `any` satisfies `0 extends 1 & T`). */
61
+ type IsAny<T> = 0 extends 1 & T ? true : false;
62
+ export type Cloneable<T> = IsAny<T> extends true ? never : T extends CloneablePrimitive | Date | RegExp ? T : T extends (...args: never[]) => unknown ? never : T extends symbol ? never : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<Cloneable<K>, Cloneable<V>> : T extends ReadonlySet<infer U> ? ReadonlySet<Cloneable<U>> : T extends readonly (infer U)[] ? T extends unknown[] ? Cloneable<U>[] : readonly Cloneable<U>[] : T extends object ? {
63
+ [K in keyof T]: Cloneable<T[K]>;
64
+ } : never;
65
+ /**
66
+ * Identity at runtime (zero cost — returns its argument unchanged); a
67
+ * compile-time assertion that `value` is structured-clone safe. Wrap a
68
+ * producer that feeds an `unknown` worker-result sink:
69
+ *
70
+ * collectCaptureSideChannel: (filePath) => assertCloneable(collectFoo(filePath))
71
+ *
72
+ * If `collectFoo`'s return type ever gains a non-cloneable member (a function, a
73
+ * `SyntaxNode`, …) the call fails to compile, pointing at the offending key.
74
+ *
75
+ * The parameter is a conditional type rather than an `extends Cloneable<T>`
76
+ * constraint because a self-referential constraint (`T extends Cloneable<T>`)
77
+ * is a "circular constraint" error in TypeScript. For a clone-safe `T` the
78
+ * parameter resolves to `T` (call type-checks as a plain identity); for an
79
+ * unsafe `T` it resolves to `Cloneable<T>` (which has `never` at the bad key),
80
+ * so the argument is rejected.
81
+ */
82
+ export declare function assertCloneable<T>(value: T extends Cloneable<T> ? T : Cloneable<T>): T;
83
+ export interface MakeCloneSafeOptions {
84
+ /**
85
+ * Array field names whose offending elements are DROPPED whole rather than
86
+ * stripped in place (e.g. `parsedFiles` — its `captureSideChannel` drives
87
+ * edge resolution, so a stripped-and-delivered file would ship WRONG edges;
88
+ * dropping it lets scope-resolution re-derive it on the main thread).
89
+ */
90
+ dropWholeElement: ReadonlySet<string>;
91
+ /** Field names to skip entirely (e.g. the `skippedPaths` field itself). */
92
+ skipFields?: ReadonlySet<string>;
93
+ /** Keys to probe for a file path when attributing a skip. */
94
+ pathKeys?: readonly string[];
95
+ }
96
+ /**
97
+ * Make a worker result's boundary-crossing array fields structured-cloneable,
98
+ * mutating `result` in place. Only arrays that actually contain a
99
+ * non-cloneable value are rewritten; everything else keeps referential
100
+ * identity. Returns the list of affected file paths for reporting.
101
+ *
102
+ * Call this after ANY failure of the fast-path post — a `DataCloneError`, OR a
103
+ * throwing getter's own error surfaced by structuredClone (the caller in
104
+ * `post-result.ts` recovers on any throw, not only `DataCloneError`).
105
+ */
106
+ export declare function makeWorkerResultCloneSafe(result: Record<string, unknown>, options: MakeCloneSafeOptions): {
107
+ skipped: SkippedPath[];
108
+ };
109
+ export {};
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Structured-clone safety for the worker result boundary (#2112).
3
+ *
4
+ * A parse worker delivers its accumulated result to the main thread via
5
+ * `parentPort.postMessage(...)`. Node serializes that payload with the
6
+ * structured-clone algorithm SYNCHRONOUSLY on the worker thread, and it
7
+ * THROWS a `DataCloneError` the instant it meets a value it can't serialize —
8
+ * a function, a symbol, a Promise, a WeakMap, etc. The reporter of #2112 hit
9
+ * exactly this: a node record whose `properties` carried an own-enumerable
10
+ * value pointing at a native function (`function toString() { [native code] }
11
+ * could not be cloned`). One such value aborted the entire parse phase,
12
+ * because the worker re-posts the throw as `{type:'error'}` which the pool
13
+ * counts as a worker death — and under `GITNEXUS_WORKER_POOL_SIZE=1` the same
14
+ * graph re-throws on every respawn until the slot's budget is exhausted.
15
+ *
16
+ * This module is the safety net. It runs ONLY after a real clone failure on
17
+ * the fast-path post (zero overhead on healthy runs), and rewrites the
18
+ * boundary-crossing arrays so the result becomes cloneable: a non-cloneable
19
+ * value inside a plain extraction record is dropped (the record is otherwise
20
+ * kept — strictly-missing data, never wrong), and a `ParsedFile` that can't be
21
+ * made cloneable is dropped whole so scope-resolution re-derives it on the
22
+ * main thread (where there is no clone boundary) with intact edge data.
23
+ *
24
+ * Language-neutral by construction: it keys on value shape and field name
25
+ * only, never on a language (AGENTS.md shared-pipeline rule). The strip
26
+ * semantics mirror what the store path's `JSON.stringify` already silently
27
+ * drops, so store / no-store / cold / warm runs converge on the same graph.
28
+ */
29
+ /**
30
+ * True iff `value` survives Node's structured-clone algorithm (the same
31
+ * algorithm `postMessage` uses). This is the authoritative probe — it matches
32
+ * the real failure exactly, including Map/Set/Date/RegExp/TypedArray support,
33
+ * so it never false-positives on the `Scope` Maps that clone fine.
34
+ */
35
+ export function isStructuredCloneable(value) {
36
+ try {
37
+ structuredClone(value);
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /**
45
+ * Identity at runtime (zero cost — returns its argument unchanged); a
46
+ * compile-time assertion that `value` is structured-clone safe. Wrap a
47
+ * producer that feeds an `unknown` worker-result sink:
48
+ *
49
+ * collectCaptureSideChannel: (filePath) => assertCloneable(collectFoo(filePath))
50
+ *
51
+ * If `collectFoo`'s return type ever gains a non-cloneable member (a function, a
52
+ * `SyntaxNode`, …) the call fails to compile, pointing at the offending key.
53
+ *
54
+ * The parameter is a conditional type rather than an `extends Cloneable<T>`
55
+ * constraint because a self-referential constraint (`T extends Cloneable<T>`)
56
+ * is a "circular constraint" error in TypeScript. For a clone-safe `T` the
57
+ * parameter resolves to `T` (call type-checks as a plain identity); for an
58
+ * unsafe `T` it resolves to `Cloneable<T>` (which has `never` at the bad key),
59
+ * so the argument is rejected.
60
+ */
61
+ export function assertCloneable(value) {
62
+ return value;
63
+ }
64
+ /**
65
+ * Recursion cap for the module's own traversal. An over-deep subtree is treated
66
+ * as non-cloneable rather than recursing to a stack overflow — without this, a
67
+ * deeply-nested record would throw `RangeError` inside the sanitizer and (since
68
+ * the recovery path is the safety net) re-arm the very cascade #2112 fixes. Set
69
+ * far below the observed ~3000-frame overflow and far above any real
70
+ * parse-result record (extraction records are shallow plain data). Note: this
71
+ * caps the module's recursion only; `structuredClone`'s own internal recursion
72
+ * (the `isStructuredCloneable` probe of non-plain objects) is bounded by that
73
+ * helper's catch-all, which turns a probe-side `RangeError` into a
74
+ * non-cloneable verdict — so do not narrow that catch.
75
+ */
76
+ const MAX_CLONE_DEPTH = 200;
77
+ /**
78
+ * True iff `key` is a canonical array-index string (`"0"`, `"1"`, … `< 2^32-1`)
79
+ * — i.e. one of the slots the numeric index loop already visits. Everything
80
+ * else returned by `Object.keys(array)` is a NON-index own-enumerable property
81
+ * (`arr.meta = …`), which the structured-clone algorithm ALSO serializes (and
82
+ * throws on if non-cloneable). The array branches of `containsNonCloneable` and
83
+ * `stripNonCloneable` use this to scan those extra keys in lockstep.
84
+ */
85
+ function isArrayIndexKey(key) {
86
+ const n = Number(key);
87
+ return Number.isInteger(n) && n >= 0 && n < 4294967295 && String(n) === key;
88
+ }
89
+ /**
90
+ * Non-allocating scan: returns true on the FIRST value structured-clone would
91
+ * reject. Used to decide whether an array (or element) needs rewriting at all,
92
+ * so clean arrays keep their referential identity and pay no copy cost.
93
+ */
94
+ function containsNonCloneable(value, seen, depth = 0) {
95
+ const t = typeof value;
96
+ if (t === 'function' || t === 'symbol')
97
+ return true;
98
+ if (value === null || t !== 'object')
99
+ return false;
100
+ // Depth bound: treat an over-deep subtree as non-cloneable (the element is
101
+ // then stripped/dropped) instead of overflowing the stack.
102
+ if (depth >= MAX_CLONE_DEPTH)
103
+ return true;
104
+ const obj = value;
105
+ // Cycles clone fine; don't recurse into one twice.
106
+ if (seen.has(obj))
107
+ return false;
108
+ // Structured-clone-native containers carry no non-cloneable payload of their
109
+ // own; their *contents* still need scanning (a Map value could be a fn).
110
+ if (obj instanceof Date || obj instanceof RegExp)
111
+ return false;
112
+ // Buffers/views usually clone, but a DETACHED one is rejected by
113
+ // structuredClone — probe rather than wave it through. No byteLength
114
+ // heuristic: a legitimately empty `new Uint8Array(0)` also has byteLength 0
115
+ // yet clones fine, so a length check would false-positive.
116
+ if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj))
117
+ return !isStructuredCloneable(obj);
118
+ seen.add(obj);
119
+ if (Array.isArray(obj)) {
120
+ for (let i = 0; i < obj.length; i++) {
121
+ if (containsNonCloneable(obj[i], seen, depth + 1))
122
+ return true;
123
+ }
124
+ // structuredClone also serializes an array's NON-index own-enumerable
125
+ // properties and throws on a non-cloneable one — scan them too (lockstep
126
+ // with stripNonCloneable's array branch; see isArrayIndexKey).
127
+ for (const key of Object.keys(obj)) {
128
+ if (isArrayIndexKey(key))
129
+ continue;
130
+ let child;
131
+ try {
132
+ child = obj[key];
133
+ }
134
+ catch {
135
+ return true; // a throwing getter can't be serialized either
136
+ }
137
+ if (containsNonCloneable(child, seen, depth + 1))
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+ if (obj instanceof Map) {
143
+ for (const [k, v] of obj) {
144
+ if (containsNonCloneable(k, seen, depth + 1) || containsNonCloneable(v, seen, depth + 1))
145
+ return true;
146
+ }
147
+ return false;
148
+ }
149
+ if (obj instanceof Set) {
150
+ for (const v of obj) {
151
+ if (containsNonCloneable(v, seen, depth + 1))
152
+ return true;
153
+ }
154
+ return false;
155
+ }
156
+ // A non-plain object (Promise, WeakMap, class instance with internal slots)
157
+ // that structured clone can't handle: detect via the authoritative probe.
158
+ // Plain objects fall through to a property scan (cheap, no allocation).
159
+ const proto = Object.getPrototypeOf(obj);
160
+ if (proto !== Object.prototype && proto !== null) {
161
+ if (!isStructuredCloneable(obj))
162
+ return true;
163
+ return false;
164
+ }
165
+ for (const key of Object.keys(obj)) {
166
+ let child;
167
+ try {
168
+ child = obj[key];
169
+ }
170
+ catch {
171
+ // A getter that throws can't be serialized either — treat as non-cloneable.
172
+ return true;
173
+ }
174
+ if (containsNonCloneable(child, seen, depth + 1))
175
+ return true;
176
+ }
177
+ return false;
178
+ }
179
+ /** Record a strip at `path` (root → `(root)`); keeps the count + key path in sync. */
180
+ function recordStrip(ctx, path) {
181
+ ctx.stripped++;
182
+ ctx.keys.push(path === '' ? '(root)' : path);
183
+ }
184
+ /**
185
+ * Deep-copy `value`, replacing any value structured-clone would reject with
186
+ * `undefined` (which clones fine). Preserves primitives, arrays, plain
187
+ * objects, and the structured-clone-native containers (Date, RegExp, Map,
188
+ * Set, ArrayBuffer, TypedArray). Rebuilds only what it must — clean leaves are
189
+ * returned by reference. `path` is the dotted key path of `value` (for the
190
+ * diagnostic record).
191
+ */
192
+ function stripNonCloneable(value, ctx, depth = 0, path = '') {
193
+ const t = typeof value;
194
+ if (t === 'function' || t === 'symbol') {
195
+ recordStrip(ctx, path);
196
+ return undefined;
197
+ }
198
+ if (value === null || t !== 'object')
199
+ return value;
200
+ // Depth bound (mirrors containsNonCloneable): drop an over-deep subtree to
201
+ // `undefined` (itself cloneable, and a legal property value / array element)
202
+ // rather than overflowing the stack.
203
+ if (depth >= MAX_CLONE_DEPTH) {
204
+ recordStrip(ctx, path);
205
+ return undefined;
206
+ }
207
+ const obj = value;
208
+ // Memoized? Return the SAME stripped copy (preserves DAG shape; terminates
209
+ // cycles by returning the in-progress copy inserted before recursing below).
210
+ if (ctx.seen.has(obj))
211
+ return ctx.seen.get(obj);
212
+ // Leaf-like values: returned by reference, but still memoize the decision so
213
+ // a second alias resolves identically.
214
+ if (obj instanceof Date || obj instanceof RegExp) {
215
+ ctx.seen.set(obj, value);
216
+ return value;
217
+ }
218
+ if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
219
+ // Keep a live buffer/view (even an empty one); drop a detached one, which
220
+ // structuredClone rejects. The probe is exact — no byteLength heuristic.
221
+ if (!isStructuredCloneable(obj)) {
222
+ recordStrip(ctx, path);
223
+ ctx.seen.set(obj, undefined);
224
+ return undefined;
225
+ }
226
+ ctx.seen.set(obj, value);
227
+ return value;
228
+ }
229
+ // Containers: allocate the empty copy, memoize it BEFORE recursing, then fill
230
+ // — so a cycle/alias that re-enters gets this in-progress copy.
231
+ if (Array.isArray(obj)) {
232
+ const out = [];
233
+ ctx.seen.set(obj, out);
234
+ for (let i = 0; i < obj.length; i++)
235
+ out.push(stripNonCloneable(obj[i], ctx, depth + 1, `${path}[${i}]`));
236
+ // Carry NON-index own-enumerable props through the same strip (lockstep
237
+ // with containsNonCloneable): structuredClone serializes them, so a
238
+ // non-cloneable one must be stripped rather than left to throw on re-post.
239
+ for (const key of Object.keys(obj)) {
240
+ if (isArrayIndexKey(key))
241
+ continue;
242
+ const childPath = `${path}.${key}`;
243
+ let child;
244
+ try {
245
+ child = obj[key];
246
+ }
247
+ catch {
248
+ recordStrip(ctx, childPath);
249
+ continue;
250
+ }
251
+ out[key] = stripNonCloneable(child, ctx, depth + 1, childPath);
252
+ }
253
+ return out;
254
+ }
255
+ if (obj instanceof Map) {
256
+ // Scope limit (acceptable): object keys aren't identity-preserved across
257
+ // stripping. Parse-result Maps are primitive-keyed, so this never bites.
258
+ const out = new Map();
259
+ ctx.seen.set(obj, out);
260
+ for (const [k, v] of obj)
261
+ out.set(stripNonCloneable(k, ctx, depth + 1, `${path}<key>`), stripNonCloneable(v, ctx, depth + 1, `${path}<map>`));
262
+ return out;
263
+ }
264
+ if (obj instanceof Set) {
265
+ const out = new Set();
266
+ ctx.seen.set(obj, out);
267
+ for (const v of obj)
268
+ out.add(stripNonCloneable(v, ctx, depth + 1, `${path}<set>`));
269
+ return out;
270
+ }
271
+ const proto = Object.getPrototypeOf(obj);
272
+ if (proto !== Object.prototype && proto !== null) {
273
+ // Non-plain object that the probe already flagged as non-cloneable and
274
+ // that we can't safely reconstruct (Promise, WeakMap, class instance with
275
+ // internal slots). Drop it whole — memoize the decision so aliases agree.
276
+ if (!isStructuredCloneable(obj)) {
277
+ recordStrip(ctx, path);
278
+ ctx.seen.set(obj, undefined);
279
+ return undefined;
280
+ }
281
+ ctx.seen.set(obj, value);
282
+ return value;
283
+ }
284
+ const out = {};
285
+ ctx.seen.set(obj, out);
286
+ for (const key of Object.keys(obj)) {
287
+ const childPath = path === '' ? key : `${path}.${key}`;
288
+ let child;
289
+ try {
290
+ child = obj[key];
291
+ }
292
+ catch {
293
+ // A getter that throws is non-serializable — drop the property.
294
+ recordStrip(ctx, childPath);
295
+ continue;
296
+ }
297
+ out[key] = stripNonCloneable(child, ctx, depth + 1, childPath);
298
+ }
299
+ return out;
300
+ }
301
+ /** Keys checked (top-level and one level deep) to attribute a record to a file. */
302
+ const DEFAULT_PATH_KEYS = ['filePath', 'path', 'file'];
303
+ /** Read `obj[key]`, returning undefined if the access throws (throwing getter / Proxy trap). */
304
+ function safeGet(obj, key) {
305
+ try {
306
+ return obj[key];
307
+ }
308
+ catch {
309
+ return undefined;
310
+ }
311
+ }
312
+ /** Read a path key off a child object (one level deep); never throws. */
313
+ function pathFromChild(child, pathKeys) {
314
+ if (child === null || typeof child !== 'object')
315
+ return undefined;
316
+ const crec = child;
317
+ for (const pk of pathKeys) {
318
+ const v = safeGet(crec, pk);
319
+ if (typeof v === 'string')
320
+ return v;
321
+ }
322
+ return undefined;
323
+ }
324
+ /**
325
+ * Best-effort source-path extraction for reporting; never throws. Reads are
326
+ * defensive (a throwing getter / Proxy trap on a path-attribution key must not
327
+ * escape and abandon the sanitize — it would re-arm the fail-closed cascade).
328
+ */
329
+ function findFilePath(element, pathKeys) {
330
+ if (element === null || typeof element !== 'object')
331
+ return undefined;
332
+ const rec = element;
333
+ // Top level first — a ParsedFile carries `filePath` here.
334
+ for (const key of pathKeys) {
335
+ const v = safeGet(rec, key);
336
+ if (typeof v === 'string')
337
+ return v;
338
+ }
339
+ // Known child next — a ParsedNode carries its path at `properties.filePath`.
340
+ // Prefer it over the generic sweep so attribution is deterministic when a
341
+ // sibling child also happens to carry a path-like key.
342
+ const fromProps = pathFromChild(safeGet(rec, 'properties'), pathKeys);
343
+ if (fromProps !== undefined)
344
+ return fromProps;
345
+ // Generic one-level sweep as the fallback for other shapes.
346
+ let keys;
347
+ try {
348
+ keys = Object.keys(rec);
349
+ }
350
+ catch {
351
+ return undefined; // a Proxy ownKeys trap that throws — give up on attribution
352
+ }
353
+ for (const key of keys) {
354
+ if (key === 'properties')
355
+ continue; // already checked above
356
+ const fromChild = pathFromChild(safeGet(rec, key), pathKeys);
357
+ if (fromChild !== undefined)
358
+ return fromChild;
359
+ }
360
+ return undefined;
361
+ }
362
+ /**
363
+ * Make a worker result's boundary-crossing array fields structured-cloneable,
364
+ * mutating `result` in place. Only arrays that actually contain a
365
+ * non-cloneable value are rewritten; everything else keeps referential
366
+ * identity. Returns the list of affected file paths for reporting.
367
+ *
368
+ * Call this after ANY failure of the fast-path post — a `DataCloneError`, OR a
369
+ * throwing getter's own error surfaced by structuredClone (the caller in
370
+ * `post-result.ts` recovers on any throw, not only `DataCloneError`).
371
+ */
372
+ export function makeWorkerResultCloneSafe(result, options) {
373
+ const pathKeys = options.pathKeys ?? DEFAULT_PATH_KEYS;
374
+ const skipped = [];
375
+ for (const field of Object.keys(result)) {
376
+ if (options.skipFields?.has(field))
377
+ continue;
378
+ const value = result[field];
379
+ if (!Array.isArray(value))
380
+ continue;
381
+ const dropWhole = options.dropWholeElement.has(field);
382
+ // `out` is built lazily — only once a dirty element appears — by copying the
383
+ // clean prefix, so a fully-clean array is never rebuilt and keeps its
384
+ // referential identity (no field reassignment). A dirty element is scanned
385
+ // (containsNonCloneable) and then stripped (stripNonCloneable): two passes,
386
+ // deliberately. The non-allocating pre-scan is exactly what lets CLEAN
387
+ // elements stay by reference (zero-copy) — replacing it with an
388
+ // always-allocating strip would regress that. This whole path is
389
+ // failure-path-only (the fast post already threw), so the second pass over
390
+ // the rare dirty element is acceptable.
391
+ let out = null;
392
+ for (let i = 0; i < value.length; i++) {
393
+ const element = value[i];
394
+ try {
395
+ if (!containsNonCloneable(element, new WeakSet())) {
396
+ if (out)
397
+ out.push(element);
398
+ continue;
399
+ }
400
+ if (!out)
401
+ out = value.slice(0, i); // first dirty element: copy clean prefix
402
+ const path = findFilePath(element, pathKeys) ?? '(unknown)';
403
+ if (dropWhole) {
404
+ skipped.push({ path, reason: `dropped non-serializable ${field} entry` });
405
+ continue;
406
+ }
407
+ const ctx = { stripped: 0, seen: new Map(), keys: [] };
408
+ const cleaned = stripNonCloneable(element, ctx);
409
+ // Last-resort guard: if stripping functions/symbols still left something
410
+ // structured-clone rejects, drop the element rather than re-throw.
411
+ if (isStructuredCloneable(cleaned)) {
412
+ out.push(cleaned);
413
+ // Name the offending key path(s) so the leak is locatable from the log
414
+ // (e.g. "from nodes: properties.toString") — not just the array field.
415
+ const at = ctx.keys.slice(0, 3).join(', ');
416
+ const more = ctx.keys.length > 3 ? `, …+${ctx.keys.length - 3}` : '';
417
+ skipped.push({
418
+ path,
419
+ reason: `stripped ${ctx.stripped} non-serializable value(s) from ${field}: ${at}${more}`,
420
+ });
421
+ }
422
+ else {
423
+ skipped.push({ path, reason: `dropped unsalvageable ${field} entry` });
424
+ }
425
+ }
426
+ catch {
427
+ // A throw DURING this element's scan/strip — a Proxy with a throwing
428
+ // `getPrototypeOf`/`ownKeys` trap reached by Object.getPrototypeOf /
429
+ // Object.keys, or any other structural-enumeration throw. Drop the
430
+ // element rather than let the throw escape to postResultCloneSafe's
431
+ // fail-closed {type:'error'} (which under POOL_SIZE=1 re-arms the
432
+ // cascade this net prevents). One pathological element can't sink the
433
+ // whole result.
434
+ if (!out)
435
+ out = value.slice(0, i);
436
+ skipped.push({ path: '(unknown)', reason: `dropped ${field} entry (sanitizer error)` });
437
+ }
438
+ }
439
+ if (out)
440
+ result[field] = out;
441
+ }
442
+ // Final safety gate. The loop above only rewrites ARRAY fields, so a future
443
+ // non-array result sink (a nested object / Map) — or an array field whose own
444
+ // non-index property the element loop didn't reach — could still hold a
445
+ // non-cloneable value and throw on the re-post. Make "the returned result is
446
+ // structured-cloneable" a hard postcondition: strip any remaining offending
447
+ // field in place. Failure-path-only and a no-op once the result is already
448
+ // clean (the per-field probe short-circuits every clean field).
449
+ if (!isStructuredCloneable(result)) {
450
+ for (const field of Object.keys(result)) {
451
+ if (options.skipFields?.has(field))
452
+ continue;
453
+ if (isStructuredCloneable(result[field]))
454
+ continue;
455
+ const ctx = { stripped: 0, seen: new Map(), keys: [] };
456
+ result[field] = stripNonCloneable(result[field], ctx);
457
+ const at = ctx.keys.slice(0, 3).join(', ');
458
+ skipped.push({
459
+ path: '(result)',
460
+ reason: `stripped ${ctx.stripped} non-serializable value(s) from ${field}${at ? `: ${at}` : ''}`,
461
+ });
462
+ }
463
+ }
464
+ return { skipped };
465
+ }
@@ -1,4 +1,5 @@
1
1
  import { SupportedLanguages } from '../../../_shared/index.js';
2
+ import type { SkippedPath } from './clone-safety.js';
2
3
  import type { ExtractedRouterInclude, ExtractedRouterImport, ExtractedRouterModuleAlias } from '../route-extractors/fastapi-router-bindings.js';
3
4
  import { type MixedChainStep } from '../utils/call-analysis.js';
4
5
  import type { ConstructorBinding } from '../type-env.js';
@@ -201,6 +202,15 @@ export interface ParseWorkerResult {
201
202
  */
202
203
  parsedFiles: ParsedFile[];
203
204
  skippedLanguages: Record<string, number>;
205
+ /**
206
+ * Files whose parse output carried a value the structured-clone algorithm
207
+ * couldn't serialize across the worker boundary (#2112). The clone-safety
208
+ * net stripped or dropped the offending value so the result could be
209
+ * delivered; these paths are surfaced to the operator so the (rare) data
210
+ * loss is visible. Optional for cache backward compatibility — older cache
211
+ * entries predate the field; consumers must guard with `?? []`.
212
+ */
213
+ skippedPaths?: SkippedPath[];
204
214
  fileCount: number;
205
215
  }
206
216
  export interface ParseWorkerInput {