@veewo/gitnexus 1.5.0 → 1.5.2

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 (137) hide show
  1. package/dist/benchmark/agent-context/runner.js +3 -0
  2. package/dist/benchmark/agent-context/runner.test.js +22 -0
  3. package/dist/benchmark/agent-context/tool-runner.d.ts +7 -6
  4. package/dist/benchmark/agent-safe-query-context/io.d.ts +2 -0
  5. package/dist/benchmark/agent-safe-query-context/io.js +86 -0
  6. package/dist/benchmark/agent-safe-query-context/io.test.d.ts +1 -0
  7. package/dist/benchmark/agent-safe-query-context/io.test.js +13 -0
  8. package/dist/benchmark/agent-safe-query-context/report.d.ts +57 -0
  9. package/dist/benchmark/agent-safe-query-context/report.js +159 -0
  10. package/dist/benchmark/agent-safe-query-context/report.test.d.ts +1 -0
  11. package/dist/benchmark/agent-safe-query-context/report.test.js +362 -0
  12. package/dist/benchmark/agent-safe-query-context/runner.d.ts +44 -0
  13. package/dist/benchmark/agent-safe-query-context/runner.js +406 -0
  14. package/dist/benchmark/agent-safe-query-context/runner.test.d.ts +1 -0
  15. package/dist/benchmark/agent-safe-query-context/runner.test.js +290 -0
  16. package/dist/benchmark/agent-safe-query-context/semantic-tuple.d.ts +20 -0
  17. package/dist/benchmark/agent-safe-query-context/semantic-tuple.js +225 -0
  18. package/dist/benchmark/agent-safe-query-context/semantic-tuple.test.d.ts +1 -0
  19. package/dist/benchmark/agent-safe-query-context/semantic-tuple.test.js +122 -0
  20. package/dist/benchmark/agent-safe-query-context/subagent-live.d.ts +47 -0
  21. package/dist/benchmark/agent-safe-query-context/subagent-live.js +128 -0
  22. package/dist/benchmark/agent-safe-query-context/subagent-live.test.d.ts +1 -0
  23. package/dist/benchmark/agent-safe-query-context/subagent-live.test.js +155 -0
  24. package/dist/benchmark/agent-safe-query-context/telemetry-tool.d.ts +9 -0
  25. package/dist/benchmark/agent-safe-query-context/telemetry-tool.js +77 -0
  26. package/dist/benchmark/agent-safe-query-context/types.d.ts +61 -0
  27. package/dist/benchmark/agent-safe-query-context/types.js +8 -0
  28. package/dist/benchmark/runtime-poc/provenance-artifact.d.ts +47 -0
  29. package/dist/benchmark/runtime-poc/provenance-artifact.js +89 -0
  30. package/dist/benchmark/runtime-poc/runner.d.ts +31 -0
  31. package/dist/benchmark/runtime-poc/runner.js +163 -0
  32. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +8 -0
  33. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +21 -0
  34. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +0 -1
  35. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +53 -51
  36. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +0 -1
  37. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +1 -1
  38. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +82 -18
  39. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +1 -2
  40. package/dist/benchmark/u2-e2e/retrieval-runner.js +15 -7
  41. package/dist/benchmark/u2-e2e/retrieval-runner.test.js +46 -0
  42. package/dist/cli/ai-context.js +2 -12
  43. package/dist/cli/ai-context.test.js +8 -0
  44. package/dist/cli/analyze-runtime-summary.js +1 -0
  45. package/dist/cli/analyze-runtime-summary.test.js +2 -0
  46. package/dist/cli/analyze-summary.d.ts +2 -0
  47. package/dist/cli/analyze-summary.js +24 -0
  48. package/dist/cli/analyze-summary.test.js +65 -1
  49. package/dist/cli/analyze.js +5 -1
  50. package/dist/cli/benchmark-agent-safe-query-context.d.ts +20 -0
  51. package/dist/cli/benchmark-agent-safe-query-context.js +39 -0
  52. package/dist/cli/benchmark-agent-safe-query-context.test.d.ts +1 -0
  53. package/dist/cli/benchmark-agent-safe-query-context.test.js +271 -0
  54. package/dist/cli/benchmark.d.ts +29 -0
  55. package/dist/cli/benchmark.js +55 -0
  56. package/dist/cli/index.js +23 -0
  57. package/dist/cli/rule-lab.d.ts +3 -7
  58. package/dist/cli/rule-lab.js +13 -22
  59. package/dist/cli/rule-lab.test.js +23 -3
  60. package/dist/cli/tool.d.ts +2 -0
  61. package/dist/cli/tool.js +2 -0
  62. package/dist/core/config/unity-config.d.ts +0 -1
  63. package/dist/core/config/unity-config.js +0 -1
  64. package/dist/core/ingestion/pipeline.js +35 -6
  65. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +18 -20
  66. package/dist/core/ingestion/unity-parity-seed.d.ts +2 -1
  67. package/dist/core/ingestion/unity-parity-seed.js +8 -0
  68. package/dist/core/ingestion/unity-resource-processor.d.ts +11 -0
  69. package/dist/core/ingestion/unity-resource-processor.js +102 -0
  70. package/dist/core/ingestion/unity-resource-processor.test.js +449 -0
  71. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +15 -0
  72. package/dist/core/ingestion/unity-runtime-binding-rules.js +178 -30
  73. package/dist/core/lbug/csv-generator.test.js +2 -2
  74. package/dist/core/unity/doc-contract.test.d.ts +1 -0
  75. package/dist/core/unity/doc-contract.test.js +30 -0
  76. package/dist/core/unity/prefab-source-scan.d.ts +25 -0
  77. package/dist/core/unity/prefab-source-scan.js +152 -0
  78. package/dist/core/unity/prefab-source-scan.test.d.ts +1 -0
  79. package/dist/core/unity/prefab-source-scan.test.js +70 -0
  80. package/dist/core/unity/scan-context.d.ts +12 -0
  81. package/dist/core/unity/scan-context.js +50 -2
  82. package/dist/core/unity/scan-context.test.js +74 -0
  83. package/dist/mcp/local/agent-safe-response.d.ts +10 -0
  84. package/dist/mcp/local/agent-safe-response.js +639 -0
  85. package/dist/mcp/local/derived-process-reader.js +1 -1
  86. package/dist/mcp/local/local-backend.d.ts +18 -1
  87. package/dist/mcp/local/local-backend.js +319 -125
  88. package/dist/mcp/local/process-confidence.d.ts +1 -2
  89. package/dist/mcp/local/process-confidence.js +0 -3
  90. package/dist/mcp/local/process-confidence.test.js +4 -2
  91. package/dist/mcp/local/process-evidence.d.ts +1 -8
  92. package/dist/mcp/local/process-evidence.js +1 -23
  93. package/dist/mcp/local/process-evidence.test.js +2 -16
  94. package/dist/mcp/local/process-ref.d.ts +1 -1
  95. package/dist/mcp/local/runtime-chain-closure-evaluator.d.ts +33 -0
  96. package/dist/mcp/local/runtime-chain-closure-evaluator.js +273 -0
  97. package/dist/mcp/local/runtime-chain-graph-candidates.d.ts +23 -0
  98. package/dist/mcp/local/runtime-chain-graph-candidates.js +131 -0
  99. package/dist/mcp/local/runtime-chain-verify.d.ts +1 -1
  100. package/dist/mcp/local/runtime-chain-verify.js +149 -138
  101. package/dist/mcp/local/runtime-chain-verify.test.js +126 -68
  102. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +4 -0
  103. package/dist/mcp/local/runtime-claim-rule-registry.js +4 -0
  104. package/dist/mcp/local/runtime-claim-rule-registry.test.js +37 -4
  105. package/dist/mcp/local/runtime-claim.d.ts +11 -0
  106. package/dist/mcp/local/runtime-claim.js +28 -0
  107. package/dist/mcp/local/unity-evidence-view.d.ts +1 -1
  108. package/dist/mcp/local/unity-evidence-view.js +1 -1
  109. package/dist/mcp/local/unity-evidence-view.test.js +22 -0
  110. package/dist/mcp/tools.js +51 -21
  111. package/dist/rule-lab/analyze.d.ts +2 -1
  112. package/dist/rule-lab/analyze.js +94 -59
  113. package/dist/rule-lab/analyze.test.js +238 -20
  114. package/dist/rule-lab/curate.d.ts +2 -1
  115. package/dist/rule-lab/curate.js +24 -3
  116. package/dist/rule-lab/curate.test.js +65 -0
  117. package/dist/rule-lab/curation-input-builder.d.ts +45 -0
  118. package/dist/rule-lab/curation-input-builder.js +133 -0
  119. package/dist/rule-lab/promote.js +80 -7
  120. package/dist/rule-lab/promote.test.js +150 -0
  121. package/dist/rule-lab/review-pack.d.ts +3 -0
  122. package/dist/rule-lab/review-pack.js +41 -1
  123. package/dist/rule-lab/review-pack.test.js +67 -0
  124. package/dist/rule-lab/types.d.ts +29 -0
  125. package/dist/types/pipeline.d.ts +3 -0
  126. package/package.json +4 -3
  127. package/scripts/run-node-tests.mjs +61 -0
  128. package/skills/_shared/unity-rule-authoring-contract.md +64 -0
  129. package/skills/_shared/unity-runtime-process-contract.md +16 -0
  130. package/skills/gitnexus-cli.md +8 -0
  131. package/skills/gitnexus-debugging.md +9 -0
  132. package/skills/gitnexus-exploring.md +66 -18
  133. package/skills/gitnexus-guide.md +42 -3
  134. package/skills/gitnexus-impact-analysis.md +8 -0
  135. package/skills/gitnexus-pr-review.md +8 -0
  136. package/skills/gitnexus-refactoring.md +8 -0
  137. package/skills/gitnexus-unity-rule-gen.md +66 -312
