@decantr/verifier 3.6.0 → 3.6.1
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 +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +114 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ npm install @decantr/verifier
|
|
|
35
35
|
- project audits check that `pack-manifest.json` references real pack markdown/JSON files on disk
|
|
36
36
|
- project audits tolerate partial or malformed generated review packs without crashing Project Health, so half-attached Brownfield projects still receive actionable findings
|
|
37
37
|
- Next.js static/document outputs are treated as framework-rendered documents instead of requiring a Vite-style `id="root"` mount element
|
|
38
|
+
- generic static apps can satisfy runtime root proof through semantic app roots such as `main`, `role="main"`, `section.todoapp`, or `#todoapp`, while framework targets still require framework mount/document evidence
|
|
38
39
|
- project source audits ignore test, spec, story, fixture, and mock files for production drift warnings such as localhost endpoints and unsafe rendering patterns
|
|
39
40
|
- project audits emit `COMP001` / `import-existing-component` findings when a production source file locally redeclares a primitive such as `Button`, `Card`, or `Dialog` while an exported reusable primitive already exists under common component paths
|
|
40
41
|
- project audits emit `COMP010` / `replace-raw-control-with-local-component` findings when production JSX renders generic raw controls such as `<button>` or text-like `<input>` while a project-owned primitive already exists; specialized inputs such as file, hidden, checkbox, radio, color, range, and Dropzone `getInputProps()` controls are not treated as generic `Input` drift
|
|
@@ -49,7 +50,7 @@ npm install @decantr/verifier
|
|
|
49
50
|
- project audits emit `TOKEN010` / `replace-arbitrary-style-with-bridge-token` findings when an accepted `.decantr/style-bridge.json` exists and production JSX uses arbitrary Tailwind values such as `bg-[#0f172a]`, values inside `cn()`, `clsx()`, `classnames()`, `cva()`, and `tv()` calls, hardcoded inline color styles such as `style={{ backgroundColor: "#0f172a" }}`, or hardcoded visual values in production CSS/module stylesheets
|
|
50
51
|
- published verifier report schemas are exercised by AJV-backed round-trip tests against real audit, critique, and shortlist-report outputs
|
|
51
52
|
- project audits include runtime evidence when a built `dist/` output is present:
|
|
52
|
-
- root document
|
|
53
|
+
- root document, including semantic static app roots for generic static apps
|
|
53
54
|
- document title
|
|
54
55
|
- document `lang` and `viewport` metadata
|
|
55
56
|
- emitted assets
|
package/dist/index.d.ts
CHANGED
|
@@ -450,6 +450,7 @@ interface RuntimeAudit {
|
|
|
450
450
|
interface BuiltDistAuditOptions {
|
|
451
451
|
distDir?: string;
|
|
452
452
|
routeHints?: string[];
|
|
453
|
+
runtimeTarget?: string | null;
|
|
453
454
|
}
|
|
454
455
|
declare function emptyRuntimeAudit(failures?: string[]): RuntimeAudit;
|
|
455
456
|
declare function auditBuiltDist(projectRoot: string, options?: BuiltDistAuditOptions): Promise<RuntimeAudit>;
|
package/dist/index.js
CHANGED
|
@@ -1008,6 +1008,49 @@ function isNextProject(projectRoot) {
|
|
|
1008
1008
|
return false;
|
|
1009
1009
|
}
|
|
1010
1010
|
}
|
|
1011
|
+
function readProjectDependencies(projectRoot) {
|
|
1012
|
+
try {
|
|
1013
|
+
const pkg = JSON.parse(readFileSync2(join(projectRoot, "package.json"), "utf-8"));
|
|
1014
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1015
|
+
} catch {
|
|
1016
|
+
return {};
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function isFrameworkRuntimeProject(projectRoot, runtimeTarget) {
|
|
1020
|
+
const normalizedTarget = runtimeTarget?.toLowerCase() ?? "";
|
|
1021
|
+
if (["angular", "next", "nextjs", "nuxt", "react", "solid", "svelte", "sveltekit", "vue"].includes(
|
|
1022
|
+
normalizedTarget
|
|
1023
|
+
)) {
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
if (existsSync(join(projectRoot, "vite.config.js")) || existsSync(join(projectRoot, "vite.config.ts")) || existsSync(join(projectRoot, "angular.json")) || existsSync(join(projectRoot, "svelte.config.js")) || existsSync(join(projectRoot, "svelte.config.ts"))) {
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
const dependencies = readProjectDependencies(projectRoot);
|
|
1030
|
+
return [
|
|
1031
|
+
"@angular/core",
|
|
1032
|
+
"@angular/router",
|
|
1033
|
+
"@sveltejs/kit",
|
|
1034
|
+
"next",
|
|
1035
|
+
"nuxt",
|
|
1036
|
+
"react",
|
|
1037
|
+
"react-dom",
|
|
1038
|
+
"solid-js",
|
|
1039
|
+
"svelte",
|
|
1040
|
+
"vue"
|
|
1041
|
+
].some((name) => Boolean(dependencies[name]));
|
|
1042
|
+
}
|
|
1043
|
+
function hasFrameworkMountPoint(html) {
|
|
1044
|
+
return /\bid=(["'])(?:root|app|__next)\1/i.test(html) || /<app-root(?:\s|>)/i.test(html) || /<[^>]+\bdata-(?:reactroot|sveltekit|astro-cid)\b/i.test(html);
|
|
1045
|
+
}
|
|
1046
|
+
function hasSemanticStaticAppRoot(html) {
|
|
1047
|
+
return /<main(?:\s|>)/i.test(html) || /<[^>]+\brole=(["'])main\1/i.test(html) || /<section\b[^>]*(?:\bid=(["'])todoapp\1|\bclass=(["'])[^"']*\btodoapp\b[^"']*\2)/i.test(html);
|
|
1048
|
+
}
|
|
1049
|
+
function rootDocumentLooksUsable(html, options) {
|
|
1050
|
+
if (options.frameworkDocumentOutput) return true;
|
|
1051
|
+
if (hasFrameworkMountPoint(html)) return true;
|
|
1052
|
+
return !options.frameworkRuntimeProject && hasSemanticStaticAppRoot(html);
|
|
1053
|
+
}
|
|
1011
1054
|
async function startStaticServer(rootDir) {
|
|
1012
1055
|
const server = createServer((req, res) => {
|
|
1013
1056
|
const requestedUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -1070,6 +1113,7 @@ async function auditBuiltDist(projectRoot, options = {}) {
|
|
|
1070
1113
|
}
|
|
1071
1114
|
const routeHints = Array.isArray(options.routeHints) && options.routeHints.length > 0 ? options.routeHints.map((route) => normalizeRouteHint(route)).filter(Boolean).slice(0, 8) : ["/"];
|
|
1072
1115
|
const frameworkDocumentOutput = isNextProject(projectRoot);
|
|
1116
|
+
const frameworkRuntimeProject = isFrameworkRuntimeProject(projectRoot, options.runtimeTarget);
|
|
1073
1117
|
const indexHtml = readFileSync2(indexPath, "utf-8");
|
|
1074
1118
|
const assetPaths = extractAssetPaths(indexHtml);
|
|
1075
1119
|
const server = await startStaticServer(distDir);
|
|
@@ -1077,7 +1121,7 @@ async function auditBuiltDist(projectRoot, options = {}) {
|
|
|
1077
1121
|
const rootResponse = await fetch(`${server.baseUrl}/`);
|
|
1078
1122
|
const rootHtml = await rootResponse.text();
|
|
1079
1123
|
const failures = frameworkDocumentOutput ? ["next-build-output"] : [];
|
|
1080
|
-
const rootDocumentOk = rootResponse.ok && (frameworkDocumentOutput
|
|
1124
|
+
const rootDocumentOk = rootResponse.ok && rootDocumentLooksUsable(rootHtml, { frameworkDocumentOutput, frameworkRuntimeProject });
|
|
1081
1125
|
const titleOk = /<title>[^<]+<\/title>/i.test(rootHtml);
|
|
1082
1126
|
const langOk = /<html[^>]*\slang=(["'])[^"']+\1/i.test(rootHtml);
|
|
1083
1127
|
const viewportOk = /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(rootHtml);
|
|
@@ -1157,7 +1201,7 @@ async function auditBuiltDist(projectRoot, options = {}) {
|
|
|
1157
1201
|
for (const routeHint of routeHints) {
|
|
1158
1202
|
const routeResponse = await fetch(`${server.baseUrl}${routeHint}`);
|
|
1159
1203
|
const routeHtml = await routeResponse.text();
|
|
1160
|
-
const routeRootDocumentOk = routeResponse.ok && (frameworkDocumentOutput
|
|
1204
|
+
const routeRootDocumentOk = routeResponse.ok && rootDocumentLooksUsable(routeHtml, { frameworkDocumentOutput, frameworkRuntimeProject });
|
|
1161
1205
|
const routeTitleOk = /<title>[^<]+<\/title>/i.test(routeHtml);
|
|
1162
1206
|
const routeLangOk = /<html[^>]*\slang=(["'])[^"']+\1/i.test(routeHtml);
|
|
1163
1207
|
const routeViewportOk = /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(routeHtml);
|
|
@@ -2647,6 +2691,42 @@ function walkNextAppRoutes(dir, projectRoot, segments) {
|
|
|
2647
2691
|
}
|
|
2648
2692
|
return routes;
|
|
2649
2693
|
}
|
|
2694
|
+
function walkSvelteKitRoutes(dir, projectRoot, segments) {
|
|
2695
|
+
let entries;
|
|
2696
|
+
try {
|
|
2697
|
+
entries = readdirSync3(dir);
|
|
2698
|
+
} catch {
|
|
2699
|
+
return [];
|
|
2700
|
+
}
|
|
2701
|
+
const routes = [];
|
|
2702
|
+
const pageFile = entries.find((entry) => /^\+page\.(svelte|ts|js)$/.test(entry));
|
|
2703
|
+
const hasLayout = entries.some((entry) => /^\+layout\.(svelte|ts|js)$/.test(entry));
|
|
2704
|
+
if (pageFile) {
|
|
2705
|
+
routes.push({
|
|
2706
|
+
path: `/${segments.filter(Boolean).join("/")}` || "/",
|
|
2707
|
+
file: relative3(projectRoot, join3(dir, pageFile)).replace(/\\/g, "/"),
|
|
2708
|
+
hasLayout
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
for (const entry of entries) {
|
|
2712
|
+
if (shouldSkipDir(entry)) continue;
|
|
2713
|
+
const fullPath = join3(dir, entry);
|
|
2714
|
+
try {
|
|
2715
|
+
if (!statSync2(fullPath).isDirectory()) continue;
|
|
2716
|
+
} catch {
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
const routeSegment = segmentToRoute(entry);
|
|
2720
|
+
routes.push(
|
|
2721
|
+
...walkSvelteKitRoutes(
|
|
2722
|
+
fullPath,
|
|
2723
|
+
projectRoot,
|
|
2724
|
+
routeSegment === null ? segments : [...segments, routeSegment]
|
|
2725
|
+
)
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
return routes;
|
|
2729
|
+
}
|
|
2650
2730
|
function fileRouteFromPath(file, baseDir) {
|
|
2651
2731
|
let withoutExt = file.slice(0, -extname3(file).length);
|
|
2652
2732
|
if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
|
|
@@ -2697,6 +2777,31 @@ function scanReactRouter(projectRoot) {
|
|
|
2697
2777
|
}
|
|
2698
2778
|
return { routes: [...routes.values()], hashRouting };
|
|
2699
2779
|
}
|
|
2780
|
+
function normalizeRouterObjectPath(path) {
|
|
2781
|
+
const cleaned = path.trim();
|
|
2782
|
+
if (!cleaned || cleaned === "/") return "/";
|
|
2783
|
+
if (cleaned === "*" || cleaned === "**" || cleaned.startsWith("#")) return null;
|
|
2784
|
+
if (ROUTE_ASSET_EXTENSION_RE.test(cleaned)) return null;
|
|
2785
|
+
return cleaned.startsWith("/") ? cleaned.replace(/\/+$/g, "") || "/" : `/${cleaned}`;
|
|
2786
|
+
}
|
|
2787
|
+
function scanAngularRouter(projectRoot) {
|
|
2788
|
+
const routes = /* @__PURE__ */ new Map();
|
|
2789
|
+
const files = walkFiles(projectRoot, {
|
|
2790
|
+
extensions: /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"])
|
|
2791
|
+
});
|
|
2792
|
+
for (const file of files) {
|
|
2793
|
+
const content = readTextFile(join3(projectRoot, file));
|
|
2794
|
+
if (!content) continue;
|
|
2795
|
+
const isRouterFile = content.includes("@angular/router") || content.includes("RouterModule.forRoot") || content.includes("provideRouter") || /\bRoutes\s*=/.test(content);
|
|
2796
|
+
if (!isRouterFile) continue;
|
|
2797
|
+
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g)) {
|
|
2798
|
+
const routePath = normalizeRouterObjectPath(match[1] ?? "");
|
|
2799
|
+
if (!routePath || routes.has(routePath)) continue;
|
|
2800
|
+
routes.set(routePath, { path: routePath, file, hasLayout: false });
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
return [...routes.values()];
|
|
2804
|
+
}
|
|
2700
2805
|
function firstRouteLiteralMatch(match) {
|
|
2701
2806
|
return match.slice(1).find((value) => typeof value === "string") ?? null;
|
|
2702
2807
|
}
|
|
@@ -2831,7 +2936,7 @@ function scanRoutes(projectRoot, detection) {
|
|
|
2831
2936
|
if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
|
|
2832
2937
|
}
|
|
2833
2938
|
if (detection.framework === "svelte") {
|
|
2834
|
-
const routes =
|
|
2939
|
+
const routes = existsSync3(join3(projectRoot, "src/routes")) ? walkSvelteKitRoutes(join3(projectRoot, "src/routes"), projectRoot, []) : [];
|
|
2835
2940
|
if (routes.length > 0) return { strategy: "sveltekit-router", routes };
|
|
2836
2941
|
}
|
|
2837
2942
|
if (detection.framework === "nuxt") {
|
|
@@ -2844,6 +2949,10 @@ function scanRoutes(projectRoot, detection) {
|
|
|
2844
2949
|
const router = scanReactRouter(projectRoot);
|
|
2845
2950
|
if (router.routes.length > 0) return { strategy: "vue-router", routes: router.routes };
|
|
2846
2951
|
}
|
|
2952
|
+
if (detection.framework === "angular") {
|
|
2953
|
+
const routes = scanAngularRouter(projectRoot);
|
|
2954
|
+
if (routes.length > 0) return { strategy: "angular-router", routes };
|
|
2955
|
+
}
|
|
2847
2956
|
const reactRouter = scanReactRouter(projectRoot);
|
|
2848
2957
|
if (reactRouter.routes.length > 0)
|
|
2849
2958
|
return { strategy: "react-router", routes: reactRouter.routes };
|
|
@@ -5650,7 +5759,8 @@ function appendTopologyFindings(findings, essence, reviewPack) {
|
|
|
5650
5759
|
}
|
|
5651
5760
|
async function runRuntimeAudit(projectRoot, essence) {
|
|
5652
5761
|
return auditBuiltDist(projectRoot, {
|
|
5653
|
-
routeHints: extractRouteHintsFromEssence(essence)
|
|
5762
|
+
routeHints: extractRouteHintsFromEssence(essence),
|
|
5763
|
+
runtimeTarget: typeof essence?.meta?.target === "string" ? essence.meta.target : null
|
|
5654
5764
|
});
|
|
5655
5765
|
}
|
|
5656
5766
|
function extractFailedRouteDocuments(runtimeAudit) {
|