@veewo/gitnexus 1.4.11-rc.2 → 1.5.0-rc.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.
- package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +55 -0
- package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +190 -0
- package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.test.js +13 -0
- package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.d.ts +22 -0
- package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.js +100 -0
- package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.js +13 -0
- package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +27 -0
- package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +118 -0
- package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +16 -0
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +60 -0
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +331 -0
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +42 -0
- package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.js +4 -4
- package/dist/benchmark/unity-lazy-context-sampler.d.ts +6 -0
- package/dist/benchmark/unity-lazy-context-sampler.js +49 -13
- package/dist/benchmark/unity-lazy-context-sampler.test.js +4 -0
- package/dist/cli/ai-context.js +6 -1
- package/dist/cli/eval-server.js +0 -3
- package/dist/cli/index.js +8 -0
- package/dist/cli/mcp.js +0 -3
- package/dist/cli/rule-lab.d.ts +42 -0
- package/dist/cli/rule-lab.js +157 -0
- package/dist/cli/rule-lab.test.d.ts +1 -0
- package/dist/cli/rule-lab.test.js +11 -0
- package/dist/cli/tool.d.ts +7 -1
- package/dist/cli/tool.js +6 -0
- package/dist/core/config/unity-config.d.ts +20 -0
- package/dist/core/config/unity-config.js +46 -0
- package/dist/core/graph/types.d.ts +1 -1
- package/dist/core/ingestion/pipeline.js +38 -13
- package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +0 -2
- package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +26 -213
- package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +1 -1
- package/dist/core/ingestion/unity-resource-processor.js +87 -22
- package/dist/core/ingestion/unity-resource-processor.test.js +67 -2
- package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +11 -0
- package/dist/core/ingestion/unity-runtime-binding-rules.js +179 -0
- package/dist/core/unity/options.d.ts +4 -0
- package/dist/core/unity/options.js +18 -0
- package/dist/core/unity/options.test.js +11 -1
- package/dist/core/unity/resolver.js +11 -1
- package/dist/core/unity/resolver.test.js +62 -0
- package/dist/core/unity/yaml-object-graph.js +1 -1
- package/dist/core/unity/yaml-object-graph.test.js +16 -0
- package/dist/mcp/local/derived-process-reader.d.ts +2 -0
- package/dist/mcp/local/derived-process-reader.js +15 -0
- package/dist/mcp/local/local-backend.d.ts +56 -0
- package/dist/mcp/local/local-backend.js +1003 -53
- package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
- package/dist/mcp/local/process-confidence.js +1 -1
- package/dist/mcp/local/process-evidence.d.ts +1 -0
- package/dist/mcp/local/process-evidence.js +22 -0
- package/dist/mcp/local/process-evidence.test.js +11 -1
- package/dist/mcp/local/process-ref.d.ts +24 -0
- package/dist/mcp/local/process-ref.js +33 -0
- package/dist/mcp/local/process-ref.test.d.ts +1 -0
- package/dist/mcp/local/process-ref.test.js +24 -0
- package/dist/mcp/local/runtime-chain-verify.d.ts +15 -1
- package/dist/mcp/local/runtime-chain-verify.js +191 -187
- package/dist/mcp/local/runtime-chain-verify.test.js +546 -19
- package/dist/mcp/local/runtime-claim-rule-registry.d.ts +63 -0
- package/dist/mcp/local/runtime-claim-rule-registry.js +308 -0
- package/dist/mcp/local/runtime-claim-rule-registry.test.d.ts +1 -0
- package/dist/mcp/local/runtime-claim-rule-registry.test.js +215 -0
- package/dist/mcp/local/runtime-claim.d.ts +38 -0
- package/dist/mcp/local/runtime-claim.js +54 -0
- package/dist/mcp/local/runtime-claim.test.d.ts +1 -0
- package/dist/mcp/local/runtime-claim.test.js +27 -0
- package/dist/mcp/local/unity-enrichment.d.ts +1 -0
- package/dist/mcp/local/unity-enrichment.js +1 -1
- package/dist/mcp/local/unity-evidence-view.d.ts +26 -0
- package/dist/mcp/local/unity-evidence-view.js +96 -0
- package/dist/mcp/local/unity-evidence-view.test.d.ts +1 -0
- package/dist/mcp/local/unity-evidence-view.test.js +39 -0
- package/dist/mcp/local/unity-lazy-hydrator.d.ts +2 -2
- package/dist/mcp/local/unity-lazy-hydrator.js +3 -3
- package/dist/mcp/local/unity-lazy-hydrator.test.js +4 -4
- package/dist/mcp/local/unity-parity-cache.js +2 -6
- package/dist/mcp/local/unity-parity-seed-loader.d.ts +1 -0
- package/dist/mcp/local/unity-parity-seed-loader.js +10 -16
- package/dist/mcp/local/unity-parity-seed-loader.test.js +3 -12
- package/dist/mcp/local/unity-runtime-hydration.d.ts +3 -2
- package/dist/mcp/local/unity-runtime-hydration.js +13 -16
- package/dist/mcp/local/unity-runtime-hydration.test.js +15 -1
- package/dist/mcp/resources.js +13 -0
- package/dist/mcp/tools.js +166 -13
- package/dist/rule-lab/analyze.d.ts +12 -0
- package/dist/rule-lab/analyze.js +90 -0
- package/dist/rule-lab/analyze.test.d.ts +1 -0
- package/dist/rule-lab/analyze.test.js +28 -0
- package/dist/rule-lab/compile.d.ts +5 -0
- package/dist/rule-lab/compile.js +51 -0
- package/dist/rule-lab/compiled-bundles.d.ts +30 -0
- package/dist/rule-lab/compiled-bundles.js +36 -0
- package/dist/rule-lab/curate.d.ts +32 -0
- package/dist/rule-lab/curate.js +134 -0
- package/dist/rule-lab/curate.test.d.ts +1 -0
- package/dist/rule-lab/curate.test.js +72 -0
- package/dist/rule-lab/discover.d.ts +13 -0
- package/dist/rule-lab/discover.js +74 -0
- package/dist/rule-lab/discover.test.d.ts +1 -0
- package/dist/rule-lab/discover.test.js +42 -0
- package/dist/rule-lab/paths.d.ts +21 -0
- package/dist/rule-lab/paths.js +37 -0
- package/dist/rule-lab/paths.test.d.ts +1 -0
- package/dist/rule-lab/paths.test.js +46 -0
- package/dist/rule-lab/promote.d.ts +26 -0
- package/dist/rule-lab/promote.js +314 -0
- package/dist/rule-lab/promote.test.d.ts +1 -0
- package/dist/rule-lab/promote.test.js +164 -0
- package/dist/rule-lab/regress.d.ts +60 -0
- package/dist/rule-lab/regress.js +122 -0
- package/dist/rule-lab/regress.test.d.ts +1 -0
- package/dist/rule-lab/regress.test.js +68 -0
- package/dist/rule-lab/review-pack.d.ts +31 -0
- package/dist/rule-lab/review-pack.js +125 -0
- package/dist/rule-lab/review-pack.test.d.ts +1 -0
- package/dist/rule-lab/review-pack.test.js +49 -0
- package/dist/rule-lab/types.d.ts +99 -0
- package/dist/rule-lab/types.js +1 -0
- package/package.json +1 -1
- package/skills/_shared/unity-hydration-contract.md +11 -0
- package/skills/_shared/unity-ui-trace-contract.md +33 -0
- package/skills/gitnexus-cli.md +14 -25
- package/skills/gitnexus-guide.md +2 -0
- package/skills/gitnexus-unity-rule-gen.md +318 -0
- package/dist/core/ingestion/unity-lifecycle-config.d.ts +0 -5
- package/dist/core/ingestion/unity-lifecycle-config.js +0 -25
- package/dist/mcp/local/unity-lazy-config.d.ts +0 -6
- package/dist/mcp/local/unity-lazy-config.js +0 -7
- package/dist/mcp/local/unity-lazy-config.test.js +0 -9
- package/dist/mcp/local/unity-process-confidence-config.d.ts +0 -1
- package/dist/mcp/local/unity-process-confidence-config.js +0 -4
- package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +0 -1
- package/dist/mcp/local/unity-runtime-chain-verify-config.js +0 -10
- /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
|
|
41
|
-
- processes[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
|
|
42
|
-
- processes[].verification_hint: { action, target, next_command } (required when confidence=low
|
|
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
|
|
47
|
-
- process_symbols[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
|
|
48
|
-
- process_symbols[].verification_hint: { action, target, next_command }
|
|
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: '
|
|
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
|
|
160
|
-
- processes[].runtime_chain_evidence_level: none | clue | verified_segment | verified_chain
|
|
161
|
-
- processes[].verification_hint: { action, target, next_command } (required when confidence=low
|
|
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: '
|
|
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,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 {};
|