@veewo/gitnexus 1.4.8 → 1.4.9-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.
- package/dist/cli/index.js +8 -0
- package/dist/cli/tool.d.ts +11 -0
- package/dist/cli/tool.js +31 -0
- package/dist/cli/unity-ui-trace.test.d.ts +1 -0
- package/dist/cli/unity-ui-trace.test.js +24 -0
- package/dist/core/ingestion/call-processor.js +9 -1
- package/dist/core/ingestion/type-env.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +17 -2
- package/dist/core/ingestion/type-extractors/types.d.ts +4 -1
- package/dist/core/unity/csharp-selector-binding.d.ts +7 -0
- package/dist/core/unity/csharp-selector-binding.js +32 -0
- package/dist/core/unity/csharp-selector-binding.test.d.ts +1 -0
- package/dist/core/unity/csharp-selector-binding.test.js +14 -0
- package/dist/core/unity/scan-context.d.ts +2 -0
- package/dist/core/unity/scan-context.js +17 -0
- package/dist/core/unity/ui-asset-ref-scanner.d.ts +14 -0
- package/dist/core/unity/ui-asset-ref-scanner.js +213 -0
- package/dist/core/unity/ui-asset-ref-scanner.test.d.ts +1 -0
- package/dist/core/unity/ui-asset-ref-scanner.test.js +44 -0
- package/dist/core/unity/ui-meta-index.d.ts +8 -0
- package/dist/core/unity/ui-meta-index.js +60 -0
- package/dist/core/unity/ui-meta-index.test.d.ts +1 -0
- package/dist/core/unity/ui-meta-index.test.js +18 -0
- package/dist/core/unity/ui-trace-storage-guard.test.d.ts +1 -0
- package/dist/core/unity/ui-trace-storage-guard.test.js +10 -0
- package/dist/core/unity/ui-trace.acceptance.test.d.ts +1 -0
- package/dist/core/unity/ui-trace.acceptance.test.js +38 -0
- package/dist/core/unity/ui-trace.d.ts +31 -0
- package/dist/core/unity/ui-trace.js +363 -0
- package/dist/core/unity/ui-trace.test.d.ts +1 -0
- package/dist/core/unity/ui-trace.test.js +183 -0
- package/dist/core/unity/uss-selector-parser.d.ts +6 -0
- package/dist/core/unity/uss-selector-parser.js +21 -0
- package/dist/core/unity/uss-selector-parser.test.d.ts +1 -0
- package/dist/core/unity/uss-selector-parser.test.js +13 -0
- package/dist/core/unity/uxml-ref-parser.d.ts +10 -0
- package/dist/core/unity/uxml-ref-parser.js +22 -0
- package/dist/core/unity/uxml-ref-parser.test.d.ts +1 -0
- package/dist/core/unity/uxml-ref-parser.test.js +31 -0
- package/dist/mcp/local/local-backend.d.ts +13 -0
- package/dist/mcp/local/local-backend.js +298 -25
- package/dist/mcp/tools.js +41 -0
- package/package.json +2 -1
- package/skills/gitnexus-cli.md +29 -0
- package/skills/gitnexus-guide.md +23 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { glob } from 'glob';
|
|
3
|
+
import { buildAssetMetaIndex } from './meta-index.js';
|
|
4
|
+
export async function buildUnityUiMetaIndex(repoRoot, options = {}) {
|
|
5
|
+
const metaFiles = await resolveUiMetaFiles(repoRoot, options.scopedPaths);
|
|
6
|
+
if (metaFiles.length === 0) {
|
|
7
|
+
return {
|
|
8
|
+
uxmlGuidToPath: new Map(),
|
|
9
|
+
ussGuidToPath: new Map(),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const guidToAssetPath = await buildAssetMetaIndex(repoRoot, { metaFiles });
|
|
13
|
+
const uxmlGuidToPath = new Map();
|
|
14
|
+
const ussGuidToPath = new Map();
|
|
15
|
+
for (const [guid, assetPath] of guidToAssetPath.entries()) {
|
|
16
|
+
const normalizedPath = assetPath.replace(/\\/g, '/');
|
|
17
|
+
if (normalizedPath.endsWith('.uxml')) {
|
|
18
|
+
uxmlGuidToPath.set(guid, normalizedPath);
|
|
19
|
+
uxmlGuidToPath.set(guid.toLowerCase(), normalizedPath);
|
|
20
|
+
}
|
|
21
|
+
if (normalizedPath.endsWith('.uss')) {
|
|
22
|
+
ussGuidToPath.set(guid, normalizedPath);
|
|
23
|
+
ussGuidToPath.set(guid.toLowerCase(), normalizedPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
uxmlGuidToPath,
|
|
28
|
+
ussGuidToPath,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function resolveUiMetaFiles(repoRoot, scopedPaths) {
|
|
32
|
+
if (!scopedPaths || scopedPaths.length === 0) {
|
|
33
|
+
return (await glob(['**/*.uxml.meta', '**/*.uss.meta'], {
|
|
34
|
+
cwd: repoRoot,
|
|
35
|
+
nodir: true,
|
|
36
|
+
dot: false,
|
|
37
|
+
})).sort((left, right) => left.localeCompare(right));
|
|
38
|
+
}
|
|
39
|
+
const out = new Set();
|
|
40
|
+
for (const scopedPath of scopedPaths) {
|
|
41
|
+
const normalized = normalizeRelativePath(repoRoot, scopedPath);
|
|
42
|
+
if (!normalized)
|
|
43
|
+
continue;
|
|
44
|
+
if (normalized.endsWith('.uxml.meta') || normalized.endsWith('.uss.meta')) {
|
|
45
|
+
out.add(normalized);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (normalized.endsWith('.uxml') || normalized.endsWith('.uss')) {
|
|
49
|
+
out.add(`${normalized}.meta`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...out].sort((left, right) => left.localeCompare(right));
|
|
53
|
+
}
|
|
54
|
+
function normalizeRelativePath(repoRoot, filePath) {
|
|
55
|
+
const relativePath = path.isAbsolute(filePath) ? path.relative(repoRoot, filePath) : filePath;
|
|
56
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
57
|
+
if (normalized.startsWith('../'))
|
|
58
|
+
return null;
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { buildUnityUiMetaIndex } from './ui-meta-index.js';
|
|
6
|
+
import { buildUnityScanContext } from './scan-context.js';
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity-ui');
|
|
9
|
+
test('builds *.uxml.meta/*.uss.meta guid indexes', async () => {
|
|
10
|
+
const index = await buildUnityUiMetaIndex(fixtureRoot);
|
|
11
|
+
assert.equal(index.uxmlGuidToPath.get('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), 'Assets/UI/Screens/EliteBossScreenNew.uxml');
|
|
12
|
+
assert.equal(index.ussGuidToPath.get('dddddddddddddddddddddddddddddddd'), 'Assets/UI/Styles/EliteBossScreenNew.uss');
|
|
13
|
+
});
|
|
14
|
+
test('buildUnityScanContext exposes uxml/uss guid indexes', async () => {
|
|
15
|
+
const context = await buildUnityScanContext({ repoRoot: fixtureRoot });
|
|
16
|
+
assert.equal(context.uxmlGuidToPath?.get('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'), 'Assets/UI/Screens/DressUpScreenNew.uxml');
|
|
17
|
+
assert.equal(context.ussGuidToPath?.get('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'), 'Assets/UI/Styles/DressUpScreenNew.uss');
|
|
18
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { NODE_TABLES, REL_TYPES } from '../lbug/schema.js';
|
|
4
|
+
test('v1 ui trace does not require schema migration', () => {
|
|
5
|
+
assert.equal(NODE_TABLES.includes('Uxml'), false);
|
|
6
|
+
assert.equal(NODE_TABLES.includes('Uss'), false);
|
|
7
|
+
assert.equal(REL_TYPES.includes('UNITY_UI_TEMPLATE_REF'), false);
|
|
8
|
+
assert.equal(REL_TYPES.includes('UNITY_UI_STYLE_REF'), false);
|
|
9
|
+
assert.equal(REL_TYPES.includes('UNITY_UI_SELECTOR_BINDS'), false);
|
|
10
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { runUnityUiTrace } from './ui-trace.js';
|
|
6
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity-ui');
|
|
8
|
+
test('Q1: EliteBossScreenNew TooltipBox layout file trace', async () => {
|
|
9
|
+
const out = await runUnityUiTrace({
|
|
10
|
+
repoRoot: fixtureRoot,
|
|
11
|
+
target: 'Assets/UI/Screens/EliteBossScreenNew.uxml',
|
|
12
|
+
goal: 'template_refs',
|
|
13
|
+
});
|
|
14
|
+
assert.equal(out.results.length, 1);
|
|
15
|
+
assert.equal(out.results[0].evidence_chain[1].path, 'Assets/UI/Components/TooltipBox.uxml');
|
|
16
|
+
});
|
|
17
|
+
test('Q2: DressUpScreenNew template refs trace', async () => {
|
|
18
|
+
const out = await runUnityUiTrace({
|
|
19
|
+
repoRoot: fixtureRoot,
|
|
20
|
+
target: 'Assets/UI/Screens/DressUpScreenNew.uxml',
|
|
21
|
+
goal: 'template_refs',
|
|
22
|
+
});
|
|
23
|
+
assert.equal(out.results.length, 1);
|
|
24
|
+
assert.equal(out.results[0].evidence_chain[1].path, 'Assets/UI/Components/TooltipBox.uxml');
|
|
25
|
+
});
|
|
26
|
+
test('csharp target and uxml target resolve identical answers for asset_refs', async () => {
|
|
27
|
+
const outByClass = await runUnityUiTrace({
|
|
28
|
+
repoRoot: fixtureRoot,
|
|
29
|
+
target: 'EliteBossScreenController',
|
|
30
|
+
goal: 'asset_refs',
|
|
31
|
+
});
|
|
32
|
+
const outByUxml = await runUnityUiTrace({
|
|
33
|
+
repoRoot: fixtureRoot,
|
|
34
|
+
target: 'Assets/UI/Screens/EliteBossScreenNew.uxml',
|
|
35
|
+
goal: 'asset_refs',
|
|
36
|
+
});
|
|
37
|
+
assert.deepEqual(outByClass.results, outByUxml.results);
|
|
38
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type UnityUiTraceGoal = 'asset_refs' | 'template_refs' | 'selector_bindings';
|
|
2
|
+
export type UnityUiSelectorMode = 'strict' | 'balanced';
|
|
3
|
+
export interface UnityUiTraceEvidenceHop {
|
|
4
|
+
path: string;
|
|
5
|
+
line: number;
|
|
6
|
+
snippet: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UnityUiTraceResult {
|
|
9
|
+
key: string;
|
|
10
|
+
evidence_chain: UnityUiTraceEvidenceHop[];
|
|
11
|
+
score?: number;
|
|
12
|
+
confidence?: 'high' | 'medium' | 'low';
|
|
13
|
+
}
|
|
14
|
+
export interface UnityUiTraceDiagnostic {
|
|
15
|
+
code: 'ambiguous' | 'not_found';
|
|
16
|
+
message: string;
|
|
17
|
+
candidates: UnityUiTraceEvidenceHop[];
|
|
18
|
+
}
|
|
19
|
+
export interface UnityUiTraceOutput {
|
|
20
|
+
goal: UnityUiTraceGoal;
|
|
21
|
+
target: string;
|
|
22
|
+
results: UnityUiTraceResult[];
|
|
23
|
+
diagnostics: UnityUiTraceDiagnostic[];
|
|
24
|
+
}
|
|
25
|
+
export interface UnityUiTraceInput {
|
|
26
|
+
repoRoot: string;
|
|
27
|
+
target: string;
|
|
28
|
+
goal: UnityUiTraceGoal;
|
|
29
|
+
selectorMode?: UnityUiSelectorMode;
|
|
30
|
+
}
|
|
31
|
+
export declare function runUnityUiTrace(input: UnityUiTraceInput): Promise<UnityUiTraceOutput>;
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { scanUiAssetRefs } from './ui-asset-ref-scanner.js';
|
|
5
|
+
import { parseUxmlRefs } from './uxml-ref-parser.js';
|
|
6
|
+
import { parseUssSelectors } from './uss-selector-parser.js';
|
|
7
|
+
import { extractCsharpSelectorBindings } from './csharp-selector-binding.js';
|
|
8
|
+
import { buildUnityUiMetaIndex } from './ui-meta-index.js';
|
|
9
|
+
import { buildMetaIndex } from './meta-index.js';
|
|
10
|
+
export async function runUnityUiTrace(input) {
|
|
11
|
+
const target = String(input.target || '').trim();
|
|
12
|
+
const goal = input.goal;
|
|
13
|
+
const selectorMode = input.selectorMode || 'balanced';
|
|
14
|
+
const key = canonicalKey(target);
|
|
15
|
+
const uiMeta = await buildUnityUiMetaIndex(input.repoRoot);
|
|
16
|
+
const uxmlCandidates = resolveTargetUxmlCandidates(input.repoRoot, target, key, uiMeta.uxmlGuidToPath);
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
if (uxmlCandidates.length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
goal,
|
|
21
|
+
target,
|
|
22
|
+
results: [],
|
|
23
|
+
diagnostics: [
|
|
24
|
+
{
|
|
25
|
+
code: 'not_found',
|
|
26
|
+
message: 'No matching UXML target found.',
|
|
27
|
+
candidates: [],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (uxmlCandidates.length > 1) {
|
|
33
|
+
return {
|
|
34
|
+
goal,
|
|
35
|
+
target,
|
|
36
|
+
results: [],
|
|
37
|
+
diagnostics: [
|
|
38
|
+
{
|
|
39
|
+
code: 'ambiguous',
|
|
40
|
+
message: 'Target resolves to multiple UXML files.',
|
|
41
|
+
candidates: uxmlCandidates.map((candidate) => ({ path: candidate, line: 1, snippet: 'target-candidate' })),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const targetUxmlPath = uxmlCandidates[0];
|
|
47
|
+
const results = await resolveGoal({
|
|
48
|
+
repoRoot: input.repoRoot,
|
|
49
|
+
goal,
|
|
50
|
+
selectorMode,
|
|
51
|
+
target,
|
|
52
|
+
targetKey: key,
|
|
53
|
+
targetUxmlPath,
|
|
54
|
+
uiMeta,
|
|
55
|
+
});
|
|
56
|
+
if (results.length === 0) {
|
|
57
|
+
diagnostics.push({
|
|
58
|
+
code: 'not_found',
|
|
59
|
+
message: 'No evidence chain found.',
|
|
60
|
+
candidates: [],
|
|
61
|
+
});
|
|
62
|
+
return { goal, target, results: [], diagnostics };
|
|
63
|
+
}
|
|
64
|
+
return { goal, target, results, diagnostics };
|
|
65
|
+
}
|
|
66
|
+
async function resolveGoal(input) {
|
|
67
|
+
if (input.goal === 'asset_refs') {
|
|
68
|
+
const targetUxmlGuid = findGuidForUxmlPath(input.targetUxmlPath, input.uiMeta.uxmlGuidToPath);
|
|
69
|
+
if (!targetUxmlGuid)
|
|
70
|
+
return [];
|
|
71
|
+
const refs = await scanUiAssetRefs({
|
|
72
|
+
repoRoot: input.repoRoot,
|
|
73
|
+
targetGuids: [targetUxmlGuid],
|
|
74
|
+
});
|
|
75
|
+
return refs
|
|
76
|
+
.filter((row) => row.guid === targetUxmlGuid)
|
|
77
|
+
.map((row, index) => ({
|
|
78
|
+
key: `${row.sourcePath}:${row.line}:${index}`,
|
|
79
|
+
evidence_chain: [
|
|
80
|
+
{ path: row.sourcePath, line: row.line, snippet: row.snippet },
|
|
81
|
+
{ path: input.targetUxmlPath, line: 1, snippet: 'resolved-by-guid' },
|
|
82
|
+
],
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
if (input.goal === 'template_refs') {
|
|
86
|
+
const source = await safeRead(path.join(input.repoRoot, input.targetUxmlPath));
|
|
87
|
+
const parsed = parseUxmlRefs(source);
|
|
88
|
+
const templates = parsed.templates
|
|
89
|
+
.map((template) => ({
|
|
90
|
+
template,
|
|
91
|
+
resolvedPath: input.uiMeta.uxmlGuidToPath.get(template.guid),
|
|
92
|
+
}))
|
|
93
|
+
.filter((row) => Boolean(row.resolvedPath));
|
|
94
|
+
return templates.map((row, index) => ({
|
|
95
|
+
key: `${row.resolvedPath}:${row.template.line}:${index}`,
|
|
96
|
+
evidence_chain: [
|
|
97
|
+
{ path: input.targetUxmlPath, line: row.template.line, snippet: row.template.snippet },
|
|
98
|
+
{ path: row.resolvedPath, line: 1, snippet: 'template-target' },
|
|
99
|
+
],
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
const scriptPaths = await resolveScriptPathsForSelectorTarget(input.repoRoot, input.target, input.targetUxmlPath, input.targetKey, input.uiMeta);
|
|
103
|
+
const csharpBindings = await scanCsharpBindings(input.repoRoot, input.targetKey, scriptPaths);
|
|
104
|
+
const styles = await resolveStyleSelectorsForUxml(input.repoRoot, input.targetUxmlPath, input.uiMeta);
|
|
105
|
+
const styleSelectorTokenIndex = buildStyleSelectorTokenIndex(styles, input.selectorMode);
|
|
106
|
+
const selectorMatches = [];
|
|
107
|
+
for (const binding of csharpBindings) {
|
|
108
|
+
const normalizedClass = normalizeClassToken(binding.className);
|
|
109
|
+
if (!normalizedClass)
|
|
110
|
+
continue;
|
|
111
|
+
const styleEvidenceList = styleSelectorTokenIndex.get(normalizedClass) || [];
|
|
112
|
+
for (const styleEvidence of styleEvidenceList) {
|
|
113
|
+
const score = scoreSelectorBindingMatch(binding, styleEvidence, input.targetKey);
|
|
114
|
+
selectorMatches.push({
|
|
115
|
+
key: `${binding.path}:${binding.line}:${styleEvidence.path}:${styleEvidence.line}:${normalizedClass}`,
|
|
116
|
+
score,
|
|
117
|
+
evidence_chain: [
|
|
118
|
+
{ path: binding.path, line: binding.line, snippet: binding.snippet },
|
|
119
|
+
{ path: styleEvidence.path, line: styleEvidence.line, snippet: styleEvidence.snippet },
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
selectorMatches.sort((left, right) => {
|
|
125
|
+
const scoreDiff = (right.score || 0) - (left.score || 0);
|
|
126
|
+
if (scoreDiff !== 0)
|
|
127
|
+
return scoreDiff;
|
|
128
|
+
const leftHop = left.evidence_chain[0];
|
|
129
|
+
const rightHop = right.evidence_chain[0];
|
|
130
|
+
const pathDiff = leftHop.path.localeCompare(rightHop.path);
|
|
131
|
+
if (pathDiff !== 0)
|
|
132
|
+
return pathDiff;
|
|
133
|
+
return leftHop.line - rightHop.line;
|
|
134
|
+
});
|
|
135
|
+
return dedupeTraceResults(selectorMatches);
|
|
136
|
+
}
|
|
137
|
+
function resolveTargetUxmlCandidates(repoRoot, target, targetKey, uxmlGuidToPath) {
|
|
138
|
+
const normalizedTarget = normalizeUxmlTargetPath(repoRoot, target);
|
|
139
|
+
if (normalizedTarget) {
|
|
140
|
+
const exactPath = findExactUxmlPath(normalizedTarget, uxmlGuidToPath);
|
|
141
|
+
if (exactPath) {
|
|
142
|
+
return [exactPath];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const set = new Set();
|
|
146
|
+
for (const p of uxmlGuidToPath.values()) {
|
|
147
|
+
if (canonicalKey(path.basename(p, '.uxml')) === targetKey) {
|
|
148
|
+
set.add(p);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return [...set];
|
|
152
|
+
}
|
|
153
|
+
function canonicalKey(input) {
|
|
154
|
+
return String(input || '')
|
|
155
|
+
.replace(/\\/g, '/')
|
|
156
|
+
.split('/')
|
|
157
|
+
.pop()
|
|
158
|
+
.replace(/\.uxml$/i, '')
|
|
159
|
+
.replace(/controller$/i, '')
|
|
160
|
+
.replace(/new$/i, '')
|
|
161
|
+
.replace(/[^a-z0-9]/gi, '')
|
|
162
|
+
.toLowerCase();
|
|
163
|
+
}
|
|
164
|
+
async function scanCsharpBindings(repoRoot, targetKey, preferredScriptPaths = []) {
|
|
165
|
+
const preferredSet = new Set(preferredScriptPaths);
|
|
166
|
+
const fallbackScriptPaths = (await glob('**/*.cs', { cwd: repoRoot, nodir: true, dot: false }))
|
|
167
|
+
.filter((scriptPath) => canonicalKey(path.basename(scriptPath, '.cs')) === targetKey);
|
|
168
|
+
const scriptPaths = [...new Set([...preferredScriptPaths, ...fallbackScriptPaths])]
|
|
169
|
+
.sort((left, right) => left.localeCompare(right));
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const scriptPath of scriptPaths) {
|
|
172
|
+
const source = await safeRead(path.join(repoRoot, scriptPath));
|
|
173
|
+
const bindings = extractCsharpSelectorBindings(source);
|
|
174
|
+
for (const binding of bindings) {
|
|
175
|
+
out.push({
|
|
176
|
+
path: scriptPath.replace(/\\/g, '/'),
|
|
177
|
+
line: binding.line,
|
|
178
|
+
snippet: binding.snippet,
|
|
179
|
+
className: binding.className,
|
|
180
|
+
source: preferredSet.has(scriptPath) ? 'resource_chain' : 'name_fallback',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
async function resolveScriptPathsForSelectorTarget(repoRoot, target, targetUxmlPath, targetKey, uiMeta) {
|
|
187
|
+
const normalizedTarget = normalizeUxmlTargetPath(repoRoot, target);
|
|
188
|
+
const isPathTarget = Boolean(normalizedTarget && findExactUxmlPath(normalizedTarget, uiMeta.uxmlGuidToPath));
|
|
189
|
+
if (!isPathTarget) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const targetUxmlGuid = findGuidForUxmlPath(targetUxmlPath, uiMeta.uxmlGuidToPath);
|
|
193
|
+
if (!targetUxmlGuid)
|
|
194
|
+
return [];
|
|
195
|
+
const assetRefs = await scanUiAssetRefs({
|
|
196
|
+
repoRoot,
|
|
197
|
+
targetGuids: [targetUxmlGuid],
|
|
198
|
+
});
|
|
199
|
+
const sourcePaths = [...new Set(assetRefs.map((row) => row.sourcePath))];
|
|
200
|
+
if (sourcePaths.length === 0)
|
|
201
|
+
return [];
|
|
202
|
+
const scopedRefs = await scanUiAssetRefs({
|
|
203
|
+
repoRoot,
|
|
204
|
+
scopedPaths: sourcePaths,
|
|
205
|
+
});
|
|
206
|
+
const scriptGuids = [...new Set(scopedRefs
|
|
207
|
+
.filter((row) => row.fieldName === 'm_Script')
|
|
208
|
+
.map((row) => row.guid.toLowerCase()))];
|
|
209
|
+
if (scriptGuids.length === 0)
|
|
210
|
+
return [];
|
|
211
|
+
const scriptMeta = await buildMetaIndex(repoRoot);
|
|
212
|
+
if (scriptMeta.size === 0)
|
|
213
|
+
return [];
|
|
214
|
+
const lowerToPath = new Map();
|
|
215
|
+
for (const [guid, scriptPath] of scriptMeta.entries()) {
|
|
216
|
+
lowerToPath.set(guid.toLowerCase(), scriptPath.replace(/\\/g, '/'));
|
|
217
|
+
}
|
|
218
|
+
const scriptPaths = scriptGuids
|
|
219
|
+
.map((guid) => lowerToPath.get(guid))
|
|
220
|
+
.filter((value) => Boolean(value))
|
|
221
|
+
.sort((left, right) => left.localeCompare(right));
|
|
222
|
+
if (scriptPaths.length > 0) {
|
|
223
|
+
return [...new Set(scriptPaths)];
|
|
224
|
+
}
|
|
225
|
+
return (await glob('**/*.cs', { cwd: repoRoot, nodir: true, dot: false }))
|
|
226
|
+
.filter((scriptPath) => canonicalKey(path.basename(scriptPath, '.cs')) === targetKey)
|
|
227
|
+
.sort((left, right) => left.localeCompare(right));
|
|
228
|
+
}
|
|
229
|
+
async function resolveStyleSelectorsForUxml(repoRoot, uxmlPath, uiMeta) {
|
|
230
|
+
const uxmlSource = await safeRead(path.join(repoRoot, uxmlPath));
|
|
231
|
+
const parsed = parseUxmlRefs(uxmlSource);
|
|
232
|
+
const stylePaths = parsed.styles
|
|
233
|
+
.map((style) => uiMeta.ussGuidToPath.get(style.guid))
|
|
234
|
+
.filter((value) => Boolean(value));
|
|
235
|
+
const out = [];
|
|
236
|
+
for (const ussPath of stylePaths) {
|
|
237
|
+
const ussSource = await safeRead(path.join(repoRoot, ussPath));
|
|
238
|
+
for (const selector of parseUssSelectors(ussSource)) {
|
|
239
|
+
out.push({
|
|
240
|
+
path: ussPath,
|
|
241
|
+
line: selector.line,
|
|
242
|
+
snippet: selector.snippet,
|
|
243
|
+
selector: selector.selector,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
async function safeRead(filePath) {
|
|
250
|
+
try {
|
|
251
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (error.code === 'ENOENT')
|
|
255
|
+
return '';
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function normalizeUxmlTargetPath(repoRoot, target) {
|
|
260
|
+
const normalizedTarget = String(target || '').trim().replace(/\\/g, '/');
|
|
261
|
+
if (!normalizedTarget.toLowerCase().endsWith('.uxml'))
|
|
262
|
+
return null;
|
|
263
|
+
const relative = path.isAbsolute(normalizedTarget)
|
|
264
|
+
? path.relative(repoRoot, normalizedTarget).replace(/\\/g, '/')
|
|
265
|
+
: normalizedTarget;
|
|
266
|
+
if (relative.startsWith('../'))
|
|
267
|
+
return null;
|
|
268
|
+
return relative;
|
|
269
|
+
}
|
|
270
|
+
function findExactUxmlPath(targetPath, uxmlGuidToPath) {
|
|
271
|
+
for (const candidate of uxmlGuidToPath.values()) {
|
|
272
|
+
if (candidate.toLowerCase() === targetPath.toLowerCase()) {
|
|
273
|
+
return candidate;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
function findGuidForUxmlPath(targetUxmlPath, uxmlGuidToPath) {
|
|
279
|
+
for (const [guid, assetPath] of uxmlGuidToPath.entries()) {
|
|
280
|
+
if (assetPath !== targetUxmlPath)
|
|
281
|
+
continue;
|
|
282
|
+
if (!/^[0-9a-f]{32}$/i.test(guid))
|
|
283
|
+
continue;
|
|
284
|
+
return guid.toLowerCase();
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function buildStyleSelectorTokenIndex(styles, selectorMode) {
|
|
289
|
+
const index = new Map();
|
|
290
|
+
for (const style of styles) {
|
|
291
|
+
for (const token of extractClassTokens(style.selector, selectorMode)) {
|
|
292
|
+
const bucket = index.get(token) || [];
|
|
293
|
+
bucket.push(style);
|
|
294
|
+
index.set(token, bucket);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return index;
|
|
298
|
+
}
|
|
299
|
+
function extractClassTokens(selector, selectorMode) {
|
|
300
|
+
if (selectorMode === 'strict') {
|
|
301
|
+
const trimmed = selector.trim();
|
|
302
|
+
if (/^\.[A-Za-z_][A-Za-z0-9_-]*$/.test(trimmed)) {
|
|
303
|
+
return [trimmed.slice(1)];
|
|
304
|
+
}
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
const out = new Set();
|
|
308
|
+
const pattern = /\.([A-Za-z_][A-Za-z0-9_-]*)/g;
|
|
309
|
+
let match = pattern.exec(selector);
|
|
310
|
+
while (match) {
|
|
311
|
+
const token = normalizeClassToken(match[1]);
|
|
312
|
+
if (token)
|
|
313
|
+
out.add(token);
|
|
314
|
+
match = pattern.exec(selector);
|
|
315
|
+
}
|
|
316
|
+
return [...out];
|
|
317
|
+
}
|
|
318
|
+
function normalizeClassToken(value) {
|
|
319
|
+
return String(value || '').trim();
|
|
320
|
+
}
|
|
321
|
+
function dedupeTraceResults(results) {
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
const out = [];
|
|
324
|
+
for (const result of results) {
|
|
325
|
+
const signature = result.evidence_chain.map((hop) => `${hop.path}:${hop.line}:${hop.snippet}`).join('|');
|
|
326
|
+
if (seen.has(signature))
|
|
327
|
+
continue;
|
|
328
|
+
seen.add(signature);
|
|
329
|
+
const score = result.score;
|
|
330
|
+
out.push({
|
|
331
|
+
...result,
|
|
332
|
+
confidence: toConfidence(score),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
function toConfidence(score) {
|
|
338
|
+
const value = Number(score || 0);
|
|
339
|
+
if (value >= 7)
|
|
340
|
+
return 'high';
|
|
341
|
+
if (value >= 4)
|
|
342
|
+
return 'medium';
|
|
343
|
+
return 'low';
|
|
344
|
+
}
|
|
345
|
+
function scoreSelectorBindingMatch(binding, styleEvidence, targetKey) {
|
|
346
|
+
let score = 0;
|
|
347
|
+
if (binding.source === 'resource_chain') {
|
|
348
|
+
score += 6;
|
|
349
|
+
}
|
|
350
|
+
const scriptBaseKey = canonicalKey(path.basename(binding.path, '.cs'));
|
|
351
|
+
if (scriptBaseKey === targetKey) {
|
|
352
|
+
score += 4;
|
|
353
|
+
}
|
|
354
|
+
const normalizedSelector = styleEvidence.selector.trim();
|
|
355
|
+
const exactSelector = `.${binding.className}`;
|
|
356
|
+
if (normalizedSelector === exactSelector) {
|
|
357
|
+
score += 2;
|
|
358
|
+
}
|
|
359
|
+
if (normalizedSelector.startsWith(`${exactSelector} `)) {
|
|
360
|
+
score += 1;
|
|
361
|
+
}
|
|
362
|
+
return score;
|
|
363
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|