Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions app/api/scan-repo/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import type {
RepoScanSummary,
RepoStructureSummary,
} from "@/types/repo-scan"
import { collectConventionValues, normalizeConventionValue } from "@/lib/convention-values"
import { loadStackQuestionMetadata, normalizeConventionValue } from "@/lib/question-metadata"
import { loadStackConventions } from "@/lib/conventions"
import { inferStackFromScan } from "@/lib/scan-to-wizard"
import { stackQuestion } from "@/lib/wizard-config"

const GITHUB_API_BASE_URL = "https://api.github.com"
const GITHUB_HOSTNAMES = new Set(["github.com", "www.github.com"])
Expand Down Expand Up @@ -302,10 +303,10 @@ const getTestingConventionValues = async (stackId: string): Promise<TestingConve
return testingConventionCache.get(normalized)!
}

const { conventions } = await loadStackConventions(normalized)
const metadata = await loadStackQuestionMetadata(normalized)
const values: TestingConventionValues = {
unit: collectConventionValues(conventions, "testingUT"),
e2e: collectConventionValues(conventions, "testingE2E"),
unit: metadata.answersByResponseKey.testingUT ?? [],
e2e: metadata.answersByResponseKey.testingE2E ?? [],
}
testingConventionCache.set(normalized, values)
return values
Expand Down Expand Up @@ -873,8 +874,16 @@ export async function GET(request: NextRequest): Promise<NextResponse<RepoScanRe

const detectedStack = inferStackFromScan(summary)
const { conventions, hasStackFile } = await loadStackConventions(detectedStack)
const stackAnswers = stackQuestion?.answers ?? []
const matchedStackAnswer = stackAnswers.find((answer) => answer.value === detectedStack) ?? null
const stackSupported = matchedStackAnswer
? matchedStackAnswer.disabled !== true && matchedStackAnswer.enabled !== false
: false
const stackLabel = matchedStackAnswer?.label ?? null
summary.conventions = {
stack: detectedStack,
stackLabel,
isSupported: stackSupported,
hasCustomConventions: hasStackFile,
structureRelevant: conventions.structureRelevant,
}
Expand Down
250 changes: 151 additions & 99 deletions app/existing/[repoUrl]/repo-scan-client.tsx

Large diffs are not rendered by default.

