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