Skip to content

Commit 49ee3f0

Browse files
authored
fix(migration): align volumetrics AI flow with access-patterns and hide curated templates from file lists (#3052)
* fix(migration): allow volumetrics AI analysis without source files * fix(migration): hide curated template files from UI file lists
1 parent 747286f commit 49ee3f0

5 files changed

Lines changed: 78 additions & 32 deletions

File tree

l10n/bundle.l10n.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,8 +612,8 @@
612612
"No schema found for {0}. Use the \"Generate schema\" option to create one.": "No schema found for {0}. Use the \"Generate schema\" option to create one.",
613613
"No scope was provided for the role assignment.": "No scope was provided for the role assignment.",
614614
"No session found for executionId: {executionId}": "No session found for executionId: {executionId}",
615-
"No source files found to analyze. Select files first, then try again.": "No source files found to analyze. Select files first, then try again.",
616615
"No subscriptions found": "No subscriptions found",
616+
"No volumetric source files selected. AI will estimate from schema and workspace configuration.": "No volumetric source files selected. AI will estimate from schema and workspace configuration.",
617617
"NoSQL quick reference": "NoSQL quick reference",
618618
"Not sure where to start? Open the access-patterns template and fill it in by hand, or use the AI button (sparkle icon) to derive patterns from your workspace code — repositories, query builders, and raw SQL.": "Not sure where to start? Open the access-patterns template and fill it in by hand, or use the AI button (sparkle icon) to derive patterns from your workspace code — repositories, query builders, and raw SQL.",
619619
"Number of output documents": "Number of output documents",

src/panels/MigrationAssistantTab.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,10 +400,18 @@ export class MigrationAssistantTab extends BaseTab {
400400
setMigrationTelemetryContext(context, this.project);
401401
context.telemetry.properties.isNewProject = String(isNewProject);
402402

403-
// Gather file lists
403+
// Gather file lists. Hide the curated template files from the UI —
404+
// they are managed via the dedicated "Open Template" buttons and AI flows,
405+
// not the user-selectable source list.
406+
const volTemplateAbs = this.projectService.getTemplateFilePath('volumetrics');
407+
const apTemplateAbs = this.projectService.getTemplateFilePath('access-patterns');
404408
const schemaFiles = await this.projectService.listDiscoveryFiles(this.project, 'schema-ddl');
405-
const volumetricFiles = await this.projectService.listDiscoveryFiles(this.project, 'volumetrics');
406-
const accessPatternFiles = await this.projectService.listDiscoveryFiles(this.project, 'access-patterns');
409+
const volumetricFiles = (await this.projectService.listDiscoveryFiles(this.project, 'volumetrics')).filter(
410+
(f) => f !== volTemplateAbs,
411+
);
412+
const accessPatternFiles = (
413+
await this.projectService.listDiscoveryFiles(this.project, 'access-patterns')
414+
).filter((f) => f !== apTemplateAbs);
407415
const excludedSchemaFiles = await this.projectService.listExcludedDiscoveryFiles(
408416
this.project,
409417
'schema-ddl',
@@ -671,9 +679,16 @@ export class MigrationAssistantTab extends BaseTab {
671679
private async refreshFileState(): Promise<void> {
672680
if (!this.project) return;
673681

682+
// Hide the curated template files from the UI (see loadProject for rationale).
683+
const volTemplateAbs = this.projectService.getTemplateFilePath('volumetrics');
684+
const apTemplateAbs = this.projectService.getTemplateFilePath('access-patterns');
674685
const schemaFiles = await this.projectService.listDiscoveryFiles(this.project, 'schema-ddl');
675-
const volumetricFiles = await this.projectService.listDiscoveryFiles(this.project, 'volumetrics');
676-
const accessPatternFiles = await this.projectService.listDiscoveryFiles(this.project, 'access-patterns');
686+
const volumetricFiles = (await this.projectService.listDiscoveryFiles(this.project, 'volumetrics')).filter(
687+
(f) => f !== volTemplateAbs,
688+
);
689+
const accessPatternFiles = (
690+
await this.projectService.listDiscoveryFiles(this.project, 'access-patterns')
691+
).filter((f) => f !== apTemplateAbs);
677692
const excludedSchemaFiles = await this.projectService.listExcludedDiscoveryFiles(this.project, 'schema-ddl');
678693
const excludedVolumetricFiles = await this.projectService.listExcludedDiscoveryFiles(
679694
this.project,

src/panels/migration/prompts/Phase1Step1AnalysisPrompts.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,39 @@ import * as path from 'path';
99
* Builds a Copilot Chat prompt that instructs the AI to read the selected
1010
* volumetric data files and fill in the volumetrics.md template.
1111
*
12-
* @param sourceRefs - A folder path (string) or individual file paths (string[]), workspace-relative.
12+
* @param sourceRefs - A folder path (string), individual file paths (string[]), or undefined if no source files.
13+
* When undefined, the AI is instructed to estimate values from the schema and workspace configuration.
1314
* @param templateRelativePath - Workspace-relative path to the template file.
1415
* @param schemaFileRefs - A folder path (string) or individual file paths (string[]), workspace-relative.
1516
* @param discoveryInstructions - Optional custom discovery instructions for the AI.
1617
*/
1718
export function buildAnalyzeVolumetricsPrompt(
18-
sourceRefs: string | string[],
19+
sourceRefs: string | string[] | undefined,
1920
templateRelativePath: string,
2021
schemaFileRefs: string | string[],
2122
discoveryInstructions?: string,
2223
): string {
23-
const isFolder = typeof sourceRefs === 'string';
24-
const sourceRefStr = isFolder ? `#file:${sourceRefs}` : sourceRefs.map((f) => `#file:${f}`).join('\n');
25-
2624
const isSchemaFolder = typeof schemaFileRefs === 'string';
2725
const schemaRefStr = isSchemaFolder
2826
? `#file:${schemaFileRefs}`
2927
: schemaFileRefs.map((f) => `#file:${f}`).join('\n');
3028

3129
const lines: string[] = [
32-
`Analyze the following volumetric data files and use the results to fill in the template at #file:${templateRelativePath}.`,
33-
'',
34-
'Source files to analyze:',
35-
sourceRefStr,
30+
sourceRefs !== undefined
31+
? `Analyze the following volumetric data files and use the results to fill in the template at #file:${templateRelativePath}.`
32+
: `Estimate volumetric data from the schema and workspace configuration, and fill in the template at #file:${templateRelativePath}.`,
3633
];
3734

38-
if (isFolder && path.dirname(templateRelativePath) === sourceRefs) {
39-
lines.push(
40-
`- The template file (${path.basename(templateRelativePath)}) is inside the source folder — do NOT treat it as source data.`,
41-
);
35+
if (sourceRefs !== undefined) {
36+
const isFolder = typeof sourceRefs === 'string';
37+
const sourceRefStr = isFolder ? `#file:${sourceRefs}` : sourceRefs.map((f) => `#file:${f}`).join('\n');
38+
lines.push('', 'Source files to analyze:', sourceRefStr);
39+
40+
if (isFolder && path.dirname(templateRelativePath) === sourceRefs) {
41+
lines.push(
42+
`- The template file (${path.basename(templateRelativePath)}) is inside the source folder — do NOT treat it as source data.`,
43+
);
44+
}
4245
}
4346

4447
lines.push(
@@ -51,7 +54,9 @@ export function buildAnalyzeVolumetricsPrompt(
5154
lines.push(
5255
'',
5356
'Instructions:',
54-
'- Read each source file and extract volumetric data (table names, row counts, row sizes, read/write TPS, growth rates).',
57+
sourceRefs !== undefined
58+
? '- Read each source file and extract volumetric data (table names, row counts, row sizes, read/write TPS, growth rates).'
59+
: '- No volumetric source files were provided. Infer table names, row counts, row sizes, read/write TPS, and growth rates from the schema files, deployment scripts, configuration files, and application code. Mark every inferred value with "(estimated)".',
5560
'- Replace the example rows in the volumetrics.md template table with real data extracted from the source files.',
5661
'- Keep the existing table headers and markdown structure intact.',
5762
'- If a field cannot be determined from the source data, mark it with "N/A" or a reasonable estimate annotated with "(estimated)".',

src/panels/migration/steps/phase1Discovery.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -702,18 +702,19 @@ export async function runAnalyzeWithAI(
702702
? projectService.getVolumetricsPath(project)
703703
: projectService.getAccessPatternsPath(project);
704704

705-
// Collect source files, excluding the template if it lives in the same directory.
706-
// Honors per-source includedFiles/excludedFiles configuration in project.json.
705+
// Collect source files, excluding the curated template (which lives in the
706+
// source folder when the user hasn't picked a custom path). The template is
707+
// treated as the output target, not as input data.
707708
const allFiles = await projectService.listDiscoveryFiles(project, subfolder);
708-
const nonTemplateFiles =
709-
sourceFolderPath === templateFolderPath ? allFiles.filter((f) => !f.endsWith(`/${fileName}`)) : allFiles;
710-
711-
// Volumetrics requires source data files; access patterns can scan the workspace without them
712-
if (subfolder === 'volumetrics' && nonTemplateFiles.length === 0) {
713-
void vscode.window.showInformationMessage(
714-
l10n.t('No source files found to analyze. Select files first, then try again.'),
709+
const sourceFiles = allFiles.filter((f) => f !== templateAbsPath);
710+
711+
// Both volumetrics and access patterns can proceed without explicit source files —
712+
// the AI will fall back to schema + workspace inference. Surface a non-blocking warning
713+
// for volumetrics so the user knows the analysis will be estimated.
714+
if (subfolder === 'volumetrics' && sourceFiles.length === 0) {
715+
void vscode.window.showWarningMessage(
716+
l10n.t('No volumetric source files selected. AI will estimate from schema and workspace configuration.'),
715717
);
716-
return;
717718
}
718719

719720
const templateRelativePath = path.relative(workspacePath, templateAbsPath);
@@ -727,13 +728,13 @@ export async function runAnalyzeWithAI(
727728
const prompt =
728729
subfolder === 'volumetrics'
729730
? buildAnalyzeVolumetricsPrompt(
730-
sourceFolderRelativePath,
731+
sourceFiles.length > 0 ? sourceFolderRelativePath : undefined,
731732
templateRelativePath,
732733
schemaFileRefs,
733734
discoveryInstructions,
734735
)
735736
: buildAnalyzeAccessPatternsPrompt(
736-
nonTemplateFiles.length > 0 ? sourceFolderRelativePath : undefined,
737+
sourceFiles.length > 0 ? sourceFolderRelativePath : undefined,
737738
templateRelativePath,
738739
schemaFileRefs,
739740
getVolumetricsTemplatePath(projectService, workspacePath),

src/services/MigrationProjectService.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,12 @@ export class MigrationProjectService {
387387
* List discovery source files for a subfolder, applying the per-source
388388
* `includedFiles` allowlist and `excludedFiles` filter from project.json
389389
* (paths relative to the resolved base).
390+
*
391+
* NOTE: The curated template files (`volumetrics.md`, `access-patterns.md`)
392+
* ARE included in the result when they live in the source folder, because
393+
* downstream discovery/assessment steps extract them by name to read
394+
* pre-filled values. UI callers that need to hide them should filter the
395+
* template file out themselves (see {@link getTemplateFilePath}).
390396
*/
391397
async listDiscoveryFiles(
392398
project: ProjectJson,
@@ -407,6 +413,25 @@ export class MigrationProjectService {
407413
return filtered;
408414
}
409415

416+
/**
417+
* Return the absolute path of the curated template file for the given
418+
* discovery subfolder (`volumetrics.md` / `access-patterns.md`), or
419+
* `undefined` for subfolders that have no template. The template always
420+
* lives in the default subfolder path; callers can use this to suppress
421+
* the template from UI file lists without affecting AI flows that need to
422+
* read it.
423+
*/
424+
getTemplateFilePath(subfolder: 'schema-ddl' | 'volumetrics' | 'access-patterns'): string | undefined {
425+
const templateFileName =
426+
subfolder === 'volumetrics'
427+
? 'volumetrics.md'
428+
: subfolder === 'access-patterns'
429+
? 'access-patterns.md'
430+
: undefined;
431+
if (templateFileName === undefined) return undefined;
432+
return path.join(this.getDefaultSubfolderPath(subfolder), templateFileName);
433+
}
434+
410435
/**
411436
* List the absolute paths of the discovery files that are currently excluded
412437
* (paths preserved relative to the resolved base; only those still present on

0 commit comments

Comments
 (0)