@decantr/verifier 3.6.1 → 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
 
@@ -2009,1048 +2009,1295 @@ function deriveVerificationDiagnostic(input) {
2009
2009
  };
2010
2010
  }
2011
2011
 
2012
- // src/graph-anchors.ts
2013
- function payloadRecord(payload) {
2014
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
2015
- }
2016
- function payloadString(payload, key) {
2017
- const value = payloadRecord(payload)[key];
2018
- return typeof value === "string" ? value : void 0;
2019
- }
2020
- function slug(value) {
2021
- return value.trim().toLowerCase().replace(/[^a-z0-9_.:/-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2022
- }
2023
- function normalizedHaystack(finding) {
2024
- return [
2025
- finding.id,
2026
- finding.category,
2027
- finding.message,
2028
- finding.target,
2029
- finding.file,
2030
- finding.rule,
2031
- ...finding.evidence ?? []
2032
- ].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);
2033
2071
  }
2034
- function graphAnchor(snapshot, node, confidence, reason, route) {
2035
- const anchor = {
2036
- snapshot_id: snapshot.id,
2037
- source_hash: snapshot.source_hash,
2038
- node_id: node.id,
2039
- node_type: node.type,
2040
- confidence,
2041
- reason
2042
- };
2043
- if (route) anchor.route = route;
2044
- 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
+ }
2045
2080
  }
2046
- function routeForNode(snapshot, nodeId, visited = /* @__PURE__ */ new Set()) {
2047
- if (visited.has(nodeId)) return void 0;
2048
- visited.add(nodeId);
2049
- if (nodeId.startsWith("rt:")) {
2050
- const route2 = snapshot.nodes.find((node) => node.id === nodeId);
2051
- 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 };
2052
2095
  }
2053
- const sourceNode = snapshot.nodes.find(
2054
- (node) => node.id === nodeId && node.type === "SourceArtifact"
2055
- );
2056
- if (sourceNode) {
2057
- const sourceEdges = snapshot.edges.filter(
2058
- (edge) => edge.relation === "NODE_DERIVED_FROM_SOURCE" && edge.dst === nodeId
2059
- );
2060
- const sortedSourceEdges = [
2061
- ...sourceEdges.filter((edge) => edge.src.startsWith("rt:")),
2062
- ...sourceEdges.filter((edge) => !edge.src.startsWith("rt:"))
2063
- ];
2064
- for (const edge of sortedSourceEdges) {
2065
- const route2 = routeForNode(snapshot, edge.src, visited);
2066
- if (route2) return route2;
2067
- }
2068
- 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;
2069
2100
  }
2070
- 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;
2071
- if (!pageId) return void 0;
2072
- const routeEdge = snapshot.edges.find(
2073
- (edge) => edge.relation === "PAGE_ROUTED_AT_ROUTE" && edge.src === pageId
2074
- );
2075
- if (!routeEdge) return void 0;
2076
- const route = snapshot.nodes.find((node) => node.id === routeEdge.dst);
2077
- return payloadString(route?.payload, "path") ?? routeEdge.dst.replace(/^rt:/, "");
2101
+ return Boolean(readPackageJson(dir).value?.workspaces);
2078
2102
  }
2079
- function findProjectNode(snapshot) {
2080
- 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
+ }
2081
2111
  }
2082
- function findLocalRuleNode(snapshot, finding) {
2083
- const candidates = [finding.rule, finding.target, finding.id].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2084
- if (candidates.length === 0) return null;
2085
- return snapshot.nodes.find((node) => {
2086
- if (node.type !== "LocalRule") return false;
2087
- const payloadId = payloadString(node.payload, "id");
2088
- const ids = [node.id.replace(/^rule:/, ""), payloadId ? slug(payloadId) : null].filter(
2089
- (entry) => Boolean(entry)
2090
- );
2091
- return ids.some((id) => candidates.some((candidate) => id === candidate));
2092
- }) ?? 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";
2093
2116
  }
2094
- function findStyleBridgeNode(snapshot, finding) {
2095
- const haystack = normalizedHaystack(finding);
2096
- const candidates = [finding.rule, finding.target, finding.id, ...finding.evidence ?? []].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2097
- const styleBridgeNodes = snapshot.nodes.filter((node) => node.type === "StyleBridge");
2098
- if (styleBridgeNodes.length === 0) return null;
2099
- return styleBridgeNodes.find((node) => {
2100
- const payloadId = payloadString(node.payload, "id");
2101
- const payloadLabel = payloadString(node.payload, "label");
2102
- const ids = [
2103
- node.id.replace(/^bridge:/, ""),
2104
- payloadId ? slug(payloadId) : null,
2105
- payloadLabel ? slug(payloadLabel) : null
2106
- ].filter((entry) => Boolean(entry));
2107
- return ids.some(
2108
- (id) => candidates.some((candidate) => id === candidate || candidate.includes(id)) || haystack.includes(id.toLowerCase())
2109
- );
2110
- }) ?? (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
+ }
2111
2132
  }
2112
- function findRouteNode(snapshot, finding) {
2113
- const haystack = normalizedHaystack(finding);
2114
- const exactTarget = finding.target?.startsWith("/") ? finding.target : null;
2115
- const routes = snapshot.nodes.filter((node) => node.type === "Route").map((node) => ({
2116
- node,
2117
- path: payloadString(node.payload, "path") ?? node.id.replace(/^rt:/, "")
2118
- })).sort((a, b) => b.path.length - a.path.length);
2119
- 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)));
2120
2135
  }
2121
- function findPageNode(snapshot, finding) {
2122
- const candidates = [finding.target, finding.id].filter((entry) => typeof entry === "string" && entry.length > 0).map(slug);
2123
- if (candidates.length === 0) return null;
2124
- return snapshot.nodes.find((node) => {
2125
- if (node.type !== "Page") return false;
2126
- const payloadId = payloadString(node.payload, "id");
2127
- const payloadSection = payloadString(node.payload, "section");
2128
- const ids = [
2129
- node.id.replace(/^pg:/, ""),
2130
- payloadId ? slug(payloadId) : null,
2131
- payloadId && payloadSection ? slug(`${payloadSection}:${payloadId}`) : null
2132
- ].filter((entry) => Boolean(entry));
2133
- return ids.some((id) => candidates.some((candidate) => id === candidate));
2134
- }) ?? 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")));
2135
2139
  }
