-
-
- {warnings.map((warning) => (
-
{warning}
- ))}
+ {showUnsupportedStackNotice ? (
+
+
+
+
+
+ We detected {detectedStackLabel}, but this stack isn’t supported yet.
+
+
+ Contribute a new entry in{" "}
+
+ data/stacks.json
+ {" "}
+ to enable full summaries for this stack.
+
- ) : null}
-
-
Raw response
-
- {JSON.stringify(scanResult, null, 2)}
-
-
- {generatedFile ? (
-
setGeneratedFile(null)}
- />
- ) : null}
+ ) : (
+ <>
+
+
+
Frameworks
+
{formatList(scanResult.frameworks)}
+
+
+
Tooling
+
{formatList(scanResult.tooling)}
+
+
+
Testing
+
{formatList(scanResult.testing)}
+
+
+
Structure hints
+
+ {structureEntries.map(({ key, value }) => (
+ -
+ {key}
+
+ {value ? "Present" : "Missing"}
+
+
+ ))}
+
+
+
+
+
+
Generate instructions
+
+ Choose the file you need—each one opens an Instructions ready preview powered by this scan.
+
+
+
+ {fileOptions.map((file) => {
+ const busy = Boolean(isGeneratingMap[file.id])
+ return (
+
+ )
+ })}
+
+
+ {warnings.length > 0 ? (
+
+
+
+
+ {warnings.map((warning) => (
+
{warning}
+ ))}
+
+
+
+ ) : null}
+
+
Raw response
+
+ {JSON.stringify(scanResult, null, 2)}
+
+
+ {generatedFile ? (
+ setGeneratedFile(null)}
+ />
+ ) : null}
+ >
+ )}
) : null}
diff --git a/conventions/angular.json b/conventions/angular.json
index ffddbb7..64389f2 100644
--- a/conventions/angular.json
+++ b/conventions/angular.json
@@ -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"] },
diff --git a/conventions/astro.json b/conventions/astro.json
index aaa4fc9..b9e5a63 100644
--- a/conventions/astro.json
+++ b/conventions/astro.json
@@ -1,12 +1,5 @@
{
"id": "astro",
- "defaults": {
- "tooling": "react",
- "dataFetching": "ssg",
- "folders": "content-collections",
- "collaboration": "vercel",
- "reactPerf": "memoHooks"
- },
"rules": [
{
"if": { "toolingIncludes": ["vue"] },
diff --git a/conventions/default.json b/conventions/default.json
index 9179177..12d48b4 100644
--- a/conventions/default.json
+++ b/conventions/default.json
@@ -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": []
}
diff --git a/conventions/nextjs.json b/conventions/nextjs.json
index 3c5c287..261affa 100644
--- a/conventions/nextjs.json
+++ b/conventions/nextjs.json
@@ -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"] },
diff --git a/conventions/nuxt.json b/conventions/nuxt.json
index 8a7e96e..8f80bd1 100644
--- a/conventions/nuxt.json
+++ b/conventions/nuxt.json
@@ -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"] },
diff --git a/conventions/python.json b/conventions/python.json
index f849c24..ce7c28f 100644
--- a/conventions/python.json
+++ b/conventions/python.json
@@ -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"] },
diff --git a/conventions/react.json b/conventions/react.json
index bd277c2..e4f0329 100644
--- a/conventions/react.json
+++ b/conventions/react.json
@@ -1,12 +1,5 @@
{
"id": "react",
- "defaults": {
- "tooling": "vite",
- "styling": "cssmodules",
- "stateManagement": "context-hooks",
- "dataFetching": "swr",
- "reactPerf": "memoHooks"
- },
"rules": [
{
"if": { "toolingIncludes": ["tailwind css", "tailwind"] },
diff --git a/conventions/remix.json b/conventions/remix.json
index 953f7da..9cc6e41 100644
--- a/conventions/remix.json
+++ b/conventions/remix.json
@@ -1,12 +1,5 @@
{
"id": "remix",
- "defaults": {
- "folders": "vercel",
- "dataFetching": "route-loaders",
- "styling": "tailwind",
- "testingUT": "vitest",
- "reactPerf": "memoHooks"
- },
"rules": [
{
"if": { "toolingIncludes": ["jest"] },
diff --git a/conventions/svelte.json b/conventions/svelte.json
index c515c0a..2fdd367 100644
--- a/conventions/svelte.json
+++ b/conventions/svelte.json
@@ -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"] },
diff --git a/conventions/vue.json b/conventions/vue.json
index 89af80f..aa172cb 100644
--- a/conventions/vue.json
+++ b/conventions/vue.json
@@ -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"] },
diff --git a/docs/scan-flow.md b/docs/scan-flow.md
index 6e2f9c2..4c6752f 100644
--- a/docs/scan-flow.md
+++ b/docs/scan-flow.md
@@ -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/
.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/.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.
@@ -34,8 +33,9 @@ This document outlines how repository scans are transformed into AI instruction
| Location | Purpose |
| --- | --- |
-| `conventions/default.json` & `/conventions/.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/.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/.json` | Stack-specific questions with default answers and metadata. These defaults are now honored automatically when scan data is missing. |
diff --git a/lib/convention-values.ts b/lib/convention-values.ts
deleted file mode 100644
index 5d7ed46..0000000
--- a/lib/convention-values.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { LoadedConvention } from "@/types/conventions"
-import type { WizardResponses } from "@/types/wizard"
-
-export const normalizeConventionValue = (value: string): string => value.trim().toLowerCase()
-
-export const collectConventionValues = (
- conventions: LoadedConvention,
- key: keyof WizardResponses,
-): string[] => {
- const values: string[] = []
- const seen = new Set()
-
- const pushValue = (candidate: unknown) => {
- if (typeof candidate !== "string") {
- return
- }
- const normalized = normalizeConventionValue(candidate)
- if (!normalized || seen.has(normalized)) {
- return
- }
- seen.add(normalized)
- values.push(candidate)
- }
-
- pushValue(conventions.defaults[key])
-
- conventions.rules.forEach((rule) => {
- pushValue(rule.set?.[key])
- })
-
- return values
-}
diff --git a/lib/question-metadata.ts b/lib/question-metadata.ts
new file mode 100644
index 0000000..5d11197
--- /dev/null
+++ b/lib/question-metadata.ts
@@ -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>
+}
+
+const metadataCache = new Map()
+
+const extractQuestionMetadata = (steps: WizardStep[], stack: string): StackQuestionMetadata => {
+ const defaults: StackQuestionDefault[] = []
+ const answersByResponseKey: Partial> = {}
+
+ 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()
+ answersByResponseKey[key] = enabledAnswers.reduce((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 => {
+ 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
+}
diff --git a/lib/scan-to-wizard.ts b/lib/scan-to-wizard.ts
index 861e24e..290a061 100644
--- a/lib/scan-to-wizard.ts
+++ b/lib/scan-to-wizard.ts
@@ -1,9 +1,9 @@
-import { collectConventionValues, normalizeConventionValue } from "@/lib/convention-values"
import { applyConventionRules, loadStackConventions } from "@/lib/conventions"
-import { buildStepsForStack } from "@/lib/wizard-summary-data"
+import { loadStackQuestionMetadata, normalizeConventionValue } from "@/lib/question-metadata"
+import { createEmptyResponses } from "@/lib/wizard-responses"
import type { RepoScanSummary } from "@/types/repo-scan"
import type { LoadedConvention } from "@/types/conventions"
-import type { WizardResponses, WizardStep } from "@/types/wizard"
+import type { WizardResponses } from "@/types/wizard"
const STACK_FALLBACK = "react"
@@ -12,14 +12,12 @@ const toLowerArray = (values: string[] | undefined | null) =>
const detectFromScanList = (
scanList: string[] | undefined | null,
- conventions: LoadedConvention,
- key: keyof WizardResponses,
+ candidates: string[] | undefined | null,
): string | null => {
if (!Array.isArray(scanList) || scanList.length === 0) {
return null
}
- const candidates = collectConventionValues(conventions, key)
- if (candidates.length === 0) {
+ if (!Array.isArray(candidates) || candidates.length === 0) {
return null
}
@@ -66,35 +64,6 @@ const detectStack = (scan: RepoScanSummary): string => {
export const inferStackFromScan = (scan: RepoScanSummary): string => detectStack(scan)
-const createEmptyResponses = (stack: string): WizardResponses => ({
- stackSelection: stack,
- tooling: null,
- language: null,
- fileStructure: null,
- styling: null,
- testingUT: null,
- testingE2E: null,
- projectPriority: null,
- codeStyle: null,
- variableNaming: null,
- fileNaming: null,
- componentNaming: null,
- exports: null,
- comments: null,
- collaboration: null,
- stateManagement: null,
- apiLayer: null,
- folders: null,
- dataFetching: null,
- reactPerf: null,
- auth: null,
- validation: null,
- logging: null,
- commitStyle: null,
- prRules: null,
- outputFile: null,
-})
-
const detectLanguage = (scan: RepoScanSummary): string | null => {
const languages = toLowerArray(scan.languages)
if (languages.includes("typescript")) return "typescript"
@@ -103,22 +72,17 @@ const detectLanguage = (scan: RepoScanSummary): string | null => {
return scan.language ? String(scan.language) : null
}
-const detectTestingUnit = (scan: RepoScanSummary, conventions: LoadedConvention): string | null =>
- detectFromScanList(scan.testing, conventions, "testingUT")
+const detectTestingUnit = (scan: RepoScanSummary, candidates: string[] | undefined | null): string | null =>
+ detectFromScanList(scan.testing, candidates)
-const detectTestingE2E = (scan: RepoScanSummary, conventions: LoadedConvention): string | null =>
- detectFromScanList(scan.testing, conventions, "testingE2E")
+const detectTestingE2E = (scan: RepoScanSummary, candidates: string[] | undefined | null): string | null =>
+ detectFromScanList(scan.testing, candidates)
-const detectToolingSummary = (scan: RepoScanSummary, conventions: LoadedConvention): string | null => {
+const detectToolingSummary = (scan: RepoScanSummary): string | null => {
if (scan.tooling && scan.tooling.length > 0) {
return scan.tooling.join(" + ")
}
- const defaultTooling = conventions.defaults.tooling
- if (typeof defaultTooling === "string" && defaultTooling.trim().length > 0) {
- return defaultTooling
- }
-
return null
}
@@ -150,57 +114,6 @@ const detectPRRules = (scan: RepoScanSummary): string | null => {
return null
}
-type StackQuestionDefault = {
- questionId: string
- responseKey: keyof WizardResponses
- value: string
- label: string
-}
-
-const defaultsCache = new Map()
-
-const extractDefaultsFromSteps = (steps: WizardStep[], template: WizardResponses): StackQuestionDefault[] => {
- const defaults: StackQuestionDefault[] = []
- steps.forEach((step) => {
- step.questions.forEach((question) => {
- const rawKey = question.responseKey ?? question.id
- if (!rawKey || rawKey === "stackSelection") {
- return
- }
- const defaultAnswer = question.answers.find((answer) => answer.isDefault && !answer.disabled)
- if (!defaultAnswer) {
- return
- }
- if (!(rawKey in template)) {
- return
- }
- const key = rawKey as keyof WizardResponses
- defaults.push({
- questionId: question.id,
- responseKey: key,
- value: defaultAnswer.value,
- label: defaultAnswer.label ?? defaultAnswer.value,
- })
- })
- })
- return defaults
-}
-
-const loadStackQuestionDefaults = async (
- stack: string,
- template: WizardResponses,
-): Promise => {
- const normalized = stack.trim().toLowerCase()
- if (defaultsCache.has(normalized)) {
- return defaultsCache.get(normalized)!
- }
-
- const { steps } = await buildStepsForStack(stack)
- const defaults = extractDefaultsFromSteps(steps, template)
- defaultsCache.set(normalized, defaults)
- return defaults
-}
-
type BuildResult = {
stack: string
responses: WizardResponses
@@ -213,20 +126,21 @@ type BuildResult = {
export const buildResponsesFromScan = async (scan: RepoScanSummary): Promise => {
const stack = detectStack(scan)
const { conventions, hasStackFile } = await loadStackConventions(stack)
+ const { defaults: questionDefaults, answersByResponseKey } = await loadStackQuestionMetadata(stack)
const base = createEmptyResponses(stack)
- const withDefaults: WizardResponses = { ...base, ...conventions.defaults }
-
- applyDetectedValue(withDefaults, "tooling", detectToolingSummary(scan, conventions))
- applyDetectedValue(withDefaults, "language", detectLanguage(scan))
- applyDetectedValue(withDefaults, "testingUT", detectTestingUnit(scan, conventions))
- applyDetectedValue(withDefaults, "testingE2E", detectTestingE2E(scan, conventions))
- applyDetectedValue(withDefaults, "fileNaming", detectFileNaming(scan))
- applyDetectedValue(withDefaults, "componentNaming", detectComponentNaming(scan))
- applyDetectedValue(withDefaults, "commitStyle", detectCommitStyle(scan))
- applyDetectedValue(withDefaults, "prRules", detectPRRules(scan))
-
- const afterRules = applyConventionRules(withDefaults, conventions.rules, scan)
+ const withDetected: WizardResponses = { ...base }
+
+ applyDetectedValue(withDetected, "tooling", detectToolingSummary(scan))
+ applyDetectedValue(withDetected, "language", detectLanguage(scan))
+ applyDetectedValue(withDetected, "testingUT", detectTestingUnit(scan, answersByResponseKey.testingUT))
+ applyDetectedValue(withDetected, "testingE2E", detectTestingE2E(scan, answersByResponseKey.testingE2E))
+ applyDetectedValue(withDetected, "fileNaming", detectFileNaming(scan))
+ applyDetectedValue(withDetected, "componentNaming", detectComponentNaming(scan))
+ applyDetectedValue(withDetected, "commitStyle", detectCommitStyle(scan))
+ applyDetectedValue(withDetected, "prRules", detectPRRules(scan))
+
+ const afterRules = applyConventionRules(withDetected, conventions.rules, scan)
afterRules.stackSelection = stack
const defaultedQuestionIds: Record = {}
@@ -234,7 +148,6 @@ export const buildResponsesFromScan = async (scan: RepoScanSummary): Promise> = {}
- const questionDefaults = await loadStackQuestionDefaults(stack, afterRules)
questionDefaults.forEach(({ responseKey, questionId, value, label }) => {
const currentValue = afterRules[responseKey]
if (currentValue === null || currentValue === undefined || currentValue === "") {
@@ -248,17 +161,14 @@ export const buildResponsesFromScan = async (scan: RepoScanSummary): Promise
+
+const BASE_RESPONSES: Omit = {
+ tooling: null,
+ language: null,
+ fileStructure: null,
+ styling: null,
+ testingUT: null,
+ testingE2E: null,
+ projectPriority: null,
+ codeStyle: null,
+ variableNaming: null,
+ fileNaming: null,
+ componentNaming: null,
+ exports: null,
+ comments: null,
+ collaboration: null,
+ stateManagement: null,
+ apiLayer: null,
+ folders: null,
+ dataFetching: null,
+ reactPerf: null,
+ auth: null,
+ validation: null,
+ logging: null,
+ commitStyle: null,
+ prRules: null,
+ outputFile: null,
+}
+
+const RESPONSE_KEY_SET = new Set(RESPONSE_KEYS)
+
+export const createEmptyResponses = (stack: string): WizardResponses => ({
+ stackSelection: stack,
+ ...BASE_RESPONSES,
+})
+
+export const isWizardResponseKey = (value: string): value is keyof WizardResponses =>
+ RESPONSE_KEY_SET.has(value as keyof WizardResponses)
+
diff --git a/types/repo-scan.ts b/types/repo-scan.ts
index a43a299..203e881 100644
--- a/types/repo-scan.ts
+++ b/types/repo-scan.ts
@@ -8,6 +8,8 @@ export type RepoStructureSummary = {
export type RepoScanConventionsMeta = {
stack: string
+ stackLabel: string | null
+ isSupported: boolean
hasCustomConventions: boolean
structureRelevant: Array
}