@@ -5,10 +5,43 @@ import path from 'node:path';
5
5
  import { test } from 'vitest';
6
6
  import { RuleRegistryLoadError, loadRuleRegistry } from './runtime-claim-rule-registry.js';
7
7
  test('loads active runtime claim rules from project catalog', async () => {
8
- const repoPath = path.resolve('.');
9
- const registry = await loadRuleRegistry(repoPath);
10
- assert.equal(registry.activeRules[0].id, 'unity.gungraph.reload.output-getvalue.v1');
11
- assert.equal(registry.activeRules[0].version, '1.0.0');
8
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-runtime-claim-rules-'));
9
+ const repoPath = path.join(tempRoot, 'repo');
10
+ const rulesRoot = path.join(repoPath, '.gitnexus', 'rules');
11
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
12
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({
13
+ rules: [
14
+ {
15
+ id: 'demo.reload.rule.v1',
16
+ version: '1.2.3',
17
+ file: 'approved/demo.reload.rule.v1.yaml',
18
+ },
19
+ ],
20
+ }), 'utf-8');
21
+ await fs.writeFile(path.join(rulesRoot, 'approved', 'demo.reload.rule.v1.yaml'), [
22
+ 'id: demo.reload.rule.v1',
23
+ 'version: 1.2.3',
24
+ 'trigger_family: reload',
25
+ 'resource_types:',
26
+ ' - asset',
27
+ 'host_base_type:',
28
+ ' - ReloadBase',
29
+ 'required_hops:',
30
+ ' - resource',
31
+ 'guarantees:',
32
+ ' - reload_chain_closed',
33
+ 'non_guarantees:',
34
+ ' - no_runtime_execution',
35
+ 'next_action: gitnexus query "reload"',
36
+ ].join('\n'), 'utf-8');
37
+ try {
38
+ const registry = await loadRuleRegistry(repoPath);
39
+ assert.equal(registry.activeRules[0].id, 'demo.reload.rule.v1');
40
+ assert.equal(registry.activeRules[0].version, '1.2.3');
41
+ }
42
+ finally {
43
+ await fs.rm(tempRoot, { recursive: true, force: true });
44
+ }
12
45
  });
