Skip to content

Commit 427a2d0

Browse files
committed
feat: enhance scan generation by adding defaulted response metadata and updating template rendering
1 parent 2a904a4 commit 427a2d0

File tree

10 files changed

+122
-114
lines changed

10 files changed

+122
-114
lines changed

app/api/scan-generate/[fileId]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ export async function POST(request: NextRequest, context: RouteContext) {
1818
return NextResponse.json({ error: "Missing scan payload" }, { status: 400 })
1919
}
2020

21-
const { stack, responses } = await buildResponsesFromScan(payload.scan)
21+
const { stack, responses, defaultedResponseMeta } = await buildResponsesFromScan(payload.scan)
2222
responses.outputFile = fileId
2323

2424
const rendered = await renderTemplate({
2525
responses,
2626
frameworkFromPath: stack,
2727
fileNameFromPath: fileId,
28+
defaultedResponses: defaultedResponseMeta,
2829
})
2930

3031
return NextResponse.json({
@@ -37,4 +38,3 @@ export async function POST(request: NextRequest, context: RouteContext) {
3738
return NextResponse.json({ error: "Failed to generate instructions from scan" }, { status: 500 })
3839
}
3940
}
40-

docs/scan-flow.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ This document outlines how repository scans are transformed into AI instruction
2828
- From the repo-scan UI, clicking “Generate” calls `lib/scan-generate.ts`, which posts to `/api/scan-generate/[fileId]`.
2929
- The API reuses `buildResponsesFromScan` server-side to ensure consistency, then renders the target template with `renderTemplate`.
3030
- Template rendering pulls `applyToGlob` from conventions so Copilot instructions target stack-appropriate file globs (e.g. `**/*.{py,pyi,md}` for Python).
31+
- When a field falls back to a stack default because the scan lacked a signal, `renderTemplate` annotates the generated instruction with a note that it came from the default rather than the scan.
3132

3233
## Key Data Sources
3334

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from "vitest"
2+
3+
import { renderTemplate } from "@/lib/template-render"
4+
import type { WizardResponses } from "@/types/wizard"
5+
6+
const buildResponses = (): WizardResponses => ({
7+
stackSelection: "react",
8+
tooling: "vite",
9+
language: "TypeScript",
10+
fileStructure: "feature-based",
11+
styling: "tailwind",
12+
testingUT: "jest",
13+
testingE2E: "cypress",
14+
projectPriority: "developer velocity",
15+
codeStyle: "eslint-config-next",
16+
variableNaming: "camelCase",
17+
fileNaming: "kebab-case",
18+
componentNaming: "PascalCase",
19+
exports: "named",
20+
comments: "jsdoc",
21+
collaboration: "github",
22+
stateManagement: "redux",
23+
apiLayer: "trpc",
24+
folders: "by-feature",
25+
dataFetching: "swr",
26+
reactPerf: "memoization",
27+
auth: "oauth",
28+
validation: "zod",
29+
logging: "pino",
30+
commitStyle: "conventional",
31+
prRules: "reviewRequired",
32+
outputFile: "instructions-md",
33+
})
34+
35+
describe("renderTemplate", () => {
36+
it("annotates defaulted responses passed from the scan pipeline", async () => {
37+
const responses = buildResponses()
38+
39+
const result = await renderTemplate({
40+
responses,
41+
frameworkFromPath: "react",
42+
fileNameFromPath: "instructions-md",
43+
defaultedResponses: {
44+
tooling: {
45+
label: "Vite",
46+
value: "vite",
47+
questionId: "react-tooling",
48+
},
49+
},
50+
})
51+
52+
expect(result.content).toContain("Vite (stack default - not detected via repo scan)")
53+
})
54+
})

lib/scan-to-wizard.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ type StackQuestionDefault = {
154154
questionId: string
155155
responseKey: keyof WizardResponses
156156
value: string
157+
label: string
157158
}
158159

159160
const defaultsCache = new Map<string, StackQuestionDefault[]>()
@@ -178,6 +179,7 @@ const extractDefaultsFromSteps = (steps: WizardStep[], template: WizardResponses
178179
questionId: question.id,
179180
responseKey: key,
180181
value: defaultAnswer.value,
182+
label: defaultAnswer.label ?? defaultAnswer.value,
181183
})
182184
})
183185
})
@@ -205,6 +207,7 @@ type BuildResult = {
205207
conventions: LoadedConvention
206208
hasCustomConventions: boolean
207209
defaultedQuestionIds: Record<string, boolean>
210+
defaultedResponseMeta: Partial<Record<keyof WizardResponses, { questionId: string; label: string; value: string }>>
208211
}
209212