2136
- function findPatternNode(snapshot, finding) {
2137
- const haystack = normalizedHaystack(finding);
2138
- return snapshot.nodes.find((node) => {
2139
- if (node.type !== "Pattern") return false;
2140
- const payloadId = payloadString(node.payload, "id");
2141
- return [node.id.replace(/^pat:/, ""), payloadId].filter((entry) => Boolean(entry)).some((id) => haystack.includes(id.toLowerCase()));
2142
- }) ?? null;
2140
+ function shouldSkipDir(name) {
2141
+ return SKIP_DIRS.has(name) || name.startsWith(".") && name !== ".github";
2143
2142
  }
2144
- function findSourceArtifactNode(snapshot, finding) {
2145
- const haystack = normalizedHaystack(finding);
2146
- return snapshot.nodes.find((node) => {
2147
- if (node.type !== "SourceArtifact") return false;
2148
- const path = payloadString(node.payload, "path");
2149
- return path ? haystack.includes(path.toLowerCase()) : false;
2150
- }) ?? 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;
2151
2175
  }
2152
- function normalizePath3(value) {
2153
- return value ? value.replace(/\\/g, "/").toLowerCase() : null;
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;
2154
2182
  }
2155
- function findExactSourceArtifactNode(snapshot, finding) {
2156
- const findingFile = normalizePath3(finding.file);
2157
- if (!findingFile) return null;
2158
- return snapshot.nodes.find((node) => {
2159
- if (node.type !== "SourceArtifact") return false;
2160
- return normalizePath3(payloadString(node.payload, "path")) === findingFile;
2161
- }) ?? null;
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";
2162
2196
  }
2163
- function resolveGraphAnchorForFinding(snapshot, finding) {
2164
- if (!snapshot) return void 0;
2165
- const localRule = findLocalRuleNode(snapshot, finding);
2166
- if (localRule) {
2167
- return graphAnchor(snapshot, localRule, "exact", "finding rule matched a LocalRule node");
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");
2168
2236
  }
2169
- const exactSourceArtifact = findExactSourceArtifactNode(snapshot, finding);
2170
- if (exactSourceArtifact) {
2171
- return graphAnchor(
2172
- snapshot,
2173
- exactSourceArtifact,
2174
- "exact",
2175
- "finding file matched a SourceArtifact node",
2176
- routeForNode(snapshot, exactSourceArtifact.id)
2177
- );
2237
+ if (framework === "unknown" && hasStaticHtmlSurface(projectRoot)) {
2238
+ framework = "html";
2239
+ evidence.push("static HTML entrypoint");
2178
2240
  }
2179
- const styleBridge = findStyleBridgeNode(snapshot, finding);
2180
- if (styleBridge) {
2181
- return graphAnchor(snapshot, styleBridge, "exact", "finding matched a StyleBridge node");
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}`;
2182
2272
  }
2183
- const route = findRouteNode(snapshot, finding);
2184
- if (route) {
2185
- return graphAnchor(
2186
- snapshot,
2187
- route,
2188
- finding.target === payloadString(route.payload, "path") ? "exact" : "inferred",
2189
- "finding text matched a Route node",
2190
- payloadString(route.payload, "path") ?? route.id.replace(/^rt:/, "")
2191
- );
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
+ ];
2192
2288
  }
2193
- const page = findPageNode(snapshot, finding);
2194
- if (page) {
2195
- return graphAnchor(
2196
- snapshot,
2197
- page,
2198
- "exact",
2199
- "finding target matched a Page node",
2200
- routeForNode(snapshot, page.id)
2201
- );
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 });
2202
2298
  }
2203
- const pattern = findPatternNode(snapshot, finding);
2204
- if (pattern) {
2205
- return graphAnchor(
2206
- snapshot,
2207
- pattern,
2208
- "inferred",
2209
- "finding text matched a Pattern node",
2210
- routeForNode(snapshot, pattern.id)
2211
- );
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
+ });
2212
2343
  }
2213
- const sourceArtifact = findSourceArtifactNode(snapshot, finding);
2214
- if (sourceArtifact) {
2215
- return graphAnchor(
2216
- snapshot,
2217
- sourceArtifact,
2218
- "inferred",
2219
- "finding evidence matched a SourceArtifact node",
2220
- routeForNode(snapshot, sourceArtifact.id)
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"
2221
2384
  );
2222
2385
  }
2223
- const project = findProjectNode(snapshot);
2224
- return project ? graphAnchor(
2225
- snapshot,
2226
- project,
2227
- "fallback",
2228
- "finding is project-level or no specific graph node matched"
2229
- ) : void 0;
2230
2386
  }
2231
- function anchorFindingsToGraph(snapshot, findings) {
2232
- if (!snapshot) return findings;
2233
- return findings.map((finding) => {
2234
- const graph = resolveGraphAnchorForFinding(snapshot, finding);
2235
- return graph ? { ...finding, graph } : finding;
2236
- });
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;
2237
2429
  }
2238
-
2239
- // src/interactions.ts
2240
- var INTERACTION_SIGNALS = {
2241
- "animate-on-mount": {
2242
- interaction: "animate-on-mount",
2243
- signals: ["d-enter-fade", "d-enter-slide-up", "d-enter-scale", "animate-on-mount"],
2244
- suggestion: "Add `d-enter-fade`, `d-enter-slide-up`, or `d-enter-scale` to the pattern root."
2245
- },
2246
- "stagger-children": {
2247
- interaction: "stagger-children",
2248
- signals: ["d-stagger-children", "--d-stagger-index"],
2249
- suggestion: 'Apply `d-stagger-children` to the parent and `style={{ "--d-stagger-index": i }}` to each child.'
2250
- },
2251
- "status-pulse": {
2252
- interaction: "status-pulse",
2253
- signals: ["d-pulse", "d-pulse-ring", /@keyframes\s+pulse/],
2254
- suggestion: "Apply `d-pulse` or `d-pulse-ring` to the status indicator element."
2255
- },
2256
- "shimmer-skeleton": {
2257
- interaction: "shimmer-skeleton",
2258
- signals: ["d-shimmer", /@keyframes\s+shimmer/],
2259
- suggestion: "Apply `d-shimmer` to the skeleton/loading element."
2260
- },
2261
- "float-idle": {
2262
- interaction: "float-idle",
2263
- signals: ["d-float"],
2264
- suggestion: "Apply `d-float` to the floating decorative element."
2265
- },
2266
- "glow-hover": {
2267
- interaction: "glow-hover",
2268
- signals: ["d-glow-hover"],
2269
- suggestion: "Apply `d-glow-hover` to the interactive surface."
2270
- },
2271
- "lift-hover": {
2272
- interaction: "lift-hover",
2273
- signals: ["d-lift-hover"],
2274
- suggestion: "Apply `d-lift-hover` to the interactive card/tile."
2275
- },
2276
- "scale-hover": {
2277
- interaction: "scale-hover",
2278
- signals: ["d-scale-hover"],
2279
- suggestion: "Apply `d-scale-hover` to the interactive element."
2280
- },
2281
- "ripple-click": {
2282
- interaction: "ripple-click",
2283
- signals: ["d-ripple"],
2284
- suggestion: "Apply `d-ripple` to the button or interactive surface."
2285
- },
2286
- "drag-nodes": {
2287
- interaction: "drag-nodes",
2288
- signals: [/onPointer(Down|Move|Up)/i, /onMouse(Down|Move|Up)/i, /'?cursor.*?grab/i],
2289
- suggestion: "Implement pointer event handlers (onPointerDown/Move/Up) with a 4px movement threshold before drag engages. Apply cursor: grab/grabbing."
2290
- },
2291
- "drag-reorder": {
2292
- interaction: "drag-reorder",
2293
- signals: [/onPointer(Down|Move)/i, /draggable/i, /dnd|drag.{0,10}drop/i],
2294
- suggestion: "Implement drag-to-reorder via pointer events or a dnd library."
2295
- },
2296
- "pan-background": {
2297
- interaction: "pan-background",
2298
- signals: [/onPointer(Down|Move)/i, /transform.*?translate/i, /viewBox/],
2299
- suggestion: "Implement pointer handlers on the canvas BACKGROUND only (not nodes); update a viewport translate transform on drag."
2300
- },
2301
- "zoom-scroll": {
2302
- interaction: "zoom-scroll",
2303
- signals: [/onWheel/i, /transform.*?scale/i],
2304
- suggestion: "Implement an onWheel handler that adjusts a `scale` transform, clamped to [0.25, 4]. Show a zoom indicator."
2305
- },
2306
- "zoom-pinch": {
2307
- interaction: "zoom-pinch",
2308
- signals: [/touchstart|touchmove|gesturechange/i, /pinch/i],
2309
- suggestion: "Implement pinch-to-zoom via touch events or gestureend."
2310
- },
2311
- "click-select": {
2312
- interaction: "click-select",
2313
- signals: [/onClick/i, /selected/i, /aria-selected/i],
2314
- suggestion: 'Implement click-to-select state via onClick handlers; reflect selection via aria-selected or a "selected" class.'
2315
- },
2316
- "click-connect": {
2317
- interaction: "click-connect",
2318
- signals: [/onClick.*?port|connect/i, /connections?\s*[:[=]/i],
2319
- suggestion: "Implement a two-click state machine: first click selects a port, second click on another port creates a connection. Store connections in component state."
2320
- },
2321
- "inline-edit": {
2322
- interaction: "inline-edit",
2323
- signals: [/contenteditable|input.*?onBlur|onKeyDown.*?Enter/i, "isEditing"],
2324
- suggestion: "Replace static text with a controlled <input> on click; commit on blur or Enter keyDown."
2325
- },
2326
- "hover-tooltip": {
2327
- interaction: "hover-tooltip",
2328
- signals: [/data-tooltip|onMouseEnter|tooltip/i, 'role="tooltip"'],
2329
- suggestion: "Implement a hover-triggered tooltip: data-tooltip attribute or onMouseEnter/Leave handlers showing/hiding a popover."
2330
- },
2331
- "hover-reveal": {
2332
- interaction: "hover-reveal",
2333
- signals: [/:hover/i, /onMouseEnter/i, /group-hover/i],
2334
- suggestion: "Implement hover-triggered content reveal via :hover CSS or onMouseEnter handlers."
2335
- },
2336
- "live-simulation": {
2337
- interaction: "live-simulation",
2338
- signals: [/setInterval|setTimeout|requestAnimationFrame/i, /useEffect/i],
2339
- suggestion: "Implement live data simulation via setInterval (2-4 second cadence) updating mock state; animate changes with `d-pulse` on the changed element."
2340
- },
2341
- "real-time-updates": {
2342
- interaction: "real-time-updates",
2343
- signals: [/setInterval|WebSocket|EventSource|subscribe/i],
2344
- suggestion: "Implement real-time updates via WebSocket, EventSource, or polling with setInterval."
2345
- },
2346
- "scroll-reveal": {
2347
- interaction: "scroll-reveal",
2348
- signals: [/IntersectionObserver/i, /onScroll/i],
2349
- suggestion: "Implement scroll-triggered reveal via IntersectionObserver (once: true)."
2350
- },
2351
- "keyboard-navigation": {
2352
- interaction: "keyboard-navigation",
2353
- signals: [/onKeyDown|onKeyUp/i, /ArrowUp|ArrowDown|ArrowLeft|ArrowRight/],
2354
- suggestion: "Implement arrow-key navigation via onKeyDown handlers (ArrowUp/Down/Left/Right)."
2355
- },
2356
- "focus-trap": {
2357
- interaction: "focus-trap",
2358
- signals: [/focusTrap|tabindex|aria-modal/i, /useFocusTrap/],
2359
- suggestion: "Implement focus trap inside modal/dialog \u2014 listen for Tab key, cycle focus within the dialog, restore focus on close."
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 "/";
2360
2435
  }
2361
- };
2362
- function sourceLineCount(source) {
2363
- return Math.max(1, source.replace(/\r?\n$/, "").split(/\r?\n/).length);
2436
+ return `/${withoutExt.split("/").filter(Boolean).join("/")}` || "/";
2364
2437
  }
2365
- function verifyInteractionsInSource(interactions, sources) {
2366
- if (interactions.length === 0 || sources.size === 0) return [];
2367
- const unique2 = Array.from(new Set(interactions));
2368
- const missing = [];
2369
- for (const interaction of unique2) {
2370
- const requirement = INTERACTION_SIGNALS[interaction];
2371
- if (!requirement) {
2372
- continue;
2373
- }
2374
- let matched = false;
2375
- for (const source of sources.values()) {
2376
- matched = requirement.signals.some((signal) => {
2377
- if (typeof signal === "string") {
2378
- return source.includes(signal);
2379
- }
2380
- signal.lastIndex = 0;
2381
- return signal.test(source);
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"
2382
2464
  });
2383
- if (matched) break;
2384
2465
  }
2385
- if (!matched) {
2386
- missing.push({
2387
- interaction,
2388
- suggestion: requirement.suggestion,
2389
- scannedFiles: sources.size,
2390
- scannedLocations: [...sources.entries()].slice(0, 8).map(([file, source]) => ({
2391
- file,
2392
- startLine: 1,
2393
- endLine: sourceLineCount(source)
2394
- })),
2395
- expectedSignals: requirement.signals.map(
2396
- (signal) => typeof signal === "string" ? signal : String(signal)
2397
- )
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"
2398
2610
  });
2399
2611
  }
2400
2612
  }
2401
- return missing;
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
+ };
2402
2637
  }
2403
- function listKnownInteractions() {
2404
- return Object.keys(INTERACTION_SIGNALS).sort();
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
+ };
2405
2653
  }
2406
-
2407
- // src/scan.ts
2408
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
2409
- import { extname as extname3, isAbsolute, join as join3, relative as relative3 } from "path";
2410
- import { decodeHTML, decodeHTMLAttribute } from "entities";
2411
- var SCAN_REPORT_SCHEMA_URL = "https://decantr.ai/schemas/scan-report.v1.json";
2412
- var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
2413
- ".ts",
2414
- ".tsx",
2415
- ".js",
2416
- ".jsx",
2417
- ".mjs",
2418
- ".cjs",
2419
- ".vue",
2420
- ".svelte"
2421
- ]);
2422
- var STYLE_EXTENSIONS = /* @__PURE__ */ new Set([".css", ".scss", ".sass", ".less"]);
2423
- var PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte", ".html"]);
2424
- var ROUTE_VARIABLE_NAMES = "(?:path|pathname|route|currentPath|currentRoute|locationPath|activePath)";
2425
- var ROUTE_ASSET_EXTENSION_RE = /\.(?:avif|bmp|css|gif|ico|jpeg|jpg|js|json|map|mp4|pdf|png|svg|webp|woff2?)$/i;
2426
- var JSX_ROUTE_PATH_RE = /<(?:Route|[A-Z][\w.]*Route)\b[^>]*\bpath\s*=\s*(?:"([^"]+)"|'([^']+)'|{\s*"([^"]+)"\s*}|{\s*'([^']+)'\s*}|{\s*`([^`]+)`\s*})/g;
2427
- var OBJECT_ROUTE_PATH_RE = /\bpath\s*:\s*(?:"([^"]+)"|'([^']+)'|`([^`]+)`)/g;
2428
- var STATIC_HTML_ENTRY_FILES = [
2429
- "index.html",
2430
- "docs/index.html",
2431
- "src/index.html",
2432
- "public/index.html",
2433
- "dist/index.html"
2434
- ];
2435
- var STATIC_HTML_ROUTE_DIRS = ["demos", "examples"];
2436
- var MAX_STATIC_HTML_CANDIDATES = 1e3;
2437
- var MAX_FILE_READ_BYTES = 512 * 1024;
2438
- var MAX_WALK_FILES = 5e3;
2439
- var MAX_REPORT_ROUTES = 80;
2440
- var SKIP_DIRS = /* @__PURE__ */ new Set([
2441
- ".git",
2442
- ".next",
2443
- ".nuxt",
2444
- ".svelte-kit",
2445
- "coverage",
2446
- "dist",
2447
- "build",
2448
- "node_modules",
2449
- "out",
2450
- "target",
2451
- "vendor"
2452
- ]);
2453
- var RULE_FILES = [
2454
- "CLAUDE.md",
2455
- "AGENTS.md",
2456
- "GEMINI.md",
2457
- ".cursorrules",
2458
- ".cursor/rules",
2459
- ".claude/rules",
2460
- ".github/copilot-instructions.md",
2461
- "copilot-instructions.md",
2462
- ".windsurfrules"
2463
- ];
2464
- var GITHUB_OWNER_RE = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/i;
2465
- var GITHUB_REPO_RE = /^[a-z0-9._-]{1,100}$/i;
2466
- function parseHttpUrl(value) {
2467
- try {
2468
- const url = new URL(value.trim());
2469
- return url.protocol === "https:" || url.protocol === "http:" ? url : null;
2470
- } catch {
2471
- return null;
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";
2472
2696
  }
2697
+ return {
2698
+ approach,
2699
+ configFile,
2700
+ cssVariableCount,
2701
+ colorTokenCount,
2702
+ darkMode,
2703
+ themeSignals: [...themeSignals]
2704
+ };
2473
2705
  }
2474
- function githubPagesOwnerFromHostname(hostname) {
2475
- const [owner, domain, tld, ...extra] = hostname.toLowerCase().split(".");
2476
- if (extra.length > 0 || domain !== "github" || tld !== "io" || !owner) return null;
2477
- return GITHUB_OWNER_RE.test(owner) ? owner : null;
2706
+ function findAssistantRules(projectRoot) {
2707
+ return RULE_FILES.filter((file) => existsSync3(join3(projectRoot, file)));
2478
2708
  }
2479
- function normalizeGitHubPathSegment(segment, kind) {
2480
- if (!segment) return null;
2481
- const normalized = kind === "repo" ? segment.replace(/\.git$/i, "") : segment;
2482
- const pattern = kind === "owner" ? GITHUB_OWNER_RE : GITHUB_REPO_RE;
2483
- return pattern.test(normalized) ? normalized : null;
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
+ };
2484
2742
  }
2485
- var WEB_FRAMEWORKS = /* @__PURE__ */ new Set([
2486
- "angular",
2487
- "astro",
2488
- "html",
2489
- "nextjs",
2490
- "nuxt",
2491
- "react",
2492
- "solid",
2493
- "svelte",
2494
- "vue"
2495
- ]);
2496
- function isRecord(value) {
2497
- return typeof value === "object" && value !== null && !Array.isArray(value);
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
+ };
2498
2771
  }
2499
- function readTextFile(path, maxBytes = MAX_FILE_READ_BYTES) {
2500
- try {
2501
- const stat = statSync2(path);
2502
- if (!stat.isFile() || stat.size > maxBytes) return null;
2503
- return readFileSync4(path, "utf-8");
2504
- } catch {
2505
- return null;
2506
- }
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;
2507
2806
  }
2508
- function readPackageJson(projectRoot) {
2509
- const path = join3(projectRoot, "package.json");
2510
- if (!existsSync3(path)) return { value: null, present: false, valid: false };
2511
- const content = readTextFile(path);
2512
- if (!content) return { value: null, present: true, valid: false };
2513
- try {
2514
- const parsed = JSON.parse(content);
2515
- return {
2516
- value: isRecord(parsed) ? parsed : null,
2517
- present: true,
2518
- valid: isRecord(parsed)
2519
- };
2520
- } catch {
2521
- return { value: null, present: true, valid: false };
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:/, "");
2522
2813
  }
2523
- }
2524
- function dependencyVersion(dependencies, names) {
2525
- for (const name of names) {
2526
- const version = dependencies[name];
2527
- if (version) return version.replace(/^[~^]/, "");
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;
2528
2830
  }
2529
- return null;
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:/, "");
2530
2839
  }
