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