Date: Tue, 11 Aug 2026 23:04:59 -0500
Subject: [PATCH 15/67] fixes: P1-8 enhance hybrid retrieval with anchor file
paths and improved import resolution
- Added support for `anchorFilePaths` in hybrid retrieval requests to allow for additional file-node anchors.
- Updated the retrieval pipeline to collect and forward editor and git references as graph anchors.
- Introduced a new `InRepoLanguageImportResolver` to handle non-relative import specifiers for various languages.
- Enhanced the `CodeIndexImportResolver` to utilize the new language resolver for improved import resolution.
- Added tests for the new functionality, ensuring correct behavior for various import scenarios and anchor handling.
---
README.md | 2 +-
apps/cli/package.json | 2 +-
apps/vscode/package.json | 2 +-
.../src/components/IndexingStatusBar.tsx | 24 +-
.../src/components/SettingsPanel.tsx | 27 +-
package.json | 2 +-
packages/host/package.json | 2 +-
.../treeSitter/WebTreeSitterRuntime.spec.ts | 29 ++
.../treeSitter/WebTreeSitterRuntime.ts | 2 +
.../createHostRepositoryContext.spec.ts | 116 +++++++
.../createHostRepositoryContext.ts | 136 +++++---
packages/sdk/package.json | 2 +-
packages/v8/package.json | 2 +-
.../pipeline/AgentEnginePipeline.ts | 14 +
packages/v8/src/index.ts | 5 +
.../src/modules/repository-context/README.md | 4 +-
.../src/modules/repository-context/index.ts | 6 +-
.../internal/context-assembly/schema.ts | 1 +
.../internal/context-assembly/types.ts | 1 +
.../HybridRetrievalRequestNormalizer.ts | 7 +
.../internal/hybrid-retrieval/README.md | 1 +
.../internal/hybrid-retrieval/constants.ts | 4 +
.../internal/hybrid-retrieval/schema.ts | 10 +
.../sources/RepoGraphRetrievalSource.ts | 126 ++++++-
.../internal/hybrid-retrieval/types.ts | 8 +
.../RepositoryContextPipeline.ts | 9 +
.../src/modules/repository-context/policy.ts | 50 ++-
.../DeriveContextSelectionBudget.spec.ts | 31 +-
.../tests/GoldenQueryRetrieval.spec.ts | 328 ++++++++++++++++++
.../tests/RepoGraphBlastRadius.spec.ts | 45 +++
.../code-indexing/CodeIndexDocumentMapper.ts | 6 +
.../code-indexing/CodeIndexImportResolver.ts | 21 +-
.../InRepoLanguageImportResolver.ts | 282 +++++++++++++++
.../internal/code-indexing/index.ts | 1 +
.../internal/code-indexing/types.ts | 1 +
.../internal/source-analysis/constants.ts | 9 +
.../languageImportResolver.spec.ts | 144 ++++++++
37 files changed, 1385 insertions(+), 77 deletions(-)
create mode 100644 packages/v8/src/modules/repository-context/tests/GoldenQueryRetrieval.spec.ts
create mode 100644 packages/v8/src/modules/repository-state/internal/code-indexing/InRepoLanguageImportResolver.ts
create mode 100644 packages/v8/src/modules/repository-state/languageImportResolver.spec.ts
diff --git a/README.md b/README.md
index 693ad492..315638e2 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-
+
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 9f2df176..df9aad87 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/cli",
- "version": "2.8.19",
+ "version": "2.8.20",
"description": "Mitii headless CLI over @mitii/sdk.",
"license": "AGPL-3.0-or-later",
"publishConfig": {
diff --git a/apps/vscode/package.json b/apps/vscode/package.json
index b1a96449..64269901 100644
--- a/apps/vscode/package.json
+++ b/apps/vscode/package.json
@@ -2,7 +2,7 @@
"name": "mitii-ai-agent",
"displayName": "Mitii AI Agent",
"description": "Local-first VS Code AI coding agent with repository-aware context and controlled execution",
- "version": "2.8.19",
+ "version": "2.8.20",
"publisher": "mitii",
"license": "AGPL-3.0-or-later",
"icon": "media/mitii-short-logo.png",
diff --git a/apps/vscode/webview-ui/src/components/IndexingStatusBar.tsx b/apps/vscode/webview-ui/src/components/IndexingStatusBar.tsx
index d7871e8e..362b9ea8 100644
--- a/apps/vscode/webview-ui/src/components/IndexingStatusBar.tsx
+++ b/apps/vscode/webview-ui/src/components/IndexingStatusBar.tsx
@@ -28,10 +28,10 @@ function resolveIndexTone(index: IndexStatusSnapshot): IndexTone {
const missingRequired = requiredCapabilities.some(
(capability) => capability.status !== 'ready',
);
- const missingVector = capabilities.some(
+ const vectorDegraded = capabilities.some(
(capability) =>
capability.capability === 'vectorIndex' &&
- capability.status !== 'ready',
+ capability.status === 'degraded',
);
if (
message.includes('indexing') ||
@@ -48,7 +48,7 @@ function resolveIndexTone(index: IndexStatusSnapshot): IndexTone {
if (missingRequired || readiness === 'unavailable' || readiness === 'degraded') {
return 'warn';
}
- if (missingVector && readiness !== 'ready') return 'warn';
+ if (vectorDegraded) return 'warn';
if (coreReady) return 'ready';
if (index.fileCount > 0 || readiness) return 'ready';
return 'idle';
@@ -70,9 +70,15 @@ function shortLabel(tone: IndexTone, index: IndexStatusSnapshot): string {
) && capability.status !== 'ready',
)
? 'Index Issue'
- : index.readiness === 'degraded'
- ? 'Degraded'
- : 'Unavailable';
+ : index.capabilities?.some(
+ (capability) =>
+ capability.capability === 'vectorIndex' &&
+ capability.status === 'degraded',
+ )
+ ? 'Embeddings'
+ : index.readiness === 'degraded'
+ ? 'Degraded'
+ : 'Unavailable';
default:
return 'Index';
}
@@ -93,7 +99,11 @@ function detailTooltip(index: IndexStatusSnapshot): string {
for (const capability of index.capabilities ?? []) {
const label =
CAPABILITY_LABELS[capability.capability] ?? capability.capability;
- parts.push(`${label}: ${capability.status}`);
+ parts.push(
+ capability.capability === 'vectorIndex' && capability.status === 'degraded'
+ ? `${label}: degraded — reindex to restore semantic search`
+ : `${label}: ${capability.status}`,
+ );
}
if (index.truncated) parts.push('Scan truncated');
if (index.message) parts.push(index.message);
diff --git a/apps/vscode/webview-ui/src/components/SettingsPanel.tsx b/apps/vscode/webview-ui/src/components/SettingsPanel.tsx
index 69a4c550..dd8cf06a 100644
--- a/apps/vscode/webview-ui/src/components/SettingsPanel.tsx
+++ b/apps/vscode/webview-ui/src/components/SettingsPanel.tsx
@@ -97,11 +97,13 @@ function displayCapabilityStatus(capability: {
status: string;
reasonCode?: string;
}): { className: string; label: string } {
- if (
- capability.capability === 'vectorIndex' &&
- capability.status === 'unavailable'
- ) {
- return { className: 'optional', label: 'not configured' };
+ if (capability.capability === 'vectorIndex') {
+ if (capability.status === 'unavailable') {
+ return { className: 'optional', label: 'not configured' };
+ }
+ if (capability.status === 'degraded') {
+ return { className: 'degraded', label: 'degraded — reindex' };
+ }
}
return { className: capability.status, label: capability.status };
}
@@ -356,10 +358,23 @@ export function SettingsPanel(props: SettingsPanelProps) {
{index.message ?? 'No index yet'}
{index.truncated ? ' · truncated' : ''}
+ {index.capabilities?.some(
+ (capability) =>
+ capability.capability === 'vectorIndex' &&
+ capability.status === 'degraded',
+ )
+ ? ' · Semantic search is degraded. Reindex to rebuild embeddings.'
+ : ''}
- Reindex
+ {index.capabilities?.some(
+ (capability) =>
+ capability.capability === 'vectorIndex' &&
+ capability.status === 'degraded',
+ )
+ ? 'Reindex embeddings'
+ : 'Reindex'}
{
]);
});
+ it('parses shell function definitions when the bash grammar is available', async () => {
+ const runtime = await createDefaultTreeSitterRuntime();
+ if (!runtime?.supports('shell')) {
+ return;
+ }
+
+ const result = await runtime.parse({
+ language: 'shell',
+ relativePath: 'scripts/setup.sh',
+ content: 'greet() {\n echo hi\n}\n',
+ symbolQuery:
+ '(function_definition name: (word) @name) @definition',
+ maximumSymbols: 10,
+ maximumImports: 10,
+ maximumReferences: 10,
+ });
+
+ if ((result.warnings ?? []).length > 0 && result.symbols.length === 0) {
+ return;
+ }
+
+ expect(result.symbols).toEqual([
+ expect.objectContaining({
+ name: 'greet',
+ startLine: 1,
+ }),
+ ]);
+ });
+
it('returns undefined when the WASM runtime cannot be resolved', async () => {
const previous = process.env.MITII_TREE_SITTER_ASSET_ROOT;
process.env.MITII_TREE_SITTER_ASSET_ROOT = '/tmp/mitii-missing-tree-sitter';
diff --git a/packages/host/src/indexing/treeSitter/WebTreeSitterRuntime.ts b/packages/host/src/indexing/treeSitter/WebTreeSitterRuntime.ts
index 5f1c1bd7..73592b03 100644
--- a/packages/host/src/indexing/treeSitter/WebTreeSitterRuntime.ts
+++ b/packages/host/src/indexing/treeSitter/WebTreeSitterRuntime.ts
@@ -102,6 +102,8 @@ export const WEB_TREE_SITTER_GRAMMAR_WASM_BY_LANGUAGE = {
python: 'tree-sitter-python.wasm',
ruby: 'tree-sitter-ruby.wasm',
rust: 'tree-sitter-rust.wasm',
+ shell: 'tree-sitter-bash.wasm',
+ sql: 'tree-sitter-sql.wasm',
swift: 'tree-sitter-swift.wasm',
tsx: 'tree-sitter-tsx.wasm',
typescript: 'tree-sitter-typescript.wasm',
diff --git a/packages/host/src/repository-context/createHostRepositoryContext.spec.ts b/packages/host/src/repository-context/createHostRepositoryContext.spec.ts
index 30bf76dd..3d663e0c 100644
--- a/packages/host/src/repository-context/createHostRepositoryContext.spec.ts
+++ b/packages/host/src/repository-context/createHostRepositoryContext.spec.ts
@@ -12,6 +12,122 @@ import { describe, expect, it } from 'vitest';
import { createHostRepositoryContext } from './createHostRepositoryContext.js';
+describe('createHostRepositoryContext file-map fallback', () => {
+ it('ranks the empty-selection fallback by repo map score and marks it partial', async () => {
+ const workspaceRoot = await mkdtemp(join(tmpdir(), 'mitii-file-map-'));
+
+ try {
+ await mkdir(join(workspaceRoot, 'src'), { recursive: true });
+ await writeFile(join(workspaceRoot, 'src', 'alpha.ts'), 'export const alpha = 1;\n', 'utf8');
+ await writeFile(join(workspaceRoot, 'src', 'zeta.ts'), 'export const zeta = 1;\n', 'utf8');
+ await mkdir(join(workspaceRoot, '.mitii'), { recursive: true });
+ await writeFile(
+ join(workspaceRoot, '.mitii', 'repository-map-workspace.json'),
+ JSON.stringify({
+ schemaVersion: 1,
+ workspaceSnapshotId:
+ '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
+ codeIndexChangeToken: 'index-1',
+ entries: [
+ {
+ file: {
+ id: 'file:zeta',
+ rootId: 'workspace',
+ relativePath: 'src/zeta.ts',
+ },
+ symbols: [],
+ score: 0.9,
+ pageRank: 0.9,
+ inboundImportCount: 4,
+ outboundImportCount: 0,
+ inboundReferenceCount: 0,
+ outboundReferenceCount: 0,
+ reasons: [],
+ },
+ {
+ file: {
+ id: 'file:alpha',
+ rootId: 'workspace',
+ relativePath: 'src/alpha.ts',
+ },
+ symbols: [],
+ score: 0.1,
+ pageRank: 0.1,
+ inboundImportCount: 0,
+ outboundImportCount: 0,
+ inboundReferenceCount: 0,
+ outboundReferenceCount: 0,
+ reasons: [],
+ },
+ ],
+ statistics: {
+ availableFiles: 2,
+ rankedFiles: 2,
+ includedFiles: 2,
+ includedSymbols: 0,
+ estimatedTokens: 40,
+ durationMs: 0,
+ },
+ status: 'complete',
+ generatedAt: new Date(0).toISOString(),
+ }),
+ 'utf8',
+ );
+
+ const repositoryState = new RepositoryStatePipeline({
+ store: new InMemoryRepositoryStateStore(),
+ });
+ const published = await repositoryState.publish(
+ publishRepositoryStateInputSchema.parse({
+ schemaVersion: 1,
+ workspaceId: 'workspace-test',
+ snapshotId:
+ '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
+ scanCompleteness: 'complete',
+ roots: [
+ {
+ rootId: 'workspace',
+ projectCatalogRevision: 'catalog-1',
+ mapRevision: 'map-1',
+ capabilities: [
+ { capability: 'catalog', status: 'ready' },
+ { capability: 'map', status: 'ready' },
+ ],
+ },
+ ],
+ reasons: [],
+ generatedAt: new Date(0).toISOString(),
+ }),
+ );
+ expect(published.status).toBe('published');
+ if (published.status !== 'published') return;
+
+ const result = await createHostRepositoryContext({
+ repositoryState,
+ workspaceRoot,
+ openDatabase: (() => {
+ throw new Error('text index database should not be opened');
+ }) as never,
+ }).execute({
+ state: published.reference,
+ query: 'where is the important code',
+ mode: 'ask',
+ });
+
+ expect(result.assembly.status).toBe('partial');
+ expect(
+ result.warnings.some((warning) => warning.code === 'file_map_fallback'),
+ ).toBe(true);
+ const content = result.assembly.blocks[0]?.content ?? '';
+ expect(content.indexOf('src/zeta.ts')).toBeLessThan(
+ content.indexOf('src/alpha.ts'),
+ );
+ } finally {
+ await rm(workspaceRoot, { recursive: true, force: true });
+ }
+ });
+});
+
describe('createHostRepositoryContext git priors', () => {
it('adds dirty git files as git_diff selection origins', async () => {
const workspaceRoot = await mkdtemp(join(tmpdir(), 'mitii-git-priors-'));
diff --git a/packages/host/src/repository-context/createHostRepositoryContext.ts b/packages/host/src/repository-context/createHostRepositoryContext.ts
index 105a278d..9d4ba2f5 100644
--- a/packages/host/src/repository-context/createHostRepositoryContext.ts
+++ b/packages/host/src/repository-context/createHostRepositoryContext.ts
@@ -83,6 +83,7 @@ export function createHostRepositoryContext(options: {
const textIndexDatabasePath =
options.textIndexDatabasePath ?? join(workspaceRoot, '.mitii', INDEX_DB_FILE);
const resolvedDescriptors = new Map();
+ const resolvedRepoMaps = new Map();
const selector: RepositoryContextSelectorPort = new ContextSelector();
const defaultAssembler = new ContextAssemblyFactory().create({
fileSystem: new NodeFileSystemAdapter(),
@@ -123,6 +124,9 @@ export function createHostRepositoryContext(options: {
workspaceRoot,
descriptor,
);
+ if (repositoryIntelligence.repoMap) {
+ resolvedRepoMaps.set(descriptor.snapshotId, repositoryIntelligence.repoMap);
+ }
return {
status: 'resolved',
artifacts: {
@@ -141,7 +145,9 @@ export function createHostRepositoryContext(options: {
semanticIndex: options.semanticIndex,
}),
selector,
- assembler: createHostAssembler(defaultAssembler),
+ assembler: createHostAssembler(defaultAssembler, (snapshotId) =>
+ resolvedRepoMaps.get(snapshotId),
+ ),
};
return new GitAwareRepositoryContextPipeline(dependencies, {
@@ -473,13 +479,13 @@ function createHostRetriever(options: {
let retrievalClose: (() => Promise) | undefined;
try {
const runtime =
- vectorRuntime.status === 'ready'
+ vectorRuntime.status === 'unavailable'
? createWorkspaceRetrievalRuntime({
textIndexDatabase: database as never,
- vector: vectorRuntime.vector,
})
: createWorkspaceRetrievalRuntime({
textIndexDatabase: database as never,
+ vector: vectorRuntime.vector,
});
retrievalClose = () => runtime.close();
const retriever = new HybridRetrievalFactory().create({
@@ -501,8 +507,10 @@ function createHostRetriever(options: {
? []
: [
{
- code: 'required_source_unavailable' as const,
- message: vectorRuntime.reason,
+ code: 'optional_source_unavailable' as const,
+ message:
+ vectorRuntime.reason ??
+ 'Vector retrieval is unavailable; remaining sources continued.',
},
]),
],
@@ -528,7 +536,8 @@ async function resolveVectorRetrievalRuntime(options: {
semanticIndex?: SemanticIndexSettings;
}): Promise<
| {
- status: 'ready';
+ status: 'ready' | 'degraded';
+ reason?: string;
vector: {
embeddingProvider: ReturnType;
lanceConnection: Awaited>;
@@ -542,7 +551,8 @@ async function resolveVectorRetrievalRuntime(options: {
if (!options.semanticIndex?.enabled) {
return {
status: 'unavailable',
- reason: 'Vector retrieval is unavailable: semantic index is disabled or not configured.',
+ reason:
+ 'Vector retrieval is unavailable (semantic_index_disabled): semantic index is disabled or not configured. Reindex after enabling embeddings.',
};
}
const metadata = readIndexRuntimeMetadata(
@@ -551,26 +561,26 @@ async function resolveVectorRetrievalRuntime(options: {
if (!metadata) {
return {
status: 'unavailable',
- reason: 'Vector retrieval is unavailable: index-runtime.json is missing or invalid.',
+ reason:
+ 'Vector retrieval is unavailable (runtime_missing): index-runtime.json is missing or invalid. Reindex the workspace.',
};
}
if (!metadata.embeddingProfile?.id) {
return {
status: 'unavailable',
reason:
- 'Vector retrieval is unavailable: index-runtime.json does not describe a ready embedding profile.',
+ 'Vector retrieval is unavailable (embedding_profile_missing): index-runtime.json does not describe a ready embedding profile. Reindex the workspace.',
};
}
- if (
- !descriptorHasReadyVectorProfile(
- options.descriptor,
- metadata.embeddingProfile.id,
- )
- ) {
+ const vectorCapability = vectorCapabilityForProfile(
+ options.descriptor,
+ metadata.embeddingProfile.id,
+ );
+ if (!vectorCapability) {
return {
status: 'unavailable',
reason:
- 'Vector retrieval is unavailable: published repository state does not expose the persisted vector profile.',
+ 'Vector retrieval is unavailable (published_profile_missing): published repository state does not expose the persisted vector profile. Reindex the workspace.',
};
}
const alignedSettings = alignSemanticSettingsWithPersistedProfile(
@@ -581,45 +591,56 @@ async function resolveVectorRetrievalRuntime(options: {
return {
status: 'unavailable',
reason:
- 'Vector retrieval is unavailable: current embedding profile differs from the profile that wrote LanceDB.',
+ 'Vector retrieval is unavailable (profile_mismatch): current embedding profile differs from the profile that wrote LanceDB. Reindex to rebuild embeddings.',
};
}
const provider = createHostEmbeddingProvider(alignedSettings);
try {
+ const vector = {
+ embeddingProvider: provider,
+ lanceConnection: await createLanceDbConnection(metadata.lanceDbPath),
+ };
+ if (vectorCapability === 'degraded') {
+ return {
+ status: 'degraded',
+ reason:
+ 'Vector retrieval is degraded (partial_index): the published embedding index is incomplete. Reindex to restore full semantic search.',
+ vector,
+ };
+ }
return {
status: 'ready',
- vector: {
- embeddingProvider: provider,
- lanceConnection: await createLanceDbConnection(metadata.lanceDbPath),
- },
+ vector,
};
} catch (error) {
return {
status: 'unavailable',
- reason: `Vector retrieval is unavailable: ${
+ reason: `Vector retrieval is unavailable (connection_failed): ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
-function descriptorHasReadyVectorProfile(
+function vectorCapabilityForProfile(
descriptor: RepositoryStateDescriptor,
profileId: string,
-): boolean {
- return descriptor.roots.some(
- (root: RepositoryRootState) =>
- root.vectorProfile === profileId &&
- root.capabilities.some(
- (capability: RepositoryCapabilityStatus) =>
- capability.capability === 'vectorIndex' &&
- capability.status === 'ready',
- ),
- );
+): 'ready' | 'degraded' | undefined {
+ for (const root of descriptor.roots) {
+ if (root.vectorProfile !== profileId) continue;
+ const capability = root.capabilities.find(
+ (entry: RepositoryCapabilityStatus) => entry.capability === 'vectorIndex',
+ );
+ if (capability?.status === 'ready' || capability?.status === 'degraded') {
+ return capability.status;
+ }
+ }
+ return undefined;
}
function createHostAssembler(
defaultAssembler: RepositoryContextAssemblerPort,
+ getRepoMap: (snapshotId: string) => RepoMap | undefined,
): RepositoryContextAssemblerPort {
return {
assemble: async (
@@ -628,7 +649,10 @@ function createHostAssembler(
if (input.selection.items.length > 0) {
return defaultAssembler.assemble(input);
}
- return assembleFileMapFallback(input);
+ return assembleFileMapFallback(
+ input,
+ getRepoMap(input.snapshot.snapshotId),
+ );
},
};
}
@@ -678,8 +702,9 @@ function emptyHostRetrieval(
function assembleFileMapFallback(
input: ContextAssemblyInput,
+ repoMap?: RepoMap,
): ContextAssemblyResult {
- const paths = input.snapshot.entries
+ const snapshotPaths = input.snapshot.entries
.filter(
(entry: unknown): entry is WorkspaceFileEntry =>
typeof entry === 'object' &&
@@ -687,21 +712,39 @@ function assembleFileMapFallback(
'kind' in entry &&
entry.kind === 'file',
)
- .map((entry: WorkspaceFileEntry) => entry.relativePath)
- .slice(0, MAX_REPO_MAP_FILES);
- let content = `Workspace file map (${paths.length} files):\n${paths
- .map((path: string) => `- ${path}`)
- .join('\n')}`;
+ .map((entry: WorkspaceFileEntry) => entry.relativePath);
+ const rankedPaths = [...(repoMap?.entries ?? [])]
+ .sort(
+ (left, right) =>
+ right.score - left.score ||
+ left.file.relativePath.localeCompare(right.file.relativePath),
+ )
+ .map((entry) => entry.file.relativePath);
+ const paths: string[] = [];
+ const seen = new Set