@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.
Files changed (139) hide show
  1. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +55 -0
  2. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +190 -0
  3. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.test.js +13 -0
  4. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.d.ts +22 -0
  5. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.js +100 -0
  6. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.d.ts +1 -0
  7. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.js +13 -0
  8. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +27 -0
  9. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +118 -0
  10. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.d.ts +1 -0
  11. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +16 -0
  12. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +60 -0
  13. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +331 -0
  14. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.d.ts +1 -0
  15. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +42 -0
  16. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.js +4 -4
  17. package/dist/benchmark/unity-lazy-context-sampler.d.ts +6 -0
  18. package/dist/benchmark/unity-lazy-context-sampler.js +49 -13
  19. package/dist/benchmark/unity-lazy-context-sampler.test.js +4 -0
  20. package/dist/cli/ai-context.js +6 -1
  21. package/dist/cli/eval-server.js +0 -3
  22. package/dist/cli/index.js +8 -0
  23. package/dist/cli/mcp.js +0 -3
  24. package/dist/cli/rule-lab.d.ts +42 -0
  25. package/dist/cli/rule-lab.js +157 -0
  26. package/dist/cli/rule-lab.test.d.ts +1 -0
  27. package/dist/cli/rule-lab.test.js +11 -0
  28. package/dist/cli/tool.d.ts +7 -1
  29. package/dist/cli/tool.js +6 -0
  30. package/dist/core/config/unity-config.d.ts +20 -0
  31. package/dist/core/config/unity-config.js +46 -0
  32. package/dist/core/graph/types.d.ts +1 -1
  33. package/dist/core/ingestion/pipeline.js +38 -13
  34. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +0 -2
  35. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +26 -213
  36. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +1 -1
  37. package/dist/core/ingestion/unity-resource-processor.js +87 -22
  38. package/dist/core/ingestion/unity-resource-processor.test.js +67 -2
  39. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +11 -0
  40. package/dist/core/ingestion/unity-runtime-binding-rules.js +179 -0
  41. package/dist/core/unity/options.d.ts +4 -0
  42. package/dist/core/unity/options.js +18 -0
  43. package/dist/core/unity/options.test.js +11 -1
  44. package/dist/core/unity/resolver.js +11 -1
  45. package/dist/core/unity/resolver.test.js +62 -0
  46. package/dist/core/unity/yaml-object-graph.js +1 -1
  47. package/dist/core/unity/yaml-object-graph.test.js +16 -0
  48. package/dist/mcp/local/derived-process-reader.d.ts +2 -0
  49. package/dist/mcp/local/derived-process-reader.js +15 -0
  50. package/dist/mcp/local/local-backend.d.ts +56 -0
  51. package/dist/mcp/local/local-backend.js +1003 -53
  52. package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
  53. package/dist/mcp/local/process-confidence.js +1 -1
  54. package/dist/mcp/local/process-evidence.d.ts +1 -0
  55. package/dist/mcp/local/process-evidence.js +22 -0
  56. package/dist/mcp/local/process-evidence.test.js +11 -1
  57. package/dist/mcp/local/process-ref.d.ts +24 -0
  58. package/dist/mcp/local/process-ref.js +33 -0
  59. package/dist/mcp/local/process-ref.test.d.ts +1 -0
  60. package/dist/mcp/local/process-ref.test.js +24 -0
  61. package/dist/mcp/local/runtime-chain-verify.d.ts +15 -1
  62. package/dist/mcp/local/runtime-chain-verify.js +191 -187
  63. package/dist/mcp/local/runtime-chain-verify.test.js +546 -19
  64. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +63 -0
  65. package/dist/mcp/local/runtime-claim-rule-registry.js +308 -0
  66. package/dist/mcp/local/runtime-claim-rule-registry.test.d.ts +1 -0
  67. package/dist/mcp/local/runtime-claim-rule-registry.test.js +215 -0
  68. package/dist/mcp/local/runtime-claim.d.ts +38 -0
  69. package/dist/mcp/local/runtime-claim.js +54 -0
  70. package/dist/mcp/local/runtime-claim.test.d.ts +1 -0
  71. package/dist/mcp/local/runtime-claim.test.js +27 -0
  72. package/dist/mcp/local/unity-enrichment.d.ts +1 -0
  73. package/dist/mcp/local/unity-enrichment.js +1 -1
  74. package/dist/mcp/local/unity-evidence-view.d.ts +26 -0
  75. package/dist/mcp/local/unity-evidence-view.js +96 -0
  76. package/dist/mcp/local/unity-evidence-view.test.d.ts +1 -0
  77. package/dist/mcp/local/unity-evidence-view.test.js +39 -0
  78. package/dist/mcp/local/unity-lazy-hydrator.d.ts +2 -2
  79. package/dist/mcp/local/unity-lazy-hydrator.js +3 -3
  80. package/dist/mcp/local/unity-lazy-hydrator.test.js +4 -4
  81. package/dist/mcp/local/unity-parity-cache.js +2 -6
  82. package/dist/mcp/local/unity-parity-seed-loader.d.ts +1 -0
  83. package/dist/mcp/local/unity-parity-seed-loader.js +10 -16
  84. package/dist/mcp/local/unity-parity-seed-loader.test.js +3 -12
  85. package/dist/mcp/local/unity-runtime-hydration.d.ts +3 -2
  86. package/dist/mcp/local/unity-runtime-hydration.js +13 -16
  87. package/dist/mcp/local/unity-runtime-hydration.test.js +15 -1
  88. package/dist/mcp/resources.js +13 -0
  89. package/dist/mcp/tools.js +166 -13
  90. package/dist/rule-lab/analyze.d.ts +12 -0
  91. package/dist/rule-lab/analyze.js +90 -0
  92. package/dist/rule-lab/analyze.test.d.ts +1 -0
  93. package/dist/rule-lab/analyze.test.js +28 -0
  94. package/dist/rule-lab/compile.d.ts +5 -0
  95. package/dist/rule-lab/compile.js +51 -0
  96. package/dist/rule-lab/compiled-bundles.d.ts +30 -0
  97. package/dist/rule-lab/compiled-bundles.js +36 -0
  98. package/dist/rule-lab/curate.d.ts +32 -0
  99. package/dist/rule-lab/curate.js +134 -0
  100. package/dist/rule-lab/curate.test.d.ts +1 -0
  101. package/dist/rule-lab/curate.test.js +72 -0
  102. package/dist/rule-lab/discover.d.ts +13 -0
  103. package/dist/rule-lab/discover.js +74 -0
  104. package/dist/rule-lab/discover.test.d.ts +1 -0
  105. package/dist/rule-lab/discover.test.js +42 -0
  106. package/dist/rule-lab/paths.d.ts +21 -0
  107. package/dist/rule-lab/paths.js +37 -0
  108. package/dist/rule-lab/paths.test.d.ts +1 -0
  109. package/dist/rule-lab/paths.test.js +46 -0
  110. package/dist/rule-lab/promote.d.ts +26 -0
  111. package/dist/rule-lab/promote.js +314 -0
  112. package/dist/rule-lab/promote.test.d.ts +1 -0
  113. package/dist/rule-lab/promote.test.js +164 -0
  114. package/dist/rule-lab/regress.d.ts +60 -0
  115. package/dist/rule-lab/regress.js +122 -0
  116. package/dist/rule-lab/regress.test.d.ts +1 -0
  117. package/dist/rule-lab/regress.test.js +68 -0
  118. package/dist/rule-lab/review-pack.d.ts +31 -0
  119. package/dist/rule-lab/review-pack.js +125 -0
  120. package/dist/rule-lab/review-pack.test.d.ts +1 -0
  121. package/dist/rule-lab/review-pack.test.js +49 -0
  122. package/dist/rule-lab/types.d.ts +99 -0
  123. package/dist/rule-lab/types.js +1 -0
  124. package/package.json +1 -1
  125. package/skills/_shared/unity-hydration-contract.md +11 -0
  126. package/skills/_shared/unity-ui-trace-contract.md +33 -0
  127. package/skills/gitnexus-cli.md +14 -25
  128. package/skills/gitnexus-guide.md +2 -0
  129. package/skills/gitnexus-unity-rule-gen.md +318 -0
  130. package/dist/core/ingestion/unity-lifecycle-config.d.ts +0 -5
  131. package/dist/core/ingestion/unity-lifecycle-config.js +0 -25
  132. package/dist/mcp/local/unity-lazy-config.d.ts +0 -6
  133. package/dist/mcp/local/unity-lazy-config.js +0 -7
  134. package/dist/mcp/local/unity-lazy-config.test.js +0 -9
  135. package/dist/mcp/local/unity-process-confidence-config.d.ts +0 -1
  136. package/dist/mcp/local/unity-process-confidence-config.js +0 -4
  137. package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +0 -1
  138. package/dist/mcp/local/unity-runtime-chain-verify-config.js +0 -10
  139. /package/dist/{mcp/local/unity-lazy-config.test.d.ts → benchmark/u2-e2e/hydration-policy-repeatability-runner.test.d.ts} +0 -0