10 changes: 0 additions & 10 deletions conventions/angular.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
{
"id": "angular",
"structureRelevant": ["src", "components", "tests", "apps"],
"defaults": {
"tooling": "angular-cli",
"language": "typescript-strict",
"fileStructure": "standalone-first",
"styling": "scss",
"stateManagement": "ngrx-store",
"apiLayer": "httpclient",
"dataFetching": "httpclient",
"reactPerf": "memoHooks"
},
"rules": [
{
"if": { "toolingIncludes": ["nx"] },
Expand Down
7 changes: 0 additions & 7 deletions conventions/astro.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
{
"id": "astro",
"defaults": {
"tooling": "react",
"dataFetching": "ssg",
"folders": "content-collections",
"collaboration": "vercel",
"reactPerf": "memoHooks"
},
"rules": [
{
"if": { "toolingIncludes": ["vue"] },
Expand Down
25 changes: 0 additions & 25 deletions conventions/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,5 @@
"id": "default",
"applyToGlob": "**/*.{ts,tsx,js,jsx,md}",
"structureRelevant": ["src", "components", "tests", "apps", "packages"],
"defaults": {
"projectPriority": "maintainability",
"codeStyle": "airbnb",
"variableNaming": "camelCase",
"fileNaming": "kebab-case",
"componentNaming": "PascalCase",
"exports": "named",
"comments": "docblocks",
"collaboration": "async",
"fileStructure": "flat",
"styling": "cssmodules",
"stateManagement": "context-hooks",
"apiLayer": "http-client",
"folders": "by-feature",
"dataFetching": "swr",
"reactPerf": "memoHooks",
"auth": "env",
"validation": "zod",
"logging": "structured",
"commitStyle": "conventional",
"prRules": "reviewRequired",
"testingUT": null,
"testingE2E": null,
"tooling": "custom-config"
},
"rules": []
}
10 changes: 0 additions & 10 deletions conventions/nextjs.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
{
"id": "nextjs",
"structureRelevant": ["src", "tests", "apps"],
"defaults": {
"tooling": "create-next-app",
"fileStructure": "app-directory",
"styling": "tailwind",
"stateManagement": "zustand",
"apiLayer": "server-actions",
"dataFetching": "server-components",
"reactPerf": "memoHooks",
"auth": "next-auth"
},
"rules": [
{
"if": { "routingIs": ["pages"] },
Expand Down
9 changes: 0 additions & 9 deletions conventions/nuxt.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
{
"id": "nuxt",
"defaults": {
"tooling": "nuxi",
"dataFetching": "server-side",
"apiLayer": "use-fetch",
"styling": "tailwind",
"folders": "vercel",
"stateManagement": "pinia",
"reactPerf": "memoHooks"
},
"rules": [
{
"if": { "toolingIncludes": ["tailwind"] },
Expand Down
22 changes: 0 additions & 22 deletions conventions/python.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,6 @@
"id": "python",
"applyToGlob": "**/*.{py,pyi,md}",
"structureRelevant": ["src", "tests", "packages"],
"defaults": {
"tooling": "pip",
"language": "Python",
"variableNaming": "snake_case",
"fileNaming": "snake_case",
"componentNaming": "Not applicable",
"exports": "module exports",
"comments": "docstrings",
"fileStructure": "top-level modules",
"styling": "Not applicable",
"stateManagement": "Not applicable",
"apiLayer": "TODO: document primary framework (FastAPI, Django, Flask, etc.)",
"folders": "by-module",
"dataFetching": "Not applicable",
"reactPerf": "Not applicable",
"testingUT": "pytest",
"testingE2E": "TODO: outline integration / end-to-end coverage",
"auth": "TODO: document auth/session strategy",
"validation": "TODO: specify validation library (Pydantic, Marshmallow, etc.)",
"logging": "TODO: describe logging approach (structlog, stdlib logging)",
"codeStyle": "pep8"
},
"rules": [
{
"if": { "toolingIncludes": ["poetry"] },
Expand Down
7 changes: 0 additions & 7 deletions conventions/react.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
{
"id": "react",
"defaults": {
"tooling": "vite",
"styling": "cssmodules",
"stateManagement": "context-hooks",
"dataFetching": "swr",
"reactPerf": "memoHooks"
},
"rules": [
{
"if": { "toolingIncludes": ["tailwind css", "tailwind"] },
Expand Down
7 changes: 0 additions & 7 deletions conventions/remix.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
{
"id": "remix",
"defaults": {
"folders": "vercel",
"dataFetching": "route-loaders",
"styling": "tailwind",
"testingUT": "vitest",
"reactPerf": "memoHooks"
},
"rules": [
{
"if": { "toolingIncludes": ["jest"] },
Expand Down
8 changes: 0 additions & 8 deletions conventions/svelte.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
{
"id": "svelte",
"defaults": {
"tooling": "sveltekit",
"language": "typescript",
"stateManagement": "svelte-stores",
"styling": "scoped-css",
"testingUT": "vitest",
"reactPerf": "memoHooks"
},
"rules": [
{
"if": { "toolingIncludes": ["vite"] },
Expand Down
8 changes: 0 additions & 8 deletions conventions/vue.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
{
"id": "vue",
"defaults": {
"tooling": "vite",
"language": "typescript",
"fileStructure": "feature-folders",
"stateManagement": "pinia",
"dataFetching": "swr",
"reactPerf": "memoHooks"
},
"rules": [
{
"if": { "toolingIncludes": ["create-vue"] },
Expand Down
14 changes: 7 additions & 7 deletions docs/scan-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@ This document outlines how repository scans are transformed into AI instruction

1. **Scan the repository** (`app/api/scan-repo/route.ts`)
- Detect languages, frameworks, tooling, structure hints, and enriched metadata.
- Use stack conventions (`collectConventionValues`) to cross-check detection lists (e.g., testing tools) so any signal we add in `conventions/<stack>.json` becomes discoverable with minimal code changes.
- Reuse convention values to expand stack-specific heuristics (e.g., Python’s Behave directories, Next.js routing shapes), so each conventions file remains the source of truth for new detections.
- Load stack question metadata (`loadStackQuestionMetadata`) so detection lists (e.g., testing tools) map onto the same canonical values the wizard exposes.
- Reuse those canonical values to expand stack-specific heuristics (e.g., Python’s Behave directories, Next.js routing shapes) without hard-coding strings inside the scanner.
- Infer the primary stack using `inferStackFromScan`.
- Load stack conventions (`loadStackConventions`) to determine which structure hints matter and whether stack-specific rules exist.
- Attach `summary.conventions` so the UI knows which directories to highlight and whether a conventions file was found.

2. **Build wizard defaults** (`lib/scan-to-wizard.ts`)
- Start with an empty `WizardResponses` object.
- Apply convention defaults from `conventions/<stack>.json` + `default.json`.
- Layer in detections from the scan (tooling, testing, naming signals, etc.), matching scan values against convention-provided options so stack JSON remains the single source of truth.
- Layer in detections from the scan (tooling, testing, naming signals, etc.), matching scan values against the question catalog so defaults stay in sync with the wizard.
- Run convention rules to tweak values based on detected tooling/testing.
- Pull default answers directly from the stack’s question set (`buildStepsForStack`) and fill any remaining empty responses. We track which questions were auto-defaulted (`defaultedQuestionIds`) so the summary can explain why.
- Pull default answers directly from the stack’s question set (`loadStackQuestionMetadata`) and fill any remaining empty responses. We track which questions were auto-defaulted (`defaultedQuestionIds`) so the summary can explain why.

3. **Persist and surface responses**
- `lib/scan-prefill.ts` merges the generated responses into local wizard state and stores both `autoFilledMap` and `defaultedMap` in localStorage.
Expand All @@ -34,8 +33,9 @@ This document outlines how repository scans are transformed into AI instruction

| Location | Purpose |
| --- | --- |
| `conventions/default.json` & `/conventions/<stack>.json` | Declarative defaults + rules for each stack (tooling choices, structure hints, apply-to glob, etc.). |
| `lib/convention-values.ts` | Helpers that normalize and aggregate convention values (e.g., testingUT/testingE2E) for both the scanner and the wizard. |
| `conventions/default.json` & `/conventions/<stack>.json` | Declarative rules, `applyToGlob`, and structure hints for each stack. |
| `lib/question-metadata.ts` | Caches question answers/defaults per stack so both the scanner and wizard share a single source of truth. |
| `lib/wizard-responses.ts` | Produces empty `WizardResponses` objects and guards response keys. |
| `data/stacks.json` | List of stacks exposed to the wizard; each should have a matching conventions file. |
| `data/questions/<stack>.json` | Stack-specific questions with default answers and metadata. These defaults are now honored automatically when scan data is missing. |

Expand Down
32 changes: 0 additions & 32 deletions lib/convention-values.ts

This file was deleted.

73 changes: 73 additions & 0 deletions lib/question-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { buildStepsForStack } from "@/lib/wizard-summary-data"
import { isWizardResponseKey } from "@/lib/wizard-responses"
import type { WizardResponses, WizardStep } from "@/types/wizard"

export const normalizeConventionValue = (value: string): string => value.trim().toLowerCase()

export type StackQuestionDefault = {
questionId: string
responseKey: keyof WizardResponses
value: string
label: string
}

export type StackQuestionMetadata = {
defaults: StackQuestionDefault[]
answersByResponseKey: Partial<Record<keyof WizardResponses, string[]>>
}

const metadataCache = new Map<string, StackQuestionMetadata>()

const extractQuestionMetadata = (steps: WizardStep[], stack: string): StackQuestionMetadata => {
const defaults: StackQuestionDefault[] = []
const answersByResponseKey: Partial<Record<keyof WizardResponses, string[]>> = {}

steps.forEach((step) => {
step.questions.forEach((question) => {
const rawKey = question.responseKey ?? question.id
if (!rawKey || rawKey === "stackSelection" || !isWizardResponseKey(rawKey)) {
return
}

const key = rawKey as keyof WizardResponses
const enabledAnswers = question.answers.filter((answer) => !answer.disabled)

if (enabledAnswers.length > 0) {
const seen = new Set<string>()
answersByResponseKey[key] = enabledAnswers.reduce<string[]>((list, answer) => {
const value = typeof answer.value === "string" ? answer.value : ""
if (!value || seen.has(value.toLowerCase())) {
return list
}
seen.add(value.toLowerCase())
list.push(value)
return list
}, [])
}

const defaultAnswer = enabledAnswers.find((answer) => answer.isDefault)
if (defaultAnswer && typeof defaultAnswer.value === "string" && defaultAnswer.value.trim().length > 0) {
defaults.push({
questionId: question.id,
responseKey: key,
value: defaultAnswer.value,
label: defaultAnswer.label ?? defaultAnswer.value,
})
}
})
})

return { defaults, answersByResponseKey }
}

export const loadStackQuestionMetadata = async (stack: string): Promise<StackQuestionMetadata> => {
const normalized = stack.trim().toLowerCase()
if (metadataCache.has(normalized)) {
return metadataCache.get(normalized)!
}

const { steps } = await buildStepsForStack(stack)
const metadata = extractQuestionMetadata(steps, stack)
metadataCache.set(normalized, metadata)
return metadata
}
Loading