@veewo/gitnexus 1.4.11-rc → 1.5.0-rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.d.ts +55 -0
  2. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.js +190 -0
  3. package/dist/benchmark/u2-e2e/hydration-policy-repeatability-runner.test.js +13 -0
  4. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.d.ts +22 -0
  5. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.js +100 -0
  6. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.d.ts +1 -0
  7. package/dist/benchmark/u2-e2e/phase1-process-ref-acceptance-runner.test.js +13 -0
  8. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.d.ts +27 -0
  9. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.js +118 -0
  10. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.d.ts +1 -0
  11. package/dist/benchmark/u2-e2e/phase2-runtime-claim-acceptance-runner.test.js +16 -0
  12. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.d.ts +60 -0
  13. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.js +331 -0
  14. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.d.ts +1 -0
  15. package/dist/benchmark/u2-e2e/phase5-rule-lab-acceptance-runner.test.js +42 -0
  16. package/dist/benchmark/u2-e2e/reload-v1-acceptance-runner.js +4 -4
  17. package/dist/benchmark/unity-lazy-context-sampler.d.ts +6 -0
  18. package/dist/benchmark/unity-lazy-context-sampler.js +49 -13
  19. package/dist/benchmark/unity-lazy-context-sampler.test.js +4 -0
  20. package/dist/cli/ai-context.js +6 -7
  21. package/dist/cli/ai-context.test.js +6 -8
  22. package/dist/cli/eval-server.js +0 -3
  23. package/dist/cli/index.js +8 -0
  24. package/dist/cli/mcp.js +0 -3
  25. package/dist/cli/rule-lab.d.ts +42 -0
  26. package/dist/cli/rule-lab.js +157 -0
  27. package/dist/cli/rule-lab.test.d.ts +1 -0
  28. package/dist/cli/rule-lab.test.js +11 -0
  29. package/dist/cli/tool.d.ts +7 -1
  30. package/dist/cli/tool.js +6 -0
  31. package/dist/core/config/unity-config.d.ts +20 -0
  32. package/dist/core/config/unity-config.js +46 -0
  33. package/dist/core/graph/types.d.ts +1 -1
  34. package/dist/core/ingestion/pipeline.js +38 -13
  35. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.d.ts +0 -2
  36. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.js +26 -213
  37. package/dist/core/ingestion/unity-lifecycle-synthetic-calls.test.js +1 -1
  38. package/dist/core/ingestion/unity-resource-processor.js +87 -22
  39. package/dist/core/ingestion/unity-resource-processor.test.js +67 -2
  40. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +11 -0
  41. package/dist/core/ingestion/unity-runtime-binding-rules.js +179 -0
  42. package/dist/core/unity/options.d.ts +4 -0
  43. package/dist/core/unity/options.js +18 -0
  44. package/dist/core/unity/options.test.js +11 -1
  45. package/dist/core/unity/resolver.js +11 -1
  46. package/dist/core/unity/resolver.test.js +62 -0
  47. package/dist/core/unity/yaml-object-graph.js +1 -1
  48. package/dist/core/unity/yaml-object-graph.test.js +16 -0
  49. package/dist/mcp/local/derived-process-reader.d.ts +2 -0
  50. package/dist/mcp/local/derived-process-reader.js +15 -0
  51. package/dist/mcp/local/local-backend.d.ts +56 -0
  52. package/dist/mcp/local/local-backend.js +1003 -53
  53. package/dist/mcp/local/local-backend.unity-merge.test.js +1 -1
  54. package/dist/mcp/local/process-confidence.js +1 -1
  55. package/dist/mcp/local/process-evidence.d.ts +1 -0
  56. package/dist/mcp/local/process-evidence.js +22 -0
  57. package/dist/mcp/local/process-evidence.test.js +11 -1
  58. package/dist/mcp/local/process-ref.d.ts +24 -0
  59. package/dist/mcp/local/process-ref.js +33 -0
  60. package/dist/mcp/local/process-ref.test.d.ts +1 -0
  61. package/dist/mcp/local/process-ref.test.js +24 -0
  62. package/dist/mcp/local/runtime-chain-verify.d.ts +15 -1
  63. package/dist/mcp/local/runtime-chain-verify.js +191 -187
  64. package/dist/mcp/local/runtime-chain-verify.test.js +546 -19
  65. package/dist/mcp/local/runtime-claim-rule-registry.d.ts +63 -0
  66. package/dist/mcp/local/runtime-claim-rule-registry.js +308 -0
  67. package/dist/mcp/local/runtime-claim-rule-registry.test.d.ts +1 -0
  68. package/dist/mcp/local/runtime-claim-rule-registry.test.js +215 -0
  69. package/dist/mcp/local/runtime-claim.d.ts +38 -0
  70. package/dist/mcp/local/runtime-claim.js +54 -0
  71. package/dist/mcp/local/runtime-claim.test.d.ts +1 -0
  72. package/dist/mcp/local/runtime-claim.test.js +27 -0
  73. package/dist/mcp/local/unity-enrichment.d.ts +1 -0
  74. package/dist/mcp/local/unity-enrichment.js +1 -1
  75. package/dist/mcp/local/unity-evidence-view.d.ts +26 -0
  76. package/dist/mcp/local/unity-evidence-view.js +96 -0
  77. package/dist/mcp/local/unity-evidence-view.test.d.ts +1 -0
  78. package/dist/mcp/local/unity-evidence-view.test.js +39 -0
  79. package/dist/mcp/local/unity-lazy-hydrator.d.ts +2 -2
  80. package/dist/mcp/local/unity-lazy-hydrator.js +3 -3
  81. package/dist/mcp/local/unity-lazy-hydrator.test.js +4 -4
  82. package/dist/mcp/local/unity-parity-cache.js +2 -6
  83. package/dist/mcp/local/unity-parity-seed-loader.d.ts +1 -0
  84. package/dist/mcp/local/unity-parity-seed-loader.js +10 -16
  85. package/dist/mcp/local/unity-parity-seed-loader.test.js +3 -12
  86. package/dist/mcp/local/unity-runtime-hydration.d.ts +3 -2
  87. package/dist/mcp/local/unity-runtime-hydration.js +13 -16
  88. package/dist/mcp/local/unity-runtime-hydration.test.js +15 -1
  89. package/dist/mcp/resources.js +13 -0
  90. package/dist/mcp/tools.js +166 -13
  91. package/dist/rule-lab/analyze.d.ts +12 -0
  92. package/dist/rule-lab/analyze.js +90 -0
  93. package/dist/rule-lab/analyze.test.d.ts +1 -0
  94. package/dist/rule-lab/analyze.test.js +28 -0
  95. package/dist/rule-lab/compile.d.ts +5 -0
  96. package/dist/rule-lab/compile.js +51 -0
  97. package/dist/rule-lab/compiled-bundles.d.ts +30 -0
  98. package/dist/rule-lab/compiled-bundles.js +36 -0
  99. package/dist/rule-lab/curate.d.ts +32 -0
  100. package/dist/rule-lab/curate.js +134 -0
  101. package/dist/rule-lab/curate.test.d.ts +1 -0
  102. package/dist/rule-lab/curate.test.js +72 -0
  103. package/dist/rule-lab/discover.d.ts +13 -0
  104. package/dist/rule-lab/discover.js +74 -0
  105. package/dist/rule-lab/discover.test.d.ts +1 -0
  106. package/dist/rule-lab/discover.test.js +42 -0
  107. package/dist/rule-lab/paths.d.ts +21 -0
  108. package/dist/rule-lab/paths.js +37 -0
  109. package/dist/rule-lab/paths.test.d.ts +1 -0
  110. package/dist/rule-lab/paths.test.js +46 -0
  111. package/dist/rule-lab/promote.d.ts +26 -0
  112. package/dist/rule-lab/promote.js +314 -0
  113. package/dist/rule-lab/promote.test.d.ts +1 -0
  114. package/dist/rule-lab/promote.test.js +164 -0
  115. package/dist/rule-lab/regress.d.ts +60 -0
  116. package/dist/rule-lab/regress.js +122 -0
  117. package/dist/rule-lab/regress.test.d.ts +1 -0
  118. package/dist/rule-lab/regress.test.js +68 -0
  119. package/dist/rule-lab/review-pack.d.ts +31 -0
  120. package/dist/rule-lab/review-pack.js +125 -0
  121. package/dist/rule-lab/review-pack.test.d.ts +1 -0
  122. package/dist/rule-lab/review-pack.test.js +49 -0
  123. package/dist/rule-lab/types.d.ts +99 -0
  124. package/dist/rule-lab/types.js +1 -0
  125. package/package.json +1 -1
  126. package/skills/_shared/unity-hydration-contract.md +11 -0
  127. package/skills/_shared/unity-ui-trace-contract.md +33 -0
  128. package/skills/gitnexus-cli.md +11 -25
  129. package/skills/gitnexus-guide.md +2 -0
  130. package/skills/gitnexus-unity-rule-gen.md +318 -0
  131. package/dist/core/ingestion/unity-lifecycle-config.d.ts +0 -5
  132. package/dist/core/ingestion/unity-lifecycle-config.js +0 -25
  133. package/dist/mcp/local/unity-lazy-config.d.ts +0 -6
  134. package/dist/mcp/local/unity-lazy-config.js +0 -7
  135. package/dist/mcp/local/unity-lazy-config.test.js +0 -9
  136. package/dist/mcp/local/unity-process-confidence-config.d.ts +0 -1
  137. package/dist/mcp/local/unity-process-confidence-config.js +0 -4
  138. package/dist/mcp/local/unity-runtime-chain-verify-config.d.ts +0 -1
  139. package/dist/mcp/local/unity-runtime-chain-verify-config.js +0 -10
  140. /package/dist/{mcp/local/unity-lazy-config.test.d.ts → benchmark/u2-e2e/hydration-policy-repeatability-runner.test.d.ts} +0 -0
