gitnexus 1.6.10-rc.83 → 1.6.10-rc.84

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.
@@ -19,6 +19,10 @@ import { generateId } from '../../../lib/utils.js';
19
19
  import { getPhaseOutput } from './types.js';
20
20
  const require = createRequire(import.meta.url);
21
21
  const yaml = require('js-yaml');
22
+ // js-yaml 5 dropped DEFAULT_SCHEMA; CORE plus these tags is what it used to be, so
23
+ // explicitly tagged values keep parsing instead of throwing (an unknown tag aborts
24
+ // the whole file). None of them can execute code.
25
+ const SPRING_YAML_SCHEMA = yaml.CORE_SCHEMA.withTags(yaml.mergeTag, yaml.timestampTag, yaml.binaryTag, yaml.omapTag, yaml.pairsTag, yaml.setTag);
22
26
  const MAX_CONFIG_FILE_BYTES = 2 * 1024 * 1024;
23
27
  const MAX_YAML_TRAVERSAL_DEPTH = 128;
24
28
  const MAX_YAML_TRAVERSAL_NODES = 100_000;
@@ -111,10 +115,10 @@ function consumeYamlTraversalBudget(state, depth) {
111
115
  function isObjectValue(value) {
112
116
  return value !== null && typeof value === 'object';
113
117
  }
114
- function resolveYamlAliasEvent(event, objectEvents) {
115
- if (event?.kind !== null || !isObjectValue(event.result))
116
- return event;
117
- return objectEvents.get(event.result) ?? event;
118
+ // Aliases are resolved to their anchor event by name while the tree is built,
119
+ // so following one here is a single pointer hop.
120
+ function resolveYamlAliasEvent(event) {
121
+ return event?.aliasOf ?? event;
118
122
  }
119
123
  function yamlMappingPairs(event) {
120
124
  const pairs = [];
@@ -127,112 +131,238 @@ function yamlMappingPairs(event) {
127
131
  }
128
132
  return pairs;
129
133
  }
130
- function findYamlMappingLocation(event, key, objectEvents, traversal, visited = new Set(), depth = 0) {
131
- consumeYamlTraversalBudget(traversal, depth);
132
- const resolved = resolveYamlAliasEvent(event, objectEvents);
133
- if (resolved === undefined || visited.has(resolved))
134
- return undefined;
135
- visited.add(resolved);
136
- if (resolved.kind === 'sequence') {
137
- for (const child of resolved.children) {
138
- const found = findYamlMappingLocation(child, key, objectEvents, traversal, visited, depth + 1);
139
- if (found !== undefined)
140
- return found;
134
+ /**
135
+ * First match in a pre-order walk of `event`, following sequences and `<<` merge
136
+ * chains. Iterative: children are pushed in reverse so the explicit stack pops
137
+ * them in declaration order, which is what makes "first match" mean the same
138
+ * thing it did when this recursed.
139
+ */
140
+ function findYamlMappingLocation(event, key, traversal) {
141
+ const visited = new Set();
142
+ const stack = [{ event, depth: 0 }];
143
+ while (stack.length > 0) {
144
+ const step = stack.pop();
145
+ if (step === undefined)
146
+ break;
147
+ consumeYamlTraversalBudget(traversal, step.depth);
148
+ const resolved = resolveYamlAliasEvent(step.event);
149
+ if (resolved === undefined || visited.has(resolved))
150
+ continue;
151
+ visited.add(resolved);
152
+ if (resolved.kind === 'sequence') {
153
+ for (let index = resolved.children.length - 1; index >= 0; index--) {
154
+ stack.push({ event: resolved.children[index], depth: step.depth + 1 });
155
+ }
156
+ continue;
157
+ }
158
+ if (resolved.kind !== 'mapping')
159
+ continue;
160
+ const pairs = yamlMappingPairs(resolved);
161
+ const direct = pairs.find((pair) => pair.key === key);
162
+ if (direct !== undefined) {
163
+ return { valueEvent: direct.valueEvent, line: direct.keyEvent.startLine };
164
+ }
165
+ const merges = pairs.filter((pair) => pair.key === '<<');
166
+ for (let index = merges.length - 1; index >= 0; index--) {
167
+ stack.push({ event: merges[index].valueEvent, depth: step.depth + 1 });
141
168
  }
142
- return undefined;
143
- }
144
- if (resolved.kind !== 'mapping')
145
- return undefined;
146
- const pairs = yamlMappingPairs(resolved);
147
- const direct = pairs.find((pair) => pair.key === key);
148
- if (direct !== undefined) {
149
- return { valueEvent: direct.valueEvent, line: direct.keyEvent.startLine };
150
- }
151
- for (const merge of pairs.filter((pair) => pair.key === '<<')) {
152
- const found = findYamlMappingLocation(merge.valueEvent, key, objectEvents, traversal, visited, depth + 1);
153
- if (found !== undefined)
154
- return found;
155
169
  }
156
170
  return undefined;
157
171
  }
158
- function flattenYamlValue(value, event, prefix, out, objectEvents, traversal, sourceLine = event?.startLine ?? 1, depth = 0) {
159
- consumeYamlTraversalBudget(traversal, depth);
160
- const resolvedEvent = resolveYamlAliasEvent(event, objectEvents);
161
- const trackedObject = isObjectValue(value) ? value : undefined;
162
- if (trackedObject !== undefined && traversal.activeObjects.has(trackedObject))
163
- return;
164
- if (trackedObject !== undefined)
165
- traversal.activeObjects.add(trackedObject);
166
- try {
167
- if (Array.isArray(value)) {
168
- if (value.length === 0 && prefix.length > 0 && !out.has(prefix))
169
- out.set(prefix, sourceLine);
170
- value.forEach((item, index) => flattenYamlValue(item, resolvedEvent?.children[index], `${prefix}[${index}]`, out, objectEvents, traversal, sourceLine, depth + 1));
171
- return;
172
+ /**
173
+ * Flatten a document to `dotted.key -> line`, iteratively. Children are pushed in
174
+ * reverse so the stack pops them in declaration order, keeping `out` in the same
175
+ * insertion order — and the traversal budget consumed in the same sequence — as
176
+ * the recursive walk this replaced.
177
+ */
178
+ function flattenYamlValue(value, event, prefix, out, traversal) {
179
+ const stack = [
180
+ { kind: 'visit', value, event, prefix, sourceLine: event?.startLine ?? 1, depth: 0 },
181
+ ];
182
+ while (stack.length > 0) {
183
+ const step = stack.pop();
184
+ if (step === undefined)
185
+ break;
186
+ if (step.kind === 'leave') {
187
+ traversal.activeObjects.delete(step.object);
188
+ continue;
189
+ }
190
+ const { value: current, prefix: currentPrefix, sourceLine, depth } = step;
191
+ consumeYamlTraversalBudget(traversal, depth);
192
+ const resolvedEvent = resolveYamlAliasEvent(step.event);
193
+ const trackedObject = isObjectValue(current) ? current : undefined;
194
+ if (trackedObject !== undefined) {
195
+ if (traversal.activeObjects.has(trackedObject))
196
+ continue;
197
+ traversal.activeObjects.add(trackedObject);
198
+ stack.push({ kind: 'leave', object: trackedObject });
172
199
  }
173
- if (value !== null &&
174
- typeof value === 'object' &&
200
+ if (Array.isArray(current)) {
201
+ if (current.length === 0 && currentPrefix.length > 0 && !out.has(currentPrefix)) {
202
+ out.set(currentPrefix, sourceLine);
203
+ }
204
+ for (let index = current.length - 1; index >= 0; index--) {
205
+ stack.push({
206
+ kind: 'visit',
207
+ value: current[index],
208
+ event: resolvedEvent?.children[index],
209
+ prefix: `${currentPrefix}[${index}]`,
210
+ sourceLine,
211
+ depth: depth + 1,
212
+ });
213
+ }
214
+ continue;
215
+ }
216
+ if (current !== null &&
217
+ typeof current === 'object' &&
175
218
  (resolvedEvent?.kind === 'mapping' || resolvedEvent === undefined)) {
176
- const entries = Object.entries(value);
177
- if (entries.length === 0 && prefix.length > 0 && !out.has(prefix))
178
- out.set(prefix, sourceLine);
179
- for (const [key, nested] of entries) {
180
- const next = prefix.length === 0 ? key : `${prefix}.${key}`;
181
- const location = findYamlMappingLocation(resolvedEvent, key, objectEvents, traversal);
182
- flattenYamlValue(nested, location?.valueEvent, next, out, objectEvents, traversal, location?.line ?? sourceLine, depth + 1);
219
+ // js-yaml 5 builds `!!set` as a native Set, whose members are not own
220
+ // properties; v4 built a plain `{member: null}` object. Enumerate them so a
221
+ // tagged set still contributes one key per member instead of a bare leaf.
222
+ const entries = current instanceof Set
223
+ ? [...current].map((member) => [String(member), null])
224
+ : Object.entries(current);
225
+ if (entries.length === 0 && currentPrefix.length > 0 && !out.has(currentPrefix)) {
226
+ out.set(currentPrefix, sourceLine);
183
227
  }
184
- return;
228
+ for (let index = entries.length - 1; index >= 0; index--) {
229
+ const [key, nested] = entries[index];
230
+ const location = findYamlMappingLocation(resolvedEvent, key, traversal);
231
+ stack.push({
232
+ kind: 'visit',
233
+ value: nested,
234
+ event: location?.valueEvent,
235
+ prefix: currentPrefix.length === 0 ? key : `${currentPrefix}.${key}`,
236
+ sourceLine: location?.line ?? sourceLine,
237
+ depth: depth + 1,
238
+ });
239
+ }
240
+ continue;
185
241
  }
186
- if (prefix.length > 0 && !out.has(prefix))
187
- out.set(prefix, sourceLine);
242
+ if (currentPrefix.length > 0 && !out.has(currentPrefix))
243
+ out.set(currentPrefix, sourceLine);
188
244
  }
189
- finally {
190
- if (trackedObject !== undefined)
191
- traversal.activeObjects.delete(trackedObject);
245
+ }
246
+ // js-yaml 5 reports node positions as source offsets; map them to 1-based lines.
247
+ function makeLineResolver(source) {
248
+ const lineStarts = [0];
249
+ for (let index = 0; index < source.length; index++) {
250
+ if (source[index] === '\n')
251
+ lineStarts.push(index + 1);
192
252
  }
253
+ return (offset) => {
254
+ let low = 0;
255
+ let high = lineStarts.length - 1;
256
+ let line = 0;
257
+ while (low <= high) {
258
+ const mid = (low + high) >> 1;
259
+ if (lineStarts[mid] <= offset) {
260
+ line = mid;
261
+ low = mid + 1;
262
+ }
263
+ else {
264
+ high = mid - 1;
265
+ }
266
+ }
267
+ return line + 1;
268
+ };
269
+ }
270
+ /**
271
+ * Rebuild the parse tree from js-yaml 5's event stream (v4's `listener` option
272
+ * was removed). Returns each document's root event, with aliases already
273
+ * resolved to their anchor event so merged/aliased keys keep the line where
274
+ * they were declared.
275
+ *
276
+ * One node per event, so this pass is bounded by MAX_CONFIG_FILE_BYTES alone —
277
+ * MAX_YAML_TRAVERSAL_NODES governs the later walk, which can revisit a shared
278
+ * anchor many times and so needs a budget this linear pass does not.
279
+ */
280
+ function buildYamlEventTree(events, source) {
281
+ const lineOf = makeLineResolver(source);
282
+ const anchors = new Map();
283
+ const stack = [];
284
+ const documentRoots = [];
285
+ const anchorName = (start, end) => start >= 0 && end > start ? source.slice(start, end) : null;
286
+ const attach = (node) => {
287
+ stack[stack.length - 1]?.children.push(node);
288
+ };
289
+ const register = (name, node) => {
290
+ if (name !== null)
291
+ anchors.set(name, node);
292
+ };
293
+ for (const event of events) {
294
+ switch (event.type) {
295
+ case yaml.EVENT_DOCUMENT:
296
+ // Anchors are document-scoped. constructFromEvents already rejects a
297
+ // cross-document alias before we get here, so this only keeps the two
298
+ // layers from disagreeing.
299
+ anchors.clear();
300
+ stack.push({
301
+ startLine: 1,
302
+ kind: null,
303
+ result: undefined,
304
+ aliasOf: undefined,
305
+ children: [],
306
+ });
307
+ break;
308
+ case yaml.EVENT_MAPPING:
309
+ case yaml.EVENT_SEQUENCE: {
310
+ const node = {
311
+ startLine: lineOf(event.start),
312
+ kind: event.type === yaml.EVENT_MAPPING ? 'mapping' : 'sequence',
313
+ result: undefined,
314
+ aliasOf: undefined,
315
+ children: [],
316
+ };
317
+ register(anchorName(event.anchorStart, event.anchorEnd), node);
318
+ attach(node);
319
+ stack.push(node);
320
+ break;
321
+ }
322
+ case yaml.EVENT_SCALAR: {
323
+ const node = {
324
+ startLine: lineOf(event.valueStart),
325
+ kind: 'scalar',
326
+ result: yaml.getScalarValue(source, event),
327
+ aliasOf: undefined,
328
+ children: [],
329
+ };
330
+ register(anchorName(event.anchorStart, event.anchorEnd), node);
331
+ attach(node);
332
+ break;
333
+ }
334
+ case yaml.EVENT_ALIAS: {
335
+ const target = anchors.get(anchorName(event.anchorStart, event.anchorEnd) ?? '');
336
+ attach({ startLine: 1, kind: 'alias', result: undefined, aliasOf: target, children: [] });
337
+ break;
338
+ }
339
+ case yaml.EVENT_POP: {
340
+ const done = stack.pop();
341
+ // Documents are the only top-level containers, so a pop that empties the
342
+ // stack closes a document; its single child is the document's root value.
343
+ if (done !== undefined && stack.length === 0)
344
+ documentRoots.push(done.children[0]);
345
+ break;
346
+ }
347
+ }
348
+ }
349
+ return documentRoots;
193
350
  }
194
351
  /** Parse and flatten YAML leaves without retaining their values. */
195
352
  export function parseSpringYaml(content, filePath, profile) {
196
353
  const flattened = new Map();
197
- const eventStack = [];
198
- const documentEvents = [];
199
- const objectEvents = new WeakMap();
200
- const documents = [];
201
354
  const traversal = {
202
355
  remainingNodes: MAX_YAML_TRAVERSAL_NODES,
203
356
  activeObjects: new Set(),
204
357
  };
205
- yaml.loadAll(content, (document) => documents.push(document), {
206
- schema: yaml.DEFAULT_SCHEMA,
358
+ const events = yaml.parseEvents(content, { maxDepth: MAX_YAML_TRAVERSAL_DEPTH });
359
+ const documents = yaml.constructFromEvents(events, {
360
+ source: content,
361
+ schema: SPRING_YAML_SCHEMA,
207
362
  json: true,
208
- listener: (eventType, state) => {
209
- if (eventType === 'open') {
210
- eventStack.push({
211
- startLine: state.line + 1,
212
- kind: null,
213
- result: undefined,
214
- tag: null,
215
- children: [],
216
- });
217
- return;
218
- }
219
- const event = eventStack.pop();
220
- if (event === undefined)
221
- return;
222
- event.kind = state.kind ?? null;
223
- event.result = state.result;
224
- event.tag = state.tag ?? null;
225
- if (isObjectValue(event.result) && event.kind !== null) {
226
- objectEvents.set(event.result, event);
227
- }
228
- const parent = eventStack[eventStack.length - 1];
229
- if (parent === undefined)
230
- documentEvents.push(event);
231
- else
232
- parent.children.push(event);
233
- },
234
363
  });
235
- documents.forEach((document, index) => flattenYamlValue(document, documentEvents[index], '', flattened, objectEvents, traversal));
364
+ const documentEvents = buildYamlEventTree(events, content);
365
+ documents.forEach((document, index) => flattenYamlValue(document, documentEvents[index], '', flattened, traversal));
236
366
  return [...flattened.entries()]
237
367
  .sort(([left], [right]) => left.localeCompare(right))
238
368
  .map(([key, line]) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.83",
3
+ "version": "1.6.10-rc.84",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -70,7 +70,7 @@
70
70
  "graphology-indices": "^0.17.0",
71
71
  "graphology-utils": "^2.3.0",
72
72
  "ignore": "^7.0.5",
73
- "js-yaml": "^4.1.1",
73
+ "js-yaml": "^5.0.0",
74
74
  "jsonc-parser": "^3.3.1",
75
75
  "mnemonist": "^0.40.3",
76
76
  "node-addon-api": "^8.0.0",
@@ -105,7 +105,6 @@
105
105
  "@types/cli-progress": "^3.11.6",
106
106
  "@types/cors": "^2.8.17",
107
107
  "@types/express": "^5.0.6",
108
- "@types/js-yaml": "^4.0.9",
109
108
  "@types/node": "^26.0.0",
110
109
  "@vitest/coverage-v8": "^4.0.18",
111
110
  "gitnexus-shared": "file:../gitnexus-shared",