@veewo/gitnexus 1.4.11-rc.2 → 1.5.0-rc

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 (139) hide show
  1. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +55 -0
  2. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +190 -0
  3. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.test.js +13 -0
  4. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.d.ts +22 -0
  5. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.js +100 -0
  6. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.d.ts +1 -0
  7. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.js +13 -0
  8. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +27 -0
  9. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +118 -0
  10. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.d.ts +1 -0
  11. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +16 -0
  12. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +60 -0
  13. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +331 -0
  14. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.d.ts +1 -0
  15. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +42 -0
  16. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.js +4 -4
  17. package/dist/benchmark/unity-lazy-context-sampler.d.ts +6 -0
  18. package/dist/benchmark/unity-lazy-context-sampler.js +49 -13
  19. package/dist/benchmark/unity-lazy-context-sampler.test.js +4 -0
  20. package/dist/cli/ai-context.js +6 -1
  21. package/dist/cli/eval-server.js +0 -3
  22. package/dist/cli/index.js +8 -0
  23. package/dist/cli/mcp.js +0 -3
  24. package/dist/cli/rule-lab.d.ts +42 -0
  25. package/dist/cli/rule-lab.js +157 -0
  26. package/dist/cli/rule-lab.test.d.ts +1 -0
  27. package/dist/cli/rule-lab.test.js +11 -0
  28. package/dist/cli/tool.d.ts +7 -1
  29. package/dist/cli/tool.js +6 -0
  30. package/dist/core/config/unity-config.d.ts +20 -0
  31. package/dist/core/config/unity-config.js +46 -0
  32. package/dist/core/graph/types.d.ts +1 -1
  33. package/dist/core/ingestion/pipeline.js +38 -13
  34. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +0 -2
  35. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +26 -213
  36. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +1 -1
  37. package/dist/core/ingestion/unity-resource-processor.js +87 -22
  38. package/dist/core/ingestion/unity-resource-processor.test.js +67 -2
  39. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +11 -0
  40. package/dist/core/ingestion/unity-runtime-binding-rules.js +179 -0
  41. package/dist/core/unity/options.d.ts +4 -0
  42. package/dist/core/unity/options.js +18 -0
  43. package/dist/core/unity/options.test.js +11 -1
  44. package/dist/core/unity/resolver.js +11 -1
  45. package/dist/core/unity/resolver.test.js +62 -0
  46. package/dist/core/unity/yaml-object-graph.js +1 -1
  47. package/dist/core/unity/yaml-object-graph.test.js +16 -0
  48. package/dist/mcp/local/derived-process-reader.d.ts +2 -0
  49. package/dist/mcp/local/derived-process-reader.js +15 -0
  50. package/dist/mcp/local/local-backend.d.ts +56 -0
  51. package/dist/mcp/local/local-backend.js +1003 -53
  52. package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
  53. package/dist/mcp/local/process-confidence.js +1 -1
  54. package/dist/mcp/local/process-evidence.d.ts +1 -0
  55. package/dist/mcp/local/process-evidence.js +22 -0
  56. package/dist/mcp/local/process-evidence.test.js +11 -1
  57. package/dist/mcp/local/process-ref.d.ts +24 -0
  58. package/dist/mcp/local/process-ref.js +33 -0
  59. package/dist/mcp/local/process-ref.test.d.ts +1 -0
  60. package/dist/mcp/local/process-ref.test.js +24 -0
  61. package/dist/mcp/local/runtime-chain-verify.d.ts +15 -1
  62. package/dist/mcp/local/runtime-chain-verify.js +191 -187
  63. package/dist/mcp/local/runtime-chain-verify.test.js +546 -19
  64. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +63 -0
  65. package/dist/mcp/local/runtime-claim-rule-registry.js +308 -0
  66. package/dist/mcp/local/runtime-claim-rule-registry.test.d.ts +1 -0
  67. package/dist/mcp/local/runtime-claim-rule-registry.test.js +215 -0
  68. package/dist/mcp/local/runtime-claim.d.ts +38 -0
  69. package/dist/mcp/local/runtime-claim.js +54 -0
  70. package/dist/mcp/local/runtime-claim.test.d.ts +1 -0
  71. package/dist/mcp/local/runtime-claim.test.js +27 -0
  72. package/dist/mcp/local/unity-enrichment.d.ts +1 -0
  73. package/dist/mcp/local/unity-enrichment.js +1 -1
  74. package/dist/mcp/local/unity-evidence-view.d.ts +26 -0
  75. package/dist/mcp/local/unity-evidence-view.js +96 -0
  76. package/dist/mcp/local/unity-evidence-view.test.d.ts +1 -0
  77. package/dist/mcp/local/unity-evidence-view.test.js +39 -0
  78. package/dist/mcp/local/unity-lazy-hydrator.d.ts +2 -2
  79. package/dist/mcp/local/unity-lazy-hydrator.js +3 -3
  80. package/dist/mcp/local/unity-lazy-hydrator.test.js +4 -4
  81. package/dist/mcp/local/unity-parity-cache.js +2 -6
  82. package/dist/mcp/local/unity-parity-seed-loader.d.ts +1 -0
  83. package/dist/mcp/local/unity-parity-seed-loader.js +10 -16
  84. package/dist/mcp/local/unity-parity-seed-loader.test.js +3 -12
  85. package/dist/mcp/local/unity-runtime-hydration.d.ts +3 -2
  86. package/dist/mcp/local/unity-runtime-hydration.js +13 -16
  87. package/dist/mcp/local/unity-runtime-hydration.test.js +15 -1
  88. package/dist/mcp/resources.js +13 -0
  89. package/dist/mcp/tools.js +166 -13
  90. package/dist/rule-lab/analyze.d.ts +12 -0
  91. package/dist/rule-lab/analyze.js +90 -0
  92. package/dist/rule-lab/analyze.test.d.ts +1 -0
  93. package/dist/rule-lab/analyze.test.js +28 -0
  94. package/dist/rule-lab/compile.d.ts +5 -0
  95. package/dist/rule-lab/compile.js +51 -0
  96. package/dist/rule-lab/compiled-bundles.d.ts +30 -0
  97. package/dist/rule-lab/compiled-bundles.js +36 -0
  98. package/dist/rule-lab/curate.d.ts +32 -0
  99. package/dist/rule-lab/curate.js +134 -0
  100. package/dist/rule-lab/curate.test.d.ts +1 -0
  101. package/dist/rule-lab/curate.test.js +72 -0
  102. package/dist/rule-lab/discover.d.ts +13 -0
  103. package/dist/rule-lab/discover.js +74 -0
  104. package/dist/rule-lab/discover.test.d.ts +1 -0
  105. package/dist/rule-lab/discover.test.js +42 -0
  106. package/dist/rule-lab/paths.d.ts +21 -0
  107. package/dist/rule-lab/paths.js +37 -0
  108. package/dist/rule-lab/paths.test.d.ts +1 -0
  109. package/dist/rule-lab/paths.test.js +46 -0
  110. package/dist/rule-lab/promote.d.ts +26 -0
  111. package/dist/rule-lab/promote.js +314 -0
  112. package/dist/rule-lab/promote.test.d.ts +1 -0
  113. package/dist/rule-lab/promote.test.js +164 -0
  114. package/dist/rule-lab/regress.d.ts +60 -0
  115. package/dist/rule-lab/regress.js +122 -0
  116. package/dist/rule-lab/regress.test.d.ts +1 -0
  117. package/dist/rule-lab/regress.test.js +68 -0
  118. package/dist/rule-lab/review-pack.d.ts +31 -0
  119. package/dist/rule-lab/review-pack.js +125 -0
  120. package/dist/rule-lab/review-pack.test.d.ts +1 -0
  121. package/dist/rule-lab/review-pack.test.js +49 -0
  122. package/dist/rule-lab/types.d.ts +99 -0
  123. package/dist/rule-lab/types.js +1 -0
  124. package/package.json +1 -1
  125. package/skills/_shared/unity-hydration-contract.md +11 -0
  126. package/skills/_shared/unity-ui-trace-contract.md +33 -0
  127. package/skills/gitnexus-cli.md +11 -25
  128. package/skills/gitnexus-guide.md +2 -0
  129. package/skills/gitnexus-unity-rule-gen.md +318 -0
  130. package/dist/core/ingestion/unity-lifecycle-config.d.ts +0 -5
  131. package/dist/core/ingestion/unity-lifecycle-config.js +0 -25
  132. package/dist/mcp/local/unity-lazy-config.d.ts +0 -6
  133. package/dist/mcp/local/unity-lazy-config.js +0 -7
  134. package/dist/mcp/local/unity-lazy-config.test.js +0 -9
  135. package/dist/mcp/local/unity-process-confidence-config.d.ts +0 -1
  136. package/dist/mcp/local/unity-process-confidence-config.js +0 -4
  137. package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +0 -1
  138. package/dist/mcp/local/unity-runtime-chain-verify-config.js +0 -10
  139. /package/dist/{mcp/local/unity-lazy-config.test.d.ts → benchmark/u2-e2e/hydration-policy-repeatability-runner.test.d.ts} +0 -0