2531
- function hasAnyFile(projectRoot, paths) {
2532
- return paths.some((path) => existsSync3(join3(projectRoot, path)));
2840
+ function findProjectNode(snapshot) {
2841
+ return snapshot.nodes.find((node) => node.id === snapshot.project_id) ?? snapshot.nodes.find((node) => node.type === "Project");
2533
2842
  }
2534
- function hasStaticHtmlSurface(projectRoot) {
2535
- if (hasAnyFile(projectRoot, STATIC_HTML_ENTRY_FILES)) return true;
2536
- return STATIC_HTML_ROUTE_DIRS.some((dir) => existsSync3(join3(projectRoot, dir, "index.html")));
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;
2537
2854
  }
2538
- function detectPackageManager(projectRoot, pkg) {
2539
- const declared = pkg?.packageManager?.split("@")[0];
2540
- if (declared === "npm" || declared === "pnpm" || declared === "yarn" || declared === "bun") {
2541
- return declared;
2542
- }
2543
- if (existsSync3(join3(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
2544
- if (existsSync3(join3(projectRoot, "yarn.lock"))) return "yarn";
2545
- if (existsSync3(join3(projectRoot, "bun.lockb"))) return "bun";
2546
- if (existsSync3(join3(projectRoot, "package-lock.json"))) return "npm";
2547
- return "unknown";
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);
2548
2872
  }
2549
- function detectPrimaryLanguage(projectRoot, packageJsonPresent) {
2550
- if (packageJsonPresent) return hasStaticHtmlSurface(projectRoot) ? "html" : "javascript";
2551
- if (hasAnyFile(projectRoot, ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"]))
2552
- return "python";
2553
- if (hasAnyFile(projectRoot, ["go.mod"])) return "go";
2554
- if (hasAnyFile(projectRoot, ["Cargo.toml"])) return "rust";
2555
- if (hasStaticHtmlSurface(projectRoot)) return "html";
2556
- return "unknown";
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;
2557
2881
  }
2558
- function detectProject(projectRoot) {
2559
- const packageRead = readPackageJson(projectRoot);
2560
- const pkg = packageRead.value;
2561
- const dependencies = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2562
- let framework = "unknown";
2563
- let frameworkVersion = null;
2564
- if (hasAnyFile(projectRoot, ["next.config.js", "next.config.ts", "next.config.mjs"]) || dependencies.next) {
2565
- framework = "nextjs";
2566
- frameworkVersion = dependencyVersion(dependencies, ["next"]);
2567
- } else if (hasAnyFile(projectRoot, ["nuxt.config.js", "nuxt.config.ts"]) || dependencies.nuxt) {
2568
- framework = "nuxt";
2569
- frameworkVersion = dependencyVersion(dependencies, ["nuxt"]);
2570
- } else if (hasAnyFile(projectRoot, ["astro.config.mjs", "astro.config.ts"]) || dependencies.astro) {
2571
- framework = "astro";
2572
- frameworkVersion = dependencyVersion(dependencies, ["astro"]);
2573
- } else if (hasAnyFile(projectRoot, ["svelte.config.js", "svelte.config.ts"]) || dependencies.svelte || dependencies["@sveltejs/kit"]) {
2574
- framework = "svelte";
2575
- frameworkVersion = dependencyVersion(dependencies, ["svelte", "@sveltejs/kit"]);
2576
- } else if (hasAnyFile(projectRoot, ["angular.json"]) || dependencies["@angular/core"]) {
2577
- framework = "angular";
2578
- frameworkVersion = dependencyVersion(dependencies, ["@angular/core"]);
2579
- } else if (dependencies["solid-js"]) {
2580
- framework = "solid";
2581
- frameworkVersion = dependencyVersion(dependencies, ["solid-js"]);
2582
- } else if (dependencies.vue) {
2583
- framework = "vue";
2584
- frameworkVersion = dependencyVersion(dependencies, ["vue"]);
2585
- } else if (dependencies.react) {
2586
- framework = "react";
2587
- frameworkVersion = dependencyVersion(dependencies, ["react"]);
2588
- } else if (hasStaticHtmlSurface(projectRoot)) {
2589
- framework = "html";
2590
- }
2591
- return {
2592
- framework,
2593
- frameworkVersion,
2594
- packageManager: detectPackageManager(projectRoot, pkg),
2595
- primaryLanguage: detectPrimaryLanguage(projectRoot, packageRead.present),
2596
- hasTypeScript: hasAnyFile(projectRoot, ["tsconfig.json"]),
2597
- hasTailwind: hasAnyFile(projectRoot, [
2598
- "tailwind.config.js",
2599
- "tailwind.config.ts",
2600
- "tailwind.config.mjs",
2601
- "tailwind.config.cjs"
2602
- ]),
2603
- hasDecantr: existsSync3(join3(projectRoot, "decantr.essence.json")) || existsSync3(join3(projectRoot, ".decantr")) || Boolean(dependencies["@decantr/cli"] || dependencies["@decantr/css"]),
2604
- packageName: typeof pkg?.name === "string" ? pkg.name : null,
2605
- packageJsonValid: packageRead.valid,
2606
- packageJsonPresent: packageRead.present,
2607
- dependencies
2608
- };
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;
2609
2896
  }
2610
- function shouldSkipDir(name) {
2611
- return SKIP_DIRS.has(name) || name.startsWith(".") && name !== ".github";
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;
2612
2904
  }
2613
- function walkFiles(projectRoot, options = {}) {
2614
- const files = [];
2615
- function walk(dir) {
2616
- if (files.length >= MAX_WALK_FILES) return;
2617
- let entries;
2618
- try {
2619
- entries = readdirSync3(dir);
2620
- } catch {
2621
- return;
2622
- }
2623
- for (const entry of entries) {
2624
- if (files.length >= MAX_WALK_FILES) return;
2625
- const fullPath = join3(dir, entry);
2626
- let stat;
2627
- try {
2628
- stat = statSync2(fullPath);
2629
- } catch {
2630
- continue;
2631
- }
2632
- if (stat.isDirectory()) {
2633
- if (shouldSkipDir(entry) && !options.includeHidden) continue;
2634
- walk(fullPath);
2635
- continue;
2636
- }
2637
- if (!stat.isFile()) continue;
2638
- const rel = relative3(projectRoot, fullPath).replace(/\\/g, "/");
2639
- const ext = extname3(entry).toLowerCase();
2640
- if (!options.extensions || options.extensions.has(ext)) {
2641
- files.push(rel);
2642
- }
2643
- }
2644
- }
2645
- walk(projectRoot);
2646
- return files;
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;
2647
2912
  }
2648
- function segmentToRoute(segment) {
2649
- if (segment.startsWith("(") && segment.endsWith(")")) return null;
2650
- if (segment.startsWith("[") && segment.endsWith("]")) {
2651
- const param = segment.slice(1, -1);
2652
- if (param.startsWith("...")) return `:${param.slice(3)}*`;
2653
- if (param.startsWith("[...") && param.endsWith("]")) return `:${param.slice(4, -1)}*`;
2654
- return `:${param}`;
2655
- }
2656
- return segment;
2913
+ function normalizePath3(value) {
2914
+ return value ? value.replace(/\\/g, "/").toLowerCase() : null;
2657
2915
  }
2658
- function walkNextAppRoutes(dir, projectRoot, segments) {
2659
- let entries;
2660
- try {
2661
- entries = readdirSync3(dir);
2662
- } catch {
2663
- return [];
2916
+ function findExactSourceArtifactNode(snapshot, finding) {
2917
+ const findingFile = normalizePath3(finding.file);
2918
+ if (!findingFile) return null;
2919
+ return snapshot.nodes.find((node) => {
2920
+ if (node.type !== "SourceArtifact") return false;
2921
+ return normalizePath3(payloadString(node.payload, "path")) === findingFile;
2922
+ }) ?? null;
2923
+ }
2924
+ function resolveGraphAnchorForFinding(snapshot, finding) {
2925
+ if (!snapshot) return void 0;
2926
+ const localRule = findLocalRuleNode(snapshot, finding);
2927
+ if (localRule) {
2928
+ return graphAnchor(snapshot, localRule, "exact", "finding rule matched a LocalRule node");
2664
2929
  }
2665
- const routes = [];
2666
- const pageFile = entries.find((entry) => /^page\.(tsx|ts|jsx|js)$/.test(entry));
2667
- const hasLayout = entries.some((entry) => /^layout\.(tsx|ts|jsx|js)$/.test(entry));
2668
- if (pageFile) {
2669
- routes.push({
2670
- path: `/${segments.filter(Boolean).join("/")}` || "/",
2671
- file: relative3(projectRoot, join3(dir, pageFile)).replace(/\\/g, "/"),
2672
- hasLayout
2673
- });
2930
+ const exactSourceArtifact = findExactSourceArtifactNode(snapshot, finding);
2931
+ if (exactSourceArtifact) {
2932
+ return graphAnchor(
2933
+ snapshot,
2934
+ exactSourceArtifact,
2935
+ "exact",
2936
+ "finding file matched a SourceArtifact node",
2937
+ routeForNode(snapshot, exactSourceArtifact.id)
2938
+ );
2674
2939
  }
2675
- for (const entry of entries) {
2676
- if (shouldSkipDir(entry)) continue;
2677
- const fullPath = join3(dir, entry);
2678
- try {
2679
- if (!statSync2(fullPath).isDirectory()) continue;
2680
- } catch {
2681
- continue;
2682
- }
2683
- const routeSegment = segmentToRoute(entry);
2684
- routes.push(
2685
- ...walkNextAppRoutes(
2686
- fullPath,
2687
- projectRoot,
2688
- routeSegment === null ? segments : [...segments, routeSegment]
2689
- )
2940
+ const styleBridge = findStyleBridgeNode(snapshot, finding);
2941
+ if (styleBridge) {
2942
+ return graphAnchor(snapshot, styleBridge, "exact", "finding matched a StyleBridge node");
2943
+ }
2944
+ const route = findRouteNode(snapshot, finding);
2945
+ if (route) {
2946
+ return graphAnchor(
2947
+ snapshot,
2948
+ route,
2949
+ finding.target === payloadString(route.payload, "path") ? "exact" : "inferred",
2950
+ "finding text matched a Route node",
2951
+ payloadString(route.payload, "path") ?? route.id.replace(/^rt:/, "")
2690
2952
  );
2691
2953
  }
2692
- return routes;
2693
- }
2694
- function walkSvelteKitRoutes(dir, projectRoot, segments) {
2695
- let entries;
2696
- try {
2697
- entries = readdirSync3(dir);
2698
- } catch {
2699
- return [];
2954
+ const page = findPageNode(snapshot, finding);
2955
+ if (page) {
2956
+ return graphAnchor(
2957
+ snapshot,
2958
+ page,
2959
+ "exact",
2960
+ "finding target matched a Page node",
2961
+ routeForNode(snapshot, page.id)
2962
+ );
2700
2963
  }
2701
- const routes = [];
2702
- const pageFile = entries.find((entry) => /^\+page\.(svelte|ts|js)$/.test(entry));
2703
- const hasLayout = entries.some((entry) => /^\+layout\.(svelte|ts|js)$/.test(entry));
2704
- if (pageFile) {
2705
- routes.push({
2706
- path: `/${segments.filter(Boolean).join("/")}` || "/",
2707
- file: relative3(projectRoot, join3(dir, pageFile)).replace(/\\/g, "/"),
2708
- hasLayout
2709
- });
2964
+ const pattern = findPatternNode(snapshot, finding);
2965
+ if (pattern) {
2966
+ return graphAnchor(
2967
+ snapshot,
2968
+ pattern,
2969
+ "inferred",
2970
+ "finding text matched a Pattern node",
2971
+ routeForNode(snapshot, pattern.id)
2972
+ );
2710
2973
  }
2711
- for (const entry of entries) {
2712
- if (shouldSkipDir(entry)) continue;
2713
- const fullPath = join3(dir, entry);
2714
- try {
2715
- if (!statSync2(fullPath).isDirectory()) continue;
2716
- } catch {
2717
- continue;
2718
- }
2719
- const routeSegment = segmentToRoute(entry);
2720
- routes.push(
2721
- ...walkSvelteKitRoutes(
2722
- fullPath,
2723
- projectRoot,
2724
- routeSegment === null ? segments : [...segments, routeSegment]
2725
- )
2974
+ const sourceArtifact = findSourceArtifactNode(snapshot, finding);
2975
+ if (sourceArtifact) {
2976
+ return graphAnchor(
2977
+ snapshot,
2978
+ sourceArtifact,
2979
+ "inferred",
2980
+ "finding evidence matched a SourceArtifact node",
2981
+ routeForNode(snapshot, sourceArtifact.id)
2726
2982
  );
2727
2983
  }
2728
- return routes;
2729
- }
2730
- function fileRouteFromPath(file, baseDir) {
2731
- let withoutExt = file.slice(0, -extname3(file).length);
2732
- if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
2733
- withoutExt = withoutExt.replace(
2734
- new RegExp(`^${baseDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`),
2735
- ""
2736
- );
2737
- const parts = withoutExt.split("/").filter(Boolean).map((part) => segmentToRoute(part)).filter(Boolean);
2738
- return `/${parts.join("/")}` || "/";
2984
+ const project = findProjectNode(snapshot);
2985
+ return project ? graphAnchor(
2986
+ snapshot,
2987
+ project,
2988
+ "fallback",
2989
+ "finding is project-level or no specific graph node matched"
2990
+ ) : void 0;
2739
2991
  }
2740
- function scanFileRoutes(projectRoot, baseDir, extensions = PAGE_EXTENSIONS) {
2741
- const fullBase = join3(projectRoot, baseDir);
2742
- if (!existsSync3(fullBase)) return [];
2743
- return walkFiles(fullBase, { extensions }).flatMap((file) => {
2744
- const baseName = file.split("/").pop() ?? file;
2745
- const routeFileName = baseName.slice(0, -extname3(baseName).length);
2746
- if (routeFileName.startsWith("_")) return [];
2747
- const rel = relative3(projectRoot, join3(fullBase, file)).replace(/\\/g, "/");
2748
- return [{ path: fileRouteFromPath(rel, baseDir), file: rel, hasLayout: false }];
2992
+ function anchorFindingsToGraph(snapshot, findings) {
2993
+ if (!snapshot) return findings;
2994
+ return findings.map((finding) => {
2995
+ const graph = resolveGraphAnchorForFinding(snapshot, finding);
2996
+ return graph ? { ...finding, graph } : finding;
2749
2997
  });
2750
2998
  }
2751
- function scanReactRouter(projectRoot) {
2752
- const files = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
2753
- const routes = /* @__PURE__ */ new Map();
2754
- let hashRouting = false;
2755
- for (const file of files) {
2756
- const content = readTextFile(join3(projectRoot, file));
2757
- if (!content) continue;
2758
- if (content.includes("HashRouter") || content.includes("createHashRouter")) hashRouting = true;
2759
- for (const match of content.matchAll(JSX_ROUTE_PATH_RE)) {
2760
- const literal = firstRouteLiteralMatch(match);
2761
- for (const route of normalizeDetectedRouteLiterals(literal || "/")) {
2762
- routes.set(route, { path: route, file, hasLayout: false });
2763
- }
2764
- }
2765
- for (const match of content.matchAll(OBJECT_ROUTE_PATH_RE)) {
2766
- const literal = firstRouteLiteralMatch(match);
2767
- for (const route of normalizeDetectedRouteLiterals(literal || "/")) {
2768
- routes.set(route, { path: route, file, hasLayout: false });
2769
- }
2770
- }
2771
- for (const route of detectPathnameBranchRoutes(content)) {
2772
- routes.set(route, { path: route, file, hasLayout: false });
2773
- }
2774
- for (const route of detectDeclarativeRouteSpecRoutes(content)) {
2775
- routes.set(route, { path: route, file, hasLayout: false });
2776
- }
2999
+
3000
+ // src/interactions.ts
3001
+ var INTERACTION_SIGNALS = {
3002
+ "animate-on-mount": {
3003
+ interaction: "animate-on-mount",
3004
+ signals: ["d-enter-fade", "d-enter-slide-up", "d-enter-scale", "animate-on-mount"],
3005
+ suggestion: "Add `d-enter-fade`, `d-enter-slide-up`, or `d-enter-scale` to the pattern root."
3006
+ },
3007
+ "stagger-children": {
3008
+ interaction: "stagger-children",
3009
+ signals: ["d-stagger-children", "--d-stagger-index"],
3010
+ suggestion: 'Apply `d-stagger-children` to the parent and `style={{ "--d-stagger-index": i }}` to each child.'
3011
+ },
3012
+ "status-pulse": {
3013
+ interaction: "status-pulse",
3014
+ signals: ["d-pulse", "d-pulse-ring", /@keyframes\s+pulse/],
3015
+ suggestion: "Apply `d-pulse` or `d-pulse-ring` to the status indicator element."
3016
+ },
3017
+ "shimmer-skeleton": {
3018
+ interaction: "shimmer-skeleton",
3019
+ signals: ["d-shimmer", /@keyframes\s+shimmer/],
3020
+ suggestion: "Apply `d-shimmer` to the skeleton/loading element."
3021
+ },
3022
+ "float-idle": {
3023
+ interaction: "float-idle",
3024
+ signals: ["d-float"],
3025
+ suggestion: "Apply `d-float` to the floating decorative element."
3026
+ },
3027
+ "glow-hover": {
3028
+ interaction: "glow-hover",
3029
+ signals: ["d-glow-hover"],
3030
+ suggestion: "Apply `d-glow-hover` to the interactive surface."
3031
+ },
3032
+ "lift-hover": {
3033
+ interaction: "lift-hover",
3034
+ signals: ["d-lift-hover"],
3035
+ suggestion: "Apply `d-lift-hover` to the interactive card/tile."
3036
+ },
3037
+ "scale-hover": {
3038
+ interaction: "scale-hover",
3039
+ signals: ["d-scale-hover"],
3040
+ suggestion: "Apply `d-scale-hover` to the interactive element."
3041
+ },
3042
+ "ripple-click": {
3043
+ interaction: "ripple-click",
3044
+ signals: ["d-ripple"],
3045
+ suggestion: "Apply `d-ripple` to the button or interactive surface."
3046
+ },
3047
+ "drag-nodes": {
3048
+ interaction: "drag-nodes",
3049
+ signals: [/onPointer(Down|Move|Up)/i, /onMouse(Down|Move|Up)/i, /'?cursor.*?grab/i],
3050
+ suggestion: "Implement pointer event handlers (onPointerDown/Move/Up) with a 4px movement threshold before drag engages. Apply cursor: grab/grabbing."
3051
+ },
3052
+ "drag-reorder": {
3053
+ interaction: "drag-reorder",
3054
+ signals: [/onPointer(Down|Move)/i, /draggable/i, /dnd|drag.{0,10}drop/i],
3055
+ suggestion: "Implement drag-to-reorder via pointer events or a dnd library."
3056
+ },
3057
+ "pan-background": {
3058
+ interaction: "pan-background",
3059
+ signals: [/onPointer(Down|Move)/i, /transform.*?translate/i, /viewBox/],
3060
+ suggestion: "Implement pointer handlers on the canvas BACKGROUND only (not nodes); update a viewport translate transform on drag."
3061
+ },
3062
+ "zoom-scroll": {
3063
+ interaction: "zoom-scroll",
3064
+ signals: [/onWheel/i, /transform.*?scale/i],
3065
+ suggestion: "Implement an onWheel handler that adjusts a `scale` transform, clamped to [0.25, 4]. Show a zoom indicator."
3066
+ },
3067
+ "zoom-pinch": {
3068
+ interaction: "zoom-pinch",
3069
+ signals: [/touchstart|touchmove|gesturechange/i, /pinch/i],
3070
+ suggestion: "Implement pinch-to-zoom via touch events or gestureend."
3071
+ },
3072
+ "click-select": {
3073
+ interaction: "click-select",
3074
+ signals: [/onClick/i, /selected/i, /aria-selected/i],
3075
+ suggestion: 'Implement click-to-select state via onClick handlers; reflect selection via aria-selected or a "selected" class.'
3076
+ },
3077
+ "click-connect": {
3078
+ interaction: "click-connect",
3079
+ signals: [/onClick.*?port|connect/i, /connections?\s*[:[=]/i],
3080
+ suggestion: "Implement a two-click state machine: first click selects a port, second click on another port creates a connection. Store connections in component state."
3081
+ },
3082
+ "inline-edit": {
3083
+ interaction: "inline-edit",
3084
+ signals: [/contenteditable|input.*?onBlur|onKeyDown.*?Enter/i, "isEditing"],
3085
+ suggestion: "Replace static text with a controlled <input> on click; commit on blur or Enter keyDown."
3086
+ },
3087
+ "hover-tooltip": {
3088
+ interaction: "hover-tooltip",
3089
+ signals: [/data-tooltip|onMouseEnter|tooltip/i, 'role="tooltip"'],
3090
+ suggestion: "Implement a hover-triggered tooltip: data-tooltip attribute or onMouseEnter/Leave handlers showing/hiding a popover."
3091
+ },
3092
+ "hover-reveal": {
3093
+ interaction: "hover-reveal",
3094
+ signals: [/:hover/i, /onMouseEnter/i, /group-hover/i],
3095
+ suggestion: "Implement hover-triggered content reveal via :hover CSS or onMouseEnter handlers."
3096
+ },
3097
+ "live-simulation": {
3098
+ interaction: "live-simulation",
3099
+ signals: [/setInterval|setTimeout|requestAnimationFrame/i, /useEffect/i],
3100
+ suggestion: "Implement live data simulation via setInterval (2-4 second cadence) updating mock state; animate changes with `d-pulse` on the changed element."
3101
+ },
3102
+ "real-time-updates": {
3103
+ interaction: "real-time-updates",
3104
+ signals: [/setInterval|WebSocket|EventSource|subscribe/i],
3105
+ suggestion: "Implement real-time updates via WebSocket, EventSource, or polling with setInterval."
3106
+ },
3107
+ "scroll-reveal": {
3108
+ interaction: "scroll-reveal",
3109
+ signals: [/IntersectionObserver/i, /onScroll/i],
3110
+ suggestion: "Implement scroll-triggered reveal via IntersectionObserver (once: true)."
3111
+ },
3112
+ "keyboard-navigation": {
3113
+ interaction: "keyboard-navigation",
3114
+ signals: [/onKeyDown|onKeyUp/i, /ArrowUp|ArrowDown|ArrowLeft|ArrowRight/],
3115
+ suggestion: "Implement arrow-key navigation via onKeyDown handlers (ArrowUp/Down/Left/Right)."
3116
+ },
3117
+ "focus-trap": {
3118
+ interaction: "focus-trap",
3119
+ signals: [/focusTrap|tabindex|aria-modal/i, /useFocusTrap/],
3120
+ suggestion: "Implement focus trap inside modal/dialog \u2014 listen for Tab key, cycle focus within the dialog, restore focus on close."
2777
3121
  }
2778
- return { routes: [...routes.values()], hashRouting };
2779
- }
2780
- function normalizeRouterObjectPath(path) {
2781
- const cleaned = path.trim();
2782
- if (!cleaned || cleaned === "/") return "/";
2783
- if (cleaned === "*" || cleaned === "**" || cleaned.startsWith("#")) return null;
2784
- if (ROUTE_ASSET_EXTENSION_RE.test(cleaned)) return null;
2785
- return cleaned.startsWith("/") ? cleaned.replace(/\/+$/g, "") || "/" : `/${cleaned}`;
3122
+ };
3123
+ function sourceLineCount(source) {
3124
+ return Math.max(1, source.replace(/\r?\n$/, "").split(/\r?\n/).length);
2786
3125
  }
2787
- function scanAngularRouter(projectRoot) {
2788
- const routes = /* @__PURE__ */ new Map();
2789
- const files = walkFiles(projectRoot, {
2790
- extensions: /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"])
2791
- });
2792
- for (const file of files) {
2793
- const content = readTextFile(join3(projectRoot, file));
2794
- if (!content) continue;
2795
- const isRouterFile = content.includes("@angular/router") || content.includes("RouterModule.forRoot") || content.includes("provideRouter") || /\bRoutes\s*=/.test(content);
2796
- if (!isRouterFile) continue;
2797
- for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g)) {
2798
- const routePath = normalizeRouterObjectPath(match[1] ?? "");
2799
- if (!routePath || routes.has(routePath)) continue;
2800
- routes.set(routePath, { path: routePath, file, hasLayout: false });
3126
+ function verifyInteractionsInSource(interactions, sources) {
3127
+ if (interactions.length === 0 || sources.size === 0) return [];
3128
+ const unique2 = Array.from(new Set(interactions));
3129
+ const missing = [];
3130
+ for (const interaction of unique2) {
3131
+ const requirement = INTERACTION_SIGNALS[interaction];
3132
+ if (!requirement) {
3133
+ continue;
2801
3134
  }
2802
- }
2803
- return [...routes.values()];
2804
- }
2805
- function firstRouteLiteralMatch(match) {
2806
- return match.slice(1).find((value) => typeof value === "string") ?? null;
2807
- }
2808
- function normalizeDetectedRouteLiterals(value) {
2809
- const withoutHash = value.trim().split("#")[0];
2810
- const cleaned = withoutHash.includes("?") && !withoutHash.endsWith(")?") ? withoutHash.split("?")[0] : withoutHash;
2811
- if (!cleaned || cleaned === "/") return ["/"];
2812
- if (cleaned === "*" || cleaned === "**" || cleaned.startsWith("#")) return [];
2813
- if (cleaned === "/*" || cleaned === "/**") return [];
2814
- if (!cleaned.startsWith("/") || cleaned.startsWith("//")) return [];
2815
- if (ROUTE_ASSET_EXTENSION_RE.test(cleaned)) return [];
2816
- const optionalGroup = cleaned.match(/^\/\(([^()]+)\)\?$/);
2817
- if (optionalGroup) {
2818
- return [
2819
- "/",
2820
- ...optionalGroup[1].split("|").map((segment) => segment.trim()).filter(Boolean).map((segment) => `/${segment}`)
2821
- ];
2822
- }
2823
- const withoutTrailingWildcard = cleaned.endsWith("*") && !cleaned.endsWith("/*") && !/:\w+\*$/.test(cleaned) ? cleaned.slice(0, -1) : cleaned;
2824
- const normalized = withoutTrailingWildcard.replace(/\/+$/g, "") || "/";
2825
- if (normalized === "/*" || normalized === "/**") return [];
2826
- return [normalized];
2827
- }
2828
- function collectRouteLiterals(pattern, content, routes) {
2829
- let count = 0;
2830
- for (const match of content.matchAll(pattern)) {
2831
- for (const route of normalizeDetectedRouteLiterals(match[1] ?? "")) {
2832
- routes.add(route);
2833
- count += 1;
3135
+ let matched = false;
3136
+ for (const source of sources.values()) {
3137
+ matched = requirement.signals.some((signal) => {
3138
+ if (typeof signal === "string") {
3139
+ return source.includes(signal);
3140
+ }
3141
+ signal.lastIndex = 0;
3142
+ return signal.test(source);
3143
+ });
3144
+ if (matched) break;
3145
+ }
3146
+ if (!matched) {
3147
+ missing.push({
3148
+ interaction,
3149
+ suggestion: requirement.suggestion,
3150
+ scannedFiles: sources.size,
3151
+ scannedLocations: [...sources.entries()].slice(0, 8).map(([file, source]) => ({
3152
+ file,
3153
+ startLine: 1,
3154
+ endLine: sourceLineCount(source)
3155
+ })),
3156
+ expectedSignals: requirement.signals.map(
3157
+ (signal) => typeof signal === "string" ? signal : String(signal)
3158
+ )
3159
+ });
2834
3160
  }
2835
3161
  }
2836
- return count;
3162
+ return missing;
2837
3163
  }
2838
- function htmlRouteFromFile(file) {
2839
- let withoutExt = file.slice(0, -extname3(file).length).replace(/\\/g, "/");
2840
- if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
2841
- if (withoutExt === "index" || withoutExt === "docs" || withoutExt === "src" || withoutExt === "public" || withoutExt === "dist") {
2842
- return "/";
2843
- }
2844
- return `/${withoutExt.split("/").filter(Boolean).join("/")}` || "/";
3164
+ function listKnownInteractions() {
3165
+ return Object.keys(INTERACTION_SIGNALS).sort();
2845
3166
  }
2846
- function collectStaticHtmlFiles(dir, projectRoot, files, depth = 0) {
2847
- if (depth > 5 || files.length >= MAX_STATIC_HTML_CANDIDATES) return;
2848
- let entries;
3167
+
3168
+ // src/scan.ts
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";
3171
+ import { decodeHTML, decodeHTMLAttribute } from "entities";
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([
3176
+ ".ts",
3177
+ ".tsx",
3178
+ ".js",
3179
+ ".jsx",
3180
+ ".mjs",
3181
+ ".cjs",
3182
+ ".vue",
3183
+ ".svelte"
3184
+ ]);
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([
3189
+ ".git",
3190
+ ".next",
3191
+ ".nuxt",
3192
+ ".svelte-kit",
3193
+ "coverage",
3194
+ "dist",
3195
+ "build",
3196
+ "node_modules",
3197
+ "out",
3198
+ "target",
3199
+ "vendor"
3200
+ ]);
3201
+ var GITHUB_OWNER_RE = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/i;
3202
+ var GITHUB_REPO_RE = /^[a-z0-9._-]{1,100}$/i;
3203
+ function parseHttpUrl(value) {
2849
3204
  try {
2850
- entries = readdirSync3(dir).sort();
3205
+ const url = new URL(value.trim());
3206
+ return url.protocol === "https:" || url.protocol === "http:" ? url : null;
2851
3207
  } catch {
2852
- return;
2853
- }
2854
- for (const entry of entries) {
2855
- if (files.length >= MAX_STATIC_HTML_CANDIDATES) return;
2856
- if (entry.startsWith(".") || entry === "node_modules") continue;
2857
- const fullPath = join3(dir, entry);
2858
- try {
2859
- const stat = statSync2(fullPath);
2860
- if (stat.isDirectory()) {
2861
- collectStaticHtmlFiles(fullPath, projectRoot, files, depth + 1);
2862
- } else if (stat.isFile() && /\.(?:html|htm)$/i.test(entry)) {
2863
- files.push(relative3(projectRoot, fullPath).replace(/\\/g, "/"));
2864
- }
2865
- } catch {
2866
- }
3208
+ return null;
2867
3209
  }
2868
3210
  }
2869
- function staticHtmlFilePriority(file) {
2870
- const base = file.split("/").pop()?.toLowerCase();
2871
- if (base === "index.html" || base === "index.htm") return 0;
2872
- if (base === "default.html" || base === "default.htm") return 1;
2873
- return 2;
3211
+ function githubPagesOwnerFromHostname(hostname) {
3212
+ const [owner, domain, tld, ...extra] = hostname.toLowerCase().split(".");
3213
+ if (extra.length > 0 || domain !== "github" || tld !== "io" || !owner) return null;
3214
+ return GITHUB_OWNER_RE.test(owner) ? owner : null;
2874
3215
  }
2875
- function scanStaticHtmlRoutes(projectRoot) {
2876
- const routes = /* @__PURE__ */ new Map();
2877
- for (const file of STATIC_HTML_ENTRY_FILES) {
2878
- if (!existsSync3(join3(projectRoot, file))) continue;
2879
- routes.set("/", { path: "/", file, hasLayout: false });
2880
- break;
2881
- }
2882
- for (const dir of STATIC_HTML_ROUTE_DIRS) {
2883
- const fullDir = join3(projectRoot, dir);
2884
- if (!existsSync3(fullDir)) continue;
2885
- const files = [];
2886
- collectStaticHtmlFiles(fullDir, projectRoot, files);
2887
- for (const file of files.sort(
2888
- (a, b) => staticHtmlFilePriority(a) - staticHtmlFilePriority(b) || a.localeCompare(b)
2889
- )) {
2890
- const routePath = htmlRouteFromFile(file);
2891
- if (routes.has(routePath)) continue;
2892
- routes.set(routePath, { path: routePath, file, hasLayout: false });
2893
- }
2894
- }
2895
- return [...routes.values()].slice(0, MAX_REPORT_ROUTES);
3216
+ function normalizeGitHubPathSegment(segment, kind) {
3217
+ if (!segment) return null;
3218
+ const normalized = kind === "repo" ? segment.replace(/\.git$/i, "") : segment;
3219
+ const pattern = kind === "owner" ? GITHUB_OWNER_RE : GITHUB_REPO_RE;
3220
+ return pattern.test(normalized) ? normalized : null;
2896
3221
  }
2897
- function detectPathnameBranchRoutes(content) {
2898
- const routes = /* @__PURE__ */ new Set();
2899
- const comparison = new RegExp(
2900
- `\\b${ROUTE_VARIABLE_NAMES}\\b\\s*(?:===|!==|==|!=)\\s*["'\`](\\/[^"'\`]+)["'\`]`,
2901
- "g"
2902
- );
2903
- const reversedComparison = new RegExp(
2904
- `["'\`](\\/[^"'\`]+)["'\`]\\s*(?:===|!==|==|!=)\\s*\\b${ROUTE_VARIABLE_NAMES}\\b`,
2905
- "g"
2906
- );
2907
- const strongMatches = collectRouteLiterals(comparison, content, routes) + collectRouteLiterals(reversedComparison, content, routes) + collectRouteLiterals(/\bcase\s+["'`](\/[^"'`]+)["'`]\s*:/g, content, routes);
2908
- const hasPathnameSignal = /\b(?:window\.|document\.)?location\.pathname\b|\bpathname\b/.test(
2909
- content
2910
- );
2911
- if (strongMatches > 0 || hasPathnameSignal) {
2912
- collectRouteLiterals(/\b(?:href|to)\s*=\s*["'`](\/[^"'`]+)["'`]/g, content, routes);
2913
- }
2914
- return [...routes];
3222
+ var WEB_FRAMEWORKS = /* @__PURE__ */ new Set([
3223
+ "angular",
3224
+ "astro",
3225
+ "html",
3226
+ "nextjs",
3227
+ "nuxt",
3228
+ "react",
3229
+ "solid",
3230
+ "svelte",
3231
+ "vue"
3232
+ ]);
3233
+ function isRecord2(value) {
3234
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2915
3235
  }
2916
- function detectDeclarativeRouteSpecRoutes(content) {
2917
- const routes = /* @__PURE__ */ new Set();
2918
- 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);
2919
- if (!hasRouteSpecSignal) return [];
2920
- collectRouteLiterals(
2921
- /\broute\s*\(\s*["'`][^"'`]*["'`]\s*,\s*["'`](\/[^"'`]*)["'`]\s*,\s*page\s*\(/g,
2922
- content,
2923
- routes
2924
- );
2925
- return [...routes];
3236
+ function readTextFile2(path, maxBytes = MAX_FILE_READ_BYTES2) {
3237
+ try {
3238
+ const stat = statSync3(path);
3239
+ if (!stat.isFile() || stat.size > maxBytes) return null;
3240
+ return readFileSync5(path, "utf-8");
3241
+ } catch {
3242
+ return null;
3243
+ }
2926
3244
  }
2927
- function scanRoutes(projectRoot, detection) {
2928
- const appRoutes = ["src/app", "app"].flatMap(
2929
- (dir) => existsSync3(join3(projectRoot, dir)) ? walkNextAppRoutes(join3(projectRoot, dir), projectRoot, []) : []
2930
- );
2931
- const pagesRoutes = ["src/pages", "pages"].flatMap((dir) => scanFileRoutes(projectRoot, dir));
2932
- if (detection.framework === "nextjs") {
2933
- if (appRoutes.length > 0 && pagesRoutes.length > 0)
2934
- return { strategy: "mixed-next-router", routes: [...appRoutes, ...pagesRoutes] };
2935
- if (appRoutes.length > 0) return { strategy: "app-router", routes: appRoutes };
2936
- if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
2937
- }
2938
- if (detection.framework === "svelte") {
2939
- const routes = existsSync3(join3(projectRoot, "src/routes")) ? walkSvelteKitRoutes(join3(projectRoot, "src/routes"), projectRoot, []) : [];
2940
- if (routes.length > 0) return { strategy: "sveltekit-router", routes };
2941
- }
2942
- if (detection.framework === "nuxt") {
2943
- const routes = ["pages", "app/pages"].flatMap(
2944
- (dir) => scanFileRoutes(projectRoot, dir, /* @__PURE__ */ new Set([".vue"]))
2945
- );
2946
- if (routes.length > 0) return { strategy: "nuxt-router", routes };
2947
- }
2948
- if (detection.framework === "vue") {
2949
- const router = scanReactRouter(projectRoot);
2950
- if (router.routes.length > 0) return { strategy: "vue-router", routes: router.routes };
2951
- }
2952
- if (detection.framework === "angular") {
2953
- const routes = scanAngularRouter(projectRoot);
2954
- if (routes.length > 0) return { strategy: "angular-router", routes };
2955
- }
2956
- const reactRouter = scanReactRouter(projectRoot);
2957
- if (reactRouter.routes.length > 0)
2958
- return { strategy: "react-router", routes: reactRouter.routes };
2959
- if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
2960
- const staticHtmlRoutes = scanStaticHtmlRoutes(projectRoot);
2961
- if (staticHtmlRoutes.length > 0) return { strategy: "static-html", routes: staticHtmlRoutes };
2962
- return { strategy: "none", routes: [] };
2963
- }
2964
- function countComponents(projectRoot, routes) {
2965
- const sourceFiles = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
2966
- const componentFiles = sourceFiles.filter((file) => {
2967
- const base = file.split("/").pop() ?? file;
2968
- return /^[A-Z][\w-]*\.(tsx|ts|jsx|js|vue|svelte)$/.test(base);
2969
- });
2970
- const directories = [
2971
- ...new Set(
2972
- componentFiles.map((file) => file.split("/").slice(0, -1).join("/")).filter(Boolean)
2973
- )
2974
- ].slice(0, 16);
2975
- return {
2976
- pageCount: routes.routes.length,
2977
- componentCount: componentFiles.length,
2978
- directories
2979
- };
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);
3249
+ if (!content) return { value: null, present: true, valid: false };
3250
+ try {
3251
+ const parsed = JSON.parse(content);
3252
+ return {
3253
+ value: isRecord2(parsed) ? parsed : null,
3254
+ present: true,
3255
+ valid: isRecord2(parsed)
3256
+ };
3257
+ } catch {
3258
+ return { value: null, present: true, valid: false };
3259
+ }
2980
3260
  }
2981
- function extractCssEvidence(content) {
2982
- const variableMatches = [...content.matchAll(/--[\w-]+\s*:/g)];
2983
- const colorMatches = [...content.matchAll(/#[0-9a-fA-F]{3,8}\b|rgb[a]?\(|hsl[a]?\(/g)];
2984
- const themeSignals = /* @__PURE__ */ new Set();
2985
- if (/\bdark\b|color-scheme:\s*dark|\[data-theme=['"]dark['"]|\.dark\b/.test(content))
2986
- themeSignals.add("dark mode");
2987
- if (/\[data-theme=|data-theme|theme-\w+/.test(content)) themeSignals.add("theme selector");
2988
- if (/prefers-color-scheme/.test(content)) themeSignals.add("system color preference");
2989
- return {
2990
- variables: variableMatches.length,
2991
- colors: colorMatches.length,
2992
- dark: themeSignals.has("dark mode") || themeSignals.has("system color preference"),
2993
- themeSignals: [...themeSignals]
2994
- };
3261
+ function shouldSkipDir2(name) {
3262
+ return SKIP_DIRS2.has(name) || name.startsWith(".") && name !== ".github";
2995
3263
  }
2996
- function scanStyling(projectRoot, detection) {
2997
- const deps = detection.dependencies;
2998
- const cssFiles = walkFiles(projectRoot, { extensions: STYLE_EXTENSIONS });
2999
- const themeSignals = /* @__PURE__ */ new Set();
3000
- let cssVariableCount = 0;
3001
- let colorTokenCount = 0;
3002
- let darkMode = false;
3003
- let configFile = null;
3004
- let approach = "unknown";
3005
- const tailwindConfig = [
3006
- "tailwind.config.js",
3007
- "tailwind.config.ts",
3008
- "tailwind.config.mjs",
3009
- "tailwind.config.cjs"
3010
- ].find((file) => existsSync3(join3(projectRoot, file)));
3011
- if (tailwindConfig || deps.tailwindcss) {
3012
- approach = "tailwind";
3013
- configFile = tailwindConfig ?? "package.json";
3014
- } else if (deps["@decantr/css"] || existsSync3(join3(projectRoot, "src/styles/tokens.css")) && existsSync3(join3(projectRoot, "src/styles/treatments.css"))) {
3015
- approach = "decantr-css";
3016
- configFile = deps["@decantr/css"] ? "package.json" : "src/styles/tokens.css";
3017
- } else if (deps.bootstrap) {
3018
- approach = "bootstrap";
3019
- configFile = "package.json";
3020
- } else if (deps["@mui/material"]) {
3021
- approach = "mui";
3022
- configFile = "package.json";
3023
- } else if (deps["@chakra-ui/react"]) {
3024
- approach = "chakra";
3025
- configFile = "package.json";
3026
- } else if (cssFiles.some((file) => file.endsWith(".module.css"))) {
3027
- approach = "css-modules";
3028
- } else if (cssFiles.length > 0) {
3029
- approach = "css";
3030
- }
3031
- for (const file of cssFiles.slice(0, 80)) {
3032
- const content = readTextFile(join3(projectRoot, file), 256 * 1024);
3033
- if (!content) continue;
3034
- const evidence = extractCssEvidence(content);
3035
- cssVariableCount += evidence.variables;
3036
- colorTokenCount += evidence.colors;
3037
- darkMode ||= evidence.dark;
3038
- for (const signal of evidence.themeSignals) themeSignals.add(signal);
3264
+ function walkFiles2(projectRoot, options = {}) {
3265
+ const files = [];
3266
+ function walk(dir) {
3267
+ if (files.length >= MAX_WALK_FILES2) return;
3268
+ let entries;
3269
+ try {
3270
+ entries = readdirSync4(dir);
3271
+ } catch {
3272
+ return;
3273
+ }
3274
+ for (const entry of entries) {
3275
+ if (files.length >= MAX_WALK_FILES2) return;
3276
+ const fullPath = join4(dir, entry);
3277
+ let stat;
3278
+ try {
3279
+ stat = statSync3(fullPath);
3280
+ } catch {
3281
+ continue;
3282
+ }
3283
+ if (stat.isDirectory()) {
3284
+ if (shouldSkipDir2(entry) && !options.includeHidden) continue;
3285
+ walk(fullPath);
3286
+ continue;
3287
+ }
3288
+ if (!stat.isFile()) continue;
3289
+ const rel = relative4(projectRoot, fullPath).replace(/\\/g, "/");
3290
+ const ext = extname4(entry).toLowerCase();
3291
+ if (!options.extensions || options.extensions.has(ext)) {
3292
+ files.push(rel);
3293
+ }
3294
+ }
3039
3295
  }
3040
- return {
3041
- approach,
3042
- configFile,
3043
- cssVariableCount,
3044
- colorTokenCount,
3045
- darkMode,
3046
- themeSignals: [...themeSignals]
3047
- };
3048
- }
3049
- function findAssistantRules(projectRoot) {
3050
- return RULE_FILES.filter((file) => existsSync3(join3(projectRoot, file)));
3296
+ walk(projectRoot);
3297
+ return files;
3051
3298
  }
3052
3299
  function scanStaticHosting(projectRoot, detection) {
3053
- const packageRead = readPackageJson(projectRoot);
3300
+ const packageRead = readPackageJson2(projectRoot);
3054
3301
  const pkg = packageRead.value;
3055
3302
  const evidence = [];
3056
3303
  const homepageUrl = typeof pkg?.homepage === "string" ? pkg.homepage : null;
@@ -3064,27 +3311,27 @@ function scanStaticHosting(projectRoot, detection) {
3064
3311
  if (detection.dependencies["gh-pages"] || pkg?.scripts?.deploy?.includes("gh-pages")) {
3065
3312
  evidence.push("package scripts or dependencies reference gh-pages");
3066
3313
  }
3067
- if (existsSync3(join3(projectRoot, "docs", "index.html")))
3314
+ if (existsSync4(join4(projectRoot, "docs", "index.html")))
3068
3315
  evidence.push("docs/index.html can serve GitHub Pages");
3069
- 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"))) {
3070
3317
  evidence.push("404.html fallback is present");
3071
3318
  }
3072
- const workflowDir = join3(projectRoot, ".github", "workflows");
3073
- if (existsSync3(workflowDir)) {
3074
- 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(
3075
3322
  0,
3076
3323
  20
3077
3324
  )) {
3078
- const content = readTextFile(join3(workflowDir, file));
3325
+ const content = readTextFile2(join4(workflowDir, file));
3079
3326
  if (content && /pages|gh-pages|upload-pages-artifact|deploy-pages/i.test(content)) {
3080
3327
  evidence.push(`GitHub Pages workflow hint: .github/workflows/${file}`);
3081
3328
  break;
3082
3329
  }
3083
3330
  }
3084
3331
  }
3085
- const sourceFiles = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
3332
+ const sourceFiles = walkFiles2(projectRoot, { extensions: SOURCE_EXTENSIONS2 });
3086
3333
  for (const file of sourceFiles.slice(0, 200)) {
3087
- const content = readTextFile(join3(projectRoot, file), 128 * 1024);
3334
+ const content = readTextFile2(join4(projectRoot, file), 128 * 1024);
3088
3335
  if (!content) continue;
3089
3336
  if (content.includes("HashRouter") || content.includes("createHashRouter")) {
3090
3337
  hashRouting = true;
@@ -3093,7 +3340,7 @@ function scanStaticHosting(projectRoot, detection) {
3093
3340
  }
3094
3341
  }
3095
3342
  for (const config of ["vite.config.ts", "vite.config.js", "vite.config.mjs"]) {
3096
- const content = readTextFile(join3(projectRoot, config), 128 * 1024);
3343
+ const content = readTextFile2(join4(projectRoot, config), 128 * 1024);
3097
3344
  const baseMatch = content?.match(/\bbase\s*:\s*['"]([^'"]+)['"]/);
3098
3345
  if (baseMatch?.[1]) {
3099
3346
  basePath = baseMatch[1];
@@ -3138,33 +3385,6 @@ function buildApplicability(detection, routes, components) {
3138
3385
  reasons: ["A web framework was detected, but route/component evidence is thin."]
3139
3386
  };
3140
3387
  }
3141
- function buildConfidence(applicability, detection, routes, styling) {
3142
- let score = 20;
3143
- const reasons = [];
3144
- if (detection.packageJsonPresent && detection.packageJsonValid) {
3145
- score += 15;
3146
- reasons.push("package.json was readable");
3147
- }
3148
- if (WEB_FRAMEWORKS.has(detection.framework)) {
3149
- score += 25;
3150
- reasons.push(`${detection.framework} framework signal found`);
3151
- }
3152
- if (routes.routes.length > 0) {
3153
- score += 20;
3154
- reasons.push(`${routes.routes.length} route signal(s) found`);
3155
- }
3156
- if (styling.approach !== "unknown") {
3157
- score += 10;
3158
- reasons.push(`${styling.approach} styling signal found`);
3159
- }
3160
- if (applicability.status === "not_applicable") score = Math.min(score, 35);
3161
- const clamped = Math.max(5, Math.min(98, score));
3162
- return {
3163
- score: clamped,
3164
- level: clamped >= 75 ? "high" : clamped >= 45 ? "medium" : "low",
3165
- reasons: reasons.length > 0 ? reasons : ["Only weak project signals were found."]
3166
- };
3167
- }
3168
3388
  function buildFindings(input) {
3169
3389
  const findings = [];
3170
3390
  const { detection, routes, styling, hosting, assistantRules, applicability, pagesProbe } = input;
@@ -3283,22 +3503,89 @@ function buildCommands(applicability) {
3283
3503
  }
3284
3504
  return commands;
3285
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
+ }
3286
3562
  async function scanProject(projectRoot, options = {}) {
3287
- const detection = detectProject(projectRoot);
3288
- const routes = scanRoutes(projectRoot, detection);
3289
- routes.routes = routes.routes.slice(0, MAX_REPORT_ROUTES);
3290
- const components = countComponents(projectRoot, routes);
3291
- const styling = scanStyling(projectRoot, detection);
3292
- 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;
3293
3580
  const staticHosting = scanStaticHosting(projectRoot, detection);
3294
3581
  const pagesProbe = options.pagesProbe ?? null;
3295
3582
  const applicability = buildApplicability(detection, routes, components);
3296
- const confidence = buildConfidence(applicability, detection, routes, styling);
3583
+ const confidence = discovery.confidence;
3297
3584
  const rawInput = options.input ?? { kind: "local", value: "." };
3298
3585
  const input = rawInput.kind === "local" && isAbsolute(rawInput.value) ? { ...rawInput, value: "." } : rawInput;
3299
3586
  return {
3300
- $schema: SCAN_REPORT_SCHEMA_URL,
3301
- schemaVersion: "scan-report.v1",
3587
+ $schema: SCAN_REPORT_V2_SCHEMA_URL,
3588
+ schemaVersion: "scan-report.v2",
3302
3589
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3303
3590
  input,
3304
3591
  source: {
@@ -3315,12 +3602,19 @@ async function scanProject(projectRoot, options = {}) {
3315
3602
  hasTypeScript: detection.hasTypeScript,
3316
3603
  hasTailwind: detection.hasTailwind,
3317
3604
  hasDecantr: detection.hasDecantr,
3318
- packageName: detection.packageName
3605
+ packageName: detection.packageName,
3606
+ evidence: discovery.project.evidence,
3607
+ workspaceScope: discovery.workspace.scope,
3608
+ projectPath: discovery.workspace.projectPath
3319
3609
  },
3320
3610
  routes: {
3321
3611
  strategy: routes.strategy,
3322
3612
  count: routes.routes.length,
3323
- 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)
3324
3618
  },
3325
3619
  components,
3326
3620
  styling,
@@ -3329,6 +3623,7 @@ async function scanProject(projectRoot, options = {}) {
3329
3623
  ruleFiles: assistantRules
3330
3624
  },
3331
3625
  pagesProbe,
3626
+ discovery: scanDiscoverySummary(discovery),
3332
3627
  findings: buildFindings({
3333
3628
  detection,
3334
3629
  routes,
@@ -3343,7 +3638,7 @@ async function scanProject(projectRoot, options = {}) {
3343
3638
  sourceUploaded: input.kind !== "local",
3344
3639
  persistedByDecantr: false,
3345
3640
  notes: [
3346
- "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.",
3347
3642
  "Hosted scans use temporary public-repo checkouts and return an ephemeral report."
3348
3643
  ]
3349
3644
  }
@@ -3351,8 +3646,8 @@ async function scanProject(projectRoot, options = {}) {
3351
3646
  }
3352
3647
  function createUnavailableScanReport(input) {
3353
3648
  return {
3354
- $schema: SCAN_REPORT_SCHEMA_URL,
3355
- schemaVersion: "scan-report.v1",
3649
+ $schema: SCAN_REPORT_V2_SCHEMA_URL,
3650
+ schemaVersion: "scan-report.v2",
3356
3651
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3357
3652
  input: input.scanInput,
3358
3653
  source: {
@@ -3377,10 +3672,29 @@ function createUnavailableScanReport(input) {
3377
3672
  hasTypeScript: false,
3378
3673
  hasTailwind: false,
3379
3674
  hasDecantr: false,
3380
- 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."]
3381
3697
  },
3382
- routes: { strategy: "none", count: 0, items: [] },
3383
- components: { pageCount: 0, componentCount: 0, directories: [] },
3384
3698
  styling: {
3385
3699
  approach: "unknown",
3386
3700
  configFile: null,
@@ -3398,6 +3712,20 @@ function createUnavailableScanReport(input) {
3398
3712
  },
3399
3713
  assistant: { ruleFiles: [] },
3400
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
+ },
3401
3729
  findings: [
3402
3730
  {
3403
3731
  id: "source-unavailable",
@@ -3442,7 +3770,7 @@ async function probePublishedSite(url, options = {}) {
3442
3770
  "User-Agent": "Decantr-Scan/1.0 (+https://decantr.ai/scan)"
3443
3771
  }
3444
3772
  });
3445
- const text = (await response.text()).slice(0, MAX_FILE_READ_BYTES);
3773
+ const text = (await response.text()).slice(0, MAX_FILE_READ_BYTES2);
3446
3774
  const titleMatch = text.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
3447
3775
  const descriptionTag = text.match(/<meta\b[^>]*(?:name|property)=["']description["'][^>]*>/i)?.[0] ?? null;
3448
3776
  const canonicalTag = text.match(/<link\b[^>]*rel=["']canonical["'][^>]*>/i)?.[0] ?? null;
@@ -3543,8 +3871,8 @@ function resolveGitHubScanInput(input) {
3543
3871
  import * as ts4 from "typescript";
3544
3872
 
3545
3873
  // src/source/inventory.ts
3546
- import { existsSync as existsSync4, readdirSync as readdirSync4, realpathSync, statSync as statSync3 } from "fs";
3547
- 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";
3548
3876
  import * as ts3 from "typescript";
3549
3877
  var DEFAULT_MAX_FILES = 5e3;
3550
3878
  var DEFAULT_MAX_FILE_SIZE_BYTES = 512 * 1024;
@@ -3575,7 +3903,7 @@ function normalizeSourcePath(path) {
3575
3903
  return path.replace(/\\/g, "/");
3576
3904
  }
3577
3905
  function isPathInsideProject(projectRoot, absolutePath) {
3578
- const normalizedRelative = normalizeSourcePath(relative4(projectRoot, absolutePath));
3906
+ const normalizedRelative = normalizeSourcePath(relative5(projectRoot, absolutePath));
3579
3907
  return normalizedRelative === "" || !normalizedRelative.startsWith("../") && normalizedRelative !== "..";
3580
3908
  }
3581
3909
  function isSupportedSourceExtension(extension, extensions) {
@@ -3583,7 +3911,7 @@ function isSupportedSourceExtension(extension, extensions) {
3583
3911
  return extensions ? extensions.map((entry) => entry.toLowerCase()).includes(normalized) : DEFAULT_EXTENSIONS.has(normalized);
3584
3912
  }
3585
3913
  function sourceKindFromPath(path) {
3586
- const extension = extname4(path).toLowerCase();
3914
+ const extension = extname5(path).toLowerCase();
3587
3915
  switch (extension) {
3588
3916
  case ".ts":
3589
3917
  return "ts";
@@ -3647,12 +3975,12 @@ function shouldSkipFile(relativePath, options) {
3647
3975
  return false;
3648
3976
  }
3649
3977
  function inventoryFile(projectRoot, absolutePath) {
3650
- const extension = extname4(absolutePath).toLowerCase();
3978
+ const extension = extname5(absolutePath).toLowerCase();
3651
3979
  const kind = sourceKindFromPath(absolutePath);
3652
3980
  const language = sourceLanguageFromPath(absolutePath);
3653
3981
  if (!kind || !language) return null;
3654
- const relativePath = normalizeSourcePath(relative4(projectRoot, absolutePath) || absolutePath);
3655
- const sizeBytes = statSync3(absolutePath).size;
3982
+ const relativePath = normalizeSourcePath(relative5(projectRoot, absolutePath) || absolutePath);
3983
+ const sizeBytes = statSync4(absolutePath).size;
3656
3984
  return {
3657
3985
  absolutePath,
3658
3986
  relativePath,
@@ -3671,19 +3999,19 @@ function compareSkipped(a, b) {
3671
3999
  return a.path.localeCompare(b.path) || a.reason.localeCompare(b.reason);
3672
4000
  }
3673
4001
  function createSourceInventory(projectRoot, options = {}) {
3674
- const root = realpathSync(resolve(projectRoot));
4002
+ const root = realpathSync(resolve2(projectRoot));
3675
4003
  const files = [];
3676
4004
  const seenFiles = /* @__PURE__ */ new Set();
3677
4005
  const skipped = [];
3678
4006
  const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
3679
4007
  const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
3680
4008
  const requestedRoots = options.roots?.length ? [...options.roots] : ["."];
3681
- 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));
3682
4010
  const addSkipped = (path, reason) => {
3683
- skipped.push({ path: normalizeSourcePath(relative4(root, path) || path), reason });
4011
+ skipped.push({ path: normalizeSourcePath(relative5(root, path) || path), reason });
3684
4012
  };
3685
4013
  const addFile = (absolutePath) => {
3686
- const normalized = normalizeSourcePath(resolve(absolutePath));
4014
+ const normalized = normalizeSourcePath(resolve2(absolutePath));
3687
4015
  if (seenFiles.has(normalized)) return;
3688
4016
  const sourceFile = inventoryFile(root, absolutePath);
3689
4017
  if (!sourceFile) return;
@@ -3695,16 +4023,16 @@ function createSourceInventory(projectRoot, options = {}) {
3695
4023
  addSkipped(directory, "walk-limit");
3696
4024
  return;
3697
4025
  }
3698
- const entries = readdirSync4(directory, { withFileTypes: true }).sort(
4026
+ const entries = readdirSync5(directory, { withFileTypes: true }).sort(
3699
4027
  (a, b) => a.name.localeCompare(b.name)
3700
4028
  );
3701
4029
  for (const entry of entries) {
3702
4030
  if (files.length >= maxFiles) {
3703
- addSkipped(join4(directory, entry.name), "walk-limit");
4031
+ addSkipped(join5(directory, entry.name), "walk-limit");
3704
4032
  continue;
3705
4033
  }
3706
- const absolutePath = join4(directory, entry.name);
3707
- const relativePath = normalizeSourcePath(relative4(root, absolutePath) || absolutePath);
4034
+ const absolutePath = join5(directory, entry.name);
4035
+ const relativePath = normalizeSourcePath(relative5(root, absolutePath) || absolutePath);
3708
4036
  if (entry.isSymbolicLink()) {
3709
4037
  addSkipped(absolutePath, "symlink");
3710
4038
  continue;
@@ -3718,13 +4046,13 @@ function createSourceInventory(projectRoot, options = {}) {
3718
4046
  continue;
3719
4047
  }
3720
4048
  if (!entry.isFile()) continue;
3721
- const extension = extname4(entry.name).toLowerCase();
4049
+ const extension = extname5(entry.name).toLowerCase();
3722
4050
  if (!isSupportedSourceExtension(extension, options.extensions)) continue;
3723
4051
  if (shouldSkipFile(relativePath, options)) {
3724
4052
  addSkipped(absolutePath, "ignored-file");
3725
4053
  continue;
3726
4054
  }
3727
- const stat = statSync3(absolutePath);
4055
+ const stat = statSync4(absolutePath);
3728
4056
  if (stat.size > maxFileSizeBytes) {
3729
4057
  addSkipped(absolutePath, "oversized");
3730
4058
  continue;
@@ -3737,18 +4065,18 @@ function createSourceInventory(projectRoot, options = {}) {
3737
4065
  addSkipped(rootPath, "walk-limit");
3738
4066
  continue;
3739
4067
  }
3740
- const stat = statSync3(rootPath);
4068
+ const stat = statSync4(rootPath);
3741
4069
  if (stat.isDirectory()) {
3742
- const relativeRoot = normalizeSourcePath(relative4(root, rootPath));
4070
+ const relativeRoot = normalizeSourcePath(relative5(root, rootPath));
3743
4071
  if (relativeRoot && shouldSkipDirectory(relativeRoot, options)) {
3744
4072
  addSkipped(rootPath, "ignored-directory");
3745
4073
  continue;
3746
4074
  }
3747
4075
  walk(rootPath);
3748
4076
  } else if (stat.isFile()) {
3749
- const extension = extname4(rootPath).toLowerCase();
4077
+ const extension = extname5(rootPath).toLowerCase();
3750
4078
  if (!isSupportedSourceExtension(extension, options.extensions)) continue;
3751
- const relativeFile = normalizeSourcePath(relative4(root, rootPath));
4079
+ const relativeFile = normalizeSourcePath(relative5(root, rootPath));
3752
4080
  if (shouldSkipFile(relativeFile, options)) {
3753
4081
  addSkipped(rootPath, "ignored-file");
3754
4082
  continue;
@@ -3854,8 +4182,8 @@ function extractSourceStringLiterals(sourceFile, options = {}) {
3854
4182
  }
3855
4183
 
3856
4184
  // src/source/program.ts
3857
- import { existsSync as existsSync5, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
3858
- 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";
3859
4187
  import * as ts5 from "typescript";
3860
4188
  var RESOLUTION_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
3861
4189
  var DEFAULT_COMPILER_OPTIONS = {
@@ -3875,11 +4203,11 @@ var DEFAULT_COMPILER_OPTIONS = {
3875
4203
  function findProjectTsConfig(projectRoot, tsconfigPath) {
3876
4204
  if (tsconfigPath === null) return null;
3877
4205
  if (tsconfigPath) {
3878
- const absolutePath = isAbsolute3(tsconfigPath) ? resolve2(tsconfigPath) : resolve2(projectRoot, tsconfigPath);
3879
- return existsSync5(absolutePath) ? absolutePath : null;
4206
+ const absolutePath = isAbsolute3(tsconfigPath) ? resolve3(tsconfigPath) : resolve3(projectRoot, tsconfigPath);
4207
+ return existsSync6(absolutePath) ? absolutePath : null;
3880
4208
  }
3881
- const localTsConfig = join5(projectRoot, "tsconfig.json");
3882
- return existsSync5(localTsConfig) ? localTsConfig : null;
4209
+ const localTsConfig = join6(projectRoot, "tsconfig.json");
4210
+ return existsSync6(localTsConfig) ? localTsConfig : null;
3883
4211
  }
3884
4212
  function fallbackCompilerConfig(tsconfigPath, options, diagnostics = []) {
3885
4213
  return {
@@ -3899,13 +4227,13 @@ function readCompilerConfig(projectRoot, options) {
3899
4227
  if (!tsconfigPath) return fallbackCompilerConfig(null, options);
3900
4228
  const parsedJson = ts5.parseConfigFileTextToJson(
3901
4229
  tsconfigPath,
3902
- readFileSync5(tsconfigPath, "utf-8")
4230
+ readFileSync6(tsconfigPath, "utf-8")
3903
4231
  );
3904
4232
  if (parsedJson.error) return fallbackCompilerConfig(tsconfigPath, options, [parsedJson.error]);
3905
4233
  const parsed = ts5.parseJsonConfigFileContent(
3906
4234
  parsedJson.config,
3907
4235
  ts5.sys,
3908
- dirname(tsconfigPath),
4236
+ dirname2(tsconfigPath),
3909
4237
  DEFAULT_COMPILER_OPTIONS,
3910
4238
  tsconfigPath
3911
4239
  );
@@ -3924,7 +4252,7 @@ function readCompilerConfig(projectRoot, options) {
3924
4252
  }
3925
4253
  function inventoryFileByAbsolutePath(inventory) {
3926
4254
  return new Map(
3927
- inventory.files.map((file) => [normalizeSourcePath(resolve2(file.absolutePath)), file])
4255
+ inventory.files.map((file) => [normalizeSourcePath(resolve3(file.absolutePath)), file])
3928
4256
  );
3929
4257
  }
3930
4258
  function createProjectSourceProgram(projectRoot, options = {}) {
@@ -3947,13 +4275,13 @@ function createProjectSourceProgram(projectRoot, options = {}) {
3947
4275
  }
3948
4276
  function getProjectSourceFile(context, pathOrSourceFile) {
3949
4277
  if (typeof pathOrSourceFile !== "string") return pathOrSourceFile;
3950
- const absolutePath = isAbsolute3(pathOrSourceFile) ? resolve2(pathOrSourceFile) : resolve2(context.projectRoot, pathOrSourceFile);
4278
+ const absolutePath = isAbsolute3(pathOrSourceFile) ? resolve3(pathOrSourceFile) : resolve3(context.projectRoot, pathOrSourceFile);
3951
4279
  const normalized = normalizeSourcePath(absolutePath);
3952
- return context.program.getSourceFiles().find((sourceFile) => normalizeSourcePath(resolve2(sourceFile.fileName)) === normalized);
4280
+ return context.program.getSourceFiles().find((sourceFile) => normalizeSourcePath(resolve3(sourceFile.fileName)) === normalized);
3953
4281
  }
3954
4282
  function sourceFileRelativePath(context, sourceFile) {
3955
4283
  return normalizeSourcePath(
3956
- relative5(context.projectRoot, sourceFile.fileName) || sourceFile.fileName
4284
+ relative6(context.projectRoot, sourceFile.fileName) || sourceFile.fileName
3957
4285
  );
3958
4286
  }
3959
4287
  function sourceLocationLineAndColumn(sourceFile, node) {
@@ -3989,32 +4317,32 @@ function manualResolutionCandidates(context, importerPath, specifier) {
3989
4317
  const candidates = [];
3990
4318
  const addCandidates = (base) => {
3991
4319
  candidates.push(base);
3992
- if (extname5(base)) return;
4320
+ if (extname6(base)) return;
3993
4321
  for (const extension of RESOLUTION_EXTENSIONS) candidates.push(`${base}${extension}`);
3994
4322
  for (const extension of RESOLUTION_EXTENSIONS) {
3995
- candidates.push(join5(base, `index${extension}`));
4323
+ candidates.push(join6(base, `index${extension}`));
3996
4324
  }
3997
4325
  };
3998
4326
  if (specifier.startsWith(".") || specifier.startsWith("/")) {
3999
- 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);
4000
4328
  addCandidates(base);
4001
4329
  } else if (specifier.startsWith("@/")) {
4002
- addCandidates(resolve2(context.projectRoot, specifier.slice(2)));
4003
- 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)));
4004
4332
  }
4005
4333
  return candidates;
4006
4334
  }
4007
4335
  function resolveFromInventory(context, source, importerPath) {
4008
4336
  const inventoryByPath = inventoryFileByAbsolutePath(context.inventory);
4009
4337
  for (const candidate of manualResolutionCandidates(context, importerPath, source)) {
4010
- const absolutePath = normalizeSourcePath(resolve2(candidate));
4338
+ const absolutePath = normalizeSourcePath(resolve3(candidate));
4011
4339
  const inventoryFile2 = inventoryByPath.get(absolutePath);
4012
- if (!inventoryFile2 || !existsSync5(inventoryFile2.absolutePath) || !statSync4(inventoryFile2.absolutePath).isFile()) {
4340
+ if (!inventoryFile2 || !existsSync6(inventoryFile2.absolutePath) || !statSync5(inventoryFile2.absolutePath).isFile()) {
4013
4341
  continue;
4014
4342
  }
4015
4343
  return {
4016
4344
  source,
4017
- importer: normalizeSourcePath(relative5(context.projectRoot, importerPath) || importerPath),
4345
+ importer: normalizeSourcePath(relative6(context.projectRoot, importerPath) || importerPath),
4018
4346
  kind: "project-local",
4019
4347
  resolvedFileName: inventoryFile2.absolutePath,
4020
4348
  relativePath: inventoryFile2.relativePath,
@@ -4028,9 +4356,9 @@ function resolveFromInventory(context, source, importerPath) {
4028
4356
  }
4029
4357
  function resolveSourceImport(context, importer, source) {
4030
4358
  const sourceFile = getProjectSourceFile(context, importer);
4031
- 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));
4032
4360
  const importerRelativePath = normalizeSourcePath(
4033
- relative5(context.projectRoot, importerPath) || importerPath
4361
+ relative6(context.projectRoot, importerPath) || importerPath
4034
4362
  );
4035
4363
  const inventoryByPath = inventoryFileByAbsolutePath(context.inventory);
4036
4364
  const resolved = ts5.resolveModuleName(
@@ -4040,7 +4368,7 @@ function resolveSourceImport(context, importer, source) {
4040
4368
  ts5.sys
4041
4369
  ).resolvedModule;
4042
4370
  if (resolved) {
4043
- const absolutePath = normalizeSourcePath(resolve2(resolved.resolvedFileName));
4371
+ const absolutePath = normalizeSourcePath(resolve3(resolved.resolvedFileName));
4044
4372
  const inventoryFile2 = inventoryByPath.get(absolutePath);
4045
4373
  if (inventoryFile2 && isPathInsideProject(context.projectRoot, absolutePath)) {
4046
4374
  return {
@@ -4061,7 +4389,7 @@ function resolveSourceImport(context, importer, source) {
4061
4389
  kind: hasExternalPackageShape(source) ? "external" : "unresolved",
4062
4390
  resolvedFileName: resolved.resolvedFileName,
4063
4391
  relativePath: null,
4064
- extension: extname5(resolved.resolvedFileName).toLowerCase() || null,
4392
+ extension: extname6(resolved.resolvedFileName).toLowerCase() || null,
4065
4393
  isProjectLocal: false,
4066
4394
  isExternal: hasExternalPackageShape(source),
4067
4395
  failed: !hasExternalPackageShape(source)
@@ -4229,10 +4557,10 @@ function resolveSourceSymbolOrigin(context, pathOrSourceFile, localName, options
4229
4557
  if (declarations.length === 0) return null;
4230
4558
  const projectFiles = inventoryFileByAbsolutePath(context.inventory);
4231
4559
  const declaration = declarations.find(
4232
- (entry) => projectFiles.has(normalizeSourcePath(resolve2(entry.getSourceFile().fileName)))
4560
+ (entry) => projectFiles.has(normalizeSourcePath(resolve3(entry.getSourceFile().fileName)))
4233
4561
  ) ?? declarations[0];
4234
4562
  const declarationFile = declaration.getSourceFile();
4235
- const declarationPath = normalizeSourcePath(resolve2(declarationFile.fileName));
4563
+ const declarationPath = normalizeSourcePath(resolve3(declarationFile.fileName));
4236
4564
  const projectFile = projectFiles.get(declarationPath);
4237
4565
  const isProjectLocal = Boolean(projectFile);
4238
4566
  if (!isProjectLocal && !options.includeExternal) return null;
@@ -4256,7 +4584,7 @@ var VERIFICATION_SCHEMA_URLS = {
4256
4584
  projectHealth: PROJECT_HEALTH_REPORT_V2_SCHEMA_URL,
4257
4585
  decantrCi: DECANTR_CI_REPORT_V2_SCHEMA_URL,
4258
4586
  evidenceBundle: EVIDENCE_BUNDLE_V2_SCHEMA_URL,
4259
- scanReport: "https://decantr.ai/schemas/scan-report.v1.json",
4587
+ scanReport: "https://decantr.ai/schemas/scan-report.v2.json",
4260
4588
  workspaceHealth: WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL,
4261
4589
  fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
4262
4590
  showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
@@ -4266,16 +4594,16 @@ function hashString(value) {
4266
4594
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
4267
4595
  }
4268
4596
  function hashFile(path) {
4269
- if (!existsSync6(path)) return null;
4597
+ if (!existsSync7(path)) return null;
4270
4598
  try {
4271
- return createHash("sha256").update(readFileSync6(path)).digest("hex");
4599
+ return createHash("sha256").update(readFileSync7(path)).digest("hex");
4272
4600
  } catch {
4273
4601
  return null;
4274
4602
  }
4275
4603
  }
4276
4604
  function provenanceEntry(projectRoot, relativePath) {
4277
- const path = join6(projectRoot, relativePath);
4278
- if (!existsSync6(path)) {
4605
+ const path = join7(projectRoot, relativePath);
4606
+ if (!existsSync7(path)) {
4279
4607
  return {
4280
4608
  path: relativePath,
4281
4609
  present: false,
@@ -4285,7 +4613,7 @@ function provenanceEntry(projectRoot, relativePath) {
4285
4613
  }
4286
4614
  let generatedAt = null;
4287
4615
  try {
4288
- generatedAt = statSync5(path).mtime.toISOString();
4616
+ generatedAt = statSync6(path).mtime.toISOString();
4289
4617
  } catch {
4290
4618
  generatedAt = null;
4291
4619
  }
@@ -4297,7 +4625,7 @@ function provenanceEntry(projectRoot, relativePath) {
4297
4625
  };
4298
4626
  }
4299
4627
  function provenanceForPath(projectRoot, path) {
4300
- const rel = isAbsolute4(path) ? relative6(projectRoot, path).replace(/\\/g, "/") : path;
4628
+ const rel = isAbsolute4(path) ? relative7(projectRoot, path).replace(/\\/g, "/") : path;
4301
4629
  return provenanceEntry(projectRoot, rel && !rel.startsWith("..") ? rel : basename2(path));
4302
4630
  }
4303
4631
  function redactEvidenceText(projectRoot, value) {
@@ -4434,7 +4762,7 @@ function createContractAssertions(projectRoot, audit) {
4434
4762
  const assertions = [];
4435
4763
  const essence = audit?.essence;
4436
4764
  const v4 = essence && isV4(essence) ? essence : null;
4437
- const contextDir = join6(projectRoot, ".decantr", "context");
4765
+ const contextDir = join7(projectRoot, ".decantr", "context");
4438
4766
  const packManifest = audit?.packManifest ?? null;
4439
4767
  assertions.push(
4440
4768
  assertion({
@@ -4444,7 +4772,7 @@ function createContractAssertions(projectRoot, audit) {
4444
4772
  rule: "essence-present",
4445
4773
  status: essence ? "passed" : "failed",
4446
4774
  message: essence ? "Decantr Essence contract is present." : "No Decantr Essence contract was found.",
4447
- evidence: [redactEvidenceText(projectRoot, join6(projectRoot, "decantr.essence.json"))],
4775
+ evidence: [redactEvidenceText(projectRoot, join7(projectRoot, "decantr.essence.json"))],
4448
4776
  suggestedFix: essence ? void 0 : "Run `decantr init` or restore decantr.essence.json."
4449
4777
  })
4450
4778
  );
@@ -4517,7 +4845,7 @@ function createContractAssertions(projectRoot, audit) {
4517
4845
  rule: "pack-manifest-present",
4518
4846
  status: packManifest ? "passed" : "failed",
4519
4847
  message: packManifest ? "Compiled execution pack manifest is present." : "Compiled execution pack manifest is missing.",
4520
- evidence: [redactEvidenceText(projectRoot, join6(contextDir, "pack-manifest.json"))],
4848
+ evidence: [redactEvidenceText(projectRoot, join7(contextDir, "pack-manifest.json"))],
4521
4849
  suggestedFix: packManifest ? void 0 : "Run `decantr registry compile-packs decantr.essence.json --write-context` to hydrate the full context pack bundle."
4522
4850
  })
4523
4851
  );
@@ -4529,21 +4857,21 @@ function createContractAssertions(projectRoot, audit) {
4529
4857
  rule: "review-pack-present",
4530
4858
  status: audit?.reviewPack ? "passed" : "failed",
4531
4859
  message: audit?.reviewPack ? "Compiled review pack is present." : "Compiled review pack is missing.",
4532
- evidence: [redactEvidenceText(projectRoot, join6(contextDir, "review-pack.json"))],
4860
+ evidence: [redactEvidenceText(projectRoot, join7(contextDir, "review-pack.json"))],
4533
4861
  suggestedFix: audit?.reviewPack ? void 0 : "Run `decantr registry compile-packs decantr.essence.json --write-context` so critique has the compiled review contract."
4534
4862
  })
4535
4863
  );
4536
- const tokensPath = join6(projectRoot, "src", "styles", "tokens.css");
4864
+ const tokensPath = join7(projectRoot, "src", "styles", "tokens.css");
4537
4865
  assertions.push(
4538
4866
  assertion({
4539
4867
  id: "contract.design-token.tokens-file",
4540
4868
  category: "design-token",
4541
- severity: existsSync6(tokensPath) ? "info" : "warn",
4869
+ severity: existsSync7(tokensPath) ? "info" : "warn",
4542
4870
  rule: "tokens-file-present",
4543
- status: existsSync6(tokensPath) ? "passed" : "failed",
4544
- 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.",
4545
4873
  evidence: [redactEvidenceText(projectRoot, tokensPath)],
4546
- 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."
4547
4875
  })
4548
4876
  );
4549
4877
  return assertions;
@@ -4556,7 +4884,7 @@ function createEvidenceBundle(input) {
4556
4884
  try {
4557
4885
  resolvedProjectRoot = realpathSync2(input.projectRoot);
4558
4886
  } catch {
4559
- resolvedProjectRoot = resolve3(input.projectRoot);
4887
+ resolvedProjectRoot = resolve4(input.projectRoot);
4560
4888
  }
4561
4889
  const projectId = `project_${hashString(resolvedProjectRoot)}`;
4562
4890
  return {
@@ -4694,21 +5022,21 @@ function scoreRatio(numerator, denominator) {
4694
5022
  return Math.min(5, Math.max(1, Math.round(numerator / denominator * 5)));
4695
5023
  }
4696
5024
  function readJsonIfExists(path) {
4697
- if (!existsSync6(path)) return null;
5025
+ if (!existsSync7(path)) return null;
4698
5026
  try {
4699
- return JSON.parse(readFileSync6(path, "utf-8"));
5027
+ return JSON.parse(readFileSync7(path, "utf-8"));
4700
5028
  } catch {
4701
5029
  return null;
4702
5030
  }
4703
5031
  }
4704
5032
  function loadReviewPack(projectRoot) {
4705
5033
  return readJsonIfExists(
4706
- join6(projectRoot, ".decantr", "context", "review-pack.json")
5034
+ join7(projectRoot, ".decantr", "context", "review-pack.json")
4707
5035
  );
4708
5036
  }
4709
5037
  function loadPackManifest(projectRoot) {
4710
5038
  return readJsonIfExists(
4711
- join6(projectRoot, ".decantr", "context", "pack-manifest.json")
5039
+ join7(projectRoot, ".decantr", "context", "pack-manifest.json")
4712
5040
  );
4713
5041
  }
4714
5042
  function collectPackManifestReferences(packManifest) {
@@ -4757,14 +5085,14 @@ function collectPackManifestReferences(packManifest) {
4757
5085
  }
4758
5086
  function collectMissingPackManifestFiles(projectRoot, packManifest = loadPackManifest(projectRoot)) {
4759
5087
  if (!packManifest) return [];
4760
- const contextDir = join6(projectRoot, ".decantr", "context");
5088
+ const contextDir = join7(projectRoot, ".decantr", "context");
4761
5089
  const missing = [];
4762
5090
  for (const reference of collectPackManifestReferences(packManifest)) {
4763
5091
  for (const field of ["markdown", "json"]) {
4764
5092
  const fileName = reference[field];
4765
5093
  if (!fileName) continue;
4766
- const absolutePath = join6(contextDir, fileName);
4767
- if (existsSync6(absolutePath)) continue;
5094
+ const absolutePath = join7(contextDir, fileName);
5095
+ if (existsSync7(absolutePath)) continue;
4768
5096
  missing.push({
4769
5097
  entryId: reference.entryId,
4770
5098
  kind: reference.kind,
@@ -4778,13 +5106,13 @@ function collectMissingPackManifestFiles(projectRoot, packManifest = loadPackMan
4778
5106
  }
4779
5107
  function readTextIfExists(path) {
4780
5108
  try {
4781
- return existsSync6(path) ? readFileSync6(path, "utf-8") : "";
5109
+ return existsSync7(path) ? readFileSync7(path, "utf-8") : "";
4782
5110
  } catch {
4783
5111
  return "";
4784
5112
  }
4785
5113
  }
4786
5114
  function readProjectAdoptionMode(projectRoot) {
4787
- const projectJson = readJsonIfExists(join6(projectRoot, ".decantr", "project.json"));
5115
+ const projectJson = readJsonIfExists(join7(projectRoot, ".decantr", "project.json"));
4788
5116
  const adoptionMode = projectJson?.initialized?.adoptionMode;
4789
5117
  return typeof adoptionMode === "string" ? adoptionMode : null;
4790
5118
  }
@@ -4870,7 +5198,7 @@ function collectProjectSourceFiles(projectRoot) {
4870
5198
  "hooks",
4871
5199
  "providers",
4872
5200
  "server"
4873
- ].map((dir) => join6(projectRoot, dir)).filter((dir) => existsSync6(dir));
5201
+ ].map((dir) => join7(projectRoot, dir)).filter((dir) => existsSync7(dir));
4874
5202
  const rootFileCandidates = [
4875
5203
  "middleware.ts",
4876
5204
  "middleware.tsx",
@@ -4884,7 +5212,7 @@ function collectProjectSourceFiles(projectRoot) {
4884
5212
  "proxy.jsx",
4885
5213
  "proxy.mts",
4886
5214
  "proxy.cts"
4887
- ].map((file) => join6(projectRoot, file)).filter((file) => existsSync6(file));
5215
+ ].map((file) => join7(projectRoot, file)).filter((file) => existsSync7(file));
4888
5216
  const files = /* @__PURE__ */ new Set();
4889
5217
  const ignoredDirNames = /* @__PURE__ */ new Set([
4890
5218
  "node_modules",
@@ -4895,9 +5223,9 @@ function collectProjectSourceFiles(projectRoot) {
4895
5223
  "coverage"
4896
5224
  ]);
4897
5225
  const walk = (dir) => {
4898
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5226
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
4899
5227
  if (ignoredDirNames.has(entry.name)) continue;
4900
- const absolutePath = join6(dir, entry.name);
5228
+ const absolutePath = join7(dir, entry.name);
4901
5229
  if (entry.isDirectory()) {
4902
5230
  walk(absolutePath);
4903
5231
  continue;
@@ -4928,7 +5256,7 @@ function isAuditableStyleFile(filePath) {
4928
5256
  return /\.css$/i.test(filePath);
4929
5257
  }
4930
5258
  function collectProjectStyleFiles2(projectRoot) {
4931
- 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));
4932
5260
  const files = /* @__PURE__ */ new Set();
4933
5261
  const ignoredDirNames = /* @__PURE__ */ new Set([
4934
5262
  "node_modules",
@@ -4939,9 +5267,9 @@ function collectProjectStyleFiles2(projectRoot) {
4939
5267
  "coverage"
4940
5268
  ]);
4941
5269
  const walk = (dir) => {
4942
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5270
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
4943
5271
  if (ignoredDirNames.has(entry.name)) continue;
4944
- const absolutePath = join6(dir, entry.name);
5272
+ const absolutePath = join7(dir, entry.name);
4945
5273
  if (entry.isDirectory()) {
4946
5274
  walk(absolutePath);
4947
5275
  continue;
@@ -5015,9 +5343,9 @@ function collectRuntimeImportSpecifiers(entry) {
5015
5343
  function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
5016
5344
  let basePath = null;
5017
5345
  if (specifier.startsWith("@/")) {
5018
- basePath = join6(projectRoot, "src", specifier.slice(2));
5346
+ basePath = join7(projectRoot, "src", specifier.slice(2));
5019
5347
  } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
5020
- basePath = resolve3(dirname2(sourceAbsolutePath), specifier);
5348
+ basePath = resolve4(dirname3(sourceAbsolutePath), specifier);
5021
5349
  }
5022
5350
  if (!basePath) return null;
5023
5351
  const candidates = [
@@ -5028,14 +5356,14 @@ function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
5028
5356
  `${basePath}.jsx`,
5029
5357
  `${basePath}.mts`,
5030
5358
  `${basePath}.cts`,
5031
- join6(basePath, "index.ts"),
5032
- join6(basePath, "index.tsx"),
5033
- join6(basePath, "index.js"),
5034
- join6(basePath, "index.jsx")
5359
+ join7(basePath, "index.ts"),
5360
+ join7(basePath, "index.tsx"),
5361
+ join7(basePath, "index.js"),
5362
+ join7(basePath, "index.jsx")
5035
5363
  ];
5036
5364
  for (const candidate of candidates) {
5037
- if (existsSync6(candidate) && isAuditableSourceFile(candidate)) {
5038
- return relative6(projectRoot, candidate) || candidate;
5365
+ if (existsSync7(candidate) && isAuditableSourceFile(candidate)) {
5366
+ return relative7(projectRoot, candidate) || candidate;
5039
5367
  }
5040
5368
  }
5041
5369
  return null;
@@ -5083,8 +5411,8 @@ function isClientAuthHeaderSource(relativePath, code, clientReachableFiles) {
5083
5411
  function auditProjectSourceTree(projectRoot, sourceFiles = collectProjectSourceFiles(projectRoot)) {
5084
5412
  const sourceEntries = sourceFiles.map((sourceFile) => ({
5085
5413
  absolutePath: sourceFile,
5086
- relativePath: relative6(projectRoot, sourceFile) || sourceFile,
5087
- code: readFileSync6(sourceFile, "utf-8")
5414
+ relativePath: relative7(projectRoot, sourceFile) || sourceFile,
5415
+ code: readFileSync7(sourceFile, "utf-8")
5088
5416
  })).filter((entry) => !isNonProductionSourceAuditFile(entry.relativePath));
5089
5417
  const clientReachableFiles = collectClientReachableSourceFiles(projectRoot, sourceEntries);
5090
5418
  const summary = {
@@ -5397,8 +5725,8 @@ function auditProjectStyleContracts(projectRoot) {
5397
5725
  reducedMotionSignals: createSourceAuditBucket()
5398
5726
  };
5399
5727
  for (const styleFile of styleFiles) {
5400
- const relativePath = relative6(projectRoot, styleFile) || styleFile;
5401
- const css = readFileSync6(styleFile, "utf-8");
5728
+ const relativePath = relative7(projectRoot, styleFile) || styleFile;
5729
+ const css = readFileSync7(styleFile, "utf-8");
5402
5730
  recordSourceAudit(summary.focusVisibleSignals, relativePath, countFocusVisibleSignals(css));
5403
5731
  recordSourceAudit(summary.reducedMotionSignals, relativePath, countReducedMotionSignals(css));
5404
5732
  }
@@ -5407,19 +5735,19 @@ function auditProjectStyleContracts(projectRoot) {
5407
5735
  function buildRegistryContext(projectRoot) {
5408
5736
  const themeRegistry = /* @__PURE__ */ new Map();
5409
5737
  const patternRegistry = /* @__PURE__ */ new Map();
5410
- const cacheDir = join6(projectRoot, ".decantr", "cache");
5411
- const customDir = join6(projectRoot, ".decantr", "custom");
5738
+ const cacheDir = join7(projectRoot, ".decantr", "cache");
5739
+ const customDir = join7(projectRoot, ".decantr", "custom");
5412
5740
  const addPattern = (id, source, data = null) => {
5413
5741
  if (typeof id !== "string" || !id.trim() || patternRegistry.has(id)) return;
5414
5742
  patternRegistry.set(id, data ?? { id, source });
5415
5743
  };
5416
- const cachedThemesDir = join6(cacheDir, "@official", "themes");
5744
+ const cachedThemesDir = join7(cacheDir, "@official", "themes");
5417
5745
  try {
5418
- if (existsSync6(cachedThemesDir)) {
5419
- for (const file of readdirSync5(cachedThemesDir).filter(
5746
+ if (existsSync7(cachedThemesDir)) {
5747
+ for (const file of readdirSync6(cachedThemesDir).filter(
5420
5748
  (name) => name.endsWith(".json") && name !== "index.json"
5421
5749
  )) {
5422
- const data = JSON.parse(readFileSync6(join6(cachedThemesDir, file), "utf-8"));
5750
+ const data = JSON.parse(readFileSync7(join7(cachedThemesDir, file), "utf-8"));
5423
5751
  if (data.id && !themeRegistry.has(data.id)) {
5424
5752
  themeRegistry.set(data.id, { modes: data.modes || ["light", "dark"] });
5425
5753
  }
@@ -5427,11 +5755,11 @@ function buildRegistryContext(projectRoot) {
5427
5755
  }
5428
5756
  } catch {
5429
5757
  }
5430
- const customThemesDir = join6(customDir, "themes");
5758
+ const customThemesDir = join7(customDir, "themes");
5431
5759
  try {
5432
- if (existsSync6(customThemesDir)) {
5433
- for (const file of readdirSync5(customThemesDir).filter((name) => name.endsWith(".json"))) {
5434
- 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"));
5435
5763
  if (data.id) {
5436
5764
  themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
5437
5765
  }
@@ -5439,29 +5767,29 @@ function buildRegistryContext(projectRoot) {
5439
5767
  }
5440
5768
  } catch {
5441
5769
  }
5442
- const cachedPatternsDir = join6(cacheDir, "@official", "patterns");
5770
+ const cachedPatternsDir = join7(cacheDir, "@official", "patterns");
5443
5771
  try {
5444
- if (existsSync6(cachedPatternsDir)) {
5445
- for (const file of readdirSync5(cachedPatternsDir).filter(
5772
+ if (existsSync7(cachedPatternsDir)) {
5773
+ for (const file of readdirSync6(cachedPatternsDir).filter(
5446
5774
  (name) => name.endsWith(".json") && name !== "index.json"
5447
5775
  )) {
5448
- const data = JSON.parse(readFileSync6(join6(cachedPatternsDir, file), "utf-8"));
5776
+ const data = JSON.parse(readFileSync7(join7(cachedPatternsDir, file), "utf-8"));
5449
5777
  addPattern(data.id, "cache", data);
5450
5778
  }
5451
5779
  }
5452
5780
  } catch {
5453
5781
  }
5454
- const customPatternsDir = join6(customDir, "patterns");
5782
+ const customPatternsDir = join7(customDir, "patterns");
5455
5783
  try {
5456
- if (existsSync6(customPatternsDir)) {
5457
- for (const file of readdirSync5(customPatternsDir).filter((name) => name.endsWith(".json"))) {
5458
- 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"));
5459
5787
  addPattern(data.id, "custom", data);
5460
5788
  }
5461
5789
  }
5462
5790
  } catch {
5463
5791
  }
5464
- const localPatterns = readJsonIfExists(join6(projectRoot, ".decantr", "local-patterns.json"));
5792
+ const localPatterns = readJsonIfExists(join7(projectRoot, ".decantr", "local-patterns.json"));
5465
5793
  for (const pattern of localPatterns?.patterns ?? []) {
5466
5794
  addPattern(pattern.id, "local-law", pattern);
5467
5795
  }
@@ -5770,8 +6098,8 @@ function extractFailedRouteDocumentHardening(runtimeAudit) {
5770
6098
  return runtimeAudit.failures.filter((failure) => failure.startsWith("route-document-hardening-failed:")).map((failure) => normalizeRouteHint2(failure.slice("route-document-hardening-failed:".length))).filter(Boolean);
5771
6099
  }
5772
6100
  function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topology, sourceAudit) {
5773
- const distPath = join6(projectRoot, "dist");
5774
- const indexPath = join6(distPath, "index.html");
6101
+ const distPath = join7(projectRoot, "dist");
6102
+ const indexPath = join7(distPath, "index.html");
5775
6103
  const isFrameworkBuildOutput = runtimeAudit.failures.includes("next-build-output");
5776
6104
  if (!runtimeAudit.distPresent) {
5777
6105
  findings.push(
@@ -6316,7 +6644,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
6316
6644
  }
6317
6645
  function readBehaviorPatterns(projectRoot) {
6318
6646
  const pack = readJsonIfExists(
6319
- join6(projectRoot, ".decantr", "local-patterns.json")
6647
+ join7(projectRoot, ".decantr", "local-patterns.json")
6320
6648
  );
6321
6649
  return (pack?.patterns ?? []).filter(
6322
6650
  (pattern) => pattern.behavior_obligations && Array.isArray(pattern.behavior_obligations.obligations) && pattern.behavior_obligations.obligations.length > 0
@@ -6363,15 +6691,15 @@ function isFormPrimitiveDefinitionFile(pattern, relativeFile) {
6363
6691
  );
6364
6692
  }
6365
6693
  function sourceAuditFilePath(projectRoot, file) {
6366
- return isAbsolute4(file) ? file : join6(projectRoot, file);
6694
+ return isAbsolute4(file) ? file : join7(projectRoot, file);
6367
6695
  }
6368
6696
  function sourceAuditRelativePath(projectRoot, file) {
6369
- return (isAbsolute4(file) ? relative6(projectRoot, file) : file).replace(/\\/g, "/");
6697
+ return (isAbsolute4(file) ? relative7(projectRoot, file) : file).replace(/\\/g, "/");
6370
6698
  }
6371
6699
  function sourceFileContainsAny(projectRoot, sourceFiles, patterns) {
6372
6700
  for (const file of sourceFiles.slice(0, 400)) {
6373
6701
  try {
6374
- const code = readFileSync6(sourceAuditFilePath(projectRoot, file), "utf-8");
6702
+ const code = readFileSync7(sourceAuditFilePath(projectRoot, file), "utf-8");
6375
6703
  if (patterns.some((pattern) => pattern.test(code))) return true;
6376
6704
  } catch {
6377
6705
  }
@@ -6425,7 +6753,7 @@ function appendBehaviorObligationFindings(findings, projectRoot, sourceFiles) {
6425
6753
  const relativeFile = sourceAuditRelativePath(projectRoot, file);
6426
6754
  let code = "";
6427
6755
  try {
6428
- code = readFileSync6(sourceAuditFilePath(projectRoot, file), "utf-8");
6756
+ code = readFileSync7(sourceAuditFilePath(projectRoot, file), "utf-8");
6429
6757
  } catch {
6430
6758
  continue;
6431
6759
  }
@@ -6502,7 +6830,7 @@ function appendBehaviorObligationFindings(findings, projectRoot, sourceFiles) {
6502
6830
  const relativeFile = sourceAuditRelativePath(projectRoot, file);
6503
6831
  let code = "";
6504
6832
  try {
6505
- code = readFileSync6(sourceAuditFilePath(projectRoot, file), "utf-8");
6833
+ code = readFileSync7(sourceAuditFilePath(projectRoot, file), "utf-8");
6506
6834
  } catch {
6507
6835
  continue;
6508
6836
  }
@@ -7869,7 +8197,7 @@ function appendStyleContractFindings(findings, styleAudit, essence) {
7869
8197
  }
7870
8198
  }
7871
8199
  async function auditProject(projectRoot) {
7872
- const essencePath = join6(projectRoot, "decantr.essence.json");
8200
+ const essencePath = join7(projectRoot, "decantr.essence.json");
7873
8201
  const findings = [];
7874
8202
  const reviewPack = loadReviewPack(projectRoot);
7875
8203
  const packManifest = loadPackManifest(projectRoot);
@@ -7877,7 +8205,7 @@ async function auditProject(projectRoot) {
7877
8205
  const packHydrationOptional = adoptionMode === "contract-only" || adoptionMode === "style-bridge";
7878
8206
  const packHydrationSeverity = packHydrationOptional ? "info" : "warn";
7879
8207
  const runtimeAudit = emptyRuntimeAudit();
7880
- if (!existsSync6(essencePath)) {
8208
+ if (!existsSync7(essencePath)) {
7881
8209
  findings.push(
7882
8210
  makeFinding({
7883
8211
  id: "essence-missing",
@@ -7912,7 +8240,7 @@ async function auditProject(projectRoot) {
7912
8240
  }
7913
8241
  let essence = null;
7914
8242
  try {
7915
- essence = JSON.parse(readFileSync6(essencePath, "utf-8"));
8243
+ essence = JSON.parse(readFileSync7(essencePath, "utf-8"));
7916
8244
  } catch (error) {
7917
8245
  findings.push(
7918
8246
  makeFinding({
@@ -7955,7 +8283,7 @@ async function auditProject(projectRoot) {
7955
8283
  category: "Execution Packs",
7956
8284
  severity: packHydrationSeverity,
7957
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.",
7958
- evidence: [join6(projectRoot, ".decantr", "context", "pack-manifest.json")],
8286
+ evidence: [join7(projectRoot, ".decantr", "context", "pack-manifest.json")],
7959
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."
7960
8288
  })
7961
8289
  );
@@ -8019,7 +8347,7 @@ async function auditProject(projectRoot) {
8019
8347
  category: "Review Contract",
8020
8348
  severity: packHydrationSeverity,
8021
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.",
8022
- evidence: [join6(projectRoot, ".decantr", "context", "review-pack.json")],
8350
+ evidence: [join7(projectRoot, ".decantr", "context", "review-pack.json")],
8023
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."
8024
8352
  })
8025
8353
  );
@@ -8166,7 +8494,7 @@ function countKeyboardShortcutSignals(code) {
8166
8494
  return matches.length;
8167
8495
  }
8168
8496
  function getScriptKind(filePath) {
8169
- switch (extname6(filePath).toLowerCase()) {
8497
+ switch (extname7(filePath).toLowerCase()) {
8170
8498
  case ".tsx":
8171
8499
  return ts6.ScriptKind.TSX;
8172
8500
  case ".jsx":
@@ -16222,10 +16550,10 @@ function critiqueSource({
16222
16550
  };
16223
16551
  }
16224
16552
  function resolveProjectFilePath(projectRoot, filePath) {
16225
- const root = existsSync6(projectRoot) ? realpathSync2.native(projectRoot) : resolve3(projectRoot);
16226
- const candidatePath = isAbsolute4(filePath) ? resolve3(filePath) : resolve3(root, filePath);
16227
- const resolvedPath = existsSync6(candidatePath) ? realpathSync2.native(candidatePath) : candidatePath;
16228
- 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);
16229
16557
  if (relativePath.startsWith("..") || isAbsolute4(relativePath)) {
16230
16558
  throw new Error(`Path escapes the project root: ${filePath}`);
16231
16559
  }
@@ -16234,7 +16562,7 @@ function resolveProjectFilePath(projectRoot, filePath) {
16234
16562
  async function critiqueFile(filePath, projectRoot) {
16235
16563
  const resolvedPath = resolveProjectFilePath(projectRoot, filePath);
16236
16564
  const code = await readFile(resolvedPath, "utf-8");
16237
- const treatmentsCss = readTextIfExists(join6(projectRoot, "src", "styles", "treatments.css"));
16565
+ const treatmentsCss = readTextIfExists(join7(projectRoot, "src", "styles", "treatments.css"));
16238
16566
  const reviewPack = loadReviewPack(projectRoot);
16239
16567
  const packManifest = loadPackManifest(projectRoot);
16240
16568
  const adoptionMode = readProjectAdoptionMode(projectRoot);
@@ -16261,6 +16589,8 @@ export {
16261
16589
  RAW_CONTROL_REUSE_RULE_ID,
16262
16590
  RUNTIME_PROBE_PAYLOAD_V2_SCHEMA_URL,
16263
16591
  SCAN_REPORT_SCHEMA_URL,
16592
+ SCAN_REPORT_V1_SCHEMA_URL,
16593
+ SCAN_REPORT_V2_SCHEMA_URL,
16264
16594
  STYLE_BRIDGE_ARBITRARY_VALUE_RULE_ID,
16265
16595
  VERIFICATION_COMMON_V2_SCHEMA_URL,
16266
16596
  VERIFICATION_SCHEMA_URLS,
@@ -16286,6 +16616,7 @@ export {
16286
16616
  critiqueFile,
16287
16617
  critiqueSource,
16288
16618
  deriveVerificationDiagnostic,
16619
+ discoverProject,
16289
16620
  emptyRuntimeAudit,
16290
16621
  extractRouteHintsFromEssence,
16291
16622
  extractSourceStringLiterals,