@decantr/verifier 3.5.4 → 3.6.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 CHANGED
@@ -37,7 +37,7 @@ npm install @decantr/verifier
37
37
  - Next.js static/document outputs are treated as framework-rendered documents instead of requiring a Vite-style `id="root"` mount element
38
38
  - project source audits ignore test, spec, story, fixture, and mock files for production drift warnings such as localhost endpoints and unsafe rendering patterns
39
39
  - 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
- - project audits emit `COMP010` / `replace-raw-control-with-local-component` findings when production JSX renders raw controls such as `<button>` or `<input>` while a project-owned primitive already exists
40
+ - 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
41
41
  - project audits emit behavior-obligation findings when accepted `.decantr/local-patterns.json` patterns declare `behavior_obligations` and production source strongly violates statically checkable dialog/form obligations:
42
42
  - `A11Y010` / `restore-dialog-accessible-name`
43
43
  - `A11Y011` / `restore-label-association`
package/dist/index.js CHANGED
@@ -69,6 +69,18 @@ var RAW_CONTROL_COMPONENT_BY_ELEMENT = {
69
69
  table: "Table",
70
70
  textarea: "Textarea"
71
71
  };
72
+ var NON_GENERIC_INPUT_TYPES = /* @__PURE__ */ new Set([
73
+ "button",
74
+ "checkbox",
75
+ "color",
76
+ "file",
77
+ "hidden",
78
+ "image",
79
+ "radio",
80
+ "range",
81
+ "reset",
82
+ "submit"
83
+ ]);
72
84
  function normalizePath(path) {
73
85
  return path.replace(/\\/g, "/");
74
86
  }
@@ -120,10 +132,14 @@ function jsxAttributeStringValue(node, attributeName) {
120
132
  }
121
133
  return null;
122
134
  }