@@ -0,0 +1,26 @@
1
+ import type { UnityEvidenceMode } from '../../core/unity/options.js';
2
+ import type { ResolvedUnityBinding, UnitySerializedFields } from '../../core/unity/resolver.js';
3
+ export interface UnityEvidenceMeta {
4
+ truncated: boolean;
5
+ omitted_count: number;
6
+ next_fetch_hint?: string;
7
+ filter_exhausted?: boolean;
8
+ minimum_evidence_satisfied: boolean;
9
+ verifier_minimum_evidence_satisfied?: boolean;
10
+ }
11
+ export interface UnityEvidenceViewResult {
12
+ resourceBindings: ResolvedUnityBinding[];
13
+ serializedFields: UnitySerializedFields;
14
+ evidence_meta: UnityEvidenceMeta;
15
+ filter_diagnostics: string[];
16
+ }
17
+ export interface UnityEvidenceViewInput {
18
+ resourceBindings: ResolvedUnityBinding[];
19
+ mode?: UnityEvidenceMode;
20
+ scopePreset?: string;
21
+ resourcePathPrefix?: string;
22
+ bindingKind?: string;
23
+ maxBindings?: number;
24
+ maxReferenceFields?: number;
25
+ }
26
+ export declare function buildUnityEvidenceView(input: UnityEvidenceViewInput): UnityEvidenceViewResult;
@@ -0,0 +1,96 @@
1
+ function normalizePath(value) {
2
+ return String(value || '').replace(/\\/g, '/').toLowerCase();
3
+ }
4
+ function parsePositiveInt(raw) {
5
+ if (raw === undefined || raw === null || raw === '')
6
+ return undefined;
7
+ const parsed = Number.parseInt(String(raw), 10);
8
+ if (!Number.isFinite(parsed) || parsed <= 0)
9
+ return undefined;
10
+ return parsed;
11
+ }
12
+ function resolveModeDefaults(mode) {
13
+ if (mode === 'summary') {
14
+ return { maxBindings: 1, maxReferenceFields: 1 };
15
+ }
16
+ if (mode === 'focused') {
17
+ return { maxBindings: 5, maxReferenceFields: 5 };
18
+ }
19
+ return {};
20
+ }
21
+ function aggregateSerializedFields(resourceBindings) {
22
+ return {
23
+ scalarFields: resourceBindings.flatMap((binding) => binding.serializedFields.scalarFields),
24
+ referenceFields: resourceBindings.flatMap((binding) => binding.serializedFields.referenceFields),
25
+ };
26
+ }
27
+ export function buildUnityEvidenceView(input) {
28
+ const diagnostics = [];
29
+ const originalCount = input.resourceBindings.length;
30
+ let filtered = [...input.resourceBindings];
31
+ if (input.scopePreset === 'unity-gameplay') {
32
+ filtered = filtered.filter((binding) => {
33
+ const p = normalizePath(binding.resourcePath);
34
+ return p.startsWith('assets/')
35
+ && !p.startsWith('assets/plugins/')
36
+ && !p.startsWith('packages/')
37
+ && !p.startsWith('library/');
38
+ });
39
+ }
40
+ const prefix = normalizePath(String(input.resourcePathPrefix || ''));
41
+ if (prefix) {
42
+ filtered = filtered.filter((binding) => normalizePath(binding.resourcePath).startsWith(prefix));
43
+ }
44
+ const bindingKind = String(input.bindingKind || '').trim();
45
+ if (bindingKind) {
46
+ filtered = filtered.filter((binding) => String(binding.bindingKind) === bindingKind);
47
+ }
48
+ const filterExhausted = filtered.length === 0 && originalCount > 0;
49
+ if (filterExhausted) {
50
+ diagnostics.push('filter_exhausted');
51
+ }
52
+ const modeDefaults = resolveModeDefaults(input.mode);
53
+ const maxBindings = parsePositiveInt(input.maxBindings) ?? modeDefaults.maxBindings;
54
+ const maxReferenceFields = parsePositiveInt(input.maxReferenceFields) ?? modeDefaults.maxReferenceFields;
55
+ const beforeBindingTrim = filtered.length;
56
+ if (maxBindings !== undefined) {
57
+ filtered = filtered.slice(0, maxBindings);
58
+ }
59
+ let omittedCount = Math.max(0, beforeBindingTrim - filtered.length);
60
+ if (maxReferenceFields !== undefined) {
61
+ filtered = filtered.map((binding) => {
62
+ const referenceFields = binding.serializedFields.referenceFields;
63
+ const referenceTrimmed = referenceFields.length > maxReferenceFields;
64
+ if (referenceTrimmed) {
65
+ omittedCount += referenceFields.length - maxReferenceFields;
66
+ }
67
+ return {
68
+ ...binding,
69
+ serializedFields: {
70
+ ...binding.serializedFields,
71
+ referenceFields: referenceTrimmed
72
+ ? referenceFields.slice(0, maxReferenceFields)
73
+ : referenceFields,
74
+ },
75
+ resolvedReferences: binding.resolvedReferences.length > (maxReferenceFields || 0)
76
+ ? binding.resolvedReferences.slice(0, maxReferenceFields)
77
+ : binding.resolvedReferences,
78
+ };
79
+ });
80
+ }
81
+ const truncated = omittedCount > 0;
82
+ const evidence_meta = {
83
+ truncated,
84
+ omitted_count: omittedCount,
85
+ ...(truncated ? { next_fetch_hint: 'Rerun with unity_evidence_mode=full to fetch complete evidence.' } : {}),
86
+ ...(filterExhausted ? { filter_exhausted: true } : {}),
87
+ minimum_evidence_satisfied: !truncated && filtered.length > 0,
88
+ verifier_minimum_evidence_satisfied: filtered.length > 0,
89
+ };
90
+ return {
91
+ resourceBindings: filtered,
92
+ serializedFields: aggregateSerializedFields(filtered),
93
+ evidence_meta,
94
+ filter_diagnostics: diagnostics,
95
+ };
96
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildUnityEvidenceView } from './unity-evidence-view.js';
4
+ test('unity evidence view emits truncation metadata and fetch hint', () => {
5
+ const out = buildUnityEvidenceView({
6
+ resourceBindings: [
7
+ {
8
+ resourcePath: 'Assets/A.prefab',
9
+ resourceType: 'prefab',
10
+ bindingKind: 'direct',
11
+ componentObjectId: '1',
12
+ evidence: { line: 1, lineText: 'x' },
13
+ serializedFields: {
14
+ scalarFields: [],
15
+ referenceFields: [
16
+ { name: 'r1', sourceLayer: 'base' },
17
+ { name: 'r2', sourceLayer: 'base' },
18
+ ],
19
+ },
20
+ resolvedReferences: [],
21
+ },
22
+ {
23
+ resourcePath: 'Assets/B.prefab',
24
+ resourceType: 'prefab',
25
+ bindingKind: 'direct',
26
+ componentObjectId: '2',
27
+ evidence: { line: 2, lineText: 'y' },
28
+ serializedFields: { scalarFields: [], referenceFields: [] },
29
+ resolvedReferences: [],
30
+ },
31
+ ],
32
+ mode: 'summary',
33
+ maxBindings: 1,
34
+ maxReferenceFields: 1,
35
+ });
36
+ assert.equal(out.evidence_meta.truncated, true);
37
+ assert.ok(out.evidence_meta.omitted_count > 0);
38
+ assert.match(out.evidence_meta.next_fetch_hint || '', /unity_evidence_mode=full/i);
39
+ });
@@ -1,8 +1,8 @@
1
1
  import type { ResolvedUnityBinding } from '../../core/unity/resolver.js';
