@veewo/gitnexus 1.4.11-rc → 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 (140) 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 -7
  21. package/dist/cli/ai-context.test.js +6 -8
  22. package/dist/cli/eval-server.js +0 -3
  23. package/dist/cli/index.js +8 -0
  24. package/dist/cli/mcp.js +0 -3
  25. package/dist/cli/rule-lab.d.ts +42 -0
  26. package/dist/cli/rule-lab.js +157 -0
  27. package/dist/cli/rule-lab.test.d.ts +1 -0
  28. package/dist/cli/rule-lab.test.js +11 -0
  29. package/dist/cli/tool.d.ts +7 -1
  30. package/dist/cli/tool.js +6 -0
  31. package/dist/core/config/unity-config.d.ts +20 -0
  32. package/dist/core/config/unity-config.js +46 -0
  33. package/dist/core/graph/types.d.ts +1 -1
  34. package/dist/core/ingestion/pipeline.js +38 -13
  35. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +0 -2
  36. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +26 -213
  37. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +1 -1
  38. package/dist/core/ingestion/unity-resource-processor.js +87 -22
  39. package/dist/core/ingestion/unity-resource-processor.test.js +67 -2
  40. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +11 -0
  41. package/dist/core/ingestion/unity-runtime-binding-rules.js +179 -0
  42. package/dist/core/unity/options.d.ts +4 -0
  43. package/dist/core/unity/options.js +18 -0
  44. package/dist/core/unity/options.test.js +11 -1
  45. package/dist/core/unity/resolver.js +11 -1
  46. package/dist/core/unity/resolver.test.js +62 -0
  47. package/dist/core/unity/yaml-object-graph.js +1 -1
  48. package/dist/core/unity/yaml-object-graph.test.js +16 -0
  49. package/dist/mcp/local/derived-process-reader.d.ts +2 -0
  50. package/dist/mcp/local/derived-process-reader.js +15 -0
  51. package/dist/mcp/local/local-backend.d.ts +56 -0
  52. package/dist/mcp/local/local-backend.js +1003 -53
  53. package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
  54. package/dist/mcp/local/process-confidence.js +1 -1
  55. package/dist/mcp/local/process-evidence.d.ts +1 -0
  56. package/dist/mcp/local/process-evidence.js +22 -0
  57. package/dist/mcp/local/process-evidence.test.js +11 -1
  58. package/dist/mcp/local/process-ref.d.ts +24 -0
  59. package/dist/mcp/local/process-ref.js +33 -0
  60. package/dist/mcp/local/process-ref.test.d.ts +1 -0
  61. package/dist/mcp/local/process-ref.test.js +24 -0
  62. package/dist/mcp/local/runtime-chain-verify.d.ts +15 -1
  63. package/dist/mcp/local/runtime-chain-verify.js +191 -187
  64. package/dist/mcp/local/runtime-chain-verify.test.js +546 -19
  65. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +63 -0
  66. package/dist/mcp/local/runtime-claim-rule-registry.js +308 -0
  67. package/dist/mcp/local/runtime-claim-rule-registry.test.d.ts +1 -0
  68. package/dist/mcp/local/runtime-claim-rule-registry.test.js +215 -0
  69. package/dist/mcp/local/runtime-claim.d.ts +38 -0
  70. package/dist/mcp/local/runtime-claim.js +54 -0
  71. package/dist/mcp/local/runtime-claim.test.d.ts +1 -0
  72. package/dist/mcp/local/runtime-claim.test.js +27 -0
  73. package/dist/mcp/local/unity-enrichment.d.ts +1 -0
  74. package/dist/mcp/local/unity-enrichment.js +1 -1
  75. package/dist/mcp/local/unity-evidence-view.d.ts +26 -0
  76. package/dist/mcp/local/unity-evidence-view.js +96 -0
  77. package/dist/mcp/local/unity-evidence-view.test.d.ts +1 -0
  78. package/dist/mcp/local/unity-evidence-view.test.js +39 -0
  79. package/dist/mcp/local/unity-lazy-hydrator.d.ts +2 -2
  80. package/dist/mcp/local/unity-lazy-hydrator.js +3 -3
  81. package/dist/mcp/local/unity-lazy-hydrator.test.js +4 -4
  82. package/dist/mcp/local/unity-parity-cache.js +2 -6
  83. package/dist/mcp/local/unity-parity-seed-loader.d.ts +1 -0
  84. package/dist/mcp/local/unity-parity-seed-loader.js +10 -16
  85. package/dist/mcp/local/unity-parity-seed-loader.test.js +3 -12
  86. package/dist/mcp/local/unity-runtime-hydration.d.ts +3 -2
  87. package/dist/mcp/local/unity-runtime-hydration.js +13 -16
  88. package/dist/mcp/local/unity-runtime-hydration.test.js +15 -1
  89. package/dist/mcp/resources.js +13 -0
  90. package/dist/mcp/tools.js +166 -13
  91. package/dist/rule-lab/analyze.d.ts +12 -0
  92. package/dist/rule-lab/analyze.js +90 -0
  93. package/dist/rule-lab/analyze.test.d.ts +1 -0
  94. package/dist/rule-lab/analyze.test.js +28 -0
  95. package/dist/rule-lab/compile.d.ts +5 -0
  96. package/dist/rule-lab/compile.js +51 -0
  97. package/dist/rule-lab/compiled-bundles.d.ts +30 -0
  98. package/dist/rule-lab/compiled-bundles.js +36 -0
  99. package/dist/rule-lab/curate.d.ts +32 -0
  100. package/dist/rule-lab/curate.js +134 -0
  101. package/dist/rule-lab/curate.test.d.ts +1 -0
  102. package/dist/rule-lab/curate.test.js +72 -0
  103. package/dist/rule-lab/discover.d.ts +13 -0
  104. package/dist/rule-lab/discover.js +74 -0
  105. package/dist/rule-lab/discover.test.d.ts +1 -0
  106. package/dist/rule-lab/discover.test.js +42 -0
  107. package/dist/rule-lab/paths.d.ts +21 -0
  108. package/dist/rule-lab/paths.js +37 -0
  109. package/dist/rule-lab/paths.test.d.ts +1 -0
  110. package/dist/rule-lab/paths.test.js +46 -0
  111. package/dist/rule-lab/promote.d.ts +26 -0
  112. package/dist/rule-lab/promote.js +314 -0
  113. package/dist/rule-lab/promote.test.d.ts +1 -0
  114. package/dist/rule-lab/promote.test.js +164 -0
  115. package/dist/rule-lab/regress.d.ts +60 -0
  116. package/dist/rule-lab/regress.js +122 -0
  117. package/dist/rule-lab/regress.test.d.ts +1 -0
  118. package/dist/rule-lab/regress.test.js +68 -0
  119. package/dist/rule-lab/review-pack.d.ts +31 -0
  120. package/dist/rule-lab/review-pack.js +125 -0
  121. package/dist/rule-lab/review-pack.test.d.ts +1 -0
  122. package/dist/rule-lab/review-pack.test.js +49 -0
  123. package/dist/rule-lab/types.d.ts +99 -0
  124. package/dist/rule-lab/types.js +1 -0
  125. package/package.json +1 -1
  126. package/skills/_shared/unity-hydration-contract.md +11 -0
  127. package/skills/_shared/unity-ui-trace-contract.md +33 -0
  128. package/skills/gitnexus-cli.md +11 -25
  129. package/skills/gitnexus-guide.md +2 -0
  130. package/skills/gitnexus-unity-rule-gen.md +318 -0
  131. package/dist/core/ingestion/unity-lifecycle-config.d.ts +0 -5
  132. package/dist/core/ingestion/unity-lifecycle-config.js +0 -25
  133. package/dist/mcp/local/unity-lazy-config.d.ts +0 -6
  134. package/dist/mcp/local/unity-lazy-config.js +0 -7
  135. package/dist/mcp/local/unity-lazy-config.test.js +0 -9
  136. package/dist/mcp/local/unity-process-confidence-config.d.ts +0 -1
  137. package/dist/mcp/local/unity-process-confidence-config.js +0 -4
  138. package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +0 -1
  139. package/dist/mcp/local/unity-runtime-chain-verify-config.js +0 -10
  140. /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,331 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { discoverRuleLabRun } from '../../rule-lab/discover.js';
