@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
package/dist/mcp/tools.js CHANGED
@@ -37,15 +37,15 @@ Returns results grouped by process (execution flow):
37
37
  - processes[].evidence_mode: direct_step | method_projected | resource_heuristic
38
38
  - processes[].confidence: high | medium | low
39
39
  - processes[].process_subtype: unity_lifecycle | static_calls (when persisted metadata exists)
40
- - processes[].runtime_chain_confidence: high | medium | low (when GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on)
41
- - processes[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain (when GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on)
42
- - processes[].verification_hint: { action, target, next_command } (required when confidence=low and confidence fields flag is on)
40
+ - processes[].runtime_chain_confidence: high | medium | low
41
+ - processes[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
42
+ - processes[].verification_hint: { action, target, next_command } (required when confidence=low)
43
43
  - process_symbols[].process_evidence_mode: direct_step | method_projected | resource_heuristic
44
44
  - process_symbols[].process_confidence: high | medium | low
45
45
  - process_symbols[].process_subtype: unity_lifecycle | static_calls (when persisted metadata exists)
46
- - process_symbols[].runtime_chain_confidence: high | medium | low (when GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on)
47
- - process_symbols[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain (when GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on)
48
- - process_symbols[].verification_hint: { action, target, next_command } (when confidence fields flag is on)
46
+ - process_symbols[].runtime_chain_confidence: high | medium | low
47
+ - process_symbols[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
48
+ - process_symbols[].verification_hint: { action, target, next_command }
49
49
 
50
50
  Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.
51
51
  Supports optional scope controls for noisy codebases:
@@ -55,7 +55,8 @@ Supports optional scope controls for noisy codebases:
55
55
  Includes optional Unity retrieval contract:
56
56
  - Set unity_resources=on|auto to include Unity resource evidence.
57
57
  - Default unity_hydration_mode=compact (fast path).
58
- - Check response hydrationMeta: when needsParityRetry=true, rerun with unity_hydration_mode=parity for completeness.`,
58
+ - Check response hydrationMeta: when needsParityRetry=true, rerun with unity_hydration_mode=parity for completeness.
59
+ - Returns next_hops[] with ranked follow-up actions when Unity evidence is available.`,
59
60
  inputSchema: {
60
61
  type: 'object',
61
62
  properties: {
@@ -79,9 +80,43 @@ Includes optional Unity retrieval contract:
79
80
  unity_hydration_mode: {
80
81
  type: 'string',
81
82
  enum: ['parity', 'compact'],
82
- description: 'Unity hydration mode when unity_resources is enabled (default: compact)',
83
+ description: 'Execution-mode input for Unity hydration (default: compact). Can be overridden by hydration_policy; inspect hydrationMeta.requestedMode/effectiveMode/reason.',
83
84
  default: 'compact',
84
85
  },
86
+ unity_evidence_mode: {
87
+ type: 'string',
88
+ enum: ['summary', 'focused', 'full'],
89
+ description: 'Unity evidence payload mode (default: summary)',
90
+ default: 'summary',
91
+ },
92
+ hydration_policy: {
93
+ type: 'string',
94
+ enum: ['fast', 'balanced', 'strict'],
95
+ description: 'Hydration strategy policy (high-priority). strict->parity, fast->compact, balanced->uses unity_hydration_mode and may escalate to parity on missing evidence.',
96
+ default: 'balanced',
97
+ },
98
+ resource_path_prefix: {
99
+ type: 'string',
100
+ description: 'Optional resource-path prefix filter applied to Unity evidence bindings',
101
+ },
102
+ binding_kind: {
103
+ type: 'string',
104
+ description: 'Optional Unity binding kind filter (for example: direct, component, scriptable_object)',
105
+ },
106
+ max_bindings: {
107
+ type: 'number',
108
+ description: 'Optional cap for number of returned evidence bindings',
109
+ },
110
+ max_reference_fields: {
111
+ type: 'number',
112
+ description: 'Optional cap for number of reference fields returned per binding',
113
+ },
114
+ resource_seed_mode: {
115
+ type: 'string',
116
+ enum: ['strict', 'balanced'],
117
+ description: 'Resource-seed policy for Unity retrieval hints. strict prioritizes user-provided asset path and deterministic mapped assets.',
118
+ default: 'balanced',
119
+ },
85
120
  runtime_chain_verify: {
86
121
  type: 'string',
87
122
  enum: ['off', 'on-demand'],
@@ -156,14 +191,15 @@ Process participation metadata:
156
191
  - processes[].evidence_mode: direct_step | method_projected | resource_heuristic
157
192
  - processes[].confidence: high | medium | low
158
193
  - processes[].process_subtype: unity_lifecycle | static_calls (when persisted metadata exists)
159
- - processes[].runtime_chain_confidence: high | medium | low (when GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on)
160
- - processes[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain (when GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on)
161
- - processes[].verification_hint: { action, target, next_command } (required when confidence=low and confidence fields flag is on)
194
+ - processes[].runtime_chain_confidence: high | medium | low
195
+ - processes[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
196
+ - processes[].verification_hint: { action, target, next_command } (required when confidence=low)
162
197
 
163
198
  Unity retrieval contract:
164
199
  - Set unity_resources=on|auto to include Unity resource evidence.
165
200
  - Default unity_hydration_mode=compact (fast path).
166
- - Check response hydrationMeta: when needsParityRetry=true, rerun with unity_hydration_mode=parity for completeness.`,
201
+ - Check response hydrationMeta: when needsParityRetry=true, rerun with unity_hydration_mode=parity for completeness.
202
+ - Returns next_hops[] with ranked follow-up actions when Unity evidence is available.`,
167
203
  inputSchema: {
168
204
  type: 'object',
169
205
  properties: {
@@ -180,9 +216,43 @@ Unity retrieval contract:
180
216
  unity_hydration_mode: {
181
217
  type: 'string',
182
218
  enum: ['parity', 'compact'],
183
- description: 'Unity hydration mode when unity_resources is enabled (default: compact)',
219
+ description: 'Execution-mode input for Unity hydration (default: compact). Can be overridden by hydration_policy; inspect hydrationMeta.requestedMode/effectiveMode/reason.',
184
220
  default: 'compact',
185
221
  },
222
+ unity_evidence_mode: {
223
+ type: 'string',
224
+ enum: ['summary', 'focused', 'full'],
225
+ description: 'Unity evidence payload mode (default: summary)',
226
+ default: 'summary',
227
+ },
228
+ hydration_policy: {
229
+ type: 'string',
230
+ enum: ['fast', 'balanced', 'strict'],
231
+ description: 'Hydration strategy policy (high-priority). strict->parity, fast->compact, balanced->uses unity_hydration_mode and may escalate to parity on missing evidence.',
232
+ default: 'balanced',
233
+ },
234
+ resource_path_prefix: {
235
+ type: 'string',
236
+ description: 'Optional resource-path prefix filter applied to Unity evidence bindings',
237
+ },
238
+ binding_kind: {
239
+ type: 'string',
240
+ description: 'Optional Unity binding kind filter (for example: direct, component, scriptable_object)',
241
+ },
242
+ max_bindings: {
243
+ type: 'number',
244
+ description: 'Optional cap for number of returned evidence bindings',
245
+ },
246
+ max_reference_fields: {
247
+ type: 'number',
248
+ description: 'Optional cap for number of reference fields returned per binding',
249
+ },
250
+ resource_seed_mode: {
251
+ type: 'string',
252
+ enum: ['strict', 'balanced'],
253
+ description: 'Resource-seed policy for Unity retrieval hints. strict prioritizes user-provided asset path and deterministic mapped assets.',
254
+ default: 'balanced',
255
+ },
186
256
  runtime_chain_verify: {
187
257
  type: 'string',
188
258
  enum: ['off', 'on-demand'],
@@ -270,6 +340,89 @@ Output enforces unique-result policy and includes path+line evidence hops.`,
270
340
  required: ['target', 'goal'],
271
341
  },
272
342
  },
343
+ {
344
+ name: 'rule_lab_discover',
345
+ description: `Start a Rule Lab run by discovering deterministic slices and persisting a manifest under .gitnexus/rules/lab/runs.`,
346
+ inputSchema: {
347
+ type: 'object',
348
+ properties: {
349
+ scope: { type: 'string', enum: ['full', 'diff'], description: 'Discovery scope (default: full)' },
350
+ seed: { type: 'string', description: 'Optional deterministic seed' },
351
+ repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
352
+ },
353
+ required: [],
354
+ },
355
+ },
356
+ {
357
+ name: 'rule_lab_analyze',
358
+ description: `Analyze one discovered Rule Lab slice and emit anchor-backed candidates.jsonl.`,
359
+ inputSchema: {
360
+ type: 'object',
361
+ properties: {
362
+ run_id: { type: 'string', description: 'Rule Lab run id' },
363
+ slice_id: { type: 'string', description: 'Slice id from discover manifest' },
364
+ repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
365
+ },
366
+ required: ['run_id', 'slice_id'],
367
+ },
368
+ },
369
+ {
370
+ name: 'rule_lab_review_pack',
371
+ description: `Pack analyzed candidates into review cards with token budget enforcement.`,
372
+ inputSchema: {
373
+ type: 'object',
374
+ properties: {
375
+ run_id: { type: 'string', description: 'Rule Lab run id' },
376
+ slice_id: { type: 'string', description: 'Slice id from discover manifest' },
377
+ max_tokens: { type: 'number', description: 'Token budget cap (default: 6000)', default: 6000 },
378
+ repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
379
+ },
380
+ required: ['run_id', 'slice_id'],
381
+ },
382
+ },
383
+ {
384
+ name: 'rule_lab_curate',
385
+ description: `Validate human-curated semantic closure input and persist curated artifacts for promotion.`,
386
+ inputSchema: {
387
+ type: 'object',
388
+ properties: {
389
+ run_id: { type: 'string', description: 'Rule Lab run id' },
390
+ slice_id: { type: 'string', description: 'Slice id from discover manifest' },
391
+ input_path: { type: 'string', description: 'Absolute or repo-relative path to curation input JSON' },
392
+ repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
393
+ },
394
+ required: ['run_id', 'slice_id', 'input_path'],
395
+ },
396
+ },
397
+ {
398
+ name: 'rule_lab_promote',
399
+ description: `Promote curated candidates into approved YAML rules and upsert catalog.json entries.`,
400
+ inputSchema: {
401
+ type: 'object',
402
+ properties: {
403
+ run_id: { type: 'string', description: 'Rule Lab run id' },
404
+ slice_id: { type: 'string', description: 'Slice id from discover manifest' },
405
+ version: { type: 'string', description: 'Promoted rule version (default: 1.0.0)', default: '1.0.0' },
406
+ repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
407
+ },
408
+ required: ['run_id', 'slice_id'],
409
+ },
410
+ },
411
+ {
412
+ name: 'rule_lab_regress',
413
+ description: `Evaluate Rule Lab precision/coverage gates and optionally persist a regression report.`,
414
+ inputSchema: {
415
+ type: 'object',
416
+ properties: {
417
+ precision: { type: 'number', description: 'Observed precision metric' },
418
+ coverage: { type: 'number', description: 'Observed coverage metric' },
419
+ probes_path: { type: 'string', description: 'Optional path to a JSON array of regression probes with bucket metadata' },
420
+ run_id: { type: 'string', description: 'Optional run id for report naming' },
421
+ repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
422
+ },
423
+ required: ['precision', 'coverage'],
424
+ },
425
+ },
273
426
  {
274
427
  name: 'impact',
275
428
  description: `Analyze the blast radius of changing a code symbol.
@@ -0,0 +1,12 @@
1
+ import { getRuleLabPaths } from './paths.js';
2
+ import type { RuleLabCandidate } from './types.js';
3
+ export interface AnalyzeInput {
4
+ repoPath: string;
5
+ runId: string;
6
+ sliceId: string;
7
+ }
8
+ export interface AnalyzeOutput {
9
+ paths: ReturnType<typeof getRuleLabPaths>;
10
+ candidates: RuleLabCandidate[];
11
+ }
12
+ export declare function analyzeRuleLabSlice(input: AnalyzeInput): Promise<AnalyzeOutput>;
@@ -0,0 +1,90 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { getRuleLabPaths } from './paths.js';
5
+ function buildCandidateId(slice, variant) {
6
+ return createHash('sha1')
7
+ .update(`${slice.id}:${slice.trigger_family}:${slice.resource_types.join('|')}:${slice.host_base_type.join('|')}:${variant}`)
8
+ .digest('hex')
9
+ .slice(0, 12);
10
+ }
11
+ function toRate(numerator, denominator) {
12
+ return numerator / Math.max(denominator, 1);
13
+ }
14
+ function buildTopologyCandidateSet(slice, anchorFile) {
15
+ const title = `${slice.trigger_family} ${slice.host_base_type.join(', ') || 'runtime'}`.trim();
16
+ const requiredHops = slice.required_hops && slice.required_hops.length > 0
17
+ ? [...slice.required_hops]
18
+ : ['resource', 'code_runtime'];
19
+ const primaryTopology = requiredHops.map((hop) => ({
20
+ hop,
21
+ from: { entity: hop === 'resource' ? 'resource' : 'script' },
22
+ to: { entity: hop === 'code_runtime' ? 'runtime' : 'script' },
23
+ edge: { kind: hop === 'resource' ? 'binds_script' : 'calls' },
24
+ }));
25
+ const fallbackTopology = primaryTopology.slice(0, Math.max(primaryTopology.length - 1, 1));
26
+ const primaryCovered = primaryTopology.length;
27
+ const total = requiredHops.length;
28
+ const fallbackCovered = Math.min(fallbackTopology.length, total);
29
+ const fallbackMissingHop = requiredHops.find((hop) => !fallbackTopology.some((node) => node.hop === hop));
30
+ const primary = {
31
+ id: buildCandidateId(slice, 'primary'),
32
+ title: `${title} candidate-a`,
33
+ rule_hint: `${slice.trigger_family}.${slice.id}.primary`,
34
+ topology: primaryTopology,
35
+ stats: {
36
+ covered: primaryCovered,
37
+ total,
38
+ conflicts: 0,
39
+ coverage_rate: toRate(primaryCovered, total),
40
+ conflict_rate: 0,
41
+ },
42
+ counter_examples: [],
43
+ evidence: {
44
+ hops: primaryTopology.map((hop, index) => ({
45
+ hop_type: hop.hop,
46
+ anchor: `${anchorFile}:${index + 1}`,
47
+ snippet: `${slice.trigger_family}:${hop.edge.kind}`,
48
+ })),
49
+ },
50
+ };
51
+ const fallback = {
52
+ id: buildCandidateId(slice, 'fallback'),
53
+ title: `${title} candidate-b`,
54
+ rule_hint: `${slice.trigger_family}.${slice.id}.fallback`,
55
+ topology: fallbackTopology,
56
+ stats: {
57
+ covered: fallbackCovered,
58
+ total,
59
+ conflicts: 1,
60
+ coverage_rate: toRate(fallbackCovered, total),
61
+ conflict_rate: toRate(1, total),
62
+ },
63
+ counter_examples: fallbackMissingHop
64
+ ? [{ reason: 'required hop missing in topology candidate', missing_hop: fallbackMissingHop, evidence_anchor: `${anchorFile}:1` }]
65
+ : [],
66
+ evidence: {
67
+ hops: fallbackTopology.map((hop, index) => ({
68
+ hop_type: hop.hop,
69
+ anchor: `${anchorFile}:${index + 1}`,
70
+ snippet: `${slice.trigger_family}:${hop.edge.kind}`,
71
+ })),
72
+ },
73
+ };
74
+ return [primary, fallback];
75
+ }
76
+ export async function analyzeRuleLabSlice(input) {
77
+ const normalizedRepoPath = path.resolve(input.repoPath);
78
+ const paths = getRuleLabPaths(normalizedRepoPath, input.runId, input.sliceId);
79
+ const slicePath = path.join(paths.slicesRoot, input.sliceId, 'slice.json');
80
+ const raw = await fs.readFile(slicePath, 'utf-8');
81
+ const slice = JSON.parse(raw);
82
+ const anchorFile = path.relative(normalizedRepoPath, slicePath).split(path.sep).join('/');
83
+ const candidates = buildTopologyCandidateSet(slice, anchorFile);
84
+ await fs.mkdir(path.dirname(paths.candidatesPath), { recursive: true });
85
+ await fs.writeFile(paths.candidatesPath, `${candidates.map((candidate) => JSON.stringify(candidate)).join('\n')}\n`, 'utf-8');
86
+ return {
87
+ paths,
88
+ candidates,
89
+ };
90
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { analyzeRuleLabSlice } from './analyze.js';
7
+ describe('rule-lab analyze', () => {
8
+ it('analyze emits multiple topology candidates with coverage/conflict stats', async () => {
9
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rule-lab-analyze-'));
10
+ const runRoot = path.join(repoRoot, '.gitnexus', 'rules', 'lab', 'runs', 'run-x');
11
+ const sliceDir = path.join(runRoot, 'slices', 'slice-a');
12
+ await fs.mkdir(sliceDir, { recursive: true });
13
+ const here = path.dirname(fileURLToPath(import.meta.url));
14
+ const fixturePath = path.join(here, '__fixtures__', 'rule-lab-slice-input.json');
15
+ const fixtureRaw = await fs.readFile(fixturePath, 'utf-8');
16
+ await fs.writeFile(path.join(sliceDir, 'slice.json'), fixtureRaw, 'utf-8');
17
+ const result = await analyzeRuleLabSlice({ repoPath: repoRoot, runId: 'run-x', sliceId: 'slice-a' });
18
+ expect(result.candidates.length).toBeGreaterThan(1);
19
+ expect(result.candidates[0]).toHaveProperty('topology');
20
+ expect(result.candidates[0]).toHaveProperty('stats.coverage_rate');
21
+ expect(result.candidates[0]).toHaveProperty('stats.conflict_rate');
22
+ expect(result.candidates[0]).toHaveProperty('counter_examples');
23
+ expect(result.candidates[0].evidence.hops[0].anchor).toMatch(/:\d+$/);
24
+ const persisted = await fs.readFile(result.paths.candidatesPath, 'utf-8');
25
+ expect(persisted.trim().length).toBeGreaterThan(0);
26
+ await fs.rm(repoRoot, { recursive: true, force: true });
27
+ });
28
+ });
@@ -0,0 +1,5 @@
1
+ import { type RuleBundleFamily } from './compiled-bundles.js';
2
+ export declare function compileRules(options: {
3
+ repoPath?: string;
4
+ family?: RuleBundleFamily;
5
+ }): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { parseRuleYaml } from '../mcp/local/runtime-claim-rule-registry.js';
4
+ import { writeCompiledRuleBundle } from './compiled-bundles.js';
5
+ export async function compileRules(options) {
6
+ const repoPath = path.resolve(options.repoPath || process.cwd());
7
+ const family = options.family || 'analyze_rules';
8
+ const rulesRoot = path.join(repoPath, '.gitnexus', 'rules');
9
+ const catalogPath = path.join(rulesRoot, 'catalog.json');
10
+ let catalog;
11
+ try {
12
+ catalog = JSON.parse(await fs.readFile(catalogPath, 'utf-8'));
13
+ }
14
+ catch {
15
+ console.error(`No catalog.json found at ${catalogPath}`);
16
+ process.exitCode = 1;
17
+ return;
18
+ }
19
+ const entries = catalog.rules.filter((e) => e.enabled !== false && e.family === family);
20
+ if (entries.length === 0) {
21
+ console.log(`No enabled ${family} rules in catalog.`);
22
+ return;
23
+ }
24
+ const compiled = [];
25
+ for (const entry of entries) {
26
+ const yamlPath = path.join(rulesRoot, entry.file);
27
+ const raw = await fs.readFile(yamlPath, 'utf-8');
28
+ const rule = parseRuleYaml(raw, entry.file);
29
+ compiled.push({
30
+ id: rule.id,
31
+ version: rule.version,
32
+ trigger_family: rule.trigger_family,
33
+ trigger_tokens: [...(rule.match?.trigger_tokens || [])],
34
+ resource_types: [...rule.resource_types],
35
+ host_base_type: [...rule.host_base_type],
36
+ required_hops: [...rule.required_hops],
37
+ guarantees: [...rule.guarantees],
38
+ non_guarantees: [...rule.non_guarantees],
39
+ next_action: rule.next_action || '',
40
+ file_path: entry.file,
41
+ match: rule.match || { trigger_tokens: [] },
42
+ topology: [],
43
+ closure: { required_hops: rule.required_hops, failure_map: {} },
44
+ claims: { guarantees: rule.guarantees, non_guarantees: rule.non_guarantees, next_action: rule.next_action || '' },
45
+ ...(rule.resource_bindings ? { resource_bindings: rule.resource_bindings } : {}),
46
+ ...(rule.lifecycle_overrides ? { lifecycle_overrides: rule.lifecycle_overrides } : {}),
47
+ });
48
+ }
49
+ const outPath = await writeCompiledRuleBundle(rulesRoot, family, compiled);
50
+ console.log(`Compiled ${compiled.length} ${family} rules → ${outPath}`);
51
+ }
@@ -0,0 +1,30 @@
1
+ import type { RuleDslClaims, RuleDslClosure, RuleDslMatch, RuleDslTopologyHop } from './types.js';
2
+ export type RuleBundleFamily = 'analyze_rules' | 'retrieval_rules' | 'verification_rules';
3
+ export interface StageAwareCompiledRule {
4
+ id: string;
5
+ version: string;
6
+ trigger_family: string;
7
+ trigger_tokens: string[];
8
+ resource_types: string[];
9
+ host_base_type: string[];
10
+ required_hops: string[];
11
+ guarantees: string[];
12
+ non_guarantees: string[];
13
+ next_action: string;
14
+ file_path: string;
15
+ match: RuleDslMatch;
16
+ topology: RuleDslTopologyHop[];
17
+ closure: RuleDslClosure;
18
+ claims: RuleDslClaims;
19
+ resource_bindings?: import('./types.js').UnityResourceBinding[];
20
+ lifecycle_overrides?: import('./types.js').LifecycleOverrides;
21
+ }
22
+ export interface CompiledRuleBundle {
23
+ bundle_version: '2.0.0';
24
+ family: RuleBundleFamily;
25
+ generated_at: string;
26
+ rules: StageAwareCompiledRule[];
27
+ }
28
+ export declare function compiledBundlePath(rulesRoot: string, family: RuleBundleFamily): string;
29
+ export declare function writeCompiledRuleBundle(rulesRoot: string, family: RuleBundleFamily, rules: StageAwareCompiledRule[]): Promise<string>;
30
+ export declare function loadCompiledRuleBundle(repoPath: string, family: RuleBundleFamily, rulesRoot?: string): Promise<CompiledRuleBundle | undefined>;
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export function compiledBundlePath(rulesRoot, family) {
4
+ return path.join(path.resolve(rulesRoot), 'compiled', `${family}.v2.json`);
5
+ }
6
+ export async function writeCompiledRuleBundle(rulesRoot, family, rules) {
7
+ const outPath = compiledBundlePath(rulesRoot, family);
8
+ const bundle = {
9
+ bundle_version: '2.0.0',
10
+ family,
11
+ generated_at: new Date().toISOString(),
12
+ rules,
13
+ };
14
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
15
+ await fs.writeFile(outPath, `${JSON.stringify(bundle, null, 2)}\n`, 'utf-8');
16
+ return outPath;
17
+ }
18
+ export async function loadCompiledRuleBundle(repoPath, family, rulesRoot) {
19
+ const root = rulesRoot
20
+ ? path.resolve(rulesRoot)
21
+ : path.join(path.resolve(repoPath), '.gitnexus', 'rules');
22
+ const bundlePath = compiledBundlePath(root, family);
23
+ try {
24
+ const raw = await fs.readFile(bundlePath, 'utf-8');
25
+ const parsed = JSON.parse(raw);
26
+ if (parsed.family !== family || !Array.isArray(parsed.rules)) {
27
+ throw new Error(`Invalid compiled ${family} bundle: ${bundlePath}`);
28
+ }
29
+ return parsed;
30
+ }
31
+ catch (error) {
32
+ if (error?.code === 'ENOENT')
33
+ return undefined;
34
+ throw error;
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ import { getRuleLabPaths } from './paths.js';
2
+ import type { RuleDslMatch, RuleDslTopologyHop, RuleDslClosure, RuleDslClaims } from './types.js';
3
+ export interface CurateInput {
4
+ repoPath: string;
5
+ runId: string;
6
+ sliceId: string;
7
+ inputPath: string;
8
+ }
9
+ export interface CuratedStep {
10
+ hop_type?: string;
11
+ anchor: string;
12
+ snippet: string;
13
+ }
14
+ export interface CuratedItem {
15
+ id: string;
16
+ rule_id?: string;
17
+ title?: string;
18
+ match?: RuleDslMatch;
19
+ topology?: RuleDslTopologyHop[];
20
+ closure?: RuleDslClosure;
21
+ claims?: RuleDslClaims;
22
+ confirmed_chain: {
23
+ steps: CuratedStep[];
24
+ };
25
+ guarantees: string[];
26
+ non_guarantees: string[];
27
+ }
28
+ export interface CurateOutput {
29
+ paths: ReturnType<typeof getRuleLabPaths>;
30
+ curated: CuratedItem[];
31
+ }
32
+ export declare function curateRuleLabSlice(input: CurateInput): Promise<CurateOutput>;
@@ -0,0 +1,134 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getRuleLabPaths } from './paths.js';
4
+ const PLACEHOLDER_RE = /TODO|TBD|placeholder|<[^>]+>/i;
5
+ function hasPlaceholderText(value) {
6
+ return PLACEHOLDER_RE.test(String(value || ''));
7
+ }
8
+ function normalizeForSet(values) {
9
+ return new Set(values.map((value) => value.trim().toLowerCase()));
10
+ }
11
+ function ensureStringArray(values, field) {
12
+ if (!Array.isArray(values) || values.length === 0) {
13
+ throw new Error(`${field} must be non-empty`);
14
+ }
15
+ if (values.some((value) => !String(value || '').trim())) {
16
+ throw new Error(`${field} entries must be non-empty strings`);
17
+ }
18
+ return values.map((value) => String(value).trim());
19
+ }
20
+ function validateDslFields(item) {
21
+ if (!item.match || !Array.isArray(item.match.trigger_tokens) || item.match.trigger_tokens.length === 0) {
22
+ throw new Error('match.trigger_tokens must be non-empty');
23
+ }
24
+ ensureStringArray(item.match.trigger_tokens, 'match.trigger_tokens');
25
+ if (!Array.isArray(item.topology) || item.topology.length === 0) {
26
+ throw new Error('topology must be non-empty');
27
+ }
28
+ item.topology.forEach((hop, index) => {
29
+ if (!String(hop.hop || '').trim()) {
30
+ throw new Error(`topology[${index}].hop must be non-empty`);
31
+ }
32
+ if (!String(hop.edge?.kind || '').trim()) {
33
+ throw new Error(`topology[${index}].edge.kind must be non-empty`);
34
+ }
35
+ });
36
+ if (!item.closure || !Array.isArray(item.closure.required_hops) || item.closure.required_hops.length === 0) {
37
+ throw new Error('closure.required_hops must be non-empty');
38
+ }
39
+ if (!item.closure.failure_map || Object.keys(item.closure.failure_map).length === 0) {
40
+ throw new Error('closure.failure_map must be non-empty');
41
+ }
42
+ ensureStringArray(item.closure.required_hops, 'closure.required_hops');
43
+ if (!item.claims) {
44
+ throw new Error('claims must be present');
45
+ }
46
+ ensureStringArray(item.claims.guarantees, 'claims.guarantees');
47
+ ensureStringArray(item.claims.non_guarantees, 'claims.non_guarantees');
48
+ if (!String(item.claims.next_action || '').trim()) {
49
+ throw new Error('claims.next_action must be non-empty');
50
+ }
51
+ }
52
+ function validateCuratedItem(item) {
53
+ if (!Array.isArray(item.confirmed_chain?.steps) || item.confirmed_chain.steps.length === 0) {
54
+ throw new Error('confirmed_chain.steps must be non-empty for promotion');
55
+ }
56
+ item.confirmed_chain.steps.forEach((step, index) => {
57
+ if (!String(step.anchor || '').trim()) {
58
+ throw new Error(`confirmed_chain.steps[${index}].anchor must be non-empty`);
59
+ }
60
+ if (!String(step.snippet || '').trim()) {
61
+ throw new Error(`confirmed_chain.steps[${index}].snippet must be non-empty`);
62
+ }
63
+ if (hasPlaceholderText(step.anchor) || hasPlaceholderText(step.snippet)) {
64
+ throw new Error(`confirmed_chain.steps[${index}] contains placeholder text`);
65
+ }
66
+ });
67
+ if (!Array.isArray(item.guarantees) || item.guarantees.length === 0) {
68
+ throw new Error('guarantees must be non-empty');
69
+ }
70
+ if (!Array.isArray(item.non_guarantees) || item.non_guarantees.length === 0) {
71
+ throw new Error('non_guarantees must be non-empty');
72
+ }
73
+ if (item.guarantees.some((entry) => !String(entry || '').trim()) || item.non_guarantees.some((entry) => !String(entry || '').trim())) {
74
+ throw new Error('guarantees/non_guarantees entries must be non-empty strings');
75
+ }
76
+ if (hasPlaceholderText(JSON.stringify(item))) {
77
+ throw new Error('curated item contains placeholder text');
78
+ }
79
+ const guaranteeSet = normalizeForSet(item.guarantees);
80
+ const nonGuaranteeSet = normalizeForSet(item.non_guarantees);
81
+ const overlap = [...guaranteeSet].filter((entry) => nonGuaranteeSet.has(entry));
82
+ if (overlap.length === guaranteeSet.size && overlap.length === nonGuaranteeSet.size) {
83
+ throw new Error('guarantees and non_guarantees must have semantic distinction');
84
+ }
85
+ validateDslFields(item);
86
+ }
87
+ function toDslDraft(item) {
88
+ return {
89
+ id: String(item.rule_id || item.id || '').trim(),
90
+ version: '2.0.0',
91
+ match: item.match,
92
+ topology: item.topology,
93
+ closure: item.closure,
94
+ claims: item.claims,
95
+ };
96
+ }
97
+ function validateDslDraft(draft) {
98
+ if (!String(draft.id || '').trim()) {
99
+ throw new Error('dsl draft id must be non-empty');
100
+ }
101
+ ensureStringArray(draft.match.trigger_tokens, 'match.trigger_tokens');
102
+ if (!Array.isArray(draft.topology) || draft.topology.length === 0) {
103
+ throw new Error('topology must be non-empty');
104
+ }
105
+ ensureStringArray(draft.closure.required_hops, 'closure.required_hops');
106
+ if (!draft.closure.failure_map || Object.keys(draft.closure.failure_map).length === 0) {
107
+ throw new Error('closure.failure_map must be non-empty');
108
+ }
109
+ ensureStringArray(draft.claims.guarantees, 'claims.guarantees');
110
+ ensureStringArray(draft.claims.non_guarantees, 'claims.non_guarantees');
111
+ if (!String(draft.claims.next_action || '').trim()) {
112
+ throw new Error('claims.next_action must be non-empty');
113
+ }
114
+ }
115
+ export async function curateRuleLabSlice(input) {
116
+ const normalizedRepoPath = path.resolve(input.repoPath);
117
+ const paths = getRuleLabPaths(normalizedRepoPath, input.runId, input.sliceId);
118
+ const raw = await fs.readFile(path.resolve(input.inputPath), 'utf-8');
119
+ const parsed = JSON.parse(raw);
120
+ const curated = Array.isArray(parsed.curated) ? parsed.curated : [];
121
+ if (curated.length === 0) {
122
+ throw new Error('curated must contain at least one candidate');
123
+ }
124
+ curated.forEach(validateCuratedItem);
125
+ const firstDraft = toDslDraft(curated[0]);
126
+ validateDslDraft(firstDraft);
127
+ await fs.mkdir(path.dirname(paths.curatedPath), { recursive: true });
128
+ await fs.writeFile(paths.curatedPath, `${JSON.stringify({ run_id: input.runId, slice_id: input.sliceId, curated }, null, 2)}\n`, 'utf-8');
129
+ await fs.writeFile(path.join(path.dirname(paths.curatedPath), 'dsl-draft.json'), `${JSON.stringify(firstDraft, null, 2)}\n`, 'utf-8');
130
+ return {
131
+ paths,
132
+ curated,
133
+ };
134
+ }
@@ -0,0 +1 @@
1
+ export {};