123
- function isHiddenInput(node) {
135
+ function isSpecializedInput(node) {
124
136
  const tagName = jsxIdentifierName(node.tagName);
125
137
  if (tagName !== "input") return false;
126
- return jsxAttributeStringValue(node, "type")?.toLowerCase() === "hidden";
138
+ const type = jsxAttributeStringValue(node, "type")?.toLowerCase();
139
+ if (type && NON_GENERIC_INPUT_TYPES.has(type)) return true;
140
+ return node.attributes.properties.some(
141
+ (property) => ts.isJsxSpreadAttribute(property) && /\bgetInputProps\s*\(/.test(property.expression.getText())
142
+ );
127
143
  }
128
144
  function isReusableComponentPath(file, name) {
129
145
  const normalized = normalizePath(file);
@@ -222,7 +238,7 @@ function collectRawControls(sourceFile, file) {
222
238
  if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
223
239
  const tagName = jsxIdentifierName(node.tagName);
224
240
  const component = tagName && Object.hasOwn(RAW_CONTROL_COMPONENT_BY_ELEMENT, tagName) ? RAW_CONTROL_COMPONENT_BY_ELEMENT[tagName] : null;
225
- if (component && !isHiddenInput(node)) {
241
+ if (component && !isSpecializedInput(node)) {
226
242
  rawControls.push({
227
243
  element: tagName,
228
244
  component,
@@ -2365,6 +2381,15 @@ var ROUTE_VARIABLE_NAMES = "(?:path|pathname|route|currentPath|currentRoute|loca
2365
2381
  var ROUTE_ASSET_EXTENSION_RE = /\.(?:avif|bmp|css|gif|ico|jpeg|jpg|js|json|map|mp4|pdf|png|svg|webp|woff2?)$/i;
2366
2382
  var JSX_ROUTE_PATH_RE = /<(?:Route|[A-Z][\w.]*Route)\b[^>]*\bpath\s*=\s*(?:"([^"]+)"|'([^']+)'|{\s*"([^"]+)"\s*}|{\s*'([^']+)'\s*}|{\s*`([^`]+)`\s*})/g;
2367
2383
  var OBJECT_ROUTE_PATH_RE = /\bpath\s*:\s*(?:"([^"]+)"|'([^']+)'|`([^`]+)`)/g;
2384
+ var STATIC_HTML_ENTRY_FILES = [
2385
+ "index.html",
2386
+ "docs/index.html",
2387
+ "src/index.html",
2388
+ "public/index.html",
2389
+ "dist/index.html"
2390
+ ];
2391
+ var STATIC_HTML_ROUTE_DIRS = ["demos", "examples"];
2392
+ var MAX_STATIC_HTML_CANDIDATES = 1e3;
2368
2393
  var MAX_FILE_READ_BYTES = 512 * 1024;
2369
2394
  var MAX_WALK_FILES = 5e3;
2370
2395
  var MAX_REPORT_ROUTES = 80;
@@ -2462,6 +2487,10 @@ function dependencyVersion(dependencies, names) {
2462
2487
  function hasAnyFile(projectRoot, paths) {
2463
2488
  return paths.some((path) => existsSync3(join3(projectRoot, path)));
2464
2489
  }
2490
+ function hasStaticHtmlSurface(projectRoot) {
2491
+ if (hasAnyFile(projectRoot, STATIC_HTML_ENTRY_FILES)) return true;
2492
+ return STATIC_HTML_ROUTE_DIRS.some((dir) => existsSync3(join3(projectRoot, dir, "index.html")));
2493
+ }
2465
2494
  function detectPackageManager(projectRoot, pkg) {
2466
2495
  const declared = pkg?.packageManager?.split("@")[0];
2467
2496
  if (declared === "npm" || declared === "pnpm" || declared === "yarn" || declared === "bun") {
@@ -2474,12 +2503,12 @@ function detectPackageManager(projectRoot, pkg) {
2474
2503
  return "unknown";
2475
2504
  }
2476
2505
  function detectPrimaryLanguage(projectRoot, packageJsonPresent) {
2477
- if (packageJsonPresent) return "javascript";
2506
+ if (packageJsonPresent) return hasStaticHtmlSurface(projectRoot) ? "html" : "javascript";
2478
2507
  if (hasAnyFile(projectRoot, ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"]))
2479
2508
  return "python";
2480
2509
  if (hasAnyFile(projectRoot, ["go.mod"])) return "go";
2481
2510
  if (hasAnyFile(projectRoot, ["Cargo.toml"])) return "rust";
2482
- if (hasAnyFile(projectRoot, ["index.html", "docs/index.html"])) return "html";
2511
+ if (hasStaticHtmlSurface(projectRoot)) return "html";
2483
2512
  return "unknown";
2484
2513
  }
2485
2514
  function detectProject(projectRoot) {
@@ -2512,7 +2541,7 @@ function detectProject(projectRoot) {
2512
2541
  } else if (dependencies.react) {
2513
2542
  framework = "react";
2514
2543
  frameworkVersion = dependencyVersion(dependencies, ["react"]);
2515
- } else if (hasAnyFile(projectRoot, ["index.html", "docs/index.html"])) {
2544
+ } else if (hasStaticHtmlSurface(projectRoot)) {
2516
2545
  framework = "html";
2517
2546
  }
2518
2547
  return {
@@ -2701,6 +2730,65 @@ function collectRouteLiterals(pattern, content, routes) {
2701
2730
  }
2702
2731
  return count;
2703
2732
  }
2733
+ function htmlRouteFromFile(file) {
2734
+ let withoutExt = file.slice(0, -extname3(file).length).replace(/\\/g, "/");
2735
+ if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
2736
+ if (withoutExt === "index" || withoutExt === "docs" || withoutExt === "src" || withoutExt === "public" || withoutExt === "dist") {
2737
+ return "/";
2738
+ }
2739
+ return `/${withoutExt.split("/").filter(Boolean).join("/")}` || "/";
2740
+ }
2741
+ function collectStaticHtmlFiles(dir, projectRoot, files, depth = 0) {
2742
+ if (depth > 5 || files.length >= MAX_STATIC_HTML_CANDIDATES) return;
2743
+ let entries;
2744
+ try {
2745
+ entries = readdirSync3(dir).sort();
2746
+ } catch {
2747
+ return;
2748
+ }
2749
+ for (const entry of entries) {
2750
+ if (files.length >= MAX_STATIC_HTML_CANDIDATES) return;
2751
+ if (entry.startsWith(".") || entry === "node_modules") continue;
2752
+ const fullPath = join3(dir, entry);
2753
+ try {
2754
+ const stat = statSync2(fullPath);
2755
+ if (stat.isDirectory()) {
2756
+ collectStaticHtmlFiles(fullPath, projectRoot, files, depth + 1);
2757
+ } else if (stat.isFile() && /\.(?:html|htm)$/i.test(entry)) {
2758
+ files.push(relative3(projectRoot, fullPath).replace(/\\/g, "/"));
2759
+ }
2760
+ } catch {
2761
+ }
2762
+ }
2763
+ }
2764
+ function staticHtmlFilePriority(file) {
2765
+ const base = file.split("/").pop()?.toLowerCase();
2766
+ if (base === "index.html" || base === "index.htm") return 0;
2767
+ if (base === "default.html" || base === "default.htm") return 1;
2768
+ return 2;
2769
+ }
2770
+ function scanStaticHtmlRoutes(projectRoot) {
2771
+ const routes = /* @__PURE__ */ new Map();
2772
+ for (const file of STATIC_HTML_ENTRY_FILES) {
2773
+ if (!existsSync3(join3(projectRoot, file))) continue;
2774
+ routes.set("/", { path: "/", file, hasLayout: false });
2775
+ break;
2776
+ }
2777
+ for (const dir of STATIC_HTML_ROUTE_DIRS) {
2778
+ const fullDir = join3(projectRoot, dir);
2779
+ if (!existsSync3(fullDir)) continue;
2780
+ const files = [];
2781
+ collectStaticHtmlFiles(fullDir, projectRoot, files);
2782
+ for (const file of files.sort(
2783
+ (a, b) => staticHtmlFilePriority(a) - staticHtmlFilePriority(b) || a.localeCompare(b)
2784
+ )) {
2785
+ const routePath = htmlRouteFromFile(file);
2786
+ if (routes.has(routePath)) continue;
2787
+ routes.set(routePath, { path: routePath, file, hasLayout: false });
2788
+ }
2789
+ }
2790
+ return [...routes.values()].slice(0, MAX_REPORT_ROUTES);
2791
+ }
2704
2792
  function detectPathnameBranchRoutes(content) {
2705
2793
  const routes = /* @__PURE__ */ new Set();
2706
2794
  const comparison = new RegExp(
@@ -2760,18 +2848,8 @@ function scanRoutes(projectRoot, detection) {
2760
2848
  if (reactRouter.routes.length > 0)
2761
2849
  return { strategy: "react-router", routes: reactRouter.routes };
2762
2850
  if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
2763
- if (existsSync3(join3(projectRoot, "index.html"))) {
2764
- return {
2765
- strategy: "static-html",
2766
- routes: [{ path: "/", file: "index.html", hasLayout: false }]
2767
- };
2768
- }
2769
- if (existsSync3(join3(projectRoot, "docs", "index.html"))) {
2770
- return {
2771
- strategy: "static-html",
2772
- routes: [{ path: "/", file: "docs/index.html", hasLayout: false }]
2773
- };
2774
- }
2851
+ const staticHtmlRoutes = scanStaticHtmlRoutes(projectRoot);
2852
+ if (staticHtmlRoutes.length > 0) return { strategy: "static-html", routes: staticHtmlRoutes };
2775
2853
  return { strategy: "none", routes: [] };
2776
2854
  }
2777
2855
  function countComponents(projectRoot, routes) {