2
- import type { UnityLazyConfig } from './unity-lazy-config.js';
2
+ import type { UnityConfig } from '../../core/config/unity-config.js';
3
3
  export interface HydrateLazyBindingsInput {
4
4
  pendingPaths: string[];
5
- config: UnityLazyConfig;
5
+ config: Pick<UnityConfig, 'lazyMaxPaths' | 'lazyBatchSize' | 'lazyMaxMs'>;
6
6
  resolveBatch: (paths: string[]) => Promise<Map<string, ResolvedUnityBinding[]>>;
7
7
  dedupeKey?: string;
8
8
  }
@@ -14,14 +14,14 @@ export async function hydrateLazyBindings(input) {
14
14
  return pending;
15
15
  }
16
16
  async function runHydration(input) {
17
- const pending = input.pendingPaths.slice(0, Math.max(0, input.config.maxPendingPathsPerRequest));
18
- const batchSize = Math.max(1, input.config.batchSize);
17
+ const pending = input.pendingPaths.slice(0, Math.max(0, input.config.lazyMaxPaths));
18
+ const batchSize = Math.max(1, input.config.lazyBatchSize);
19
19
  const startedAt = Date.now();
20
20
  const resolvedByPath = new Map();
21
21
  let timedOut = false;
22
22
  const diagnostics = [];
23
23
  for (let i = 0; i < pending.length; i += batchSize) {
24
- if (Date.now() - startedAt > input.config.maxHydrationMs) {
24
+ if (Date.now() - startedAt > input.config.lazyMaxMs) {
25
25
  timedOut = true;
26
26
  break;
27
27
  }
@@ -5,7 +5,7 @@ test('hydrateLazyBindings processes pending paths in bounded chunks', async () =
5
5
  const calls = [];
6
6
  await hydrateLazyBindings({
7
7
  pendingPaths: ['a', 'b', 'c', 'd', 'e'],
8
- config: { maxPendingPathsPerRequest: 4, batchSize: 2, maxHydrationMs: 5000 },
8
+ config: { lazyMaxPaths: 4, lazyBatchSize: 2, lazyMaxMs: 5000 },
9
9
  resolveBatch: async (paths) => {
10
10
  calls.push(paths);
11
11
  return new Map();
@@ -17,7 +17,7 @@ test('parallel requests dedupe same hydration work', async () => {
17
17
  let resolveCalls = 0;
18
18
  const sharedInput = {
19
19
  pendingPaths: ['Assets/A.prefab'],
20
- config: { maxPendingPathsPerRequest: 10, batchSize: 5, maxHydrationMs: 5000 },
20
+ config: { lazyMaxPaths: 10, lazyBatchSize: 5, lazyMaxMs: 5000 },
21
21
  dedupeKey: 'symbol:door::Assets/A.prefab',
22
22
  resolveBatch: async (_paths) => {
23
23
  resolveCalls += 1;
@@ -34,7 +34,7 @@ test('parallel requests dedupe same hydration work', async () => {
34
34
  test('context lazy hydration returns partial results when budget exceeded and reports diagnostics', async () => {
35
35
  const out = await hydrateLazyBindings({
36
36
  pendingPaths: ['a', 'b', 'c', 'd'],
37
- config: { maxPendingPathsPerRequest: 4, batchSize: 2, maxHydrationMs: 1 },
37
+ config: { lazyMaxPaths: 4, lazyBatchSize: 2, lazyMaxMs: 1 },
38
38
  resolveBatch: async (paths) => {
39
39
  await new Promise((resolve) => setTimeout(resolve, 5));
40
40
  return new Map(paths.map((p) => [p, []]));
@@ -46,7 +46,7 @@ test('context lazy hydration returns partial results when budget exceeded and re
46
46
  test('summary-only Unity analyze persistence still returns full bindings after lazy hydration', async () => {
47
47
  const out = await hydrateLazyBindings({
48
48
  pendingPaths: ['Assets/Doors/Door.prefab'],
49
- config: { maxPendingPathsPerRequest: 10, batchSize: 5, maxHydrationMs: 5000 },
49
+ config: { lazyMaxPaths: 10, lazyBatchSize: 5, lazyMaxMs: 5000 },
50
50
  resolveBatch: async () => new Map([
51
51
  ['Assets/Doors/Door.prefab', [{
52
52
  resourcePath: 'Assets/Doors/Door.prefab',
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
+ import { resolveUnityConfig } from '../../core/config/unity-config.js';
4
5
  const PARITY_CACHE_DIRNAME = 'unity-parity-cache';
5
6
  const DEFAULT_MAX_PARITY_CACHE_ENTRIES = 500;
6
7
  function buildKey(symbolUid) {
@@ -66,12 +67,7 @@ function resolveMaxEntries(options) {
66
67
  if (Number.isFinite(options?.maxEntries) && Number(options?.maxEntries) > 0) {
67
68
  return Math.floor(Number(options?.maxEntries));
68
69
  }
69
- const raw = String(process.env.GITNEXUS_UNITY_PARITY_CACHE_MAX_ENTRIES || '').trim();
70
- const parsed = Number.parseInt(raw, 10);
71
- if (Number.isFinite(parsed) && parsed > 0) {
72
- return parsed;
73
- }
74
- return DEFAULT_MAX_PARITY_CACHE_ENTRIES;
70
+ return resolveUnityConfig().config.parityCacheMaxEntries ?? DEFAULT_MAX_PARITY_CACHE_ENTRIES;
75
71
  }
76
72
  function pruneOldestEntries(entries, maxEntries) {
77
73
  const rows = Object.entries(entries);
@@ -1,6 +1,7 @@
1
1
  import type { UnityParitySeed } from '../../core/ingestion/unity-parity-seed.js';
2
2
  interface LoadUnityParitySeedOptions {
3
3
  indexedCommit?: string;
4
+ idleMsOverride?: number;
4
5
  }
5
6
  export declare function loadUnityParitySeed(storagePath: string, options?: LoadUnityParitySeedOptions): Promise<UnityParitySeed | null>;
6
7
  export declare function __resetUnityParitySeedLoaderCacheForTest(): void;
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { resolveUnityConfig } from '../../core/config/unity-config.js';
3
4
  const SEED_FILENAME = 'unity-parity-seed.json';
4
5
  const DEFAULT_IDLE_MS = 30_000;
5
6
  const DEFAULT_MAX_ENTRIES = 2;
@@ -32,7 +33,7 @@ export async function loadUnityParitySeed(storagePath, options) {
32
33
  throw error;
33
34
  }
34
35
  const parsed = parseSeed(raw);
35
- setCacheEntry(cacheKey, parsed);
36
+ setCacheEntry(cacheKey, parsed, options?.idleMsOverride);
36
37
  return parsed;
37
38
  })().finally(() => {
38
39
  inFlightLoads.delete(cacheKey);
@@ -69,7 +70,7 @@ async function buildSeedCacheKey(seedPath, storagePath, indexedCommit) {
69
70
  throw error;
70
71
  }
71
72
  }
72
- function setCacheEntry(cacheKey, value) {
73
+ function setCacheEntry(cacheKey, value, idleMsOverride) {
73
74
  const now = Date.now();
74
75
  const existing = seedCache.get(cacheKey);
75
76
  if (existing?.idleTimer) {
@@ -78,9 +79,10 @@ function setCacheEntry(cacheKey, value) {
78
79
  const entry = {
79
80
  value,
80
81
  lastAccessMs: now,
82
+ idleMsOverride,
81
83
  };
82
84
  seedCache.set(cacheKey, entry);
83
- scheduleEviction(cacheKey, entry);
85
+ scheduleEviction(cacheKey, entry, idleMsOverride);
84
86
  pruneOldestEntries(resolveMaxEntries());
85
87
  }
86
88
  function touchCacheEntry(cacheKey, entry) {
@@ -88,10 +90,10 @@ function touchCacheEntry(cacheKey, entry) {
88
90
  if (entry.idleTimer) {
89
91
  clearTimeout(entry.idleTimer);
90
92
  }
91
- scheduleEviction(cacheKey, entry);
93
+ scheduleEviction(cacheKey, entry, entry.idleMsOverride);
92
94
  }
93
- function scheduleEviction(cacheKey, entry) {
94
- const idleMs = resolveIdleMs();
95
+ function scheduleEviction(cacheKey, entry, idleMsOverride) {
96
+ const idleMs = idleMsOverride ?? resolveIdleMs();
95
97
  entry.idleTimer = setTimeout(() => {
96
98
  const current = seedCache.get(cacheKey);
97
99
  if (!current || current !== entry) {
@@ -116,18 +118,10 @@ function pruneOldestEntries(maxEntries) {
116
118
  }
117
119
  }
118
120
  function resolveIdleMs() {
119
- const parsed = Number.parseInt(String(process.env.GITNEXUS_UNITY_PARITY_SEED_CACHE_IDLE_MS || '').trim(), 10);
120
- if (Number.isFinite(parsed) && parsed > 0) {
121
- return parsed;
122
- }
123
- return DEFAULT_IDLE_MS;
121
+ return resolveUnityConfig().config.paritySeedCacheIdleMs ?? DEFAULT_IDLE_MS;
124
122
  }
125
123
  function resolveMaxEntries() {
126
- const parsed = Number.parseInt(String(process.env.GITNEXUS_UNITY_PARITY_SEED_CACHE_MAX_ENTRIES || '').trim(), 10);
127
- if (Number.isFinite(parsed) && parsed > 0) {
128
- return parsed;
129
- }
130
- return DEFAULT_MAX_ENTRIES;
124
+ return resolveUnityConfig().config.paritySeedCacheMaxEntries ?? DEFAULT_MAX_ENTRIES;
131
125
  }
132
126
  export function __resetUnityParitySeedLoaderCacheForTest() {
133
127
  for (const entry of seedCache.values()) {
@@ -54,9 +54,6 @@ test('loadUnityParitySeed deduplicates concurrent requests for same storage key'
54
54
  });
55
55
  test('loadUnityParitySeed evicts idle cache entry after ttl', async (t) => {
56
56
  const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
57
- const idleEnvKey = 'GITNEXUS_UNITY_PARITY_SEED_CACHE_IDLE_MS';
58
- const previousIdle = process.env[idleEnvKey];
59
- process.env[idleEnvKey] = '15';
60
57
  try {
61
58
  await writeSeed(storagePath, 'IdleSymbol');
62
59
  const readFileOriginal = fs.readFile.bind(fs);
@@ -65,21 +62,15 @@ test('loadUnityParitySeed evicts idle cache entry after ttl', async (t) => {
65
62
  readFileCalls += 1;
66
63
  return readFileOriginal(...args);
67
64
  });
68
- await loadUnityParitySeed(storagePath);
69
- await loadUnityParitySeed(storagePath);
65
+ await loadUnityParitySeed(storagePath, { idleMsOverride: 15 });
66
+ await loadUnityParitySeed(storagePath, { idleMsOverride: 15 });
70
67
  assert.equal(readFileCalls, 1);
71
68
  await new Promise((resolve) => setTimeout(resolve, 30));
72
- await loadUnityParitySeed(storagePath);
69
+ await loadUnityParitySeed(storagePath, { idleMsOverride: 15 });
73
70
  assert.equal(readFileCalls, 2);
74
71
  }
75
72
  finally {
76
73
  __resetUnityParitySeedLoaderCacheForTest();
77
- if (previousIdle === undefined) {
78
- delete process.env[idleEnvKey];
79
- }
80
- else {
81
- process.env[idleEnvKey] = previousIdle;
82
- }
83
74
  await fs.rm(storagePath, { recursive: true, force: true });
84
75
  }
85
76
  });
@@ -2,7 +2,7 @@ import { buildUnityScanContext, buildUnityScanContextFromSeed } from '../../core
2
2
  import { resolveUnityBindings, type ResolvedUnityBinding } from '../../core/unity/resolver.js';
3
3
  import type { UnityHydrationMode } from '../../core/unity/options.js';
4
4
  import { type UnityContextPayload, type UnityHydrationMeta } from './unity-enrichment.js';
5
- import { resolveUnityLazyConfig } from './unity-lazy-config.js';
5
+ import { resolveUnityConfig } from '../../core/config/unity-config.js';
6
6
  import { hydrateLazyBindings, type HydrateLazyBindingsInput } from './unity-lazy-hydrator.js';
7
7
  import { readUnityOverlayBindings, upsertUnityOverlayBindings } from './unity-lazy-overlay.js';
8
8
  import { readUnityParityCache, upsertUnityParityCache } from './unity-parity-cache.js';
@@ -28,7 +28,7 @@ export interface HydrateUnityInput {
28
28
  interface HydrationRuntime {
29
29
  now: () => number;
30
30
  queue: ParityWarmupQueue;
31
- resolveLazyConfig: typeof resolveUnityLazyConfig;
31
+ resolveLazyConfig: () => Pick<ReturnType<typeof resolveUnityConfig>['config'], 'lazyMaxPaths' | 'lazyBatchSize' | 'lazyMaxMs'>;
32
32
  hydrateLazyBindings: (input: HydrateLazyBindingsInput) => ReturnType<typeof hydrateLazyBindings>;
33
33
  readOverlayBindings: typeof readUnityOverlayBindings;
34
34
  upsertOverlayBindings: typeof upsertUnityOverlayBindings;
@@ -45,6 +45,7 @@ export declare function mergeParityUnityBindings(baseNonLightweightBindings: Res
45
45
  export declare function attachUnityHydrationMeta(payload: UnityContextPayload, input: Pick<UnityHydrationMeta, 'requestedMode' | 'effectiveMode' | 'elapsedMs' | 'fallbackToCompact'> & {
46
46
  hasExpandableBindings: boolean;
47
47
  }): UnityContextPayload;
48
+ export declare function buildMissingEvidenceFromHydrationMeta(meta?: UnityHydrationMeta): string[];
48
49
  export declare function hydrateUnityForSymbol(input: HydrateUnityInput): Promise<UnityContextPayload>;
49
50
  export declare function __resetUnityRuntimeHydrationStateForTest(): void;
50
51
  export {};
@@ -1,7 +1,7 @@
1
1
  import { buildUnityScanContext, buildUnityScanContextFromSeed } from '../../core/unity/scan-context.js';
2
2
  import { resolveUnityBindings } from '../../core/unity/resolver.js';
3
3
  import { formatLazyHydrationBudgetDiagnostic, } from './unity-enrichment.js';
4
- import { resolveUnityLazyConfig } from './unity-lazy-config.js';
4
+ import { resolveUnityConfig } from '../../core/config/unity-config.js';
5
5
  import { hydrateLazyBindings } from './unity-lazy-hydrator.js';
6
6
  import { readUnityOverlayBindings, upsertUnityOverlayBindings } from './unity-lazy-overlay.js';
7
7
  import { readUnityParityCache, upsertUnityParityCache } from './unity-parity-cache.js';
@@ -9,16 +9,8 @@ import { createParityWarmupQueue } from './unity-parity-warmup-queue.js';
9
9
  import { loadUnityParitySeed } from './unity-parity-seed-loader.js';
10
10
  const inFlightParityHydration = new Map();
11
11
  const parityWarmupQueue = createParityWarmupQueue({
12
- maxParallel: resolveParityWarmupMaxParallel(process.env),
12
+ maxParallel: resolveUnityConfig().config.parityWarmupMaxParallel ?? 2,
13
13
  });
14
- function resolveParityWarmupMaxParallel(env) {
15
- const raw = String(env.GITNEXUS_UNITY_PARITY_WARMUP_MAX_PARALLEL || '').trim();
16
- const parsed = Number.parseInt(raw, 10);
17
- if (Number.isFinite(parsed) && parsed > 0) {
18
- return parsed;
19
- }
20
- return 2;
21
- }
22
14
  function normalizePath(filePath) {
23
15
  return String(filePath || '').replace(/\\/g, '/');
24
16
  }
@@ -92,6 +84,12 @@ export function attachUnityHydrationMeta(payload, input) {
92
84
  },
93
85
  };
94
86
  }
87
+ export function buildMissingEvidenceFromHydrationMeta(meta) {
88
+ if (!meta || meta.isComplete) {
89
+ return [];
90
+ }
91
+ return [...new Set((meta.completenessReason || []).map((value) => String(value || '').trim()).filter(Boolean))];
92
+ }
95
93
  export async function hydrateUnityForSymbol(input) {
96
94
  const runtime = resolveRuntime(input.runtime);
97
95
  const startedAt = runtime.now();
@@ -122,7 +120,7 @@ function resolveRuntime(overrides) {
122
120
  return {
123
121
  now: () => Date.now(),
124
122
  queue: parityWarmupQueue,
125
- resolveLazyConfig: resolveUnityLazyConfig,
123
+ resolveLazyConfig: () => resolveUnityConfig().config,
126
124
  hydrateLazyBindings,
127
125
  readOverlayBindings: readUnityOverlayBindings,
128
126
  upsertOverlayBindings: upsertUnityOverlayBindings,
@@ -153,9 +151,8 @@ function scheduleParityWarmup(input, runtime) {
153
151
  .then(() => undefined)
154
152
  .catch(() => undefined);
155
153
  }
156
- function shouldEnableWarmup(env) {
157
- const raw = String(env.GITNEXUS_UNITY_PARITY_WARMUP || '').trim().toLowerCase();
158
- return raw === '1' || raw === 'true' || raw === 'on';
154
+ function shouldEnableWarmup(_env) {
155
+ return resolveUnityConfig().config.parityWarmup ?? false;
159
156
  }
160
157
  async function getOrRunParityHydration(input, runtime) {
161
158
  const key = buildParityWarmupKey(input);
@@ -256,7 +253,7 @@ async function runCompactHydration(input, runtime) {
256
253
  const unityDiagnostics = [...input.basePayload.unityDiagnostics];
257
254
  if (pendingPaths.length > 0) {
258
255
  try {
259
- const cfg = runtime.resolveLazyConfig(process.env);
256
+ const cfg = runtime.resolveLazyConfig();
260
257
  const hydration = await runtime.hydrateLazyBindings({
261
258
  pendingPaths,
262
259
  config: cfg,
@@ -315,7 +312,7 @@ function toUnityContextPayload(resourceBindings, unityDiagnostics) {
315
312
  scalarFields: resourceBindings.flatMap((binding) => binding.serializedFields.scalarFields),
316
313
  referenceFields: resourceBindings.flatMap((binding) => binding.serializedFields.referenceFields),
317
314
  },
318
- unityDiagnostics,
315
+ unityDiagnostics: [...new Set(unityDiagnostics)],
319
316
  };
320
317
  }
321
318
  export function __resetUnityRuntimeHydrationStateForTest() {
@@ -1,6 +1,6 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { hydrateUnityForSymbol } from './unity-runtime-hydration.js';
3
+ import { buildMissingEvidenceFromHydrationMeta, hydrateUnityForSymbol } from './unity-runtime-hydration.js';
4
4
  test('hydrateUnityForSymbol(compact) marks needsParityRetry when lightweight bindings remain', async () => {
5
5
  const out = await hydrateUnityForSymbol({
6
6
  mode: 'compact',
@@ -106,3 +106,17 @@ test('hydrateUnityForSymbol(parity) sets isComplete=true on parity success', asy
106
106
  assert.equal(out.hydrationMeta?.effectiveMode, 'parity');
107
107
  assert.equal(out.hydrationMeta?.isComplete, true);
108
108
  });
109
+ test('buildMissingEvidenceFromHydrationMeta maps incomplete reasons', () => {
110
+ const missing = buildMissingEvidenceFromHydrationMeta({
111
+ requestedMode: 'compact',
112
+ effectiveMode: 'compact',
113
+ elapsedMs: 1,
114
+ fallbackToCompact: false,
115
+ resourceBindingCount: 1,
116
+ unityDiagnosticsCount: 0,
117
+ isComplete: false,
118
+ completenessReason: ['lightweight_bindings_remaining', 'budget_exceeded'],
119
+ needsParityRetry: true,
120
+ });
121
+ assert.deepEqual(missing, ['lightweight_bindings_remaining', 'budget_exceeded']);
122
+ });
@@ -4,6 +4,7 @@
4
4
  * Provides structured on-demand data to AI agents.
5
5
  * All resources use repo-scoped URIs: gitnexus://repo/{name}/context
6
6
  */
7
+ import { getDerivedProcessDetailResource } from './local/derived-process-reader.js';
7
8
  import { checkStaleness } from './staleness.js';
8
9
  import { loadCLIConfig } from '../storage/repo-manager.js';
9
10
  import { buildNpxCommand, resolveCliSpec } from '../config/cli-spec.js';
@@ -67,6 +68,12 @@ export function getResourceTemplates() {
67
68
  description: 'Step-by-step execution trace with lifecycle subtype and step evidence when available',
68
69
  mimeType: 'text/yaml',
69
70
  },
71
+ {
72
+ uriTemplate: 'gitnexus://repo/{name}/derived-process/{id}',
73
+ name: 'Derived Process Trace',
74
+ description: 'Readable trace metadata for derived process references',
75
+ mimeType: 'text/yaml',
76
+ },
70
77
  ];
71
78
  }
72
79
  /**
@@ -88,6 +95,9 @@ function parseUri(uri) {
88
95
  if (rest.startsWith('process/')) {
89
96
  return { repoName, resourceType: 'process', param: decodeURIComponent(rest.replace('process/', '')) };
90
97
  }
98
+ if (rest.startsWith('derived-process/')) {
99
+ return { repoName, resourceType: 'derived-process', param: decodeURIComponent(rest.replace('derived-process/', '')) };
100
+ }
91
101
  return { repoName, resourceType: rest };
92
102
  }
93
103
  throw new Error(`Unknown resource URI: ${uri}`);
@@ -119,6 +129,8 @@ export async function readResource(uri, backend) {
119
129
  return getClusterDetailResource(parsed.param, backend, repoName);
120
130
  case 'process':
121
131
  return getProcessDetailResource(parsed.param, backend, repoName);
132
+ case 'derived-process':
133
+ return getDerivedProcessDetailResource(parsed.param, backend, repoName);
122
134
  default:
123
135
  throw new Error(`Unknown resource: ${uri}`);
124
136
  }
@@ -202,6 +214,7 @@ async function getContextResource(backend, repoName) {
202
214
  lines.push(` - gitnexus://repo/${context.projectName}/processes: All execution flows`);
203
215
  lines.push(` - gitnexus://repo/${context.projectName}/cluster/{name}: Module details`);
204
216
  lines.push(` - gitnexus://repo/${context.projectName}/process/{name}: Process trace`);
217
+ lines.push(` - gitnexus://repo/${context.projectName}/derived-process/{id}: Derived process trace metadata`);
205
218
  return lines.join('\n');
206
219
  }
207
220
  /**