@veewo/gitnexus 1.3.10 → 1.3.11

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 (109) hide show
  1. package/README.md +3 -3
  2. package/dist/benchmark/analyze-memory-sampler.d.ts +10 -0
  3. package/dist/benchmark/analyze-memory-sampler.js +12 -0
  4. package/dist/benchmark/analyze-memory-sampler.test.d.ts +1 -0
  5. package/dist/benchmark/analyze-memory-sampler.test.js +12 -0
  6. package/dist/benchmark/io.test.js +48 -5
  7. package/dist/benchmark/u2-e2e/config.d.ts +1 -0
  8. package/dist/benchmark/u2-e2e/retrieval-runner.js +25 -3
  9. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +44 -1
  10. package/dist/benchmark/unity-lazy-context-sampler.d.ts +58 -0
  11. package/dist/benchmark/unity-lazy-context-sampler.js +217 -0
  12. package/dist/benchmark/unity-lazy-context-sampler.test.d.ts +1 -0
  13. package/dist/benchmark/unity-lazy-context-sampler.test.js +32 -0
  14. package/dist/cli/analyze-close-policy.d.ts +5 -0
  15. package/dist/cli/analyze-close-policy.js +9 -0
  16. package/dist/cli/analyze-close-policy.test.d.ts +1 -0
  17. package/dist/cli/analyze-close-policy.test.js +12 -0
  18. package/dist/cli/analyze-runtime-summary.d.ts +2 -0
  19. package/dist/cli/analyze-runtime-summary.js +9 -0
  20. package/dist/cli/analyze-runtime-summary.test.d.ts +1 -0
  21. package/dist/cli/analyze-runtime-summary.test.js +14 -0
  22. package/dist/cli/analyze.js +42 -15
  23. package/dist/cli/eval-server.js +3 -0
  24. package/dist/cli/exit-code.d.ts +13 -0
  25. package/dist/cli/exit-code.js +25 -0
  26. package/dist/cli/exit-code.test.d.ts +1 -0
  27. package/dist/cli/exit-code.test.js +28 -0
  28. package/dist/cli/index.js +8 -2
  29. package/dist/cli/mcp.js +3 -0
  30. package/dist/cli/setup.js +3 -2
  31. package/dist/cli/setup.test.js +67 -0
  32. package/dist/cli/tool.d.ts +3 -1
  33. package/dist/cli/tool.js +2 -0
  34. package/dist/core/graph/types.d.ts +1 -1
  35. package/dist/core/ingestion/filesystem-walker.d.ts +6 -0
  36. package/dist/core/ingestion/filesystem-walker.js +17 -0
  37. package/dist/core/ingestion/filesystem-walker.test.d.ts +1 -0
  38. package/dist/core/ingestion/filesystem-walker.test.js +51 -0
  39. package/dist/core/ingestion/pipeline.js +4 -3
  40. package/dist/core/ingestion/unity-parity-seed.d.ts +9 -0
  41. package/dist/core/ingestion/unity-parity-seed.js +69 -0
  42. package/dist/core/ingestion/unity-parity-seed.test.d.ts +1 -0
  43. package/dist/core/ingestion/unity-parity-seed.test.js +35 -0
  44. package/dist/core/ingestion/unity-resource-processor.d.ts +2 -0
  45. package/dist/core/ingestion/unity-resource-processor.js +87 -53
  46. package/dist/core/ingestion/unity-resource-processor.test.js +37 -39
  47. package/dist/core/kuzu/csv-generator.d.ts +20 -1
  48. package/dist/core/kuzu/csv-generator.js +92 -25
  49. package/dist/core/kuzu/csv-generator.test.d.ts +1 -0
  50. package/dist/core/kuzu/csv-generator.test.js +28 -0
  51. package/dist/core/kuzu/kuzu-adapter.js +35 -54
  52. package/dist/core/kuzu/relationship-pair-buckets.d.ts +17 -0
  53. package/dist/core/kuzu/relationship-pair-buckets.js +79 -0
  54. package/dist/core/kuzu/relationship-pair-buckets.test.d.ts +1 -0
  55. package/dist/core/kuzu/relationship-pair-buckets.test.js +10 -0
  56. package/dist/core/kuzu/schema.d.ts +1 -1
  57. package/dist/core/kuzu/schema.js +1 -0
  58. package/dist/core/unity/options.d.ts +2 -0
  59. package/dist/core/unity/options.js +9 -0
  60. package/dist/core/unity/options.test.js +8 -1
  61. package/dist/core/unity/resolver.d.ts +3 -0
  62. package/dist/core/unity/resolver.js +56 -2
  63. package/dist/core/unity/resolver.test.js +46 -0
  64. package/dist/core/unity/scan-context.d.ts +5 -0
  65. package/dist/core/unity/scan-context.js +133 -44
  66. package/dist/core/unity/scan-context.test.js +41 -2
  67. package/dist/core/unity/serialized-type-index.d.ts +5 -0
  68. package/dist/core/unity/serialized-type-index.js +44 -13
  69. package/dist/core/unity/serialized-type-index.test.js +9 -1
  70. package/dist/mcp/local/local-backend.d.ts +16 -0
  71. package/dist/mcp/local/local-backend.js +320 -4
  72. package/dist/mcp/local/local-backend.unity-merge.test.d.ts +1 -0
  73. package/dist/mcp/local/local-backend.unity-merge.test.js +261 -0
  74. package/dist/mcp/local/unity-enrichment.d.ts +15 -0
  75. package/dist/mcp/local/unity-enrichment.js +69 -5
  76. package/dist/mcp/local/unity-enrichment.test.js +69 -1
  77. package/dist/mcp/local/unity-lazy-config.d.ts +6 -0
  78. package/dist/mcp/local/unity-lazy-config.js +7 -0
  79. package/dist/mcp/local/unity-lazy-config.test.d.ts +1 -0
  80. package/dist/mcp/local/unity-lazy-config.test.js +9 -0
  81. package/dist/mcp/local/unity-lazy-hydrator.d.ts +15 -0
  82. package/dist/mcp/local/unity-lazy-hydrator.js +43 -0
  83. package/dist/mcp/local/unity-lazy-hydrator.test.d.ts +1 -0
  84. package/dist/mcp/local/unity-lazy-hydrator.test.js +66 -0
  85. package/dist/mcp/local/unity-lazy-overlay.d.ts +3 -0
  86. package/dist/mcp/local/unity-lazy-overlay.js +89 -0
  87. package/dist/mcp/local/unity-lazy-overlay.test.d.ts +1 -0
  88. package/dist/mcp/local/unity-lazy-overlay.test.js +83 -0
  89. package/dist/mcp/local/unity-parity-cache.d.ts +7 -0
  90. package/dist/mcp/local/unity-parity-cache.js +88 -0
  91. package/dist/mcp/local/unity-parity-cache.test.d.ts +1 -0
  92. package/dist/mcp/local/unity-parity-cache.test.js +143 -0
  93. package/dist/mcp/local/unity-parity-seed-loader.d.ts +2 -0
  94. package/dist/mcp/local/unity-parity-seed-loader.js +30 -0
  95. package/dist/mcp/local/unity-parity-seed-loader.test.d.ts +1 -0
  96. package/dist/mcp/local/unity-parity-seed-loader.test.js +25 -0
  97. package/dist/mcp/local/unity-parity-warmup-queue.d.ts +6 -0
  98. package/dist/mcp/local/unity-parity-warmup-queue.js +28 -0
  99. package/dist/mcp/local/unity-parity-warmup-queue.test.d.ts +1 -0
  100. package/dist/mcp/local/unity-parity-warmup-queue.test.js +15 -0
  101. package/dist/mcp/tools.js +24 -2
  102. package/dist/types/pipeline.d.ts +7 -0
  103. package/package.json +4 -1
  104. package/skills/gitnexus-cli.md +18 -0
  105. package/skills/gitnexus-debugging.md +16 -2
  106. package/skills/gitnexus-exploring.md +15 -1
  107. package/skills/gitnexus-guide.md +15 -0
  108. package/skills/gitnexus-impact-analysis.md +2 -0
  109. package/skills/gitnexus-refactoring.md +5 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Works with **Cursor**, **Claude Code**, **Codex**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