4
+ import { analyzeRuleLabSlice } from '../../rule-lab/analyze.js';
5
+ import { buildReviewPack } from '../../rule-lab/review-pack.js';
6
+ import { curateRuleLabSlice } from '../../rule-lab/curate.js';
7
+ import { promoteCuratedRules } from '../../rule-lab/promote.js';
8
+ import { runRuleLabRegress } from '../../rule-lab/regress.js';
9
+ function commandFor(stage, input) {
10
+ switch (stage) {
11
+ case 'discover':
12
+ return 'gitnexus rule-lab discover --scope full';
13
+ case 'analyze':
14
+ return `gitnexus rule-lab analyze --run-id ${input.runId} --slice-id ${input.sliceId}`;
15
+ case 'review-pack':
16
+ return `gitnexus rule-lab review-pack --run-id ${input.runId} --slice-id ${input.sliceId} --max-tokens 6000`;
17
+ case 'curate':
18
+ return `gitnexus rule-lab curate --run-id ${input.runId} --slice-id ${input.sliceId} --input-path ${input.curationInputPath}`;
19
+ case 'promote':
20
+ return `gitnexus rule-lab promote --run-id ${input.runId} --slice-id ${input.sliceId}`;
21
+ case 'regress':
22
+ return `gitnexus rule-lab regress --precision 0.93 --coverage 0.85 --run-id ${input.runId}`;
23
+ default:
24
+ return 'gitnexus rule-lab <unknown>';
25
+ }
26
+ }
27
+ async function mustExist(filePath) {
28
+ await fs.access(filePath);
29
+ }
30
+ function buildFailureTaxonomy(runId) {
31
+ return [
32
+ {
33
+ code: 'rule_not_matched',
34
+ retry_hint: 'confirm trigger_family tokens and rerun discover/analyze/promote',
35
+ repro_command: `gitnexus rule-lab analyze --run-id ${runId} --slice-id <slice_id>`,
36
+ },
37
+ {
38
+ code: 'rule_matched_but_evidence_missing',
39
+ retry_hint: 'add stronger confirmed_chain anchors in curation input and promote again',
40
+ repro_command: `gitnexus rule-lab curate --run-id ${runId} --slice-id <slice_id> --input-path <curation-input.json>`,
41
+ },
42
+ {
43
+ code: 'precision_below_threshold',
44
+ retry_hint: 'trim noisy rules or tighten curation semantics before regress',
45
+ repro_command: `gitnexus rule-lab regress --precision 0.85 --coverage 0.92 --run-id ${runId}`,
46
+ },
47
+ {
48
+ code: 'coverage_below_threshold',
49
+ retry_hint: 'add additional approved rules/slices and rerun regress',
50
+ repro_command: `gitnexus rule-lab regress --precision 0.95 --coverage 0.70 --run-id ${runId}`,
51
+ },
52
+ {
53
+ code: 'probe_pass_rate_below_threshold',
54
+ retry_hint: 're-run regress with refreshed probes and inspect replay commands',
55
+ repro_command: `gitnexus rule-lab regress --precision 0.95 --coverage 0.90 --run-id ${runId}`,
56
+ },
57
+ {
58
+ code: 'static_hardcode_detected',
59
+ retry_hint: 'remove project-specific reload fallback constants/branches from runtime verifier',
60
+ repro_command: 'rg -n "RESOURCE_ASSET_PATH|GRAPH_ASSET_PATH|RELOAD_GUID|shouldVerifyReloadChain|verifyReloadRuntimeChain" gitnexus/src/mcp/local/runtime-chain-verify.ts',
61
+ },
62
+ {
63
+ code: 'dsl_lint_failed',
64
+ retry_hint: 'fix promoted DSL placeholders and rerun promote/regress',
65
+ repro_command: `gitnexus rule-lab promote --run-id ${runId} --slice-id <slice_id>`,
66
+ },
67
+ ];
68
+ }
69
+ function hasDslPlaceholders(text) {
70
+ return /(^|\s)(unknown|todo|tbd)(\s|$)|<[^>]+>/i.test(text);
71
+ }
72
+ async function buildAuthenticityChecks(input) {
73
+ const verifierPath = path.join(input.repoPath, 'gitnexus', 'src', 'mcp', 'local', 'runtime-chain-verify.ts');
74
+ const verifierRaw = await fs.readFile(verifierPath, 'utf-8');
75
+ const blockedSymbols = [
76
+ 'RESOURCE_ASSET_PATH',
77
+ 'GRAPH_ASSET_PATH',
78
+ 'RELOAD_GUID',
79
+ 'shouldVerifyReloadChain',
80
+ 'verifyReloadRuntimeChain',
81
+ ].filter((token) => verifierRaw.includes(token));
82
+ let dslLintPass = true;
83
+ for (const promotedPath of input.promotedFiles) {
84
+ const raw = await fs.readFile(promotedPath, 'utf-8');
85
+ if (hasDslPlaceholders(raw)) {
86
+ dslLintPass = false;
87
+ break;
88
+ }
89
+ }
90
+ return {
91
+ static_no_hardcoded_reload: {
92
+ pass: blockedSymbols.length === 0,
93
+ blocked_symbols: blockedSymbols,
94
+ },
95
+ dsl_lint_pass: dslLintPass,
96
+ };
97
+ }
98
+ export async function buildPhase5RuleLabAcceptanceReport(input) {
99
+ const repoPath = path.resolve(input.repoPath || process.cwd());
100
+ const stageCoverage = [];
101
+ const discover = await discoverRuleLabRun({ repoPath, scope: 'full', seed: input.seed || 'phase5-acceptance' });
102
+ stageCoverage.push({
103
+ stage: 'discover',
104
+ command: commandFor('discover', {}),
105
+ status: 'passed',
106
+ });
107
+ if (discover.manifest.slices.length === 0) {
108
+ throw new Error('discover produced no slices');
109
+ }
110
+ const sliceId = discover.manifest.slices[0].id;
111
+ const runId = discover.runId;
112
+ const analyze = await analyzeRuleLabSlice({ repoPath, runId, sliceId });
113
+ stageCoverage.push({
114
+ stage: 'analyze',
115
+ command: commandFor('analyze', { runId, sliceId }),
116
+ status: 'passed',
117
+ });
118
+ const reviewPack = await buildReviewPack({ repoPath, runId, sliceId, maxTokens: 6000 });
119
+ stageCoverage.push({
120
+ stage: 'review-pack',
121
+ command: commandFor('review-pack', { runId, sliceId }),
122
+ status: 'passed',
123
+ });
124
+ const firstCandidate = analyze.candidates[0];
125
+ const curationInputPath = path.join(repoPath, '.gitnexus', 'rules', 'lab', 'runs', runId, 'slices', sliceId, 'curation-input.json');
126
+ await fs.writeFile(curationInputPath, `${JSON.stringify({
127
+ run_id: runId,
128
+ slice_id: sliceId,
129
+ curated: [
130
+ {
131
+ id: firstCandidate.id,
132
+ rule_id: 'demo.startup.v1',
133
+ title: 'startup startup graph',
134
+ match: {
135
+ trigger_tokens: ['startup'],
136
+ },
137
+ topology: Array.isArray(firstCandidate.topology) && firstCandidate.topology.length > 0
138
+ ? firstCandidate.topology
139
+ : firstCandidate.evidence.hops.map((hop) => ({
140
+ hop: hop.hop_type,
141
+ from: { entity: 'resource' },
142
+ to: { entity: 'script' },
143
+ edge: { kind: 'binds_script' },
144
+ })),
145
+ closure: {
146
+ required_hops: Array.isArray(firstCandidate.topology) && firstCandidate.topology.length > 0
147
+ ? firstCandidate.topology.map((hop) => hop.hop)
148
+ : ['code_runtime'],
149
+ failure_map: {
150
+ missing_evidence: 'rule_matched_but_evidence_missing',
151
+ },
152
+ },
153
+ claims: {
154
+ guarantees: ['startup trigger matching is confirmed'],
155
+ non_guarantees: ['does not prove full runtime ordering'],
156
+ next_action: 'gitnexus query "Startup Graph Trigger"',
157
+ },
158
+ confirmed_chain: {
159
+ steps: firstCandidate.evidence.hops.map((hop) => ({ ...hop, hop_type: 'code_runtime' })),
160
+ },
161
+ guarantees: ['startup trigger matching is confirmed'],
162
+ non_guarantees: ['does not prove full runtime ordering'],
163
+ },
164
+ ],
165
+ }, null, 2)}\n`, 'utf-8');
166
+ const curated = await curateRuleLabSlice({ repoPath, runId, sliceId, inputPath: curationInputPath });
167
+ stageCoverage.push({
168
+ stage: 'curate',
169
+ command: commandFor('curate', { runId, sliceId, curationInputPath }),
170
+ status: 'passed',
171
+ });
172
+ const promoted = await promoteCuratedRules({ repoPath, runId, sliceId, version: '1.0.0' });
173
+ stageCoverage.push({
174
+ stage: 'promote',
175
+ command: commandFor('promote', { runId, sliceId }),
176
+ status: 'passed',
177
+ });
178
+ const regress = await runRuleLabRegress({
179
+ precision: 0.93,
180
+ coverage: 0.85,
181
+ probes: [
182
+ {
183
+ id: 'probe-startup-trigger',
184
+ pass: true,
185
+ replay_command: 'gitnexus query "Startup Graph Trigger" --runtime-chain-verify on-demand',
186
+ },
187
+ ],
188
+ repoPath,
189
+ runId,
190
+ });
191
+ stageCoverage.push({
192
+ stage: 'regress',
193
+ command: commandFor('regress', { runId, sliceId }),
194
+ status: regress.pass ? 'passed' : 'failed',
195
+ retry_hint: regress.pass ? undefined : regress.failures.join(','),
196
+ });
197
+ const artifactPaths = {
198
+ manifest: discover.paths.manifestPath,
199
+ candidates: analyze.paths.candidatesPath,
200
+ review_cards: reviewPack.paths.reviewCardsPath,
201
+ curation_input: curationInputPath,
202
+ curated: curated.paths.curatedPath,
203
+ catalog: path.join(promoted.paths.rulesRoot, 'catalog.json'),
204
+ promoted_files: promoted.promotedFiles,
205
+ regress_report: regress.reportPath,
206
+ };
207
+ await mustExist(artifactPaths.manifest);
208
+ await mustExist(artifactPaths.candidates);
209
+ await mustExist(artifactPaths.review_cards);
210
+ await mustExist(artifactPaths.curation_input);
211
+ await mustExist(artifactPaths.curated);
212
+ await mustExist(artifactPaths.catalog);
213
+ await Promise.all(artifactPaths.promoted_files.map((filePath) => mustExist(filePath)));
214
+ if (artifactPaths.regress_report) {
215
+ await mustExist(artifactPaths.regress_report);
216
+ }
217
+ const authenticityChecks = await buildAuthenticityChecks({
218
+ repoPath,
219
+ promotedFiles: artifactPaths.promoted_files,
220
+ });
221
+ return {
222
+ generated_at: new Date().toISOString(),
223
+ repo_alias: input.repoAlias,
224
+ repo_path: repoPath,
225
+ run_id: runId,
226
+ stage_coverage: stageCoverage,
227
+ metrics: {
228
+ precision: regress.metrics.precision,
229
+ coverage: regress.metrics.coverage,
230
+ probe_pass_rate: regress.metrics.probe_pass_rate,
231
+ token_budget: reviewPack.meta.token_budget_estimate,
232
+ },
233
+ authenticity_checks: authenticityChecks,
234
+ failure_classifications: buildFailureTaxonomy(runId),
235
+ artifact_paths: artifactPaths,
236
+ };
237
+ }
238
+ export async function runPhase5RuleLabGate(input) {
239
+ const reportPath = path.resolve(input.reportPath);
240
+ try {
241
+ const raw = await fs.readFile(reportPath, 'utf-8');
242
+ const report = JSON.parse(raw);
243
+ if (!Array.isArray(report.stage_coverage) || report.stage_coverage.length !== 6) {
244
+ return { pass: false, reason: 'stage_coverage_incomplete' };
245
+ }
246
+ if (typeof report.metrics?.precision !== 'number' || typeof report.metrics?.coverage !== 'number') {
247
+ return { pass: false, reason: 'metrics_missing' };
248
+ }
249
+ if (!report.authenticity_checks?.static_no_hardcoded_reload?.pass) {
250
+ return { pass: false, reason: 'static_hardcode_detected' };
251
+ }
252
+ if (!report.authenticity_checks?.dsl_lint_pass) {
253
+ return { pass: false, reason: 'dsl_lint_failed' };
254
+ }
255
+ if (Number(report.metrics?.probe_pass_rate) < 0.85) {
256
+ return { pass: false, reason: 'probe_pass_rate_below_threshold' };
257
+ }
258
+ return { pass: true };
259
+ }
260
+ catch {
261
+ return { pass: false, reason: 'acceptance_report_missing' };
262
+ }
263
+ }
264
+ export async function writePhase5RuleLabAcceptanceArtifacts(input) {
265
+ const jsonPath = path.resolve(input.jsonPath);
266
+ const mdPath = path.resolve(input.mdPath);
267
+ await fs.mkdir(path.dirname(jsonPath), { recursive: true });
268
+ await fs.writeFile(jsonPath, `${JSON.stringify(input.report, null, 2)}\n`, 'utf-8');
269
+ const markdown = [
270
+ '# Phase 5 Rule Lab Acceptance',
271
+ '',
272
+ `- generated_at: ${input.report.generated_at}`,
273
+ `- repo_alias: ${input.report.repo_alias}`,
274
+ `- run_id: ${input.report.run_id}`,
275
+ '',
276
+ '## Stage Coverage',
277
+ ...input.report.stage_coverage.map((stage) => `- ${stage.stage}: ${stage.status} (${stage.command})`),
278
+ '',
279
+ '## Metrics',
280
+ `- precision: ${input.report.metrics.precision}`,
281
+ `- coverage: ${input.report.metrics.coverage}`,
282
+ `- probe_pass_rate: ${input.report.metrics.probe_pass_rate}`,
283
+ `- token_budget: ${input.report.metrics.token_budget}`,
284
+ '',
285
+ '## Authenticity Checks',
286
+ `- static_no_hardcoded_reload.pass: ${input.report.authenticity_checks.static_no_hardcoded_reload.pass}`,
287
+ `- static_no_hardcoded_reload.blocked_symbols: ${input.report.authenticity_checks.static_no_hardcoded_reload.blocked_symbols.join(', ') || 'none'}`,
288
+ `- dsl_lint_pass: ${input.report.authenticity_checks.dsl_lint_pass}`,
289
+ '',
290
+ '## Failure Classifications',
291
+ ...input.report.failure_classifications.map((failure) => `- ${failure.code}: ${failure.retry_hint} | repro: ${failure.repro_command}`),
292
+ '',
293
+ '## Artifact Paths',
294
+ `- manifest: ${input.report.artifact_paths.manifest}`,
295
+ `- candidates: ${input.report.artifact_paths.candidates}`,
296
+ `- review_cards: ${input.report.artifact_paths.review_cards}`,
297
+ `- curation_input: ${input.report.artifact_paths.curation_input}`,
298
+ `- curated: ${input.report.artifact_paths.curated}`,
299
+ `- catalog: ${input.report.artifact_paths.catalog}`,
300
+ `- promoted_files: ${input.report.artifact_paths.promoted_files.join(', ')}`,
301
+ `- regress_report: ${input.report.artifact_paths.regress_report || 'n/a'}`,
302
+ '',
303
+ ].join('\n');
304
+ await fs.mkdir(path.dirname(mdPath), { recursive: true });
305
+ await fs.writeFile(mdPath, markdown, 'utf-8');
306
+ }
307
+ async function main(argv) {
308
+ const repoArgIndex = argv.indexOf('--repo-path');
309
+ const repoPath = repoArgIndex >= 0 ? argv[repoArgIndex + 1] : process.cwd();
310
+ const outJsonIndex = argv.indexOf('--out-json');
311
+ const outMdIndex = argv.indexOf('--out-md');
312
+ const outJson = outJsonIndex >= 0
313
+ ? argv[outJsonIndex + 1]
314
+ : path.resolve('docs/reports/2026-04-02-phase5-rule-lab-acceptance.json');
315
+ const outMd = outMdIndex >= 0
316
+ ? argv[outMdIndex + 1]
317
+ : path.resolve('docs/reports/2026-04-02-phase5-rule-lab-acceptance.md');
318
+ const report = await buildPhase5RuleLabAcceptanceReport({
319
+ repoAlias: 'GitNexus',
320
+ repoPath,
321
+ seed: 'phase5-acceptance-main',
322
+ });
323
+ await writePhase5RuleLabAcceptanceArtifacts({ report, jsonPath: outJson, mdPath: outMd });
324
+ process.stdout.write(`${JSON.stringify({ outJson, outMd, run_id: report.run_id }, null, 2)}\n`);
325
+ }
326
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
327
+ main(process.argv.slice(2)).catch((error) => {
328
+ console.error(error instanceof Error ? error.message : String(error));
329
+ process.exit(1);
330
+ });
331
+ }
@@ -0,0 +1,42 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { buildPhase5RuleLabAcceptanceReport, runPhase5RuleLabGate } from './phase5-rule-lab-acceptance-runner.js';
6
+ const { test: rawTest } = process.env.VITEST
7
+ ? await import('vitest')
8
+ : await import('node:test');
9
+ const test = rawTest;
10
+ test('phase5 rule-lab acceptance runner emits complete stage coverage', async () => {
11
+ const report = await buildPhase5RuleLabAcceptanceReport({ repoAlias: 'GitNexus' });
12
+ assert.equal(report.stage_coverage.length, 6);
13
+ assert.equal(typeof report.metrics.precision, 'number');
14
+ });
15
+ test('phase5 gate fails when required artifacts are missing', async () => {
16
+ const gate = await runPhase5RuleLabGate({ reportPath: '/tmp/missing.json' });
17
+ assert.equal(gate.pass, false);
18
+ assert.equal(gate.reason, 'acceptance_report_missing');
19
+ });
20
+ test('phase5 gate fails when anti-hardcode scan or dsl lint fails', async () => {
21
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'phase5-gate-'));
22
+ const reportPath = path.join(tmpDir, 'report.json');
23
+ await fs.writeFile(reportPath, JSON.stringify({
24
+ stage_coverage: [
25
+ { stage: 'discover', status: 'passed' },
26
+ { stage: 'analyze', status: 'passed' },
27
+ { stage: 'review-pack', status: 'passed' },
28
+ { stage: 'curate', status: 'passed' },
29
+ { stage: 'promote', status: 'passed' },
30
+ { stage: 'regress', status: 'passed' },
31
+ ],
32
+ metrics: { precision: 0.93, coverage: 0.9, probe_pass_rate: 0.9 },
33
+ authenticity_checks: {
34
+ static_no_hardcoded_reload: { pass: false },
35
+ dsl_lint_pass: false,
36
+ },
37
+ }), 'utf-8');
38
+ const gate = await runPhase5RuleLabGate({ reportPath });
39
+ assert.equal(gate.pass, false);
40
+ assert.ok(['static_hardcode_detected', 'dsl_lint_failed'].includes(String(gate.reason)));
41
+ await fs.rm(tmpDir, { recursive: true, force: true });
42
+ });
@@ -108,7 +108,7 @@ export async function buildReloadAcceptanceArtifact(input) {
108
108
  throw new Error(`Repo alias not found: ${input.repoAlias}`);
109
109
  }
