@decantr/verifier 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/index.ts
2
2
  import { createHash } from "crypto";
3
- import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync5 } from "fs";
3
+ import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync7, realpathSync as realpathSync2, statSync as statSync6 } from "fs";
4
4
  import { readFile } from "fs/promises";
5
- import { basename as basename2, dirname as dirname2, extname as extname6, isAbsolute as isAbsolute4, join as join6, relative as relative6, resolve as resolve3 } from "path";
5
+ import { basename as basename2, dirname as dirname3, extname as extname7, isAbsolute as isAbsolute4, join as join7, relative as relative7, resolve as resolve4 } from "path";
6
6
  import { evaluateGuard, isV4, validateEssence } from "@decantr/essence-spec";
7
7
  import * as ts6 from "typescript";
8
8
 
@@ -1008,6 +1008,49 @@ function isNextProject(projectRoot) {
1008
1008
  return false;
1009
1009
  }
1010
1010
  }
1011
+ function readProjectDependencies(projectRoot) {
1012
+ try {
1013
+ const pkg = JSON.parse(readFileSync2(join(projectRoot, "package.json"), "utf-8"));
1014
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1015
+ } catch {
1016
+ return {};
1017
+ }
1018
+ }
1019
+ function isFrameworkRuntimeProject(projectRoot, runtimeTarget) {
1020
+ const normalizedTarget = runtimeTarget?.toLowerCase() ?? "";
1021
+ if (["angular", "next", "nextjs", "nuxt", "react", "solid", "svelte", "sveltekit", "vue"].includes(
1022
+ normalizedTarget
1023
+ )) {
1024
+ return true;
1025
+ }
1026
+ if (existsSync(join(projectRoot, "vite.config.js")) || existsSync(join(projectRoot, "vite.config.ts")) || existsSync(join(projectRoot, "angular.json")) || existsSync(join(projectRoot, "svelte.config.js")) || existsSync(join(projectRoot, "svelte.config.ts"))) {
1027
+ return true;
1028
+ }
1029
+ const dependencies = readProjectDependencies(projectRoot);
1030
+ return [
1031
+ "@angular/core",
1032
+ "@angular/router",
1033
+ "@sveltejs/kit",
1034
+ "next",
1035
+ "nuxt",
1036
+ "react",
1037
+ "react-dom",
1038
+ "solid-js",
1039
+ "svelte",
1040
+ "vue"
1041
+ ].some((name) => Boolean(dependencies[name]));
1042
+ }
1043
+ function hasFrameworkMountPoint(html) {
1044
+ return /\bid=(["'])(?:root|app|__next)\1/i.test(html) || /<app-root(?:\s|>)/i.test(html) || /<[^>]+\bdata-(?:reactroot|sveltekit|astro-cid)\b/i.test(html);
1045
+ }
1046
+ function hasSemanticStaticAppRoot(html) {
1047
+ return /<main(?:\s|>)/i.test(html) || /<[^>]+\brole=(["'])main\1/i.test(html) || /<section\b[^>]*(?:\bid=(["'])todoapp\1|\bclass=(["'])[^"']*\btodoapp\b[^"']*\2)/i.test(html);
1048
+ }
1049
+ function rootDocumentLooksUsable(html, options) {
1050
+ if (options.frameworkDocumentOutput) return true;
1051
+ if (hasFrameworkMountPoint(html)) return true;
1052
+ return !options.frameworkRuntimeProject && hasSemanticStaticAppRoot(html);
1053
+ }
1011
1054
  async function startStaticServer(rootDir) {
1012
1055
  const server = createServer((req, res) => {
1013
1056
  const requestedUrl = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -1070,6 +1113,7 @@ async function auditBuiltDist(projectRoot, options = {}) {
1070
1113
  }
1071
1114
  const routeHints = Array.isArray(options.routeHints) && options.routeHints.length > 0 ? options.routeHints.map((route) => normalizeRouteHint(route)).filter(Boolean).slice(0, 8) : ["/"];
1072
1115
  const frameworkDocumentOutput = isNextProject(projectRoot);
1116
+ const frameworkRuntimeProject = isFrameworkRuntimeProject(projectRoot, options.runtimeTarget);
1073
1117
  const indexHtml = readFileSync2(indexPath, "utf-8");
1074
1118
  const assetPaths = extractAssetPaths(indexHtml);
1075
1119
  const server = await startStaticServer(distDir);
@@ -1077,7 +1121,7 @@ async function auditBuiltDist(projectRoot, options = {}) {
1077
1121
  const rootResponse = await fetch(`${server.baseUrl}/`);
1078
1122
  const rootHtml = await rootResponse.text();
1079
1123
  const failures = frameworkDocumentOutput ? ["next-build-output"] : [];
1080
- const rootDocumentOk = rootResponse.ok && (frameworkDocumentOutput || /id="root"/.test(rootHtml));
1124
+ const rootDocumentOk = rootResponse.ok && rootDocumentLooksUsable(rootHtml, { frameworkDocumentOutput, frameworkRuntimeProject });
1081
1125
  const titleOk = /<title>[^<]+<\/title>/i.test(rootHtml);
1082
1126
  const langOk = /<html[^>]*\slang=(["'])[^"']+\1/i.test(rootHtml);
1083
1127
  const viewportOk = /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(rootHtml);
@@ -1157,7 +1201,7 @@ async function auditBuiltDist(projectRoot, options = {}) {
1157
1201
  for (const routeHint of routeHints) {
1158
1202
  const routeResponse = await fetch(`${server.baseUrl}${routeHint}`);
1159
1203
  const routeHtml = await routeResponse.text();
1160
- const routeRootDocumentOk = routeResponse.ok && (frameworkDocumentOutput || /id="root"/.test(routeHtml));
1204
+ const routeRootDocumentOk = routeResponse.ok && rootDocumentLooksUsable(routeHtml, { frameworkDocumentOutput, frameworkRuntimeProject });
1161
1205
  const routeTitleOk = /<title>[^<]+<\/title>/i.test(routeHtml);
1162
1206
  const routeLangOk = /<html[^>]*\slang=(["'])[^"']+\1/i.test(routeHtml);
1163
1207
  const routeViewportOk = /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(routeHtml);
@@ -1965,147 +2009,908 @@ function deriveVerificationDiagnostic(input) {
1965
2009
  };
1966
2010
  }
1967
2011
 
1968
- // src/graph-anchors.ts
1969
- function payloadRecord(payload) {
1970
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
1971
- }
1972
- function payloadString(payload, key) {
1973
- const value = payloadRecord(payload)[key];
1974
- return typeof value === "string" ? value : void 0;
1975
- }
1976
- function slug(value) {
1977
- return value.trim().toLowerCase().replace(/[^a-z0-9_.:/-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1978
- }
1979
- function normalizedHaystack(finding) {
1980
- return [
1981
- finding.id,
1982
- finding.category,
1983
- finding.message,
1984
- finding.target,
1985
- finding.file,
1986
- finding.rule,
1987
- ...finding.evidence ?? []
1988
- ].filter((entry) => typeof entry === "string" && entry.length > 0).join("\n").toLowerCase();
2012
+ // src/discovery.ts
2013
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
2014
+ import { dirname, extname as extname3, join as join3, relative as relative3, resolve } from "path";
2015
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
2016
+ ".ts",
2017
+ ".tsx",
2018
+ ".js",
2019
+ ".jsx",
2020
+ ".mjs",
2021
+ ".cjs",
2022
+ ".vue",
2023
+ ".svelte"
2024
+ ]);
2025
+ var STYLE_EXTENSIONS = /* @__PURE__ */ new Set([".css", ".scss", ".sass", ".less"]);
2026
+ var PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte", ".html"]);
2027
+ var MAX_FILE_READ_BYTES = 512 * 1024;
2028
+ var MAX_WALK_FILES = 8e3;
2029
+ var MAX_REPORT_ROUTES = 120;
2030
+ var MAX_COMPONENT_ITEMS = 160;
2031
+ var ROUTE_ASSET_EXTENSION_RE = /\.(?:avif|bmp|css|gif|ico|jpeg|jpg|js|json|map|mp4|pdf|png|svg|webp|woff2?)$/i;
2032
+ var ROUTE_VARIABLE_NAMES = "(?:path|pathname|route|currentPath|currentRoute|locationPath|activePath)";
2033
+ var JSX_ROUTE_PATH_RE = /<(?:Route|[A-Z][\w.]*Route)\b[^>]*\bpath\s*=\s*(?:"([^"]+)"|'([^']+)'|{\s*"([^"]+)"\s*}|{\s*'([^']+)'\s*}|{\s*`([^`]+)`\s*})/g;
2034
+ var OBJECT_ROUTE_PATH_RE = /\bpath\s*:\s*(?:"([^"]+)"|'([^']+)'|`([^`]+)`)/g;
2035
+ var TANSTACK_FILE_ROUTE_RE = /\bcreate(?:Lazy)?FileRoute\s*\(\s*(?:"([^"]+)"|'([^']+)'|`([^`]+)`)/g;
2036
+ var STATIC_HTML_ENTRY_FILES = [
2037
+ "index.html",
2038
+ "docs/index.html",
2039
+ "src/index.html",
2040
+ "public/index.html",
2041
+ "dist/index.html"
2042
+ ];
2043
+ var STATIC_HTML_ROUTE_DIRS = ["demos", "examples"];
2044
+ var RULE_FILES = [
2045
+ "CLAUDE.md",
2046
+ "AGENTS.md",
2047
+ "GEMINI.md",
2048
+ ".cursorrules",
2049
+ ".cursor/rules",
2050
+ ".claude/rules",
2051
+ ".github/copilot-instructions.md",
2052
+ "copilot-instructions.md",
2053
+ ".windsurfrules"
2054
+ ];
2055
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
2056
+ ".git",
2057
+ ".next",
2058
+ ".nuxt",
2059
+ ".svelte-kit",
2060
+ "coverage",
2061
+ "dist",
2062
+ "build",
2063
+ "node_modules",
2064
+ "out",
2065
+ "target",
2066
+ "vendor"
2067
+ ]);
2068
+ var EXCLUDED_COMPONENT_FILE_RE = /(?:^|\/)(?:__tests__|tests?|mocks?|fixtures?|generated|__generated__)(?:\/|$)|(?:\.test|\.spec|\.stories|\.story|\.mock|\.fixture|\.gen|\.d)\.[cm]?[tj]sx?$/i;
2069
+ function isRecord(value) {
2070
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1989
2071
  }
1990
- function graphAnchor(snapshot, node, confidence, reason, route) {
1991
- const anchor = {
1992
- snapshot_id: snapshot.id,
1993
- source_hash: snapshot.source_hash,
1994
- node_id: node.id,
1995
- node_type: node.type,
1996
- confidence,
1997
- reason
1998
- };
1999
- if (route) anchor.route = route;
2000
- return anchor;
2072
+ function readTextFile(path, maxBytes = MAX_FILE_READ_BYTES) {
2073
+ try {
2074
+ const stat = statSync2(path);
2075
+ if (!stat.isFile() || stat.size > maxBytes) return null;
2076
+ return readFileSync4(path, "utf-8");
2077
+ } catch {
2078
+ return null;
2079
+ }
2001
2080
  }
2002
- function routeForNode(snapshot, nodeId, visited = /* @__PURE__ */ new Set()) {
2003
- if (visited.has(nodeId)) return void 0;
2004
- visited.add(nodeId);
2005
- if (nodeId.startsWith("rt:")) {
2006
- const route2 = snapshot.nodes.find((node) => node.id === nodeId);
2007
- return payloadString(route2?.payload, "path") ?? nodeId.replace(/^rt:/, "");
2081
+ function readPackageJson(dir) {
2082
+ const path = join3(dir, "package.json");
2083
+ if (!existsSync3(path)) return { value: null, present: false, valid: false };
2084
+ const content = readTextFile(path);
2085
+ if (!content) return { value: null, present: true, valid: false };
2086
+ try {
2087
+ const parsed = JSON.parse(content);
2088
+ return {
2089
+ value: isRecord(parsed) ? parsed : null,
2090
+ present: true,
2091
+ valid: isRecord(parsed)
2092
+ };
2093
+ } catch {
2094
+ return { value: null, present: true, valid: false };
2008
2095
  }
2009
- const sourceNode = snapshot.nodes.find(
2010
- (node) => node.id === nodeId && node.type === "SourceArtifact"
2011
- );
2012
- if (sourceNode) {
2013
- const sourceEdges = snapshot.edges.filter(
2014
- (edge) => edge.relation === "NODE_DERIVED_FROM_SOURCE" && edge.dst === nodeId
2015
- );
2016
- const sortedSourceEdges = [
2017
- ...sourceEdges.filter((edge) => edge.src.startsWith("rt:")),
2018
- ...sourceEdges.filter((edge) => !edge.src.startsWith("rt:"))
2019
- ];
2020
- for (const edge of sortedSourceEdges) {
2021
- const route2 = routeForNode(snapshot, edge.src, visited);
2022
- if (route2) return route2;
2023
- }
2024
- return void 0;
2096
+ }
2097
+ function hasWorkspaceMarker(dir) {
2098
+ if (existsSync3(join3(dir, "pnpm-workspace.yaml")) || existsSync3(join3(dir, "turbo.json")) || existsSync3(join3(dir, "nx.json"))) {
2099
+ return true;
2025
2100
  }
2026
- const pageId = snapshot.nodes.find((node) => node.id === nodeId && node.type === "Page")?.id ?? snapshot.edges.find((edge) => edge.relation === "PAGE_COMPOSES_PATTERN" && edge.dst === nodeId)?.src ?? snapshot.edges.find((edge) => edge.relation === "PAGE_USES_SHELL" && edge.dst === nodeId)?.src;
2027
- if (!pageId) return void 0;
2028
- const routeEdge = snapshot.edges.find(
2029
- (edge) => edge.relation === "PAGE_ROUTED_AT_ROUTE" && edge.src === pageId
2030
- );
2031
- if (!routeEdge) return void 0;
2032
- const route = snapshot.nodes.find((node) => node.id === routeEdge.dst);
2033
- return payloadString(route?.payload, "path") ?? routeEdge.dst.replace(/^rt:/, "");
2101
+ return Boolean(readPackageJson(dir).value?.workspaces);
2034
2102
  }
2035
- function findProjectNode(snapshot) {
2036
- return snapshot.nodes.find((node) => node.id === snapshot.project_id) ?? snapshot.nodes.find((node) => node.type === "Project");
2103
+ function findWorkspaceRoot(startDir) {
2104
+ let current = resolve(startDir);
2105
+ while (true) {
2106
+ if (hasWorkspaceMarker(current)) return current;
2107
+ const parent = dirname(current);
2108
+ if (parent === current) return resolve(startDir);
2109
+ current = parent;
2110
+ }
2037
2111
  }
2038
- function findLocalRuleNode(snapshot, finding) {
2039
- const candidates = [finding.rule, finding.target, finding.id].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2040
- if (candidates.length === 0) return null;
2041
- return snapshot.nodes.find((node) => {
2042
- if (node.type !== "LocalRule") return false;
2043
- const payloadId = payloadString(node.payload, "id");
2044
- const ids = [node.id.replace(/^rule:/, ""), payloadId ? slug(payloadId) : null].filter(
2045
- (entry) => Boolean(entry)
2046
- );
2047
- return ids.some((id) => candidates.some((candidate) => id === candidate));
2048
- }) ?? null;
2112
+ function packageManagerFromName(value) {
2113
+ const name = value?.split("@")[0];
2114
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
2115
+ return "unknown";
2049
2116
  }
2050
- function findStyleBridgeNode(snapshot, finding) {
2051
- const haystack = normalizedHaystack(finding);
2052
- const candidates = [finding.rule, finding.target, finding.id, ...finding.evidence ?? []].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2053
- const styleBridgeNodes = snapshot.nodes.filter((node) => node.type === "StyleBridge");
2054
- if (styleBridgeNodes.length === 0) return null;
2055
- return styleBridgeNodes.find((node) => {
2056
- const payloadId = payloadString(node.payload, "id");
2057
- const payloadLabel = payloadString(node.payload, "label");
2058
- const ids = [
2059
- node.id.replace(/^bridge:/, ""),
2060
- payloadId ? slug(payloadId) : null,
2061
- payloadLabel ? slug(payloadLabel) : null
2062
- ].filter((entry) => Boolean(entry));
2063
- return ids.some(
2064
- (id) => candidates.some((candidate) => id === candidate || candidate.includes(id)) || haystack.includes(id.toLowerCase())
2065
- );
2066
- }) ?? (haystack.includes("style-bridge") || haystack.includes("style bridge") ? styleBridgeNodes[0] ?? null : null);
2117
+ function detectPackageManager(appRoot, workspaceRoot) {
2118
+ let current = resolve(appRoot);
2119
+ const stop = resolve(workspaceRoot);
2120
+ while (true) {
2121
+ const declared = packageManagerFromName(readPackageJson(current).value?.packageManager);
2122
+ if (declared !== "unknown") return declared;
2123
+ if (existsSync3(join3(current, "pnpm-lock.yaml"))) return "pnpm";
2124
+ if (existsSync3(join3(current, "yarn.lock"))) return "yarn";
2125
+ if (existsSync3(join3(current, "bun.lockb")) || existsSync3(join3(current, "bun.lock")))
2126
+ return "bun";
2127
+ if (existsSync3(join3(current, "package-lock.json"))) return "npm";
2128
+ const parent = dirname(current);
2129
+ if (parent === current || current === stop) return "unknown";
2130
+ current = parent;
2131
+ }
2067
2132
  }
2068
- function findRouteNode(snapshot, finding) {
2069
- const haystack = normalizedHaystack(finding);
2070
- const exactTarget = finding.target?.startsWith("/") ? finding.target : null;
2071
- const routes = snapshot.nodes.filter((node) => node.type === "Route").map((node) => ({
2072
- node,
2073
- path: payloadString(node.payload, "path") ?? node.id.replace(/^rt:/, "")
2074
- })).sort((a, b) => b.path.length - a.path.length);
2075
- return routes.find((route) => route.path === exactTarget)?.node ?? routes.find((route) => route.path !== "/" && haystack.includes(route.path.toLowerCase()))?.node ?? null;
2133
+ function hasAnyFile(projectRoot, paths) {
2134
+ return paths.some((path) => existsSync3(join3(projectRoot, path)));
2076
2135
  }
2077
- function findPageNode(snapshot, finding) {
2078
- const candidates = [finding.target, finding.id].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2079
- if (candidates.length === 0) return null;
2080
- return snapshot.nodes.find((node) => {
2081
- if (node.type !== "Page") return false;
2082
- const payloadId = payloadString(node.payload, "id");
2083
- const payloadSection = payloadString(node.payload, "section");
2084
- const ids = [
2085
- node.id.replace(/^pg:/, ""),
2086
- payloadId ? slug(payloadId) : null,
2087
- payloadId && payloadSection ? slug(`${payloadSection}:${payloadId}`) : null
2088
- ].filter((entry) => Boolean(entry));
2089
- return ids.some((id) => candidates.some((candidate) => id === candidate));
2090
- }) ?? null;
2136
+ function hasStaticHtmlSurface(projectRoot) {
2137
+ if (hasAnyFile(projectRoot, STATIC_HTML_ENTRY_FILES)) return true;
2138
+ return STATIC_HTML_ROUTE_DIRS.some((dir) => existsSync3(join3(projectRoot, dir, "index.html")));
2091
2139
  }
2092
- function findPatternNode(snapshot, finding) {
2093
- const haystack = normalizedHaystack(finding);
2094
- return snapshot.nodes.find((node) => {
2095
- if (node.type !== "Pattern") return false;
2096
- const payloadId = payloadString(node.payload, "id");
2097
- return [node.id.replace(/^pat:/, ""), payloadId].filter((entry) => Boolean(entry)).some((id) => haystack.includes(id.toLowerCase()));
2098
- }) ?? null;
2140
+ function shouldSkipDir(name) {
2141
+ return SKIP_DIRS.has(name) || name.startsWith(".") && name !== ".github";
2099
2142
  }
2100
- function findSourceArtifactNode(snapshot, finding) {
2101
- const haystack = normalizedHaystack(finding);
2102
- return snapshot.nodes.find((node) => {
2103
- if (node.type !== "SourceArtifact") return false;
2104
- const path = payloadString(node.payload, "path");
2105
- return path ? haystack.includes(path.toLowerCase()) : false;
2106
- }) ?? null;
2143
+ function walkFiles(projectRoot, options = {}) {
2144
+ const files = [];
2145
+ function walk(dir) {
2146
+ if (files.length >= MAX_WALK_FILES) return;
2147
+ let entries;
2148
+ try {
2149
+ entries = readdirSync3(dir);
2150
+ } catch {
2151
+ return;
2152
+ }
2153
+ for (const entry of entries) {
2154
+ if (files.length >= MAX_WALK_FILES) return;
2155
+ const fullPath = join3(dir, entry);
2156
+ let stat;
2157
+ try {
2158
+ stat = statSync2(fullPath);
2159
+ } catch {
2160
+ continue;
2161
+ }
2162
+ if (stat.isDirectory()) {
2163
+ if (shouldSkipDir(entry) && !options.includeHidden) continue;
2164
+ walk(fullPath);
2165
+ continue;
2166
+ }
2167
+ if (!stat.isFile()) continue;
2168
+ const rel = relative3(projectRoot, fullPath).replace(/\\/g, "/");
2169
+ const ext = extname3(entry).toLowerCase();
2170
+ if (!options.extensions || options.extensions.has(ext)) files.push(rel);
2171
+ }
2172
+ }
2173
+ walk(projectRoot);
2174
+ return files;
2107
2175
  }
2108
- function normalizePath3(value) {
2176
+ function dependencyVersion(dependencies, names) {
2177
+ for (const name of names) {
2178
+ const version = dependencies[name];
2179
+ if (version) return version.replace(/^[~^]/, "");
2180
+ }
2181
+ return null;
2182
+ }
2183
+ function detectPrimaryLanguage(projectRoot, packageJsonPresent) {
2184
+ const sourceFiles = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
2185
+ if (hasAnyFile(projectRoot, ["tsconfig.json"]) || sourceFiles.some((file) => /\.(tsx|ts)$/.test(file))) {
2186
+ return "typescript";
2187
+ }
2188
+ if (sourceFiles.some((file) => /\.(jsx|js|mjs|cjs)$/.test(file))) return "javascript";
2189
+ if (packageJsonPresent) return hasStaticHtmlSurface(projectRoot) ? "html" : "javascript";
2190
+ if (hasAnyFile(projectRoot, ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"]))
2191
+ return "python";
2192
+ if (hasAnyFile(projectRoot, ["go.mod"])) return "go";
2193
+ if (hasAnyFile(projectRoot, ["Cargo.toml"])) return "rust";
2194
+ if (hasStaticHtmlSurface(projectRoot)) return "html";
2195
+ return "unknown";
2196
+ }
2197
+ function detectProjectIdentity(projectRoot, workspaceRoot) {
2198
+ const packageRead = readPackageJson(projectRoot);
2199
+ const pkg = packageRead.value;
2200
+ const dependencies = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2201
+ const evidence = [];
2202
+ let framework = "unknown";
2203
+ let frameworkVersion = null;
2204
+ if (hasAnyFile(projectRoot, ["next.config.js", "next.config.ts", "next.config.mjs"]) || dependencies.next) {
2205
+ framework = "nextjs";
2206
+ frameworkVersion = dependencyVersion(dependencies, ["next"]);
2207
+ evidence.push("Next.js config or dependency");
2208
+ } else if (hasAnyFile(projectRoot, ["nuxt.config.js", "nuxt.config.ts"]) || dependencies.nuxt) {
2209
+ framework = "nuxt";
2210
+ frameworkVersion = dependencyVersion(dependencies, ["nuxt"]);
2211
+ evidence.push("Nuxt config or dependency");
2212
+ } else if (hasAnyFile(projectRoot, ["astro.config.mjs", "astro.config.ts"]) || dependencies.astro) {
2213
+ framework = "astro";
2214
+ frameworkVersion = dependencyVersion(dependencies, ["astro"]);
2215
+ evidence.push("Astro config or dependency");
2216
+ } else if (hasAnyFile(projectRoot, ["svelte.config.js", "svelte.config.ts"]) || dependencies.svelte || dependencies["@sveltejs/kit"]) {
2217
+ framework = "svelte";
2218
+ frameworkVersion = dependencyVersion(dependencies, ["svelte", "@sveltejs/kit"]);
2219
+ evidence.push("Svelte/SvelteKit config or dependency");
2220
+ } else if (hasAnyFile(projectRoot, ["angular.json"]) || dependencies["@angular/core"]) {
2221
+ framework = "angular";
2222
+ frameworkVersion = dependencyVersion(dependencies, ["@angular/core"]);
2223
+ evidence.push("Angular config or dependency");
2224
+ } else if (dependencies["solid-js"]) {
2225
+ framework = "solid";
2226
+ frameworkVersion = dependencyVersion(dependencies, ["solid-js"]);
2227
+ evidence.push("Solid dependency");
2228
+ } else if (dependencies.vue) {
2229
+ framework = "vue";
2230
+ frameworkVersion = dependencyVersion(dependencies, ["vue"]);
2231
+ evidence.push("Vue dependency");
2232
+ } else if (dependencies.react || dependencies["react-dom"] || hasAnyFile(projectRoot, ["vite.config.ts", "vite.config.js"])) {
2233
+ framework = dependencies.react || dependencies["react-dom"] ? "react" : "unknown";
2234
+ frameworkVersion = dependencyVersion(dependencies, ["react", "react-dom"]);
2235
+ if (framework === "react") evidence.push("React dependency");
2236
+ }
2237
+ if (framework === "unknown" && hasStaticHtmlSurface(projectRoot)) {
2238
+ framework = "html";
2239
+ evidence.push("static HTML entrypoint");
2240
+ }
2241
+ const cssFiles = walkFiles(projectRoot, { extensions: STYLE_EXTENSIONS });
2242
+ const hasTailwind = hasAnyFile(projectRoot, [
2243
+ "tailwind.config.js",
2244
+ "tailwind.config.ts",
2245
+ "tailwind.config.mjs",
2246
+ "tailwind.config.cjs"
2247
+ ]) || Boolean(dependencies.tailwindcss) || cssFiles.some((file) => readTextFile(join3(projectRoot, file), 128 * 1024)?.includes("@theme"));
2248
+ return {
2249
+ framework,
2250
+ frameworkVersion,
2251
+ packageManager: detectPackageManager(projectRoot, workspaceRoot),
2252
+ primaryLanguage: detectPrimaryLanguage(projectRoot, packageRead.present),
2253
+ hasTypeScript: hasAnyFile(projectRoot, ["tsconfig.json"]) || walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS }).some(
2254
+ (file) => /\.(tsx|ts)$/.test(file)
2255
+ ),
2256
+ hasTailwind,
2257
+ hasDecantr: existsSync3(join3(projectRoot, "decantr.essence.json")) || existsSync3(join3(projectRoot, ".decantr")) || Boolean(dependencies["@decantr/cli"] || dependencies["@decantr/css"]),
2258
+ packageName: typeof pkg?.name === "string" ? pkg.name : null,
2259
+ packageJsonPresent: packageRead.present,
2260
+ packageJsonValid: packageRead.valid,
2261
+ dependencies,
2262
+ evidence
2263
+ };
2264
+ }
2265
+ function segmentToRoute(segment) {
2266
+ if (segment.startsWith("(") && segment.endsWith(")")) return null;
2267
+ if (segment.startsWith("[") && segment.endsWith("]")) {
2268
+ const param = segment.slice(1, -1);
2269
+ if (param.startsWith("...")) return `:${param.slice(3)}*`;
2270
+ if (param.startsWith("[...") && param.endsWith("]")) return `:${param.slice(4, -1)}*`;
2271
+ return `:${param}`;
2272
+ }
2273
+ return segment;
2274
+ }
2275
+ function normalizeRouteLiteral(value) {
2276
+ const withoutHash = value.trim().split("#")[0];
2277
+ const cleaned = withoutHash.includes("?") && !withoutHash.endsWith(")?") ? withoutHash.split("?")[0] : withoutHash;
2278
+ if (!cleaned || cleaned === "/") return ["/"];
2279
+ if (cleaned === "*" || cleaned === "**" || cleaned.startsWith("#")) return [];
2280
+ if (cleaned === "/*" || cleaned === "/**") return [];
2281
+ if (ROUTE_ASSET_EXTENSION_RE.test(cleaned)) return [];
2282
+ const optionalGroup = cleaned.match(/^\/\(([^()]+)\)\?$/);
2283
+ if (optionalGroup) {
2284
+ return [
2285
+ "/",
2286
+ ...optionalGroup[1].split("|").map((segment) => segment.trim()).filter(Boolean).map((segment) => `/${segment}`)
2287
+ ];
2288
+ }
2289
+ const withoutTrailingWildcard = cleaned.endsWith("*") && !cleaned.endsWith("/*") && !/:\w+\*$/.test(cleaned) ? cleaned.slice(0, -1) : cleaned;
2290
+ const normalized = withoutTrailingWildcard.replace(/\/+$/g, "") || "/";
2291
+ if (normalized === "/*" || normalized === "/**") return [];
2292
+ return [normalized.startsWith("/") ? normalized : `/${normalized}`];
2293
+ }
2294
+ function addRouteSignal(signals, input) {
2295
+ if (input.path === null || input.path === void 0) return;
2296
+ for (const path of normalizeRouteLiteral(input.path)) {
2297
+ signals.push({ ...input, path });
2298
+ }
2299
+ }
2300
+ function fileRouteFromPath(file, baseDir) {
2301
+ let withoutExt = file.slice(0, -extname3(file).length);
2302
+ if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
2303
+ withoutExt = withoutExt.replace(/\/(?:page|\+page)$/u, "");
2304
+ withoutExt = withoutExt.replace(
2305
+ new RegExp(`^${baseDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`),
2306
+ ""
2307
+ );
2308
+ const parts = withoutExt.split("/").filter(Boolean).map((part) => segmentToRoute(part)).filter(Boolean);
2309
+ return `/${parts.join("/")}` || "/";
2310
+ }
2311
+ function scanFileRoutes(projectRoot, baseDir, extensions = PAGE_EXTENSIONS, routeFileNames) {
2312
+ const fullBase = join3(projectRoot, baseDir);
2313
+ if (!existsSync3(fullBase)) return [];
2314
+ return walkFiles(fullBase, { extensions }).flatMap((file) => {
2315
+ const baseName = file.split("/").pop() ?? file;
2316
+ const routeFileName = baseName.slice(0, -extname3(baseName).length);
2317
+ if (routeFileNames && !routeFileNames.has(routeFileName)) return [];
2318
+ if (routeFileName.startsWith("_")) return [];
2319
+ const rel = relative3(projectRoot, join3(fullBase, file)).replace(/\\/g, "/");
2320
+ return [
2321
+ {
2322
+ path: fileRouteFromPath(rel, baseDir),
2323
+ file: rel,
2324
+ kind: "file-route",
2325
+ confidence: "high",
2326
+ taskable: true,
2327
+ evidence: `file route ${rel}`
2328
+ }
2329
+ ];
2330
+ });
2331
+ }
2332
+ function collectRouteLiterals(pattern, content, file, signals, kind) {
2333
+ for (const match of content.matchAll(pattern)) {
2334
+ const value = match.slice(1).find((item) => typeof item === "string");
2335
+ addRouteSignal(signals, {
2336
+ path: value,
2337
+ file,
2338
+ kind,
2339
+ confidence: kind === "pathname-branch" ? "medium" : "high",
2340
+ taskable: true,
2341
+ evidence: `${kind} route literal`
2342
+ });
2343
+ }
2344
+ }
2345
+ function collectPathnameBranchRoutes(content, file, signals) {
2346
+ const beforeCount = signals.length;
2347
+ collectRouteLiterals(
2348
+ new RegExp(
2349
+ `\\b${ROUTE_VARIABLE_NAMES}\\b\\s*(?:===|!==|==|!=)\\s*["'\`](\\/[^"'\`]+)["'\`]`,
2350
+ "g"
2351
+ ),
2352
+ content,
2353
+ file,
2354
+ signals,
2355
+ "pathname-branch"
2356
+ );
2357
+ collectRouteLiterals(
2358
+ new RegExp(
2359
+ `["'\`](\\/[^"'\`]+)["'\`]\\s*(?:===|!==|==|!=)\\s*\\b${ROUTE_VARIABLE_NAMES}\\b`,
2360
+ "g"
2361
+ ),
2362
+ content,
2363
+ file,
2364
+ signals,
2365
+ "pathname-branch"
2366
+ );
2367
+ collectRouteLiterals(
2368
+ /\bcase\s+["'`](\/[^"'`]+)["'`]\s*:/g,
2369
+ content,
2370
+ file,
2371
+ signals,
2372
+ "pathname-branch"
2373
+ );
2374
+ const hasPathnameSignal = /\b(?:window\.|document\.)?location\.pathname\b|\bpathname\b/.test(
2375
+ content
2376
+ );
2377
+ if (signals.length > beforeCount || hasPathnameSignal) {
2378
+ collectRouteLiterals(
2379
+ /\b(?:href|to)\s*=\s*(?:"([^"]+)"|'([^']+)'|{\s*"([^"]+)"\s*}|{\s*'([^']+)'\s*}|{\s*`([^`]+)`\s*})/g,
2380
+ content,
2381
+ file,
2382
+ signals,
2383
+ "pathname-branch"
2384
+ );
2385
+ }
2386
+ }
2387
+ function detectSourceRouteSignals(projectRoot, identity) {
2388
+ const signals = [];
2389
+ const files = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
2390
+ const hasTanstack = Boolean(
2391
+ identity.dependencies["@tanstack/react-router"] || identity.dependencies["@tanstack/router-core"]
2392
+ );
2393
+ for (const file of files) {
2394
+ const content = readTextFile(join3(projectRoot, file));
2395
+ if (!content) continue;
2396
+ const isTanstackFile = content.includes("@tanstack/react-router") || content.includes("createFileRoute") || content.includes("createLazyFileRoute") || content.includes("createRoute") || hasTanstack && (/\brouteTree\b/.test(content) || /routeTree\.gen\.[cm]?[tj]sx?$/.test(file));
2397
+ const isReactRouterFile = content.includes("react-router-dom") || content.includes("react-router") || content.includes("<Routes") || content.includes("createBrowserRouter") || content.includes("createHashRouter") || content.includes("RouterProvider") || content.includes("HashRouter") || content.includes("BrowserRouter");
2398
+ if (isTanstackFile) {
2399
+ collectRouteLiterals(TANSTACK_FILE_ROUTE_RE, content, file, signals, "tanstack-router");
2400
+ collectRouteLiterals(OBJECT_ROUTE_PATH_RE, content, file, signals, "tanstack-router");
2401
+ if (/\bcreateRootRoute(?:WithContext)?\s*\(/.test(content)) {
2402
+ addRouteSignal(signals, {
2403
+ path: "/",
2404
+ file,
2405
+ kind: "tanstack-router",
2406
+ confidence: "high",
2407
+ taskable: true,
2408
+ evidence: "TanStack root route"
2409
+ });
2410
+ }
2411
+ }
2412
+ if (isReactRouterFile) {
2413
+ collectRouteLiterals(JSX_ROUTE_PATH_RE, content, file, signals, "react-router");
2414
+ collectRouteLiterals(OBJECT_ROUTE_PATH_RE, content, file, signals, "react-router");
2415
+ }
2416
+ collectPathnameBranchRoutes(content, file, signals);
2417
+ const hasRouteSpecSignal = content.includes("@wasp.sh/spec") || /\b(?:const|export\s+const)\s+\w*Spec\b/.test(content) || /\bapp\s*\(\s*\{[\s\S]*\bspec\s*:/.test(content);
2418
+ if (hasRouteSpecSignal) {
2419
+ collectRouteLiterals(
2420
+ /\broute\s*\(\s*["'`][^"'`]*["'`]\s*,\s*["'`](\/[^"'`]*)["'`]\s*,\s*page\s*\(/g,
2421
+ content,
2422
+ file,
2423
+ signals,
2424
+ "source-declared"
2425
+ );
2426
+ }
2427
+ }
2428
+ return signals;
2429
+ }
2430
+ function htmlRouteFromFile(file) {
2431
+ let withoutExt = file.slice(0, -extname3(file).length).replace(/\\/g, "/");
2432
+ if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
2433
+ if (withoutExt === "index" || withoutExt === "docs" || withoutExt === "src" || withoutExt === "public" || withoutExt === "dist") {
2434
+ return "/";
2435
+ }
2436
+ return `/${withoutExt.split("/").filter(Boolean).join("/")}` || "/";
2437
+ }
2438
+ function scanStaticHtmlRouteSignals(projectRoot) {
2439
+ const signals = [];
2440
+ for (const file of STATIC_HTML_ENTRY_FILES) {
2441
+ if (!existsSync3(join3(projectRoot, file))) continue;
2442
+ signals.push({
2443
+ path: "/",
2444
+ file,
2445
+ kind: "html-route",
2446
+ confidence: "high",
2447
+ taskable: true,
2448
+ evidence: "static HTML entrypoint"
2449
+ });
2450
+ break;
2451
+ }
2452
+ for (const dir of STATIC_HTML_ROUTE_DIRS) {
2453
+ const fullDir = join3(projectRoot, dir);
2454
+ if (!existsSync3(fullDir)) continue;
2455
+ for (const file of walkFiles(fullDir, { extensions: /* @__PURE__ */ new Set([".html", ".htm"]) })) {
2456
+ const rel = relative3(projectRoot, join3(fullDir, file)).replace(/\\/g, "/");
2457
+ signals.push({
2458
+ path: htmlRouteFromFile(rel),
2459
+ file: rel,
2460
+ kind: "html-route",
2461
+ confidence: "medium",
2462
+ taskable: true,
2463
+ evidence: "nested static HTML route"
2464
+ });
2465
+ }
2466
+ }
2467
+ return signals;
2468
+ }
2469
+ function discoverRoutes(projectRoot, identity) {
2470
+ const fileRouteSignals = [];
2471
+ const nextAppSignals = ["src/app", "app"].flatMap(
2472
+ (dir) => scanFileRoutes(projectRoot, dir, PAGE_EXTENSIONS, /* @__PURE__ */ new Set(["page"]))
2473
+ );
2474
+ const pagesSignals = ["src/pages", "pages"].flatMap((dir) => scanFileRoutes(projectRoot, dir));
2475
+ if (identity.framework === "nextjs") {
2476
+ fileRouteSignals.push(...nextAppSignals, ...pagesSignals);
2477
+ } else if (identity.framework === "svelte") {
2478
+ fileRouteSignals.push(
2479
+ ...scanFileRoutes(
2480
+ projectRoot,
2481
+ "src/routes",
2482
+ /* @__PURE__ */ new Set([".svelte", ".ts", ".js"]),
2483
+ /* @__PURE__ */ new Set(["+page"])
2484
+ )
2485
+ );
2486
+ } else if (identity.framework === "nuxt") {
2487
+ fileRouteSignals.push(...scanFileRoutes(projectRoot, "pages", /* @__PURE__ */ new Set([".vue"])));
2488
+ fileRouteSignals.push(...scanFileRoutes(projectRoot, "app/pages", /* @__PURE__ */ new Set([".vue"])));
2489
+ } else if (identity.framework !== "react") {
2490
+ fileRouteSignals.push(...pagesSignals);
2491
+ }
2492
+ const sourceSignals = detectSourceRouteSignals(projectRoot, identity);
2493
+ const angularSignals = [];
2494
+ if (identity.framework === "angular") {
2495
+ for (const file of walkFiles(projectRoot, { extensions: /* @__PURE__ */ new Set([".ts", ".js"]) })) {
2496
+ const content = readTextFile(join3(projectRoot, file));
2497
+ if (!content || !/@angular\/router|RouterModule\.forRoot|provideRouter|\bRoutes\s*=/.test(content))
2498
+ continue;
2499
+ collectRouteLiterals(
2500
+ /\bpath\s*:\s*["'`]([^"'`]*)["'`]/g,
2501
+ content,
2502
+ file,
2503
+ angularSignals,
2504
+ "angular-router"
2505
+ );
2506
+ }
2507
+ }
2508
+ const htmlSignals = scanStaticHtmlRouteSignals(projectRoot);
2509
+ let selectedSignals = [];
2510
+ let strategy = "none";
2511
+ if (identity.framework === "nextjs" && nextAppSignals.length > 0 && pagesSignals.length > 0) {
2512
+ selectedSignals = [...nextAppSignals, ...pagesSignals];
2513
+ strategy = "mixed-next-router";
2514
+ } else if (identity.framework === "nextjs" && nextAppSignals.length > 0) {
2515
+ selectedSignals = nextAppSignals;
2516
+ strategy = "app-router";
2517
+ } else if (identity.framework === "nextjs" && pagesSignals.length > 0) {
2518
+ selectedSignals = pagesSignals;
2519
+ strategy = "pages-router";
2520
+ } else if (identity.framework === "angular" && angularSignals.length > 0) {
2521
+ selectedSignals = angularSignals;
2522
+ strategy = "angular-router";
2523
+ } else if (identity.framework === "svelte" && fileRouteSignals.length > 0) {
2524
+ selectedSignals = fileRouteSignals;
2525
+ strategy = "sveltekit-router";
2526
+ } else if (identity.framework === "nuxt" && fileRouteSignals.length > 0) {
2527
+ selectedSignals = fileRouteSignals;
2528
+ strategy = "nuxt-router";
2529
+ } else if (sourceSignals.some((signal) => signal.kind === "tanstack-router")) {
2530
+ selectedSignals = sourceSignals;
2531
+ strategy = "source-declared";
2532
+ } else if (sourceSignals.length > 0) {
2533
+ selectedSignals = sourceSignals;
2534
+ strategy = identity.framework === "vue" ? "vue-router" : "react-router";
2535
+ } else if (pagesSignals.length > 0) {
2536
+ selectedSignals = pagesSignals;
2537
+ strategy = "pages-router";
2538
+ } else if (htmlSignals.length > 0) {
2539
+ selectedSignals = htmlSignals;
2540
+ strategy = "static-html";
2541
+ }
2542
+ const taskable = /* @__PURE__ */ new Map();
2543
+ for (const signal of selectedSignals) {
2544
+ if (!signal.taskable) continue;
2545
+ if (taskable.has(signal.path)) continue;
2546
+ taskable.set(signal.path, {
2547
+ path: signal.path,
2548
+ file: signal.file,
2549
+ hasLayout: false,
2550
+ confidence: signal.confidence,
2551
+ source: signal.kind
2552
+ });
2553
+ }
2554
+ const routeSignals = selectedSignals.slice(0, MAX_REPORT_ROUTES);
2555
+ const taskableRoutes = [...taskable.values()].slice(0, MAX_REPORT_ROUTES);
2556
+ const confidence = taskableRoutes.length >= 3 ? "high" : taskableRoutes.length > 0 ? "medium" : "low";
2557
+ return {
2558
+ strategy,
2559
+ routeSignals,
2560
+ taskableRoutes,
2561
+ routeSignalCount: selectedSignals.length,
2562
+ taskableRouteCount: taskableRoutes.length,
2563
+ confidence,
2564
+ limitations: taskableRoutes.length === 0 ? ["No taskable route declarations were discovered from static source evidence."] : selectedSignals.length > taskableRoutes.length ? ["Some duplicate or non-taskable route signals were collapsed before task context."] : []
2565
+ };
2566
+ }
2567
+ function discoverComponents(projectRoot, routes) {
2568
+ const sourceFiles = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
2569
+ const items = /* @__PURE__ */ new Map();
2570
+ const evidence = [];
2571
+ for (const file of sourceFiles) {
2572
+ if (!/\.(tsx|jsx|vue|svelte)$/.test(file) && !/\.(ts|js)$/.test(file)) continue;
2573
+ if (EXCLUDED_COMPONENT_FILE_RE.test(file)) continue;
2574
+ const content = readTextFile(join3(projectRoot, file), 256 * 1024) ?? "";
2575
+ if (!/[<][A-Za-z][^>]*>/.test(content) && !/\.(vue|svelte)$/.test(file)) continue;
2576
+ const base = file.split("/").pop() ?? file;
2577
+ const nameFromFile = base.slice(0, -extname3(base).length);
2578
+ const names = /* @__PURE__ */ new Set();
2579
+ const isRouteLocal = routes.taskableRoutes.some((route) => route.file === file);
2580
+ for (const match of content.matchAll(/\bexport\s+function\s+([A-Z][A-Za-z0-9_]*)\b/g))
2581
+ names.add(match[1] ?? "");
2582
+ for (const match of content.matchAll(
2583
+ /\bexport\s+default\s+function\s+([A-Z][A-Za-z0-9_]*)?\b/g
2584
+ ))
2585
+ names.add(match[1] || nameFromFile);
2586
+ for (const match of content.matchAll(
2587
+ /\bexport\s+const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:memo|forwardRef|\()/g
2588
+ ))
2589
+ names.add(match[1] ?? "");
2590
+ if (isRouteLocal) {
2591
+ for (const match of content.matchAll(/\bfunction\s+([A-Z][A-Za-z0-9_]*)\b/g))
2592
+ names.add(match[1] ?? "");
2593
+ for (const match of content.matchAll(
2594
+ /\bconst\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:memo|forwardRef|\()/g
2595
+ ))
2596
+ names.add(match[1] ?? "");
2597
+ }
2598
+ if (/^[A-Z][A-Za-z0-9_-]*$/.test(nameFromFile) && /\.(tsx|jsx|vue|svelte)$/.test(file))
2599
+ names.add(nameFromFile);
2600
+ for (const name of [...names].filter(Boolean)) {
2601
+ const key = `${file}:${name}`;
2602
+ const kind = /export\s+default\s+function/.test(content) && name === nameFromFile ? "default-export" : /\b(?:memo|forwardRef)\b/.test(content) ? "wrapper" : isRouteLocal && !new RegExp(`\\bexport\\s+(?:default\\s+)?(?:function|const)\\s+${name}\\b`).test(
2603
+ content
2604
+ ) ? "route-local" : /^[A-Z]/.test(nameFromFile) && name === nameFromFile ? "pascal-file" : "exported";
2605
+ items.set(key, {
2606
+ name,
2607
+ file,
2608
+ kind,
2609
+ confidence: kind === "pascal-file" ? "medium" : "high"
2610
+ });
2611
+ }
2612
+ }
2613
+ const components = [...items.values()].slice(0, MAX_COMPONENT_ITEMS);
2614
+ const directories = [
2615
+ ...new Set(
2616
+ components.map((component) => component.file.split("/").slice(0, -1).join("/")).filter(Boolean)
2617
+ )
2618
+ ].slice(0, 24);
2619
+ if (directories.some((dir) => !/^src\/(?:components|ui)\b/.test(dir))) {
2620
+ evidence.push("feature-folder and route-local components were included");
2621
+ }
2622
+ if (components.some((component) => component.kind === "wrapper")) {
2623
+ evidence.push("memo/forwardRef wrappers were included");
2624
+ }
2625
+ const confidence = components.length >= 5 && evidence.length > 0 ? "high" : components.length > 0 ? "medium" : "low";
2626
+ return {
2627
+ pageCount: routes.taskableRouteCount,
2628
+ componentCount: components.length,
2629
+ directories,
2630
+ items: components,
2631
+ confidence,
2632
+ evidence,
2633
+ limitations: confidence === "low" ? ["Component inventory is partial because only weak static component signals were found."] : [
2634
+ "Component inventory is static and advisory, not proof that every reusable component was found."
2635
+ ]
2636
+ };
2637
+ }
2638
+ function extractCssEvidence(content) {
2639
+ const variableMatches = [...content.matchAll(/--[\w-]+\s*:/g)];
2640
+ const colorMatches = [...content.matchAll(/#[0-9a-fA-F]{3,8}\b|rgb[a]?\(|hsl[a]?\(/g)];
2641
+ const themeSignals = /* @__PURE__ */ new Set();
2642
+ if (/\bdark\b|color-scheme:\s*dark|\[data-theme=['"]dark['"]|\.dark\b/.test(content))
2643
+ themeSignals.add("dark mode");
2644
+ if (/@theme\b/.test(content)) themeSignals.add("tailwind v4 @theme");
2645
+ if (/\[data-theme=|data-theme|theme-\w+/.test(content)) themeSignals.add("theme selector");
2646
+ if (/prefers-color-scheme/.test(content)) themeSignals.add("system color preference");
2647
+ return {
2648
+ variables: variableMatches.length,
2649
+ colors: colorMatches.length,
2650
+ dark: themeSignals.has("dark mode") || themeSignals.has("system color preference"),
2651
+ themeSignals: [...themeSignals]
2652
+ };
2653
+ }
2654
+ function discoverStyling(projectRoot, identity) {
2655
+ const cssFiles = walkFiles(projectRoot, { extensions: STYLE_EXTENSIONS });
2656
+ const themeSignals = /* @__PURE__ */ new Set();
2657
+ let cssVariableCount = 0;
2658
+ let colorTokenCount = 0;
2659
+ let darkMode = false;
2660
+ let configFile = null;
2661
+ let approach = "unknown";
2662
+ const tailwindConfig = [
2663
+ "tailwind.config.js",
2664
+ "tailwind.config.ts",
2665
+ "tailwind.config.mjs",
2666
+ "tailwind.config.cjs"
2667
+ ].find((file) => existsSync3(join3(projectRoot, file)));
2668
+ for (const file of cssFiles.slice(0, 100)) {
2669
+ const content = readTextFile(join3(projectRoot, file), 256 * 1024);
2670
+ if (!content) continue;
2671
+ const evidence = extractCssEvidence(content);
2672
+ cssVariableCount += evidence.variables;
2673
+ colorTokenCount += evidence.colors;
2674
+ darkMode ||= evidence.dark;
2675
+ for (const signal of evidence.themeSignals) themeSignals.add(signal);
2676
+ }
2677
+ if (tailwindConfig || identity.dependencies.tailwindcss || themeSignals.has("tailwind v4 @theme")) {
2678
+ approach = "tailwind";
2679
+ configFile = tailwindConfig ?? (themeSignals.has("tailwind v4 @theme") ? "css @theme" : "package.json");
2680
+ } else if (identity.dependencies["@decantr/css"]) {
2681
+ approach = "decantr-css";
2682
+ configFile = "package.json";
2683
+ } else if (identity.dependencies.bootstrap) {
2684
+ approach = "bootstrap";
2685
+ configFile = "package.json";
2686
+ } else if (identity.dependencies["@mui/material"]) {
2687
+ approach = "mui";
2688
+ configFile = "package.json";
2689
+ } else if (identity.dependencies["@chakra-ui/react"]) {
2690
+ approach = "chakra";
2691
+ configFile = "package.json";
2692
+ } else if (cssFiles.some((file) => file.endsWith(".module.css"))) {
2693
+ approach = "css-modules";
2694
+ } else if (cssFiles.length > 0) {
2695
+ approach = "css";
2696
+ }
2697
+ return {
2698
+ approach,
2699
+ configFile,
2700
+ cssVariableCount,
2701
+ colorTokenCount,
2702
+ darkMode,
2703
+ themeSignals: [...themeSignals]
2704
+ };
2705
+ }
2706
+ function findAssistantRules(projectRoot) {
2707
+ return RULE_FILES.filter((file) => existsSync3(join3(projectRoot, file)));
2708
+ }
2709
+ function calculateConfidence(input) {
2710
+ let score = 20;
2711
+ const reasons = [];
2712
+ if (input.project.packageJsonPresent && input.project.packageJsonValid) {
2713
+ score += 15;
2714
+ reasons.push("package.json was readable");
2715
+ }
2716
+ if (input.project.framework !== "unknown") {
2717
+ score += 25;
2718
+ reasons.push(`${input.project.framework} framework signal found`);
2719
+ }
2720
+ if (input.project.primaryLanguage === "typescript") {
2721
+ score += 10;
2722
+ reasons.push("TypeScript source evidence found");
2723
+ }
2724
+ if (input.routes.taskableRouteCount > 0) {
2725
+ score += 20;
2726
+ reasons.push(`${input.routes.taskableRouteCount} taskable route(s) found`);
2727
+ }
2728
+ if (input.components.componentCount > 1) {
2729
+ score += 10;
2730
+ reasons.push(`${input.components.componentCount} component candidate(s) found`);
2731
+ }
2732
+ if (input.styling.approach !== "unknown") {
2733
+ score += 10;
2734
+ reasons.push(`${input.styling.approach} styling signal found`);
2735
+ }
2736
+ const clamped = Math.max(5, Math.min(98, score));
2737
+ return {
2738
+ level: clamped >= 75 ? "high" : clamped >= 45 ? "medium" : "low",
2739
+ score: clamped,
2740
+ reasons: reasons.length > 0 ? reasons : ["Only weak project signals were found."]
2741
+ };
2742
+ }
2743
+ function discoverProject(projectRoot) {
2744
+ const appRoot = resolve(projectRoot);
2745
+ const workspaceRoot = findWorkspaceRoot(appRoot);
2746
+ const projectPath = relative3(workspaceRoot, appRoot).replace(/\\/g, "/") || ".";
2747
+ const project = detectProjectIdentity(appRoot, workspaceRoot);
2748
+ const routes = discoverRoutes(appRoot, project);
2749
+ const components = discoverComponents(appRoot, routes);
2750
+ const styling = discoverStyling(appRoot, project);
2751
+ const assistant = { ruleFiles: findAssistantRules(appRoot) };
2752
+ const confidence = calculateConfidence({ project, routes, components, styling });
2753
+ const limitations = [...routes.limitations, ...components.limitations];
2754
+ return {
2755
+ schemaVersion: "discovery.v1",
2756
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2757
+ workspace: {
2758
+ workspaceRoot,
2759
+ appRoot,
2760
+ projectPath,
2761
+ scope: projectPath === "." ? "single-app" : "workspace-app"
2762
+ },
2763
+ project,
2764
+ routes,
2765
+ components,
2766
+ styling,
2767
+ assistant,
2768
+ confidence,
2769
+ limitations
2770
+ };
2771
+ }
2772
+
2773
+ // src/graph-anchors.ts
2774
+ function payloadRecord(payload) {
2775
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
2776
+ }
2777
+ function payloadString(payload, key) {
2778
+ const value = payloadRecord(payload)[key];
2779
+ return typeof value === "string" ? value : void 0;
2780
+ }
2781
+ function slug(value) {
2782
+ return value.trim().toLowerCase().replace(/[^a-z0-9_.:/-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2783
+ }
2784
+ function normalizedHaystack(finding) {
2785
+ return [
2786
+ finding.id,
2787
+ finding.category,
2788
+ finding.message,
2789
+ finding.target,
2790
+ finding.file,
2791
+ finding.rule,
2792
+ ...finding.evidence ?? []
2793
+ ].filter((entry) => typeof entry === "string" && entry.length > 0).join("\n").toLowerCase();
2794
+ }
2795
+ function graphAnchor(snapshot, node, confidence, reason, route) {
2796
+ const anchor = {
2797
+ snapshot_id: snapshot.id,
2798
+ source_hash: snapshot.source_hash,
2799
+ node_id: node.id,
2800
+ node_type: node.type,
2801
+ confidence,
2802
+ reason
2803
+ };
2804
+ if (route) anchor.route = route;
2805
+ return anchor;
2806
+ }
2807
+ function routeForNode(snapshot, nodeId, visited = /* @__PURE__ */ new Set()) {
2808
+ if (visited.has(nodeId)) return void 0;
2809
+ visited.add(nodeId);
2810
+ if (nodeId.startsWith("rt:")) {
2811
+ const route2 = snapshot.nodes.find((node) => node.id === nodeId);
2812
+ return payloadString(route2?.payload, "path") ?? nodeId.replace(/^rt:/, "");
2813
+ }
2814
+ const sourceNode = snapshot.nodes.find(
2815
+ (node) => node.id === nodeId && node.type === "SourceArtifact"
2816
+ );
2817
+ if (sourceNode) {
2818
+ const sourceEdges = snapshot.edges.filter(
2819
+ (edge) => edge.relation === "NODE_DERIVED_FROM_SOURCE" && edge.dst === nodeId
2820
+ );
2821
+ const sortedSourceEdges = [
2822
+ ...sourceEdges.filter((edge) => edge.src.startsWith("rt:")),
2823
+ ...sourceEdges.filter((edge) => !edge.src.startsWith("rt:"))
2824
+ ];
2825
+ for (const edge of sortedSourceEdges) {
2826
+ const route2 = routeForNode(snapshot, edge.src, visited);
2827
+ if (route2) return route2;
2828
+ }
2829
+ return void 0;
2830
+ }
2831
+ const pageId = snapshot.nodes.find((node) => node.id === nodeId && node.type === "Page")?.id ?? snapshot.edges.find((edge) => edge.relation === "PAGE_COMPOSES_PATTERN" && edge.dst === nodeId)?.src ?? snapshot.edges.find((edge) => edge.relation === "PAGE_USES_SHELL" && edge.dst === nodeId)?.src;
2832
+ if (!pageId) return void 0;
2833
+ const routeEdge = snapshot.edges.find(
2834
+ (edge) => edge.relation === "PAGE_ROUTED_AT_ROUTE" && edge.src === pageId
2835
+ );
2836
+ if (!routeEdge) return void 0;
2837
+ const route = snapshot.nodes.find((node) => node.id === routeEdge.dst);
2838
+ return payloadString(route?.payload, "path") ?? routeEdge.dst.replace(/^rt:/, "");
2839
+ }
2840
+ function findProjectNode(snapshot) {
2841
+ return snapshot.nodes.find((node) => node.id === snapshot.project_id) ?? snapshot.nodes.find((node) => node.type === "Project");
2842
+ }
2843
+ function findLocalRuleNode(snapshot, finding) {
2844
+ const candidates = [finding.rule, finding.target, finding.id].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2845
+ if (candidates.length === 0) return null;
2846
+ return snapshot.nodes.find((node) => {
2847
+ if (node.type !== "LocalRule") return false;
2848
+ const payloadId = payloadString(node.payload, "id");
2849
+ const ids = [node.id.replace(/^rule:/, ""), payloadId ? slug(payloadId) : null].filter(
2850
+ (entry) => Boolean(entry)
2851
+ );
2852
+ return ids.some((id) => candidates.some((candidate) => id === candidate));
2853
+ }) ?? null;
2854
+ }
2855
+ function findStyleBridgeNode(snapshot, finding) {
2856
+ const haystack = normalizedHaystack(finding);
2857
+ const candidates = [finding.rule, finding.target, finding.id, ...finding.evidence ?? []].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2858
+ const styleBridgeNodes = snapshot.nodes.filter((node) => node.type === "StyleBridge");
2859
+ if (styleBridgeNodes.length === 0) return null;
2860
+ return styleBridgeNodes.find((node) => {
2861
+ const payloadId = payloadString(node.payload, "id");
2862
+ const payloadLabel = payloadString(node.payload, "label");
2863
+ const ids = [
2864
+ node.id.replace(/^bridge:/, ""),
2865
+ payloadId ? slug(payloadId) : null,
2866
+ payloadLabel ? slug(payloadLabel) : null
2867
+ ].filter((entry) => Boolean(entry));
2868
+ return ids.some(
2869
+ (id) => candidates.some((candidate) => id === candidate || candidate.includes(id)) || haystack.includes(id.toLowerCase())
2870
+ );
2871
+ }) ?? (haystack.includes("style-bridge") || haystack.includes("style bridge") ? styleBridgeNodes[0] ?? null : null);
2872
+ }
2873
+ function findRouteNode(snapshot, finding) {
2874
+ const haystack = normalizedHaystack(finding);
2875
+ const exactTarget = finding.target?.startsWith("/") ? finding.target : null;
2876
+ const routes = snapshot.nodes.filter((node) => node.type === "Route").map((node) => ({
2877
+ node,
2878
+ path: payloadString(node.payload, "path") ?? node.id.replace(/^rt:/, "")
2879
+ })).sort((a, b) => b.path.length - a.path.length);
2880
+ return routes.find((route) => route.path === exactTarget)?.node ?? routes.find((route) => route.path !== "/" && haystack.includes(route.path.toLowerCase()))?.node ?? null;
2881
+ }
2882
+ function findPageNode(snapshot, finding) {
2883
+ const candidates = [finding.target, finding.id].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2884
+ if (candidates.length === 0) return null;
2885
+ return snapshot.nodes.find((node) => {
2886
+ if (node.type !== "Page") return false;
2887
+ const payloadId = payloadString(node.payload, "id");
2888
+ const payloadSection = payloadString(node.payload, "section");
2889
+ const ids = [
2890
+ node.id.replace(/^pg:/, ""),
2891
+ payloadId ? slug(payloadId) : null,
2892
+ payloadId && payloadSection ? slug(`${payloadSection}:${payloadId}`) : null
2893
+ ].filter((entry) => Boolean(entry));
2894
+ return ids.some((id) => candidates.some((candidate) => id === candidate));
2895
+ }) ?? null;
2896
+ }
2897
+ function findPatternNode(snapshot, finding) {
2898
+ const haystack = normalizedHaystack(finding);
2899
+ return snapshot.nodes.find((node) => {
2900
+ if (node.type !== "Pattern") return false;
2901
+ const payloadId = payloadString(node.payload, "id");
2902
+ return [node.id.replace(/^pat:/, ""), payloadId].filter((entry) => Boolean(entry)).some((id) => haystack.includes(id.toLowerCase()));
2903
+ }) ?? null;
2904
+ }
2905
+ function findSourceArtifactNode(snapshot, finding) {
2906
+ const haystack = normalizedHaystack(finding);
2907
+ return snapshot.nodes.find((node) => {
2908
+ if (node.type !== "SourceArtifact") return false;
2909
+ const path = payloadString(node.payload, "path");
2910
+ return path ? haystack.includes(path.toLowerCase()) : false;
2911
+ }) ?? null;
2912
+ }
2913
+ function normalizePath3(value) {
2109
2914
  return value ? value.replace(/\\/g, "/").toLowerCase() : null;
2110
2915
  }
2111
2916
  function findExactSourceArtifactNode(snapshot, finding) {
@@ -2361,11 +3166,13 @@ function listKnownInteractions() {
2361
3166
  }
2362
3167
 
2363
3168
  // src/scan.ts
2364
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
2365
- import { extname as extname3, isAbsolute, join as join3, relative as relative3 } from "path";
3169
+ import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
3170
+ import { extname as extname4, isAbsolute, join as join4, relative as relative4 } from "path";
2366
3171
  import { decodeHTML, decodeHTMLAttribute } from "entities";
2367
- var SCAN_REPORT_SCHEMA_URL = "https://decantr.ai/schemas/scan-report.v1.json";
2368
- var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
3172
+ var SCAN_REPORT_V1_SCHEMA_URL = "https://decantr.ai/schemas/scan-report.v1.json";
3173
+ var SCAN_REPORT_V2_SCHEMA_URL = "https://decantr.ai/schemas/scan-report.v2.json";
3174
+ var SCAN_REPORT_SCHEMA_URL = SCAN_REPORT_V2_SCHEMA_URL;
3175
+ var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([
2369
3176
  ".ts",
2370
3177
  ".tsx",
2371
3178
  ".js",
@@ -2375,25 +3182,10 @@ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
2375
3182
  ".vue",
2376
3183
  ".svelte"
2377
3184
  ]);
2378
- var STYLE_EXTENSIONS = /* @__PURE__ */ new Set([".css", ".scss", ".sass", ".less"]);
2379
- var PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte", ".html"]);
2380
- var ROUTE_VARIABLE_NAMES = "(?:path|pathname|route|currentPath|currentRoute|locationPath|activePath)";
2381
- var ROUTE_ASSET_EXTENSION_RE = /\.(?:avif|bmp|css|gif|ico|jpeg|jpg|js|json|map|mp4|pdf|png|svg|webp|woff2?)$/i;
2382
- var JSX_ROUTE_PATH_RE = /<(?:Route|[A-Z][\w.]*Route)\b[^>]*\bpath\s*=\s*(?:"([^"]+)"|'([^']+)'|{\s*"([^"]+)"\s*}|{\s*'([^']+)'\s*}|{\s*`([^`]+)`\s*})/g;
2383
- var OBJECT_ROUTE_PATH_RE = /\bpath\s*:\s*(?:"([^"]+)"|'([^']+)'|`([^`]+)`)/g;
2384
- var STATIC_HTML_ENTRY_FILES = [
2385
- "index.html",
2386
- "docs/index.html",
2387
- "src/index.html",
2388
- "public/index.html",
2389
- "dist/index.html"
2390
- ];
2391
- var STATIC_HTML_ROUTE_DIRS = ["demos", "examples"];
2392
- var MAX_STATIC_HTML_CANDIDATES = 1e3;
2393
- var MAX_FILE_READ_BYTES = 512 * 1024;
2394
- var MAX_WALK_FILES = 5e3;
2395
- var MAX_REPORT_ROUTES = 80;
2396
- var SKIP_DIRS = /* @__PURE__ */ new Set([
3185
+ var MAX_FILE_READ_BYTES2 = 512 * 1024;
3186
+ var MAX_WALK_FILES2 = 5e3;
3187
+ var MAX_REPORT_ROUTES2 = 80;
3188
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
2397
3189
  ".git",
2398
3190
  ".next",
2399
3191
  ".nuxt",
@@ -2406,17 +3198,6 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
2406
3198
  "target",
2407
3199
  "vendor"
2408
3200
  ]);
2409
- var RULE_FILES = [
2410
- "CLAUDE.md",
2411
- "AGENTS.md",
2412
- "GEMINI.md",
2413
- ".cursorrules",
2414
- ".cursor/rules",
2415
- ".claude/rules",
2416
- ".github/copilot-instructions.md",
2417
- "copilot-instructions.md",
2418
- ".windsurfrules"
2419
- ];
2420
3201
  var GITHUB_OWNER_RE = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/i;
2421
3202
  var GITHUB_REPO_RE = /^[a-z0-9._-]{1,100}$/i;
2422
3203
  function parseHttpUrl(value) {
@@ -2449,150 +3230,64 @@ var WEB_FRAMEWORKS = /* @__PURE__ */ new Set([
2449
3230
  "svelte",
2450
3231
  "vue"
2451
3232
  ]);
2452
- function isRecord(value) {
3233
+ function isRecord2(value) {
2453
3234
  return typeof value === "object" && value !== null && !Array.isArray(value);
2454
3235
  }
2455
- function readTextFile(path, maxBytes = MAX_FILE_READ_BYTES) {
3236
+ function readTextFile2(path, maxBytes = MAX_FILE_READ_BYTES2) {
2456
3237
  try {
2457
- const stat = statSync2(path);
3238
+ const stat = statSync3(path);
2458
3239
  if (!stat.isFile() || stat.size > maxBytes) return null;
2459
- return readFileSync4(path, "utf-8");
3240
+ return readFileSync5(path, "utf-8");
2460
3241
  } catch {
2461
3242
  return null;
2462
3243
  }
2463
3244
  }
2464
- function readPackageJson(projectRoot) {
2465
- const path = join3(projectRoot, "package.json");
2466
- if (!existsSync3(path)) return { value: null, present: false, valid: false };
2467
- const content = readTextFile(path);
3245
+ function readPackageJson2(projectRoot) {
3246
+ const path = join4(projectRoot, "package.json");
3247
+ if (!existsSync4(path)) return { value: null, present: false, valid: false };
3248
+ const content = readTextFile2(path);
2468
3249
  if (!content) return { value: null, present: true, valid: false };
2469
3250
  try {
2470
3251
  const parsed = JSON.parse(content);
2471
3252
  return {
2472
- value: isRecord(parsed) ? parsed : null,
3253
+ value: isRecord2(parsed) ? parsed : null,
2473
3254
  present: true,
2474
- valid: isRecord(parsed)
3255
+ valid: isRecord2(parsed)
2475
3256
  };
2476
3257
  } catch {
2477
- return { value: null, present: true, valid: false };
2478
- }
2479
- }
2480
- function dependencyVersion(dependencies, names) {
2481
- for (const name of names) {
2482
- const version = dependencies[name];
2483
- if (version) return version.replace(/^[~^]/, "");
2484
- }
2485
- return null;
2486
- }
2487
- function hasAnyFile(projectRoot, paths) {
2488
- return paths.some((path) => existsSync3(join3(projectRoot, path)));
2489
- }
2490
- function hasStaticHtmlSurface(projectRoot) {
2491
- if (hasAnyFile(projectRoot, STATIC_HTML_ENTRY_FILES)) return true;
2492
- return STATIC_HTML_ROUTE_DIRS.some((dir) => existsSync3(join3(projectRoot, dir, "index.html")));
2493
- }
2494
- function detectPackageManager(projectRoot, pkg) {
2495
- const declared = pkg?.packageManager?.split("@")[0];
2496
- if (declared === "npm" || declared === "pnpm" || declared === "yarn" || declared === "bun") {
2497
- return declared;
2498
- }
2499
- if (existsSync3(join3(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
2500
- if (existsSync3(join3(projectRoot, "yarn.lock"))) return "yarn";
2501
- if (existsSync3(join3(projectRoot, "bun.lockb"))) return "bun";
2502
- if (existsSync3(join3(projectRoot, "package-lock.json"))) return "npm";
2503
- return "unknown";
2504
- }
2505
- function detectPrimaryLanguage(projectRoot, packageJsonPresent) {
2506
- if (packageJsonPresent) return hasStaticHtmlSurface(projectRoot) ? "html" : "javascript";
2507
- if (hasAnyFile(projectRoot, ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"]))
2508
- return "python";
2509
- if (hasAnyFile(projectRoot, ["go.mod"])) return "go";
2510
- if (hasAnyFile(projectRoot, ["Cargo.toml"])) return "rust";
2511
- if (hasStaticHtmlSurface(projectRoot)) return "html";
2512
- return "unknown";
2513
- }
2514
- function detectProject(projectRoot) {
2515
- const packageRead = readPackageJson(projectRoot);
2516
- const pkg = packageRead.value;
2517
- const dependencies = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2518
- let framework = "unknown";
2519
- let frameworkVersion = null;
2520
- if (hasAnyFile(projectRoot, ["next.config.js", "next.config.ts", "next.config.mjs"]) || dependencies.next) {
2521
- framework = "nextjs";
2522
- frameworkVersion = dependencyVersion(dependencies, ["next"]);
2523
- } else if (hasAnyFile(projectRoot, ["nuxt.config.js", "nuxt.config.ts"]) || dependencies.nuxt) {
2524
- framework = "nuxt";
2525
- frameworkVersion = dependencyVersion(dependencies, ["nuxt"]);
2526
- } else if (hasAnyFile(projectRoot, ["astro.config.mjs", "astro.config.ts"]) || dependencies.astro) {
2527
- framework = "astro";
2528
- frameworkVersion = dependencyVersion(dependencies, ["astro"]);
2529
- } else if (hasAnyFile(projectRoot, ["svelte.config.js", "svelte.config.ts"]) || dependencies.svelte || dependencies["@sveltejs/kit"]) {
2530
- framework = "svelte";
2531
- frameworkVersion = dependencyVersion(dependencies, ["svelte", "@sveltejs/kit"]);
2532
- } else if (hasAnyFile(projectRoot, ["angular.json"]) || dependencies["@angular/core"]) {
2533
- framework = "angular";
2534
- frameworkVersion = dependencyVersion(dependencies, ["@angular/core"]);
2535
- } else if (dependencies["solid-js"]) {
2536
- framework = "solid";
2537
- frameworkVersion = dependencyVersion(dependencies, ["solid-js"]);
2538
- } else if (dependencies.vue) {
2539
- framework = "vue";
2540
- frameworkVersion = dependencyVersion(dependencies, ["vue"]);
2541
- } else if (dependencies.react) {
2542
- framework = "react";
2543
- frameworkVersion = dependencyVersion(dependencies, ["react"]);
2544
- } else if (hasStaticHtmlSurface(projectRoot)) {
2545
- framework = "html";
2546
- }
2547
- return {
2548
- framework,
2549
- frameworkVersion,
2550
- packageManager: detectPackageManager(projectRoot, pkg),
2551
- primaryLanguage: detectPrimaryLanguage(projectRoot, packageRead.present),
2552
- hasTypeScript: hasAnyFile(projectRoot, ["tsconfig.json"]),
2553
- hasTailwind: hasAnyFile(projectRoot, [
2554
- "tailwind.config.js",
2555
- "tailwind.config.ts",
2556
- "tailwind.config.mjs",
2557
- "tailwind.config.cjs"
2558
- ]),
2559
- hasDecantr: existsSync3(join3(projectRoot, "decantr.essence.json")) || existsSync3(join3(projectRoot, ".decantr")) || Boolean(dependencies["@decantr/cli"] || dependencies["@decantr/css"]),
2560
- packageName: typeof pkg?.name === "string" ? pkg.name : null,
2561
- packageJsonValid: packageRead.valid,
2562
- packageJsonPresent: packageRead.present,
2563
- dependencies
2564
- };
3258
+ return { value: null, present: true, valid: false };
3259
+ }
2565
3260
  }
2566
- function shouldSkipDir(name) {
2567
- return SKIP_DIRS.has(name) || name.startsWith(".") && name !== ".github";
3261
+ function shouldSkipDir2(name) {
3262
+ return SKIP_DIRS2.has(name) || name.startsWith(".") && name !== ".github";
2568
3263
  }
2569
- function walkFiles(projectRoot, options = {}) {
3264
+ function walkFiles2(projectRoot, options = {}) {
2570
3265
  const files = [];
2571
3266
  function walk(dir) {
2572
- if (files.length >= MAX_WALK_FILES) return;
3267
+ if (files.length >= MAX_WALK_FILES2) return;
2573
3268
  let entries;
2574
3269
  try {
2575
- entries = readdirSync3(dir);
3270
+ entries = readdirSync4(dir);
2576
3271
  } catch {
2577
3272
  return;
2578
3273
  }
2579
3274
  for (const entry of entries) {
2580
- if (files.length >= MAX_WALK_FILES) return;
2581
- const fullPath = join3(dir, entry);
3275
+ if (files.length >= MAX_WALK_FILES2) return;
3276
+ const fullPath = join4(dir, entry);
2582
3277
  let stat;
2583
3278
  try {
2584
- stat = statSync2(fullPath);
3279
+ stat = statSync3(fullPath);
2585
3280
  } catch {
2586
3281
  continue;
2587
3282
  }
2588
3283
  if (stat.isDirectory()) {
2589
- if (shouldSkipDir(entry) && !options.includeHidden) continue;
3284
+ if (shouldSkipDir2(entry) && !options.includeHidden) continue;
2590
3285
  walk(fullPath);
2591
3286
  continue;
2592
3287
  }
2593
3288
  if (!stat.isFile()) continue;
2594
- const rel = relative3(projectRoot, fullPath).replace(/\\/g, "/");
2595
- const ext = extname3(entry).toLowerCase();
3289
+ const rel = relative4(projectRoot, fullPath).replace(/\\/g, "/");
3290
+ const ext = extname4(entry).toLowerCase();
2596
3291
  if (!options.extensions || options.extensions.has(ext)) {
2597
3292
  files.push(rel);
2598
3293
  }
@@ -2601,347 +3296,8 @@ function walkFiles(projectRoot, options = {}) {
2601
3296
  walk(projectRoot);
2602
3297
  return files;
2603
3298
  }
2604
- function segmentToRoute(segment) {
2605
- if (segment.startsWith("(") && segment.endsWith(")")) return null;
2606
- if (segment.startsWith("[") && segment.endsWith("]")) {
2607
- const param = segment.slice(1, -1);
2608
- if (param.startsWith("...")) return `:${param.slice(3)}*`;
2609
- if (param.startsWith("[...") && param.endsWith("]")) return `:${param.slice(4, -1)}*`;
2610
- return `:${param}`;
2611
- }
2612
- return segment;
2613
- }
2614
- function walkNextAppRoutes(dir, projectRoot, segments) {
2615
- let entries;
2616
- try {
2617
- entries = readdirSync3(dir);
2618
- } catch {
2619
- return [];
2620
- }
2621
- const routes = [];
2622
- const pageFile = entries.find((entry) => /^page\.(tsx|ts|jsx|js)$/.test(entry));
2623
- const hasLayout = entries.some((entry) => /^layout\.(tsx|ts|jsx|js)$/.test(entry));
2624
- if (pageFile) {
2625
- routes.push({
2626
- path: `/${segments.filter(Boolean).join("/")}` || "/",
2627
- file: relative3(projectRoot, join3(dir, pageFile)).replace(/\\/g, "/"),
2628
- hasLayout
2629
- });
2630
- }
2631
- for (const entry of entries) {
2632
- if (shouldSkipDir(entry)) continue;
2633
- const fullPath = join3(dir, entry);
2634
- try {
2635
- if (!statSync2(fullPath).isDirectory()) continue;
2636
- } catch {
2637
- continue;
2638
- }
2639
- const routeSegment = segmentToRoute(entry);
2640
- routes.push(
2641
- ...walkNextAppRoutes(
2642
- fullPath,
2643
- projectRoot,
2644
- routeSegment === null ? segments : [...segments, routeSegment]
2645
- )
2646
- );
2647
- }
2648
- return routes;
2649
- }
2650
- function fileRouteFromPath(file, baseDir) {
2651
- let withoutExt = file.slice(0, -extname3(file).length);
2652
- if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
2653
- withoutExt = withoutExt.replace(
2654
- new RegExp(`^${baseDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`),
2655
- ""
2656
- );
2657
- const parts = withoutExt.split("/").filter(Boolean).map((part) => segmentToRoute(part)).filter(Boolean);
2658
- return `/${parts.join("/")}` || "/";
2659
- }
2660
- function scanFileRoutes(projectRoot, baseDir, extensions = PAGE_EXTENSIONS) {
2661
- const fullBase = join3(projectRoot, baseDir);
2662
- if (!existsSync3(fullBase)) return [];
2663
- return walkFiles(fullBase, { extensions }).flatMap((file) => {
2664
- const baseName = file.split("/").pop() ?? file;
2665
- const routeFileName = baseName.slice(0, -extname3(baseName).length);
2666
- if (routeFileName.startsWith("_")) return [];
2667
- const rel = relative3(projectRoot, join3(fullBase, file)).replace(/\\/g, "/");
2668
- return [{ path: fileRouteFromPath(rel, baseDir), file: rel, hasLayout: false }];
2669
- });
2670
- }
2671
- function scanReactRouter(projectRoot) {
2672
- const files = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
2673
- const routes = /* @__PURE__ */ new Map();
2674
- let hashRouting = false;
2675
- for (const file of files) {
2676
- const content = readTextFile(join3(projectRoot, file));
2677
- if (!content) continue;
2678
- if (content.includes("HashRouter") || content.includes("createHashRouter")) hashRouting = true;
2679
- for (const match of content.matchAll(JSX_ROUTE_PATH_RE)) {
2680
- const literal = firstRouteLiteralMatch(match);
2681
- for (const route of normalizeDetectedRouteLiterals(literal || "/")) {
2682
- routes.set(route, { path: route, file, hasLayout: false });
2683
- }
2684
- }
2685
- for (const match of content.matchAll(OBJECT_ROUTE_PATH_RE)) {
2686
- const literal = firstRouteLiteralMatch(match);
2687
- for (const route of normalizeDetectedRouteLiterals(literal || "/")) {
2688
- routes.set(route, { path: route, file, hasLayout: false });
2689
- }
2690
- }
2691
- for (const route of detectPathnameBranchRoutes(content)) {
2692
- routes.set(route, { path: route, file, hasLayout: false });
2693
- }
2694
- for (const route of detectDeclarativeRouteSpecRoutes(content)) {
2695
- routes.set(route, { path: route, file, hasLayout: false });
2696
- }
2697
- }
2698
- return { routes: [...routes.values()], hashRouting };
2699
- }
2700
- function firstRouteLiteralMatch(match) {
2701
- return match.slice(1).find((value) => typeof value === "string") ?? null;
2702
- }
2703
- function normalizeDetectedRouteLiterals(value) {
2704
- const withoutHash = value.trim().split("#")[0];
2705
- const cleaned = withoutHash.includes("?") && !withoutHash.endsWith(")?") ? withoutHash.split("?")[0] : withoutHash;
2706
- if (!cleaned || cleaned === "/") return ["/"];
2707
- if (cleaned === "*" || cleaned === "**" || cleaned.startsWith("#")) return [];
2708
- if (cleaned === "/*" || cleaned === "/**") return [];
2709
- if (!cleaned.startsWith("/") || cleaned.startsWith("//")) return [];
2710
- if (ROUTE_ASSET_EXTENSION_RE.test(cleaned)) return [];
2711
- const optionalGroup = cleaned.match(/^\/\(([^()]+)\)\?$/);
2712
- if (optionalGroup) {
2713
- return [
2714
- "/",
2715
- ...optionalGroup[1].split("|").map((segment) => segment.trim()).filter(Boolean).map((segment) => `/${segment}`)
2716
- ];
2717
- }
2718
- const withoutTrailingWildcard = cleaned.endsWith("*") && !cleaned.endsWith("/*") && !/:\w+\*$/.test(cleaned) ? cleaned.slice(0, -1) : cleaned;
2719
- const normalized = withoutTrailingWildcard.replace(/\/+$/g, "") || "/";
2720
- if (normalized === "/*" || normalized === "/**") return [];
2721
- return [normalized];
2722
- }
2723
- function collectRouteLiterals(pattern, content, routes) {
2724
- let count = 0;
2725
- for (const match of content.matchAll(pattern)) {
2726
- for (const route of normalizeDetectedRouteLiterals(match[1] ?? "")) {
2727
- routes.add(route);
2728
- count += 1;
2729
- }
2730
- }
2731
- return count;
2732
- }
2733
- function htmlRouteFromFile(file) {
2734
- let withoutExt = file.slice(0, -extname3(file).length).replace(/\\/g, "/");
2735
- if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
2736
- if (withoutExt === "index" || withoutExt === "docs" || withoutExt === "src" || withoutExt === "public" || withoutExt === "dist") {
2737
- return "/";
2738
- }
2739
- return `/${withoutExt.split("/").filter(Boolean).join("/")}` || "/";
2740
- }
2741
- function collectStaticHtmlFiles(dir, projectRoot, files, depth = 0) {
2742
- if (depth > 5 || files.length >= MAX_STATIC_HTML_CANDIDATES) return;
2743
- let entries;
2744
- try {
2745
- entries = readdirSync3(dir).sort();
2746
- } catch {
2747
- return;
2748
- }
2749
- for (const entry of entries) {
2750
- if (files.length >= MAX_STATIC_HTML_CANDIDATES) return;
2751
- if (entry.startsWith(".") || entry === "node_modules") continue;
2752
- const fullPath = join3(dir, entry);
2753
- try {
2754
- const stat = statSync2(fullPath);
2755
- if (stat.isDirectory()) {
2756
- collectStaticHtmlFiles(fullPath, projectRoot, files, depth + 1);
2757
- } else if (stat.isFile() && /\.(?:html|htm)$/i.test(entry)) {
2758
- files.push(relative3(projectRoot, fullPath).replace(/\\/g, "/"));
2759
- }
2760
- } catch {
2761
- }
2762
- }
2763
- }
2764
- function staticHtmlFilePriority(file) {
2765
- const base = file.split("/").pop()?.toLowerCase();
2766
- if (base === "index.html" || base === "index.htm") return 0;
2767
- if (base === "default.html" || base === "default.htm") return 1;
2768
- return 2;
2769
- }
2770
- function scanStaticHtmlRoutes(projectRoot) {
2771
- const routes = /* @__PURE__ */ new Map();
2772
- for (const file of STATIC_HTML_ENTRY_FILES) {
2773
- if (!existsSync3(join3(projectRoot, file))) continue;
2774
- routes.set("/", { path: "/", file, hasLayout: false });
2775
- break;
2776
- }
2777
- for (const dir of STATIC_HTML_ROUTE_DIRS) {
2778
- const fullDir = join3(projectRoot, dir);
2779
- if (!existsSync3(fullDir)) continue;
2780
- const files = [];
2781
- collectStaticHtmlFiles(fullDir, projectRoot, files);
2782
- for (const file of files.sort(
2783
- (a, b) => staticHtmlFilePriority(a) - staticHtmlFilePriority(b) || a.localeCompare(b)
2784
- )) {
2785
- const routePath = htmlRouteFromFile(file);
2786
- if (routes.has(routePath)) continue;
2787
- routes.set(routePath, { path: routePath, file, hasLayout: false });
2788
- }
2789
- }
2790
- return [...routes.values()].slice(0, MAX_REPORT_ROUTES);
2791
- }
2792
- function detectPathnameBranchRoutes(content) {
2793
- const routes = /* @__PURE__ */ new Set();
2794
- const comparison = new RegExp(
2795
- `\\b${ROUTE_VARIABLE_NAMES}\\b\\s*(?:===|!==|==|!=)\\s*["'\`](\\/[^"'\`]+)["'\`]`,
2796
- "g"
2797
- );
2798
- const reversedComparison = new RegExp(
2799
- `["'\`](\\/[^"'\`]+)["'\`]\\s*(?:===|!==|==|!=)\\s*\\b${ROUTE_VARIABLE_NAMES}\\b`,
2800
- "g"
2801
- );
2802
- const strongMatches = collectRouteLiterals(comparison, content, routes) + collectRouteLiterals(reversedComparison, content, routes) + collectRouteLiterals(/\bcase\s+["'`](\/[^"'`]+)["'`]\s*:/g, content, routes);
2803
- const hasPathnameSignal = /\b(?:window\.|document\.)?location\.pathname\b|\bpathname\b/.test(
2804
- content
2805
- );
2806
- if (strongMatches > 0 || hasPathnameSignal) {
2807
- collectRouteLiterals(/\b(?:href|to)\s*=\s*["'`](\/[^"'`]+)["'`]/g, content, routes);
2808
- }
2809
- return [...routes];
2810
- }
2811
- function detectDeclarativeRouteSpecRoutes(content) {
2812
- const routes = /* @__PURE__ */ new Set();
2813
- const hasRouteSpecSignal = content.includes("@wasp.sh/spec") || /\b(?:const|export\s+const)\s+\w*Spec\b/.test(content) || /\bapp\s*\(\s*\{[\s\S]*\bspec\s*:/.test(content);
2814
- if (!hasRouteSpecSignal) return [];
2815
- collectRouteLiterals(
2816
- /\broute\s*\(\s*["'`][^"'`]*["'`]\s*,\s*["'`](\/[^"'`]*)["'`]\s*,\s*page\s*\(/g,
2817
- content,
2818
- routes
2819
- );
2820
- return [...routes];
2821
- }
2822
- function scanRoutes(projectRoot, detection) {
2823
- const appRoutes = ["src/app", "app"].flatMap(
2824
- (dir) => existsSync3(join3(projectRoot, dir)) ? walkNextAppRoutes(join3(projectRoot, dir), projectRoot, []) : []
2825
- );
2826
- const pagesRoutes = ["src/pages", "pages"].flatMap((dir) => scanFileRoutes(projectRoot, dir));
2827
- if (detection.framework === "nextjs") {
2828
- if (appRoutes.length > 0 && pagesRoutes.length > 0)
2829
- return { strategy: "mixed-next-router", routes: [...appRoutes, ...pagesRoutes] };
2830
- if (appRoutes.length > 0) return { strategy: "app-router", routes: appRoutes };
2831
- if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
2832
- }
2833
- if (detection.framework === "svelte") {
2834
- const routes = scanFileRoutes(projectRoot, "src/routes", /* @__PURE__ */ new Set([".svelte", ".ts", ".js"]));
2835
- if (routes.length > 0) return { strategy: "sveltekit-router", routes };
2836
- }
2837
- if (detection.framework === "nuxt") {
2838
- const routes = ["pages", "app/pages"].flatMap(
2839
- (dir) => scanFileRoutes(projectRoot, dir, /* @__PURE__ */ new Set([".vue"]))
2840
- );
2841
- if (routes.length > 0) return { strategy: "nuxt-router", routes };
2842
- }
2843
- if (detection.framework === "vue") {
2844
- const router = scanReactRouter(projectRoot);
2845
- if (router.routes.length > 0) return { strategy: "vue-router", routes: router.routes };
2846
- }
2847
- const reactRouter = scanReactRouter(projectRoot);
2848
- if (reactRouter.routes.length > 0)
2849
- return { strategy: "react-router", routes: reactRouter.routes };
2850
- if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
2851
- const staticHtmlRoutes = scanStaticHtmlRoutes(projectRoot);
2852
- if (staticHtmlRoutes.length > 0) return { strategy: "static-html", routes: staticHtmlRoutes };
2853
- return { strategy: "none", routes: [] };
2854
- }
2855
- function countComponents(projectRoot, routes) {
2856
- const sourceFiles = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
2857
- const componentFiles = sourceFiles.filter((file) => {
2858
- const base = file.split("/").pop() ?? file;
2859
- return /^[A-Z][\w-]*\.(tsx|ts|jsx|js|vue|svelte)$/.test(base);
2860
- });
2861
- const directories = [
2862
- ...new Set(
2863
- componentFiles.map((file) => file.split("/").slice(0, -1).join("/")).filter(Boolean)
2864
- )
2865
- ].slice(0, 16);
2866
- return {
2867
- pageCount: routes.routes.length,
2868
- componentCount: componentFiles.length,
2869
- directories
2870
- };
2871
- }
2872
- function extractCssEvidence(content) {
2873
- const variableMatches = [...content.matchAll(/--[\w-]+\s*:/g)];
2874
- const colorMatches = [...content.matchAll(/#[0-9a-fA-F]{3,8}\b|rgb[a]?\(|hsl[a]?\(/g)];
2875
- const themeSignals = /* @__PURE__ */ new Set();
2876
- if (/\bdark\b|color-scheme:\s*dark|\[data-theme=['"]dark['"]|\.dark\b/.test(content))
2877
- themeSignals.add("dark mode");
2878
- if (/\[data-theme=|data-theme|theme-\w+/.test(content)) themeSignals.add("theme selector");
2879
- if (/prefers-color-scheme/.test(content)) themeSignals.add("system color preference");
2880
- return {
2881
- variables: variableMatches.length,
2882
- colors: colorMatches.length,
2883
- dark: themeSignals.has("dark mode") || themeSignals.has("system color preference"),
2884
- themeSignals: [...themeSignals]
2885
- };
2886
- }
2887
- function scanStyling(projectRoot, detection) {
2888
- const deps = detection.dependencies;
2889
- const cssFiles = walkFiles(projectRoot, { extensions: STYLE_EXTENSIONS });
2890
- const themeSignals = /* @__PURE__ */ new Set();
2891
- let cssVariableCount = 0;
2892
- let colorTokenCount = 0;
2893
- let darkMode = false;
2894
- let configFile = null;
2895
- let approach = "unknown";
2896
- const tailwindConfig = [
2897
- "tailwind.config.js",
2898
- "tailwind.config.ts",
2899
- "tailwind.config.mjs",
2900
- "tailwind.config.cjs"
2901
- ].find((file) => existsSync3(join3(projectRoot, file)));
2902
- if (tailwindConfig || deps.tailwindcss) {
2903
- approach = "tailwind";
2904
- configFile = tailwindConfig ?? "package.json";
2905
- } else if (deps["@decantr/css"] || existsSync3(join3(projectRoot, "src/styles/tokens.css")) && existsSync3(join3(projectRoot, "src/styles/treatments.css"))) {
2906
- approach = "decantr-css";
2907
- configFile = deps["@decantr/css"] ? "package.json" : "src/styles/tokens.css";
2908
- } else if (deps.bootstrap) {
2909
- approach = "bootstrap";
2910
- configFile = "package.json";
2911
- } else if (deps["@mui/material"]) {
2912
- approach = "mui";
2913
- configFile = "package.json";
2914
- } else if (deps["@chakra-ui/react"]) {
2915
- approach = "chakra";
2916
- configFile = "package.json";
2917
- } else if (cssFiles.some((file) => file.endsWith(".module.css"))) {
2918
- approach = "css-modules";
2919
- } else if (cssFiles.length > 0) {
2920
- approach = "css";
2921
- }
2922
- for (const file of cssFiles.slice(0, 80)) {
2923
- const content = readTextFile(join3(projectRoot, file), 256 * 1024);
2924
- if (!content) continue;
2925
- const evidence = extractCssEvidence(content);
2926
- cssVariableCount += evidence.variables;
2927
- colorTokenCount += evidence.colors;
2928
- darkMode ||= evidence.dark;
2929
- for (const signal of evidence.themeSignals) themeSignals.add(signal);
2930
- }
2931
- return {
2932
- approach,
2933
- configFile,
2934
- cssVariableCount,
2935
- colorTokenCount,
2936
- darkMode,
2937
- themeSignals: [...themeSignals]
2938
- };
2939
- }
2940
- function findAssistantRules(projectRoot) {
2941
- return RULE_FILES.filter((file) => existsSync3(join3(projectRoot, file)));
2942
- }
2943
3299
  function scanStaticHosting(projectRoot, detection) {
2944
- const packageRead = readPackageJson(projectRoot);
3300
+ const packageRead = readPackageJson2(projectRoot);
2945
3301
  const pkg = packageRead.value;
2946
3302
  const evidence = [];
2947
3303
  const homepageUrl = typeof pkg?.homepage === "string" ? pkg.homepage : null;
@@ -2955,27 +3311,27 @@ function scanStaticHosting(projectRoot, detection) {
2955
3311
  if (detection.dependencies["gh-pages"] || pkg?.scripts?.deploy?.includes("gh-pages")) {
2956
3312
  evidence.push("package scripts or dependencies reference gh-pages");
2957
3313
  }
2958
- if (existsSync3(join3(projectRoot, "docs", "index.html")))
3314
+ if (existsSync4(join4(projectRoot, "docs", "index.html")))
2959
3315
  evidence.push("docs/index.html can serve GitHub Pages");
2960
- if (existsSync3(join3(projectRoot, "404.html")) || existsSync3(join3(projectRoot, "docs", "404.html"))) {
3316
+ if (existsSync4(join4(projectRoot, "404.html")) || existsSync4(join4(projectRoot, "docs", "404.html"))) {
2961
3317
  evidence.push("404.html fallback is present");
2962
3318
  }
2963
- const workflowDir = join3(projectRoot, ".github", "workflows");
2964
- if (existsSync3(workflowDir)) {
2965
- for (const file of walkFiles(workflowDir, { extensions: /* @__PURE__ */ new Set([".yml", ".yaml"]) }).slice(
3319
+ const workflowDir = join4(projectRoot, ".github", "workflows");
3320
+ if (existsSync4(workflowDir)) {
3321
+ for (const file of walkFiles2(workflowDir, { extensions: /* @__PURE__ */ new Set([".yml", ".yaml"]) }).slice(
2966
3322
  0,
2967
3323
  20
2968
3324
  )) {
2969
- const content = readTextFile(join3(workflowDir, file));
3325
+ const content = readTextFile2(join4(workflowDir, file));
2970
3326
  if (content && /pages|gh-pages|upload-pages-artifact|deploy-pages/i.test(content)) {
2971
3327
  evidence.push(`GitHub Pages workflow hint: .github/workflows/${file}`);
2972
3328
  break;
2973
3329
  }
2974
3330
  }
2975
3331
  }
2976
- const sourceFiles = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
3332
+ const sourceFiles = walkFiles2(projectRoot, { extensions: SOURCE_EXTENSIONS2 });
2977
3333
  for (const file of sourceFiles.slice(0, 200)) {
2978
- const content = readTextFile(join3(projectRoot, file), 128 * 1024);
3334
+ const content = readTextFile2(join4(projectRoot, file), 128 * 1024);
2979
3335
  if (!content) continue;
2980
3336
  if (content.includes("HashRouter") || content.includes("createHashRouter")) {
2981
3337
  hashRouting = true;
@@ -2984,7 +3340,7 @@ function scanStaticHosting(projectRoot, detection) {
2984
3340
  }
2985
3341
  }
2986
3342
  for (const config of ["vite.config.ts", "vite.config.js", "vite.config.mjs"]) {
2987
- const content = readTextFile(join3(projectRoot, config), 128 * 1024);
3343
+ const content = readTextFile2(join4(projectRoot, config), 128 * 1024);
2988
3344
  const baseMatch = content?.match(/\bbase\s*:\s*['"]([^'"]+)['"]/);
2989
3345
  if (baseMatch?.[1]) {
2990
3346
  basePath = baseMatch[1];
@@ -3029,33 +3385,6 @@ function buildApplicability(detection, routes, components) {
3029
3385
  reasons: ["A web framework was detected, but route/component evidence is thin."]
3030
3386
  };
3031
3387
  }
3032
- function buildConfidence(applicability, detection, routes, styling) {
3033
- let score = 20;
3034
- const reasons = [];
3035
- if (detection.packageJsonPresent && detection.packageJsonValid) {
3036
- score += 15;
3037
- reasons.push("package.json was readable");
3038
- }
3039
- if (WEB_FRAMEWORKS.has(detection.framework)) {
3040
- score += 25;
3041
- reasons.push(`${detection.framework} framework signal found`);
3042
- }
3043
- if (routes.routes.length > 0) {
3044
- score += 20;
3045
- reasons.push(`${routes.routes.length} route signal(s) found`);
3046
- }
3047
- if (styling.approach !== "unknown") {
3048
- score += 10;
3049
- reasons.push(`${styling.approach} styling signal found`);
3050
- }
3051
- if (applicability.status === "not_applicable") score = Math.min(score, 35);
3052
- const clamped = Math.max(5, Math.min(98, score));
3053
- return {
3054
- score: clamped,
3055
- level: clamped >= 75 ? "high" : clamped >= 45 ? "medium" : "low",
3056
- reasons: reasons.length > 0 ? reasons : ["Only weak project signals were found."]
3057
- };
3058
- }
3059
3388
  function buildFindings(input) {
3060
3389
  const findings = [];
3061
3390
  const { detection, routes, styling, hosting, assistantRules, applicability, pagesProbe } = input;
@@ -3174,22 +3503,89 @@ function buildCommands(applicability) {
3174
3503
  }
3175
3504
  return commands;
3176
3505
  }
3506
+ function routeFromDiscovery(route) {
3507
+ return {
3508
+ path: route.path,
3509
+ file: route.file,
3510
+ hasLayout: route.hasLayout
3511
+ };
3512
+ }
3513
+ function routeSignalFromDiscovery(signal) {
3514
+ return {
3515
+ path: signal.path,
3516
+ file: signal.file,
3517
+ kind: signal.kind,
3518
+ confidence: signal.confidence,
3519
+ taskable: signal.taskable,
3520
+ evidence: signal.evidence
3521
+ };
3522
+ }
3523
+ function componentFromDiscovery(component) {
3524
+ return {
3525
+ name: component.name,
3526
+ file: component.file,
3527
+ kind: component.kind,
3528
+ confidence: component.confidence
3529
+ };
3530
+ }
3531
+ function detectionFromDiscovery(discovery) {
3532
+ return {
3533
+ framework: discovery.project.framework,
3534
+ frameworkVersion: discovery.project.frameworkVersion,
3535
+ packageManager: discovery.project.packageManager,
3536
+ primaryLanguage: discovery.project.primaryLanguage,
3537
+ hasTypeScript: discovery.project.hasTypeScript,
3538
+ hasTailwind: discovery.project.hasTailwind,
3539
+ hasDecantr: discovery.project.hasDecantr,
3540
+ packageName: discovery.project.packageName,
3541
+ packageJsonValid: discovery.project.packageJsonValid,
3542
+ packageJsonPresent: discovery.project.packageJsonPresent,
3543
+ dependencies: discovery.project.dependencies
3544
+ };
3545
+ }
3546
+ function scanDiscoverySummary(discovery) {
3547
+ return {
3548
+ schemaVersion: "discovery.v1",
3549
+ workspace: {
3550
+ projectPath: discovery.workspace.projectPath,
3551
+ scope: discovery.workspace.scope
3552
+ },
3553
+ projectEvidence: discovery.project.evidence,
3554
+ routeSignalCount: discovery.routes.routeSignalCount,
3555
+ taskableRouteCount: discovery.routes.taskableRouteCount,
3556
+ routeConfidence: discovery.routes.confidence,
3557
+ componentConfidence: discovery.components.confidence,
3558
+ componentEvidence: discovery.components.evidence,
3559
+ limitations: discovery.limitations
3560
+ };
3561
+ }
3177
3562
  async function scanProject(projectRoot, options = {}) {
3178
- const detection = detectProject(projectRoot);
3179
- const routes = scanRoutes(projectRoot, detection);
3180
- routes.routes = routes.routes.slice(0, MAX_REPORT_ROUTES);
3181
- const components = countComponents(projectRoot, routes);
3182
- const styling = scanStyling(projectRoot, detection);
3183
- const assistantRules = findAssistantRules(projectRoot);
3563
+ const discovery = discoverProject(projectRoot);
3564
+ const detection = detectionFromDiscovery(discovery);
3565
+ const routes = {
3566
+ strategy: discovery.routes.strategy,
3567
+ routes: discovery.routes.taskableRoutes.map(routeFromDiscovery).slice(0, MAX_REPORT_ROUTES2)
3568
+ };
3569
+ const components = {
3570
+ pageCount: discovery.components.pageCount,
3571
+ componentCount: discovery.components.componentCount,
3572
+ directories: discovery.components.directories,
3573
+ confidence: discovery.components.confidence,
3574
+ evidence: discovery.components.evidence,
3575
+ items: discovery.components.items.map(componentFromDiscovery),
3576
+ limitations: discovery.components.limitations
3577
+ };
3578
+ const styling = discovery.styling;
3579
+ const assistantRules = discovery.assistant.ruleFiles;
3184
3580
  const staticHosting = scanStaticHosting(projectRoot, detection);
3185
3581
  const pagesProbe = options.pagesProbe ?? null;
3186
3582
  const applicability = buildApplicability(detection, routes, components);
3187
- const confidence = buildConfidence(applicability, detection, routes, styling);
3583
+ const confidence = discovery.confidence;
3188
3584
  const rawInput = options.input ?? { kind: "local", value: "." };
3189
3585
  const input = rawInput.kind === "local" && isAbsolute(rawInput.value) ? { ...rawInput, value: "." } : rawInput;
3190
3586
  return {
3191
- $schema: SCAN_REPORT_SCHEMA_URL,
3192
- schemaVersion: "scan-report.v1",
3587
+ $schema: SCAN_REPORT_V2_SCHEMA_URL,
3588
+ schemaVersion: "scan-report.v2",
3193
3589
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3194
3590
  input,
3195
3591
  source: {
@@ -3206,12 +3602,19 @@ async function scanProject(projectRoot, options = {}) {
3206
3602
  hasTypeScript: detection.hasTypeScript,
3207
3603
  hasTailwind: detection.hasTailwind,
3208
3604
  hasDecantr: detection.hasDecantr,
3209
- packageName: detection.packageName
3605
+ packageName: detection.packageName,
3606
+ evidence: discovery.project.evidence,
3607
+ workspaceScope: discovery.workspace.scope,
3608
+ projectPath: discovery.workspace.projectPath
3210
3609
  },
3211
3610
  routes: {
3212
3611
  strategy: routes.strategy,
3213
3612
  count: routes.routes.length,
3214
- items: routes.routes
3613
+ items: routes.routes,
3614
+ routeSignalCount: discovery.routes.routeSignalCount,
3615
+ taskableRouteCount: discovery.routes.taskableRouteCount,
3616
+ confidence: discovery.routes.confidence,
3617
+ signals: discovery.routes.routeSignals.map(routeSignalFromDiscovery)
3215
3618
  },
3216
3619
  components,
3217
3620
  styling,
@@ -3220,6 +3623,7 @@ async function scanProject(projectRoot, options = {}) {
3220
3623
  ruleFiles: assistantRules
3221
3624
  },
3222
3625
  pagesProbe,
3626
+ discovery: scanDiscoverySummary(discovery),
3223
3627
  findings: buildFindings({
3224
3628
  detection,
3225
3629
  routes,
@@ -3234,7 +3638,7 @@ async function scanProject(projectRoot, options = {}) {
3234
3638
  sourceUploaded: input.kind !== "local",
3235
3639
  persistedByDecantr: false,
3236
3640
  notes: [
3237
- "V1 scans are read-only and do not install dependencies, build projects, execute scripts, or open pull requests.",
3641
+ "V2 scans are read-only and do not install dependencies, build projects, execute scripts, or open pull requests.",
3238
3642
  "Hosted scans use temporary public-repo checkouts and return an ephemeral report."
3239
3643
  ]
3240
3644
  }
@@ -3242,8 +3646,8 @@ async function scanProject(projectRoot, options = {}) {
3242
3646
  }
3243
3647
  function createUnavailableScanReport(input) {
3244
3648
  return {
3245
- $schema: SCAN_REPORT_SCHEMA_URL,
3246
- schemaVersion: "scan-report.v1",
3649
+ $schema: SCAN_REPORT_V2_SCHEMA_URL,
3650
+ schemaVersion: "scan-report.v2",
3247
3651
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3248
3652
  input: input.scanInput,
3249
3653
  source: {
@@ -3268,10 +3672,29 @@ function createUnavailableScanReport(input) {
3268
3672
  hasTypeScript: false,
3269
3673
  hasTailwind: false,
3270
3674
  hasDecantr: false,
3271
- packageName: null
3675
+ packageName: null,
3676
+ evidence: [],
3677
+ workspaceScope: "single-app",
3678
+ projectPath: "."
3679
+ },
3680
+ routes: {
3681
+ strategy: "none",
3682
+ count: 0,
3683
+ items: [],
3684
+ routeSignalCount: 0,
3685
+ taskableRouteCount: 0,
3686
+ confidence: "low",
3687
+ signals: []
3688
+ },
3689
+ components: {
3690
+ pageCount: 0,
3691
+ componentCount: 0,
3692
+ directories: [],
3693
+ confidence: "low",
3694
+ evidence: [],
3695
+ items: [],
3696
+ limitations: ["Source could not be inspected."]
3272
3697
  },
3273
- routes: { strategy: "none", count: 0, items: [] },
3274
- components: { pageCount: 0, componentCount: 0, directories: [] },
3275
3698
  styling: {
3276
3699
  approach: "unknown",
3277
3700
  configFile: null,
@@ -3289,6 +3712,20 @@ function createUnavailableScanReport(input) {
3289
3712
  },
3290
3713
  assistant: { ruleFiles: [] },
3291
3714
  pagesProbe: input.pagesProbe ?? null,
3715
+ discovery: {
3716
+ schemaVersion: "discovery.v1",
3717
+ workspace: {
3718
+ projectPath: ".",
3719
+ scope: "single-app"
3720
+ },
3721
+ projectEvidence: [],
3722
+ routeSignalCount: 0,
3723
+ taskableRouteCount: 0,
3724
+ routeConfidence: "low",
3725
+ componentConfidence: "low",
3726
+ componentEvidence: [],
3727
+ limitations: ["Source could not be inspected."]
3728
+ },
3292
3729
  findings: [
3293
3730
  {
3294
3731
  id: "source-unavailable",
@@ -3333,7 +3770,7 @@ async function probePublishedSite(url, options = {}) {
3333
3770
  "User-Agent": "Decantr-Scan/1.0 (+https://decantr.ai/scan)"
3334
3771
  }
3335
3772
  });
3336
- const text = (await response.text()).slice(0, MAX_FILE_READ_BYTES);
3773
+ const text = (await response.text()).slice(0, MAX_FILE_READ_BYTES2);
3337
3774
  const titleMatch = text.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
3338
3775
  const descriptionTag = text.match(/<meta\b[^>]*(?:name|property)=["']description["'][^>]*>/i)?.[0] ?? null;
3339
3776
  const canonicalTag = text.match(/<link\b[^>]*rel=["']canonical["'][^>]*>/i)?.[0] ?? null;
@@ -3434,8 +3871,8 @@ function resolveGitHubScanInput(input) {
3434
3871
  import * as ts4 from "typescript";
3435
3872
 
3436
3873
  // src/source/inventory.ts
3437
- import { existsSync as existsSync4, readdirSync as readdirSync4, realpathSync, statSync as statSync3 } from "fs";
3438
- import { extname as extname4, isAbsolute as isAbsolute2, join as join4, relative as relative4, resolve } from "path";
3874
+ import { existsSync as existsSync5, readdirSync as readdirSync5, realpathSync, statSync as statSync4 } from "fs";
3875
+ import { extname as extname5, isAbsolute as isAbsolute2, join as join5, relative as relative5, resolve as resolve2 } from "path";
3439
3876
  import * as ts3 from "typescript";
3440
3877
  var DEFAULT_MAX_FILES = 5e3;
3441
3878
  var DEFAULT_MAX_FILE_SIZE_BYTES = 512 * 1024;
@@ -3466,7 +3903,7 @@ function normalizeSourcePath(path) {
3466
3903
  return path.replace(/\\/g, "/");
3467
3904
  }
3468
3905
  function isPathInsideProject(projectRoot, absolutePath) {
3469
- const normalizedRelative = normalizeSourcePath(relative4(projectRoot, absolutePath));
3906
+ const normalizedRelative = normalizeSourcePath(relative5(projectRoot, absolutePath));
3470
3907
  return normalizedRelative === "" || !normalizedRelative.startsWith("../") && normalizedRelative !== "..";
3471
3908
  }
3472
3909
  function isSupportedSourceExtension(extension, extensions) {
@@ -3474,7 +3911,7 @@ function isSupportedSourceExtension(extension, extensions) {
3474
3911
  return extensions ? extensions.map((entry) => entry.toLowerCase()).includes(normalized) : DEFAULT_EXTENSIONS.has(normalized);
3475
3912
  }
3476
3913
  function sourceKindFromPath(path) {
3477
- const extension = extname4(path).toLowerCase();
3914
+ const extension = extname5(path).toLowerCase();
3478
3915
  switch (extension) {
3479
3916
  case ".ts":
3480
3917
  return "ts";
@@ -3538,12 +3975,12 @@ function shouldSkipFile(relativePath, options) {
3538
3975
  return false;
3539
3976
  }
3540
3977
  function inventoryFile(projectRoot, absolutePath) {
3541
- const extension = extname4(absolutePath).toLowerCase();
3978
+ const extension = extname5(absolutePath).toLowerCase();
3542
3979
  const kind = sourceKindFromPath(absolutePath);
3543
3980
  const language = sourceLanguageFromPath(absolutePath);
3544
3981
  if (!kind || !language) return null;
3545
- const relativePath = normalizeSourcePath(relative4(projectRoot, absolutePath) || absolutePath);
3546
- const sizeBytes = statSync3(absolutePath).size;
3982
+ const relativePath = normalizeSourcePath(relative5(projectRoot, absolutePath) || absolutePath);
3983
+ const sizeBytes = statSync4(absolutePath).size;
3547
3984
  return {
3548
3985
  absolutePath,
3549
3986
  relativePath,
@@ -3562,19 +3999,19 @@ function compareSkipped(a, b) {
3562
3999
  return a.path.localeCompare(b.path) || a.reason.localeCompare(b.reason);
3563
4000
  }
3564
4001
  function createSourceInventory(projectRoot, options = {}) {
3565
- const root = realpathSync(resolve(projectRoot));
4002
+ const root = realpathSync(resolve2(projectRoot));
3566
4003
  const files = [];
3567
4004
  const seenFiles = /* @__PURE__ */ new Set();
3568
4005
  const skipped = [];
3569
4006
  const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
3570
4007
  const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
3571
4008
  const requestedRoots = options.roots?.length ? [...options.roots] : ["."];
3572
- const roots = requestedRoots.map((entry) => isAbsolute2(entry) ? resolve(entry) : resolve(root, entry)).filter((entry) => existsSync4(entry) && isPathInsideProject(root, entry));
4009
+ const roots = requestedRoots.map((entry) => isAbsolute2(entry) ? resolve2(entry) : resolve2(root, entry)).filter((entry) => existsSync5(entry) && isPathInsideProject(root, entry));
3573
4010
  const addSkipped = (path, reason) => {
3574
- skipped.push({ path: normalizeSourcePath(relative4(root, path) || path), reason });
4011
+ skipped.push({ path: normalizeSourcePath(relative5(root, path) || path), reason });
3575
4012
  };
3576
4013
  const addFile = (absolutePath) => {
3577
- const normalized = normalizeSourcePath(resolve(absolutePath));
4014
+ const normalized = normalizeSourcePath(resolve2(absolutePath));
3578
4015
  if (seenFiles.has(normalized)) return;
3579
4016
  const sourceFile = inventoryFile(root, absolutePath);
3580
4017
  if (!sourceFile) return;
@@ -3586,16 +4023,16 @@ function createSourceInventory(projectRoot, options = {}) {
3586
4023
  addSkipped(directory, "walk-limit");
3587
4024
  return;
3588
4025
  }
3589
- const entries = readdirSync4(directory, { withFileTypes: true }).sort(
4026
+ const entries = readdirSync5(directory, { withFileTypes: true }).sort(
3590
4027
  (a, b) => a.name.localeCompare(b.name)
3591
4028
  );
3592
4029
  for (const entry of entries) {
3593
4030
  if (files.length >= maxFiles) {
3594
- addSkipped(join4(directory, entry.name), "walk-limit");
4031
+ addSkipped(join5(directory, entry.name), "walk-limit");
3595
4032
  continue;
3596
4033
  }
3597
- const absolutePath = join4(directory, entry.name);
3598
- const relativePath = normalizeSourcePath(relative4(root, absolutePath) || absolutePath);
4034
+ const absolutePath = join5(directory, entry.name);
4035
+ const relativePath = normalizeSourcePath(relative5(root, absolutePath) || absolutePath);
3599
4036
  if (entry.isSymbolicLink()) {
3600
4037
  addSkipped(absolutePath, "symlink");
3601
4038
  continue;
@@ -3609,13 +4046,13 @@ function createSourceInventory(projectRoot, options = {}) {
3609
4046
  continue;
3610
4047
  }
3611
4048
  if (!entry.isFile()) continue;
3612
- const extension = extname4(entry.name).toLowerCase();
4049
+ const extension = extname5(entry.name).toLowerCase();
3613
4050
  if (!isSupportedSourceExtension(extension, options.extensions)) continue;
3614
4051
  if (shouldSkipFile(relativePath, options)) {
3615
4052
  addSkipped(absolutePath, "ignored-file");
3616
4053
  continue;
3617
4054
  }
3618
- const stat = statSync3(absolutePath);
4055
+ const stat = statSync4(absolutePath);
3619
4056
  if (stat.size > maxFileSizeBytes) {
3620
4057
  addSkipped(absolutePath, "oversized");
3621
4058
  continue;
@@ -3628,18 +4065,18 @@ function createSourceInventory(projectRoot, options = {}) {
3628
4065
  addSkipped(rootPath, "walk-limit");
3629
4066
  continue;
3630
4067
  }
3631
- const stat = statSync3(rootPath);
4068
+ const stat = statSync4(rootPath);
3632
4069
  if (stat.isDirectory()) {
3633
- const relativeRoot = normalizeSourcePath(relative4(root, rootPath));
4070
+ const relativeRoot = normalizeSourcePath(relative5(root, rootPath));
3634
4071
  if (relativeRoot && shouldSkipDirectory(relativeRoot, options)) {
3635
4072
  addSkipped(rootPath, "ignored-directory");
3636
4073
  continue;
3637
4074
  }
3638
4075
  walk(rootPath);
3639
4076
  } else if (stat.isFile()) {
3640
- const extension = extname4(rootPath).toLowerCase();
4077
+ const extension = extname5(rootPath).toLowerCase();
3641
4078
  if (!isSupportedSourceExtension(extension, options.extensions)) continue;
3642
- const relativeFile = normalizeSourcePath(relative4(root, rootPath));
4079
+ const relativeFile = normalizeSourcePath(relative5(root, rootPath));
3643
4080
  if (shouldSkipFile(relativeFile, options)) {
3644
4081
  addSkipped(rootPath, "ignored-file");
3645
4082
  continue;
@@ -3745,8 +4182,8 @@ function extractSourceStringLiterals(sourceFile, options = {}) {
3745
4182
  }
3746
4183
 
3747
4184
  // src/source/program.ts
3748
- import { existsSync as existsSync5, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
3749
- import { dirname, extname as extname5, isAbsolute as isAbsolute3, join as join5, relative as relative5, resolve as resolve2 } from "path";
4185
+ import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
4186
+ import { dirname as dirname2, extname as extname6, isAbsolute as isAbsolute3, join as join6, relative as relative6, resolve as resolve3 } from "path";
3750
4187
  import * as ts5 from "typescript";
3751
4188
  var RESOLUTION_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
3752
4189
  var DEFAULT_COMPILER_OPTIONS = {
@@ -3766,11 +4203,11 @@ var DEFAULT_COMPILER_OPTIONS = {
3766
4203
  function findProjectTsConfig(projectRoot, tsconfigPath) {
3767
4204
  if (tsconfigPath === null) return null;
3768
4205
  if (tsconfigPath) {
3769
- const absolutePath = isAbsolute3(tsconfigPath) ? resolve2(tsconfigPath) : resolve2(projectRoot, tsconfigPath);
3770
- return existsSync5(absolutePath) ? absolutePath : null;
4206
+ const absolutePath = isAbsolute3(tsconfigPath) ? resolve3(tsconfigPath) : resolve3(projectRoot, tsconfigPath);
4207
+ return existsSync6(absolutePath) ? absolutePath : null;
3771
4208
  }
3772
- const localTsConfig = join5(projectRoot, "tsconfig.json");
3773
- return existsSync5(localTsConfig) ? localTsConfig : null;
4209
+ const localTsConfig = join6(projectRoot, "tsconfig.json");
4210
+ return existsSync6(localTsConfig) ? localTsConfig : null;
3774
4211
  }
3775
4212
  function fallbackCompilerConfig(tsconfigPath, options, diagnostics = []) {
3776
4213
  return {
@@ -3790,13 +4227,13 @@ function readCompilerConfig(projectRoot, options) {
3790
4227
  if (!tsconfigPath) return fallbackCompilerConfig(null, options);
3791
4228
  const parsedJson = ts5.parseConfigFileTextToJson(
3792
4229
  tsconfigPath,
3793
- readFileSync5(tsconfigPath, "utf-8")
4230
+ readFileSync6(tsconfigPath, "utf-8")
3794
4231
  );
3795
4232
  if (parsedJson.error) return fallbackCompilerConfig(tsconfigPath, options, [parsedJson.error]);
3796
4233
  const parsed = ts5.parseJsonConfigFileContent(
3797
4234
  parsedJson.config,
3798
4235
  ts5.sys,
3799
- dirname(tsconfigPath),
4236
+ dirname2(tsconfigPath),
3800
4237
  DEFAULT_COMPILER_OPTIONS,
3801
4238
  tsconfigPath
3802
4239
  );
@@ -3815,7 +4252,7 @@ function readCompilerConfig(projectRoot, options) {
3815
4252
  }
3816
4253
  function inventoryFileByAbsolutePath(inventory) {
3817
4254
  return new Map(
3818
- inventory.files.map((file) => [normalizeSourcePath(resolve2(file.absolutePath)), file])
4255
+ inventory.files.map((file) => [normalizeSourcePath(resolve3(file.absolutePath)), file])
3819
4256
  );
3820
4257
  }
3821
4258
  function createProjectSourceProgram(projectRoot, options = {}) {
@@ -3838,13 +4275,13 @@ function createProjectSourceProgram(projectRoot, options = {}) {
3838
4275
  }
3839
4276
  function getProjectSourceFile(context, pathOrSourceFile) {
3840
4277
  if (typeof pathOrSourceFile !== "string") return pathOrSourceFile;
3841
- const absolutePath = isAbsolute3(pathOrSourceFile) ? resolve2(pathOrSourceFile) : resolve2(context.projectRoot, pathOrSourceFile);
4278
+ const absolutePath = isAbsolute3(pathOrSourceFile) ? resolve3(pathOrSourceFile) : resolve3(context.projectRoot, pathOrSourceFile);
3842
4279
  const normalized = normalizeSourcePath(absolutePath);
3843
- return context.program.getSourceFiles().find((sourceFile) => normalizeSourcePath(resolve2(sourceFile.fileName)) === normalized);
4280
+ return context.program.getSourceFiles().find((sourceFile) => normalizeSourcePath(resolve3(sourceFile.fileName)) === normalized);
3844
4281
  }
3845
4282
  function sourceFileRelativePath(context, sourceFile) {
3846
4283
  return normalizeSourcePath(
3847
- relative5(context.projectRoot, sourceFile.fileName) || sourceFile.fileName
4284
+ relative6(context.projectRoot, sourceFile.fileName) || sourceFile.fileName
3848
4285
  );
3849
4286
  }
3850
4287
  function sourceLocationLineAndColumn(sourceFile, node) {
@@ -3880,32 +4317,32 @@ function manualResolutionCandidates(context, importerPath, specifier) {
3880
4317
  const candidates = [];
3881
4318
  const addCandidates = (base) => {
3882
4319
  candidates.push(base);
3883
- if (extname5(base)) return;
4320
+ if (extname6(base)) return;
3884
4321
  for (const extension of RESOLUTION_EXTENSIONS) candidates.push(`${base}${extension}`);
3885
4322
  for (const extension of RESOLUTION_EXTENSIONS) {
3886
- candidates.push(join5(base, `index${extension}`));
4323
+ candidates.push(join6(base, `index${extension}`));
3887
4324
  }
3888
4325
  };
3889
4326
  if (specifier.startsWith(".") || specifier.startsWith("/")) {
3890
- const base = specifier.startsWith("/") ? resolve2(context.projectRoot, specifier.slice(1)) : resolve2(dirname(importerPath), specifier);
4327
+ const base = specifier.startsWith("/") ? resolve3(context.projectRoot, specifier.slice(1)) : resolve3(dirname2(importerPath), specifier);
3891
4328
  addCandidates(base);
3892
4329
  } else if (specifier.startsWith("@/")) {
3893
- addCandidates(resolve2(context.projectRoot, specifier.slice(2)));
3894
- addCandidates(resolve2(context.projectRoot, "src", specifier.slice(2)));
4330
+ addCandidates(resolve3(context.projectRoot, specifier.slice(2)));
4331
+ addCandidates(resolve3(context.projectRoot, "src", specifier.slice(2)));
3895
4332
  }
3896
4333
  return candidates;
3897
4334
  }
3898
4335
  function resolveFromInventory(context, source, importerPath) {
3899
4336
  const inventoryByPath = inventoryFileByAbsolutePath(context.inventory);
3900
4337
  for (const candidate of manualResolutionCandidates(context, importerPath, source)) {
3901
- const absolutePath = normalizeSourcePath(resolve2(candidate));
4338
+ const absolutePath = normalizeSourcePath(resolve3(candidate));
3902
4339
  const inventoryFile2 = inventoryByPath.get(absolutePath);
3903
- if (!inventoryFile2 || !existsSync5(inventoryFile2.absolutePath) || !statSync4(inventoryFile2.absolutePath).isFile()) {
4340
+ if (!inventoryFile2 || !existsSync6(inventoryFile2.absolutePath) || !statSync5(inventoryFile2.absolutePath).isFile()) {
3904
4341
  continue;
3905
4342
  }
3906
4343
  return {
3907
4344
  source,
3908
- importer: normalizeSourcePath(relative5(context.projectRoot, importerPath) || importerPath),
4345
+ importer: normalizeSourcePath(relative6(context.projectRoot, importerPath) || importerPath),
3909
4346
  kind: "project-local",
3910
4347
  resolvedFileName: inventoryFile2.absolutePath,
3911
4348
  relativePath: inventoryFile2.relativePath,
@@ -3919,9 +4356,9 @@ function resolveFromInventory(context, source, importerPath) {
3919
4356
  }
3920
4357
  function resolveSourceImport(context, importer, source) {
3921
4358
  const sourceFile = getProjectSourceFile(context, importer);
3922
- const importerPath = sourceFile ? resolve2(sourceFile.fileName) : isAbsolute3(String(importer)) ? resolve2(String(importer)) : resolve2(context.projectRoot, String(importer));
4359
+ const importerPath = sourceFile ? resolve3(sourceFile.fileName) : isAbsolute3(String(importer)) ? resolve3(String(importer)) : resolve3(context.projectRoot, String(importer));
3923
4360
  const importerRelativePath = normalizeSourcePath(
3924
- relative5(context.projectRoot, importerPath) || importerPath
4361
+ relative6(context.projectRoot, importerPath) || importerPath
3925
4362
  );
3926
4363
  const inventoryByPath = inventoryFileByAbsolutePath(context.inventory);
3927
4364
  const resolved = ts5.resolveModuleName(
@@ -3931,7 +4368,7 @@ function resolveSourceImport(context, importer, source) {
3931
4368
  ts5.sys
3932
4369
  ).resolvedModule;
3933
4370
  if (resolved) {
3934
- const absolutePath = normalizeSourcePath(resolve2(resolved.resolvedFileName));
4371
+ const absolutePath = normalizeSourcePath(resolve3(resolved.resolvedFileName));
3935
4372
  const inventoryFile2 = inventoryByPath.get(absolutePath);
3936
4373
  if (inventoryFile2 && isPathInsideProject(context.projectRoot, absolutePath)) {
3937
4374
  return {
@@ -3952,7 +4389,7 @@ function resolveSourceImport(context, importer, source) {
3952
4389
  kind: hasExternalPackageShape(source) ? "external" : "unresolved",
3953
4390
  resolvedFileName: resolved.resolvedFileName,
3954
4391
  relativePath: null,
3955
- extension: extname5(resolved.resolvedFileName).toLowerCase() || null,
4392
+ extension: extname6(resolved.resolvedFileName).toLowerCase() || null,
3956
4393
  isProjectLocal: false,
3957
4394
  isExternal: hasExternalPackageShape(source),
3958
4395
  failed: !hasExternalPackageShape(source)
@@ -4120,10 +4557,10 @@ function resolveSourceSymbolOrigin(context, pathOrSourceFile, localName, options
4120
4557
  if (declarations.length === 0) return null;
4121
4558
  const projectFiles = inventoryFileByAbsolutePath(context.inventory);
4122
4559
  const declaration = declarations.find(
4123
- (entry) => projectFiles.has(normalizeSourcePath(resolve2(entry.getSourceFile().fileName)))
4560
+ (entry) => projectFiles.has(normalizeSourcePath(resolve3(entry.getSourceFile().fileName)))
4124
4561
  ) ?? declarations[0];
4125
4562
  const declarationFile = declaration.getSourceFile();
4126
- const declarationPath = normalizeSourcePath(resolve2(declarationFile.fileName));
4563
+ const declarationPath = normalizeSourcePath(resolve3(declarationFile.fileName));
4127
4564
  const projectFile = projectFiles.get(declarationPath);
4128
4565
  const isProjectLocal = Boolean(projectFile);
4129
4566
  if (!isProjectLocal && !options.includeExternal) return null;
@@ -4147,7 +4584,7 @@ var VERIFICATION_SCHEMA_URLS = {
4147
4584
  projectHealth: PROJECT_HEALTH_REPORT_V2_SCHEMA_URL,
4148
4585
  decantrCi: DECANTR_CI_REPORT_V2_SCHEMA_URL,
4149
4586
  evidenceBundle: EVIDENCE_BUNDLE_V2_SCHEMA_URL,
4150
- scanReport: "https://decantr.ai/schemas/scan-report.v1.json",
4587
+ scanReport: "https://decantr.ai/schemas/scan-report.v2.json",
4151
4588
  workspaceHealth: WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL,
4152
4589
  fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
4153
4590
  showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
@@ -4157,16 +4594,16 @@ function hashString(value) {
4157
4594
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
4158
4595
  }
4159
4596
  function hashFile(path) {
4160
- if (!existsSync6(path)) return null;
4597
+ if (!existsSync7(path)) return null;
4161
4598
  try {
4162
- return createHash("sha256").update(readFileSync6(path)).digest("hex");
4599
+ return createHash("sha256").update(readFileSync7(path)).digest("hex");
4163
4600
  } catch {
4164
4601
  return null;
4165
4602
  }
4166
4603
  }
4167
4604
  function provenanceEntry(projectRoot, relativePath) {
4168
- const path = join6(projectRoot, relativePath);
4169
- if (!existsSync6(path)) {
4605
+ const path = join7(projectRoot, relativePath);
4606
+ if (!existsSync7(path)) {
4170
4607
  return {
4171
4608
  path: relativePath,
4172
4609
  present: false,
@@ -4176,7 +4613,7 @@ function provenanceEntry(projectRoot, relativePath) {
4176
4613
  }
4177
4614
  let generatedAt = null;
4178
4615
  try {
4179
- generatedAt = statSync5(path).mtime.toISOString();
4616
+ generatedAt = statSync6(path).mtime.toISOString();
4180
4617
  } catch {
4181
4618
  generatedAt = null;
4182
4619
  }
@@ -4188,7 +4625,7 @@ function provenanceEntry(projectRoot, relativePath) {
4188
4625
  };
4189
4626
  }
4190
4627
  function provenanceForPath(projectRoot, path) {
4191
- const rel = isAbsolute4(path) ? relative6(projectRoot, path).replace(/\\/g, "/") : path;
4628
+ const rel = isAbsolute4(path) ? relative7(projectRoot, path).replace(/\\/g, "/") : path;
4192
4629
  return provenanceEntry(projectRoot, rel && !rel.startsWith("..") ? rel : basename2(path));
4193
4630
  }
4194
4631
  function redactEvidenceText(projectRoot, value) {
@@ -4325,7 +4762,7 @@ function createContractAssertions(projectRoot, audit) {
4325
4762
  const assertions = [];
4326
4763
  const essence = audit?.essence;
4327
4764
  const v4 = essence && isV4(essence) ? essence : null;
4328
- const contextDir = join6(projectRoot, ".decantr", "context");
4765
+ const contextDir = join7(projectRoot, ".decantr", "context");
4329
4766
  const packManifest = audit?.packManifest ?? null;
4330
4767
  assertions.push(
4331
4768
  assertion({
@@ -4335,7 +4772,7 @@ function createContractAssertions(projectRoot, audit) {
4335
4772
  rule: "essence-present",
4336
4773
  status: essence ? "passed" : "failed",
4337
4774
  message: essence ? "Decantr Essence contract is present." : "No Decantr Essence contract was found.",
4338
- evidence: [redactEvidenceText(projectRoot, join6(projectRoot, "decantr.essence.json"))],
4775
+ evidence: [redactEvidenceText(projectRoot, join7(projectRoot, "decantr.essence.json"))],
4339
4776
  suggestedFix: essence ? void 0 : "Run `decantr init` or restore decantr.essence.json."
4340
4777
  })
4341
4778
  );
@@ -4408,7 +4845,7 @@ function createContractAssertions(projectRoot, audit) {
4408
4845
  rule: "pack-manifest-present",
4409
4846
  status: packManifest ? "passed" : "failed",
4410
4847
  message: packManifest ? "Compiled execution pack manifest is present." : "Compiled execution pack manifest is missing.",
4411
- evidence: [redactEvidenceText(projectRoot, join6(contextDir, "pack-manifest.json"))],
4848
+ evidence: [redactEvidenceText(projectRoot, join7(contextDir, "pack-manifest.json"))],
4412
4849
  suggestedFix: packManifest ? void 0 : "Run `decantr registry compile-packs decantr.essence.json --write-context` to hydrate the full context pack bundle."
4413
4850
  })
4414
4851
  );
@@ -4420,21 +4857,21 @@ function createContractAssertions(projectRoot, audit) {
4420
4857
  rule: "review-pack-present",
4421
4858
  status: audit?.reviewPack ? "passed" : "failed",
4422
4859
  message: audit?.reviewPack ? "Compiled review pack is present." : "Compiled review pack is missing.",
4423
- evidence: [redactEvidenceText(projectRoot, join6(contextDir, "review-pack.json"))],
4860
+ evidence: [redactEvidenceText(projectRoot, join7(contextDir, "review-pack.json"))],
4424
4861
  suggestedFix: audit?.reviewPack ? void 0 : "Run `decantr registry compile-packs decantr.essence.json --write-context` so critique has the compiled review contract."
4425
4862
  })
4426
4863
  );
4427
- const tokensPath = join6(projectRoot, "src", "styles", "tokens.css");
4864
+ const tokensPath = join7(projectRoot, "src", "styles", "tokens.css");
4428
4865
  assertions.push(
4429
4866
  assertion({
4430
4867
  id: "contract.design-token.tokens-file",
4431
4868
  category: "design-token",
4432
- severity: existsSync6(tokensPath) ? "info" : "warn",
4869
+ severity: existsSync7(tokensPath) ? "info" : "warn",
4433
4870
  rule: "tokens-file-present",
4434
- status: existsSync6(tokensPath) ? "passed" : "failed",
4435
- message: existsSync6(tokensPath) ? "Decantr token CSS file is present." : "Decantr token CSS file was not found.",
4871
+ status: existsSync7(tokensPath) ? "passed" : "failed",
4872
+ message: existsSync7(tokensPath) ? "Decantr token CSS file is present." : "Decantr token CSS file was not found.",
4436
4873
  evidence: [redactEvidenceText(projectRoot, tokensPath)],
4437
- suggestedFix: existsSync6(tokensPath) ? void 0 : "Run `decantr refresh` or map Decantr tokens into the existing styling system."
4874
+ suggestedFix: existsSync7(tokensPath) ? void 0 : "Run `decantr refresh` or map Decantr tokens into the existing styling system."
4438
4875
  })
4439
4876
  );
4440
4877
  return assertions;
@@ -4447,7 +4884,7 @@ function createEvidenceBundle(input) {
4447
4884
  try {
4448
4885
  resolvedProjectRoot = realpathSync2(input.projectRoot);
4449
4886
  } catch {
4450
- resolvedProjectRoot = resolve3(input.projectRoot);
4887
+ resolvedProjectRoot = resolve4(input.projectRoot);
4451
4888
  }
4452
4889
  const projectId = `project_${hashString(resolvedProjectRoot)}`;
4453
4890
  return {
@@ -4585,21 +5022,21 @@ function scoreRatio(numerator, denominator) {
4585
5022
  return Math.min(5, Math.max(1, Math.round(numerator / denominator * 5)));
4586
5023
  }
4587
5024
  function readJsonIfExists(path) {
4588
- if (!existsSync6(path)) return null;
5025
+ if (!existsSync7(path)) return null;
4589
5026
  try {
4590
- return JSON.parse(readFileSync6(path, "utf-8"));
5027
+ return JSON.parse(readFileSync7(path, "utf-8"));
4591
5028
  } catch {
4592
5029
  return null;
4593
5030
  }
4594
5031
  }
4595
5032
  function loadReviewPack(projectRoot) {
4596
5033
  return readJsonIfExists(
4597
- join6(projectRoot, ".decantr", "context", "review-pack.json")
5034
+ join7(projectRoot, ".decantr", "context", "review-pack.json")
4598
5035
  );
4599
5036
  }
4600
5037
  function loadPackManifest(projectRoot) {
4601
5038
  return readJsonIfExists(
4602
- join6(projectRoot, ".decantr", "context", "pack-manifest.json")
5039
+ join7(projectRoot, ".decantr", "context", "pack-manifest.json")
4603
5040
  );
4604
5041
  }
4605
5042
  function collectPackManifestReferences(packManifest) {
@@ -4648,14 +5085,14 @@ function collectPackManifestReferences(packManifest) {
4648
5085
  }
4649
5086
  function collectMissingPackManifestFiles(projectRoot, packManifest = loadPackManifest(projectRoot)) {
4650
5087
  if (!packManifest) return [];
4651
- const contextDir = join6(projectRoot, ".decantr", "context");
5088
+ const contextDir = join7(projectRoot, ".decantr", "context");
4652
5089
  const missing = [];
4653
5090
  for (const reference of collectPackManifestReferences(packManifest)) {
4654
5091
  for (const field of ["markdown", "json"]) {
4655
5092
  const fileName = reference[field];
4656
5093
  if (!fileName) continue;
4657
- const absolutePath = join6(contextDir, fileName);
4658
- if (existsSync6(absolutePath)) continue;
5094
+ const absolutePath = join7(contextDir, fileName);
5095
+ if (existsSync7(absolutePath)) continue;
4659
5096
  missing.push({
4660
5097
  entryId: reference.entryId,
4661
5098
  kind: reference.kind,
@@ -4669,13 +5106,13 @@ function collectMissingPackManifestFiles(projectRoot, packManifest = loadPackMan
4669
5106
  }
4670
5107
  function readTextIfExists(path) {
4671
5108
  try {
4672
- return existsSync6(path) ? readFileSync6(path, "utf-8") : "";
5109
+ return existsSync7(path) ? readFileSync7(path, "utf-8") : "";
4673
5110
  } catch {
4674
5111
  return "";
4675
5112
  }
4676
5113
  }
4677
5114
  function readProjectAdoptionMode(projectRoot) {
4678
- const projectJson = readJsonIfExists(join6(projectRoot, ".decantr", "project.json"));
5115
+ const projectJson = readJsonIfExists(join7(projectRoot, ".decantr", "project.json"));
4679
5116
  const adoptionMode = projectJson?.initialized?.adoptionMode;
4680
5117
  return typeof adoptionMode === "string" ? adoptionMode : null;
4681
5118
  }
@@ -4761,7 +5198,7 @@ function collectProjectSourceFiles(projectRoot) {
4761
5198
  "hooks",
4762
5199
  "providers",
4763
5200
  "server"
4764
- ].map((dir) => join6(projectRoot, dir)).filter((dir) => existsSync6(dir));
5201
+ ].map((dir) => join7(projectRoot, dir)).filter((dir) => existsSync7(dir));
4765
5202
  const rootFileCandidates = [
4766
5203
  "middleware.ts",
4767
5204
  "middleware.tsx",
@@ -4775,7 +5212,7 @@ function collectProjectSourceFiles(projectRoot) {
4775
5212
  "proxy.jsx",
4776
5213
  "proxy.mts",
4777
5214
  "proxy.cts"
4778
- ].map((file) => join6(projectRoot, file)).filter((file) => existsSync6(file));
5215
+ ].map((file) => join7(projectRoot, file)).filter((file) => existsSync7(file));
4779
5216
  const files = /* @__PURE__ */ new Set();
4780
5217
  const ignoredDirNames = /* @__PURE__ */ new Set([
4781
5218
  "node_modules",
@@ -4786,9 +5223,9 @@ function collectProjectSourceFiles(projectRoot) {
4786
5223
  "coverage"
4787
5224
  ]);
4788
5225
  const walk = (dir) => {
4789
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5226
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
4790
5227
  if (ignoredDirNames.has(entry.name)) continue;
4791
- const absolutePath = join6(dir, entry.name);
5228
+ const absolutePath = join7(dir, entry.name);
4792
5229
  if (entry.isDirectory()) {
4793
5230
  walk(absolutePath);
4794
5231
  continue;
@@ -4819,7 +5256,7 @@ function isAuditableStyleFile(filePath) {
4819
5256
  return /\.css$/i.test(filePath);
4820
5257
  }
4821
5258
  function collectProjectStyleFiles2(projectRoot) {
4822
- const candidates = ["src", "app", "styles"].map((dir) => join6(projectRoot, dir)).filter((dir) => existsSync6(dir));
5259
+ const candidates = ["src", "app", "styles"].map((dir) => join7(projectRoot, dir)).filter((dir) => existsSync7(dir));
4823
5260
  const files = /* @__PURE__ */ new Set();
4824
5261
  const ignoredDirNames = /* @__PURE__ */ new Set([
4825
5262
  "node_modules",
@@ -4830,9 +5267,9 @@ function collectProjectStyleFiles2(projectRoot) {
4830
5267
  "coverage"
4831
5268
  ]);
4832
5269
  const walk = (dir) => {
4833
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5270
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
4834
5271
  if (ignoredDirNames.has(entry.name)) continue;
4835
- const absolutePath = join6(dir, entry.name);
5272
+ const absolutePath = join7(dir, entry.name);
4836
5273
  if (entry.isDirectory()) {
4837
5274
  walk(absolutePath);
4838
5275
  continue;
@@ -4906,9 +5343,9 @@ function collectRuntimeImportSpecifiers(entry) {
4906
5343
  function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
4907
5344
  let basePath = null;
4908
5345
  if (specifier.startsWith("@/")) {
4909
- basePath = join6(projectRoot, "src", specifier.slice(2));
5346
+ basePath = join7(projectRoot, "src", specifier.slice(2));
4910
5347
  } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
4911
- basePath = resolve3(dirname2(sourceAbsolutePath), specifier);
5348
+ basePath = resolve4(dirname3(sourceAbsolutePath), specifier);
4912
5349
  }
4913
5350
  if (!basePath) return null;
4914
5351
  const candidates = [
@@ -4919,14 +5356,14 @@ function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
4919
5356
  `${basePath}.jsx`,
4920
5357
  `${basePath}.mts`,
4921
5358
  `${basePath}.cts`,
4922
- join6(basePath, "index.ts"),
4923
- join6(basePath, "index.tsx"),
4924
- join6(basePath, "index.js"),
4925
- join6(basePath, "index.jsx")
5359
+ join7(basePath, "index.ts"),
5360
+ join7(basePath, "index.tsx"),
5361
+ join7(basePath, "index.js"),
5362
+ join7(basePath, "index.jsx")
4926
5363
  ];
4927
5364
  for (const candidate of candidates) {
4928
- if (existsSync6(candidate) && isAuditableSourceFile(candidate)) {
4929
- return relative6(projectRoot, candidate) || candidate;
5365
+ if (existsSync7(candidate) && isAuditableSourceFile(candidate)) {
5366
+ return relative7(projectRoot, candidate) || candidate;
4930
5367
  }
4931
5368
  }
4932
5369
  return null;
@@ -4974,8 +5411,8 @@ function isClientAuthHeaderSource(relativePath, code, clientReachableFiles) {
4974
5411
  function auditProjectSourceTree(projectRoot, sourceFiles = collectProjectSourceFiles(projectRoot)) {
4975
5412
  const sourceEntries = sourceFiles.map((sourceFile) => ({
4976
5413
  absolutePath: sourceFile,
4977
- relativePath: relative6(projectRoot, sourceFile) || sourceFile,
4978
- code: readFileSync6(sourceFile, "utf-8")
5414
+ relativePath: relative7(projectRoot, sourceFile) || sourceFile,
5415
+ code: readFileSync7(sourceFile, "utf-8")
4979
5416
  })).filter((entry) => !isNonProductionSourceAuditFile(entry.relativePath));
4980
5417
  const clientReachableFiles = collectClientReachableSourceFiles(projectRoot, sourceEntries);
4981
5418
  const summary = {
@@ -5288,8 +5725,8 @@ function auditProjectStyleContracts(projectRoot) {
5288
5725
  reducedMotionSignals: createSourceAuditBucket()
5289
5726
  };
5290
5727
  for (const styleFile of styleFiles) {
5291
- const relativePath = relative6(projectRoot, styleFile) || styleFile;
5292
- const css = readFileSync6(styleFile, "utf-8");
5728
+ const relativePath = relative7(projectRoot, styleFile) || styleFile;
5729
+ const css = readFileSync7(styleFile, "utf-8");
5293
5730
  recordSourceAudit(summary.focusVisibleSignals, relativePath, countFocusVisibleSignals(css));
5294
5731
  recordSourceAudit(summary.reducedMotionSignals, relativePath, countReducedMotionSignals(css));
5295
5732
  }
@@ -5298,19 +5735,19 @@ function auditProjectStyleContracts(projectRoot) {
5298
5735
  function buildRegistryContext(projectRoot) {
5299
5736
  const themeRegistry = /* @__PURE__ */ new Map();
5300
5737
  const patternRegistry = /* @__PURE__ */ new Map();
5301
- const cacheDir = join6(projectRoot, ".decantr", "cache");
5302
- const customDir = join6(projectRoot, ".decantr", "custom");
5738
+ const cacheDir = join7(projectRoot, ".decantr", "cache");
5739
+ const customDir = join7(projectRoot, ".decantr", "custom");
5303
5740
  const addPattern = (id, source, data = null) => {
5304
5741
  if (typeof id !== "string" || !id.trim() || patternRegistry.has(id)) return;
5305
5742
  patternRegistry.set(id, data ?? { id, source });
5306
5743
  };
5307
- const cachedThemesDir = join6(cacheDir, "@official", "themes");
5744
+ const cachedThemesDir = join7(cacheDir, "@official", "themes");
5308
5745
  try {
5309
- if (existsSync6(cachedThemesDir)) {
5310
- for (const file of readdirSync5(cachedThemesDir).filter(
5746
+ if (existsSync7(cachedThemesDir)) {
5747
+ for (const file of readdirSync6(cachedThemesDir).filter(
5311
5748
  (name) => name.endsWith(".json") && name !== "index.json"
5312
5749
  )) {
5313
- const data = JSON.parse(readFileSync6(join6(cachedThemesDir, file), "utf-8"));
5750
+ const data = JSON.parse(readFileSync7(join7(cachedThemesDir, file), "utf-8"));
5314
5751
  if (data.id && !themeRegistry.has(data.id)) {
5315
5752
  themeRegistry.set(data.id, { modes: data.modes || ["light", "dark"] });
5316
5753
  }
@@ -5318,11 +5755,11 @@ function buildRegistryContext(projectRoot) {
5318
5755
  }
5319
5756
  } catch {
5320
5757
  }
5321
- const customThemesDir = join6(customDir, "themes");
5758
+ const customThemesDir = join7(customDir, "themes");
5322
5759
  try {
5323
- if (existsSync6(customThemesDir)) {
5324
- for (const file of readdirSync5(customThemesDir).filter((name) => name.endsWith(".json"))) {
5325
- const data = JSON.parse(readFileSync6(join6(customThemesDir, file), "utf-8"));
5760
+ if (existsSync7(customThemesDir)) {
5761
+ for (const file of readdirSync6(customThemesDir).filter((name) => name.endsWith(".json"))) {
5762
+ const data = JSON.parse(readFileSync7(join7(customThemesDir, file), "utf-8"));
5326
5763
  if (data.id) {
5327
5764
  themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
5328
5765
  }
@@ -5330,29 +5767,29 @@ function buildRegistryContext(projectRoot) {
5330
5767
  }
5331
5768
  } catch {
5332
5769
  }
5333
- const cachedPatternsDir = join6(cacheDir, "@official", "patterns");
5770
+ const cachedPatternsDir = join7(cacheDir, "@official", "patterns");
5334
5771
  try {
5335
- if (existsSync6(cachedPatternsDir)) {
5336
- for (const file of readdirSync5(cachedPatternsDir).filter(
5772
+ if (existsSync7(cachedPatternsDir)) {
5773
+ for (const file of readdirSync6(cachedPatternsDir).filter(
5337
5774
  (name) => name.endsWith(".json") && name !== "index.json"
5338
5775
  )) {
5339
- const data = JSON.parse(readFileSync6(join6(cachedPatternsDir, file), "utf-8"));
5776
+ const data = JSON.parse(readFileSync7(join7(cachedPatternsDir, file), "utf-8"));
5340
5777
  addPattern(data.id, "cache", data);
5341
5778
  }
5342
5779
  }
5343
5780
  } catch {
5344
5781
  }
5345
- const customPatternsDir = join6(customDir, "patterns");
5782
+ const customPatternsDir = join7(customDir, "patterns");
5346
5783
  try {
5347
- if (existsSync6(customPatternsDir)) {
5348
- for (const file of readdirSync5(customPatternsDir).filter((name) => name.endsWith(".json"))) {
5349
- const data = JSON.parse(readFileSync6(join6(customPatternsDir, file), "utf-8"));
5784
+ if (existsSync7(customPatternsDir)) {
5785
+ for (const file of readdirSync6(customPatternsDir).filter((name) => name.endsWith(".json"))) {
5786
+ const data = JSON.parse(readFileSync7(join7(customPatternsDir, file), "utf-8"));
5350
5787
  addPattern(data.id, "custom", data);
5351
5788
  }
5352
5789
  }
5353
5790
  } catch {
5354
5791
  }
5355
- const localPatterns = readJsonIfExists(join6(projectRoot, ".decantr", "local-patterns.json"));
5792
+ const localPatterns = readJsonIfExists(join7(projectRoot, ".decantr", "local-patterns.json"));
5356
5793
  for (const pattern of localPatterns?.patterns ?? []) {
5357
5794
  addPattern(pattern.id, "local-law", pattern);
5358
5795
  }
@@ -5650,7 +6087,8 @@ function appendTopologyFindings(findings, essence, reviewPack) {
5650
6087
  }
5651
6088
  async function runRuntimeAudit(projectRoot, essence) {
5652
6089
  return auditBuiltDist(projectRoot, {
5653
- routeHints: extractRouteHintsFromEssence(essence)
6090
+ routeHints: extractRouteHintsFromEssence(essence),
6091
+ runtimeTarget: typeof essence?.meta?.target === "string" ? essence.meta.target : null
5654
6092
  });
5655
6093
  }
5656
6094
  function extractFailedRouteDocuments(runtimeAudit) {
@@ -5660,8 +6098,8 @@ function extractFailedRouteDocumentHardening(runtimeAudit) {
5660
6098
  return runtimeAudit.failures.filter((failure) => failure.startsWith("route-document-hardening-failed:")).map((failure) => normalizeRouteHint2(failure.slice("route-document-hardening-failed:".length))).filter(Boolean);
5661
6099
  }
5662
6100
  function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topology, sourceAudit) {
5663
- const distPath = join6(projectRoot, "dist");
5664
- const indexPath = join6(distPath, "index.html");
6101
+ const distPath = join7(projectRoot, "dist");
6102
+ const indexPath = join7(distPath, "index.html");
5665
6103
  const isFrameworkBuildOutput = runtimeAudit.failures.includes("next-build-output");
5666
6104
  if (!runtimeAudit.distPresent) {
5667
6105
  findings.push(
@@ -6206,7 +6644,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
6206
6644
  }
6207
6645
  function readBehaviorPatterns(projectRoot) {
6208
6646
  const pack = readJsonIfExists(
6209
- join6(projectRoot, ".decantr", "local-patterns.json")
6647
+ join7(projectRoot, ".decantr", "local-patterns.json")
6210
6648
  );
6211
6649
  return (pack?.patterns ?? []).filter(
6212
6650
  (pattern) => pattern.behavior_obligations && Array.isArray(pattern.behavior_obligations.obligations) && pattern.behavior_obligations.obligations.length > 0
@@ -6253,15 +6691,15 @@ function isFormPrimitiveDefinitionFile(pattern, relativeFile) {
6253
6691
  );
6254
6692
  }
6255
6693
  function sourceAuditFilePath(projectRoot, file) {
6256
- return isAbsolute4(file) ? file : join6(projectRoot, file);
6694
+ return isAbsolute4(file) ? file : join7(projectRoot, file);
6257
6695
  }
6258
6696
  function sourceAuditRelativePath(projectRoot, file) {
6259
- return (isAbsolute4(file) ? relative6(projectRoot, file) : file).replace(/\\/g, "/");
6697
+ return (isAbsolute4(file) ? relative7(projectRoot, file) : file).replace(/\\/g, "/");
6260
6698
  }
6261
6699
  function sourceFileContainsAny(projectRoot, sourceFiles, patterns) {
6262
6700
  for (const file of sourceFiles.slice(0, 400)) {
6263
6701
  try {
6264
- const code = readFileSync6(sourceAuditFilePath(projectRoot, file), "utf-8");
6702
+ const code = readFileSync7(sourceAuditFilePath(projectRoot, file), "utf-8");
6265
6703
  if (patterns.some((pattern) => pattern.test(code))) return true;
6266
6704
  } catch {
6267
6705
  }
@@ -6315,7 +6753,7 @@ function appendBehaviorObligationFindings(findings, projectRoot, sourceFiles) {
6315
6753
  const relativeFile = sourceAuditRelativePath(projectRoot, file);
6316
6754
  let code = "";
6317
6755
  try {
6318
- code = readFileSync6(sourceAuditFilePath(projectRoot, file), "utf-8");
6756
+ code = readFileSync7(sourceAuditFilePath(projectRoot, file), "utf-8");
6319
6757
  } catch {
6320
6758
  continue;
6321
6759
  }
@@ -6392,7 +6830,7 @@ function appendBehaviorObligationFindings(findings, projectRoot, sourceFiles) {
6392
6830
  const relativeFile = sourceAuditRelativePath(projectRoot, file);
6393
6831
  let code = "";
6394
6832
  try {
6395
- code = readFileSync6(sourceAuditFilePath(projectRoot, file), "utf-8");
6833
+ code = readFileSync7(sourceAuditFilePath(projectRoot, file), "utf-8");
6396
6834
  } catch {
6397
6835
  continue;
6398
6836
  }
@@ -7759,7 +8197,7 @@ function appendStyleContractFindings(findings, styleAudit, essence) {
7759
8197
  }
7760
8198
  }
7761
8199
  async function auditProject(projectRoot) {
7762
- const essencePath = join6(projectRoot, "decantr.essence.json");
8200
+ const essencePath = join7(projectRoot, "decantr.essence.json");
7763
8201
  const findings = [];
7764
8202
  const reviewPack = loadReviewPack(projectRoot);
7765
8203
  const packManifest = loadPackManifest(projectRoot);
@@ -7767,7 +8205,7 @@ async function auditProject(projectRoot) {
7767
8205
  const packHydrationOptional = adoptionMode === "contract-only" || adoptionMode === "style-bridge";
7768
8206
  const packHydrationSeverity = packHydrationOptional ? "info" : "warn";
7769
8207
  const runtimeAudit = emptyRuntimeAudit();
7770
- if (!existsSync6(essencePath)) {
8208
+ if (!existsSync7(essencePath)) {
7771
8209
  findings.push(
7772
8210
  makeFinding({
7773
8211
  id: "essence-missing",
@@ -7802,7 +8240,7 @@ async function auditProject(projectRoot) {
7802
8240
  }
7803
8241
  let essence = null;
7804
8242
  try {
7805
- essence = JSON.parse(readFileSync6(essencePath, "utf-8"));
8243
+ essence = JSON.parse(readFileSync7(essencePath, "utf-8"));
7806
8244
  } catch (error) {
7807
8245
  findings.push(
7808
8246
  makeFinding({
@@ -7845,7 +8283,7 @@ async function auditProject(projectRoot) {
7845
8283
  category: "Execution Packs",
7846
8284
  severity: packHydrationSeverity,
7847
8285
  message: packHydrationOptional ? "Compiled execution pack manifest is not hydrated yet; this is optional for contract-only/style-bridge Brownfield adoption." : "Compiled execution pack manifest is missing.",
7848
- evidence: [join6(projectRoot, ".decantr", "context", "pack-manifest.json")],
8286
+ evidence: [join7(projectRoot, ".decantr", "context", "pack-manifest.json")],
7849
8287
  suggestedFix: packHydrationOptional ? "Optional: run `decantr registry compile-packs decantr.essence.json --write-context` when you want hosted page packs and review packs for richer assistant context." : "Run `decantr registry compile-packs decantr.essence.json --write-context` to hydrate scaffold, review, mutation, section, and page packs."
7850
8288
  })
7851
8289
  );
@@ -7909,7 +8347,7 @@ async function auditProject(projectRoot) {
7909
8347
  category: "Review Contract",
7910
8348
  severity: packHydrationSeverity,
7911
8349
  message: packHydrationOptional ? "The compiled review pack file is not hydrated yet; contract-only/style-bridge Brownfield projects can continue without it." : "The compiled review pack file is missing.",
7912
- evidence: [join6(projectRoot, ".decantr", "context", "review-pack.json")],
8350
+ evidence: [join7(projectRoot, ".decantr", "context", "review-pack.json")],
7913
8351
  suggestedFix: packHydrationOptional ? "Optional: hydrate hosted packs later if you want critique consumers to anchor findings to the compiled review contract." : "Hydrate the full hosted context bundle with `decantr registry compile-packs decantr.essence.json --write-context` so critique consumers can anchor findings to the compiled review contract."
7914
8352
  })
7915
8353
  );
@@ -8056,7 +8494,7 @@ function countKeyboardShortcutSignals(code) {
8056
8494
  return matches.length;
8057
8495
  }
8058
8496
  function getScriptKind(filePath) {
8059
- switch (extname6(filePath).toLowerCase()) {
8497
+ switch (extname7(filePath).toLowerCase()) {
8060
8498
  case ".tsx":
8061
8499
  return ts6.ScriptKind.TSX;
8062
8500
  case ".jsx":
@@ -16112,10 +16550,10 @@ function critiqueSource({
16112
16550
  };
16113
16551
  }
16114
16552
  function resolveProjectFilePath(projectRoot, filePath) {
16115
- const root = existsSync6(projectRoot) ? realpathSync2.native(projectRoot) : resolve3(projectRoot);
16116
- const candidatePath = isAbsolute4(filePath) ? resolve3(filePath) : resolve3(root, filePath);
16117
- const resolvedPath = existsSync6(candidatePath) ? realpathSync2.native(candidatePath) : candidatePath;
16118
- const relativePath = relative6(root, resolvedPath);
16553
+ const root = existsSync7(projectRoot) ? realpathSync2.native(projectRoot) : resolve4(projectRoot);
16554
+ const candidatePath = isAbsolute4(filePath) ? resolve4(filePath) : resolve4(root, filePath);
16555
+ const resolvedPath = existsSync7(candidatePath) ? realpathSync2.native(candidatePath) : candidatePath;
16556
+ const relativePath = relative7(root, resolvedPath);
16119
16557
  if (relativePath.startsWith("..") || isAbsolute4(relativePath)) {
16120
16558
  throw new Error(`Path escapes the project root: ${filePath}`);
16121
16559
  }
@@ -16124,7 +16562,7 @@ function resolveProjectFilePath(projectRoot, filePath) {
16124
16562
  async function critiqueFile(filePath, projectRoot) {
16125
16563
  const resolvedPath = resolveProjectFilePath(projectRoot, filePath);
16126
16564
  const code = await readFile(resolvedPath, "utf-8");
16127
- const treatmentsCss = readTextIfExists(join6(projectRoot, "src", "styles", "treatments.css"));
16565
+ const treatmentsCss = readTextIfExists(join7(projectRoot, "src", "styles", "treatments.css"));
16128
16566
  const reviewPack = loadReviewPack(projectRoot);
16129
16567
  const packManifest = loadPackManifest(projectRoot);
16130
16568
  const adoptionMode = readProjectAdoptionMode(projectRoot);
@@ -16151,6 +16589,8 @@ export {
16151
16589
  RAW_CONTROL_REUSE_RULE_ID,
16152
16590
  RUNTIME_PROBE_PAYLOAD_V2_SCHEMA_URL,
16153
16591
  SCAN_REPORT_SCHEMA_URL,
16592
+ SCAN_REPORT_V1_SCHEMA_URL,
16593
+ SCAN_REPORT_V2_SCHEMA_URL,
16154
16594
  STYLE_BRIDGE_ARBITRARY_VALUE_RULE_ID,
16155
16595
  VERIFICATION_COMMON_V2_SCHEMA_URL,
16156
16596
  VERIFICATION_SCHEMA_URLS,
@@ -16176,6 +16616,7 @@ export {
16176
16616
  critiqueFile,
16177
16617
  critiqueSource,
16178
16618
  deriveVerificationDiagnostic,
16619
+ discoverProject,
16179
16620
  emptyRuntimeAudit,
16180
16621
  extractRouteHintsFromEssence,
16181
16622
  extractSourceStringLiterals,