6
6
 
7
- [![npm version](https://img.shields.io/npm/v/gitnexus.svg)](https://www.npmjs.com/package/gitnexus)
7
+ [![npm version](https://img.shields.io/npm/v/%40veewo%2Fgitnexus.svg)](https://www.npmjs.com/package/@veewo/gitnexus)
8
8
  [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm%20Noncommercial-blue.svg)](https://polyformproject.org/licenses/noncommercial/1.0.0/)
9
9
 
10
10
  ---
@@ -19,12 +19,12 @@ AI coding tools don't understand your codebase structure. They edit a function w
19
19
 
20
20
  ```bash
21
21
  # Index your repo (run from repo root)
22
- npx gitnexus analyze
22
+ npx -y @veewo/gitnexus analyze
23
23
  ```
24
24
 
25
25
  That's it. This indexes the codebase, updates `AGENTS.md` / `CLAUDE.md` context files, and (when using project scope) installs repo-local agent skills.
26
26
 
27
- To configure MCP + skills, run `npx gitnexus setup --agent <claude|opencode|codex>` once (default global mode), or add `--scope project` for project-local mode.
27
+ To configure MCP + skills, run `npx -y @veewo/gitnexus setup --agent <claude|opencode|codex>` once (default global mode), or add `--scope project` for project-local mode.
28
28
 
29
29
  `gitnexus setup` requires an agent selection:
30
30
  - `--agent claude`: configure Claude MCP only
@@ -0,0 +1,10 @@
1
+ export declare function buildAnalyzeMemoryReport(input: any): {
2
+ capturedAt: string;
3
+ summary: {
4
+ analyzeRealSec: any;
5
+ analyzeMaxRssBytes: any;
6
+ coldResourceBindings: any;
7
+ warmResourceBindings: any;
8
+ };
9
+ input: any;
10
+ };
@@ -0,0 +1,12 @@
1
+ export function buildAnalyzeMemoryReport(input) {
2
+ return {
3
+ capturedAt: new Date().toISOString(),
4
+ summary: {
5
+ analyzeRealSec: input.analyze.realSec,
6
+ analyzeMaxRssBytes: input.analyze.maxRssBytes,
7
+ coldResourceBindings: input.queryCold.resourceBindings,
8
+ warmResourceBindings: input.queryWarm.resourceBindings,
9
+ },
10
+ input,
11
+ };
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildAnalyzeMemoryReport } from './analyze-memory-sampler.js';
4
+ test('buildAnalyzeMemoryReport summarizes analyze and query measurements', () => {
5
+ const report = buildAnalyzeMemoryReport({
6
+ analyze: { realSec: 10, maxRssBytes: 1024, phases: { pipelineSec: 3, kuzuSec: 5, ftsSec: 1 } },
7
+ queryCold: { realSec: 2, maxRssBytes: 512, resourceBindings: 4, unityDiagnostics: [] },
8
+ queryWarm: { realSec: 1, maxRssBytes: 256, resourceBindings: 4, unityDiagnostics: [] },
9
+ });
10
+ assert.equal(report.summary.analyzeRealSec, 10);
11
+ assert.equal(report.summary.coldResourceBindings, 4);
12
+ });
@@ -1,9 +1,14 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import path from 'node:path';
4
+ import fs from 'node:fs/promises';
5
+ import os from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
4
7
  import { loadBenchmarkDataset } from './io.js';
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ const projectRoot = path.resolve(here, '../..');
5
10
  test('loadBenchmarkDataset parses thresholds and jsonl rows', async () => {
6
- const root = path.resolve('../benchmarks/unity-baseline/v1');
11
+ const root = path.resolve(projectRoot, '../benchmarks/unity-baseline/v1');
7
12
  const ds = await loadBenchmarkDataset(root);
8
13
  assert.equal(typeof ds.thresholds.query.precisionMin, 'number');
9
14
  assert.ok(ds.symbols.length > 0);
@@ -11,11 +16,41 @@ test('loadBenchmarkDataset parses thresholds and jsonl rows', async () => {
11
16
  assert.ok(ds.tasks.length > 0);
12
17
  });
13
18
  test('loadBenchmarkDataset rejects missing required fields', async () => {
14
- const badRoot = path.resolve('src/benchmark/__fixtures__/bad-dataset');
15
- await assert.rejects(() => loadBenchmarkDataset(badRoot), /missing required field/i);
19
+ const badRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-bad-dataset-'));
20
+ try {
21
+ await fs.writeFile(path.join(badRoot, 'thresholds.json'), JSON.stringify({
22
+ query: { precisionMin: 0.7, recallMin: 0.7, avgLatencyMsMax: 200, p95LatencyMsMax: 400 },
23
+ context: { coverageMin: 0.7, latencyMsMax: 400 },
24
+ impact: { recallMin: 0.7, avgLatencyMsMax: 400 },
25
+ }), 'utf-8');
26
+ await fs.writeFile(path.join(badRoot, 'symbols.jsonl'), `${JSON.stringify({
27
+ symbol_uid: 'Class:Foo',
28
+ file_path: 'Assets/Foo.cs',
29
+ symbol_name: 'Foo',
30
+ symbol_type: 'Class',
31
+ start_line: 1,
32
+ end_line: 10,
33
+ })}\n`, 'utf-8');
34
+ await fs.writeFile(path.join(badRoot, 'relations.jsonl'), `${JSON.stringify({
35
+ src_uid: 'Class:Foo',
36
+ edge_type: 'CALLS',
37
+ dst_uid: 'Method:Bar',
38
+ must_exist: true,
39
+ })}\n`, 'utf-8');
40
+ await fs.writeFile(path.join(badRoot, 'tasks.jsonl'), `${JSON.stringify({
41
+ tool: 'query',
42
+ input: {},
43
+ must_hit_uids: [],
44
+ // intentionally omit must_not_hit_uids
45
+ })}\n`, 'utf-8');
46
+ await assert.rejects(() => loadBenchmarkDataset(badRoot), /missing required field/i);
47
+ }
48
+ finally {
49
+ await fs.rm(badRoot, { recursive: true, force: true });
50
+ }
16
51
  });
17
52
  test('loadBenchmarkDataset parses neonspark-v1 dataset', async () => {
18
- const root = path.resolve('../benchmarks/unity-baseline/neonspark-v1');
53
+ const root = path.resolve(projectRoot, '../benchmarks/unity-baseline/neonspark-v1');
19
54
  const ds = await loadBenchmarkDataset(root);
20
55
  assert.equal(ds.symbols.length, 20);
21
56
  assert.ok(ds.relations.length > 0);
@@ -24,7 +59,7 @@ test('loadBenchmarkDataset parses neonspark-v1 dataset', async () => {
24
59
  assert.ok(ds.tasks.some((t) => t.tool === 'impact'));
25
60
  });
26
61
  test('loadBenchmarkDataset parses neonspark-v2 dataset', async () => {
27
- const root = path.resolve('../benchmarks/unity-baseline/neonspark-v2');
62
+ const root = path.resolve(projectRoot, '../benchmarks/unity-baseline/neonspark-v2');
28
63
  const ds = await loadBenchmarkDataset(root);
29
64
  assert.ok(ds.symbols.length >= 40 && ds.symbols.length <= 60);
30
65
  assert.ok(ds.relations.length > 0);
@@ -33,3 +68,11 @@ test('loadBenchmarkDataset parses neonspark-v2 dataset', async () => {
33
68
  assert.ok(ds.tasks.some((t) => t.tool === 'context'));
34
69
  assert.ok(ds.tasks.some((t) => t.tool === 'impact'));
35
70
  });
71
+ test('latest unity hydration gate report includes hydrationMetaSummary schema', async () => {
72
+ const reportPath = path.resolve(projectRoot, 'docs/reports/2026-03-15-unity-hydration-gates.json');
73
+ const raw = await fs.readFile(reportPath, 'utf-8');
74
+ const report = JSON.parse(raw);
75
+ assert.ok(report.hydrationMetaSummary);
76
+ assert.equal(typeof report.hydrationMetaSummary.compactNeedsRetryRate, 'number');
77
+ assert.equal(typeof report.hydrationMetaSummary.parityCompleteRate, 'number');
78
+ });
@@ -17,6 +17,7 @@ export interface SymbolScenario {
17
17
  kind: 'component' | 'scriptableobject' | 'serializable-class' | 'partial-component';
18
18
  objectives: string[];
19
19
  contextFileHint?: string;
20
+ contextUnityHydration?: 'compact' | 'parity';
20
21
  deepDivePlan: Array<{
21
22
  tool: 'query' | 'context' | 'impact' | 'cypher';
22
23
  input: Record<string, unknown>;
@@ -53,12 +53,29 @@ function hasDeepDiveEvidence(output) {
53
53
  const outgoingRefs = countRefs(output?.outgoing);
54
54
  return processSymbols + definitions + candidates + rows + byDepth + impacted + incomingRefs + outgoingRefs > 0;
55
55
  }
56
- function assertScenario(scenario, contextOnOutput, deepDiveOutputs) {
56
+ function assertScenario(scenario, contextOnOutput, deepDiveOutputs, contextUnityHydration) {
57
57
  const failures = [];
58
58
  const bindings = Array.isArray(contextOnOutput?.resourceBindings) ? contextOnOutput.resourceBindings : [];
59
59
  const hasBindings = bindings.length > 0;
60
60
  const hasResolvedReferences = bindings.some((binding) => Array.isArray(binding?.resolvedReferences) && binding.resolvedReferences.length > 0);
61
61
  const hasAssetTypeBinding = bindings.some((binding) => typeof binding?.resourceType === 'string' && binding.resourceType.length > 0);
62
+ const hydrationMeta = contextOnOutput?.hydrationMeta && typeof contextOnOutput.hydrationMeta === 'object'
63
+ ? contextOnOutput.hydrationMeta
64
+ : null;
65
+ if (!hydrationMeta) {
66
+ failures.push(`${scenario.symbol}: context(on) must include hydrationMeta`);
67
+ }
68
+ else if (contextUnityHydration === 'compact') {
69
+ if (typeof hydrationMeta.needsParityRetry !== 'boolean') {
70
+ failures.push(`${scenario.symbol}: context(on) hydrationMeta.needsParityRetry must be boolean`);
71
+ }
72
+ if (hydrationMeta.isComplete === false && hydrationMeta.needsParityRetry !== true) {
73
+ failures.push(`${scenario.symbol}: context(on) incomplete compact response must set hydrationMeta.needsParityRetry=true`);
74
+ }
75
+ }
76
+ else if (hydrationMeta.isComplete !== true) {
77
+ failures.push(`${scenario.symbol}: context(on) parity response must set hydrationMeta.isComplete=true`);
78
+ }
62
79
  if (scenario.symbol === 'MainUIManager' || scenario.symbol === 'PlayerActor') {
63
80
  if (!hasBindings) {
64
81
  failures.push(`${scenario.symbol}: context(on) must include resourceBindings`);
@@ -142,7 +159,12 @@ export async function runSymbolScenario(runner, scenario, repo) {
142
159
  const t0 = performance.now();
143
160
  const contextOff = await runContextWithDisambiguation(runner, scenario, contextOffInput);
144
161
  steps.push(buildMetric('context-off', 'context', performance.now() - t0, contextOffInput, contextOff));
145
- const contextOnInput = { ...baseContextInput, unity_resources: 'on' };
162
+ const contextUnityHydration = scenario.contextUnityHydration === 'parity' ? 'parity' : 'compact';
163
+ const contextOnInput = {
164
+ ...baseContextInput,
165
+ unity_resources: 'on',
166
+ unity_hydration_mode: contextUnityHydration,
167
+ };
146
168
  const t1 = performance.now();
147
169
  const contextOn = await runContextWithDisambiguation(runner, scenario, contextOnInput);
148
170
  steps.push(buildMetric('context-on', 'context', performance.now() - t1, contextOnInput, contextOn));
@@ -161,6 +183,6 @@ export async function runSymbolScenario(runner, scenario, repo) {
161
183
  return {
162
184
  symbol: scenario.symbol,
163
185
  steps,
164
- assertions: assertScenario(scenario, contextOn, deepDiveOutputs),
186
+ assertions: assertScenario(scenario, contextOn, deepDiveOutputs, contextUnityHydration),
165
187
  };
166
188
  }
@@ -8,6 +8,12 @@ test('runSymbolScenario executes context off/on + deepDive and records metrics',
8
8
  if (input.unity_resources === 'on') {
9
9
  return {
10
10
  status: 'found',
11
+ hydrationMeta: {
12
+ requestedMode: 'compact',
13
+ effectiveMode: 'compact',
14
+ isComplete: false,
15
+ needsParityRetry: true,
16
+ },
11
17
  resourceBindings: [
12
18
  {
13
19
  resourcePath: 'Assets/Prefabs/UI.prefab',
@@ -36,7 +42,11 @@ test('runSymbolScenario executes context off/on + deepDive and records metrics',
36
42
  });
37
43
  test('AssetRef requires context(on) resourceBindings after serializable-class coverage', async () => {
38
44
  const noEvidenceRunner = {
39
- context: async () => ({ status: 'found', resourceBindings: [] }),
45
+ context: async () => ({
46
+ status: 'found',
47
+ hydrationMeta: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false, needsParityRetry: true },
48
+ resourceBindings: [],
49
+ }),
40
50
  query: async () => ({ process_symbols: [] }),
41
51
  impact: async () => ({ impactedCount: 0 }),
42
52
  cypher: async () => ({ rows: [] }),
@@ -54,6 +64,7 @@ test('AssetRef requires deep-dive evidence even when context(on) has resourceBin
54
64
  const noDeepDiveEvidenceRunner = {
55
65
  context: async () => ({
56
66
  status: 'found',
67
+ hydrationMeta: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false, needsParityRetry: true },
57
68
  resourceBindings: [{ resourcePath: 'Assets/Data/Unlock.asset', resourceType: 'asset' }],
58
69
  }),
59
70
  query: async () => ({ process_symbols: [] }),
@@ -73,6 +84,7 @@ test('AssetRef passes when context(on) bindings and deep-dive evidence are both
73
84
  const satisfiedRunner = {
74
85
  context: async () => ({
75
86
  status: 'found',
87
+ hydrationMeta: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false, needsParityRetry: true },
76
88
  resourceBindings: [{ resourcePath: 'Assets/Data/Unlock.asset', resourceType: 'asset' }],
77
89
  }),
78
90
  query: async () => ({ process_symbols: [{ id: 'Class:Assets/Scripts/UnlockContent.cs:UnlockContent' }] }),
@@ -107,6 +119,12 @@ test('runSymbolScenario retries context with file hint when response is ambiguou
107
119
  if (input.file_path === hint) {
108
120
  return {
109
121
  status: 'found',
122
+ hydrationMeta: {
123
+ requestedMode: 'compact',
124
+ effectiveMode: 'compact',
125
+ isComplete: false,
126
+ needsParityRetry: true,
127
+ },
110
128
  resourceBindings: [
111
129
  {
112
130
  resourcePath: 'Assets/Prefabs/Player.prefab',
@@ -143,3 +161,28 @@ test('runSymbolScenario retries context with file hint when response is ambiguou
143
161
  assert.equal(out.steps[1]?.output?.status, 'found');
144
162
  assert.equal(out.assertions.pass, true);
145
163
  });
164
+ test('runSymbolScenario fails when compact context hydrationMeta.needsParityRetry is missing', async () => {
165
+ const runner = {
166
+ context: async (input) => {
167
+ if (input.unity_resources === 'on') {
168
+ return {
169
+ status: 'found',
170
+ hydrationMeta: { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false },
171
+ resourceBindings: [{ resourcePath: 'Assets/Prefabs/A.prefab', resourceType: 'prefab' }],
172
+ };
173
+ }
174
+ return { status: 'found' };
175
+ },
176
+ query: async () => ({ process_symbols: [{ id: 'Class:A' }] }),
177
+ impact: async () => ({ impactedCount: 1 }),
178
+ cypher: async () => ({ rows: [] }),
179
+ };
180
+ const out = await runSymbolScenario(runner, {
181
+ symbol: 'MainUIManager',
182
+ kind: 'component',
183
+ objectives: ['verify hydration contract'],
184
+ deepDivePlan: [{ tool: 'query', input: { query: 'MainUIManager' } }],
185
+ });
186
+ assert.equal(out.assertions.pass, false);
187
+ assert.ok(out.assertions.failures.some((f) => f.includes('hydrationMeta.needsParityRetry')));
188
+ });
@@ -0,0 +1,58 @@
1
+ export interface UnityLazyContextMetrics {
2
+ coldMs: number;
3
+ warmMs: number;
4
+ coldMaxRssBytes: number;
5
+ warmMaxRssBytes: number;
6
+ }
7
+ export interface UnityLazyContextThresholds {
8
+ coldMsMax?: number;
9
+ warmMsMax?: number;
10
+ coldMaxRssBytesMax?: number;
11
+ warmMaxRssBytesMax?: number;
12
+ }
13
+ export interface UnityLazyThresholdVerdict {
14
+ pass: boolean;
15
+ checks: Record<string, {
16
+ pass: boolean;
17
+ actual: number;
18
+ expected: number;
19
+ }>;
20
+ }
21
+ export interface UnityLazyContextSample {
22
+ durationMs: number;
23
+ maxRssBytes: number;
24
+ exitCode: number;
25
+ stdout: string;
26
+ stderr: string;
27
+ hydrationMeta?: {
28
+ requestedMode?: string;
29
+ effectiveMode?: string;
30
+ isComplete?: boolean;
31
+ needsParityRetry?: boolean;
32
+ };
33
+ }
34
+ export interface UnityHydrationMetaSummary {
35
+ compactSamples: number;
36
+ paritySamples: number;
37
+ compactNeedsRetryRate: number;
38
+ parityCompleteRate: number;
39
+ }
40
+ export interface UnityLazyContextSamplerConfig {
41
+ targetPath: string;
42
+ repo: string;
43
+ symbol: string;
44
+ file: string;
45
+ unityHydration?: 'compact' | 'parity';
46
+ thresholds?: UnityLazyContextThresholds;
47
+ }
48
+ export type UnityLazyContextRunner = (input: UnityLazyContextSamplerConfig & {
49
+ warm: boolean;
50
+ }) => Promise<UnityLazyContextSample>;
51
+ export declare function evaluateUnityLazyContextThresholds(metrics: UnityLazyContextMetrics, thresholds?: UnityLazyContextThresholds): UnityLazyThresholdVerdict;
52
+ export declare function runUnityLazyContextSampler(runner: UnityLazyContextRunner, config: UnityLazyContextSamplerConfig): Promise<{
53
+ capturedAt: string;
54
+ config: Omit<UnityLazyContextSamplerConfig, 'thresholds'>;
55
+ metrics: UnityLazyContextMetrics;
56
+ hydrationMetaSummary: UnityHydrationMetaSummary;
57
+ thresholdVerdict: UnityLazyThresholdVerdict;
58
+ }>;
@@ -0,0 +1,217 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ export function evaluateUnityLazyContextThresholds(metrics, thresholds) {
6
+ const verdict = { pass: true, checks: {} };
7
+ if (!thresholds) {
8
+ return verdict;
9
+ }
10
+ const checks = [
11
+ ['coldMs', metrics.coldMs, thresholds.coldMsMax],
12
+ ['warmMs', metrics.warmMs, thresholds.warmMsMax],
13
+ ['coldMaxRssBytes', metrics.coldMaxRssBytes, thresholds.coldMaxRssBytesMax],
14
+ ['warmMaxRssBytes', metrics.warmMaxRssBytes, thresholds.warmMaxRssBytesMax],
15
+ ];
16
+ for (const [name, actual, expected] of checks) {
17
+ if (typeof expected !== 'number')
18
+ continue;
19
+ const pass = actual <= expected;
20
+ verdict.checks[name] = { pass, actual, expected };
21
+ if (!pass)
22
+ verdict.pass = false;
23
+ }
24
+ return verdict;
25
+ }
26
+ export async function runUnityLazyContextSampler(runner, config) {
27
+ const cold = await runner({ ...config, warm: false });
28
+ if (cold.exitCode !== 0) {
29
+ throw new Error(`Cold run failed: ${cold.stderr || cold.stdout}`);
30
+ }
31
+ const warm = await runner({ ...config, warm: true });
32
+ if (warm.exitCode !== 0) {
33
+ throw new Error(`Warm run failed: ${warm.stderr || warm.stdout}`);
34
+ }
35
+ const metrics = {
36
+ coldMs: round1(cold.durationMs),
37
+ warmMs: round1(warm.durationMs),
38
+ coldMaxRssBytes: cold.maxRssBytes,
39
+ warmMaxRssBytes: warm.maxRssBytes,
40
+ };
41
+ const hydrationMetaSummary = summarizeHydrationMeta([cold, warm]);
42
+ return {
43
+ capturedAt: new Date().toISOString(),
44
+ config: {
45
+ targetPath: config.targetPath,
46
+ repo: config.repo,
47
+ symbol: config.symbol,
48
+ file: config.file,
49
+ unityHydration: config.unityHydration || 'compact',
50
+ },
51
+ metrics,
52
+ hydrationMetaSummary,
53
+ thresholdVerdict: evaluateUnityLazyContextThresholds(metrics, config.thresholds),
54
+ };
55
+ }
56
+ async function runCliContextSample(input) {
57
+ const thisFile = fileURLToPath(import.meta.url);
58
+ const thisDir = path.dirname(thisFile);
59
+ const cliPath = path.resolve(thisDir, '../cli/index.js');
60
+ const args = [
61
+ '-l',
62
+ 'node',
63
+ cliPath,
64
+ 'context',
65
+ input.symbol,
66
+ '--repo',
67
+ input.repo,
68
+ '--file',
69
+ input.file,
70
+ '--unity-resources',
71
+ 'auto',
72
+ '--unity-hydration',
73
+ input.unityHydration || 'compact',
74
+ ];
75
+ const startedAt = Date.now();
76
+ const proc = spawn('/usr/bin/time', args, { cwd: input.targetPath, stdio: ['ignore', 'pipe', 'pipe'] });
77
+ let stdout = '';
78
+ let stderr = '';
79
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
80
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
81
+ const exitCode = await new Promise((resolve) => {
82
+ proc.on('close', (code) => resolve(code ?? 1));
83
+ });
84
+ const rssMatch = stderr.match(/maximum resident set size[^0-9]*([0-9]+)|([0-9]+)\s+maximum resident set size/i);
85
+ const maxRssBytes = rssMatch ? Number(rssMatch[1] || rssMatch[2] || 0) : 0;
86
+ const parsedPayload = extractFirstJsonObject(stdout) || extractFirstJsonObject(stderr);
87
+ const hydrationMeta = parsedPayload && typeof parsedPayload === 'object' && parsedPayload.hydrationMeta
88
+ ? parsedPayload.hydrationMeta
89
+ : undefined;
90
+ return {
91
+ durationMs: Date.now() - startedAt,
92
+ maxRssBytes,
93
+ exitCode,
94
+ stdout,
95
+ stderr,
96
+ hydrationMeta,
97
+ };
98
+ }
99
+ function parseArgs(argv) {
100
+ const get = (name) => {
101
+ const index = argv.indexOf(name);
102
+ if (index === -1 || index + 1 >= argv.length)
103
+ return undefined;
104
+ return argv[index + 1];
105
+ };
106
+ const targetPath = get('--target-path');
107
+ const repo = get('--repo');
108
+ const symbol = get('--symbol');
109
+ const file = get('--file');
110
+ const unityHydrationRaw = String(get('--unity-hydration') || 'compact').trim().toLowerCase();
111
+ const unityHydration = unityHydrationRaw === 'parity' ? 'parity' : 'compact';
112
+ if (!targetPath)
113
+ throw new Error('Missing required arg: --target-path <path>');
114
+ if (!repo)
115
+ throw new Error('Missing required arg: --repo <repo>');
116
+ if (!symbol)
117
+ throw new Error('Missing required arg: --symbol <symbol>');
118
+ if (!file)
119
+ throw new Error('Missing required arg: --file <file>');
120
+ return {
121
+ targetPath: path.resolve(targetPath),
122
+ repo,
123
+ symbol,
124
+ file,
125
+ unityHydration,
126
+ thresholds: get('--thresholds') ? path.resolve(get('--thresholds')) : undefined,
127
+ report: get('--report') ? path.resolve(get('--report')) : undefined,
128
+ };
129
+ }
130
+ function round1(value) {
131
+ return Number(value.toFixed(1));
132
+ }
133
+ async function main() {
134
+ const args = parseArgs(process.argv.slice(2));
135
+ const thresholds = args.thresholds
136
+ ? JSON.parse(await fs.readFile(args.thresholds, 'utf-8'))
137
+ : undefined;
138
+ const report = await runUnityLazyContextSampler(runCliContextSample, {
139
+ targetPath: args.targetPath,
140
+ repo: args.repo,
141
+ symbol: args.symbol,
142
+ file: args.file,
143
+ unityHydration: args.unityHydration,
144
+ thresholds,
145
+ });
146
+ const payload = JSON.stringify(report, null, 2);
147
+ if (args.report) {
148
+ await fs.mkdir(path.dirname(args.report), { recursive: true });
149
+ await fs.writeFile(args.report, payload, 'utf-8');
150
+ console.log(`[unity-lazy-context-sampler] report written: ${args.report}`);
151
+ }
152
+ console.log(payload);
153
+ if (!report.thresholdVerdict.pass) {
154
+ process.exitCode = 1;
155
+ }
156
+ }
157
+ const modulePath = process.argv[1] ? path.resolve(process.argv[1]) : '';
158
+ if (import.meta.url === `file://${modulePath}`) {
159
+ main().catch((error) => {
160
+ console.error(`[unity-lazy-context-sampler] failed: ${error instanceof Error ? error.message : String(error)}`);
161
+ process.exitCode = 1;
162
+ });
163
+ }
164
+ function summarizeHydrationMeta(samples) {
165
+ let compactSamples = 0;
166
+ let compactNeedsRetry = 0;
167
+ let paritySamples = 0;
168
+ let parityComplete = 0;
169
+ for (const sample of samples) {
170
+ const mode = String(sample.hydrationMeta?.effectiveMode || '').toLowerCase();
171
+ if (mode === 'parity') {
172
+ paritySamples += 1;
173
+ if (sample.hydrationMeta?.isComplete === true) {
174
+ parityComplete += 1;
175
+ }
176
+ continue;
177
+ }
178
+ if (mode === 'compact') {
179
+ compactSamples += 1;
180
+ if (sample.hydrationMeta?.needsParityRetry === true) {
181
+ compactNeedsRetry += 1;
182
+ }
183
+ }
184
+ }
185
+ return {
186
+ compactSamples,
187
+ paritySamples,
188
+ compactNeedsRetryRate: compactSamples > 0 ? round1(compactNeedsRetry / compactSamples) : 0,
189
+ parityCompleteRate: paritySamples > 0 ? round1(parityComplete / paritySamples) : 0,
190
+ };
191
+ }
192
+ function extractFirstJsonObject(text) {
193
+ if (!text)
194
+ return null;
195
+ const start = text.indexOf('{');
196
+ if (start < 0)
197
+ return null;
198
+ let depth = 0;
199
+ for (let i = start; i < text.length; i += 1) {
200
+ const char = text[i];
201
+ if (char === '{')
202
+ depth += 1;
203
+ if (char === '}') {
204
+ depth -= 1;
205
+ if (depth === 0) {
206
+ const candidate = text.slice(start, i + 1);
207
+ try {
208
+ return JSON.parse(candidate);
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ return null;
217
+ }
@@ -0,0 +1,32 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { runUnityLazyContextSampler } from './unity-lazy-context-sampler.js';
4
+ test('sampler emits cold/warm latency and rss metrics with threshold verdict', async () => {
5
+ const fakeRunner = async ({ warm }) => ({
6
+ durationMs: warm ? 420 : 6200,
7
+ maxRssBytes: warm ? 650 * 1024 * 1024 : 1700 * 1024 * 1024,
8
+ exitCode: 0,
9
+ stdout: '',
10
+ stderr: '',
11
+ hydrationMeta: warm
12
+ ? { requestedMode: 'compact', effectiveMode: 'parity', isComplete: true, needsParityRetry: false }
13
+ : { requestedMode: 'compact', effectiveMode: 'compact', isComplete: false, needsParityRetry: true },
14
+ });
15
+ const report = await runUnityLazyContextSampler(fakeRunner, {
16
+ targetPath: '/tmp/repo',
17
+ repo: 'neonnew-core',
18
+ symbol: 'DoorObj',
19
+ file: 'Assets/NEON/Code/Game/Doors/DoorObj.cs',
20
+ thresholds: {
21
+ coldMsMax: 7000,
22
+ warmMsMax: 1000,
23
+ coldMaxRssBytesMax: 2 * 1024 * 1024 * 1024,
24
+ warmMaxRssBytesMax: 1 * 1024 * 1024 * 1024,
25
+ },
26
+ });
27
+ assert.ok(report.metrics.coldMs > 0);
28
+ assert.equal(typeof report.hydrationMetaSummary.compactNeedsRetryRate, 'number');
29
+ assert.equal(typeof report.hydrationMetaSummary.parityCompleteRate, 'number');
30
+ assert.ok(typeof report.thresholdVerdict.pass === 'boolean');
31
+ assert.equal(report.thresholdVerdict.pass, true);
32
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Kuzu native close may segfault on some macOS environments.
3
+ * We skip explicit close on analyze exit there, unless force-enabled.
4
+ */
5
+ export declare function shouldCloseKuzuOnAnalyzeExit(platform?: NodeJS.Platform, forceCloseOnExit?: string | undefined): boolean;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Kuzu native close may segfault on some macOS environments.
3
+ * We skip explicit close on analyze exit there, unless force-enabled.
4
+ */
5
+ export function shouldCloseKuzuOnAnalyzeExit(platform = process.platform, forceCloseOnExit = process.env.GITNEXUS_FORCE_KUZU_CLOSE_ON_EXIT) {
6
+ if (forceCloseOnExit === '1')
7
+ return true;
8
+ return platform !== 'darwin';
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { shouldCloseKuzuOnAnalyzeExit } from './analyze-close-policy.js';
4
+ test('shouldCloseKuzuOnAnalyzeExit skips close on darwin by default', () => {
5
+ assert.equal(shouldCloseKuzuOnAnalyzeExit('darwin', undefined), false);
6
+ });
7
+ test('shouldCloseKuzuOnAnalyzeExit closes on non-darwin platforms', () => {
8
+ assert.equal(shouldCloseKuzuOnAnalyzeExit('linux', undefined), true);
9
+ });
10
+ test('shouldCloseKuzuOnAnalyzeExit can be force-enabled on darwin', () => {
11
+ assert.equal(shouldCloseKuzuOnAnalyzeExit('darwin', '1'), true);
12
+ });
@@ -0,0 +1,2 @@
1
+ import type { PipelineResult, PipelineRuntimeSummary } from '../types/pipeline.js';
2
+ export declare function toPipelineRuntimeSummary(input: PipelineResult): PipelineRuntimeSummary;
@@ -0,0 +1,9 @@
1
+ export function toPipelineRuntimeSummary(input) {
2
+ return {
3
+ totalFileCount: input.totalFileCount,
4
+ communityResult: input.communityResult,
5
+ processResult: input.processResult,
6
+ unityResult: input.unityResult,
7
+ scopeDiagnostics: input.scopeDiagnostics,
8
+ };
9
+ }
@@ -0,0 +1 @@
1
+ export {};