210213
export const buildResponsesFromScan = async (scan: RepoScanSummary): Promise<BuildResult> => {
@@ -227,12 +230,21 @@ export const buildResponsesFromScan = async (scan: RepoScanSummary): Promise<Bui
227230
afterRules.stackSelection = stack
228231

229232
const defaultedQuestionIds: Record<string, boolean> = {}
233+
const defaultedResponseMeta: Partial<Record<
234+
keyof WizardResponses,
235+
{ questionId: string; label: string; value: string }
236+
>> = {}
230237
const questionDefaults = await loadStackQuestionDefaults(stack, afterRules)
231-
questionDefaults.forEach(({ responseKey, questionId, value }) => {
238+
questionDefaults.forEach(({ responseKey, questionId, value, label }) => {
232239
const currentValue = afterRules[responseKey]
233240
if (currentValue === null || currentValue === undefined || currentValue === "") {
234241
afterRules[responseKey] = value
235242
defaultedQuestionIds[questionId] = true
243+
defaultedResponseMeta[responseKey] = {
244+
questionId,
245+
label,
246+
value,
247+
}
236248
}
237249
})
238250

@@ -261,6 +273,7 @@ export const buildResponsesFromScan = async (scan: RepoScanSummary): Promise<Bui
261273
conventions,
262274
hasCustomConventions: hasStackFile,
263275
defaultedQuestionIds,
276+
defaultedResponseMeta,
264277
}
265278
}
266279

lib/template-render.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,17 @@ function mapOutputFileToTemplateType(outputFile: string): string {
3030
return mapping[outputFile] ?? outputFile
3131
}
3232

33+
type DefaultedResponseMeta = {
34+
label: string
35+
value: string
36+
questionId: string
37+
}
38+
3339
export type RenderTemplateParams = {
3440
responses: WizardResponses
3541
frameworkFromPath?: string | undefined
3642
fileNameFromPath?: string | undefined
43+
defaultedResponses?: Partial<Record<keyof WizardResponses, DefaultedResponseMeta>>
3744
}
3845

3946
export type RenderTemplateResult = {
@@ -46,6 +53,7 @@ export async function renderTemplate({
4653
responses,
4754
frameworkFromPath,
4855
fileNameFromPath,
56+
defaultedResponses,
4957
}: RenderTemplateParams): Promise<RenderTemplateResult> {
5058
const framework = frameworkFromPath && !['general', 'none', 'undefined'].includes(frameworkFromPath)
5159
? frameworkFromPath
@@ -97,13 +105,19 @@ export async function renderTemplate({
97105

98106
const value = responses[key]
99107

108+
const defaultMeta = defaultedResponses?.[key]
109+
100110
if (value === null || value === undefined || value === '') {
101111
const replacement = isJsonTemplate ? escapeForJson(fallback) : fallback
102112
generatedContent = generatedContent.replace(placeholder, replacement)
103113
} else {
104-
const replacementValue = String(value)
105-
const replacement = isJsonTemplate ? escapeForJson(replacementValue) : replacementValue
106-
generatedContent = generatedContent.replace(placeholder, replacement)
114+
const rawValue = String(value)
115+
const baseValue = defaultMeta?.label ?? rawValue
116+
const displayValue = defaultMeta
117+
? `${baseValue} (stack default - not detected via repo scan)`
118+
: baseValue
119+
const replacementValue = isJsonTemplate ? escapeForJson(displayValue) : displayValue
120+
generatedContent = generatedContent.replace(placeholder, replacementValue)
107121
}
108122
}
109123

playwright/tests/repo-scan.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ test('repo scan success path generates instructions preview', async ({ page }) =
3838
await expect(page.getByText('TypeScript').first()).toBeVisible()
3939
await expect(page.getByText('Playwright').first()).toBeVisible()
4040

41-
await page.route('**/api/generate/**', async (route) => {
41+
await page.route('**/api/scan-generate/**', async (route) => {
4242
await route.fulfill({
4343
status: 200,
4444
contentType: 'application/json',

playwright/tests/wizard-free-text.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ test("wizard accepts custom free text answers and shows them in the summary", as
2828
const customInput = page.getByPlaceholder("Type your custom preference")
2929

3030
await expect(customInput).toBeVisible()
31-
await customInput.fill(customAnswer)
31+
await customInput.click()
32+
await customInput.fill("")
33+
await customInput.type(customAnswer)
34+
await expect(customInput).toHaveValue(customAnswer)
3235

33-
await expect(questionHeading).toHaveText("What build tooling do you use?")
36+
const saveButton = page.getByRole("button", { name: "Save custom answer" })
37+
await expect(saveButton).toBeEnabled()
38+
await saveButton.click()
3439

35-
const confirmationMessage = page.getByTestId("custom-answer-confirmation")
36-
await expect(confirmationMessage).toBeVisible()
37-
await expect(confirmationMessage).toContainText(customAnswer)
38-
await expect(confirmationMessage).toContainText(
39-
"for this question when we generate your context file."
40-
)
40+
await expect(questionHeading).toHaveText("What language do you use?")
4141

4242
await expect.poll(
4343
() =>

playwright/tests/wizard.spec.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,29 @@ test('wizard supports filtering, defaults, and reset', async ({ page }) => {
1919

2020
const questionHeading = page.getByTestId('wizard-question-heading')
2121

22-
await page.getByRole('button', { name: 'Use default (Vite)' }).click()
23-
await expect(page.getByTestId('answer-option-react-language-typescript')).toBeVisible()
22+
const defaultButton = page.getByRole('button', { name: 'Use default (Vite)' })
23+
await expect(defaultButton).toBeEnabled()
24+
await defaultButton.click()
25+
await expect(defaultButton).toBeDisabled()
26+
27+
await expect.poll(
28+
() =>
29+
page.evaluate(() => {
30+
const raw = window.localStorage.getItem('devcontext:wizard:react')
31+
if (!raw) {
32+
return null
33+
}
34+
35+
try {
36+
const state = JSON.parse(raw)
37+
return state.responses?.['react-tooling'] ?? null
38+
} catch (error) {
39+
console.warn('Unable to parse wizard state', error)
40+
return 'PARSE_ERROR'
41+
}
42+
}),
43+
{ timeout: 15000 }
44+
).toBe('vite')
2445

2546
await page.getByRole('button', { name: 'Start Over' }).click()
2647
await expect(page.getByTestId('wizard-confirmation-dialog')).toBeVisible()

test-results/.last-run.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
{
2-
"status": "failed",
3-
"failedTests": [
4-
"6b578dd8037bfa8e4836-6a36192f84de2a39d93f"
5-
]
2+
"status": "passed",
3+
"failedTests": []
64
}

test-results/repo-scan-repo-scan-succes-fbe6f-erates-instructions-preview-chromium/error-context.md

Lines changed: 0 additions & 93 deletions
This file was deleted.

0 commit comments

Comments
 (0)