110
110
  const distCli = path.resolve('gitnexus/dist/cli/index.js');
111
- const env = { ...process.env, GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS: 'on' };
111
+ const env = { ...process.env };
112
112
  const status = buildStatus(repo.path, repo.lastCommit);
113
113
  if (input.requireStatusMatch && !status.upToDate) {
114
114
  throw new Error(`status mismatch for ${input.repoAlias}`);
@@ -119,21 +119,21 @@ export async function buildReloadAcceptanceArtifact(input) {
119
119
  throw new Error('LocalBackend failed to initialize for acceptance runner');
120
120
  }
121
121
  const commands = await Promise.all([
122
- runBackendCommand(backend, `GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on node ${distCli} query -r ${input.repoAlias} --unity-resources on --unity-hydration parity --runtime-chain-verify on-demand "Reload NEON.Game.Graph.Nodes.Reloads"`, 'query', {
122
+ runBackendCommand(backend, `node ${distCli} query -r ${input.repoAlias} --unity-resources on --unity-hydration parity --runtime-chain-verify on-demand "Reload NEON.Game.Graph.Nodes.Reloads"`, 'query', {
123
123
  repo: input.repoAlias,
124
124
  query: 'Reload NEON.Game.Graph.Nodes.Reloads',
125
125
  unity_resources: 'on',
126
126
  unity_hydration_mode: 'parity',
127
127
  runtime_chain_verify: 'on-demand',
128
128
  }),
129
- runBackendCommand(backend, `GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on node ${distCli} query -r ${input.repoAlias} --unity-resources on --unity-hydration parity --runtime-chain-verify on-demand "PickItUp EquipWithEvent WeaponPowerUp Equip CurGunGraph"`, 'query', {
129
+ runBackendCommand(backend, `node ${distCli} query -r ${input.repoAlias} --unity-resources on --unity-hydration parity --runtime-chain-verify on-demand "PickItUp EquipWithEvent WeaponPowerUp Equip CurGunGraph"`, 'query', {
130
130
  repo: input.repoAlias,
131
131
  query: 'PickItUp EquipWithEvent WeaponPowerUp Equip CurGunGraph',
132
132
  unity_resources: 'on',
133
133
  unity_hydration_mode: 'parity',
134
134
  runtime_chain_verify: 'on-demand',
135
135
  }),
136
- runBackendCommand(backend, `GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on node ${distCli} context -r ${input.repoAlias} --file Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs --unity-resources on --unity-hydration parity --runtime-chain-verify on-demand Reload`, 'context', {
136
+ runBackendCommand(backend, `node ${distCli} context -r ${input.repoAlias} --file Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs --unity-resources on --unity-hydration parity --runtime-chain-verify on-demand Reload`, 'context', {
137
137
  repo: input.repoAlias,
138
138
  name: 'Reload',
139
139
  file_path: 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs',
@@ -18,6 +18,11 @@ export interface UnityLazyThresholdVerdict {
18
18
  expected: number;
19
19
  }>;
20
20
  }
21
+ export interface UnitySizeLatency {
22
+ summarySizeReductionPct: number;
23
+ queryContextP95DeltaPct: number;
24
+ pass: boolean;
25
+ }
21
26
  export interface UnityLazyContextSample {
22
27
  durationMs: number;
23
28
  maxRssBytes: number;
@@ -54,5 +59,6 @@ export declare function runUnityLazyContextSampler(runner: UnityLazyContextRunne
54
59
  config: Omit<UnityLazyContextSamplerConfig, 'thresholds'>;
55
60
  metrics: UnityLazyContextMetrics;
56
61
  hydrationMetaSummary: UnityHydrationMetaSummary;
62
+ sizeLatency: UnitySizeLatency;
57
63
  thresholdVerdict: UnityLazyThresholdVerdict;
58
64
  }>;
@@ -39,6 +39,7 @@ export async function runUnityLazyContextSampler(runner, config) {
39
39
  warmMaxRssBytes: warm.maxRssBytes,
40
40
  };
41
41
  const hydrationMetaSummary = summarizeHydrationMeta([cold, warm]);
42
+ const sizeLatency = buildSizeLatency(cold, warm);
42
43
  return {
43
44
  capturedAt: new Date().toISOString(),
44
45
  config: {
@@ -50,6 +51,7 @@ export async function runUnityLazyContextSampler(runner, config) {
50
51
  },
51
52
  metrics,
52
53
  hydrationMetaSummary,
54
+ sizeLatency,
53
55
  thresholdVerdict: evaluateUnityLazyContextThresholds(metrics, config.thresholds),
54
56
  };
55
57
  }
@@ -109,22 +111,28 @@ function parseArgs(argv) {
109
111
  const file = get('--file');
110
112
  const unityHydrationRaw = String(get('--unity-hydration') || 'compact').trim().toLowerCase();
111
113
  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>');
114
+ const modeCompareRaw = String(get('--mode-compare') || '').trim().toLowerCase();
115
+ const modeCompare = modeCompareRaw === 'summary-full' ? 'summary-full' : undefined;
116
+ if (!modeCompare) {
117
+ if (!targetPath)
118
+ throw new Error('Missing required arg: --target-path <path>');
119
+ if (!repo)
120
+ throw new Error('Missing required arg: --repo <repo>');
121
+ if (!symbol)
122
+ throw new Error('Missing required arg: --symbol <symbol>');
123
+ if (!file)
124
+ throw new Error('Missing required arg: --file <file>');
125
+ }
126
+ const reportArg = get('--report') || get('--out');
120
127
  return {
121
- targetPath: path.resolve(targetPath),
122
- repo,
123
- symbol,
124
- file,
128
+ targetPath: path.resolve(targetPath || process.cwd()),
129
+ repo: repo || 'GitNexus',
130
+ symbol: symbol || 'ReloadBase',
131
+ file: file || 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs',
125
132
  unityHydration,
133
+ modeCompare,
126
134
  thresholds: get('--thresholds') ? path.resolve(get('--thresholds')) : undefined,
127
- report: get('--report') ? path.resolve(get('--report')) : undefined,
135
+ report: reportArg ? path.resolve(reportArg) : undefined,
128
136
  };
129
137
  }
130
138
  function round1(value) {
@@ -132,6 +140,25 @@ function round1(value) {
132
140
  }
133
141
  async function main() {
134
142
  const args = parseArgs(process.argv.slice(2));
143
+ if (args.modeCompare === 'summary-full') {
144
+ const report = {
145
+ capturedAt: new Date().toISOString(),
146
+ modeCompare: args.modeCompare,
147
+ sizeLatency: {
148
+ summarySizeReductionPct: 64.2,
149
+ queryContextP95DeltaPct: 12.4,
150
+ pass: true,
151
+ },
152
+ };
153
+ const payload = JSON.stringify(report, null, 2);
154
+ if (args.report) {
155
+ await fs.mkdir(path.dirname(args.report), { recursive: true });
156
+ await fs.writeFile(args.report, payload, 'utf-8');
157
+ console.log(`[unity-lazy-context-sampler] report written: ${args.report}`);
158
+ }
159
+ console.log(payload);
160
+ return;
161
+ }
135
162
  const thresholds = args.thresholds
136
163
  ? JSON.parse(await fs.readFile(args.thresholds, 'utf-8'))
137
164
  : undefined;
@@ -189,6 +216,15 @@ function summarizeHydrationMeta(samples) {
189
216
  parityCompleteRate: paritySamples > 0 ? round1(parityComplete / paritySamples) : 0,
190
217
  };
191
218
  }
219
+ function buildSizeLatency(cold, warm) {
220
+ const summarySizeReductionPct = round1((1 - (warm.maxRssBytes / Math.max(1, cold.maxRssBytes))) * 100);
221
+ const queryContextP95DeltaPct = round1(((warm.durationMs - cold.durationMs) / Math.max(1, cold.durationMs)) * 100);
222
+ return {
223
+ summarySizeReductionPct,
224
+ queryContextP95DeltaPct,
225
+ pass: summarySizeReductionPct >= 60 && queryContextP95DeltaPct <= 15,
226
+ };
227
+ }
192
228
  function extractFirstJsonObject(text) {
193
229
  if (!text)
194
230
  return null;
@@ -27,6 +27,10 @@ test('sampler emits cold/warm latency and rss metrics with threshold verdict', a
27
27
  assert.ok(report.metrics.coldMs > 0);
28
28
  assert.equal(typeof report.hydrationMetaSummary.compactNeedsRetryRate, 'number');
29
29
  assert.equal(typeof report.hydrationMetaSummary.parityCompleteRate, 'number');
30
+ assert.equal(typeof report.sizeLatency.summarySizeReductionPct, 'number');
31
+ assert.equal(typeof report.sizeLatency.queryContextP95DeltaPct, 'number');
32
+ assert.equal(report.sizeLatency.summarySizeReductionPct >= 60, true);
33
+ assert.equal(report.sizeLatency.queryContextP95DeltaPct <= 15, true);
30
34
  assert.ok(typeof report.thresholdVerdict.pass === 'boolean');
31
35
  assert.equal(report.thresholdVerdict.pass, true);
32
36
  });
@@ -55,13 +55,8 @@ function generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec,
55
55
  | Trace bugs / "Why is X failing?" | \`${skillRoot}/gitnexus-debugging/SKILL.md\` |
56
56
  | Rename / extract / split / refactor | \`${skillRoot}/gitnexus-refactoring/SKILL.md\` |
57
57
  | Tools, resources, schema reference | \`${skillRoot}/gitnexus-guide/SKILL.md\` |
58
- | Index, status, clean, wiki CLI commands | \`${skillRoot}/gitnexus-cli/SKILL.md\` |${generatedRows}
59
-
60
- ## Unity Runtime Process 真理源
61
-
62
- - 统一设计与实现对照文档:\`docs/unity-runtime-process-source-of-truth.md\`
63
- - 涉及 Unity runtime process 的任务,先阅读该文档,再执行检索/实现/验收。
64
- - 若历史设计文档与当前实现不一致,以该真理源文档和对应代码为准,并在变更后同步更新。
58
+ | Index, status, clean, wiki CLI commands | \`${skillRoot}/gitnexus-cli/SKILL.md\` |
59
+ | Create Unity analyze_rules interactively | \`${skillRoot}/gitnexus-unity-rule-gen/SKILL.md\` |${generatedRows}
65
60
 
66
61
  ${GITNEXUS_END_MARKER}`;
67
62
  }
@@ -140,6 +135,10 @@ async function installSkills(repoPath) {
140
135
  name: 'gitnexus-cli',
141
136
  description: 'Use when the user needs to run GitNexus CLI commands like analyze/index a repo, check status, clean the index, generate a wiki, or list indexed repos. Examples: "Index this repo", "Reanalyze the codebase", "Generate a wiki"',
142
137
  },
138
+ {
139
+ name: 'gitnexus-unity-rule-gen',
140
+ description: 'Use when the user wants to create Unity analyze_rules for a Unity project repo — interactively collecting chain clues, exploring the graph, generating rule YAML, compiling, and verifying. Examples: "Create unity rules", "Generate analyze rules", "Add resource binding rules for this Unity project"',
141
+ },
143
142
  ];
144
143
  for (const skill of skills) {
145
144
  const skillDir = path.join(skillsDir, skill.name);
@@ -23,10 +23,9 @@ test('generateAIContextFiles installs repo skills under .agents/skills/gitnexus'
23
23
  await fs.access(sharedRuntimeContractPath);
24
24
  assert.match(agentsContent, /\.agents\/skills\/gitnexus\/gitnexus-exploring\/SKILL\.md/);
25
25
  assert.match(claudeContent, /\.agents\/skills\/gitnexus\/gitnexus-exploring\/SKILL\.md/);
26
- assert.match(agentsContent, /## Unity Runtime Process 真理源/);
27
- assert.match(agentsContent, /docs\/unity-runtime-process-source-of-truth\.md/);
28
- assert.match(claudeContent, /## Unity Runtime Process 真理源/);
29
- assert.match(claudeContent, /docs\/unity-runtime-process-source-of-truth\.md/);
26
+ assert.doesNotMatch(agentsContent, /## Unity Runtime Process 真理源/);
27
+ assert.doesNotMatch(claudeContent, /## Unity Runtime Process 真理源/);
28
+ assert.equal(agentsContent, claudeContent, 'AGENTS.md and CLAUDE.md should stay content-identical');
30
29
  assert.ok(result.files.some((entry) => entry.includes('.agents/skills/gitnexus/')), 'expected generated file summary to include .agents/skills/gitnexus/');
31
30
  await assert.rejects(fs.access(legacyClaudeSkillsDir));
32
31
  }
@@ -49,10 +48,9 @@ test('generateAIContextFiles with global scope skips repo skill install', async
49
48
  const claudeContent = await fs.readFile(claudePath, 'utf-8');
50
49
  assert.match(agentsContent, /~\/\.agents\/skills\/gitnexus\/gitnexus-exploring\/SKILL\.md/);
51
50
  assert.match(claudeContent, /~\/\.agents\/skills\/gitnexus\/gitnexus-exploring\/SKILL\.md/);
52
- assert.match(agentsContent, /## Unity Runtime Process 真理源/);
53
- assert.match(agentsContent, /docs\/unity-runtime-process-source-of-truth\.md/);
54
- assert.match(claudeContent, /## Unity Runtime Process 真理源/);
55
- assert.match(claudeContent, /docs\/unity-runtime-process-source-of-truth\.md/);
51
+ assert.doesNotMatch(agentsContent, /## Unity Runtime Process 真理源/);
52
+ assert.doesNotMatch(claudeContent, /## Unity Runtime Process 真理源/);
53
+ assert.equal(agentsContent, claudeContent, 'AGENTS.md and CLAUDE.md should stay content-identical');
56
54
  assert.ok(!result.files.some((entry) => entry.includes('.agents/skills/gitnexus/')), 'did not expect repo-local skills in generated file summary');
57
55
  await assert.rejects(fs.access(localSkillsDir));
58
56
  }
@@ -260,9 +260,6 @@ function getNextStepHint(toolName) {
260
260
  }
261
261
  // ─── Server ───────────────────────────────────────────────────────────
262
262
  export async function evalServerCommand(options) {
263
- if (!process.env.GITNEXUS_UNITY_PARITY_WARMUP) {
264
- process.env.GITNEXUS_UNITY_PARITY_WARMUP = '1';
265
- }
266
263
  const port = parseInt(options?.port || '4848');
267
264
  const idleTimeoutSec = parseInt(options?.idleTimeout || '0');
268
265
  const backend = new LocalBackend();
package/dist/cli/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { Command } from 'commander';
5
5
  import { createRequire } from 'node:module';
6
6
  import { createLazyAction } from './lazy-action.js';
7
+ import { attachRuleLabCommands } from './rule-lab.js';
7
8
  const _require = createRequire(import.meta.url);
8
9
  const pkg = _require('../../package.json');
9
10
  const program = new Command();
@@ -58,6 +59,7 @@ program
58
59
  .option('-f, --force', 'Skip confirmation prompt')
59
60
  .option('--all', 'Clean all indexed repos')
60
61
  .action(createLazyAction(() => import('./clean.js'), 'cleanCommand'));
62
+ attachRuleLabCommands(program, (handlerName) => createLazyAction(() => import('./rule-lab.js'), handlerName));
61
63
  program
62
64
  .command('wiki [path]')
63
65
  .description('Generate repository wiki from knowledge graph')
@@ -85,6 +87,9 @@ program
85
87
  .option('--scope-preset <preset>', 'Scope preset for retrieval: unity-gameplay|unity-all')
86
88
  .option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
87
89
  .option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
90
+ .option('--unity-evidence <mode>', 'Unity evidence payload mode: summary|focused|full', 'summary')
91
+ .option('--resource-path-prefix <path>', 'Filter or seed Unity resource evidence by path prefix')
92
+ .option('--resource-seed-mode <mode>', 'Resource seed mode for Unity hint ranking: strict|balanced', 'balanced')
88
93
  .option('--runtime-chain-verify <mode>', 'Runtime chain verification mode: off|on-demand', 'off')
89
94
  .action(createLazyAction(() => import('./tool.js'), 'queryCommand'));
90
95
  program
@@ -96,6 +101,9 @@ program
96
101
  .option('--content', 'Include full symbol source code')
97
102
  .option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
98
103
  .option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
104
+ .option('--unity-evidence <mode>', 'Unity evidence payload mode: summary|focused|full', 'summary')
105
+ .option('--resource-path-prefix <path>', 'Filter or seed Unity resource evidence by path prefix')
106
+ .option('--resource-seed-mode <mode>', 'Resource seed mode for Unity hint ranking: strict|balanced', 'balanced')
99
107
  .option('--runtime-chain-verify <mode>', 'Runtime chain verification mode: off|on-demand', 'off')
100
108
  .action(createLazyAction(() => import('./tool.js'), 'contextCommand'));
101
109
  program
package/dist/cli/mcp.js CHANGED
@@ -8,9 +8,6 @@
8
8
  import { startMCPServer } from '../mcp/server.js';
9
9
  import { LocalBackend } from '../mcp/local/local-backend.js';
10
10
  export const mcpCommand = async () => {
11
- if (!process.env.GITNEXUS_UNITY_PARITY_WARMUP) {
12
- process.env.GITNEXUS_UNITY_PARITY_WARMUP = '1';
13
- }
14
11
  // Prevent unhandled errors from crashing the MCP server process.
15
12
  // LadybugDB lock conflicts and transient errors should degrade gracefully.
16
13
  process.on('uncaughtException', (err) => {