@decantr/verifier 1.1.0 → 2.0.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
@@ -51,7 +51,7 @@ function isBlocking(report: ProjectHealthReport) {
51
51
 
52
52
  ## Compatibility
53
53
 
54
- `@decantr/verifier` is stable in the `1.x` line for the documented verifier APIs and published report-schema exports.
54
+ `@decantr/verifier` is stable in the `2.x` line for the documented verifier APIs and published report-schema exports.
55
55
 
56
56
  - new checks and additive report fields may appear in compatible releases
57
57
  - breaking report-shape or exported API changes require a major version
package/dist/index.d.ts CHANGED
@@ -54,7 +54,7 @@ declare function emptyRuntimeAudit(failures?: string[]): RuntimeAudit;
54
54
  declare function auditBuiltDist(projectRoot: string, options?: BuiltDistAuditOptions): Promise<RuntimeAudit>;
55
55
 
56
56
  /**
57
- * v2.1 Tier C4 — Experiential interaction verifier.
57
+ * Experiential interaction verifier.
58
58
  *
59
59
  * Scans generated source files for evidence that declared `interactions[]`
60
60
  * (from pattern.v2.json) are actually implemented. This is the enforcement
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // src/index.ts
2
- import { existsSync as existsSync2, realpathSync, readdirSync, readFileSync as readFileSync2 } from "fs";
2
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync } from "fs";
3
3
  import { readFile } from "fs/promises";
4
- import { extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
5
- import { evaluateGuard, isV3, validateEssence } from "@decantr/essence-spec";
4
+ import { dirname, extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
5
+ import { evaluateGuard, isV4, validateEssence } from "@decantr/essence-spec";
6
6
  import * as ts from "typescript";
7
7
 
8
8
  // src/runtime.ts
9
- import { existsSync, readFileSync } from "fs";
9
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
10
10
  import { createServer } from "http";
11
11
  import { extname, join, normalize } from "path";
12
12
  var CONTENT_TYPES = {
@@ -224,6 +224,122 @@ function countSecretLeakSignals(js) {
224
224
  ];
225
225
  return patterns.reduce((count, pattern) => count + (js.match(pattern)?.length ?? 0), 0);
226
226
  }
227
+ function collectFiles(rootDir, predicate) {
228
+ if (!existsSync(rootDir)) return [];
229
+ const files = [];
230
+ const walk = (dir) => {
231
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
232
+ const path = join(dir, entry.name);
233
+ if (entry.isDirectory()) {
234
+ walk(path);
235
+ continue;
236
+ }
237
+ if (entry.isFile() && predicate(path)) {
238
+ files.push(path);
239
+ }
240
+ }
241
+ };
242
+ walk(rootDir);
243
+ return files.sort();
244
+ }
245
+ function auditNextBuildOutput(projectRoot) {
246
+ const nextDir = join(projectRoot, ".next");
247
+ const buildIdPresent = existsSync(join(nextDir, "BUILD_ID"));
248
+ const buildManifestPresent = existsSync(join(nextDir, "build-manifest.json"));
249
+ const appServerDir = join(nextDir, "server", "app");
250
+ const htmlFiles = collectFiles(appServerDir, (filePath) => {
251
+ const relativePath = filePath.slice(appServerDir.length + 1).replace(/\\/g, "/");
252
+ return filePath.endsWith(".html") && !relativePath.startsWith("_");
253
+ });
254
+ const htmlDocuments = htmlFiles.map((filePath) => readFileSync(filePath, "utf-8"));
255
+ const staticAssetFiles = collectFiles(
256
+ join(nextDir, "static"),
257
+ (filePath) => /\.(?:js|css|json|svg|png|jpe?g|webp)$/i.test(filePath)
258
+ );
259
+ let totalAssetBytes = 0;
260
+ let jsAssetBytes = 0;
261
+ let cssAssetBytes = 0;
262
+ let largestAssetPath = null;
263
+ let largestAssetBytes = 0;
264
+ let combinedJs = "";
265
+ for (const assetFile of staticAssetFiles) {
266
+ const byteLength = statSync(assetFile).size;
267
+ const assetPath = `/_next/static/${assetFile.slice(join(nextDir, "static").length + 1).replace(/\\/g, "/")}`;
268
+ totalAssetBytes += byteLength;
269
+ if (assetFile.endsWith(".js")) {
270
+ jsAssetBytes += byteLength;
271
+ combinedJs += readFileSync(assetFile, "utf-8");
272
+ }
273
+ if (assetFile.endsWith(".css")) {
274
+ cssAssetBytes += byteLength;
275
+ }
276
+ if (byteLength > largestAssetBytes) {
277
+ largestAssetBytes = byteLength;
278
+ largestAssetPath = assetPath;
279
+ }
280
+ }
281
+ const hasHtmlDocuments = htmlDocuments.length > 0;
282
+ const titleOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<title>[^<]+<\/title>/i.test(html));
283
+ const langOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<html[^>]*\slang=(["'])[^"']+\1/i.test(html));
284
+ const viewportOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(html));
285
+ const charsetOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<meta[^>]+charset=/i.test(html));
286
+ const failures = ["next-build-output"];
287
+ if (!buildIdPresent) {
288
+ failures.push("next-build-id-missing");
289
+ }
290
+ if (!buildManifestPresent) {
291
+ failures.push("next-build-manifest-missing");
292
+ }
293
+ if (staticAssetFiles.length === 0) {
294
+ failures.push("next-assets-missing");
295
+ }
296
+ const passed = buildIdPresent && buildManifestPresent && staticAssetFiles.length > 0;
297
+ return {
298
+ distPresent: true,
299
+ indexPresent: true,
300
+ checked: true,
301
+ passed,
302
+ rootDocumentOk: hasHtmlDocuments || passed,
303
+ titleOk,
304
+ langOk,
305
+ viewportOk,
306
+ charsetOk,
307
+ cspSignalOk: true,
308
+ inlineScriptCount: 0,
309
+ inlineEventHandlerCount: 0,
310
+ externalScriptsWithoutIntegrityCount: 0,
311
+ externalScriptsWithIntegrityMissingCrossoriginCount: 0,
312
+ externalStylesheetsWithoutIntegrityCount: 0,
313
+ externalStylesheetsWithIntegrityMissingCrossoriginCount: 0,
314
+ externalScriptsWithInsecureTransportCount: 0,
315
+ externalStylesheetsWithInsecureTransportCount: 0,
316
+ externalMediaSourcesWithInsecureTransportCount: 0,
317
+ externalBlankLinksWithoutRelCount: 0,
318
+ externalIframesWithoutSandboxCount: 0,
319
+ externalIframesWithInsecureTransportCount: 0,
320
+ jsEvalSignalCount: countDynamicCodeSignals(combinedJs),
321
+ jsHtmlInjectionSignalCount: countHtmlInjectionSignals(combinedJs),
322
+ jsInsecureTransportSignalCount: countInsecureTransportSignals(combinedJs),
323
+ jsSecretSignalCount: countSecretLeakSignals(combinedJs),
324
+ assetCount: staticAssetFiles.length,
325
+ assetsPassed: staticAssetFiles.length,
326
+ routeHintsChecked: [],
327
+ routeHintsMatched: 0,
328
+ routeHintsCoverageOk: true,
329
+ routeDocumentsChecked: 0,
330
+ routeDocumentsPassed: 0,
331
+ routeDocumentsHardenedCount: 0,
332
+ routeDocumentsCoverageOk: true,
333
+ routeDocumentsHardeningOk: true,
334
+ fullRouteCoverageOk: true,
335
+ totalAssetBytes,
336
+ jsAssetBytes,
337
+ cssAssetBytes,
338
+ largestAssetPath,
339
+ largestAssetBytes,
340
+ failures
341
+ };
342
+ }
227
343
  function normalizeRouteHint(route) {
228
344
  if (!route || route === "/") return "/";
229
345
  const dynamicIndex = route.indexOf("/:");
@@ -280,6 +396,9 @@ async function startStaticServer(rootDir) {
280
396
  async function auditBuiltDist(projectRoot, options = {}) {
281
397
  const distDir = options.distDir ?? join(projectRoot, "dist");
282
398
  if (!existsSync(distDir)) {
399
+ if (!options.distDir && existsSync(join(projectRoot, ".next"))) {
400
+ return auditNextBuildOutput(projectRoot);
401
+ }
283
402
  return emptyRuntimeAudit(["dist-missing"]);
284
403
  }
285
404
  const indexPath = join(distDir, "index.html");
@@ -686,6 +805,54 @@ function recordSourceAudit(bucket, filePath, count) {
686
805
  function sourceAuditBucketsOverlap(a, b) {
687
806
  return a.files.some((file) => b.files.includes(file));
688
807
  }
808
+ function normalizeSourceAuditPath(filePath) {
809
+ return filePath.replace(/\\/g, "/");
810
+ }
811
+ function getNextAppLayoutGuardRoot(filePath) {
812
+ const normalized = normalizeSourceAuditPath(filePath);
813
+ const match = normalized.match(
814
+ /^(?:src\/)?app\/(?:\([^/]+\)\/)*([^/.()[\]]+)\/layout\.[cm]?[jt]sx?$/i
815
+ );
816
+ return match?.[1] ?? null;
817
+ }
818
+ function getNextAppRouteRoot(filePath) {
819
+ const normalized = normalizeSourceAuditPath(filePath);
820
+ const match = normalized.match(/^(?:src\/)?app\/(?:\([^/]+\)\/)*([^/.()[\]]+)(?:\/|$)/i);
821
+ return match?.[1] ?? null;
822
+ }
823
+ function getRouteRoot(route) {
824
+ const firstSegment = route.replace(/^\/+/, "").split("/")[0];
825
+ if (!firstSegment || firstSegment.startsWith(":") || firstSegment.startsWith("[")) {
826
+ return null;
827
+ }
828
+ return firstSegment;
829
+ }
830
+ function isNextAppPublicChromeFile(filePath) {
831
+ const normalized = normalizeSourceAuditPath(filePath);
832
+ return /^(?:src\/)?app\/(?:\([^/]+\)\/)*(?:layout|nav|nav-header|navigation|header|footer|sidebar)\.[cm]?[jt]sx?$/i.test(
833
+ normalized
834
+ );
835
+ }
836
+ function sourceAuditProtectedSurfacesCoveredByGuardedNextLayouts(protectedSurfaces, authGuards, topology) {
837
+ const guardedRoots = /* @__PURE__ */ new Set();
838
+ for (const file of authGuards.files) {
839
+ const root = getNextAppLayoutGuardRoot(file);
840
+ if (root) {
841
+ guardedRoots.add(root);
842
+ }
843
+ }
844
+ if (guardedRoots.size === 0 || protectedSurfaces.files.length === 0) {
845
+ return false;
846
+ }
847
+ const guardedPrimaryRouteRoots = topology.primaryRoutes.map(getRouteRoot).filter((root) => Boolean(root)).filter((root) => guardedRoots.has(root));
848
+ return protectedSurfaces.files.every((file) => {
849
+ const appRoot = getNextAppRouteRoot(file);
850
+ if (appRoot && guardedRoots.has(appRoot)) {
851
+ return true;
852
+ }
853
+ return isNextAppPublicChromeFile(file) && guardedPrimaryRouteRoots.length > 0;
854
+ });
855
+ }
689
856
  function isAuditableSourceFile(filePath) {
690
857
  if (/\.d\.ts$/i.test(filePath)) return false;
691
858
  return /\.(?:[cm]?[jt]sx?)$/i.test(filePath);
@@ -726,7 +893,7 @@ function collectProjectSourceFiles(projectRoot) {
726
893
  "coverage"
727
894
  ]);
