@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 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 || /id="root"/.test(rootHtml));
1124
+ const rootDocumentOk = rootResponse.ok && rootDocumentLooksUsable(rootHtml, { frameworkDocumentOutput, frameworkRuntimeProject });
1081
1125
  const titleOk = /<title>[^<]+<\/title>/i.test(rootHtml);
1082
1126
  const langOk = /<html[^>]*\slang=(["'])[^"']+\1/i.test(rootHtml);
1083
1127
  const viewportOk = /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(rootHtml);
@@ -1157,7 +1201,7 @@ async function auditBuiltDist(projectRoot, options = {}) {
1157
1201
  for (const routeHint of routeHints) {
1158
1202
  const routeResponse = await fetch(`${server.baseUrl}${routeHint}`);
1159
1203
  const routeHtml = await routeResponse.text();
1160
- const routeRootDocumentOk = routeResponse.ok && (frameworkDocumentOutput || /id="root"/.test(routeHtml));
1204
+ const routeRootDocumentOk = routeResponse.ok && rootDocumentLooksUsable(routeHtml, { frameworkDocumentOutput, frameworkRuntimeProject });
1161
1205
  const routeTitleOk = /<title>[^<]+<\/title>/i.test(routeHtml);
1162
1206
  const routeLangOk = /<html[^>]*\slang=(["'])[^"']+\1/i.test(routeHtml);
1163
1207
  const routeViewportOk = /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(routeHtml);
@@ -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 = scanFileRoutes(projectRoot, "src/routes", /* @__PURE__ */ new Set([".svelte", ".ts", ".js"]));
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) {