13
46
  test('throws rule_catalog_missing when target repo has no catalog (no ancestor fallback)', async () => {
14
47
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-runtime-claim-rules-'));
@@ -16,9 +16,19 @@ export interface RuntimeClaim {
16
16
  non_guarantees: string[];
17
17
  hops: RuntimeChainHop[];
18
18
  gaps: RuntimeChainGap[];
19
+ verification_core_status?: 'verified_full' | 'failed';
20
+ verification_core_evidence_level?: RuntimeChainEvidenceLevel;
21
+ policy_adjusted?: boolean;
22
+ policy_adjust_reason?: string;
19
23
  reason?: RuntimeClaimReason;
20
24
  next_action?: string;
21
25
  }
26
+ type HydrationPolicyOption = 'fast' | 'balanced' | 'strict';
27
+ export declare function adjustRuntimeClaimForPolicy(input: {
28
+ claim: RuntimeClaim;
29
+ hydrationPolicy: HydrationPolicyOption;
30
+ fallbackToCompact: boolean;
31
+ }): RuntimeClaim;
22
32
  export declare function buildRuntimeClaimFromRule(input: {
23
33
  rule: RuntimeClaimRule;
24
34
  status: Exclude<RuntimeChainStatus, 'pending'>;
@@ -36,3 +46,4 @@ export declare function buildReloadRuntimeClaim(input: {
36
46
  reason?: RuntimeClaimReason;
37
47
  next_action?: string;
38
48
  }): RuntimeClaim;
49
+ export {};
@@ -1,3 +1,31 @@
1
+ export function adjustRuntimeClaimForPolicy(input) {
2
+ const base = input.claim;
3
+ const coreStatus = base.status === 'verified_full' ? 'verified_full' : 'failed';
4
+ const adjusted = {
5
+ ...base,
6
+ verification_core_status: coreStatus,
7
+ verification_core_evidence_level: base.evidence_level,
8
+ policy_adjusted: false,
9
+ };
10
+ if (input.hydrationPolicy === 'strict' && input.fallbackToCompact) {
11
+ let changed = false;
12
+ if (adjusted.status === 'verified_full') {
13
+ adjusted.status = 'verified_partial';
14
+ changed = true;
15
+ }
16
+ if (adjusted.evidence_level === 'verified_chain' || adjusted.status === 'verified_partial') {
17
+ if (adjusted.evidence_level !== 'verified_segment') {
18
+ adjusted.evidence_level = 'verified_segment';
19
+ changed = true;
20
+ }
21
+ }
22
+ if (changed) {
23
+ adjusted.policy_adjusted = true;
24
+ adjusted.policy_adjust_reason = 'strict_fallback_to_compact';
25
+ }
26
+ }
27
+ return adjusted;
28
+ }
1
29
  function resolveNonGuarantees(rule) {
2
30
  if (rule && Array.isArray(rule.non_guarantees) && rule.non_guarantees.length > 0) {
3
31
  return [...rule.non_guarantees];
@@ -10,7 +10,7 @@ export interface UnityEvidenceMeta {
10
10
  }
11
11
  export interface UnityEvidenceViewResult {
12
12
  resourceBindings: ResolvedUnityBinding[];
13
- serializedFields: UnitySerializedFields;
13
+ serializedFields?: UnitySerializedFields;
14
14
  evidence_meta: UnityEvidenceMeta;
15
15
  filter_diagnostics: string[];
16
16
  }
@@ -89,7 +89,7 @@ export function buildUnityEvidenceView(input) {
89
89
  };
90
90
  return {
91
91
  resourceBindings: filtered,
92
- serializedFields: aggregateSerializedFields(filtered),
92
+ serializedFields: input.mode === 'full' ? aggregateSerializedFields(filtered) : undefined,
93
93
  evidence_meta,
94
94
  filter_diagnostics: diagnostics,
95
95
  };
@@ -36,4 +36,26 @@ test('unity evidence view emits truncation metadata and fetch hint', () => {
36
36
  assert.equal(out.evidence_meta.truncated, true);
37
37
  assert.ok(out.evidence_meta.omitted_count > 0);
38
38
  assert.match(out.evidence_meta.next_fetch_hint || '', /unity_evidence_mode=full/i);
39
+ assert.equal(out.serializedFields, undefined);
40
+ });
41
+ test('unity evidence view keeps serialized fields in full mode', () => {
42
+ const out = buildUnityEvidenceView({
43
+ resourceBindings: [
44
+ {
45
+ resourcePath: 'Assets/A.prefab',
46
+ resourceType: 'prefab',
47
+ bindingKind: 'direct',
48
+ componentObjectId: '1',
49
+ evidence: { line: 1, lineText: 'x' },
50
+ serializedFields: {
51
+ scalarFields: [{ name: 'scalarA', sourceLayer: 'base' }],
52
+ referenceFields: [{ name: 'refA', sourceLayer: 'base' }],
53
+ },
54
+ resolvedReferences: [],
55
+ },
56
+ ],
57
+ mode: 'full',
58
+ });
59
+ assert.equal(out.serializedFields?.scalarFields.length, 1);
60
+ assert.equal(out.serializedFields?.referenceFields.length, 1);
39
61
  });
package/dist/mcp/tools.js CHANGED
@@ -34,19 +34,32 @@ Returns results grouped by process (execution flow):
34
34
  - processes: ranked execution flows with relevance priority
35
35
  - process_symbols: all symbols in those flows with file locations and module (functional area)
36
36
  - definitions: standalone types/interfaces not in any process
37
- - processes[].evidence_mode: direct_step | method_projected | resource_heuristic
37
+ - processes[].evidence_mode: direct_step | method_projected
38
38
  - processes[].confidence: high | medium | low
39
39
  - processes[].process_subtype: unity_lifecycle | static_calls (when persisted metadata exists)
40
40
  - processes[].runtime_chain_confidence: high | medium | low
41
41
  - processes[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
42
42
  - processes[].verification_hint: { action, target, next_command } (required when confidence=low)
43
- - process_symbols[].process_evidence_mode: direct_step | method_projected | resource_heuristic
43
+ - process_symbols[].process_evidence_mode: direct_step | method_projected
44
44
  - process_symbols[].process_confidence: high | medium | low
45
45
  - process_symbols[].process_subtype: unity_lifecycle | static_calls (when persisted metadata exists)
46
46
  - process_symbols[].runtime_chain_confidence: high | medium | low
47
47
  - process_symbols[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
48
48
  - process_symbols[].verification_hint: { action, target, next_command }
49
49
 
50
+ Default response_profile=slim shape:
51
+ - summary, candidates, process_hints, resource_hints, decision, upgrade_hints, runtime_preview
52
+ - facts, closure, clues, tier_envelope
53
+ - missing_proof_targets, suggested_context_targets
54
+ - read order in strict-anchor mode: facts -> closure -> clues
55
+ - suggested_context_targets[]: { name, uid?, filePath?, why } for direct context disambiguation
56
+ - upgrade_hints may include exact \`context --uid\` follow-ups when same-name symbols are ambiguous
57
+ - decision.recommended_follow_up prefers narrowing hints (for example resource_path_prefix/name) before response_profile=full fallback
58
+ - response_profile=slim is the default and sufficient for all normal agent workflows
59
+ - response_profile=full is for debugging and deep evidence inspection only
60
+ - recommended runtime retrieval sequence: discovery -> seed narrowing -> closure verification
61
+ - strong graph hops can coexist with failed closure when verifier-core remains failed
62
+
50
63
  Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.
51
64
  Supports optional scope controls for noisy codebases:
52
65
  - scope_preset=unity-gameplay to prioritize project gameplay code and suppress plugin-heavy paths.
@@ -56,6 +69,9 @@ Includes optional Unity retrieval contract:
56
69
  - Set unity_resources=on|auto to include Unity resource evidence.
57
70
  - Default unity_hydration_mode=compact (fast path).
58
71
  - Check response hydrationMeta: when needsParityRetry=true, rerun with unity_hydration_mode=parity for completeness.
72
+ - Runtime-chain semantics are two-layered:
73
+ - verifier-core: binary (verified_full | failed)
74
+ - policy-adjusted: query-visible result; under strict policy fallback (hydrationMeta.fallbackToCompact=true) this may downgrade to partial semantics.
59
75
  - Returns next_hops[] with ranked follow-up actions when Unity evidence is available.`,
60
76
  inputSchema: {
61
77
  type: 'object',
@@ -66,6 +82,12 @@ Includes optional Unity retrieval contract:
66
82
  limit: { type: 'number', description: 'Max processes to return (default: 5)', default: 5 },
67
83
  max_symbols: { type: 'number', description: 'Max symbols per process (default: 10)', default: 10 },
68
84
  include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
85
+ response_profile: {
86
+ type: 'string',
87
+ enum: ['slim', 'full'],
88
+ description: 'Response payload profile: slim (default, sufficient for normal workflows) or full (debug-only for deep evidence inspection).',
89
+ default: 'slim',
90
+ },
69
91
  scope_preset: {
70
92
  type: 'string',
71
93
  enum: ['unity-gameplay', 'unity-all'],
@@ -188,17 +210,32 @@ AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/pro
188
210
  Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.
189
211
 
190
212
  Process participation metadata:
191
- - processes[].evidence_mode: direct_step | method_projected | resource_heuristic
213
+ - processes[].evidence_mode: direct_step | method_projected
192
214
  - processes[].confidence: high | medium | low
193
215
  - processes[].process_subtype: unity_lifecycle | static_calls (when persisted metadata exists)
194
216
  - processes[].runtime_chain_confidence: high | medium | low
195
217
  - processes[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
196
218
  - processes[].verification_hint: { action, target, next_command } (required when confidence=low)
197
219
 
220
+ Default response_profile=slim shape:
221
+ - summary, symbol, incoming, outgoing, processes, resource_hints, verification_hint, upgrade_hints, runtime_preview
222
+ - facts, closure, clues, tier_envelope
223
+ - missing_proof_targets, suggested_context_targets
224
+ - read order in strict-anchor mode: facts -> closure -> clues
225
+ - suggested_context_targets[]: { name, uid?, filePath?, why } for direct context disambiguation
226
+ - upgrade_hints may include exact \`context --uid\` follow-ups when same-name symbols are ambiguous
227
+ - response_profile=slim is the default and sufficient for all normal agent workflows
228
+ - response_profile=full is for debugging and deep evidence inspection only
229
+ - recommended runtime retrieval sequence: discovery -> seed narrowing -> closure verification
230
+ - strong graph hops can coexist with failed closure when verifier-core remains failed
231
+
198
232
  Unity retrieval contract:
199
233
  - Set unity_resources=on|auto to include Unity resource evidence.
200
234
  - Default unity_hydration_mode=compact (fast path).
201
235
  - Check response hydrationMeta: when needsParityRetry=true, rerun with unity_hydration_mode=parity for completeness.
236
+ - Runtime-chain semantics are two-layered:
237
+ - verifier-core: binary (verified_full | failed)
238
+ - policy-adjusted: context-visible result; under strict policy fallback (hydrationMeta.fallbackToCompact=true) this may downgrade to partial semantics.
202
239
  - Returns next_hops[] with ranked follow-up actions when Unity evidence is available.`,
203
240
  inputSchema: {
204
241
  type: 'object',
@@ -207,6 +244,12 @@ Unity retrieval contract:
207
244
  uid: { type: 'string', description: 'Direct symbol UID from prior tool results (zero-ambiguity lookup)' },
208
245
  file_path: { type: 'string', description: 'File path to disambiguate common names' },
209
246
  include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
247
+ response_profile: {
248
+ type: 'string',
249
+ enum: ['slim', 'full'],
250
+ description: 'Response payload profile: slim (default, sufficient for normal workflows) or full (debug-only for deep evidence inspection).',
251
+ default: 'slim',
252
+ },
210
253
  unity_resources: {
211
254
  type: 'string',
212
255
  enum: ['off', 'on', 'auto'],
@@ -340,27 +383,14 @@ Output enforces unique-result policy and includes path+line evidence hops.`,
340
383
  required: ['target', 'goal'],
341
384
  },
342
385
  },
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
386
  {
357
387
  name: 'rule_lab_analyze',
358
- description: `Analyze one discovered Rule Lab slice and emit anchor-backed candidates.jsonl.`,
388
+ description: `Analyze one Rule Lab slice and emit anchor-backed candidates.jsonl.`,
359
389
  inputSchema: {
360
390
  type: 'object',
361
391
  properties: {
362
392
  run_id: { type: 'string', description: 'Rule Lab run id' },
363
- slice_id: { type: 'string', description: 'Slice id from discover manifest' },
393
+ slice_id: { type: 'string', description: 'Rule Lab slice id' },
364
394
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
365
395
  },
366
396
  required: ['run_id', 'slice_id'],
@@ -373,7 +403,7 @@ Output enforces unique-result policy and includes path+line evidence hops.`,
373
403
  type: 'object',
374
404
  properties: {
375
405
  run_id: { type: 'string', description: 'Rule Lab run id' },
376
- slice_id: { type: 'string', description: 'Slice id from discover manifest' },
406
+ slice_id: { type: 'string', description: 'Rule Lab slice id' },
377
407
  max_tokens: { type: 'number', description: 'Token budget cap (default: 6000)', default: 6000 },
378
408
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
379
409
  },
@@ -387,7 +417,7 @@ Output enforces unique-result policy and includes path+line evidence hops.`,
387
417
  type: 'object',
388
418
  properties: {
389
419
  run_id: { type: 'string', description: 'Rule Lab run id' },
390
- slice_id: { type: 'string', description: 'Slice id from discover manifest' },
420
+ slice_id: { type: 'string', description: 'Rule Lab slice id' },
391
421
  input_path: { type: 'string', description: 'Absolute or repo-relative path to curation input JSON' },
392
422
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
393
423
  },
@@ -401,7 +431,7 @@ Output enforces unique-result policy and includes path+line evidence hops.`,
401
431
  type: 'object',
402
432
  properties: {
403
433
  run_id: { type: 'string', description: 'Rule Lab run id' },
404
- slice_id: { type: 'string', description: 'Slice id from discover manifest' },
434
+ slice_id: { type: 'string', description: 'Rule Lab slice id' },
405
435
  version: { type: 'string', description: 'Promoted rule version (default: 1.0.0)', default: '1.0.0' },
406
436
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
407
437
  },
@@ -1,5 +1,5 @@
1
1
  import { getRuleLabPaths } from './paths.js';
2
- import type { RuleLabCandidate } from './types.js';
2
+ import type { RuleLabCandidate, RuleLabSlice } from './types.js';
3
3
  export interface AnalyzeInput {
4
4
  repoPath: string;
5
5
  runId: string;
@@ -8,5 +8,6 @@ export interface AnalyzeInput {
8
8
  export interface AnalyzeOutput {
9
9
  paths: ReturnType<typeof getRuleLabPaths>;
10
10
  candidates: RuleLabCandidate[];
11
+ slice: RuleLabSlice;
11
12
  }
12
13
  export declare function analyzeRuleLabSlice(input: AnalyzeInput): Promise<AnalyzeOutput>;
@@ -2,89 +2,124 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { getRuleLabPaths } from './paths.js';
5
+ import { buildCurationInput } from './curation-input-builder.js';
5
6
  function buildCandidateId(slice, variant) {
6
7
  return createHash('sha1')
7
8
  .update(`${slice.id}:${slice.trigger_family}:${slice.resource_types.join('|')}:${slice.host_base_type.join('|')}:${variant}`)
8
9
  .digest('hex')
9
10
  .slice(0, 12);
10
11
  }
11
- function toRate(numerator, denominator) {
12
- return numerator / Math.max(denominator, 1);
12
+ function normalizeToken(value) {
13
+ return String(value || '')
14
+ .trim()
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '');
13
18
  }
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]
19
+ function inferRuleStem(slice, pair) {
20
+ const source = normalizeToken(pair.source_anchor.symbol || pair.source_anchor.file || `${slice.id}-source`);
21
+ const target = normalizeToken(pair.target_anchor.symbol || pair.target_anchor.file || `${slice.id}-target`);
22
+ const joined = [source, target].filter(Boolean).join('-');
23
+ return joined || normalizeToken(slice.id) || 'runtime-rule';
24
+ }
25
+ function buildProposalTopology(slice) {
26
+ const requiredHops = Array.isArray(slice.required_hops) && slice.required_hops.length > 0
27
+ ? slice.required_hops
18
28
  : ['resource', 'code_runtime'];
19
- const primaryTopology = requiredHops.map((hop) => ({
29
+ return requiredHops.map((hop) => ({
20
30
  hop,
21
31
  from: { entity: hop === 'resource' ? 'resource' : 'script' },
22
32
  to: { entity: hop === 'code_runtime' ? 'runtime' : 'script' },
23
33
  edge: { kind: hop === 'resource' ? 'binds_script' : 'calls' },
24
34
  }));
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];
35
+ }
36
+ function buildExactPairCandidates(slice) {
37
+ const pairs = Array.isArray(slice.exact_pairs) ? slice.exact_pairs : [];
38
+ if (pairs.length === 0) {
39
+ throw new Error('exact_pairs must be non-empty for reduced rule-lab analyze flow');
40
+ }
41
+ const seenPairIds = new Set();
42
+ for (const pair of pairs) {
43
+ const pairId = String(pair.id || '').trim();
44
+ if (!pairId)
45
+ continue;
46
+ if (seenPairIds.has(pairId)) {
47
+ throw new Error(`duplicate_exact_pair_id: ${pairId}`);
48
+ }
49
+ seenPairIds.add(pairId);
50
+ }
51
+ const topology = buildProposalTopology(slice);
52
+ return pairs.map((pair, index) => {
53
+ const pairKey = String(pair.id || `${index + 1}`).trim();
54
+ const sourceAnchor = `${String(pair.source_anchor.file || '').trim()}:${Number(pair.source_anchor.line || 1)}`;
55
+ const targetAnchor = `${String(pair.target_anchor.file || '').trim()}:${Number(pair.target_anchor.line || 1)}`;
56
+ const draftRuleId = String(pair.draft_rule_id || '').trim() || `unity.event.${inferRuleStem(slice, pair)}.v1`;
57
+ const bindingKind = pair.binding_kind || 'method_triggers_method';
58
+ return {
59
+ id: buildCandidateId(slice, `exact:${pairKey}`),
60
+ title: `${slice.trigger_family} exact pair ${pairKey}`,
61
+ rule_hint: `${slice.trigger_family}.${slice.id}.exact.${pairKey}`,
62
+ proposal_kind: 'per_anchor_rule',
63
+ aggregation_mode: 'per_anchor_rules',
64
+ binding_kind: bindingKind,
65
+ draft_rule_id: draftRuleId,
66
+ topology,
67
+ closure: {
68
+ required_hops: topology.map((hop) => hop.hop),
69
+ failure_map: {
70
+ missing_evidence: 'rule_matched_but_evidence_missing',
71
+ },
72
+ },
73
+ claims: {
74
+ guarantees: [`exact pair linked: ${sourceAnchor} -> ${targetAnchor}`],
75
+ non_guarantees: ['sparse gap path only; no exhaustive discovery semantics'],
76
+ next_action: `gitnexus query "${slice.trigger_family}"`,
77
+ },
78
+ exact_pair: pair,
79
+ evidence: {
80
+ hops: [
81
+ {
82
+ hop_type: 'code_runtime',
83
+ anchor: sourceAnchor,
84
+ snippet: String(pair.source_anchor.symbol || 'source'),
85
+ },
86
+ {
87
+ hop_type: 'code_runtime',
88
+ anchor: targetAnchor,
89
+ snippet: String(pair.target_anchor.symbol || 'target'),
90
+ },
91
+ ],
92
+ },
93
+ };
94
+ });
95
+ }
96
+ function assertNoPlaceholderIds(runId, sliceId) {
97
+ const placeholderRe = /<[^>]+>|placeholder|todo|tbd/i;
98
+ if (placeholderRe.test(runId) || placeholderRe.test(sliceId)) {
99
+ throw new Error('placeholder run/slice ids are not allowed');
100
+ }
75
101
  }
76
102
  export async function analyzeRuleLabSlice(input) {
103
+ assertNoPlaceholderIds(input.runId, input.sliceId);
77
104
  const normalizedRepoPath = path.resolve(input.repoPath);
78
105
  const paths = getRuleLabPaths(normalizedRepoPath, input.runId, input.sliceId);
79
106
  const slicePath = path.join(paths.slicesRoot, input.sliceId, 'slice.json');
80
107
  const raw = await fs.readFile(slicePath, 'utf-8');
81
108
  const slice = JSON.parse(raw);
82
- const anchorFile = path.relative(normalizedRepoPath, slicePath).split(path.sep).join('/');
83
- const candidates = buildTopologyCandidateSet(slice, anchorFile);
109
+ const candidates = buildExactPairCandidates(slice);
110
+ const curation = buildCurationInput({
111
+ runId: input.runId,
112
+ sliceId: input.sliceId,
113
+ slice,
114
+ candidates,
115
+ });
84
116
  await fs.mkdir(path.dirname(paths.candidatesPath), { recursive: true });
85
117
  await fs.writeFile(paths.candidatesPath, `${candidates.map((candidate) => JSON.stringify(candidate)).join('\n')}\n`, 'utf-8');
118
+ await fs.writeFile(slicePath, `${JSON.stringify(slice, null, 2)}\n`, 'utf-8');
119
+ await fs.writeFile(path.join(path.dirname(paths.candidatesPath), 'curation-input.json'), `${JSON.stringify(curation, null, 2)}\n`, 'utf-8');
86
120
  return {
87
121
  paths,
88
122
  candidates,
123
+ slice,
89
124
  };
90
125
  }