728
895
  const walk = (dir) => {
729
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
896
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
730
897
  if (ignoredDirNames.has(entry.name)) continue;
731
898
  const absolutePath = join2(dir, entry.name);
732
899
  if (entry.isDirectory()) {
@@ -770,7 +937,7 @@ function collectProjectStyleFiles(projectRoot) {
770
937
  "coverage"
771
938
  ]);
772
939
  const walk = (dir) => {
773
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
940
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
774
941
  if (ignoredDirNames.has(entry.name)) continue;
775
942
  const absolutePath = join2(dir, entry.name);
776
943
  if (entry.isDirectory()) {
@@ -799,8 +966,126 @@ function countReducedMotionSignals(code) {
799
966
  ];
800
967
  return patterns.reduce((count, pattern) => count + (pattern.test(code) ? 1 : 0), 0);
801
968
  }
969
+ function hasModuleDirective(filePath, code, directive) {
970
+ const sourceFile = ts.createSourceFile(
971
+ filePath,
972
+ code,
973
+ ts.ScriptTarget.Latest,
974
+ true,
975
+ getScriptKind(filePath)
976
+ );
977
+ for (const statement of sourceFile.statements) {
978
+ if (ts.isExpressionStatement(statement) && ts.isStringLiteralLike(statement.expression)) {
979
+ if (statement.expression.text === directive) {
980
+ return true;
981
+ }
982
+ continue;
983
+ }
984
+ break;
985
+ }
986
+ return false;
987
+ }
988
+ function importDeclarationHasRuntimeBinding(statement) {
989
+ const clause = statement.importClause;
990
+ if (!clause) return true;
991
+ if (clause.isTypeOnly) return false;
992
+ if (clause.name) return true;
993
+ if (!clause.namedBindings) return true;
994
+ if (ts.isNamespaceImport(clause.namedBindings)) return true;
995
+ return clause.namedBindings.elements.some((element) => !element.isTypeOnly);
996
+ }
997
+ function collectRuntimeImportSpecifiers(entry) {
998
+ const sourceFile = ts.createSourceFile(
999
+ entry.relativePath,
1000
+ entry.code,
1001
+ ts.ScriptTarget.Latest,
1002
+ true,
1003
+ getScriptKind(entry.relativePath)
1004
+ );
1005
+ const specifiers = [];
1006
+ for (const statement of sourceFile.statements) {
1007
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) && importDeclarationHasRuntimeBinding(statement)) {
1008
+ specifiers.push(statement.moduleSpecifier.text);
1009
+ }
1010
+ }
1011
+ return specifiers;
1012
+ }
1013
+ function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
1014
+ let basePath = null;
1015
+ if (specifier.startsWith("@/")) {
1016
+ basePath = join2(projectRoot, "src", specifier.slice(2));
1017
+ } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
1018
+ basePath = resolve(dirname(sourceAbsolutePath), specifier);
1019
+ }
1020
+ if (!basePath) return null;
1021
+ const candidates = [
1022
+ basePath,
1023
+ `${basePath}.ts`,
1024
+ `${basePath}.tsx`,
1025
+ `${basePath}.js`,
1026
+ `${basePath}.jsx`,
1027
+ `${basePath}.mts`,
1028
+ `${basePath}.cts`,
1029
+ join2(basePath, "index.ts"),
1030
+ join2(basePath, "index.tsx"),
1031
+ join2(basePath, "index.js"),
1032
+ join2(basePath, "index.jsx")
1033
+ ];
1034
+ for (const candidate of candidates) {
1035
+ if (existsSync2(candidate) && isAuditableSourceFile(candidate)) {
1036
+ return relative(projectRoot, candidate) || candidate;
1037
+ }
1038
+ }
1039
+ return null;
1040
+ }
1041
+ function collectClientReachableSourceFiles(projectRoot, entries) {
1042
+ const entriesByRelativePath = new Map(entries.map((entry) => [entry.relativePath, entry]));
1043
+ const runtimeImportGraph = /* @__PURE__ */ new Map();
1044
+ for (const entry of entries) {
1045
+ const targets = collectRuntimeImportSpecifiers(entry).map((specifier) => resolveSourceImportTarget(projectRoot, entry.absolutePath, specifier)).filter((target) => Boolean(target));
1046
+ runtimeImportGraph.set(entry.relativePath, targets);
1047
+ }
1048
+ const reachable = /* @__PURE__ */ new Set();
1049
+ const queue = entries.filter((entry) => hasModuleDirective(entry.relativePath, entry.code, "use client")).map((entry) => entry.relativePath);
1050
+ for (const root of queue) {
1051
+ reachable.add(root);
1052
+ }
1053
+ while (queue.length > 0) {
1054
+ const current = queue.shift();
1055
+ for (const target of runtimeImportGraph.get(current) ?? []) {
1056
+ const targetEntry = entriesByRelativePath.get(target);
1057
+ if (targetEntry && hasModuleDirective(target, targetEntry.code, "use server")) {
1058
+ continue;
1059
+ }
1060
+ if (!reachable.has(target)) {
1061
+ reachable.add(target);
1062
+ queue.push(target);
1063
+ }
1064
+ }
1065
+ }
1066
+ return reachable;
1067
+ }
1068
+ function isClientAuthHeaderSource(relativePath, code, clientReachableFiles) {
1069
+ if (hasModuleDirective(relativePath, code, "use server")) {
1070
+ return false;
1071
+ }
1072
+ if (clientReachableFiles.has(relativePath)) {
1073
+ return true;
1074
+ }
1075
+ const normalized = normalizeSourceAuditPath(relativePath);
1076
+ if (/^(?:src\/)?app\//i.test(normalized)) {
1077
+ return false;
1078
+ }
1079
+ return /(?:^|\/)(?:components|routes|pages|hooks|providers)\//i.test(normalized);
1080
+ }
802
1081
  function auditProjectSourceTree(projectRoot) {
803
1082
  const sourceFiles = collectProjectSourceFiles(projectRoot);
1083
+ const sourceEntries = sourceFiles.map((sourceFile) => ({
1084
+ absolutePath: sourceFile,
1085
+ relativePath: relative(projectRoot, sourceFile) || sourceFile,
1086
+ code: readFileSync2(sourceFile, "utf-8")
1087
+ }));
1088
+ const clientReachableFiles = collectClientReachableSourceFiles(projectRoot, sourceEntries);
804
1089
  const summary = {
805
1090
  filesChecked: sourceFiles.length,
806
1091
  inlineStyles: createSourceAuditBucket(),
@@ -869,10 +1154,13 @@ function auditProjectSourceTree(projectRoot) {
869
1154
  interactionSafetyIssues: createSourceAuditBucket(),
870
1155
  authInputHintIssues: createSourceAuditBucket()
871
1156
  };
872
- for (const sourceFile of sourceFiles) {
873
- const relativePath = relative(projectRoot, sourceFile) || sourceFile;
874
- const code = readFileSync2(sourceFile, "utf-8");
1157
+ for (const { relativePath, code } of sourceEntries) {
875
1158
  const signals = analyzeAstSignals(relativePath, code);
1159
+ const isClientAuthHeaderSourceFile = isClientAuthHeaderSource(
1160
+ relativePath,
1161
+ code,
1162
+ clientReachableFiles
1163
+ );
876
1164
  const accessibilityIssueCount = signals.iconOnlyButtonWithoutLabelCount + signals.iconOnlyLinkWithoutLabelCount + signals.clickableNonSemanticCount + signals.unlabeledNavigationLandmarkCount + signals.imageWithoutAltCount + signals.iframeWithoutTitleCount + signals.dialogWithoutLabelCount + signals.dialogWithoutModalHintCount + signals.tableWithoutHeaderCount + signals.tableWithoutCaptionCount + signals.formControlWithoutLabelCount;
877
1165
  const securityRiskPatternCount = signals.dangerousHtmlCount + signals.rawHtmlInjectionCount + signals.dynamicEvalCount + signals.hardcodedSecretSignalCount + signals.clientSecretEnvReferenceCount + signals.localhostEndpointCount + signals.wildcardPostMessageCount + signals.windowOpenWithoutNoopenerCount + signals.externalIframeWithoutSandboxCount + signals.insecureExternalIframeCount + signals.insecureFormActionCount + signals.insecureAuthFormMethodCount + signals.insecureTransportEndpointCount + signals.insecureExternalImageCount + signals.authCookieMissingHardeningCount + signals.authOpenRedirectSignalCount + signals.authExternalRedirectSignalCount + signals.authProviderStateMissingCount + signals.authProviderPkceMissingCount + signals.authProviderNonceMissingCount + signals.externalBlankLinkWithoutRelCount;
878
1166
  const authInputHintIssueCount = signals.emailAutocompleteMissingCount + signals.passwordAutocompleteMissingCount + signals.otpAutocompleteMissingCount + signals.authAutocompleteDisabledCount + signals.authAutocompleteSemanticMismatchCount + signals.authInputTypeMismatchCount;
@@ -1042,8 +1330,16 @@ function auditProjectSourceTree(projectRoot) {
1042
1330
  recordSourceAudit(summary.authStorageClears, relativePath, signals.authStorageClearCount);
1043
1331
  recordSourceAudit(summary.authCookieWrites, relativePath, signals.authCookieWriteCount);
1044
1332
  recordSourceAudit(summary.authCookieClears, relativePath, signals.authCookieClearCount);
1045
- recordSourceAudit(summary.authHeaderWrites, relativePath, signals.authHeaderWriteCount);
1046
- recordSourceAudit(summary.authHeaderClears, relativePath, signals.authHeaderClearCount);
1333
+ recordSourceAudit(
1334
+ summary.authHeaderWrites,
1335
+ relativePath,
1336
+ isClientAuthHeaderSourceFile ? signals.authHeaderWriteCount : 0
1337
+ );
1338
+ recordSourceAudit(
1339
+ summary.authHeaderClears,
1340
+ relativePath,
1341
+ isClientAuthHeaderSourceFile ? signals.authHeaderClearCount : 0
1342
+ );
1047
1343
  recordSourceAudit(summary.authCacheClients, relativePath, signals.authCacheClientCount);
1048
1344
  recordSourceAudit(summary.authCacheClears, relativePath, signals.authCacheClearCount);
1049
1345
  recordSourceAudit(summary.authRefreshSignals, relativePath, signals.authRefreshSignalCount);
@@ -1115,7 +1411,7 @@ function buildRegistryContext(projectRoot) {
1115
1411
  const cachedThemesDir = join2(cacheDir, "@official", "themes");
1116
1412
  try {
1117
1413
  if (existsSync2(cachedThemesDir)) {
1118
- for (const file of readdirSync(cachedThemesDir).filter(
1414
+ for (const file of readdirSync2(cachedThemesDir).filter(
1119
1415
  (name) => name.endsWith(".json") && name !== "index.json"
1120
1416
  )) {
1121
1417
  const data = JSON.parse(readFileSync2(join2(cachedThemesDir, file), "utf-8"));
@@ -1129,7 +1425,7 @@ function buildRegistryContext(projectRoot) {
1129
1425
  const customThemesDir = join2(customDir, "themes");
1130
1426
  try {
1131
1427
  if (existsSync2(customThemesDir)) {
1132
- for (const file of readdirSync(customThemesDir).filter((name) => name.endsWith(".json"))) {
1428
+ for (const file of readdirSync2(customThemesDir).filter((name) => name.endsWith(".json"))) {
1133
1429
  const data = JSON.parse(readFileSync2(join2(customThemesDir, file), "utf-8"));
1134
1430
  if (data.id) {
1135
1431
  themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
@@ -1141,7 +1437,7 @@ function buildRegistryContext(projectRoot) {
1141
1437
  const cachedPatternsDir = join2(cacheDir, "@official", "patterns");
1142
1438
  try {
1143
1439
  if (existsSync2(cachedPatternsDir)) {
1144
- for (const file of readdirSync(cachedPatternsDir).filter(
1440
+ for (const file of readdirSync2(cachedPatternsDir).filter(
1145
1441
  (name) => name.endsWith(".json") && name !== "index.json"
1146
1442
  )) {
1147
1443
  const data = JSON.parse(readFileSync2(join2(cachedPatternsDir, file), "utf-8"));
@@ -1167,17 +1463,7 @@ function guardViolationToFinding(violation) {
1167
1463
  }
1168
1464
  function countPages(essence) {
1169
1465
  if (!essence) return 0;
1170
- if (isV3(essence)) {
1171
- const v3 = essence;
1172
- if (v3.blueprint.sections?.length) {
1173
- return v3.blueprint.sections.reduce((sum, section) => sum + section.pages.length, 0);
1174
- }
1175
- return v3.blueprint.pages?.length ?? 0;
1176
- }
1177
- if ("structure" in essence && Array.isArray(essence.structure)) {
1178
- return essence.structure.length;
1179
- }
1180
- return 0;
1466
+ return essence.blueprint.sections.reduce((sum, section) => sum + section.pages.length, 0);
1181
1467
  }
1182
1468
  function makeFinding(input) {
1183
1469
  return input;
@@ -1252,31 +1538,19 @@ function extractRouteHintsFromEssence(essence) {
1252
1538
  return ["/"];
1253
1539
  }
1254
1540
  const routes = /* @__PURE__ */ new Set(["/"]);
1255
- if (isV3(essence)) {
1256
- const v3 = essence;
1257
- for (const section of v3.blueprint.sections ?? []) {
1258
- for (const page of section.pages ?? []) {
1541
+ if (isV4(essence)) {
1542
+ for (const section of essence.blueprint.sections) {
1543
+ for (const page of section.pages) {
1259
1544
  if (typeof page.route === "string" && page.route.length > 0) {
1260
1545
  routes.add(normalizeRouteHint2(page.route));
1261
1546
  }
1262
1547
  }
1263
1548
  }
1264
- for (const page of v3.blueprint.pages ?? []) {
1265
- if (typeof page.route === "string" && page.route.length > 0) {
1266
- routes.add(normalizeRouteHint2(page.route));
1267
- }
1268
- }
1269
- if (v3.blueprint.routes && typeof v3.blueprint.routes === "object") {
1270
- for (const route of Object.keys(v3.blueprint.routes)) {
1549
+ if (essence.blueprint.routes && typeof essence.blueprint.routes === "object") {
1550
+ for (const route of Object.keys(essence.blueprint.routes)) {
1271
1551
  routes.add(normalizeRouteHint2(route));
1272
1552
  }
1273
1553
  }
1274
- } else if ("structure" in essence && Array.isArray(essence.structure)) {
1275
- for (const page of essence.structure) {
1276
- if (page && typeof page === "object" && "route" in page && typeof page.route === "string" && page.route.length > 0) {
1277
- routes.add(normalizeRouteHint2(page.route));
1278
- }
1279
- }
1280
1554
  }
1281
1555
  return [...routes].filter(Boolean).slice(0, 8);
1282
1556
  }
@@ -1288,14 +1562,13 @@ function summarizeTopology(essence, reviewPack) {
1288
1562
  let gatewayRouteCount = 0;
1289
1563
  let primaryRouteCount = 0;
1290
1564
  let hasAnonymousEntryRoute = false;
1291
- if (essence && isV3(essence)) {
1292
- const v3 = essence;
1293
- for (const feature of v3.blueprint.features ?? []) {
1565
+ if (essence && isV4(essence)) {
1566
+ for (const feature of essence.blueprint.features ?? []) {
1294
1567
  if (typeof feature === "string" && feature.length > 0) {
1295
1568
  features.add(feature);
1296
1569
  }
1297
1570
  }
1298
- for (const section of v3.blueprint.sections ?? []) {
1571
+ for (const section of essence.blueprint.sections ?? []) {
1299
1572
  const role = typeof section.role === "string" && section.role.length > 0 ? section.role : "unknown";
1300
1573
  sectionRoles.add(role);
1301
1574
  for (const page of section.pages ?? []) {
@@ -1472,6 +1745,7 @@ function extractFailedRouteDocumentHardening(runtimeAudit) {
1472
1745
  function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topology, sourceAudit) {
1473
1746
  const distPath = join2(projectRoot, "dist");
1474
1747
  const indexPath = join2(distPath, "index.html");
1748
+ const isFrameworkBuildOutput = runtimeAudit.failures.includes("next-build-output");
1475
1749
  if (!runtimeAudit.distPresent) {
1476
1750
  findings.push(
1477
1751
  makeFinding({
@@ -1966,7 +2240,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
1966
2240
  }
1967
2241
  }
1968
2242
  const largestIsJs = typeof runtimeAudit.largestAssetPath === "string" && runtimeAudit.largestAssetPath.endsWith(".js");
1969
- if (largestIsJs && runtimeAudit.largestAssetBytes > PERFORMANCE_BUDGETS.largestJsAssetWarnBytes || runtimeAudit.jsAssetBytes > PERFORMANCE_BUDGETS.totalJsWarnBytes) {
2243
+ if (!isFrameworkBuildOutput && (largestIsJs && runtimeAudit.largestAssetBytes > PERFORMANCE_BUDGETS.largestJsAssetWarnBytes || runtimeAudit.jsAssetBytes > PERFORMANCE_BUDGETS.totalJsWarnBytes)) {
1970
2244
  findings.push(
1971
2245
  makeFinding({
1972
2246
  id: "runtime-js-bundle-large",
@@ -1981,7 +2255,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
1981
2255
  })
1982
2256
  );
1983
2257
  }
1984
- if (runtimeAudit.cssAssetBytes > PERFORMANCE_BUDGETS.totalCssWarnBytes) {
2258
+ if (!isFrameworkBuildOutput && runtimeAudit.cssAssetBytes > PERFORMANCE_BUDGETS.totalCssWarnBytes) {
1985
2259
  findings.push(
1986
2260
  makeFinding({
1987
2261
  id: "runtime-css-bundle-large",
@@ -1996,7 +2270,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
1996
2270
  })
1997
2271
  );
1998
2272
  }
1999
- if (runtimeAudit.totalAssetBytes > PERFORMANCE_BUDGETS.totalAssetsWarnBytes) {
2273
+ if (!isFrameworkBuildOutput && runtimeAudit.totalAssetBytes > PERFORMANCE_BUDGETS.totalAssetsWarnBytes) {
2000
2274
  findings.push(
2001
2275
  makeFinding({
2002
2276
  id: "runtime-total-assets-large",
@@ -2113,7 +2387,7 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
2113
2387
  })
2114
2388
  );
2115
2389
  }
2116
- const navigation = essence && isV3(essence) ? essence.meta.navigation : null;
2390
+ const navigation = essence && isV4(essence) ? essence.meta.navigation : null;
2117
2391
  if (navigation?.command_palette && sourceAudit.commandPaletteSignals.count === 0) {
2118
2392
  findings.push(
2119
2393
  makeFinding({
@@ -2341,7 +2615,11 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
2341
2615
  })
2342
2616
  );
2343
2617
  }
2344
- if (topology.hasAuthFeature && topology.primaryRoutes.length > 0 && sourceAudit.protectedSurfaceSignals.count > 0 && sourceAudit.authGuardSignals.count > 0 && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authGuardSignals) && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authSessionSignals)) {
2618
+ if (topology.hasAuthFeature && topology.primaryRoutes.length > 0 && sourceAudit.protectedSurfaceSignals.count > 0 && sourceAudit.authGuardSignals.count > 0 && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authGuardSignals) && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authSessionSignals) && !sourceAuditProtectedSurfacesCoveredByGuardedNextLayouts(
2619
+ sourceAudit.protectedSurfaceSignals,
2620
+ sourceAudit.authGuardSignals,
2621
+ topology
2622
+ )) {
2345
2623
  findings.push(
2346
2624
  makeFinding({
2347
2625
  id: "source-protected-surface-auth-checks-missing",
@@ -3209,10 +3487,11 @@ async function auditProject(projectRoot) {
3209
3487
  })
3210
3488
  );
3211
3489
  }
3212
- }
3213
- const { themeRegistry, patternRegistry } = buildRegistryContext(projectRoot);
3214
- for (const violation of evaluateGuard(essence, { themeRegistry, patternRegistry })) {
3215
- findings.push(guardViolationToFinding(violation));
3490
+ } else {
3491
+ const { themeRegistry, patternRegistry } = buildRegistryContext(projectRoot);
3492
+ for (const violation of evaluateGuard(essence, { themeRegistry, patternRegistry })) {
3493
+ findings.push(guardViolationToFinding(violation));
3494
+ }
3216
3495
  }
3217
3496
  }
3218
3497
  appendTopologyFindings(findings, essence, reviewPack);
@@ -3329,7 +3608,9 @@ function buildDecoratorInventory(treatmentsCss) {
3329
3608
  decoratorNames.add(name);
3330
3609
  }
3331
3610
  }
3332
- return [...decoratorNames].filter((name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name));
3611
+ return [...decoratorNames].filter(
3612
+ (name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name)
3613
+ );
3333
3614
  }
3334
3615
  function isCssClassNameChar(char) {
3335
3616
  const codePoint = char.charCodeAt(0);
@@ -4485,7 +4766,7 @@ function isHeaderClearValue(node) {
4485
4766
  }
4486
4767
  function countAuthGuardSignals(code) {
4487
4768
  const patterns = [
4488
- /\b(?:ProtectedRoute|AuthGuard|RequireAuth|withAuth|requireAuth|ensureAuth|useRequireAuth)\b/,
4769
+ /\b(?:ProtectedRoute|AuthGuard|RequireAuth|withAuth|requireAuth|requireAdmin|ensureAuth|ensureAdmin|useRequireAuth)\b/,
4489
4770
  /\b(?:useAuth|useSession|getServerSession|authGuard|isAuthenticated|isSignedIn)\b/,
4490
4771
  /\b(?:redirect|navigate|push|replace)\s*\(\s*['"`]\/(?:auth|login|log-?in|sign-?in|sign-?up|register|forgot-password|reset-password)[^'"`]*['"`]/i,
4491
4772
  /\bNextResponse\.redirect\s*\(/,