gitnexus 1.6.13-rc.81 → 1.6.13-rc.83

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.
@@ -12,6 +12,12 @@ import type { NodeLabel } from '../../_shared/index.js';
12
12
  import { KnowledgeGraph } from '../graph/types.js';
13
13
  /** Graphology Graph instance type (AbstractGraph from graphology-types avoids CJS/ESM interop namespace issue) */
14
14
  type GraphInstance = AbstractGraph<Attributes, Attributes, Attributes>;
15
+ /** Result returned by leiden.detailed() */
16
+ interface LeidenDetailedResult {
17
+ communities: Record<string, number>;
18
+ count: number;
19
+ modularity: number;
20
+ }
15
21
  type CommunityEngine = 'graphology' | 'icebug';
16
22
  export type CommunityDetectionEngine = CommunityEngine | 'auto';
17
23
  export interface CommunityDetectionOptions {
@@ -48,6 +54,11 @@ export interface CommunityCsr {
48
54
  indptr: BigUint64Array;
49
55
  indices: BigUint64Array;
50
56
  }
57
+ interface CommunityEngineResult extends LeidenDetailedResult {
58
+ engine: CommunityEngine;
59
+ engineRequested: CommunityDetectionEngine;
60
+ fallbackReason?: string;
61
+ }
51
62
  export declare const resolveCommunityDetectionEngine: (raw?: string) => CommunityDetectionEngine;
52
63
  export interface CommunityNode {
53
64
  id: string;
@@ -92,6 +103,7 @@ export declare const processCommunities: (knowledgeGraph: KnowledgeGraph, onProg
92
103
  export declare const buildCommunityProjection: (knowledgeGraph: KnowledgeGraph) => CommunityProjection;
93
104
  export declare const buildCommunityCsr: (projection: CommunityProjection) => CommunityCsr;
94
105
  export declare const buildGraphologyGraph: (projection: CommunityProjection) => GraphInstance;
106
+ export declare const runGraphologyLeiden: (graph: GraphInstance, isLarge: boolean, engineRequested: CommunityDetectionEngine, timeoutMs?: number) => Promise<CommunityEngineResult>;
95
107
  /**
96
108
  * Runs Leiden in a worker so a native crash cannot take the analyze process
97
109
  * with it. Written against @ladybugmem/icebug's published surface (lib/index.js
@@ -21,7 +21,9 @@ const __dirname = dirname(__filename);
21
21
  // Navigate to package root (works from both src/ and dist/)
22
22
  const leidenPath = resolve(__dirname, '..', '..', '..', 'vendor', 'leiden', 'index.cjs');
23
23
  const _require = createRequire(import.meta.url);
24
- const leiden = _require(leidenPath);
24
+ // The Leiden worker loads its own copy; this load keeps a missing vendor/ asset
25
+ // failing at import time and keeps it visible to dockerfile-runtime-asset-parity.
26
+ _require(leidenPath);
25
27
  /**
26
28
  * Deterministic PRNG (mulberry32) seed for the vendored Leiden algorithm.
27
29
  * Vendored Leiden defaults `rng: Math.random`, which makes community
@@ -30,15 +32,6 @@ const leiden = _require(leidenPath);
30
32
  * incremental-indexing equivalence test (incremental ≡ full rebuild).
31
33
  */
32
34
  const LEIDEN_SEED = 0xc0de;
33
- function createSeededRng(seed) {
34
- let s = seed >>> 0;
35
- return () => {
36
- s = (s + 0x6d2b79f5) >>> 0;
37
- let t = Math.imul(s ^ (s >>> 15), 1 | s);
38
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
39
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
40
- };
41
- }
42
35
  const COMMUNITY_ENGINE_ENV = 'GITNEXUS_COMMUNITY_ENGINE';
43
36
  /**
44
37
  * Not a declared dependency: the prebuilds need system Arrow 24, libomp and
@@ -289,19 +282,96 @@ const runCommunityEngine = async (graph, projection, engineRequested, options, o
289
282
  const fallbackReason = error instanceof Error ? error.message : String(error);
290
283
  onProgress?.(`Icebug community engine unavailable, falling back to Graphology: ${fallbackReason}`, 35);
291
284
  const fallback = await runGraphologyLeiden(graph, projection.isLarge, engineRequested);
292
- return { ...fallback, fallbackReason };
285
+ return {
286
+ ...fallback,
287
+ fallbackReason: fallback.fallbackReason
288
+ ? `${fallbackReason}; ${fallback.fallbackReason}`
289
+ : fallbackReason,
290
+ };
293
291
  }
294
292
  };
295
- const runGraphologyLeiden = async (graph, isLarge, engineRequested) => {
293
+ /**
294
+ * Runs the vendored Leiden in a worker so LEIDEN_TIMEOUT_MS can actually fire.
295
+ * leiden.detailed is synchronous and, on some graphs, never returns
296
+ * (graphology/graphology#557); a timer on the main thread cannot interrupt it.
297
+ * The worker is pure JS, so terminate() is safe (unlike the icebug N-API worker).
298
+ */
299
+ const GRAPHOLOGY_WORKER_SOURCE = `
300
+ const { parentPort, workerData } = require('node:worker_threads');
301
+ try {
302
+ const Graph = require(workerData.graphologyPath);
303
+ const leiden = require(workerData.leidenPath);
304
+ const graph = Graph.from(workerData.graph);
305
+ const rng = (() => {
306
+ let s = workerData.seed >>> 0;
307
+ return () => {
308
+ s = (s + 0x6d2b79f5) >>> 0;
309
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
310
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
311
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
312
+ };
313
+ })();
314
+ const d = leiden.detailed(graph, {
315
+ resolution: workerData.resolution,
316
+ maxIterations: workerData.maxIterations,
317
+ rng,
318
+ });
319
+ parentPort.postMessage({
320
+ ok: true,
321
+ communities: d.communities,
322
+ count: d.count,
323
+ modularity: d.modularity,
324
+ });
325
+ } catch (error) {
326
+ parentPort.postMessage({ ok: false, error: error instanceof Error ? error.message : String(error) });
327
+ }
328
+ `;
329
+ export const runGraphologyLeiden = async (graph, isLarge, engineRequested, timeoutMs = LEIDEN_TIMEOUT_MS) => {
296
330
  try {
297
- const details = await Promise.race([
298
- Promise.resolve(leiden.detailed(graph, {
299
- resolution: isLarge ? 2.0 : 1.0,
300
- maxIterations: isLarge ? 3 : 0,
301
- rng: createSeededRng(LEIDEN_SEED),
302
- })),
303
- new Promise((_, reject) => setTimeout(() => reject(new Error('Leiden timeout')), LEIDEN_TIMEOUT_MS)),
304
- ]);
331
+ const details = await new Promise((resolve, reject) => {
332
+ const worker = new Worker(GRAPHOLOGY_WORKER_SOURCE, {
333
+ eval: true,
334
+ workerData: {
335
+ graphologyPath: _require.resolve('graphology'),
336
+ leidenPath,
337
+ graph: graph.export(),
338
+ seed: LEIDEN_SEED,
339
+ resolution: isLarge ? 2.0 : 1.0,
340
+ maxIterations: isLarge ? 3 : 0,
341
+ },
342
+ });
343
+ let settled = false;
344
+ const timeout = setTimeout(() => {
345
+ settled = true;
346
+ void worker.terminate();
347
+ reject(new Error('Leiden timeout'));
348
+ }, timeoutMs);
349
+ worker.once('message', (message) => {
350
+ settled = true;
351
+ clearTimeout(timeout);
352
+ if (message.ok === true) {
353
+ resolve({
354
+ communities: message.communities,
355
+ count: message.count,
356
+ modularity: message.modularity,
357
+ });
358
+ }
359
+ else {
360
+ reject(new Error(message.error));
361
+ }
362
+ });
363
+ worker.once('error', (error) => {
364
+ settled = true;
365
+ clearTimeout(timeout);
366
+ reject(error);
367
+ });
368
+ worker.once('exit', (code) => {
369
+ if (settled)
370
+ return;
371
+ clearTimeout(timeout);
372
+ reject(new Error(`Graphology Leiden worker exited with code ${code}`));
373
+ });
374
+ });
305
375
  return { ...details, engine: 'graphology', engineRequested };
306
376
  }
307
377
  catch (e) {
@@ -15,14 +15,21 @@ export interface DartPackageConfigOptions {
15
15
  * is listed and before its entries are opened.
16
16
  */
17
17
  readonly beforeEntryOpen?: (relativePath: string) => void | Promise<void>;
18
+ /**
19
+ * Test seam. Production calls omit it. Invoked after the directory is
20
+ * opened and before it is listed.
21
+ */
22
+ readonly beforeDirectoryList?: (relativePath: string) => void | Promise<void>;
18
23
  }
19
24
  /** Linux anchors child opens. macOS verifies them. Every other platform does neither. */
20
25
  export declare function pubspecWalkAnchored(): boolean;
21
26
  export declare function directoryOpenFlags(): number;
22
27
  /**
23
28
  * Path that lists the directory inode already open on `fd`.
24
- * Linux uses `/proc/self/fd/N` and macOS uses `/dev/fd/N`.
25
- * Other platforms have no such path; callers refuse instead of listing by name.
29
+ * Only Linux has one: `/proc/self/fd/N`. macOS refuses `opendir` on
30
+ * `/dev/fd/N` for a directory (ENOTDIR) and Node has no `fdopendir`, so the
31
+ * walker lists the lexical path and verifies the pinned chain around it.
32
+ * Every other platform returns null and is not walked.
26
33
  */
27
34
  export declare function descriptorDirectoryPath(fd: number): string | null;
28
35
  /**
@@ -94,14 +94,14 @@ function directoryIdentity(stat) {
94
94
  }
95
95
  /**
96
96
  * Path that lists the directory inode already open on `fd`.
97
- * Linux uses `/proc/self/fd/N` and macOS uses `/dev/fd/N`.
98
- * Other platforms have no such path; callers refuse instead of listing by name.
97
+ * Only Linux has one: `/proc/self/fd/N`. macOS refuses `opendir` on
98
+ * `/dev/fd/N` for a directory (ENOTDIR) and Node has no `fdopendir`, so the
99
+ * walker lists the lexical path and verifies the pinned chain around it.
100
+ * Every other platform returns null and is not walked.
99
101
  */
100
102
  export function descriptorDirectoryPath(fd) {
101
103
  if (process.platform === 'linux')
102
104
  return `/proc/self/fd/${fd}`;
103
- if (process.platform === 'darwin')
104
- return `/dev/fd/${fd}`;
105
105
  return null;
106
106
  }
107
107
  /**
@@ -204,11 +204,31 @@ async function openChildFile(frames, parentFd, name, lexicalPath) {
204
204
  return openLexicalFile(frames, lexicalPath);
205
205
  throw Object.assign(new Error('no descriptor anchor'), { code: 'ENOTSUP' });
206
206
  }
207
- async function listOpenedDirectory(handle, entryLimit) {
208
- const listing = descriptorDirectoryPath(handle.fd);
209
- if (listing === null) {
207
+ /**
208
+ * List the directory open on the last frame of `frames`. Linux lists the
209
+ * pinned inode through its descriptor. macOS re-checks the pinned chain, lists
210
+ * the lexical path, then re-checks the chain, so a path replaced around the
211
+ * listing fails the walk instead of being listed.
212
+ */
213
+ async function listOpenedDirectory(frames, entryLimit, beforeList) {
214
+ const frame = frames[frames.length - 1];
215
+ if (frame === undefined) {
216
+ throw Object.assign(new Error('no directory to list'), { code: 'EINVAL' });
217
+ }
218
+ const anchored = descriptorDirectoryPath(frame.handle.fd);
219
+ if (anchored === null && process.platform !== 'darwin') {
210
220
  throw Object.assign(new Error('no descriptor listing'), { code: 'ENOTSUP' });
211
221
  }
222
+ if (anchored === null)
223
+ await assertPinnedChain(frames);
224
+ if (beforeList)
225
+ await beforeList();
226
+ const entries = await readDirectoryBounded(anchored ?? frame.absolute, entryLimit);
227
+ if (anchored === null)
228
+ await assertPinnedChain(frames);
229
+ return entries;
230
+ }
231
+ async function readDirectoryBounded(listing, entryLimit) {
212
232
  const dir = await opendir(listing);
213
233
  const entries = [];
214
234
  try {
@@ -236,8 +256,16 @@ async function listOpenedDirectory(handle, entryLimit) {
236
256
  */
237
257
  export async function readDirectoryNoFollow(directory) {
238
258
  const opened = await openVerifiedDirectory(directory);
259
+ const frame = {
260
+ relative: '',
261
+ absolute: directory,
262
+ handle: opened.handle,
263
+ identity: opened.identity,
264
+ entries: [],
265
+ next: 0,
266
+ };
239
267
  try {
240
- return await listOpenedDirectory(opened.handle, DART_PUBSPEC_DIRECTORY_ENTRY_LIMIT);
268
+ return await listOpenedDirectory([frame], DART_PUBSPEC_DIRECTORY_ENTRY_LIMIT);
241
269
  }
242
270
  finally {
243
271
  await opened.handle.close();
@@ -280,7 +308,8 @@ export async function loadDartPackageConfig(repoPath, options) {
280
308
  if (++visited > directoryLimit)
281
309
  return incomplete('directory-limit', frame.relative || '.');
282
310
  try {
283
- frame.entries = await listOpenedDirectory(frame.handle, directoryEntryLimit);
311
+ const beforeList = options?.beforeDirectoryList;
312
+ frame.entries = await listOpenedDirectory(stack, directoryEntryLimit, beforeList ? () => beforeList(frame.relative) : undefined);
284
313
  }
285
314
  catch (error) {
286
315
  const reason = error.code === 'E2BIG' ? 'directory-entries' : 'read-directory';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.81",
3
+ "version": "1.6.13-rc.83",
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",
@@ -59,6 +59,7 @@
59
59
  "test:watch": "vitest",
60
60
  "test:coverage": "vitest run --coverage",
61
61
  "test:cross-platform": "tsx scripts/run-cross-platform.ts",
62
+ "test:benchmarks": "node --import tsx scripts/run-benchmarks.ts",
62
63
  "postinstall": "node scripts/build-tree-sitter-grammars.cjs",
63
64
  "assert-publish-coverage": "node scripts/assert-publish-grammar-coverage.cjs",
64
65
  "prepare": "node scripts/build.js",
@@ -206,9 +206,8 @@ const LBUG_NATIVE = [
206
206
  // (the same reason fts-extension-e2e.test.ts is registered below), so the
207
207
  // FTS-unavailable branch has to run on a real Windows/macOS runner rather
208
208
  // than only on Ubuntu where FTS always loads. And its both-blocked case is
209
- // gated on GITNEXUS_REQUIRE_VECTOR=1, which ci-tests.yml sets ONLY on this
210
- // job — everywhere else an unavailable VECTOR extension skips instead of
211
- // failing. Budget: four real analyze runs, so expect it to sit alongside the
209
+ // gated on GITNEXUS_REQUIRE_VECTOR=1, which ci-tests.yml requires in both
210
+ // coverage and platform jobs so an unavailable VECTOR fails loudly. Budget: four real analyze runs, so expect it to sit alongside the
212
211
  // VECTOR sibling's ~87s Windows measurement.
213
212
  'test/unit/incremental-index-extension-dml-gate.test.ts',
214
213
  ];
@@ -290,6 +289,7 @@ const SPAWN_CLI = [
290
289
  // Worker threads tests — exercise real worker_threads which have
291
290
  // platform-specific behavior (thread spawning, IPC, exit handling)
292
291
  const WORKER_THREADS = [
292
+ 'test/unit/community-processor.test.ts',
293
293
  'test/integration/worker-pool.test.ts',
294
294
  'test/integration/parse-impl-quarantine-cache-skip.test.ts',
295
295
  ];
@@ -308,6 +308,10 @@ const NATIVE_ADDON_SMOKE = [
308
308
  // Filesystem behavior tests — exercise operations that vary across
309
309
  // platforms (CRLF, symlinks, permissions, temp dirs)
310
310
  const FILESYSTEM = [
311
+ // The deletion-guard cases in this file require real Windows path semantics.
312
+ 'test/unit/canonicalize-path-long-path-prefix.test.ts',
313
+ 'test/unit/storage-resolver.test.ts',
314
+ 'test/unit/dart-package-imports.test.ts',
311
315
  // Cargo membership uses path normalization, descriptor validation, symlinks,
312
316
  // and Rust native parsing (including long Windows source strings).
313
317
  'test/unit/scope-resolution/rust-cargo-targets.test.ts',
@@ -0,0 +1,33 @@
1
+ import { JsonReporter, type TestRunEndReason } from 'vitest/node';
2
+
3
+ /** Vitest's stock JSON omits unhandled errors, even when success is true. */
4
+ export default class ExecutionReporter extends JsonReporter {
5
+ private executionErrors = 0;
6
+ private executionReason: TestRunEndReason = 'interrupted';
7
+
8
+ constructor() {
9
+ super({});
10
+ }
11
+
12
+ override async onTestRunEnd(
13
+ modules: Parameters<JsonReporter['onTestRunEnd']>[0],
14
+ errors: readonly unknown[] = [],
15
+ reason: TestRunEndReason = 'interrupted',
16
+ ): Promise<void> {
17
+ this.executionErrors = errors.length;
18
+ this.executionReason = reason;
19
+ await super.onTestRunEnd(modules);
20
+ }
21
+
22
+ override async writeReport(json: string): Promise<void> {
23
+ const report = JSON.parse(json);
24
+ await super.writeReport(
25
+ JSON.stringify({
26
+ ...report,
27
+ success: report.success && this.executionErrors === 0 && this.executionReason === 'passed',
28
+ executionErrors: this.executionErrors,
29
+ executionReason: this.executionReason,
30
+ }),
31
+ );
32
+ }
33
+ }
@@ -0,0 +1,32 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { globSync, readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const root = fileURLToPath(new URL('../', import.meta.url));
6
+ // Discover the opt-in suites from source so a new benchmark cannot silently
7
+ // miss a hand-maintained workflow list. Keep wall-clock measurements serial.
8
+ const files = globSync('test/**/*.test.ts', { cwd: root })
9
+ .filter((file) =>
10
+ /process\.env(?:\.GITNEXUS_BENCH|\[['"]GITNEXUS_BENCH['"]\])/.test(
11
+ readFileSync(new URL(`../${file.replaceAll('\\', '/')}`, import.meta.url), 'utf8'),
12
+ ),
13
+ )
14
+ .sort();
15
+ if (!files.length) throw new Error('No benchmark test suites discovered');
16
+ execFileSync(
17
+ process.execPath,
18
+ [
19
+ fileURLToPath(new URL('../node_modules/vitest/vitest.mjs', import.meta.url)),
20
+ 'run',
21
+ '--no-file-parallelism',
22
+ ...files,
23
+ '--reporter=default',
24
+ '--reporter=./scripts/execution-reporter.ts',
25
+ '--outputFile=benchmarks.json',
26
+ ],
27
+ {
28
+ cwd: root,
29
+ stdio: 'inherit',
30
+ env: { ...process.env, GITNEXUS_BENCH: '1' },
31
+ },
32
+ );
@@ -80,8 +80,19 @@ console.log(
80
80
  );
81
81
 
82
82
  const startedAt = Date.now();
83
+ const reportName = process.env.GITNEXUS_TEST_REPORT;
84
+ if (reportName && !/^[a-zA-Z0-9_-]+\.json$/.test(reportName)) {
85
+ throw new Error('GITNEXUS_TEST_REPORT must be a JSON basename');
86
+ }
87
+ const reportArgs = reportName
88
+ ? [
89
+ '--reporter=default',
90
+ '--reporter=./scripts/execution-reporter.ts',
91
+ `--outputFile=${reportName}`,
92
+ ]
93
+ : [];
83
94
  try {
84
- execFileSync('npx', ['vitest', 'run', ...files], {
95
+ execFileSync('npx', ['vitest', 'run', ...files, ...reportArgs], {
85
96
  cwd: ROOT,
86
97
  stdio: 'inherit',
87
98
  timeout: timeoutMs,
@@ -0,0 +1,203 @@
1
+ import { globSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import type { TestRunEndReason } from 'vitest/node';
5
+
6
+ type Assertion = {
7
+ ancestorTitles: string[];
8
+ title: string;
9
+ fullName: string;
10
+ status: string;
11
+ location?: { line: number; column: number };
12
+ };
13
+ type Suite = {
14
+ name: string;
15
+ status: string;
16
+ assertionResults: Assertion[];
17
+ endTime?: number;
18
+ message?: string;
19
+ };
20
+ type Report = {
21
+ success?: boolean;
22
+ numTotalTests?: number;
23
+ startTime?: number;
24
+ executionErrors: number;
25
+ executionReason: TestRunEndReason;
26
+ testResults: Suite[];
27
+ };
28
+
29
+ /** Stable across GitHub's Linux, macOS and Windows checkout roots. */
30
+ function testPath(name: string, packageName: string): string {
31
+ const normalized = name.replaceAll('\\', '/');
32
+ const relative =
33
+ normalized.match(new RegExp(`(?:^|/)${packageName}/((?:test|src)/.+)$`))?.[1] ?? normalized;
34
+ if (!/^(?:test|src)\/.+\.test\.[jt]sx?$/.test(relative)) {
35
+ throw new Error(`Invalid test report path: ${name}`);
36
+ }
37
+ return relative;
38
+ }
39
+
40
+ /** A skip is resolved only by a recorded pass of that same test in another job. */
41
+ export function reconcileTestReports(
42
+ inputs: unknown[],
43
+ expectedFiles: string[] = [],
44
+ packageName = 'gitnexus',
45
+ ) {
46
+ if (inputs.length === 0) throw new Error('No execution reports supplied');
47
+ const tests = new Map<
48
+ string,
49
+ { file: string; assertion: Assertion; passed: boolean; failed: boolean }
50
+ >();
51
+ const failures = new Set<string>();
52
+ const failedSuites = new Set<string>();
53
+ const startTimes: number[] = [];
54
+ const endTimes: number[] = [];
55
+ for (const [index, input] of inputs.entries()) {
56
+ const report = input as Report;
57
+ if (!report || !Array.isArray(report.testResults) || report.testResults.length === 0) {
58
+ throw new Error(`Missing or empty execution report ${index}`);
59
+ }
60
+ if (report.success === false) failures.add(`Report ${index}: unsuccessful runner result`);
61
+ if (
62
+ !Number.isInteger(report.executionErrors) ||
63
+ report.executionErrors < 0 ||
64
+ !['passed', 'failed', 'interrupted'].includes(report.executionReason)
65
+ ) {
66
+ throw new Error(`Report ${index}: missing execution reporter evidence`);
67
+ }
68
+ if (report.executionErrors || report.executionReason !== 'passed') {
69
+ failures.add(
70
+ `Report ${index}: ${report.executionErrors} unhandled errors; ${report.executionReason}`,
71
+ );
72
+ }
73
+ if (typeof report.startTime === 'number') startTimes.push(report.startTime);
74
+ let count = 0;
75
+ const seen = new Set<string>();
76
+ for (const suite of report.testResults) {
77
+ const file = testPath(suite.name, packageName);
78
+ if (!Array.isArray(suite.assertionResults) || suite.assertionResults.length === 0) {
79
+ throw new Error(`No collected tests in ${file}`);
80
+ }
81
+ if (suite.status === 'failed') {
82
+ failures.add(`${file}: failed suite`);
83
+ failedSuites.add(file);
84
+ }
85
+ if (typeof suite.endTime === 'number') endTimes.push(suite.endTime);
86
+ for (const assertion of suite.assertionResults) {
87
+ if (
88
+ !['passed', 'failed', 'pending', 'skipped', 'todo', 'disabled'].includes(
89
+ assertion.status,
90
+ ) ||
91
+ !Array.isArray(assertion.ancestorTitles) ||
92
+ typeof assertion.title !== 'string' ||
93
+ (assertion.location !== undefined &&
94
+ (!Number.isInteger(assertion.location.line) ||
95
+ !Number.isInteger(assertion.location.column)))
96
+ ) {
97
+ throw new Error(`Malformed assertion in ${file}`);
98
+ }
99
+ // Collection order differs across platforms. Source location, not an
100
+ // ordinal, distinguishes equal titles declared at different locations.
101
+ // Helper-generated tests may have no location; their complete title
102
+ // must then be unique within the file (the duplicate check still applies).
103
+ const title = JSON.stringify([...assertion.ancestorTitles, assertion.title]);
104
+ const key = JSON.stringify([
105
+ file,
106
+ title,
107
+ assertion.location?.line ?? null,
108
+ assertion.location?.column ?? null,
109
+ ]);
110
+ if (seen.has(key))
111
+ throw new Error(
112
+ `Ambiguous test identity in ${file}: ${title}; give parameterized cases unique titles`,
113
+ );
114
+ seen.add(key);
115
+ const result = tests.get(key) ?? { file, assertion, passed: false, failed: false };
116
+ result.passed ||= assertion.status === 'passed';
117
+ result.failed ||= assertion.status === 'failed';
118
+ tests.set(key, result);
119
+ if (result.failed) failures.add(`${file}: ${assertion.fullName ?? title}`);
120
+ count++;
121
+ }
122
+ }
123
+ if (typeof report.numTotalTests === 'number' && report.numTotalTests !== count) {
124
+ throw new Error(`Report ${index}: expected ${report.numTotalTests} tests, found ${count}`);
125
+ }
126
+ }
127
+ const unverified = [...tests.values()]
128
+ .filter((test) => !test.passed && !test.failed)
129
+ .map((test) => `${test.file}: ${test.assertion.fullName}`);
130
+ const suites = new Map<string, Suite>();
131
+ for (const test of tests.values()) {
132
+ const status = test.failed ? 'failed' : test.passed ? 'passed' : 'pending';
133
+ const suite = suites.get(test.file) ?? {
134
+ name: test.file,
135
+ status: 'passed',
136
+ assertionResults: [],
137
+ };
138
+ suite.assertionResults.push({ ...test.assertion, status });
139
+ if (status === 'failed' || failedSuites.has(test.file)) suite.status = 'failed';
140
+ suites.set(test.file, suite);
141
+ }
142
+ for (const file of expectedFiles) {
143
+ if (!suites.has(file.replaceAll('\\', '/')))
144
+ throw new Error(`Test file was never collected: ${file}`);
145
+ }
146
+ const passed = [...tests.values()].filter((test) => test.passed && !test.failed).length;
147
+ const endTime = endTimes.length ? Math.max(...endTimes) : 0;
148
+ return {
149
+ total: tests.size,
150
+ passed,
151
+ unverified,
152
+ failures: [...failures],
153
+ report: {
154
+ success: failures.size === 0 && unverified.length === 0,
155
+ numTotalTests: tests.size,
156
+ numPassedTests: passed,
157
+ numFailedTests: [...tests.values()].filter((test) => test.failed).length,
158
+ numPendingTests: unverified.length,
159
+ numTotalTestSuites: suites.size,
160
+ numFailedTestSuites: failedSuites.size,
161
+ executionFailures: [...failures],
162
+ startTime: startTimes.length ? Math.min(...startTimes) : 0,
163
+ testResults: [...suites.values()].map((suite) => ({ ...suite, endTime })),
164
+ },
165
+ };
166
+ }
167
+
168
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
169
+ const [directory, output, shardsText] = process.argv.slice(2);
170
+ const shards = Number(shardsText);
171
+ if (!directory || !output || !Number.isInteger(shards) || shards < 1) {
172
+ throw new Error('Usage: test-completeness.ts <reports-dir> <output.json> <platform-shards>');
173
+ }
174
+ const names = ['coverage.json', 'benchmarks.json', 'preflight.json'];
175
+ for (const os of ['windows-latest', 'macos-latest']) {
176
+ for (let shard = 1; shard <= shards; shard++) names.push(`${os}-${shard}.json`);
177
+ }
178
+ // Exact expected receipts: an absent/cancelled job is never an empty success.
179
+ const root = fileURLToPath(new URL('../', import.meta.url));
180
+ const expected = globSync('test/**/*.test.ts', { cwd: root });
181
+ const result = reconcileTestReports(
182
+ names.map((name) => JSON.parse(readFileSync(path.join(directory, name), 'utf8'))),
183
+ expected,
184
+ );
185
+ writeFileSync(output, JSON.stringify(result.report));
186
+ console.log(
187
+ `Test execution: ${result.passed}/${result.total} passed; ${result.unverified.length} unverified; ${result.failures.length} failures`,
188
+ );
189
+ for (const message of [...result.unverified, ...result.failures]) console.error(message);
190
+ if (!result.report.success) process.exitCode = 1;
191
+ const webPath = path.join(root, '../gitnexus-web/web-test-results.json');
192
+ const web = reconcileTestReports(
193
+ [JSON.parse(readFileSync(webPath, 'utf8'))],
194
+ globSync('{test,src}/**/*.test.{ts,tsx}', { cwd: path.join(root, '../gitnexus-web') }),
195
+ 'gitnexus-web',
196
+ );
197
+ writeFileSync(webPath, JSON.stringify(web.report));
198
+ console.log(
199
+ `Web execution: ${web.passed}/${web.total} passed; ${web.unverified.length} unverified; ${web.failures.length} failures`,
200
+ );
201
+ for (const message of [...web.unverified, ...web.failures]) console.error(message);
202
+ if (!web.report.success) process.exitCode = 1;
203
+ }