@@ -0,0 +1,308 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadCompiledRuleBundle } from '../../rule-lab/compiled-bundles.js';
4
+ export class RuleRegistryLoadError extends Error {
5
+ code;
6
+ details;
7
+ constructor(code, message, details) {
8
+ super(message);
9
+ this.name = 'RuleRegistryLoadError';
10
+ this.code = code;
11
+ this.details = details;
12
+ }
13
+ }
14
+ function decodeYamlScalar(raw) {
15
+ const trimmed = String(raw || '').trim();
16
+ if (trimmed.length < 2)
17
+ return trimmed;
18
+ const first = trimmed[0];
19
+ const last = trimmed[trimmed.length - 1];
20
+ if (first === '"' && last === '"') {
21
+ return trimmed
22
+ .slice(1, -1)
23
+ .replace(/\\"/g, '"')
24
+ .replace(/\\\\/g, '\\');
25
+ }
26
+ if (first === '\'' && last === '\'') {
27
+ return trimmed
28
+ .slice(1, -1)
29
+ .replace(/''/g, '\'');
30
+ }
31
+ return trimmed;
32
+ }
33
+ function readScalar(raw, key) {
34
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
35
+ const match = raw.match(new RegExp(`^${escaped}:\\s*(.+)$`, 'm'));
36
+ if (!match)
37
+ return undefined;
38
+ return decodeYamlScalar(match[1]);
39
+ }
40
+ function readList(raw, key) {
41
+ const lines = raw.split(/\r?\n/);
42
+ const start = lines.findIndex((line) => new RegExp(`^${key}:\\s*$`).test(line.trim()));
43
+ if (start < 0)
44
+ return [];
45
+ const out = [];
46
+ for (let i = start + 1; i < lines.length; i++) {
47
+ const line = lines[i];
48
+ if (!/^\s+-\s+/.test(line))
49
+ break;
50
+ out.push(decodeYamlScalar(line.replace(/^\s+-\s+/, '')));
51
+ }
52
+ return out;
53
+ }
54
+ function readSectionLines(raw, key) {
55
+ const lines = raw.split(/\r?\n/);
56
+ const sectionHeader = new RegExp(`^(\\s*)${key}:\\s*$`);
57
+ const start = lines.findIndex((line) => sectionHeader.test(line));
58
+ if (start < 0)
59
+ return [];
60
+ const startMatch = lines[start].match(sectionHeader);
61
+ const sectionIndent = startMatch ? startMatch[1].length : 0;
62
+ const out = [];
63
+ for (let i = start + 1; i < lines.length; i++) {
64
+ const line = lines[i];
65
+ if (!line.trim()) {
66
+ out.push(line);
67
+ continue;
68
+ }
69
+ const indent = (line.match(/^(\s*)/)?.[1] || '').length;
70
+ if (indent <= sectionIndent)
71
+ break;
72
+ out.push(line);
73
+ }
74
+ return out;
75
+ }
76
+ function readNestedList(raw, section, key) {
77
+ const sectionLines = readSectionLines(raw, section);
78
+ if (sectionLines.length === 0)
79
+ return [];
80
+ const nestedKey = new RegExp(`^(\\s*)${key}:\\s*$`);
81
+ const start = sectionLines.findIndex((line) => nestedKey.test(line.trimStart()));
82
+ if (start < 0)
83
+ return [];
84
+ const normalized = sectionLines.map((line) => line.replace(/^\s{2}/, ''));
85
+ const startMatch = normalized[start].match(nestedKey);
86
+ const baseIndent = startMatch ? startMatch[1].length : 0;
87
+ const out = [];
88
+ for (let i = start + 1; i < normalized.length; i++) {
89
+ const line = normalized[i];
90
+ if (!line.trim())
91
+ continue;
92
+ const indent = (line.match(/^(\s*)/)?.[1] || '').length;
93
+ if (indent <= baseIndent)
94
+ break;
95
+ if (!/^\s*-\s+/.test(line))
96
+ break;
97
+ out.push(decodeYamlScalar(line.replace(/^\s*-\s+/, '')));
98
+ }
99
+ return out;
100
+ }
101
+ function readNestedScalar(raw, section, key) {
102
+ const sectionLines = readSectionLines(raw, section).map((line) => line.replace(/^\s{2}/, ''));
103
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
+ const match = sectionLines
105
+ .map((line) => line.trimStart())
106
+ .join('\n')
107
+ .match(new RegExp(`^${escaped}:\\s*(.+)$`, 'm'));
108
+ if (!match)
109
+ return undefined;
110
+ return decodeYamlScalar(match[1]);
111
+ }
112
+ function majorVersion(version) {
113
+ const n = Number(String(version || '').trim().split('.')[0]);
114
+ return Number.isFinite(n) ? n : 0;
115
+ }
116
+ function assertDslShape(raw, filePath, version) {
117
+ if (majorVersion(version) < 2)
118
+ return;
119
+ const required = ['match', 'topology', 'closure', 'claims'];
120
+ const missing = required.filter((key) => !new RegExp(`^${key}:\\s*$`, 'm').test(raw));
121
+ if (missing.length > 0) {
122
+ throw new Error(`Rule yaml missing required DSL sections (${missing.join(', ')}): ${filePath}`);
123
+ }
124
+ }
125
+ export function parseRuleYaml(raw, filePath) {
126
+ const id = readScalar(raw, 'id');
127
+ const version = readScalar(raw, 'version');
128
+ if (!id || !version) {
129
+ throw new Error(`Rule yaml missing required id/version: ${filePath}`);
130
+ }
131
+ assertDslShape(raw, filePath, version);
132
+ const triggerTokens = readNestedList(raw, 'match', 'trigger_tokens');
133
+ const closureRequiredHops = readNestedList(raw, 'closure', 'required_hops');
134
+ const claimGuarantees = readNestedList(raw, 'claims', 'guarantees');
135
+ const claimNonGuarantees = readNestedList(raw, 'claims', 'non_guarantees');
136
+ const claimNextAction = readNestedScalar(raw, 'claims', 'next_action');
137
+ const legacyTriggerFamily = readScalar(raw, 'trigger_family');
138
+ const legacyRequiredHops = readList(raw, 'required_hops');
139
+ const legacyGuarantees = readList(raw, 'guarantees');
140
+ const legacyNonGuarantees = readList(raw, 'non_guarantees');
141
+ const legacyNextAction = readScalar(raw, 'next_action');
142
+ // Parse resource_bindings
143
+ const rbLines = readSectionLines(raw, 'resource_bindings');
144
+ let resource_bindings;
145
+ if (rbLines.length > 0) {
146
+ resource_bindings = [];
147
+ const joined = rbLines.map((l) => l.replace(/^\s{2}/, '')).join('\n');
148
+ const entries = joined.split(/(?=^\s*- kind:)/m).filter((s) => s.trim());
149
+ for (const entry of entries) {
150
+ const kindMatch = entry.match(/- kind:\s*(.+)/);
151
+ if (!kindMatch)
152
+ continue;
153
+ const binding = { kind: decodeYamlScalar(kindMatch[1]) };
154
+ const scalar = (k) => {
155
+ const m = entry.match(new RegExp(`^\\s+${k}:\\s*(.+)$`, 'm'));
156
+ return m ? decodeYamlScalar(m[1]) : undefined;
157
+ };
158
+ const list = (k) => {
159
+ const lines = entry.split('\n');
160
+ const idx = lines.findIndex((l) => new RegExp(`^\\s+${k}:\\s*$`).test(l));
161
+ if (idx < 0)
162
+ return undefined;
163
+ const out = [];
164
+ for (let i = idx + 1; i < lines.length; i++) {
165
+ if (!/^\s+-\s+/.test(lines[i]))
166
+ break;
167
+ out.push(decodeYamlScalar(lines[i].replace(/^\s+-\s+/, '')));
168
+ }
169
+ return out.length > 0 ? out : undefined;
170
+ };
171
+ binding.ref_field_pattern = scalar('ref_field_pattern');
172
+ binding.target_entry_points = list('target_entry_points');
173
+ binding.host_class_pattern = scalar('host_class_pattern');
174
+ binding.field_name = scalar('field_name');
175
+ binding.loader_methods = list('loader_methods');
176
+ resource_bindings.push(binding);
177
+ }
178
+ if (resource_bindings.length === 0)
179
+ resource_bindings = undefined;
180
+ }
181
+ // Parse lifecycle_overrides
182
+ const loEntryPoints = readNestedList(raw, 'lifecycle_overrides', 'additional_entry_points');
183
+ const loScope = readNestedScalar(raw, 'lifecycle_overrides', 'scope');
184
+ const lifecycle_overrides = loEntryPoints.length > 0 || loScope
185
+ ? {
186
+ ...(loEntryPoints.length > 0 ? { additional_entry_points: loEntryPoints } : {}),
187
+ ...(loScope ? { scope: loScope } : {}),
188
+ }
189
+ : undefined;
190
+ return {
191
+ id,
192
+ version,
193
+ trigger_family: legacyTriggerFamily || triggerTokens[0] || 'unknown',
194
+ resource_types: readList(raw, 'resource_types'),
195
+ host_base_type: readList(raw, 'host_base_type'),
196
+ match: {
197
+ trigger_tokens: triggerTokens,
198
+ symbol_kind: readNestedList(raw, 'match', 'symbol_kind'),
199
+ module_scope: readNestedList(raw, 'match', 'module_scope'),
200
+ resource_types: readNestedList(raw, 'match', 'resource_types'),
201
+ host_base_type: readNestedList(raw, 'match', 'host_base_type'),
202
+ },
203
+ required_hops: closureRequiredHops.length > 0 ? closureRequiredHops : legacyRequiredHops,
204
+ guarantees: claimGuarantees.length > 0 ? claimGuarantees : legacyGuarantees,
205
+ non_guarantees: claimNonGuarantees.length > 0 ? claimNonGuarantees : legacyNonGuarantees,
206
+ next_action: claimNextAction || legacyNextAction,
207
+ family: readScalar(raw, 'family') || 'verification_rules',
208
+ resource_bindings,
209
+ lifecycle_overrides,
210
+ file_path: filePath,
211
+ };
212
+ }
213
+ export async function loadRuleRegistry(repoPath, rulesRoot) {
214
+ const normalizedRepoPath = path.resolve(repoPath);
215
+ const root = rulesRoot
216
+ ? path.resolve(rulesRoot)
217
+ : path.join(normalizedRepoPath, '.gitnexus', 'rules');
218
+ const compiledVerificationBundle = await loadCompiledRuleBundle(normalizedRepoPath, 'verification_rules', root);
219
+ if (compiledVerificationBundle && compiledVerificationBundle.rules.length > 0) {
220
+ return {
221
+ repoPath: normalizedRepoPath,
222
+ rulesRoot: root,
223
+ catalogPath: path.join(root, 'compiled', 'verification_rules.v2.json'),
224
+ activeRules: compiledVerificationBundle.rules.map((rule) => ({
225
+ id: rule.id,
226
+ version: rule.version,
227
+ trigger_family: rule.trigger_family,
228
+ resource_types: rule.resource_types,
229
+ host_base_type: rule.host_base_type,
230
+ match: rule.match,
231
+ required_hops: rule.required_hops,
232
+ guarantees: rule.guarantees,
233
+ non_guarantees: rule.non_guarantees,
234
+ next_action: rule.next_action,
235
+ file_path: rule.file_path,
236
+ topology: rule.topology,
237
+ closure: rule.closure,
238
+ claims: rule.claims,
239
+ })),
240
+ };
241
+ }
242
+ const catalogPath = path.join(root, 'catalog.json');
243
+ let catalogRaw;
244
+ try {
245
+ catalogRaw = await fs.readFile(catalogPath, 'utf-8');
246
+ }
247
+ catch (error) {
248
+ if (error?.code === 'ENOENT') {
249
+ throw new RuleRegistryLoadError('rule_catalog_missing', `Runtime claim rule catalog not found: ${catalogPath}`, { repoPath: normalizedRepoPath, rulesRoot: root, catalogPath });
250
+ }
251
+ throw error;
252
+ }
253
+ let catalog;
254
+ try {
255
+ catalog = JSON.parse(catalogRaw);
256
+ }
257
+ catch {
258
+ throw new RuleRegistryLoadError('rule_catalog_invalid', `Runtime claim rule catalog is invalid JSON: ${catalogPath}`, { repoPath: normalizedRepoPath, rulesRoot: root, catalogPath });
259
+ }
260
+ const catalogRules = Array.isArray(catalog.rules) ? catalog.rules : [];
261
+ const activeRules = [];
262
+ for (const entry of catalogRules) {
263
+ if (entry.enabled === false)
264
+ continue;
265
+ const relativeRulePath = String(entry.file || path.join('approved', `${entry.id}.yaml`));
266
+ const rulePath = path.join(root, relativeRulePath);
267
+ let raw;
268
+ try {
269
+ raw = await fs.readFile(rulePath, 'utf-8');
270
+ }
271
+ catch (error) {
272
+ if (error?.code === 'ENOENT') {
273
+ throw new RuleRegistryLoadError('rule_file_missing', `Runtime claim rule file not found: ${rulePath}`, { repoPath: normalizedRepoPath, rulesRoot: root, catalogPath, rulePath, ruleId: entry.id });
274
+ }
275
+ throw error;
276
+ }
277
+ const parsed = parseRuleYaml(raw, rulePath);
278
+ if (parsed.id !== entry.id) {
279
+ throw new Error(`Rule id mismatch between catalog and yaml: ${entry.id} vs ${parsed.id}`);
280
+ }
281
+ activeRules.push({
282
+ ...parsed,
283
+ version: entry.version || parsed.version,
284
+ family: entry.family || parsed.family || 'verification_rules',
285
+ });
286
+ }
287
+ return {
288
+ repoPath: normalizedRepoPath,
289
+ rulesRoot: root,
290
+ catalogPath,
291
+ activeRules,
292
+ };
293
+ }
294
+ export async function loadAnalyzeRules(repoPath, rulesRoot) {
295
+ const normalizedRepoPath = path.resolve(repoPath);
296
+ const root = rulesRoot
297
+ ? path.resolve(rulesRoot)
298
+ : path.join(normalizedRepoPath, '.gitnexus', 'rules');
299
+ const analyzeBundle = await loadCompiledRuleBundle(normalizedRepoPath, 'analyze_rules', root);
300
+ if (analyzeBundle && analyzeBundle.rules.length > 0) {
301
+ return analyzeBundle.rules.map((rule) => ({
302
+ ...rule,
303
+ family: 'analyze_rules',
304
+ }));
305
+ }
306
+ const registry = await loadRuleRegistry(repoPath, rulesRoot);
307
+ return registry.activeRules.filter((r) => r.family === 'analyze_rules');
308
+ }
@@ -0,0 +1,215 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { test } from 'vitest';
6
+ import { RuleRegistryLoadError, loadRuleRegistry } from './runtime-claim-rule-registry.js';
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');
12
+ });
13
+ test('throws rule_catalog_missing when target repo has no catalog (no ancestor fallback)', async () => {
14
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-runtime-claim-rules-'));
15
+ const workspaceRoot = path.join(tempRoot, 'workspace');
16
+ const nestedCwd = path.join(workspaceRoot, 'packages', 'app');
17
+ const rulesRoot = path.join(workspaceRoot, '.gitnexus', 'rules');
18
+ const catalogPath = path.join(rulesRoot, 'catalog.json');
19
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
20
+ await fs.mkdir(nestedCwd, { recursive: true });
21
+ await fs.writeFile(catalogPath, JSON.stringify({
22
+ rules: [
23
+ {
24
+ id: 'demo.reload.rule.v1',
25
+ version: '1.2.3',
26
+ file: 'approved/demo.reload.rule.v1.yaml',
27
+ },
28
+ ],
29
+ }), 'utf-8');
30
+ await fs.writeFile(path.join(rulesRoot, 'approved', 'demo.reload.rule.v1.yaml'), ['id: demo.reload.rule.v1', 'version: 1.2.3', 'trigger_family: reload'].join('\n'), 'utf-8');
31
+ const originalCwd = process.cwd();
32
+ process.chdir(nestedCwd);
33
+ try {
34
+ await assert.rejects(() => loadRuleRegistry(path.join(tempRoot, 'does-not-exist')), (error) => {
35
+ assert.ok(error instanceof RuleRegistryLoadError);
36
+ assert.equal(error.code, 'rule_catalog_missing');
37
+ assert.match(String(error.message || ''), /catalog not found/i);
38
+ return true;
39
+ });
40
+ }
41
+ finally {
42
+ process.chdir(originalCwd);
43
+ await fs.rm(tempRoot, { recursive: true, force: true });
44
+ }
45
+ });
46
+ test('throws rule_catalog_missing when rulesRoot exists but catalog.json is missing', async () => {
47
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-runtime-claim-rules-'));
48
+ const repoPath = path.join(tempRoot, 'repo');
49
+ const rulesRoot = path.join(repoPath, '.gitnexus', 'rules');
50
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
51
+ try {
52
+ await assert.rejects(() => loadRuleRegistry(repoPath), (error) => {
53
+ assert.ok(error instanceof RuleRegistryLoadError);
54
+ assert.equal(error.code, 'rule_catalog_missing');
55
+ return true;
56
+ });
57
+ }
58
+ finally {
59
+ await fs.rm(tempRoot, { recursive: true, force: true });
60
+ }
61
+ });
62
+ test('throws rule_file_missing when catalog entry points to missing yaml file', async () => {
63
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-runtime-claim-rules-'));
64
+ const repoPath = path.join(tempRoot, 'repo');
65
+ const rulesRoot = path.join(repoPath, '.gitnexus', 'rules');
66
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
67
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({
68
+ rules: [
69
+ {
70
+ id: 'demo.reload.rule.v1',
71
+ version: '1.0.0',
72
+ file: 'approved/demo.reload.rule.v1.yaml',
73
+ },
74
+ ],
75
+ }), 'utf-8');
76
+ try {
77
+ await assert.rejects(() => loadRuleRegistry(repoPath), (error) => {
78
+ assert.ok(error instanceof RuleRegistryLoadError);
79
+ assert.equal(error.code, 'rule_file_missing');
80
+ assert.match(String(error.message || ''), /rule file not found/i);
81
+ return true;
82
+ });
83
+ }
84
+ finally {
85
+ await fs.rm(tempRoot, { recursive: true, force: true });
86
+ }
87
+ });
88
+ test('parses scalar/list values with spaces, quotes, and escapes without truncation', async () => {
89
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-runtime-claim-rules-'));
90
+ const repoPath = path.join(tempRoot, 'repo');
91
+ const rulesRoot = path.join(repoPath, '.gitnexus', 'rules');
92
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
93
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({
94
+ rules: [
95
+ {
96
+ id: 'demo.scalar-parser.v1',
97
+ version: '1.0.0',
98
+ file: 'approved/demo.scalar-parser.v1.yaml',
99
+ },
100
+ ],
101
+ }), 'utf-8');
102
+ await fs.writeFile(path.join(rulesRoot, 'approved', 'demo.scalar-parser.v1.yaml'), [
103
+ 'id: demo.scalar-parser.v1',
104
+ 'version: 1.0.0',
105
+ 'trigger_family: reload',
106
+ 'resource_types:',
107
+ ' - "asset ref"',
108
+ " - 'prefab ref'",
109
+ 'host_base_type:',
110
+ " - 'ReloadBase'",
111
+ 'required_hops:',
112
+ ' - resource',
113
+ 'guarantees:',
114
+ " - 'guarantee with spaces'",
115
+ 'non_guarantees:',
116
+ ' - "double-quote \\"inside\\""',
117
+ " - 'single-quote ''inside'''",
118
+ 'next_action: node gitnexus/dist/cli/index.js query --runtime-chain-verify on-demand "Reload NEON.Game.Graph.Nodes.Reloads"',
119
+ ].join('\n'), 'utf-8');
120
+ try {
121
+ const registry = await loadRuleRegistry(repoPath);
122
+ const rule = registry.activeRules[0];
123
+ assert.equal(rule.id, 'demo.scalar-parser.v1');
124
+ assert.deepEqual(rule.resource_types, ['asset ref', 'prefab ref']);
125
+ assert.deepEqual(rule.guarantees, ['guarantee with spaces']);
126
+ assert.deepEqual(rule.non_guarantees, ['double-quote "inside"', "single-quote 'inside'"]);
127
+ assert.equal(rule.next_action, 'node gitnexus/dist/cli/index.js query --runtime-chain-verify on-demand "Reload NEON.Game.Graph.Nodes.Reloads"');
128
+ }
129
+ finally {
130
+ await fs.rm(tempRoot, { recursive: true, force: true });
131
+ }
132
+ });
133
+ test('rejects rule yaml when topology/closure/claims are missing', async () => {
134
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-runtime-claim-rules-'));
135
+ const repoPath = path.join(tempRoot, 'repo');
136
+ const rulesRoot = path.join(repoPath, '.gitnexus', 'rules');
137
+ await fs.mkdir(path.join(rulesRoot, 'approved'), { recursive: true });
138
+ await fs.writeFile(path.join(rulesRoot, 'catalog.json'), JSON.stringify({
139
+ rules: [
140
+ {
141
+ id: 'demo.reload.rule.v2',
142
+ version: '2.0.0',
143
+ file: 'approved/demo.reload.rule.v2.yaml',
144
+ },
145
+ ],
146
+ }), 'utf-8');
147
+ await fs.writeFile(path.join(rulesRoot, 'approved', 'demo.reload.rule.v2.yaml'), [
148
+ 'id: demo.reload.rule.v2',
149
+ 'version: 2.0.0',
150
+ 'trigger_family: reload',
151
+ 'resource_types:',
152
+ ' - asset',
153
+ 'host_base_type:',
154
+ ' - ReloadBase',
155
+ 'required_hops:',
156
+ ' - resource',
157
+ 'guarantees:',
158
+ ' - reload_chain_closed',
159
+ 'non_guarantees:',
160
+ ' - no_runtime_execution_guarantee',
161
+ ].join('\n'), 'utf-8');
162
+ try {
163
+ await assert.rejects(() => loadRuleRegistry(repoPath), /topology|closure|claims/i);
164
+ }
165
+ finally {
166
+ await fs.rm(tempRoot, { recursive: true, force: true });
167
+ }
168
+ });
169
+ test('loads v2 verification bundle from explicit compiled path without catalog fallback', async () => {
170
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-runtime-claim-rules-'));
171
+ const repoPath = path.join(tempRoot, 'repo');
172
+ const compiledRoot = path.join(repoPath, '.gitnexus', 'rules', 'compiled');
173
+ await fs.mkdir(compiledRoot, { recursive: true });
174
+ await fs.writeFile(path.join(compiledRoot, 'verification_rules.v2.json'), JSON.stringify({
175
+ bundle_version: '2.0.0',
176
+ family: 'verification_rules',
177
+ generated_at: new Date().toISOString(),
178
+ rules: [
179
+ {
180
+ id: 'demo.bundle.rule.v2',
181
+ version: '2.0.0',
182
+ trigger_family: 'reload',
183
+ resource_types: ['asset'],
184
+ host_base_type: ['ReloadBase'],
185
+ required_hops: ['resource', 'code_runtime'],
186
+ guarantees: ['reload_chain_closed'],
187
+ non_guarantees: ['no_runtime_execution'],
188
+ next_action: 'gitnexus query "reload"',
189
+ file_path: '.gitnexus/rules/compiled/verification_rules.v2.json',
190
+ match: { trigger_tokens: ['reload'] },
191
+ topology: [
192
+ { hop: 'resource', from: { entity: 'resource' }, to: { entity: 'script' }, edge: { kind: 'binds_script' } },
193
+ ],
194
+ closure: {
195
+ required_hops: ['resource', 'code_runtime'],
196
+ failure_map: { missing_evidence: 'rule_matched_but_evidence_missing' },
197
+ },
198
+ claims: {
199
+ guarantees: ['reload_chain_closed'],
200
+ non_guarantees: ['no_runtime_execution'],
201
+ next_action: 'gitnexus query "reload"',
202
+ },
203
+ },
204
+ ],
205
+ }, null, 2), 'utf-8');
206
+ try {
207
+ const registry = await loadRuleRegistry(repoPath);
208
+ assert.equal(registry.activeRules[0].id, 'demo.bundle.rule.v2');
209
+ assert.equal(registry.activeRules[0].version, '2.0.0');
210
+ assert.deepEqual(registry.activeRules[0].required_hops, ['resource', 'code_runtime']);
211
+ }
212
+ finally {
213
+ await fs.rm(tempRoot, { recursive: true, force: true });
214
+ }
215
+ });
@@ -0,0 +1,38 @@
1
+ import type { RuntimeChainEvidenceLevel } from './runtime-chain-evidence.js';
2
+ import type { RuntimeChainGap, RuntimeChainHop, RuntimeChainStatus } from './runtime-chain-verify.js';
3
+ import type { RuntimeClaimRule } from './runtime-claim-rule-registry.js';
4
+ export type RuntimeClaimReason = 'rule_not_matched' | 'rule_matched_but_evidence_missing' | 'rule_matched_but_verification_failed' | 'gate_disabled';
5
+ export interface RuntimeClaim {
6
+ rule_id: string;
7
+ rule_version: string;
8
+ scope: {
9
+ resource_types: string[];
10
+ host_base_type: string[];
11
+ trigger_family: string;
12
+ };
13
+ status: Exclude<RuntimeChainStatus, 'pending'>;
14
+ evidence_level: RuntimeChainEvidenceLevel;
15
+ guarantees: string[];
16
+ non_guarantees: string[];
17
+ hops: RuntimeChainHop[];
18
+ gaps: RuntimeChainGap[];
19
+ reason?: RuntimeClaimReason;
20
+ next_action?: string;
21
+ }
22
+ export declare function buildRuntimeClaimFromRule(input: {
23
+ rule: RuntimeClaimRule;
24
+ status: Exclude<RuntimeChainStatus, 'pending'>;
25
+ evidence_level: RuntimeChainEvidenceLevel;
26
+ hops: RuntimeChainHop[];
27
+ gaps: RuntimeChainGap[];
28
+ reason?: RuntimeClaimReason;
29
+ next_action?: string;
30
+ }): RuntimeClaim;
31
+ export declare function buildReloadRuntimeClaim(input: {
32
+ status: Exclude<RuntimeChainStatus, 'pending'>;
33
+ evidence_level: RuntimeChainEvidenceLevel;
34
+ hops: RuntimeChainHop[];
35
+ gaps: RuntimeChainGap[];
36
+ reason?: RuntimeClaimReason;
37
+ next_action?: string;
38
+ }): RuntimeClaim;
@@ -0,0 +1,54 @@
1
+ function resolveNonGuarantees(rule) {
2
+ if (rule && Array.isArray(rule.non_guarantees) && rule.non_guarantees.length > 0) {
3
+ return [...rule.non_guarantees];
4
+ }
5
+ return [
6
+ 'no_runtime_execution',
7
+ 'no_dynamic_data_flow_proof',
8
+ 'no_state_transition_proof',
9
+ ];
10
+ }
11
+ export function buildRuntimeClaimFromRule(input) {
12
+ const guarantees = input.status === 'verified_full'
13
+ ? [...(input.rule.guarantees || [])]
14
+ : [];
15
+ return {
16
+ rule_id: input.rule.id,
17
+ rule_version: input.rule.version,
18
+ scope: {
19
+ resource_types: [...(input.rule.resource_types || [])],
20
+ host_base_type: [...(input.rule.host_base_type || [])],
21
+ trigger_family: input.rule.trigger_family || 'unknown',
22
+ },
23
+ status: input.status,
24
+ evidence_level: input.evidence_level,
25
+ guarantees,
26
+ non_guarantees: resolveNonGuarantees(input.rule),
27
+ hops: input.hops,
28
+ gaps: input.gaps,
29
+ ...(input.reason ? { reason: input.reason } : {}),
30
+ ...(input.next_action ? { next_action: input.next_action } : {}),
31
+ };
32
+ }
33
+ export function buildReloadRuntimeClaim(input) {
34
+ return buildRuntimeClaimFromRule({
35
+ rule: {
36
+ id: 'unity.gungraph.reload.output-getvalue.v1',
37
+ version: '1.0.0',
38
+ trigger_family: 'reload',
39
+ resource_types: ['asset', 'prefab', 'meta'],
40
+ host_base_type: ['ReloadBase'],
41
+ required_hops: ['resource', 'guid_map', 'code_loader', 'code_runtime'],
42
+ guarantees: ['resource_to_runtime_chain_closed'],
43
+ non_guarantees: ['no_runtime_execution', 'no_dynamic_data_flow_proof', 'no_state_transition_proof'],
44
+ next_action: input.next_action,
45
+ file_path: '.gitnexus/rules/approved/unity.gungraph.reload.output-getvalue.v1.yaml',
46
+ },
47
+ status: input.status,
48
+ evidence_level: input.evidence_level,
49
+ hops: input.hops,
50
+ gaps: input.gaps,
51
+ reason: input.reason,
52
+ next_action: input.next_action,
53
+ });
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'vitest';
3
+ import { buildRuntimeClaimFromRule } from './runtime-claim.js';
4
+ test('runtime_claim contract includes rule-driven metadata and guarantees', () => {
5
+ const claim = buildRuntimeClaimFromRule({
6
+ rule: {
7
+ id: 'demo.reload.v1',
8
+ version: '1.2.3',
9
+ trigger_family: 'reload',
10
+ resource_types: ['asset'],
11
+ host_base_type: ['ReloadBase'],
12
+ required_hops: ['resource'],
13
+ guarantees: ['demo_chain_closed'],
14
+ non_guarantees: ['demo_non_guarantee'],
15
+ next_action: 'node demo',
16
+ file_path: '.gitnexus/rules/approved/demo.reload.v1.yaml',
17
+ },
18
+ status: 'verified_full',
19
+ evidence_level: 'verified_chain',
20
+ hops: [{ hop_type: 'resource', anchor: 'Assets/A.prefab:1', confidence: 'high', note: 'resource anchor' }],
21
+ gaps: [],
22
+ });
23
+ assert.equal(claim.rule_id, 'demo.reload.v1');
24
+ assert.equal(claim.rule_version, '1.2.3');
25
+ assert.deepEqual(claim.guarantees, ['demo_chain_closed']);
26
+ assert.deepEqual(claim.non_guarantees, ['demo_non_guarantee']);
27
+ });
@@ -10,6 +10,7 @@ export interface UnityHydrationMeta {
10
10
  isComplete: boolean;
11
11
  completenessReason: string[];
12
12
  needsParityRetry: boolean;
13
+ reason?: string;
13
14
  retryHint?: string;
14
15
  }
15
16
  export interface UnityContextPayload extends Pick<ResolveOutput, 'resourceBindings' | 'serializedFields' | 'unityDiagnostics'> {
@@ -5,7 +5,7 @@ export async function loadUnityContext(_repoId, symbolId, execute) {
5
5
  MATCH (symbol {id: '${escapedSymbolId}'})-[r:CodeRelation]->(target)
6
6
  WHERE r.type IN ['UNITY_COMPONENT_INSTANCE', 'UNITY_SERIALIZED_TYPE_IN', 'UNITY_RESOURCE_SUMMARY']
7
7
  RETURN target.filePath AS resourcePath,
8
- CASE WHEN r.type = 'UNITY_RESOURCE_SUMMARY' THEN '' ELSE target.description END AS payload,
8
+ CASE WHEN r.type = 'UNITY_RESOURCE_SUMMARY' THEN '' ELSE COALESCE(target.description, r.reason, '') END AS payload,
9
9
  r.type AS relationType,
10
10
  r.reason AS relationReason
11
11
  ORDER BY target.filePath, target.id