diff --git a/gitnexus/src/core/ingestion/documents/xaml.ts b/gitnexus/src/core/ingestion/documents/xaml.ts
index 828a1b9b3..cfc544344 100644
--- a/gitnexus/src/core/ingestion/documents/xaml.ts
+++ b/gitnexus/src/core/ingestion/documents/xaml.ts
@@ -68,10 +68,14 @@ export function extractXamlDeclarations(source: string): DocumentDeclaration[] {
!DIRECTIVES.has(parts[1])
)
continue;
- if (typeof value !== 'string' || !value.trim() || /[{}&]/.test(value)) continue;
+ if (typeof value !== 'string' || value.includes('&')) continue;
+ // A leading {} escapes the entire remaining value, including further braces.
+ const escaped = value.startsWith('{}');
+ const name = escaped ? value.slice(2) : value;
+ if (!name.trim() || (!escaped && /[{}]/.test(name))) continue;
if (span?.startIndex === undefined || span.endIndex === undefined) continue;
declarations.push({
- name: value,
+ name,
description: `${tag} x:${parts[1]} declaration`,
startIndex: span.startIndex,
startLine: lineAt(span.startIndex),
diff --git a/gitnexus/test/integration/xaml-documents.test.ts b/gitnexus/test/integration/xaml-documents.test.ts
index 904efc502..5575c506f 100644
--- a/gitnexus/test/integration/xaml-documents.test.ts
+++ b/gitnexus/test/integration/xaml-documents.test.ts
@@ -17,7 +17,7 @@ describe('XAML pipeline documents (#3202)', () => {
fixtures.push(repo);
await fs.writeFile(
path.join(repo.dbPath, 'Home.XAML'),
- `\n\n\n`,
+ `\n\n\n\n\n\n`,
);
await fs.writeFile(
path.join(repo.dbPath, 'Broken.xaml'),
@@ -35,7 +35,9 @@ describe('XAML pipeline documents (#3202)', () => {
expect(declarations.map((node) => node.properties.name).sort()).toEqual([
'Accent',
'App.Home',
+ 'EscapedAccent',
'Save',
+ '{x:Type Button}',
]);
expect(declarations.find((node) => node.properties.name === 'Save')?.properties.startLine).toBe(
1,
diff --git a/gitnexus/test/integration/xaml-search.test.ts b/gitnexus/test/integration/xaml-search.test.ts
index 9359f6e70..a7f0d4443 100644
--- a/gitnexus/test/integration/xaml-search.test.ts
+++ b/gitnexus/test/integration/xaml-search.test.ts
@@ -87,12 +87,21 @@ describe('persisted XAML declarations (#3202)', () => {
'',
);
const options = { skipAgentsMd: true, registryName: 'copy-thread-failure-fixture' };
+ let initialThreads = 0;
+ const originalCopy = adapter.loadGraphToLbug;
+ const copy = vi.spyOn(adapter, 'loadGraphToLbug').mockImplementationOnce(async (...args) => {
+ const rows = await adapter.executeQuery("CALL current_setting('threads') RETURN *");
+ initialThreads = Number(rows[0].threads);
+ return originalCopy(...args);
+ });
await runFullAnalysis(repo.dbPath, options, { onProgress() {} });
+ expect(initialThreads).toBeGreaterThan(0);
+ copy.mockClear();
const { storagePath } = getStoragePaths(repo.dbPath);
const before = await loadMeta(storagePath);
expect(before).not.toBeNull();
const failure = new Error('Injected incremental COPY failure');
- const copy = vi.spyOn(adapter, 'loadGraphToLbug').mockImplementationOnce(async () => {
+ copy.mockImplementationOnce(async () => {
const rows = await adapter.executeQuery("CALL current_setting('threads') RETURN *");
expect(Number(rows[0].threads)).toBe(1);
throw failure;
@@ -109,6 +118,51 @@ describe('persisted XAML declarations (#3202)', () => {
indexedAt: before?.indexedAt,
fileHashes: before?.fileHashes,
});
+ copy.mockImplementationOnce(async (...args) => {
+ const rows = await adapter.executeQuery("CALL current_setting('threads') RETURN *");
+ expect(Number(rows[0].threads)).toBe(initialThreads);
+ return originalCopy(...args);
+ });
+ await runFullAnalysis(repo.dbPath, options, { onProgress() {} });
+ expect(copy).toHaveBeenCalledTimes(2);
+ expect((await loadMeta(storagePath))?.incrementalInProgress).toBeUndefined();
+ await initLbug(getStoragePaths(repo.dbPath).lbugPath);
+ expect((await searchFTSFromLbug('AfterFailure')).results).toEqual(
+ expect.arrayContaining([expect.objectContaining({ filePath: 'CopyFailure.xaml' })]),
+ );
+ }, 180_000);
+
+ it('persists and searches escaped literal resource keys without the escape prefix', async () => {
+ const repo = await setupMiniRepo();
+ const home = await createTempDir();
+ fixtures.push(repo, home);
+ vi.stubEnv('GITNEXUS_HOME', home.dbPath);
+ await fs.writeFile(
+ path.join(repo.dbPath, 'Escaped.xaml'),
+ '' +
+ '' +
+ '' +
+ '',
+ );
+ await runFullAnalysis(
+ repo.dbPath,
+ { skipAgentsMd: true, registryName: 'escaped-key-fixture' },
+ { onProgress() {} },
+ );
+ await initLbug(getStoragePaths(repo.dbPath).lbugPath);
+ const rows = await executePrepared(
+ 'MATCH (n:Section) WHERE n.filePath = $file RETURN n.id AS id, n.name AS name ORDER BY name',
+ { file: 'Escaped.xaml' },
+ );
+ expect(rows).toEqual([
+ { id: expect.any(String), name: 'EscapedResource' },
+ { id: expect.any(String), name: '{LiteralResource}' },
+ ]);
+ for (const [index, term] of ['EscapedResource', 'LiteralResource'].entries()) {
+ expect((await searchFTSFromLbug(term)).results.flatMap((hit) => hit.nodeIds ?? [])).toContain(
+ rows[index].id,
+ );
+ }
}, 180_000);
it.each(['node-id', 'file-path'])(
diff --git a/gitnexus/test/unit/xaml-declarations.test.ts b/gitnexus/test/unit/xaml-declarations.test.ts
index 46d357005..735b44597 100644
--- a/gitnexus/test/unit/xaml-declarations.test.ts
+++ b/gitnexus/test/unit/xaml-declarations.test.ts
@@ -58,7 +58,7 @@ describe('XAML document declarations (#3202)', () => {
expect(
extractXamlDeclarations(`
-
+
]]>
`),
@@ -66,6 +66,32 @@ describe('XAML document declarations (#3202)', () => {
expect(extractXamlDeclarations('')).toEqual([]);
});
+ it.each([
+ ['{}escaped', 'escaped'],
+ ['{}{x:Type Button}', '{x:Type Button}'],
+ ['{}{}nested', '{}nested'],
+ ['{}left{right}', 'left{right}'],
+ ])('indexes escaped literal resource key %s as %s', (value, name) => {
+ const source = ``;
+ expect(extractXamlDeclarations(source)).toEqual([
+ {
+ name,
+ description: 'Style x:Key declaration',
+ startIndex: 0,
+ startLine: 0,
+ endLine: 0,
+ level: 1,
+ },
+ ]);
+ });
+
+ it.each(['{}', '{} ', '{}A&B', '{}{key'])(
+ 'does not index empty or unresolved entity values after an escape: %s',
+ (value) => {
+ expect(extractXamlDeclarations(``)).toEqual([]);
+ },
+ );
+
it('keeps duplicate names distinct and preserves Unicode names', () => {
const declarations = extractXamlDeclarations(
``,