@decantr/verifier 2.4.0 → 2.5.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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/index.ts
2
2
  import { createHash } from "crypto";
3
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2 } from "fs";
3
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, realpathSync, statSync as statSync3 } from "fs";
4
4
  import { readFile } from "fs/promises";
5
- import { basename, dirname, extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
5
+ import { basename, dirname, extname as extname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve } from "path";
6
6
  import { evaluateGuard, isV4, validateEssence } from "@decantr/essence-spec";
7
7
  import * as ts from "typescript";
8
8
 
@@ -754,6 +754,850 @@ function listKnownInteractions() {
754
754
  return Object.keys(INTERACTION_SIGNALS).sort();
755
755
  }
756
756
 
757
+ // src/scan.ts
758
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
759
+ import { extname as extname2, isAbsolute, join as join2, relative } from "path";
760
+ var SCAN_REPORT_SCHEMA_URL = "https://decantr.ai/schemas/scan-report.v1.json";
761
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"]);
762
+ var STYLE_EXTENSIONS = /* @__PURE__ */ new Set([".css", ".scss", ".sass", ".less"]);
763
+ var PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte", ".html"]);
764
+ var MAX_FILE_READ_BYTES = 512 * 1024;
765
+ var MAX_WALK_FILES = 5e3;
766
+ var MAX_REPORT_ROUTES = 80;
767
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
768
+ ".git",
769
+ ".next",
770
+ ".nuxt",
771
+ ".svelte-kit",
772
+ "coverage",
773
+ "dist",
774
+ "build",
775
+ "node_modules",
776
+ "out",
777
+ "target",
778
+ "vendor"
779
+ ]);
780
+ var RULE_FILES = [
781
+ "CLAUDE.md",
782
+ "AGENTS.md",
783
+ "GEMINI.md",
784
+ ".cursorrules",
785
+ ".cursor/rules",
786
+ ".claude/rules",
787
+ ".github/copilot-instructions.md",
788
+ "copilot-instructions.md",
789
+ ".windsurfrules"
790
+ ];
791
+ var WEB_FRAMEWORKS = /* @__PURE__ */ new Set([
792
+ "angular",
793
+ "astro",
794
+ "html",
795
+ "nextjs",
796
+ "nuxt",
797
+ "react",
798
+ "solid",
799
+ "svelte",
800
+ "vue"
801
+ ]);
802
+ function isRecord(value) {
803
+ return typeof value === "object" && value !== null && !Array.isArray(value);
804
+ }
805
+ function readTextFile(path, maxBytes = MAX_FILE_READ_BYTES) {
806
+ try {
807
+ const stat = statSync2(path);
808
+ if (!stat.isFile() || stat.size > maxBytes) return null;
809
+ return readFileSync2(path, "utf-8");
810
+ } catch {
811
+ return null;
812
+ }
813
+ }
814
+ function readPackageJson(projectRoot) {
815
+ const path = join2(projectRoot, "package.json");
816
+ if (!existsSync2(path)) return { value: null, present: false, valid: false };
817
+ const content = readTextFile(path);
818
+ if (!content) return { value: null, present: true, valid: false };
819
+ try {
820
+ const parsed = JSON.parse(content);
821
+ return { value: isRecord(parsed) ? parsed : null, present: true, valid: isRecord(parsed) };
822
+ } catch {
823
+ return { value: null, present: true, valid: false };
824
+ }
825
+ }
826
+ function dependencyVersion(dependencies, names) {
827
+ for (const name of names) {
828
+ const version = dependencies[name];
829
+ if (version) return version.replace(/^[~^]/, "");
830
+ }
831
+ return null;
832
+ }
833
+ function hasAnyFile(projectRoot, paths) {
834
+ return paths.some((path) => existsSync2(join2(projectRoot, path)));
835
+ }
836
+ function detectPackageManager(projectRoot, pkg) {
837
+ const declared = pkg?.packageManager?.split("@")[0];
838
+ if (declared === "npm" || declared === "pnpm" || declared === "yarn" || declared === "bun") {
839
+ return declared;
840
+ }
841
+ if (existsSync2(join2(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
842
+ if (existsSync2(join2(projectRoot, "yarn.lock"))) return "yarn";
843
+ if (existsSync2(join2(projectRoot, "bun.lockb"))) return "bun";
844
+ if (existsSync2(join2(projectRoot, "package-lock.json"))) return "npm";
845
+ return "unknown";
846
+ }
847
+ function detectPrimaryLanguage(projectRoot, packageJsonPresent) {
848
+ if (packageJsonPresent) return "javascript";
849
+ if (hasAnyFile(projectRoot, ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"])) return "python";
850
+ if (hasAnyFile(projectRoot, ["go.mod"])) return "go";
851
+ if (hasAnyFile(projectRoot, ["Cargo.toml"])) return "rust";
852
+ if (hasAnyFile(projectRoot, ["index.html", "docs/index.html"])) return "html";
853
+ return "unknown";
854
+ }
855
+ function detectProject(projectRoot) {
856
+ const packageRead = readPackageJson(projectRoot);
857
+ const pkg = packageRead.value;
858
+ const dependencies = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
859
+ let framework = "unknown";
860
+ let frameworkVersion = null;
861
+ if (hasAnyFile(projectRoot, ["next.config.js", "next.config.ts", "next.config.mjs"]) || dependencies.next) {
862
+ framework = "nextjs";
863
+ frameworkVersion = dependencyVersion(dependencies, ["next"]);
864
+ } else if (hasAnyFile(projectRoot, ["nuxt.config.js", "nuxt.config.ts"]) || dependencies.nuxt) {
865
+ framework = "nuxt";
866
+ frameworkVersion = dependencyVersion(dependencies, ["nuxt"]);
867
+ } else if (hasAnyFile(projectRoot, ["astro.config.mjs", "astro.config.ts"]) || dependencies.astro) {
868
+ framework = "astro";
869
+ frameworkVersion = dependencyVersion(dependencies, ["astro"]);
870
+ } else if (hasAnyFile(projectRoot, ["svelte.config.js", "svelte.config.ts"]) || dependencies.svelte || dependencies["@sveltejs/kit"]) {
871
+ framework = "svelte";
872
+ frameworkVersion = dependencyVersion(dependencies, ["svelte", "@sveltejs/kit"]);
873
+ } else if (hasAnyFile(projectRoot, ["angular.json"]) || dependencies["@angular/core"]) {
874
+ framework = "angular";
875
+ frameworkVersion = dependencyVersion(dependencies, ["@angular/core"]);
876
+ } else if (dependencies["solid-js"]) {
877
+ framework = "solid";
878
+ frameworkVersion = dependencyVersion(dependencies, ["solid-js"]);
879
+ } else if (dependencies.vue) {
880
+ framework = "vue";
881
+ frameworkVersion = dependencyVersion(dependencies, ["vue"]);
882
+ } else if (dependencies.react) {
883
+ framework = "react";
884
+ frameworkVersion = dependencyVersion(dependencies, ["react"]);
885
+ } else if (hasAnyFile(projectRoot, ["index.html", "docs/index.html"])) {
886
+ framework = "html";
887
+ }
888
+ return {
889
+ framework,
890
+ frameworkVersion,
891
+ packageManager: detectPackageManager(projectRoot, pkg),
892
+ primaryLanguage: detectPrimaryLanguage(projectRoot, packageRead.present),
893
+ hasTypeScript: hasAnyFile(projectRoot, ["tsconfig.json"]),
894
+ hasTailwind: hasAnyFile(projectRoot, [
895
+ "tailwind.config.js",
896
+ "tailwind.config.ts",
897
+ "tailwind.config.mjs",
898
+ "tailwind.config.cjs"
899
+ ]),
900
+ hasDecantr: existsSync2(join2(projectRoot, "decantr.essence.json")) || existsSync2(join2(projectRoot, ".decantr")) || Boolean(dependencies["@decantr/cli"] || dependencies["@decantr/css"]),
901
+ packageName: typeof pkg?.name === "string" ? pkg.name : null,
902
+ packageJsonValid: packageRead.valid,
903
+ packageJsonPresent: packageRead.present,
904
+ dependencies
905
+ };
906
+ }
907
+ function shouldSkipDir(name) {
908
+ return SKIP_DIRS.has(name) || name.startsWith(".") && name !== ".github";
909
+ }
910
+ function walkFiles(projectRoot, options = {}) {
911
+ const files = [];
912
+ function walk(dir) {
913
+ if (files.length >= MAX_WALK_FILES) return;
914
+ let entries;
915
+ try {
916
+ entries = readdirSync2(dir);
917
+ } catch {
918
+ return;
919
+ }
920
+ for (const entry of entries) {
921
+ if (files.length >= MAX_WALK_FILES) return;
922
+ const fullPath = join2(dir, entry);
923
+ let stat;
924
+ try {
925
+ stat = statSync2(fullPath);
926
+ } catch {
927
+ continue;
928
+ }
929
+ if (stat.isDirectory()) {
930
+ if (shouldSkipDir(entry) && !options.includeHidden) continue;
931
+ walk(fullPath);
932
+ continue;
933
+ }
934
+ if (!stat.isFile()) continue;
935
+ const rel = relative(projectRoot, fullPath).replace(/\\/g, "/");
936
+ const ext = extname2(entry).toLowerCase();
937
+ if (!options.extensions || options.extensions.has(ext)) {
938
+ files.push(rel);
939
+ }
940
+ }
941
+ }
942
+ walk(projectRoot);
943
+ return files;
944
+ }
945
+ function segmentToRoute(segment) {
946
+ if (segment.startsWith("(") && segment.endsWith(")")) return null;
947
+ if (segment.startsWith("[") && segment.endsWith("]")) {
948
+ const param = segment.slice(1, -1);
949
+ if (param.startsWith("...")) return `:${param.slice(3)}*`;
950
+ if (param.startsWith("[...") && param.endsWith("]")) return `:${param.slice(4, -1)}*`;
951
+ return `:${param}`;
952
+ }
953
+ return segment;
954
+ }
955
+ function walkNextAppRoutes(dir, projectRoot, segments) {
956
+ let entries;
957
+ try {
958
+ entries = readdirSync2(dir);
959
+ } catch {
960
+ return [];
961
+ }
962
+ const routes = [];
963
+ const pageFile = entries.find((entry) => /^page\.(tsx|ts|jsx|js)$/.test(entry));
964
+ const hasLayout = entries.some((entry) => /^layout\.(tsx|ts|jsx|js)$/.test(entry));
965
+ if (pageFile) {
966
+ routes.push({
967
+ path: `/${segments.filter(Boolean).join("/")}` || "/",
968
+ file: relative(projectRoot, join2(dir, pageFile)).replace(/\\/g, "/"),
969
+ hasLayout
970
+ });
971
+ }
972
+ for (const entry of entries) {
973
+ if (shouldSkipDir(entry)) continue;
974
+ const fullPath = join2(dir, entry);
975
+ try {
976
+ if (!statSync2(fullPath).isDirectory()) continue;
977
+ } catch {
978
+ continue;
979
+ }
980
+ const routeSegment = segmentToRoute(entry);
981
+ routes.push(...walkNextAppRoutes(fullPath, projectRoot, routeSegment === null ? segments : [...segments, routeSegment]));
982
+ }
983
+ return routes;
984
+ }
985
+ function fileRouteFromPath(file, baseDir) {
986
+ let withoutExt = file.slice(0, -extname2(file).length);
987
+ if (withoutExt.endsWith("/index")) withoutExt = withoutExt.slice(0, -"/index".length);
988
+ withoutExt = withoutExt.replace(new RegExp(`^${baseDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), "");
989
+ const parts = withoutExt.split("/").filter(Boolean).map((part) => segmentToRoute(part)).filter(Boolean);
990
+ return `/${parts.join("/")}` || "/";
991
+ }
992
+ function scanFileRoutes(projectRoot, baseDir, extensions = PAGE_EXTENSIONS) {
993
+ const fullBase = join2(projectRoot, baseDir);
994
+ if (!existsSync2(fullBase)) return [];
995
+ return walkFiles(fullBase, { extensions }).map((file) => {
996
+ const rel = relative(projectRoot, join2(fullBase, file)).replace(/\\/g, "/");
997
+ return { path: fileRouteFromPath(rel, baseDir), file: rel, hasLayout: false };
998
+ });
999
+ }
1000
+ function scanReactRouter(projectRoot) {
1001
+ const files = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
1002
+ const routes = /* @__PURE__ */ new Map();
1003
+ let hashRouting = false;
1004
+ for (const file of files) {
1005
+ const content = readTextFile(join2(projectRoot, file));
1006
+ if (!content) continue;
1007
+ if (content.includes("HashRouter") || content.includes("createHashRouter")) hashRouting = true;
1008
+ const jsxRouteRegex = /<Route\b[^>]*\bpath\s*=\s*["']([^"']+)["']/g;
1009
+ let match;
1010
+ while ((match = jsxRouteRegex.exec(content)) !== null) {
1011
+ const route = match[1] || "/";
1012
+ routes.set(route, { path: route, file, hasLayout: false });
1013
+ }
1014
+ const objectRouteRegex = /\bpath\s*:\s*["']([^"']+)["']/g;
1015
+ while ((match = objectRouteRegex.exec(content)) !== null) {
1016
+ const route = match[1] || "/";
1017
+ if (route.startsWith("/")) routes.set(route, { path: route, file, hasLayout: false });
1018
+ }
1019
+ }
1020
+ return { routes: [...routes.values()], hashRouting };
1021
+ }
1022
+ function scanRoutes(projectRoot, detection) {
1023
+ const appRoutes = ["src/app", "app"].flatMap(
1024
+ (dir) => existsSync2(join2(projectRoot, dir)) ? walkNextAppRoutes(join2(projectRoot, dir), projectRoot, []) : []
1025
+ );
1026
+ const pagesRoutes = ["src/pages", "pages"].flatMap((dir) => scanFileRoutes(projectRoot, dir));
1027
+ if (detection.framework === "nextjs") {
1028
+ if (appRoutes.length > 0 && pagesRoutes.length > 0) return { strategy: "mixed-next-router", routes: [...appRoutes, ...pagesRoutes] };
1029
+ if (appRoutes.length > 0) return { strategy: "app-router", routes: appRoutes };
1030
+ if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
1031
+ }
1032
+ if (detection.framework === "svelte") {
1033
+ const routes = scanFileRoutes(projectRoot, "src/routes", /* @__PURE__ */ new Set([".svelte", ".ts", ".js"]));
1034
+ if (routes.length > 0) return { strategy: "sveltekit-router", routes };
1035
+ }
1036
+ if (detection.framework === "nuxt") {
1037
+ const routes = ["pages", "app/pages"].flatMap((dir) => scanFileRoutes(projectRoot, dir, /* @__PURE__ */ new Set([".vue"])));
1038
+ if (routes.length > 0) return { strategy: "nuxt-router", routes };
1039
+ }
1040
+ if (detection.framework === "vue") {
1041
+ const router = scanReactRouter(projectRoot);
1042
+ if (router.routes.length > 0) return { strategy: "vue-router", routes: router.routes };
1043
+ }
1044
+ const reactRouter = scanReactRouter(projectRoot);
1045
+ if (reactRouter.routes.length > 0) return { strategy: "react-router", routes: reactRouter.routes };
1046
+ if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
1047
+ if (existsSync2(join2(projectRoot, "index.html"))) {
1048
+ return { strategy: "static-html", routes: [{ path: "/", file: "index.html", hasLayout: false }] };
1049
+ }
1050
+ if (existsSync2(join2(projectRoot, "docs", "index.html"))) {
1051
+ return { strategy: "static-html", routes: [{ path: "/", file: "docs/index.html", hasLayout: false }] };
1052
+ }
1053
+ return { strategy: "none", routes: [] };
1054
+ }
1055
+ function countComponents(projectRoot, routes) {
1056
+ const sourceFiles = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
1057
+ const componentFiles = sourceFiles.filter((file) => {
1058
+ const base = file.split("/").pop() ?? file;
1059
+ return /^[A-Z][\w-]*\.(tsx|ts|jsx|js|vue|svelte)$/.test(base);
1060
+ });
1061
+ const directories = [...new Set(componentFiles.map((file) => file.split("/").slice(0, -1).join("/")).filter(Boolean))].slice(0, 16);
1062
+ return {
1063
+ pageCount: routes.routes.length,
1064
+ componentCount: componentFiles.length,
1065
+ directories
1066
+ };
1067
+ }
1068
+ function extractCssEvidence(content) {
1069
+ const variableMatches = [...content.matchAll(/--[\w-]+\s*:/g)];
1070
+ const colorMatches = [...content.matchAll(/#[0-9a-fA-F]{3,8}\b|rgb[a]?\(|hsl[a]?\(/g)];
1071
+ const themeSignals = /* @__PURE__ */ new Set();
1072
+ if (/\bdark\b|color-scheme:\s*dark|\[data-theme=['"]dark['"]|\.dark\b/.test(content)) themeSignals.add("dark mode");
1073
+ if (/\[data-theme=|data-theme|theme-\w+/.test(content)) themeSignals.add("theme selector");
1074
+ if (/prefers-color-scheme/.test(content)) themeSignals.add("system color preference");
1075
+ return {
1076
+ variables: variableMatches.length,
1077
+ colors: colorMatches.length,
1078
+ dark: themeSignals.has("dark mode") || themeSignals.has("system color preference"),
1079
+ themeSignals: [...themeSignals]
1080
+ };
1081
+ }
1082
+ function scanStyling(projectRoot, detection) {
1083
+ const deps = detection.dependencies;
1084
+ const cssFiles = walkFiles(projectRoot, { extensions: STYLE_EXTENSIONS });
1085
+ const themeSignals = /* @__PURE__ */ new Set();
1086
+ let cssVariableCount = 0;
1087
+ let colorTokenCount = 0;
1088
+ let darkMode = false;
1089
+ let configFile = null;
1090
+ let approach = "unknown";
1091
+ const tailwindConfig = ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs", "tailwind.config.cjs"].find(
1092
+ (file) => existsSync2(join2(projectRoot, file))
1093
+ );
1094
+ if (tailwindConfig || deps.tailwindcss) {
1095
+ approach = "tailwind";
1096
+ configFile = tailwindConfig ?? "package.json";
1097
+ } else if (deps["@decantr/css"] || existsSync2(join2(projectRoot, "src/styles/tokens.css")) && existsSync2(join2(projectRoot, "src/styles/treatments.css"))) {
1098
+ approach = "decantr-css";
1099
+ configFile = deps["@decantr/css"] ? "package.json" : "src/styles/tokens.css";
1100
+ } else if (deps.bootstrap) {
1101
+ approach = "bootstrap";
1102
+ configFile = "package.json";
1103
+ } else if (deps["@mui/material"]) {
1104
+ approach = "mui";
1105
+ configFile = "package.json";
1106
+ } else if (deps["@chakra-ui/react"]) {
1107
+ approach = "chakra";
1108
+ configFile = "package.json";
1109
+ } else if (cssFiles.some((file) => file.endsWith(".module.css"))) {
1110
+ approach = "css-modules";
1111
+ } else if (cssFiles.length > 0) {
1112
+ approach = "css";
1113
+ }
1114
+ for (const file of cssFiles.slice(0, 80)) {
1115
+ const content = readTextFile(join2(projectRoot, file), 256 * 1024);
1116
+ if (!content) continue;
1117
+ const evidence = extractCssEvidence(content);
1118
+ cssVariableCount += evidence.variables;
1119
+ colorTokenCount += evidence.colors;
1120
+ darkMode ||= evidence.dark;
1121
+ for (const signal of evidence.themeSignals) themeSignals.add(signal);
1122
+ }
1123
+ return {
1124
+ approach,
1125
+ configFile,
1126
+ cssVariableCount,
1127
+ colorTokenCount,
1128
+ darkMode,
1129
+ themeSignals: [...themeSignals]
1130
+ };
1131
+ }
1132
+ function findAssistantRules(projectRoot) {
1133
+ return RULE_FILES.filter((file) => existsSync2(join2(projectRoot, file)));
1134
+ }
1135
+ function scanStaticHosting(projectRoot, detection) {
1136
+ const packageRead = readPackageJson(projectRoot);
1137
+ const pkg = packageRead.value;
1138
+ const evidence = [];
1139
+ const homepageUrl = typeof pkg?.homepage === "string" ? pkg.homepage : null;
1140
+ let basePath = null;
1141
+ let hashRouting = false;
1142
+ if (homepageUrl?.includes("github.io")) {
1143
+ evidence.push("package.json homepage points at GitHub Pages");
1144
+ try {
1145
+ const url = new URL(homepageUrl);
1146
+ basePath = url.pathname && url.pathname !== "/" ? url.pathname : null;
1147
+ } catch {
1148
+ }
1149
+ }
1150
+ if (detection.dependencies["gh-pages"] || pkg?.scripts?.deploy?.includes("gh-pages")) {
1151
+ evidence.push("package scripts or dependencies reference gh-pages");
1152
+ }
1153
+ if (existsSync2(join2(projectRoot, "docs", "index.html"))) evidence.push("docs/index.html can serve GitHub Pages");
1154
+ if (existsSync2(join2(projectRoot, "404.html")) || existsSync2(join2(projectRoot, "docs", "404.html"))) {
1155
+ evidence.push("404.html fallback is present");
1156
+ }
1157
+ const workflowDir = join2(projectRoot, ".github", "workflows");
1158
+ if (existsSync2(workflowDir)) {
1159
+ for (const file of walkFiles(workflowDir, { extensions: /* @__PURE__ */ new Set([".yml", ".yaml"]) }).slice(0, 20)) {
1160
+ const content = readTextFile(join2(workflowDir, file));
1161
+ if (content && /pages|gh-pages|upload-pages-artifact|deploy-pages/i.test(content)) {
1162
+ evidence.push(`GitHub Pages workflow hint: .github/workflows/${file}`);
1163
+ break;
1164
+ }
1165
+ }
1166
+ }
1167
+ const sourceFiles = walkFiles(projectRoot, { extensions: SOURCE_EXTENSIONS });
1168
+ for (const file of sourceFiles.slice(0, 200)) {
1169
+ const content = readTextFile(join2(projectRoot, file), 128 * 1024);
1170
+ if (!content) continue;
1171
+ if (content.includes("HashRouter") || content.includes("createHashRouter")) {
1172
+ hashRouting = true;
1173
+ evidence.push(`hash routing detected in ${file}`);
1174
+ break;
1175
+ }
1176
+ }
1177
+ for (const config of ["vite.config.ts", "vite.config.js", "vite.config.mjs"]) {
1178
+ const content = readTextFile(join2(projectRoot, config), 128 * 1024);
1179
+ const baseMatch = content?.match(/\bbase\s*:\s*['"]([^'"]+)['"]/);
1180
+ if (baseMatch?.[1]) {
1181
+ basePath = baseMatch[1];
1182
+ evidence.push(`Vite base path configured in ${config}`);
1183
+ break;
1184
+ }
1185
+ }
1186
+ return {
1187
+ githubPagesLikely: evidence.some((item) => item.toLowerCase().includes("github") || item.toLowerCase().includes("pages")),
1188
+ evidence: [...new Set(evidence)].slice(0, 12),
1189
+ homepageUrl,
1190
+ basePath,
1191
+ hashRouting
1192
+ };
1193
+ }
1194
+ function buildApplicability(detection, routes, components) {
1195
+ if (!WEB_FRAMEWORKS.has(detection.framework)) {
1196
+ const label = detection.primaryLanguage === "python" ? "Not a Brownfield UI target" : "No web UI target detected";
1197
+ return {
1198
+ status: "not_applicable",
1199
+ label,
1200
+ reasons: [
1201
+ detection.primaryLanguage === "python" ? "This looks like a Python/backend repository rather than a frontend app." : "Decantr did not find a supported frontend framework or static HTML entrypoint."
1202
+ ]
1203
+ };
1204
+ }
1205
+ if (routes.routes.length > 0 || components.componentCount > 0) {
1206
+ return {
1207
+ status: "strong_fit",
1208
+ label: "Good Brownfield scan target",
1209
+ reasons: ["A supported web UI framework or static site entrypoint was detected.", "Routes or UI component files are present."]
1210
+ };
1211
+ }
1212
+ return {
1213
+ status: "partial_fit",
1214
+ label: "Partial Brownfield signal",
1215
+ reasons: ["A web framework was detected, but route/component evidence is thin."]
1216
+ };
1217
+ }
1218
+ function buildConfidence(applicability, detection, routes, styling) {
1219
+ let score = 20;
1220
+ const reasons = [];
1221
+ if (detection.packageJsonPresent && detection.packageJsonValid) {
1222
+ score += 15;
1223
+ reasons.push("package.json was readable");
1224
+ }
1225
+ if (WEB_FRAMEWORKS.has(detection.framework)) {
1226
+ score += 25;
1227
+ reasons.push(`${detection.framework} framework signal found`);
1228
+ }
1229
+ if (routes.routes.length > 0) {
1230
+ score += 20;
1231
+ reasons.push(`${routes.routes.length} route signal(s) found`);
1232
+ }
1233
+ if (styling.approach !== "unknown") {
1234
+ score += 10;
1235
+ reasons.push(`${styling.approach} styling signal found`);
1236
+ }
1237
+ if (applicability.status === "not_applicable") score = Math.min(score, 35);
1238
+ const clamped = Math.max(5, Math.min(98, score));
1239
+ return {
1240
+ score: clamped,
1241
+ level: clamped >= 75 ? "high" : clamped >= 45 ? "medium" : "low",
1242
+ reasons: reasons.length > 0 ? reasons : ["Only weak project signals were found."]
1243
+ };
1244
+ }
1245
+ function buildFindings(input) {
1246
+ const findings = [];
1247
+ const { detection, routes, styling, hosting, assistantRules, applicability, pagesProbe } = input;
1248
+ if (!detection.packageJsonPresent && detection.primaryLanguage !== "html") {
1249
+ findings.push({
1250
+ id: "package-manifest-missing",
1251
+ severity: "warn",
1252
+ title: "No JavaScript package manifest",
1253
+ message: "Decantr could not find package.json, so framework and dependency confidence is limited.",
1254
+ evidence: ["package.json missing"]
1255
+ });
1256
+ } else if (detection.packageJsonPresent && !detection.packageJsonValid) {
1257
+ findings.push({
1258
+ id: "package-manifest-invalid",
1259
+ severity: "warn",
1260
+ title: "package.json could not be parsed",
1261
+ message: "The scan continued with file-system evidence, but dependency confidence is limited.",
1262
+ evidence: ["package.json parse failed"]
1263
+ });
1264
+ }
1265
+ if (applicability.status === "not_applicable") {
1266
+ findings.push({
1267
+ id: "not-brownfield-ui-target",
1268
+ severity: "info",
1269
+ title: "Not a Brownfield UI target",
1270
+ message: "This repository does not look like a supported frontend application for Decantr adoption.",
1271
+ evidence: [`primary language: ${detection.primaryLanguage}`, `framework: ${detection.framework}`],
1272
+ recommendation: "Use Decantr scan on a frontend app or GitHub Pages site."
1273
+ });
1274
+ return findings;
1275
+ }
1276
+ if (routes.routes.length === 0) {
1277
+ findings.push({
1278
+ id: "route-map-thin",
1279
+ severity: "warn",
1280
+ title: "Route map is thin",
1281
+ message: "No route declarations were detected, so a future Decantr contract would need manual route confirmation.",
1282
+ evidence: [`route strategy: ${routes.strategy}`],
1283
+ recommendation: "Run `decantr analyze` locally when you are ready for a proposal-backed attach."
1284
+ });
1285
+ } else {
1286
+ findings.push({
1287
+ id: "route-map-detected",
1288
+ severity: "success",
1289
+ title: "Route map detected",
1290
+ message: `Decantr found ${routes.routes.length} route signal(s) that can seed a Brownfield contract.`,
1291
+ evidence: routes.routes.slice(0, 5).map((route) => `${route.path} -> ${route.file}`)
1292
+ });
1293
+ }
1294
+ if (styling.approach === "unknown") {
1295
+ findings.push({
1296
+ id: "style-authority-unclear",
1297
+ severity: "warn",
1298
+ title: "Style authority is unclear",
1299
+ message: "The scan did not find Tailwind, Decantr CSS, a common component library, or clear CSS files.",
1300
+ evidence: ["styling approach: unknown"]
1301
+ });
1302
+ } else {
1303
+ findings.push({
1304
+ id: "style-authority-detected",
1305
+ severity: "success",
1306
+ title: "Style authority detected",
1307
+ message: `The project appears to use ${styling.approach}. Decantr should preserve that authority during Brownfield adoption.`,
1308
+ evidence: [styling.configFile ? `${styling.approach}: ${styling.configFile}` : `styling approach: ${styling.approach}`]
1309
+ });
1310
+ }
1311
+ if (hosting.githubPagesLikely) {
1312
+ findings.push({
1313
+ id: "github-pages-signal",
1314
+ severity: "info",
1315
+ title: "GitHub Pages signal",
1316
+ message: "The repository has static-hosting evidence. The report separates repo evidence from published-site evidence.",
1317
+ evidence: hosting.evidence.slice(0, 5)
1318
+ });
1319
+ }
1320
+ if (hosting.githubPagesLikely && !hosting.hashRouting && detection.framework === "react") {
1321
+ findings.push({
1322
+ id: "github-pages-routing-risk",
1323
+ severity: "warn",
1324
+ title: "Static routing risk",
1325
+ message: "React apps on GitHub Pages usually need hash routing or a 404 fallback for direct routes.",
1326
+ evidence: ["HashRouter not detected"],
1327
+ recommendation: "Confirm whether the published site uses hash routing or a Pages 404 fallback."
1328
+ });
1329
+ }
1330
+ if (assistantRules.length === 0) {
1331
+ findings.push({
1332
+ id: "assistant-rules-missing",
1333
+ severity: "info",
1334
+ title: "No assistant rules detected",
1335
+ message: "No common AI-assistant rule file was found. Decantr can add scoped task context after adoption.",
1336
+ evidence: ["No CLAUDE.md, AGENTS.md, .cursorrules, or Copilot instructions detected"]
1337
+ });
1338
+ }
1339
+ if (pagesProbe?.checked && !pagesProbe.reachable) {
1340
+ findings.push({
1341
+ id: "published-site-unreachable",
1342
+ severity: "warn",
1343
+ title: "Published site was not reachable",
1344
+ message: "Decantr could inspect the repository, but the inferred published site did not return a usable response.",
1345
+ evidence: [pagesProbe.error ?? `status: ${pagesProbe.status ?? "unknown"}`]
1346
+ });
1347
+ }
1348
+ return findings;
1349
+ }
1350
+ function buildCommands(applicability) {
1351
+ const commands = ["npx @decantr/cli scan"];
1352
+ if (applicability.status === "strong_fit" || applicability.status === "partial_fit") {
1353
+ commands.push("npx @decantr/cli adopt --yes");
1354
+ commands.push("npx @decantr/cli doctor");
1355
+ }
1356
+ return commands;
1357
+ }
1358
+ async function scanProject(projectRoot, options = {}) {
1359
+ const detection = detectProject(projectRoot);
1360
+ const routes = scanRoutes(projectRoot, detection);
1361
+ routes.routes = routes.routes.slice(0, MAX_REPORT_ROUTES);
1362
+ const components = countComponents(projectRoot, routes);
1363
+ const styling = scanStyling(projectRoot, detection);
1364
+ const assistantRules = findAssistantRules(projectRoot);
1365
+ const staticHosting = scanStaticHosting(projectRoot, detection);
1366
+ const pagesProbe = options.pagesProbe ?? null;
1367
+ const applicability = buildApplicability(detection, routes, components);
1368
+ const confidence = buildConfidence(applicability, detection, routes, styling);
1369
+ const rawInput = options.input ?? { kind: "local", value: "." };
1370
+ const input = rawInput.kind === "local" && isAbsolute(rawInput.value) ? { ...rawInput, value: "." } : rawInput;
1371
+ return {
1372
+ $schema: SCAN_REPORT_SCHEMA_URL,
1373
+ schemaVersion: "scan-report.v1",
1374
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1375
+ input,
1376
+ source: {
1377
+ repository: options.repository ?? null,
1378
+ publishedSiteUrl: options.publishedSiteUrl ?? staticHosting.homepageUrl ?? null
1379
+ },
1380
+ confidence,
1381
+ applicability,
1382
+ project: {
1383
+ framework: detection.framework,
1384
+ frameworkVersion: detection.frameworkVersion,
1385
+ packageManager: detection.packageManager,
1386
+ primaryLanguage: detection.primaryLanguage,
1387
+ hasTypeScript: detection.hasTypeScript,
1388
+ hasTailwind: detection.hasTailwind,
1389
+ hasDecantr: detection.hasDecantr,
1390
+ packageName: detection.packageName
1391
+ },
1392
+ routes: {
1393
+ strategy: routes.strategy,
1394
+ count: routes.routes.length,
1395
+ items: routes.routes
1396
+ },
1397
+ components,
1398
+ styling,
1399
+ staticHosting,
1400
+ assistant: {
1401
+ ruleFiles: assistantRules
1402
+ },
1403
+ pagesProbe,
1404
+ findings: buildFindings({ detection, routes, styling, hosting: staticHosting, assistantRules, applicability, pagesProbe }),
1405
+ recommendedCommands: buildCommands(applicability),
1406
+ privacy: {
1407
+ sourceUploaded: input.kind !== "local",
1408
+ persistedByDecantr: false,
1409
+ notes: [
1410
+ "V1 scans are read-only and do not install dependencies, build projects, execute scripts, or open pull requests.",
1411
+ "Hosted scans use temporary public-repo checkouts and return an ephemeral report."
1412
+ ]
1413
+ }
1414
+ };
1415
+ }
1416
+ function createUnavailableScanReport(input) {
1417
+ return {
1418
+ $schema: SCAN_REPORT_SCHEMA_URL,
1419
+ schemaVersion: "scan-report.v1",
1420
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1421
+ input: input.scanInput,
1422
+ source: {
1423
+ repository: input.repository ?? null,
1424
+ publishedSiteUrl: input.publishedSiteUrl ?? null
1425
+ },
1426
+ confidence: {
1427
+ level: "low",
1428
+ score: 5,
1429
+ reasons: ["Repository source could not be inspected."]
1430
+ },
1431
+ applicability: {
1432
+ status: "unknown",
1433
+ label: "Scan unavailable",
1434
+ reasons: [input.message]
1435
+ },
1436
+ project: {
1437
+ framework: "unknown",
1438
+ frameworkVersion: null,
1439
+ packageManager: "unknown",
1440
+ primaryLanguage: "unknown",
1441
+ hasTypeScript: false,
1442
+ hasTailwind: false,
1443
+ hasDecantr: false,
1444
+ packageName: null
1445
+ },
1446
+ routes: { strategy: "none", count: 0, items: [] },
1447
+ components: { pageCount: 0, componentCount: 0, directories: [] },
1448
+ styling: {
1449
+ approach: "unknown",
1450
+ configFile: null,
1451
+ cssVariableCount: 0,
1452
+ colorTokenCount: 0,
1453
+ darkMode: false,
1454
+ themeSignals: []
1455
+ },
1456
+ staticHosting: {
1457
+ githubPagesLikely: false,
1458
+ evidence: [],
1459
+ homepageUrl: null,
1460
+ basePath: null,
1461
+ hashRouting: false
1462
+ },
1463
+ assistant: { ruleFiles: [] },
1464
+ pagesProbe: input.pagesProbe ?? null,
1465
+ findings: [
1466
+ {
1467
+ id: "source-unavailable",
1468
+ severity: "error",
1469
+ title: input.title,
1470
+ message: input.message,
1471
+ evidence: input.evidence ?? []
1472
+ }
1473
+ ],
1474
+ recommendedCommands: ["npx @decantr/cli scan"],
1475
+ privacy: {
1476
+ sourceUploaded: input.scanInput.kind !== "local",
1477
+ persistedByDecantr: false,
1478
+ notes: [
1479
+ "Decantr did not persist this report.",
1480
+ "No dependencies were installed, no scripts were executed, and no pull requests were opened."
1481
+ ]
1482
+ }
1483
+ };
1484
+ }
1485
+ function decodeHtml(value) {
1486
+ return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
1487
+ }
1488
+ function extractHtmlAttribute(tag, attr) {
1489
+ const match = tag.match(new RegExp(`${attr}\\s*=\\s*["']([^"']+)["']`, "i"));
1490
+ return match?.[1] ? decodeHtml(match[1]) : null;
1491
+ }
1492
+ async function probePublishedSite(url, options = {}) {
1493
+ const fetchImpl = options.fetchImpl ?? fetch;
1494
+ const controller = new AbortController();
1495
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 6e3);
1496
+ try {
1497
+ const response = await fetchImpl(url, {
1498
+ method: "GET",
1499
+ redirect: "follow",
1500
+ signal: controller.signal,
1501
+ headers: {
1502
+ Accept: "text/html,application/xhtml+xml",
1503
+ "User-Agent": "Decantr-Scan/1.0 (+https://decantr.ai/scan)"
1504
+ }
1505
+ });
1506
+ const text = (await response.text()).slice(0, MAX_FILE_READ_BYTES);
1507
+ const titleMatch = text.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
1508
+ const descriptionTag = text.match(/<meta\b[^>]*(?:name|property)=["']description["'][^>]*>/i)?.[0] ?? null;
1509
+ const canonicalTag = text.match(/<link\b[^>]*rel=["']canonical["'][^>]*>/i)?.[0] ?? null;
1510
+ const assetSamples = [...text.matchAll(/\b(?:src|href)=["']([^"']+)["']/gi)].map((match) => match[1]).filter((value) => Boolean(value)).slice(0, 40);
1511
+ const routingHints = [];
1512
+ if (text.includes("/#/") || text.includes("#/")) routingHints.push("hash route URLs present");
1513
+ if (text.includes("data-reactroot") || text.includes('id="root"') || text.includes("id='root'")) {
1514
+ routingHints.push("client app mount detected");
1515
+ }
1516
+ return {
1517
+ url,
1518
+ finalUrl: response.url || url,
1519
+ checked: true,
1520
+ reachable: response.ok,
1521
+ status: response.status,
1522
+ title: titleMatch?.[1] ? decodeHtml(titleMatch[1].replace(/\s+/g, " ")) : null,
1523
+ description: descriptionTag ? extractHtmlAttribute(descriptionTag, "content") : null,
1524
+ canonicalUrl: canonicalTag ? extractHtmlAttribute(canonicalTag, "href") : null,
1525
+ assetHints: {
1526
+ rootRelative: assetSamples.filter((item) => item.startsWith("/") && !item.startsWith("//")).length,
1527
+ relative: assetSamples.filter((item) => !item.startsWith("/") && !/^https?:\/\//.test(item)).length,
1528
+ absolute: assetSamples.filter((item) => /^https?:\/\//.test(item) || item.startsWith("//")).length,
1529
+ samples: assetSamples.slice(0, 8)
1530
+ },
1531
+ routingHints,
1532
+ error: null
1533
+ };
1534
+ } catch (error) {
1535
+ return {
1536
+ url,
1537
+ finalUrl: null,
1538
+ checked: true,
1539
+ reachable: false,
1540
+ status: null,
1541
+ title: null,
1542
+ description: null,
1543
+ canonicalUrl: null,
1544
+ assetHints: { rootRelative: 0, relative: 0, absolute: 0, samples: [] },
1545
+ routingHints: [],
1546
+ error: error instanceof Error ? error.message : "Published site probe failed."
1547
+ };
1548
+ } finally {
1549
+ clearTimeout(timeout);
1550
+ }
1551
+ }
1552
+ function resolveGitHubScanInput(input) {
1553
+ let url;
1554
+ try {
1555
+ url = new URL(input.trim());
1556
+ } catch {
1557
+ throw new Error("Enter a valid GitHub repository URL or GitHub Pages URL.");
1558
+ }
1559
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
1560
+ throw new Error("Scan URLs must use http or https.");
1561
+ }
1562
+ const host = url.hostname.toLowerCase();
1563
+ if (host === "github.com" || host === "www.github.com") {
1564
+ const [owner, repoSegment] = url.pathname.split("/").filter(Boolean);
1565
+ if (!owner || !repoSegment) {
1566
+ throw new Error("GitHub repository URLs must include owner and repository.");
1567
+ }
1568
+ const repo = repoSegment.replace(/\.git$/, "");
1569
+ return {
1570
+ inputKind: "github-repo",
1571
+ normalizedInput: `https://github.com/${owner}/${repo}`,
1572
+ repository: {
1573
+ owner,
1574
+ repo,
1575
+ url: `https://github.com/${owner}/${repo}`
1576
+ },
1577
+ publishedSiteUrl: `https://${owner}.github.io/${repo}/`,
1578
+ warnings: url.pathname.split("/").filter(Boolean).length > 2 ? ["Extra GitHub URL path segments were ignored."] : []
1579
+ };
1580
+ }
1581
+ if (host.endsWith(".github.io")) {
1582
+ const owner = host.slice(0, -".github.io".length);
1583
+ const segments = url.pathname.split("/").filter(Boolean);
1584
+ const repo = segments[0] ?? `${owner}.github.io`;
1585
+ const publishedSiteUrl = segments[0] ? `https://${owner}.github.io/${repo}/` : `https://${owner}.github.io/`;
1586
+ return {
1587
+ inputKind: "github-pages",
1588
+ normalizedInput: publishedSiteUrl,
1589
+ repository: {
1590
+ owner,
1591
+ repo,
1592
+ url: `https://github.com/${owner}/${repo}`
1593
+ },
1594
+ publishedSiteUrl,
1595
+ warnings: segments.length > 1 ? ["Only the first GitHub Pages path segment was used to infer the repository."] : []
1596
+ };
1597
+ }
1598
+ throw new Error("V1 hosted scans support GitHub repositories and GitHub Pages URLs.");
1599
+ }
1600
+
757
1601
  // src/index.ts
758
1602
  var VERIFICATION_SCHEMA_URLS = {
759
1603
  common: "https://decantr.ai/schemas/verification-report.common.v1.json",
@@ -761,6 +1605,7 @@ var VERIFICATION_SCHEMA_URLS = {
761
1605
  projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json",
762
1606
  decantrCi: "https://decantr.ai/schemas/decantr-ci-report.v1.json",
763
1607
  evidenceBundle: "https://decantr.ai/schemas/evidence-bundle.v1.json",
1608
+ scanReport: "https://decantr.ai/schemas/scan-report.v1.json",
764
1609
  workspaceHealth: "https://decantr.ai/schemas/workspace-health-report.v1.json",
765
1610
  fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
766
1611
  showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
@@ -770,16 +1615,16 @@ function hashString(value) {
770
1615
  return createHash("sha256").update(value).digest("hex").slice(0, 16);
771
1616
  }
772
1617
  function hashFile(path) {
773
- if (!existsSync2(path)) return null;
1618
+ if (!existsSync3(path)) return null;
774
1619
  try {
775
- return createHash("sha256").update(readFileSync2(path)).digest("hex");
1620
+ return createHash("sha256").update(readFileSync3(path)).digest("hex");
776
1621
  } catch {
777
1622
  return null;
778
1623
  }
779
1624
  }
780
1625
  function provenanceEntry(projectRoot, relativePath) {
781
- const path = join2(projectRoot, relativePath);
782
- if (!existsSync2(path)) {
1626
+ const path = join3(projectRoot, relativePath);
1627
+ if (!existsSync3(path)) {
783
1628
  return {
784
1629
  path: relativePath,
785
1630
  present: false,
@@ -789,7 +1634,7 @@ function provenanceEntry(projectRoot, relativePath) {
789
1634
  }
790
1635
  let generatedAt = null;
791
1636
  try {
792
- generatedAt = statSync2(path).mtime.toISOString();
1637
+ generatedAt = statSync3(path).mtime.toISOString();
793
1638
  } catch {
794
1639
  generatedAt = null;
795
1640
  }
@@ -801,7 +1646,7 @@ function provenanceEntry(projectRoot, relativePath) {
801
1646
  };
802
1647
  }
803
1648
  function provenanceForPath(projectRoot, path) {
804
- const rel = isAbsolute(path) ? relative(projectRoot, path).replace(/\\/g, "/") : path;
1649
+ const rel = isAbsolute2(path) ? relative2(projectRoot, path).replace(/\\/g, "/") : path;
805
1650
  return provenanceEntry(projectRoot, rel && !rel.startsWith("..") ? rel : basename(path));
806
1651
  }
807
1652
  function redactEvidenceText(projectRoot, value) {
@@ -825,7 +1670,7 @@ function createContractAssertions(projectRoot, audit) {
825
1670
  const assertions = [];
826
1671
  const essence = audit?.essence;
827
1672
  const v4 = essence && isV4(essence) ? essence : null;
828
- const contextDir = join2(projectRoot, ".decantr", "context");
1673
+ const contextDir = join3(projectRoot, ".decantr", "context");
829
1674
  const packManifest = audit?.packManifest ?? null;
830
1675
  assertions.push(
831
1676
  assertion({
@@ -835,7 +1680,7 @@ function createContractAssertions(projectRoot, audit) {
835
1680
  rule: "essence-present",
836
1681
  status: essence ? "passed" : "failed",
837
1682
  message: essence ? "Decantr Essence contract is present." : "No Decantr Essence contract was found.",
838
- evidence: [redactEvidenceText(projectRoot, join2(projectRoot, "decantr.essence.json"))],
1683
+ evidence: [redactEvidenceText(projectRoot, join3(projectRoot, "decantr.essence.json"))],
839
1684
  suggestedFix: essence ? void 0 : "Run `decantr init` or restore decantr.essence.json."
840
1685
  })
841
1686
  );
@@ -907,7 +1752,7 @@ function createContractAssertions(projectRoot, audit) {
907
1752
  rule: "pack-manifest-present",
908
1753
  status: packManifest ? "passed" : "failed",
909
1754
  message: packManifest ? "Compiled execution pack manifest is present." : "Compiled execution pack manifest is missing.",
910
- evidence: [redactEvidenceText(projectRoot, join2(contextDir, "pack-manifest.json"))],
1755
+ evidence: [redactEvidenceText(projectRoot, join3(contextDir, "pack-manifest.json"))],
911
1756
  suggestedFix: packManifest ? void 0 : "Run `decantr registry compile-packs decantr.essence.json --write-context` to hydrate the full context pack bundle."
912
1757
  })
913
1758
  );
@@ -919,21 +1764,21 @@ function createContractAssertions(projectRoot, audit) {
919
1764
  rule: "review-pack-present",
920
1765
  status: audit?.reviewPack ? "passed" : "failed",
921
1766
  message: audit?.reviewPack ? "Compiled review pack is present." : "Compiled review pack is missing.",
922
- evidence: [redactEvidenceText(projectRoot, join2(contextDir, "review-pack.json"))],
1767
+ evidence: [redactEvidenceText(projectRoot, join3(contextDir, "review-pack.json"))],
923
1768
  suggestedFix: audit?.reviewPack ? void 0 : "Run `decantr registry compile-packs decantr.essence.json --write-context` so critique has the compiled review contract."
924
1769
  })
925
1770
  );
926
- const tokensPath = join2(projectRoot, "src", "styles", "tokens.css");
1771
+ const tokensPath = join3(projectRoot, "src", "styles", "tokens.css");
927
1772
  assertions.push(
928
1773
  assertion({
929
1774
  id: "contract.design-token.tokens-file",
930
1775
  category: "design-token",
931
- severity: existsSync2(tokensPath) ? "info" : "warn",
1776
+ severity: existsSync3(tokensPath) ? "info" : "warn",
932
1777
  rule: "tokens-file-present",
933
- status: existsSync2(tokensPath) ? "passed" : "failed",
934
- message: existsSync2(tokensPath) ? "Decantr token CSS file is present." : "Decantr token CSS file was not found.",
1778
+ status: existsSync3(tokensPath) ? "passed" : "failed",
1779
+ message: existsSync3(tokensPath) ? "Decantr token CSS file is present." : "Decantr token CSS file was not found.",
935
1780
  evidence: [redactEvidenceText(projectRoot, tokensPath)],
936
- suggestedFix: existsSync2(tokensPath) ? void 0 : "Run `decantr refresh` or map Decantr tokens into the existing styling system."
1781
+ suggestedFix: existsSync3(tokensPath) ? void 0 : "Run `decantr refresh` or map Decantr tokens into the existing styling system."
937
1782
  })
938
1783
  );
939
1784
  return assertions;
@@ -1038,21 +1883,21 @@ function scoreRatio(numerator, denominator) {
1038
1883
  return Math.min(5, Math.max(1, Math.round(numerator / denominator * 5)));
1039
1884
  }
1040
1885
  function readJsonIfExists(path) {
1041
- if (!existsSync2(path)) return null;
1886
+ if (!existsSync3(path)) return null;
1042
1887
  try {
1043
- return JSON.parse(readFileSync2(path, "utf-8"));
1888
+ return JSON.parse(readFileSync3(path, "utf-8"));
1044
1889
  } catch {
1045
1890
  return null;
1046
1891
  }
1047
1892
  }
1048
1893
  function loadReviewPack(projectRoot) {
1049
1894
  return readJsonIfExists(
1050
- join2(projectRoot, ".decantr", "context", "review-pack.json")
1895
+ join3(projectRoot, ".decantr", "context", "review-pack.json")
1051
1896
  );
1052
1897
  }
1053
1898
  function loadPackManifest(projectRoot) {
1054
1899
  return readJsonIfExists(
1055
- join2(projectRoot, ".decantr", "context", "pack-manifest.json")
1900
+ join3(projectRoot, ".decantr", "context", "pack-manifest.json")
1056
1901
  );
1057
1902
  }
1058
1903
  function collectPackManifestReferences(packManifest) {
@@ -1101,14 +1946,14 @@ function collectPackManifestReferences(packManifest) {
1101
1946
  }
1102
1947
  function collectMissingPackManifestFiles(projectRoot, packManifest = loadPackManifest(projectRoot)) {
1103
1948
  if (!packManifest) return [];
1104
- const contextDir = join2(projectRoot, ".decantr", "context");
1949
+ const contextDir = join3(projectRoot, ".decantr", "context");
1105
1950
  const missing = [];
1106
1951
  for (const reference of collectPackManifestReferences(packManifest)) {
1107
1952
  for (const field of ["markdown", "json"]) {
1108
1953
  const fileName = reference[field];
1109
1954
  if (!fileName) continue;
1110
- const absolutePath = join2(contextDir, fileName);
1111
- if (existsSync2(absolutePath)) continue;
1955
+ const absolutePath = join3(contextDir, fileName);
1956
+ if (existsSync3(absolutePath)) continue;
1112
1957
  missing.push({
1113
1958
  entryId: reference.entryId,
1114
1959
  kind: reference.kind,
@@ -1122,13 +1967,13 @@ function collectMissingPackManifestFiles(projectRoot, packManifest = loadPackMan
1122
1967
  }
1123
1968
  function readTextIfExists(path) {
1124
1969
  try {
1125
- return existsSync2(path) ? readFileSync2(path, "utf-8") : "";
1970
+ return existsSync3(path) ? readFileSync3(path, "utf-8") : "";
1126
1971
  } catch {
1127
1972
  return "";
1128
1973
  }
1129
1974
  }
1130
1975
  function readProjectAdoptionMode(projectRoot) {
1131
- const projectJson = readJsonIfExists(join2(projectRoot, ".decantr", "project.json"));
1976
+ const projectJson = readJsonIfExists(join3(projectRoot, ".decantr", "project.json"));
1132
1977
  const adoptionMode = projectJson?.initialized?.adoptionMode;
1133
1978
  return typeof adoptionMode === "string" ? adoptionMode : null;
1134
1979
  }
@@ -1214,7 +2059,7 @@ function collectProjectSourceFiles(projectRoot) {
1214
2059
  "hooks",
1215
2060
  "providers",
1216
2061
  "server"
1217
- ].map((dir) => join2(projectRoot, dir)).filter((dir) => existsSync2(dir));
2062
+ ].map((dir) => join3(projectRoot, dir)).filter((dir) => existsSync3(dir));
1218
2063
  const rootFileCandidates = [
1219
2064
  "middleware.ts",
1220
2065
  "middleware.tsx",
@@ -1228,7 +2073,7 @@ function collectProjectSourceFiles(projectRoot) {
1228
2073
  "proxy.jsx",
1229
2074
  "proxy.mts",
1230
2075
  "proxy.cts"
1231
- ].map((file) => join2(projectRoot, file)).filter((file) => existsSync2(file));
2076
+ ].map((file) => join3(projectRoot, file)).filter((file) => existsSync3(file));
1232
2077
  const files = /* @__PURE__ */ new Set();
1233
2078
  const ignoredDirNames = /* @__PURE__ */ new Set([
1234
2079
  "node_modules",
@@ -1239,9 +2084,9 @@ function collectProjectSourceFiles(projectRoot) {
1239
2084
  "coverage"
1240
2085
  ]);
1241
2086
  const walk = (dir) => {
1242
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2087
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1243
2088
  if (ignoredDirNames.has(entry.name)) continue;
1244
- const absolutePath = join2(dir, entry.name);
2089
+ const absolutePath = join3(dir, entry.name);
1245
2090
  if (entry.isDirectory()) {
1246
2091
  walk(absolutePath);
1247
2092
  continue;
@@ -1272,7 +2117,7 @@ function isAuditableStyleFile(filePath) {
1272
2117
  return /\.css$/i.test(filePath);
1273
2118
  }
1274
2119
  function collectProjectStyleFiles(projectRoot) {
1275
- const candidates = ["src", "app", "styles"].map((dir) => join2(projectRoot, dir)).filter((dir) => existsSync2(dir));
2120
+ const candidates = ["src", "app", "styles"].map((dir) => join3(projectRoot, dir)).filter((dir) => existsSync3(dir));
1276
2121
  const files = /* @__PURE__ */ new Set();
1277
2122
  const ignoredDirNames = /* @__PURE__ */ new Set([
1278
2123
  "node_modules",
@@ -1283,9 +2128,9 @@ function collectProjectStyleFiles(projectRoot) {
1283
2128
  "coverage"
1284
2129
  ]);
1285
2130
  const walk = (dir) => {
1286
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2131
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1287
2132
  if (ignoredDirNames.has(entry.name)) continue;
1288
- const absolutePath = join2(dir, entry.name);
2133
+ const absolutePath = join3(dir, entry.name);
1289
2134
  if (entry.isDirectory()) {
1290
2135
  walk(absolutePath);
1291
2136
  continue;
@@ -1359,7 +2204,7 @@ function collectRuntimeImportSpecifiers(entry) {
1359
2204
  function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
1360
2205
  let basePath = null;
1361
2206
  if (specifier.startsWith("@/")) {
1362
- basePath = join2(projectRoot, "src", specifier.slice(2));
2207
+ basePath = join3(projectRoot, "src", specifier.slice(2));
1363
2208
  } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
1364
2209
  basePath = resolve(dirname(sourceAbsolutePath), specifier);
1365
2210
  }
@@ -1372,14 +2217,14 @@ function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
1372
2217
  `${basePath}.jsx`,
1373
2218
  `${basePath}.mts`,
1374
2219
  `${basePath}.cts`,
1375
- join2(basePath, "index.ts"),
1376
- join2(basePath, "index.tsx"),
1377
- join2(basePath, "index.js"),
1378
- join2(basePath, "index.jsx")
2220
+ join3(basePath, "index.ts"),
2221
+ join3(basePath, "index.tsx"),
2222
+ join3(basePath, "index.js"),
2223
+ join3(basePath, "index.jsx")
1379
2224
  ];
1380
2225
  for (const candidate of candidates) {
1381
- if (existsSync2(candidate) && isAuditableSourceFile(candidate)) {
1382
- return relative(projectRoot, candidate) || candidate;
2226
+ if (existsSync3(candidate) && isAuditableSourceFile(candidate)) {
2227
+ return relative2(projectRoot, candidate) || candidate;
1383
2228
  }
1384
2229
  }
1385
2230
  return null;
@@ -1428,8 +2273,8 @@ function auditProjectSourceTree(projectRoot) {
1428
2273
  const sourceFiles = collectProjectSourceFiles(projectRoot);
1429
2274
  const sourceEntries = sourceFiles.map((sourceFile) => ({
1430
2275
  absolutePath: sourceFile,
1431
- relativePath: relative(projectRoot, sourceFile) || sourceFile,
1432
- code: readFileSync2(sourceFile, "utf-8")
2276
+ relativePath: relative2(projectRoot, sourceFile) || sourceFile,
2277
+ code: readFileSync3(sourceFile, "utf-8")
1433
2278
  })).filter((entry) => !isNonProductionSourceAuditFile(entry.relativePath));
1434
2279
  const clientReachableFiles = collectClientReachableSourceFiles(projectRoot, sourceEntries);
1435
2280
  const summary = {
@@ -1742,8 +2587,8 @@ function auditProjectStyleContracts(projectRoot) {
1742
2587
  reducedMotionSignals: createSourceAuditBucket()
1743
2588
  };
1744
2589
  for (const styleFile of styleFiles) {
1745
- const relativePath = relative(projectRoot, styleFile) || styleFile;
1746
- const css = readFileSync2(styleFile, "utf-8");
2590
+ const relativePath = relative2(projectRoot, styleFile) || styleFile;
2591
+ const css = readFileSync3(styleFile, "utf-8");
1747
2592
  recordSourceAudit(summary.focusVisibleSignals, relativePath, countFocusVisibleSignals(css));
1748
2593
  recordSourceAudit(summary.reducedMotionSignals, relativePath, countReducedMotionSignals(css));
1749
2594
  }
@@ -1752,15 +2597,15 @@ function auditProjectStyleContracts(projectRoot) {
1752
2597
  function buildRegistryContext(projectRoot) {
1753
2598
  const themeRegistry = /* @__PURE__ */ new Map();
1754
2599
  const patternRegistry = /* @__PURE__ */ new Map();
1755
- const cacheDir = join2(projectRoot, ".decantr", "cache");
1756
- const customDir = join2(projectRoot, ".decantr", "custom");
1757
- const cachedThemesDir = join2(cacheDir, "@official", "themes");
2600
+ const cacheDir = join3(projectRoot, ".decantr", "cache");
2601
+ const customDir = join3(projectRoot, ".decantr", "custom");
2602
+ const cachedThemesDir = join3(cacheDir, "@official", "themes");
1758
2603
  try {
1759
- if (existsSync2(cachedThemesDir)) {
1760
- for (const file of readdirSync2(cachedThemesDir).filter(
2604
+ if (existsSync3(cachedThemesDir)) {
2605
+ for (const file of readdirSync3(cachedThemesDir).filter(
1761
2606
  (name) => name.endsWith(".json") && name !== "index.json"
1762
2607
  )) {
1763
- const data = JSON.parse(readFileSync2(join2(cachedThemesDir, file), "utf-8"));
2608
+ const data = JSON.parse(readFileSync3(join3(cachedThemesDir, file), "utf-8"));
1764
2609
  if (data.id && !themeRegistry.has(data.id)) {
1765
2610
  themeRegistry.set(data.id, { modes: data.modes || ["light", "dark"] });
1766
2611
  }
@@ -1768,11 +2613,11 @@ function buildRegistryContext(projectRoot) {
1768
2613
  }
1769
2614
  } catch {
1770
2615
  }
1771
- const customThemesDir = join2(customDir, "themes");
2616
+ const customThemesDir = join3(customDir, "themes");
1772
2617
  try {
1773
- if (existsSync2(customThemesDir)) {
1774
- for (const file of readdirSync2(customThemesDir).filter((name) => name.endsWith(".json"))) {
1775
- const data = JSON.parse(readFileSync2(join2(customThemesDir, file), "utf-8"));
2618
+ if (existsSync3(customThemesDir)) {
2619
+ for (const file of readdirSync3(customThemesDir).filter((name) => name.endsWith(".json"))) {
2620
+ const data = JSON.parse(readFileSync3(join3(customThemesDir, file), "utf-8"));
1776
2621
  if (data.id) {
1777
2622
  themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
1778
2623
  }
@@ -1780,13 +2625,13 @@ function buildRegistryContext(projectRoot) {
1780
2625
  }
1781
2626
  } catch {
1782
2627
  }
1783
- const cachedPatternsDir = join2(cacheDir, "@official", "patterns");
2628
+ const cachedPatternsDir = join3(cacheDir, "@official", "patterns");
1784
2629
  try {
1785
- if (existsSync2(cachedPatternsDir)) {
1786
- for (const file of readdirSync2(cachedPatternsDir).filter(
2630
+ if (existsSync3(cachedPatternsDir)) {
2631
+ for (const file of readdirSync3(cachedPatternsDir).filter(
1787
2632
  (name) => name.endsWith(".json") && name !== "index.json"
1788
2633
  )) {
1789
- const data = JSON.parse(readFileSync2(join2(cachedPatternsDir, file), "utf-8"));
2634
+ const data = JSON.parse(readFileSync3(join3(cachedPatternsDir, file), "utf-8"));
1790
2635
  if (data.id && !patternRegistry.has(data.id)) {
1791
2636
  patternRegistry.set(data.id, data);
1792
2637
  }
@@ -2100,8 +2945,8 @@ function extractFailedRouteDocumentHardening(runtimeAudit) {
2100
2945
  return runtimeAudit.failures.filter((failure) => failure.startsWith("route-document-hardening-failed:")).map((failure) => normalizeRouteHint2(failure.slice("route-document-hardening-failed:".length))).filter(Boolean);
2101
2946
  }
2102
2947
  function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topology, sourceAudit) {
2103
- const distPath = join2(projectRoot, "dist");
2104
- const indexPath = join2(distPath, "index.html");
2948
+ const distPath = join3(projectRoot, "dist");
2949
+ const indexPath = join3(distPath, "index.html");
2105
2950
  const isFrameworkBuildOutput = runtimeAudit.failures.includes("next-build-output");
2106
2951
  if (!runtimeAudit.distPresent) {
2107
2952
  findings.push(
@@ -2648,20 +3493,20 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack, a
2648
3493
  if (sourceAudit.filesChecked === 0) {
2649
3494
  return;
2650
3495
  }
2651
- const isContractOnly = adoptionMode === "contract-only";
3496
+ const isProjectOwnedStyling = adoptionMode === "contract-only" || adoptionMode === "style-bridge";
2652
3497
  if (sourceAudit.inlineStyles.count > 0) {
2653
3498
  findings.push(
2654
3499
  makeFinding({
2655
3500
  id: "source-inline-styles-present",
2656
3501
  category: "Source Audit",
2657
3502
  severity: "warn",
2658
- message: isContractOnly ? "Source files contain inline style attributes; contract-only projects should route static visual decisions through project-owned styling law." : "Source files still contain disallowed inline style attributes, which undermines the compiled treatment contract.",
3503
+ message: isProjectOwnedStyling ? "Source files contain inline style attributes; project-owned styling projects should route static visual decisions through local law or the style bridge." : "Source files still contain disallowed inline style attributes, which undermines the compiled treatment contract.",
2659
3504
  evidence: buildSourceAuditEvidence(
2660
3505
  sourceAudit,
2661
3506
  sourceAudit.inlineStyles,
2662
3507
  "Disallowed inline style attributes"
2663
3508
  ),
2664
- suggestedFix: isContractOnly ? "Move static visual styling into the app design system, Tailwind/theme tokens, component variants, or accepted local rules. Keep inline style only for truly dynamic geometry." : "Move static visual styling into treatments, atoms, or design-token-backed classes. Inline style remains acceptable for Decantr CSS-variable writes and truly dynamic geometry."
3509
+ suggestedFix: isProjectOwnedStyling ? "Move static visual styling into the app design system, Tailwind/theme tokens, component variants, or accepted local rules. Keep inline style only for truly dynamic geometry." : "Move static visual styling into treatments, atoms, or design-token-backed classes. Inline style remains acceptable for Decantr CSS-variable writes and truly dynamic geometry."
2665
3510
  })
2666
3511
  );
2667
3512
  }
@@ -2671,17 +3516,17 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack, a
2671
3516
  id: "source-component-style-tags-present",
2672
3517
  category: "Source Audit",
2673
3518
  severity: "warn",
2674
- message: isContractOnly ? "Source files inject component-scoped style tags or dynamic style elements, which makes project-owned styling rules harder to enforce." : "Source files inject component-scoped style tags or dynamic style elements, which bypass the compiled Decantr layer contract.",
3519
+ message: isProjectOwnedStyling ? "Source files inject component-scoped style tags or dynamic style elements, which makes project-owned styling rules harder to enforce." : "Source files inject component-scoped style tags or dynamic style elements, which bypass the compiled Decantr layer contract.",
2675
3520
  evidence: buildSourceAuditEvidence(
2676
3521
  sourceAudit,
2677
3522
  sourceAudit.componentStyleTags,
2678
3523
  "Component-level style tag signals"
2679
3524
  ),
2680
- suggestedFix: isContractOnly ? "Move shared keyframes, media queries, and visual rules into the project stylesheet, component library, or accepted local pattern/rule manifest." : "Move shared keyframes, media queries, and visual rules into global.css or treatments.css so styling stays inside the reviewed Decantr layer stack."
3525
+ suggestedFix: isProjectOwnedStyling ? "Move shared keyframes, media queries, and visual rules into the project stylesheet, component library, or accepted local pattern/rule manifest." : "Move shared keyframes, media queries, and visual rules into global.css or treatments.css so styling stays inside the reviewed Decantr layer stack."
2681
3526
  })
2682
3527
  );
2683
3528
  }
2684
- if (!isContractOnly && sourceAudit.localCssRuntimeSignals.count > 0) {
3529
+ if (!isProjectOwnedStyling && sourceAudit.localCssRuntimeSignals.count > 0) {
2685
3530
  findings.push(
2686
3531
  makeFinding({
2687
3532
  id: "source-local-css-runtime-stub-present",
@@ -3780,15 +4625,15 @@ function appendStyleContractFindings(findings, styleAudit, essence) {
3780
4625
  }
3781
4626
  }
3782
4627
  async function auditProject(projectRoot) {
3783
- const essencePath = join2(projectRoot, "decantr.essence.json");
4628
+ const essencePath = join3(projectRoot, "decantr.essence.json");
3784
4629
  const findings = [];
3785
4630
  const reviewPack = loadReviewPack(projectRoot);
3786
4631
  const packManifest = loadPackManifest(projectRoot);
3787
4632
  const adoptionMode = readProjectAdoptionMode(projectRoot);
3788
- const packHydrationOptional = adoptionMode === "contract-only";
4633
+ const packHydrationOptional = adoptionMode === "contract-only" || adoptionMode === "style-bridge";
3789
4634
  const packHydrationSeverity = packHydrationOptional ? "info" : "warn";
3790
4635
  const runtimeAudit = emptyRuntimeAudit();
3791
- if (!existsSync2(essencePath)) {
4636
+ if (!existsSync3(essencePath)) {
3792
4637
  findings.push(
3793
4638
  makeFinding({
3794
4639
  id: "essence-missing",
@@ -3823,7 +4668,7 @@ async function auditProject(projectRoot) {
3823
4668
  }
3824
4669
  let essence = null;
3825
4670
  try {
3826
- essence = JSON.parse(readFileSync2(essencePath, "utf-8"));
4671
+ essence = JSON.parse(readFileSync3(essencePath, "utf-8"));
3827
4672
  } catch (error) {
3828
4673
  findings.push(
3829
4674
  makeFinding({
@@ -3865,8 +4710,8 @@ async function auditProject(projectRoot) {
3865
4710
  id: "pack-manifest-missing",
3866
4711
  category: "Execution Packs",
3867
4712
  severity: packHydrationSeverity,
3868
- message: packHydrationOptional ? "Compiled execution pack manifest is not hydrated yet; this is optional for contract-only Brownfield adoption." : "Compiled execution pack manifest is missing.",
3869
- evidence: [join2(projectRoot, ".decantr", "context", "pack-manifest.json")],
4713
+ 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.",
4714
+ evidence: [join3(projectRoot, ".decantr", "context", "pack-manifest.json")],
3870
4715
  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."
3871
4716
  })
3872
4717
  );
@@ -3929,8 +4774,8 @@ async function auditProject(projectRoot) {
3929
4774
  id: "review-pack-file-missing",
3930
4775
  category: "Review Contract",
3931
4776
  severity: packHydrationSeverity,
3932
- message: packHydrationOptional ? "The compiled review pack file is not hydrated yet; contract-only Brownfield projects can continue without it." : "The compiled review pack file is missing.",
3933
- evidence: [join2(projectRoot, ".decantr", "context", "review-pack.json")],
4777
+ 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.",
4778
+ evidence: [join3(projectRoot, ".decantr", "context", "review-pack.json")],
3934
4779
  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."
3935
4780
  })
3936
4781
  );
@@ -4071,7 +4916,7 @@ function countKeyboardShortcutSignals(code) {
4071
4916
  return matches.length;
4072
4917
  }
4073
4918
  function getScriptKind(filePath) {
4074
- switch (extname2(filePath).toLowerCase()) {
4919
+ switch (extname3(filePath).toLowerCase()) {
4075
4920
  case ".tsx":
4076
4921
  return ts.ScriptKind.TSX;
4077
4922
  case ".jsx":
@@ -10358,16 +11203,16 @@ function critiqueSource({
10358
11203
  const findings = [];
10359
11204
  const scores = [];
10360
11205
  const antiPatternIds = new Set(reviewPack?.antiPatterns.map((entry) => entry.id) ?? []);
10361
- const isContractOnly = adoptionMode === "contract-only";
11206
+ const isProjectOwnedStyling = adoptionMode === "contract-only" || adoptionMode === "style-bridge";
10362
11207
  const usedTreatments = TREATMENT_CLASSES.filter((token) => code.includes(token));
10363
- if (isContractOnly) {
11208
+ if (isProjectOwnedStyling) {
10364
11209
  scores.push({
10365
11210
  category: "Styling Authority",
10366
11211
  focusArea: "treatment-usage",
10367
11212
  score: 3,
10368
- details: "Contract-only adoption does not require Decantr d-* treatment classes in source files.",
11213
+ details: "Contract-only/style-bridge adoption does not require Decantr d-* treatment classes in source files.",
10369
11214
  suggestions: [
10370
- "Codify project-owned component variants and local rules when button/card/surface drift needs mechanical enforcement."
11215
+ "Codify project-owned component variants, local rules, or a style bridge when button/card/surface drift needs stronger enforcement."
10371
11216
  ]
10372
11217
  });
10373
11218
  } else {
@@ -10404,12 +11249,12 @@ function critiqueSource({
10404
11249
  const decoratorNames = buildDecoratorInventory(treatmentsCss);
10405
11250
  const usedDecorators = decoratorNames.filter((name) => code.includes(name));
10406
11251
  const usesCssVars = code.includes("var(--");
10407
- if (isContractOnly) {
11252
+ if (isProjectOwnedStyling) {
10408
11253
  scores.push({
10409
11254
  category: "Theme Consistency",
10410
11255
  focusArea: "theme-consistency",
10411
11256
  score: usesCssVars ? 4 : 3,
10412
- details: `Contract-only mode: Decantr decorators are not required; CSS vars: ${usesCssVars ? "yes" : "no"}.`,
11257
+ details: `Contract-only/style-bridge mode: Decantr decorators are not required; CSS vars: ${usesCssVars ? "yes" : "no"}.`,
10413
11258
  suggestions: [
10414
11259
  "Use the app design system, Tailwind theme, Sass variables, component variants, or accepted local rules as the styling authority."
10415
11260
  ]
@@ -11436,7 +12281,7 @@ function critiqueSource({
11436
12281
  `Disallowed inline style attributes: ${astSignals.inlineStyleAttributeCount}`
11437
12282
  ],
11438
12283
  file: filePath,
11439
- suggestedFix: isContractOnly ? "Replace static inline visual values with the project design system, accepted component variants, or local style rules. Keep inline style only for truly dynamic geometry." : "Replace static inline visual values with treatments, decorators, and CSS variables from the compiled contract. Keep inline style only for Decantr CSS-variable writes and truly dynamic geometry."
12284
+ suggestedFix: isProjectOwnedStyling ? "Replace static inline visual values with the project design system, accepted component variants, or local style rules. Keep inline style only for truly dynamic geometry." : "Replace static inline visual values with treatments, decorators, and CSS variables from the compiled contract. Keep inline style only for Decantr CSS-variable writes and truly dynamic geometry."
11440
12285
  })
11441
12286
  );
11442
12287
  }
@@ -11453,7 +12298,7 @@ function critiqueSource({
11453
12298
  message: "Component-scoped style tags or dynamic style elements were detected in the reviewed file.",
11454
12299
  evidence: [filePath, `Component style tag signals: ${componentStyleTagSignals}`],
11455
12300
  file: filePath,
11456
- suggestedFix: isContractOnly ? "Move shared keyframes, media queries, and visual rules into the project stylesheet, component library, or accepted local pattern/rule manifest." : "Move shared keyframes, media queries, and visual rules into global.css or treatments.css so the file stays aligned with the Decantr layer contract."
12301
+ suggestedFix: isProjectOwnedStyling ? "Move shared keyframes, media queries, and visual rules into the project stylesheet, component library, or accepted local pattern/rule manifest." : "Move shared keyframes, media queries, and visual rules into global.css or treatments.css so the file stays aligned with the Decantr layer contract."
11457
12302
  })
11458
12303
  );
11459
12304
  }
@@ -12091,11 +12936,11 @@ function critiqueSource({
12091
12936
  };
12092
12937
  }
12093
12938
  function resolveProjectFilePath(projectRoot, filePath) {
12094
- const root = existsSync2(projectRoot) ? realpathSync.native(projectRoot) : resolve(projectRoot);
12095
- const candidatePath = isAbsolute(filePath) ? resolve(filePath) : resolve(root, filePath);
12096
- const resolvedPath = existsSync2(candidatePath) ? realpathSync.native(candidatePath) : candidatePath;
12097
- const relativePath = relative(root, resolvedPath);
12098
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
12939
+ const root = existsSync3(projectRoot) ? realpathSync.native(projectRoot) : resolve(projectRoot);
12940
+ const candidatePath = isAbsolute2(filePath) ? resolve(filePath) : resolve(root, filePath);
12941
+ const resolvedPath = existsSync3(candidatePath) ? realpathSync.native(candidatePath) : candidatePath;
12942
+ const relativePath = relative2(root, resolvedPath);
12943
+ if (relativePath.startsWith("..") || isAbsolute2(relativePath)) {
12099
12944
  throw new Error(`Path escapes the project root: ${filePath}`);
12100
12945
  }
12101
12946
  return resolvedPath;
@@ -12103,7 +12948,7 @@ function resolveProjectFilePath(projectRoot, filePath) {
12103
12948
  async function critiqueFile(filePath, projectRoot) {
12104
12949
  const resolvedPath = resolveProjectFilePath(projectRoot, filePath);
12105
12950
  const code = await readFile(resolvedPath, "utf-8");
12106
- const treatmentsCss = readTextIfExists(join2(projectRoot, "src", "styles", "treatments.css"));
12951
+ const treatmentsCss = readTextIfExists(join3(projectRoot, "src", "styles", "treatments.css"));
12107
12952
  const reviewPack = loadReviewPack(projectRoot);
12108
12953
  const packManifest = loadPackManifest(projectRoot);
12109
12954
  const adoptionMode = readProjectAdoptionMode(projectRoot);
@@ -12119,17 +12964,22 @@ async function critiqueFile(filePath, projectRoot) {
12119
12964
  export {
12120
12965
  EVIDENCE_BUNDLE_SCHEMA_URL,
12121
12966
  INTERACTION_SIGNALS,
12967
+ SCAN_REPORT_SCHEMA_URL,
12122
12968
  VERIFICATION_SCHEMA_URLS,
12123
12969
  auditBuiltDist,
12124
12970
  auditProject,
12125
12971
  collectMissingPackManifestFiles,
12126
12972
  createContractAssertions,
12127
12973
  createEvidenceBundle,
12974
+ createUnavailableScanReport,
12128
12975
  critiqueFile,
12129
12976
  critiqueSource,
12130
12977
  emptyRuntimeAudit,
12131
12978
  extractRouteHintsFromEssence,
12132
12979
  listKnownInteractions,
12980
+ probePublishedSite,
12981
+ resolveGitHubScanInput,
12982
+ scanProject,
12133
12983
  verifyInteractionsInSource
12134
12984
  };
12135
12985
  //# sourceMappingURL=index.js.map