@@ -39,7 +39,7 @@ test('summary-only rows hydrate and merge into full bindings with preserved fiel
39
39
  .map((binding) => binding.resourcePath))];
40
40
  const hydration = await hydrateLazyBindings({
41
41
  pendingPaths,
42
- config: { maxPendingPathsPerRequest: 10, batchSize: 10, maxHydrationMs: 5000 },
42
+ config: { lazyMaxPaths: 10, lazyBatchSize: 10, lazyMaxMs: 5000 },
43
43
  resolveBatch: async () => new Map([
44
44
  ['Assets/Doors/Door.prefab', [
45
45
  {
@@ -18,7 +18,7 @@ export function buildVerificationHint(input) {
18
18
  return {
19
19
  action: 'rerun_parity_hydration',
20
20
  target,
21
- next_command: 'GITNEXUS_UNITY_PROCESS_CONFIDENCE_FIELDS=on gitnexus query --unity-resources on --unity-hydration parity "<symbol-or-query>"',
21
+ next_command: 'gitnexus query --unity-resources on --unity-hydration parity "<symbol-or-query>"',
22
22
  };
23
23
  }
24
24
  return {
@@ -21,6 +21,7 @@ export interface MergedProcessEvidenceRow extends ProcessEvidenceRow {
21
21
  runtime_chain_evidence_level: RuntimeChainEvidenceLevel;
22
22
  verification_hint?: VerificationHint;
23
23
  }
24
+ export declare function deriveEvidenceFingerprint(...parts: unknown[]): string;
24
25
  export declare function mergeProcessEvidence(input: {
25
26
  directRows: ProcessEvidenceRow[];
26
27
  projectedRows: ProjectedProcessEvidenceRow[];
@@ -1,5 +1,27 @@
1
1
  import { buildVerificationHint, deriveConfidence, } from './process-confidence.js';
2
2
  import { deriveRuntimeChainEvidenceLevel, } from './runtime-chain-evidence.js';
3
+ function asFingerprintToken(value) {
4
+ if (value === null || value === undefined)
5
+ return '';
6
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
7
+ return String(value);
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => asFingerprintToken(item)).join(',');
11
+ }
12
+ const obj = value;
13
+ const ordered = {};
14
+ for (const key of Object.keys(obj).sort()) {
15
+ ordered[key] = obj[key];
16
+ }
17
+ return JSON.stringify(ordered);
18
+ }
19
+ export function deriveEvidenceFingerprint(...parts) {
20
+ return parts
21
+ .map((part) => asFingerprintToken(part))
22
+ .filter((token) => token.length > 0)
23
+ .join('::');
24
+ }
3
25
  const normalizeProcessConfidence = (raw, fallback) => {
4
26
  if (raw === 'high' || raw === 'medium' || raw === 'low')
5
27
  return raw;
@@ -1,6 +1,6 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mergeProcessEvidence } from './process-evidence.js';
3
+ import { deriveEvidenceFingerprint, mergeProcessEvidence } from './process-evidence.js';
4
4
  test('projected-only rows are method_projected + medium', () => {
5
5
  const out = mergeProcessEvidence({
6
6
  directRows: [],
@@ -54,3 +54,13 @@ test('heuristic-only rows emit low confidence with verification hint', () => {
54
54
  assert.equal(out[0].verification_hint?.action, 'rerun_parity_hydration');
55
55
  assert.match(out[0].verification_hint?.next_command || '', /parity/i);
56
56
  });
57
+ test('deriveEvidenceFingerprint is stable for same input ordering', () => {
58
+ const left = deriveEvidenceFingerprint({ resourcePath: 'Assets/A.prefab', bindingKind: 'component', line: 10 }, { pid: 'proc:123', step: 1 });
59
+ const right = deriveEvidenceFingerprint({ line: 10, bindingKind: 'component', resourcePath: 'Assets/A.prefab' }, { step: 1, pid: 'proc:123' });
60
+ assert.equal(left, right);
61
+ });
62
+ test('deriveEvidenceFingerprint changes when signal changes', () => {
63
+ const left = deriveEvidenceFingerprint({ resourcePath: 'Assets/A.prefab', line: 10 });
64
+ const right = deriveEvidenceFingerprint({ resourcePath: 'Assets/A.prefab', line: 11 });
65
+ assert.notEqual(left, right);
66
+ });
@@ -0,0 +1,24 @@
1
+ export type ProcessRefKind = 'persistent' | 'derived';
2
+ export type ProcessRefOrigin = 'step_in_process' | 'method_projected' | 'resource_heuristic';
3
+ export interface ProcessRef {
4
+ id: string;
5
+ kind: ProcessRefKind;
6
+ readable: boolean;
7
+ reader_uri: string;
8
+ origin: ProcessRefOrigin;
9
+ }
10
+ export interface BuildDerivedProcessIdInput {
11
+ indexedCommit: string;
12
+ symbolUid: string;
13
+ evidenceFingerprint: string;
14
+ }
15
+ export declare function buildDerivedProcessId(input: BuildDerivedProcessIdInput): string;
16
+ export interface BuildProcessRefInput {
17
+ repoName: string;
18
+ processId?: string;
19
+ origin: ProcessRefOrigin;
20
+ indexedCommit: string;
21
+ symbolUid: string;
22
+ evidenceFingerprint: string;
23
+ }
24
+ export declare function buildProcessRef(input: BuildProcessRefInput): ProcessRef;
@@ -0,0 +1,33 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function buildDerivedProcessId(input) {
3
+ const key = `${input.indexedCommit}::${input.symbolUid}::${input.evidenceFingerprint}`;
4
+ const hash = createHash('sha1').update(key).digest('hex').slice(0, 16);
5
+ return `derived:${hash}`;
6
+ }
7
+ function isPersistentProcessId(processId) {
8
+ return processId.length > 0 && !processId.startsWith('proc:heuristic:');
9
+ }
10
+ export function buildProcessRef(input) {
11
+ const processId = String(input.processId || '').trim();
12
+ if (isPersistentProcessId(processId)) {
13
+ return {
14
+ id: processId,
15
+ kind: 'persistent',
16
+ readable: true,
17
+ reader_uri: `gitnexus://repo/${encodeURIComponent(input.repoName)}/process/${encodeURIComponent(processId)}`,
18
+ origin: input.origin,
19
+ };
20
+ }
21
+ const id = buildDerivedProcessId({
22
+ indexedCommit: input.indexedCommit,
23
+ symbolUid: input.symbolUid,
24
+ evidenceFingerprint: input.evidenceFingerprint,
25
+ });
26
+ return {
27
+ id,
28
+ kind: 'derived',
29
+ readable: true,
30
+ reader_uri: `gitnexus://repo/${encodeURIComponent(input.repoName)}/derived-process/${encodeURIComponent(id)}`,
31
+ origin: input.origin,
32
+ };
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildDerivedProcessId } from './process-ref.js';
4
+ test('buildDerivedProcessId is stable for identical fingerprint input', () => {
5
+ const left = buildDerivedProcessId({
6
+ indexedCommit: 'abc',
7
+ symbolUid: 'Class:Assets/A.cs:A',
8
+ evidenceFingerprint: 'resource=Assets/A.prefab;line=10',
9
+ });
10
+ const right = buildDerivedProcessId({
11
+ indexedCommit: 'abc',
12
+ symbolUid: 'Class:Assets/A.cs:A',
13
+ evidenceFingerprint: 'resource=Assets/A.prefab;line=10',
14
+ });
15
+ assert.equal(left, right);
16
+ });
17
+ test('buildDerivedProcessId does not leak heuristic process id prefix', () => {
18
+ const id = buildDerivedProcessId({
19
+ indexedCommit: 'abc',
20
+ symbolUid: 'Class:Assets/A.cs:A',
21
+ evidenceFingerprint: 'resource=Assets/A.prefab;line=11',
22
+ });
23
+ assert.doesNotMatch(id, /^proc:heuristic:/);
24
+ });
@@ -1,4 +1,6 @@
1
- import { type RuntimeChainEvidenceLevel } from './runtime-chain-evidence.js';
1
+ import type { RuntimeChainEvidenceLevel } from './runtime-chain-evidence.js';
2
+ import { type RuntimeClaim } from './runtime-claim.js';
3
+ import { type RuntimeClaimRule } from './runtime-claim-rule-registry.js';
2
4
  export type RuntimeChainVerifyMode = 'off' | 'on-demand';
3
5
  export type RuntimeChainStatus = 'pending' | 'verified_partial' | 'verified_full' | 'failed';
4
6
  export type RuntimeChainHopType = 'resource' | 'guid_map' | 'code_loader' | 'code_runtime';
@@ -13,12 +15,15 @@ export interface RuntimeChainGap {
13
15
  segment: 'resource' | 'guid_map' | 'loader' | 'runtime';
14
16
  reason: string;
15
17
  next_command: string;
18
+ why_not_next?: string;
16
19
  }
17
20
  export interface RuntimeChainResult {
18
21
  status: RuntimeChainStatus;
19
22
  evidence_level: RuntimeChainEvidenceLevel;
23
+ evidence_source?: 'analyze_time' | 'query_time';
20
24
  hops: RuntimeChainHop[];
21
25
  gaps: RuntimeChainGap[];
26
+ why_not_next?: string[];
22
27
  }
23
28
  interface QueryExecutor {
24
29
  (query: string, params?: Record<string, unknown>): Promise<any[]>;
@@ -27,11 +32,20 @@ interface VerifyRuntimeChainInput {
27
32
  repoPath: string;
28
33
  executeParameterized: QueryExecutor;
29
34
  queryText?: string;
35
+ resourceSeedPath?: string;
36
+ mappedSeedTargets?: string[];
30
37
  symbolName?: string;
31
38
  symbolFilePath?: string;
32
39
  resourceBindings?: Array<{
33
40
  resourcePath?: string;
34
41
  }>;
42
+ requiredHops?: string[];
43
+ rule?: RuntimeClaimRule;
44
+ }
45
+ interface VerifyRuntimeClaimInput extends VerifyRuntimeChainInput {
46
+ rulesRoot?: string;
47
+ minimumEvidenceSatisfied?: boolean;
35
48
  }
36
49
  export declare function verifyRuntimeChainOnDemand(input: VerifyRuntimeChainInput): Promise<RuntimeChainResult | undefined>;
50
+ export declare function verifyRuntimeClaimOnDemand(input: VerifyRuntimeClaimInput): Promise<RuntimeClaim>;
37
51
  export {};
@@ -1,221 +1,225 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { deriveRuntimeChainEvidenceLevel, } from './runtime-chain-evidence.js';
4
- const RELOAD_QUERY_TOKENS = [
5
- 'reload',
6
- 'pickitup',
7
- 'equipwithevent',
8
- 'weaponpowerup',
9
- 'curgungraph',
10
- 'registerevents',
11
- 'startroutinewithevents',
12
- ];
13
- const RESOURCE_ASSET_PATH = 'Assets/NEON/DataAssets/Powerups/1_newWeapon/0_pick/法器_Orb/1_weapon_orb_key.asset';
14
- const GRAPH_ASSET_PATH = 'Assets/NEON/Graphs/PlayerGun/Gungraph_use/1_weapon_orb_key.asset';
15
- const RELOAD_META_PATH = 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/Reload.cs.meta';
16
- const RELOAD_GUID = 'bd387039cacb475381a86f156b54bac2';
17
- const GRAPH_GUID = '69199acacbf8a7e489ad4aa872efcabd';
18
- const VERIFY_NEXT_COMMAND = 'node gitnexus/dist/cli/index.js query --unity-resources on --unity-hydration parity --runtime-chain-verify on-demand "Reload NEON.Game.Graph.Nodes.Reloads"';
19
- function normalizeText(value) {
20
- return String(value || '').trim();
1
+ import { buildRuntimeClaimFromRule } from './runtime-claim.js';
2
+ import { RuleRegistryLoadError, loadRuleRegistry } from './runtime-claim-rule-registry.js';
3
+ function buildDefaultVerifyNextCommand(queryText) {
4
+ const normalizedQuery = String(queryText || '').trim() || 'Reload NEON.Game.Graph.Nodes.Reloads';
5
+ const escapedQuery = normalizedQuery
6
+ .replace(/\\/g, '\\\\')
7
+ .replace(/"/g, '\\"');
8
+ return `node gitnexus/dist/cli/index.js query --unity-resources on --unity-hydration parity --runtime-chain-verify on-demand "${escapedQuery}"`;
21
9
  }
22
- function shouldVerifyReloadChain(input) {
23
- const haystack = [
10
+ function buildRuntimeMatchHaystack(input) {
11
+ return [
24
12
  input.queryText,
13
+ input.resourceSeedPath,
25
14
  input.symbolName,
26
15
  input.symbolFilePath,
16
+ ...(input.mappedSeedTargets || []),
27
17
  ...(input.resourceBindings || []).map((binding) => binding.resourcePath),
28
18
  ]
29
19
  .filter(Boolean)
30
20
  .join(' ')
31
21
  .toLowerCase();
32
- return RELOAD_QUERY_TOKENS.some((token) => haystack.includes(token));
33
22
  }
34
- function buildGap(segment, reason) {
35
- return {
36
- segment,
37
- reason,
38
- next_command: VERIFY_NEXT_COMMAND,
39
- };
23
+ function parseTriggerTokens(triggerFamily) {
24
+ return String(triggerFamily || '')
25
+ .toLowerCase()
26
+ .split(/[\s,|/]+/)
27
+ .map((token) => token.trim())
28
+ .filter((token) => token.length > 0);
40
29
  }
41
- async function readFileLines(repoPath, relativePath) {
30
+ async function verifyRuleDrivenRuntimeChain(input) {
31
+ const ruleId = input.rule?.id;
32
+ if (!ruleId) {
33
+ return { status: 'failed', evidence_level: 'none', evidence_source: 'analyze_time', hops: [], gaps: [] };
34
+ }
42
35
  try {
43
- const fullPath = path.join(repoPath, relativePath);
44
- const raw = await fs.readFile(fullPath, 'utf-8');
45
- return raw.split(/\r?\n/);
36
+ const rows = await input.executeParameterized(`
37
+ MATCH (s)-[r:CodeRelation {type: 'CALLS'}]->(t)
38
+ WHERE r.reason CONTAINS $ruleId
39
+ AND r.reason STARTS WITH 'unity-rule-'
40
+ RETURN s.name AS sourceName, s.filePath AS sourceFilePath, s.startLine AS sourceStartLine,
41
+ t.name AS targetName, t.filePath AS targetFilePath, t.startLine AS targetStartLine,
42
+ r.reason AS reason
43
+ LIMIT 20
44
+ `, { ruleId });
45
+ if (rows.length > 0) {
46
+ const hops = rows.map((row) => ({
47
+ hop_type: 'code_runtime',
48
+ anchor: `${row.sourceFilePath}:${row.sourceStartLine || 1}->${row.targetFilePath}:${row.targetStartLine || 1}`,
49
+ confidence: 'high',
50
+ note: `Synthetic edge injected at analyze time (${row.reason}).`,
51
+ snippet: `${row.sourceName} -> ${row.targetName}`,
52
+ }));
53
+ return {
54
+ status: 'verified_full',
55
+ evidence_level: 'verified_chain',
56
+ evidence_source: 'analyze_time',
57
+ hops,
58
+ gaps: [],
59
+ };
60
+ }
46
61
  }
47
62
  catch {
48
- return null;
63
+ // Graph query failed; fall through to no match.
49
64
  }
50
- }
51
- async function findLineAnchor(repoPath, relativePath, pattern) {
52
- const lines = await readFileLines(repoPath, relativePath);
53
- if (!lines)
54
- return null;
55
- const index = lines.findIndex((line) => pattern.test(line));
56
- if (index < 0)
57
- return null;
58
65
  return {
59
- anchor: `${relativePath}:${index + 1}`,
60
- snippet: lines[index].trim(),
66
+ status: 'failed',
67
+ evidence_level: 'none',
68
+ evidence_source: 'analyze_time',
69
+ hops: [],
70
+ gaps: [],
61
71
  };
62
72
  }
63
- async function findCurGunGraphAssignmentAnchor(repoPath, relativePath) {
64
- return findLineAnchor(repoPath, relativePath, /\bCurGunGraph\b\s*=/i);
65
- }
66
- async function findMethodAnchor(executeParameterized, filePathPattern, name) {
67
- const rows = await executeParameterized(`
68
- MATCH (n:Method)
69
- WHERE n.name = $name AND n.filePath CONTAINS $filePathPattern
70
- RETURN n.filePath AS filePath, n.startLine AS startLine
71
- ORDER BY n.startLine ASC
72
- LIMIT 1
73
- `, { name, filePathPattern });
74
- if (!rows[0])
75
- return null;
76
- return {
77
- filePath: String(rows[0].filePath || rows[0][0] || ''),
78
- line: Number(rows[0].startLine || rows[0][1] || 1),
79
- };
73
+ export async function verifyRuntimeChainOnDemand(input) {
74
+ if (!input.rule)
75
+ return undefined;
76
+ return await verifyRuleDrivenRuntimeChain(input);
80
77
  }
81
- async function buildMethodHop(repoPath, executeParameterized, filePathPattern, name, hopType, note) {
82
- const method = await findMethodAnchor(executeParameterized, filePathPattern, name);
83
- if (!method?.filePath)
84
- return null;
85
- const lines = await readFileLines(repoPath, method.filePath);
86
- const snippet = lines?.[Math.max(0, method.line - 1)]?.trim() || name;
78
+ function buildFailureRuntimeClaim(input) {
87
79
  return {
88
- hop_type: hopType,
89
- anchor: `${method.filePath}:${method.line}`,
90
- confidence: 'high',
91
- note,
92
- snippet,
80
+ rule_id: input.rule?.id || 'none',
81
+ rule_version: input.rule?.version || '0.0.0',
82
+ scope: {
83
+ resource_types: input.rule?.resource_types || [],
84
+ host_base_type: input.rule?.host_base_type || [],
85
+ trigger_family: input.rule?.trigger_family || 'none',
86
+ },
87
+ status: 'failed',
88
+ evidence_level: 'none',
89
+ guarantees: [],
90
+ non_guarantees: input.rule?.non_guarantees?.length
91
+ ? [...input.rule.non_guarantees]
92
+ : ['runtime_chain_verification_not_executed'],
93
+ hops: [],
94
+ gaps: [],
95
+ reason: input.reason,
96
+ next_action: input.next_action,
93
97
  };
94
98
  }
95
- export async function verifyRuntimeChainOnDemand(input) {
96
- if (!shouldVerifyReloadChain(input))
97
- return undefined;
98
- const hops = [];
99
- const gaps = [];
100
- const foundSegments = new Set();
101
- const resourceAssetPath = (input.resourceBindings || [])
102
- .map((binding) => normalizeText(binding.resourcePath))
103
- .find((resourcePath) => resourcePath.includes('1_weapon_orb_key.asset'))
104
- || RESOURCE_ASSET_PATH;
105
- const resourceAnchor = await findLineAnchor(input.repoPath, resourceAssetPath, /gungraph|m_Script|guid/i)
106
- || await findLineAnchor(input.repoPath, resourceAssetPath, /.*/)
107
- || { anchor: `${resourceAssetPath}:1`, snippet: 'resource binding anchor unavailable in test fixture' };
108
- const hasResourceAnchor = !/unavailable in test fixture/i.test(resourceAnchor.snippet);
109
- hops.push({
110
- hop_type: 'resource',
111
- anchor: resourceAnchor.anchor,
112
- confidence: hasResourceAnchor ? 'high' : 'medium',
113
- note: `PowerUp asset references WeaponPowerUp and gungraph guid ${GRAPH_GUID}.`,
114
- snippet: resourceAnchor.snippet,
115
- });
116
- if (hasResourceAnchor) {
117
- foundSegments.add('resource');
99
+ function scoreRuntimeClaimRule(rule, input) {
100
+ const haystack = buildRuntimeMatchHaystack(input);
101
+ const tokens = Array.isArray(rule.match?.trigger_tokens) && rule.match.trigger_tokens.length > 0
102
+ ? rule.match.trigger_tokens
103
+ : parseTriggerTokens(rule.trigger_family);
104
+ if (tokens.length === 0)
105
+ return Number.NEGATIVE_INFINITY;
106
+ let score = 0;
107
+ let matchedTrigger = false;
108
+ for (const token of tokens) {
109
+ const normalized = String(token || '').trim().toLowerCase();
110
+ if (!normalized)
111
+ continue;
112
+ if (haystack.includes(normalized)) {
113
+ matchedTrigger = true;
114
+ score += 10 + normalized.length;
115
+ }
118
116
  }
119
- else {
120
- gaps.push(buildGap('resource', 'missing PowerUp asset anchor'));
117
+ if (!matchedTrigger)
118
+ return Number.NEGATIVE_INFINITY;
119
+ const boostLists = [
120
+ ...(Array.isArray(rule.match?.host_base_type) ? [rule.match.host_base_type] : []),
121
+ ...(Array.isArray(rule.host_base_type) ? [rule.host_base_type] : []),
122
+ ];
123
+ for (const list of boostLists) {
124
+ for (const token of list) {
125
+ const normalized = String(token || '').trim().toLowerCase();
126
+ if (normalized && haystack.includes(normalized))
127
+ score += 20 + normalized.length;
128
+ }
121
129
  }
122
- const graphAnchor = await findLineAnchor(input.repoPath, GRAPH_ASSET_PATH, /ResultRPM|GunOutput\.RPM/i);
123
- const reloadMetaAnchor = await findLineAnchor(input.repoPath, RELOAD_META_PATH, new RegExp(RELOAD_GUID, 'i'));
124
- if (graphAnchor || reloadMetaAnchor) {
125
- hops.push({
126
- hop_type: 'guid_map',
127
- anchor: graphAnchor?.anchor || reloadMetaAnchor.anchor,
128
- confidence: 'high',
129
- note: `Graph asset guid ${GRAPH_GUID} maps to Reload.cs.meta guid ${RELOAD_GUID}; wiring includes ResultRPM -> GunOutput.RPM.`,
130
- snippet: graphAnchor?.snippet || reloadMetaAnchor?.snippet,
131
- });
132
- foundSegments.add('guid_map');
130
+ const resourceLists = [
131
+ ...(Array.isArray(rule.match?.resource_types) ? [rule.match.resource_types] : []),
132
+ ...(Array.isArray(rule.resource_types) ? [rule.resource_types] : []),
133
+ ];
134
+ for (const list of resourceLists) {
135
+ for (const token of list) {
136
+ const normalized = String(token || '').trim().toLowerCase();
137
+ if (normalized && haystack.includes(normalized))
138
+ score += 4 + normalized.length;
139
+ }
133
140
  }
134
- else {
135
- hops.push({
136
- hop_type: 'guid_map',
137
- anchor: `${GRAPH_ASSET_PATH}:1`,
138
- confidence: 'medium',
139
- note: `Graph asset guid ${GRAPH_GUID} maps to Reload.cs.meta guid ${RELOAD_GUID}; wiring includes ResultRPM -> GunOutput.RPM.`,
140
- snippet: 'guid_map anchor unavailable in test fixture',
141
- });
142
- gaps.push(buildGap('guid_map', 'missing Reload guid_map or graph wiring anchor'));
141
+ for (const token of rule.match?.module_scope || []) {
142
+ const normalized = String(token || '').trim().toLowerCase();
143
+ if (normalized && haystack.includes(normalized))
144
+ score += 8 + normalized.length;
143
145
  }
144
- const loaderMethodHop = await buildMethodHop(input.repoPath, input.executeParameterized, 'Assets/NEON/Code/Game/PowerUps/WeaponPowerUp.cs', 'Equip', 'code_loader', 'PickItUp -> EquipWithEvent -> Equip; CurGunGraph assignment happens in WeaponPowerUp.Equip.');
145
- const loaderAssignmentAnchor = await findCurGunGraphAssignmentAnchor(input.repoPath, 'Assets/NEON/Code/Game/PowerUps/WeaponPowerUp.cs');
146
- if (loaderAssignmentAnchor) {
147
- hops.push({
148
- hop_type: 'code_loader',
149
- anchor: loaderAssignmentAnchor.anchor,
150
- confidence: 'high',
151
- note: 'PickItUp -> EquipWithEvent -> Equip; CurGunGraph assignment happens in WeaponPowerUp.Equip.',
152
- snippet: loaderAssignmentAnchor.snippet,
153
- });
154
- foundSegments.add('code_loader');
146
+ return score;
147
+ }
148
+ export async function verifyRuntimeClaimOnDemand(input) {
149
+ let registry;
150
+ try {
151
+ registry = await loadRuleRegistry(input.repoPath, input.rulesRoot);
155
152
  }
156
- else if (loaderMethodHop) {
157
- hops.push({
158
- ...loaderMethodHop,
159
- confidence: 'medium',
160
- note: 'PickItUp -> EquipWithEvent -> Equip path found, but CurGunGraph assignment anchor is missing.',
161
- });
162
- gaps.push(buildGap('loader', 'missing CurGunGraph assignment anchor in Equip path'));
153
+ catch (error) {
154
+ if (error instanceof RuleRegistryLoadError) {
155
+ if (error.code === 'rule_catalog_missing' || error.code === 'rule_file_missing') {
156
+ return buildFailureRuntimeClaim({
157
+ reason: 'rule_not_matched',
158
+ next_action: buildDefaultVerifyNextCommand(input.queryText),
159
+ });
160
+ }
161
+ }
162
+ throw error;
163
163
  }
164
- else {
165
- hops.push({
166
- hop_type: 'code_loader',
167
- anchor: 'Assets/NEON/Code/Game/PowerUps/WeaponPowerUp.cs:1',
168
- confidence: 'medium',
169
- note: 'PickItUp -> EquipWithEvent -> Equip; CurGunGraph assignment happens in WeaponPowerUp.Equip.',
170
- snippet: 'loader anchor unavailable in test fixture',
164
+ const activeRules = registry.activeRules || [];
165
+ const fallbackNextAction = buildDefaultVerifyNextCommand(input.queryText);
166
+ if (activeRules.length === 0) {
167
+ return buildFailureRuntimeClaim({
168
+ reason: 'rule_not_matched',
169
+ next_action: fallbackNextAction,
171
170
  });
172
- gaps.push(buildGap('loader', 'missing PickItUp/EquipWithEvent/Equip anchor and CurGunGraph assignment anchor'));
173
- }
174
- const runtimeGraphHop = await buildMethodHop(input.repoPath, input.executeParameterized, 'GunGraph', 'StartRoutineWithEvents', 'code_runtime', 'GunGraphMB.RegisterGraphEvents -> GunGraph.RegisterEvents -> StartRoutineWithEvents.') || await buildMethodHop(input.repoPath, input.executeParameterized, 'GunGraphMB.cs', 'RegisterGraphEvents', 'code_runtime', 'GunGraphMB.RegisterGraphEvents -> GunGraph.RegisterEvents -> StartRoutineWithEvents.');
175
- let hasRuntimeGraphAnchor = false;
176
- if (runtimeGraphHop) {
177
- hops.push(runtimeGraphHop);
178
- hasRuntimeGraphAnchor = true;
179
- }
180
- const reloadRuntimeHop = await buildMethodHop(input.repoPath, input.executeParameterized, 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs', 'GetValue', 'code_runtime', 'ReloadBase.GetValue -> CheckReload -> ReloadRoutine closes the runtime reload chain.');
181
- let hasReloadRuntimeAnchor = false;
182
- if (reloadRuntimeHop) {
183
- hops.push(reloadRuntimeHop);
184
- hasReloadRuntimeAnchor = true;
185
171
  }
186
- if (hasRuntimeGraphAnchor && hasReloadRuntimeAnchor) {
187
- foundSegments.add('code_runtime');
172
+ const matchedRule = [...activeRules]
173
+ .map((rule) => ({ rule, score: scoreRuntimeClaimRule(rule, input) }))
174
+ .filter((entry) => Number.isFinite(entry.score))
175
+ .sort((a, b) => (b.score - a.score) || a.rule.id.localeCompare(b.rule.id))[0]?.rule;
176
+ if (!matchedRule) {
177
+ return buildFailureRuntimeClaim({
178
+ reason: 'rule_not_matched',
179
+ next_action: fallbackNextAction,
180
+ });
188
181
  }
189
- else {
190
- const missingRuntimeAnchors = [];
191
- if (!hasRuntimeGraphAnchor)
192
- missingRuntimeAnchors.push('RegisterEvents/StartRoutineWithEvents');
193
- if (!hasReloadRuntimeAnchor)
194
- missingRuntimeAnchors.push('ReloadBase.GetValue/CheckReload/ReloadRoutine');
195
- if (!hasRuntimeGraphAnchor && !hasReloadRuntimeAnchor) {
196
- hops.push({
197
- hop_type: 'code_runtime',
198
- anchor: 'Assets/NEON/Code/Game/Graph/Nodes/Reloads/ReloadBase.cs:1',
199
- confidence: 'medium',
200
- note: 'RegisterEvents -> StartRoutineWithEvents -> ReloadBase.GetValue -> CheckReload -> ReloadRoutine.',
201
- snippet: 'runtime anchor unavailable in test fixture',
202
- });
203
- }
204
- gaps.push(buildGap('runtime', `missing runtime closure anchors: ${missingRuntimeAnchors.join(' + ')}`));
182
+ const runtimeChain = await verifyRuntimeChainOnDemand({
183
+ ...input,
184
+ requiredHops: matchedRule.required_hops,
185
+ rule: matchedRule,
186
+ });
187
+ if (!runtimeChain) {
188
+ return buildFailureRuntimeClaim({
189
+ reason: 'rule_matched_but_evidence_missing',
190
+ next_action: matchedRule.next_action || buildDefaultVerifyNextCommand(input.queryText),
191
+ rule: matchedRule,
192
+ });
205
193
  }
206
- const requiredSegments = ['resource', 'guid_map', 'code_loader', 'code_runtime'];
207
- const evidence_level = deriveRuntimeChainEvidenceLevel({
208
- mode: hops.length > 0 ? 'verified_hops' : 'none',
209
- requiredSegments,
210
- foundSegments: [...foundSegments],
194
+ const normalizedStatus = (runtimeChain.status === 'pending' ? 'failed' : runtimeChain.status);
195
+ const verificationFailed = normalizedStatus === 'failed'
196
+ || (runtimeChain.evidence_level === 'none' && normalizedStatus !== 'verified_full');
197
+ const resolved = buildRuntimeClaimFromRule({
198
+ rule: matchedRule,
199
+ status: verificationFailed ? 'failed' : normalizedStatus,
200
+ evidence_level: runtimeChain.evidence_level,
201
+ hops: runtimeChain.hops,
202
+ gaps: runtimeChain.gaps,
203
+ ...(verificationFailed
204
+ ? {
205
+ reason: 'rule_matched_but_verification_failed',
206
+ next_action: matchedRule.next_action || buildDefaultVerifyNextCommand(input.queryText),
207
+ }
208
+ : {}),
211
209
  });
212
- const status = evidence_level === 'verified_chain' ? 'verified_full'
213
- : hops.length > 0 ? 'verified_partial'
214
- : 'failed';
215
- return {
216
- status,
217
- evidence_level,
218
- hops,
219
- gaps,
220
- };
210
+ const chainClosed = resolved.status === 'verified_full'
211
+ && resolved.evidence_level === 'verified_chain'
212
+ && resolved.gaps.length === 0;
213
+ if (input.minimumEvidenceSatisfied === false && !chainClosed) {
214
+ return {
215
+ ...resolved,
216
+ status: 'failed',
217
+ evidence_level: 'clue',
218
+ guarantees: [],
219
+ non_guarantees: [...resolved.non_guarantees, 'minimum_evidence_contract_not_satisfied'],
220
+ reason: 'rule_matched_but_evidence_missing',
221
+ next_action: matchedRule.next_action || buildDefaultVerifyNextCommand(input.queryText),
222
+ };
223
+ }
224
+ return resolved;
221
225
  }