@decantr/verifier 1.1.1 → 2.1.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/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  // src/index.ts
2
- import { existsSync as existsSync2, realpathSync, readdirSync, readFileSync as readFileSync2 } from "fs";
2
+ import { createHash } from "crypto";
3
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2 } from "fs";
3
4
  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";
5
+ import { basename, dirname, extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
6
+ import { evaluateGuard, isV4, validateEssence } from "@decantr/essence-spec";
6
7
  import * as ts from "typescript";
7
8
 
8
9
  // src/runtime.ts
9
- import { existsSync, readFileSync } from "fs";
10
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
10
11
  import { createServer } from "http";
11
12
  import { extname, join, normalize } from "path";
12
13
  var CONTENT_TYPES = {
@@ -224,6 +225,122 @@ function countSecretLeakSignals(js) {
224
225
  ];
225
226
  return patterns.reduce((count, pattern) => count + (js.match(pattern)?.length ?? 0), 0);
226
227
  }
228
+ function collectFiles(rootDir, predicate) {
229
+ if (!existsSync(rootDir)) return [];
230
+ const files = [];
231
+ const walk = (dir) => {
232
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
233
+ const path = join(dir, entry.name);
234
+ if (entry.isDirectory()) {
235
+ walk(path);
236
+ continue;
237
+ }
238
+ if (entry.isFile() && predicate(path)) {
239
+ files.push(path);
240
+ }
241
+ }
242
+ };
243
+ walk(rootDir);
244
+ return files.sort();
245
+ }
246
+ function auditNextBuildOutput(projectRoot) {
247
+ const nextDir = join(projectRoot, ".next");
248
+ const buildIdPresent = existsSync(join(nextDir, "BUILD_ID"));
249
+ const buildManifestPresent = existsSync(join(nextDir, "build-manifest.json"));
250
+ const appServerDir = join(nextDir, "server", "app");
251
+ const htmlFiles = collectFiles(appServerDir, (filePath) => {
252
+ const relativePath = filePath.slice(appServerDir.length + 1).replace(/\\/g, "/");
253
+ return filePath.endsWith(".html") && !relativePath.startsWith("_");
254
+ });
255
+ const htmlDocuments = htmlFiles.map((filePath) => readFileSync(filePath, "utf-8"));
256
+ const staticAssetFiles = collectFiles(
257
+ join(nextDir, "static"),
258
+ (filePath) => /\.(?:js|css|json|svg|png|jpe?g|webp)$/i.test(filePath)
259
+ );
260
+ let totalAssetBytes = 0;
261
+ let jsAssetBytes = 0;
262
+ let cssAssetBytes = 0;
263
+ let largestAssetPath = null;
264
+ let largestAssetBytes = 0;
265
+ let combinedJs = "";
266
+ for (const assetFile of staticAssetFiles) {
267
+ const byteLength = statSync(assetFile).size;
268
+ const assetPath = `/_next/static/${assetFile.slice(join(nextDir, "static").length + 1).replace(/\\/g, "/")}`;
269
+ totalAssetBytes += byteLength;
270
+ if (assetFile.endsWith(".js")) {
271
+ jsAssetBytes += byteLength;
272
+ combinedJs += readFileSync(assetFile, "utf-8");
273
+ }
274
+ if (assetFile.endsWith(".css")) {
275
+ cssAssetBytes += byteLength;
276
+ }
277
+ if (byteLength > largestAssetBytes) {
278
+ largestAssetBytes = byteLength;
279
+ largestAssetPath = assetPath;
280
+ }
281
+ }
282
+ const hasHtmlDocuments = htmlDocuments.length > 0;
283
+ const titleOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<title>[^<]+<\/title>/i.test(html));
284
+ const langOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<html[^>]*\slang=(["'])[^"']+\1/i.test(html));
285
+ const viewportOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(html));
286
+ const charsetOk = !hasHtmlDocuments || htmlDocuments.every((html) => /<meta[^>]+charset=/i.test(html));
287
+ const failures = ["next-build-output"];
288
+ if (!buildIdPresent) {
289
+ failures.push("next-build-id-missing");
290
+ }
291
+ if (!buildManifestPresent) {
292
+ failures.push("next-build-manifest-missing");
293
+ }
294
+ if (staticAssetFiles.length === 0) {
295
+ failures.push("next-assets-missing");
296
+ }
297
+ const passed = buildIdPresent && buildManifestPresent && staticAssetFiles.length > 0;
298
+ return {
299
+ distPresent: true,
300
+ indexPresent: true,
301
+ checked: true,
302
+ passed,
303
+ rootDocumentOk: hasHtmlDocuments || passed,
304
+ titleOk,
305
+ langOk,
306
+ viewportOk,
307
+ charsetOk,
308
+ cspSignalOk: true,
309
+ inlineScriptCount: 0,
310
+ inlineEventHandlerCount: 0,
311
+ externalScriptsWithoutIntegrityCount: 0,
312
+ externalScriptsWithIntegrityMissingCrossoriginCount: 0,
313
+ externalStylesheetsWithoutIntegrityCount: 0,
314
+ externalStylesheetsWithIntegrityMissingCrossoriginCount: 0,
315
+ externalScriptsWithInsecureTransportCount: 0,
316
+ externalStylesheetsWithInsecureTransportCount: 0,
317
+ externalMediaSourcesWithInsecureTransportCount: 0,
318
+ externalBlankLinksWithoutRelCount: 0,
319
+ externalIframesWithoutSandboxCount: 0,
320
+ externalIframesWithInsecureTransportCount: 0,
321
+ jsEvalSignalCount: countDynamicCodeSignals(combinedJs),
322
+ jsHtmlInjectionSignalCount: countHtmlInjectionSignals(combinedJs),
323
+ jsInsecureTransportSignalCount: countInsecureTransportSignals(combinedJs),
324
+ jsSecretSignalCount: countSecretLeakSignals(combinedJs),
325
+ assetCount: staticAssetFiles.length,
326
+ assetsPassed: staticAssetFiles.length,
327
+ routeHintsChecked: [],
328
+ routeHintsMatched: 0,
329
+ routeHintsCoverageOk: true,
330
+ routeDocumentsChecked: 0,
331
+ routeDocumentsPassed: 0,
332
+ routeDocumentsHardenedCount: 0,
333
+ routeDocumentsCoverageOk: true,
334
+ routeDocumentsHardeningOk: true,
335
+ fullRouteCoverageOk: true,
336
+ totalAssetBytes,
337
+ jsAssetBytes,
338
+ cssAssetBytes,
339
+ largestAssetPath,
340
+ largestAssetBytes,
341
+ failures
342
+ };
343
+ }
227
344
  function normalizeRouteHint(route) {
228
345
  if (!route || route === "/") return "/";
229
346
  const dynamicIndex = route.indexOf("/:");
@@ -280,6 +397,9 @@ async function startStaticServer(rootDir) {
280
397
  async function auditBuiltDist(projectRoot, options = {}) {
281
398
  const distDir = options.distDir ?? join(projectRoot, "dist");
282
399
  if (!existsSync(distDir)) {
400
+ if (!options.distDir && existsSync(join(projectRoot, ".next"))) {
401
+ return auditNextBuildOutput(projectRoot);
402
+ }
283
403
  return emptyRuntimeAudit(["dist-missing"]);
284
404
  }
285
405
  const indexPath = join(distDir, "index.html");
@@ -611,9 +731,249 @@ var VERIFICATION_SCHEMA_URLS = {
611
731
  common: "https://decantr.ai/schemas/verification-report.common.v1.json",
612
732
  projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json",
613
733
  projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json",
734
+ evidenceBundle: "https://decantr.ai/schemas/evidence-bundle.v1.json",
735
+ workspaceHealth: "https://decantr.ai/schemas/workspace-health-report.v1.json",
614
736
  fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
615
737
  showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
616
738
  };
739
+ var EVIDENCE_BUNDLE_SCHEMA_URL = "https://decantr.ai/schemas/evidence-bundle.v1.json";
740
+ function hashString(value) {
741
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
742
+ }
743
+ function hashFile(path) {
744
+ if (!existsSync2(path)) return null;
745
+ try {
746
+ return createHash("sha256").update(readFileSync2(path)).digest("hex");
747
+ } catch {
748
+ return null;
749
+ }
750
+ }
751
+ function provenanceEntry(projectRoot, relativePath) {
752
+ const path = join2(projectRoot, relativePath);
753
+ if (!existsSync2(path)) {
754
+ return {
755
+ path: relativePath,
756
+ present: false,
757
+ hash: null,
758
+ generatedAt: null
759
+ };
760
+ }
761
+ let generatedAt = null;
762
+ try {
763
+ generatedAt = statSync2(path).mtime.toISOString();
764
+ } catch {
765
+ generatedAt = null;
766
+ }
767
+ return {
768
+ path: relativePath,
769
+ present: true,
770
+ hash: hashFile(path),
771
+ generatedAt
772
+ };
773
+ }
774
+ function provenanceForPath(projectRoot, path) {
775
+ const rel = isAbsolute(path) ? relative(projectRoot, path).replace(/\\/g, "/") : path;
776
+ return provenanceEntry(projectRoot, rel && !rel.startsWith("..") ? rel : basename(path));
777
+ }
778
+ function redactEvidenceText(projectRoot, value) {
779
+ const normalizedRoot = projectRoot.replace(/\\/g, "/");
780
+ const normalized = value.replace(/\\/g, "/");
781
+ if (normalized.includes(normalizedRoot)) {
782
+ return normalized.split(normalizedRoot).join("<project>");
783
+ }
784
+ return normalized.replace(/\/Users\/[^/\s]+/g, "~").replace(/\/home\/[^/\s]+/g, "~");
785
+ }
786
+ function redactEvidenceList(projectRoot, evidence) {
787
+ return evidence.map((entry) => redactEvidenceText(projectRoot, entry));
788
+ }
789
+ function assertion(input) {
790
+ return {
791
+ status: "passed",
792
+ ...input
793
+ };
794
+ }
795
+ function createContractAssertions(projectRoot, audit) {
796
+ const assertions = [];
797
+ const essence = audit?.essence;
798
+ const v4 = essence && isV4(essence) ? essence : null;
799
+ const contextDir = join2(projectRoot, ".decantr", "context");
800
+ const packManifest = audit?.packManifest ?? null;
801
+ assertions.push(
802
+ assertion({
803
+ id: "contract.essence.present",
804
+ category: "context",
805
+ severity: "error",
806
+ rule: "essence-present",
807
+ status: essence ? "passed" : "failed",
808
+ message: essence ? "Decantr Essence contract is present." : "No Decantr Essence contract was found.",
809
+ evidence: [redactEvidenceText(projectRoot, join2(projectRoot, "decantr.essence.json"))],
810
+ suggestedFix: essence ? void 0 : "Run `decantr init` or restore decantr.essence.json."
811
+ })
812
+ );
813
+ assertions.push(
814
+ assertion({
815
+ id: "contract.essence.v4",
816
+ category: "context",
817
+ severity: "error",
818
+ rule: "essence-v4",
819
+ status: v4 ? "passed" : essence ? "failed" : "not_applicable",
820
+ message: v4 ? "Essence uses the active v4.0.0 contract." : essence ? "Essence is not the active v4.0.0 contract." : "Essence version could not be checked.",
821
+ evidence: [essence ? `version=${String(essence.version)}` : "essence missing"],
822
+ suggestedFix: v4 ? void 0 : "Run `decantr migrate --to v4` before reliability checks."
823
+ })
824
+ );
825
+ if (v4) {
826
+ const declaredRoutes = Object.keys(v4.blueprint.routes ?? {}).sort();
827
+ for (const route of declaredRoutes) {
828
+ const routeTarget = (v4.blueprint.routes ?? {})[route];
829
+ const section = v4.blueprint.sections.find((entry) => entry.id === routeTarget.section);
830
+ const page = section?.pages.find((entry) => entry.id === routeTarget.page);
831
+ assertions.push(
832
+ assertion({
833
+ id: `contract.route.${hashString(route)}`,
834
+ category: "route",
835
+ severity: page ? "info" : "warn",
836
+ rule: "declared-route-resolves",
837
+ status: page ? "passed" : "failed",
838
+ target: route,
839
+ message: page ? `Declared route ${route} resolves to a section page.` : `Declared route ${route} does not resolve to an existing section page.`,
840
+ evidence: [`section=${routeTarget.section}`, `page=${routeTarget.page}`],
841
+ suggestedFix: page ? void 0 : "Update blueprint.routes or add the missing section/page before running AI repair."
842
+ })
843
+ );
844
+ }
845
+ for (const section of v4.blueprint.sections) {
846
+ assertions.push(
847
+ assertion({
848
+ id: `contract.shell.${section.id}`,
849
+ category: "shell",
850
+ severity: section.shell && section.shell !== "inherit" ? "info" : "warn",
851
+ rule: "section-shell-concrete",
852
+ status: section.shell && section.shell !== "inherit" ? "passed" : "failed",
853
+ target: section.id,
854
+ message: section.shell && section.shell !== "inherit" ? `Section ${section.id} uses concrete shell ${section.shell}.` : `Section ${section.id} does not declare a concrete shell.`,
855
+ evidence: [`shell=${section.shell || "missing"}`],
856
+ suggestedFix: section.shell && section.shell !== "inherit" ? void 0 : "Resolve inherited shells during composition so AI agents get concrete layout intent."
857
+ })
858
+ );
859
+ }
860
+ assertions.push(
861
+ assertion({
862
+ id: "contract.accessibility.focus-visible",
863
+ category: "accessibility",
864
+ severity: v4.dna.accessibility.focus_visible ? "info" : "warn",
865
+ rule: "focus-visible-enabled",
866
+ status: v4.dna.accessibility.focus_visible ? "passed" : "failed",
867
+ message: v4.dna.accessibility.focus_visible ? "DNA requires visible focus states." : "DNA does not require visible focus states.",
868
+ evidence: [`focus_visible=${String(v4.dna.accessibility.focus_visible)}`],
869
+ suggestedFix: v4.dna.accessibility.focus_visible ? void 0 : "Set dna.accessibility.focus_visible=true for stronger UI reliability gates."
870
+ })
871
+ );
872
+ }
873
+ assertions.push(
874
+ assertion({
875
+ id: "contract.context.pack-manifest",
876
+ category: "context",
877
+ severity: packManifest ? "info" : "warn",
878
+ rule: "pack-manifest-present",
879
+ status: packManifest ? "passed" : "failed",
880
+ message: packManifest ? "Compiled execution pack manifest is present." : "Compiled execution pack manifest is missing.",
881
+ evidence: [redactEvidenceText(projectRoot, join2(contextDir, "pack-manifest.json"))],
882
+ suggestedFix: packManifest ? void 0 : "Run `decantr refresh` to regenerate context packs."
883
+ })
884
+ );
885
+ assertions.push(
886
+ assertion({
887
+ id: "contract.context.review-pack",
888
+ category: "context",
889
+ severity: audit?.reviewPack ? "info" : "warn",
890
+ rule: "review-pack-present",
891
+ status: audit?.reviewPack ? "passed" : "failed",
892
+ message: audit?.reviewPack ? "Compiled review pack is present." : "Compiled review pack is missing.",
893
+ evidence: [redactEvidenceText(projectRoot, join2(contextDir, "review-pack.json"))],
894
+ suggestedFix: audit?.reviewPack ? void 0 : "Run `decantr refresh` or hydrate the review pack from the registry."
895
+ })
896
+ );
897
+ const tokensPath = join2(projectRoot, "src", "styles", "tokens.css");
898
+ assertions.push(
899
+ assertion({
900
+ id: "contract.design-token.tokens-file",
901
+ category: "design-token",
902
+ severity: existsSync2(tokensPath) ? "info" : "warn",
903
+ rule: "tokens-file-present",
904
+ status: existsSync2(tokensPath) ? "passed" : "failed",
905
+ message: existsSync2(tokensPath) ? "Decantr token CSS file is present." : "Decantr token CSS file was not found.",
906
+ evidence: [redactEvidenceText(projectRoot, tokensPath)],
907
+ suggestedFix: existsSync2(tokensPath) ? void 0 : "Run `decantr refresh` or map Decantr tokens into the existing styling system."
908
+ })
909
+ );
910
+ return assertions;
911
+ }
912
+ function createEvidenceBundle(input) {
913
+ const assertions = input.assertions ?? createContractAssertions(input.projectRoot, input.audit);
914
+ let resolvedProjectRoot = input.projectRoot;
915
+ try {
916
+ resolvedProjectRoot = realpathSync(input.projectRoot);
917
+ } catch {
918
+ resolvedProjectRoot = resolve(input.projectRoot);
919
+ }
920
+ const projectId = `project_${hashString(resolvedProjectRoot)}`;
921
+ return {
922
+ $schema: EVIDENCE_BUNDLE_SCHEMA_URL,
923
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
924
+ project: {
925
+ id: projectId,
926
+ rootLabel: basename(input.projectRoot) || "project"
927
+ },
928
+ toolchain: {
929
+ verifierVersion: input.verifierVersion ?? null
930
+ },
931
+ privacy: {
932
+ localOnly: true,
933
+ redactedFields: [
934
+ "source",
935
+ "prompt",
936
+ "secret",
937
+ "environment",
938
+ "absolute_path",
939
+ "repository_name"
940
+ ],
941
+ screenshotsLocalOnly: true
942
+ },
943
+ health: {
944
+ status: input.report.status,
945
+ score: input.report.score,
946
+ errorCount: input.report.summary.errorCount,
947
+ warnCount: input.report.summary.warnCount,
948
+ infoCount: input.report.summary.infoCount,
949
+ findingCount: input.report.summary.findingCount
950
+ },
951
+ provenance: {
952
+ essence: provenanceEntry(input.projectRoot, "decantr.essence.json"),
953
+ packManifest: provenanceEntry(input.projectRoot, ".decantr/context/pack-manifest.json"),
954
+ reviewPack: provenanceEntry(input.projectRoot, ".decantr/context/review-pack.json"),
955
+ ...input.workspaceConfigPath ? { workspaceConfig: provenanceForPath(input.projectRoot, input.workspaceConfigPath) } : {},
956
+ ...input.designTokensPath ? { designTokens: provenanceForPath(input.projectRoot, input.designTokensPath) } : {}
957
+ },
958
+ assertions,
959
+ findings: input.report.findings.map((finding) => ({
960
+ id: finding.id,
961
+ source: finding.source,
962
+ category: finding.category,
963
+ severity: finding.severity,
964
+ message: redactEvidenceText(input.projectRoot, finding.message),
965
+ evidence: redactEvidenceList(input.projectRoot, finding.evidence),
966
+ target: finding.target ? redactEvidenceText(input.projectRoot, finding.target) : void 0,
967
+ rule: finding.rule,
968
+ suggestedFix: finding.suggestedFix,
969
+ remediationSummary: finding.remediation.summary,
970
+ commands: finding.remediation.commands,
971
+ promptCommand: `decantr health --prompt ${finding.id}`
972
+ })),
973
+ ...input.browser ? { browser: input.browser } : {},
974
+ ...input.designTokens ? { designTokens: input.designTokens } : {}
975
+ };
976
+ }
617
977
  var DEFAULT_FOCUS_AREAS = [
618
978
  "route-topology",
619
979
  "theme-consistency",
@@ -686,6 +1046,54 @@ function recordSourceAudit(bucket, filePath, count) {
686
1046
  function sourceAuditBucketsOverlap(a, b) {
687
1047
  return a.files.some((file) => b.files.includes(file));
688
1048
  }
1049
+ function normalizeSourceAuditPath(filePath) {
1050
+ return filePath.replace(/\\/g, "/");
1051
+ }
1052
+ function getNextAppLayoutGuardRoot(filePath) {
1053
+ const normalized = normalizeSourceAuditPath(filePath);
1054
+ const match = normalized.match(
1055
+ /^(?:src\/)?app\/(?:\([^/]+\)\/)*([^/.()[\]]+)\/layout\.[cm]?[jt]sx?$/i
1056
+ );
1057
+ return match?.[1] ?? null;
1058
+ }
1059
+ function getNextAppRouteRoot(filePath) {
1060
+ const normalized = normalizeSourceAuditPath(filePath);
1061
+ const match = normalized.match(/^(?:src\/)?app\/(?:\([^/]+\)\/)*([^/.()[\]]+)(?:\/|$)/i);
1062
+ return match?.[1] ?? null;
1063
+ }
1064
+ function getRouteRoot(route) {
1065
+ const firstSegment = route.replace(/^\/+/, "").split("/")[0];
1066
+ if (!firstSegment || firstSegment.startsWith(":") || firstSegment.startsWith("[")) {
1067
+ return null;
1068
+ }
1069
+ return firstSegment;
1070
+ }
1071
+ function isNextAppPublicChromeFile(filePath) {
1072
+ const normalized = normalizeSourceAuditPath(filePath);
1073
+ return /^(?:src\/)?app\/(?:\([^/]+\)\/)*(?:layout|nav|nav-header|navigation|header|footer|sidebar)\.[cm]?[jt]sx?$/i.test(
1074
+ normalized
1075
+ );
1076
+ }
1077
+ function sourceAuditProtectedSurfacesCoveredByGuardedNextLayouts(protectedSurfaces, authGuards, topology) {
1078
+ const guardedRoots = /* @__PURE__ */ new Set();
1079
+ for (const file of authGuards.files) {
1080
+ const root = getNextAppLayoutGuardRoot(file);
1081
+ if (root) {
1082
+ guardedRoots.add(root);
1083
+ }
1084
+ }
1085
+ if (guardedRoots.size === 0 || protectedSurfaces.files.length === 0) {
1086
+ return false;
1087
+ }
1088
+ const guardedPrimaryRouteRoots = topology.primaryRoutes.map(getRouteRoot).filter((root) => Boolean(root)).filter((root) => guardedRoots.has(root));
1089
+ return protectedSurfaces.files.every((file) => {
1090
+ const appRoot = getNextAppRouteRoot(file);
1091
+ if (appRoot && guardedRoots.has(appRoot)) {
1092
+ return true;
1093
+ }
1094
+ return isNextAppPublicChromeFile(file) && guardedPrimaryRouteRoots.length > 0;
1095
+ });
1096
+ }
689
1097
  function isAuditableSourceFile(filePath) {
690
1098
  if (/\.d\.ts$/i.test(filePath)) return false;
691
1099
  return /\.(?:[cm]?[jt]sx?)$/i.test(filePath);
@@ -726,7 +1134,7 @@ function collectProjectSourceFiles(projectRoot) {
726
1134
  "coverage"
727
1135
  ]);
728
1136
  const walk = (dir) => {
729
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1137
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
730
1138
  if (ignoredDirNames.has(entry.name)) continue;
731
1139
  const absolutePath = join2(dir, entry.name);
732
1140
  if (entry.isDirectory()) {
@@ -770,7 +1178,7 @@ function collectProjectStyleFiles(projectRoot) {
770
1178
  "coverage"
771
1179
  ]);
772
1180
  const walk = (dir) => {
773
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1181
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
774
1182
  if (ignoredDirNames.has(entry.name)) continue;
775
1183
  const absolutePath = join2(dir, entry.name);
776
1184
  if (entry.isDirectory()) {
@@ -799,8 +1207,126 @@ function countReducedMotionSignals(code) {
799
1207
  ];
800
1208
  return patterns.reduce((count, pattern) => count + (pattern.test(code) ? 1 : 0), 0);
801
1209
  }
1210
+ function hasModuleDirective(filePath, code, directive) {
1211
+ const sourceFile = ts.createSourceFile(
1212
+ filePath,
1213
+ code,
1214
+ ts.ScriptTarget.Latest,
1215
+ true,
1216
+ getScriptKind(filePath)
1217
+ );
1218
+ for (const statement of sourceFile.statements) {
1219
+ if (ts.isExpressionStatement(statement) && ts.isStringLiteralLike(statement.expression)) {
1220
+ if (statement.expression.text === directive) {
1221
+ return true;
1222
+ }
1223
+ continue;
1224
+ }
1225
+ break;
1226
+ }
1227
+ return false;
1228
+ }
1229
+ function importDeclarationHasRuntimeBinding(statement) {
1230
+ const clause = statement.importClause;
1231
+ if (!clause) return true;
1232
+ if (clause.isTypeOnly) return false;
1233
+ if (clause.name) return true;
1234
+ if (!clause.namedBindings) return true;
1235
+ if (ts.isNamespaceImport(clause.namedBindings)) return true;
1236
+ return clause.namedBindings.elements.some((element) => !element.isTypeOnly);
1237
+ }
1238
+ function collectRuntimeImportSpecifiers(entry) {
1239
+ const sourceFile = ts.createSourceFile(
1240
+ entry.relativePath,
1241
+ entry.code,
1242
+ ts.ScriptTarget.Latest,
1243
+ true,
1244
+ getScriptKind(entry.relativePath)
1245
+ );
1246
+ const specifiers = [];
1247
+ for (const statement of sourceFile.statements) {
1248
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) && importDeclarationHasRuntimeBinding(statement)) {
1249
+ specifiers.push(statement.moduleSpecifier.text);
1250
+ }
1251
+ }
1252
+ return specifiers;
1253
+ }
1254
+ function resolveSourceImportTarget(projectRoot, sourceAbsolutePath, specifier) {
1255
+ let basePath = null;
1256
+ if (specifier.startsWith("@/")) {
1257
+ basePath = join2(projectRoot, "src", specifier.slice(2));
1258
+ } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
1259
+ basePath = resolve(dirname(sourceAbsolutePath), specifier);
1260
+ }
1261
+ if (!basePath) return null;
1262
+ const candidates = [
1263
+ basePath,
1264
+ `${basePath}.ts`,
1265
+ `${basePath}.tsx`,
1266
+ `${basePath}.js`,
1267
+ `${basePath}.jsx`,
1268
+ `${basePath}.mts`,
1269
+ `${basePath}.cts`,
1270
+ join2(basePath, "index.ts"),
1271
+ join2(basePath, "index.tsx"),
1272
+ join2(basePath, "index.js"),
1273
+ join2(basePath, "index.jsx")
1274
+ ];
1275
+ for (const candidate of candidates) {
1276
+ if (existsSync2(candidate) && isAuditableSourceFile(candidate)) {
1277
+ return relative(projectRoot, candidate) || candidate;
1278
+ }
1279
+ }
1280
+ return null;
1281
+ }
1282
+ function collectClientReachableSourceFiles(projectRoot, entries) {
1283
+ const entriesByRelativePath = new Map(entries.map((entry) => [entry.relativePath, entry]));
1284
+ const runtimeImportGraph = /* @__PURE__ */ new Map();
1285
+ for (const entry of entries) {
1286
+ const targets = collectRuntimeImportSpecifiers(entry).map((specifier) => resolveSourceImportTarget(projectRoot, entry.absolutePath, specifier)).filter((target) => Boolean(target));
1287
+ runtimeImportGraph.set(entry.relativePath, targets);
1288
+ }
1289
+ const reachable = /* @__PURE__ */ new Set();
1290
+ const queue = entries.filter((entry) => hasModuleDirective(entry.relativePath, entry.code, "use client")).map((entry) => entry.relativePath);
1291
+ for (const root of queue) {
1292
+ reachable.add(root);
1293
+ }
1294
+ while (queue.length > 0) {
1295
+ const current = queue.shift();
1296
+ for (const target of runtimeImportGraph.get(current) ?? []) {
1297
+ const targetEntry = entriesByRelativePath.get(target);
1298
+ if (targetEntry && hasModuleDirective(target, targetEntry.code, "use server")) {
1299
+ continue;
1300
+ }
1301
+ if (!reachable.has(target)) {
1302
+ reachable.add(target);
1303
+ queue.push(target);
1304
+ }
1305
+ }
1306
+ }
1307
+ return reachable;
1308
+ }
1309
+ function isClientAuthHeaderSource(relativePath, code, clientReachableFiles) {
1310
+ if (hasModuleDirective(relativePath, code, "use server")) {
1311
+ return false;
1312
+ }
1313
+ if (clientReachableFiles.has(relativePath)) {
1314
+ return true;
1315
+ }
1316
+ const normalized = normalizeSourceAuditPath(relativePath);
1317
+ if (/^(?:src\/)?app\//i.test(normalized)) {
1318
+ return false;
1319
+ }
1320
+ return /(?:^|\/)(?:components|routes|pages|hooks|providers)\//i.test(normalized);
1321
+ }
802
1322
  function auditProjectSourceTree(projectRoot) {
803
1323
  const sourceFiles = collectProjectSourceFiles(projectRoot);
1324
+ const sourceEntries = sourceFiles.map((sourceFile) => ({
1325
+ absolutePath: sourceFile,
1326
+ relativePath: relative(projectRoot, sourceFile) || sourceFile,
1327
+ code: readFileSync2(sourceFile, "utf-8")
1328
+ }));
1329
+ const clientReachableFiles = collectClientReachableSourceFiles(projectRoot, sourceEntries);
804
1330
  const summary = {
805
1331
  filesChecked: sourceFiles.length,
806
1332
  inlineStyles: createSourceAuditBucket(),
@@ -869,10 +1395,13 @@ function auditProjectSourceTree(projectRoot) {
869
1395
  interactionSafetyIssues: createSourceAuditBucket(),
870
1396
  authInputHintIssues: createSourceAuditBucket()
871
1397
  };
872
- for (const sourceFile of sourceFiles) {
873
- const relativePath = relative(projectRoot, sourceFile) || sourceFile;
874
- const code = readFileSync2(sourceFile, "utf-8");
1398
+ for (const { relativePath, code } of sourceEntries) {
875
1399
  const signals = analyzeAstSignals(relativePath, code);
1400
+ const isClientAuthHeaderSourceFile = isClientAuthHeaderSource(
1401
+ relativePath,
1402
+ code,
1403
+ clientReachableFiles
1404
+ );
876
1405
  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
1406
  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
1407
  const authInputHintIssueCount = signals.emailAutocompleteMissingCount + signals.passwordAutocompleteMissingCount + signals.otpAutocompleteMissingCount + signals.authAutocompleteDisabledCount + signals.authAutocompleteSemanticMismatchCount + signals.authInputTypeMismatchCount;
@@ -1042,8 +1571,16 @@ function auditProjectSourceTree(projectRoot) {
1042
1571
  recordSourceAudit(summary.authStorageClears, relativePath, signals.authStorageClearCount);
1043
1572
  recordSourceAudit(summary.authCookieWrites, relativePath, signals.authCookieWriteCount);
1044
1573
  recordSourceAudit(summary.authCookieClears, relativePath, signals.authCookieClearCount);
1045
- recordSourceAudit(summary.authHeaderWrites, relativePath, signals.authHeaderWriteCount);
1046
- recordSourceAudit(summary.authHeaderClears, relativePath, signals.authHeaderClearCount);
1574
+ recordSourceAudit(
1575
+ summary.authHeaderWrites,
1576
+ relativePath,
1577
+ isClientAuthHeaderSourceFile ? signals.authHeaderWriteCount : 0
1578
+ );
1579
+ recordSourceAudit(
1580
+ summary.authHeaderClears,
1581
+ relativePath,
1582
+ isClientAuthHeaderSourceFile ? signals.authHeaderClearCount : 0
1583
+ );
1047
1584
  recordSourceAudit(summary.authCacheClients, relativePath, signals.authCacheClientCount);
1048
1585
  recordSourceAudit(summary.authCacheClears, relativePath, signals.authCacheClearCount);
1049
1586
  recordSourceAudit(summary.authRefreshSignals, relativePath, signals.authRefreshSignalCount);
@@ -1115,7 +1652,7 @@ function buildRegistryContext(projectRoot) {
1115
1652
  const cachedThemesDir = join2(cacheDir, "@official", "themes");
1116
1653
  try {
1117
1654
  if (existsSync2(cachedThemesDir)) {
1118
- for (const file of readdirSync(cachedThemesDir).filter(
1655
+ for (const file of readdirSync2(cachedThemesDir).filter(
1119
1656
  (name) => name.endsWith(".json") && name !== "index.json"
1120
1657
  )) {
1121
1658
  const data = JSON.parse(readFileSync2(join2(cachedThemesDir, file), "utf-8"));
@@ -1129,7 +1666,7 @@ function buildRegistryContext(projectRoot) {
1129
1666
  const customThemesDir = join2(customDir, "themes");
1130
1667
  try {
1131
1668
  if (existsSync2(customThemesDir)) {
1132
- for (const file of readdirSync(customThemesDir).filter((name) => name.endsWith(".json"))) {
1669
+ for (const file of readdirSync2(customThemesDir).filter((name) => name.endsWith(".json"))) {
1133
1670
  const data = JSON.parse(readFileSync2(join2(customThemesDir, file), "utf-8"));
1134
1671
  if (data.id) {
1135
1672
  themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
@@ -1141,7 +1678,7 @@ function buildRegistryContext(projectRoot) {
1141
1678
  const cachedPatternsDir = join2(cacheDir, "@official", "patterns");
1142
1679
  try {
1143
1680
  if (existsSync2(cachedPatternsDir)) {
1144
- for (const file of readdirSync(cachedPatternsDir).filter(
1681
+ for (const file of readdirSync2(cachedPatternsDir).filter(
1145
1682
  (name) => name.endsWith(".json") && name !== "index.json"
1146
1683
  )) {
1147
1684
  const data = JSON.parse(readFileSync2(join2(cachedPatternsDir, file), "utf-8"));
@@ -1167,17 +1704,13 @@ function guardViolationToFinding(violation) {
1167
1704
  }
1168
1705
  function countPages(essence) {
1169
1706
  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;
1707
+ const blueprint = essence.blueprint;
1708
+ const sectionPages = Array.isArray(blueprint.sections) ? blueprint.sections.reduce(
1709
+ (sum, section) => sum + (Array.isArray(section.pages) ? section.pages.length : 0),
1710
+ 0
1711
+ ) : 0;
1712
+ const flatPages = Array.isArray(blueprint.pages) ? blueprint.pages.length : 0;
1713
+ return sectionPages + flatPages;
1181
1714
  }
1182
1715
  function makeFinding(input) {
1183
1716
  return input;
@@ -1252,31 +1785,19 @@ function extractRouteHintsFromEssence(essence) {
1252
1785
  return ["/"];
1253
1786
  }
1254
1787
  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 ?? []) {
1788
+ if (isV4(essence)) {
1789
+ for (const section of essence.blueprint.sections) {
1790
+ for (const page of section.pages) {
1259
1791
  if (typeof page.route === "string" && page.route.length > 0) {
1260
1792
  routes.add(normalizeRouteHint2(page.route));
1261
1793
  }
1262
1794
  }
1263
1795
  }
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)) {
1796
+ if (essence.blueprint.routes && typeof essence.blueprint.routes === "object") {
1797
+ for (const route of Object.keys(essence.blueprint.routes)) {
1271
1798
  routes.add(normalizeRouteHint2(route));
1272
1799
  }
1273
1800
  }
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
1801
  }
1281
1802
  return [...routes].filter(Boolean).slice(0, 8);
1282
1803
  }
@@ -1288,14 +1809,13 @@ function summarizeTopology(essence, reviewPack) {
1288
1809
  let gatewayRouteCount = 0;
1289
1810
  let primaryRouteCount = 0;
1290
1811
  let hasAnonymousEntryRoute = false;
1291
- if (essence && isV3(essence)) {
1292
- const v3 = essence;
1293
- for (const feature of v3.blueprint.features ?? []) {
1812
+ if (essence && isV4(essence)) {
1813
+ for (const feature of essence.blueprint.features ?? []) {
1294
1814
  if (typeof feature === "string" && feature.length > 0) {
1295
1815
  features.add(feature);
1296
1816
  }
1297
1817
  }
1298
- for (const section of v3.blueprint.sections ?? []) {
1818
+ for (const section of essence.blueprint.sections ?? []) {
1299
1819
  const role = typeof section.role === "string" && section.role.length > 0 ? section.role : "unknown";
1300
1820
  sectionRoles.add(role);
1301
1821
  for (const page of section.pages ?? []) {
@@ -1472,6 +1992,7 @@ function extractFailedRouteDocumentHardening(runtimeAudit) {
1472
1992
  function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topology, sourceAudit) {
1473
1993
  const distPath = join2(projectRoot, "dist");
1474
1994
  const indexPath = join2(distPath, "index.html");
1995
+ const isFrameworkBuildOutput = runtimeAudit.failures.includes("next-build-output");
1475
1996
  if (!runtimeAudit.distPresent) {
1476
1997
  findings.push(
1477
1998
  makeFinding({
@@ -1966,7 +2487,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
1966
2487
  }
1967
2488
  }
1968
2489
  const largestIsJs = typeof runtimeAudit.largestAssetPath === "string" && runtimeAudit.largestAssetPath.endsWith(".js");
1969
- if (largestIsJs && runtimeAudit.largestAssetBytes > PERFORMANCE_BUDGETS.largestJsAssetWarnBytes || runtimeAudit.jsAssetBytes > PERFORMANCE_BUDGETS.totalJsWarnBytes) {
2490
+ if (!isFrameworkBuildOutput && (largestIsJs && runtimeAudit.largestAssetBytes > PERFORMANCE_BUDGETS.largestJsAssetWarnBytes || runtimeAudit.jsAssetBytes > PERFORMANCE_BUDGETS.totalJsWarnBytes)) {
1970
2491
  findings.push(
1971
2492
  makeFinding({
1972
2493
  id: "runtime-js-bundle-large",
@@ -1981,7 +2502,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
1981
2502
  })
1982
2503
  );
1983
2504
  }
1984
- if (runtimeAudit.cssAssetBytes > PERFORMANCE_BUDGETS.totalCssWarnBytes) {
2505
+ if (!isFrameworkBuildOutput && runtimeAudit.cssAssetBytes > PERFORMANCE_BUDGETS.totalCssWarnBytes) {
1985
2506
  findings.push(
1986
2507
  makeFinding({
1987
2508
  id: "runtime-css-bundle-large",
@@ -1996,7 +2517,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
1996
2517
  })
1997
2518
  );
1998
2519
  }
1999
- if (runtimeAudit.totalAssetBytes > PERFORMANCE_BUDGETS.totalAssetsWarnBytes) {
2520
+ if (!isFrameworkBuildOutput && runtimeAudit.totalAssetBytes > PERFORMANCE_BUDGETS.totalAssetsWarnBytes) {
2000
2521
  findings.push(
2001
2522
  makeFinding({
2002
2523
  id: "runtime-total-assets-large",
@@ -2113,7 +2634,7 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
2113
2634
  })
2114
2635
  );
2115
2636
  }
2116
- const navigation = essence && isV3(essence) ? essence.meta.navigation : null;
2637
+ const navigation = essence && isV4(essence) ? essence.meta.navigation : null;
2117
2638
  if (navigation?.command_palette && sourceAudit.commandPaletteSignals.count === 0) {
2118
2639
  findings.push(
2119
2640
  makeFinding({
@@ -2341,7 +2862,11 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
2341
2862
  })
2342
2863
  );
2343
2864
  }
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)) {
2865
+ 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(
2866
+ sourceAudit.protectedSurfaceSignals,
2867
+ sourceAudit.authGuardSignals,
2868
+ topology
2869
+ )) {
2345
2870
  findings.push(
2346
2871
  makeFinding({
2347
2872
  id: "source-protected-surface-auth-checks-missing",
@@ -3209,10 +3734,11 @@ async function auditProject(projectRoot) {
3209
3734
  })
3210
3735
  );
3211
3736
  }
3212
- }
3213
- const { themeRegistry, patternRegistry } = buildRegistryContext(projectRoot);
3214
- for (const violation of evaluateGuard(essence, { themeRegistry, patternRegistry })) {
3215
- findings.push(guardViolationToFinding(violation));
3737
+ } else {
3738
+ const { themeRegistry, patternRegistry } = buildRegistryContext(projectRoot);
3739
+ for (const violation of evaluateGuard(essence, { themeRegistry, patternRegistry })) {
3740
+ findings.push(guardViolationToFinding(violation));
3741
+ }
3216
3742
  }
3217
3743
  }
3218
3744
  appendTopologyFindings(findings, essence, reviewPack);
@@ -3329,7 +3855,9 @@ function buildDecoratorInventory(treatmentsCss) {
3329
3855
  decoratorNames.add(name);
3330
3856
  }
3331
3857
  }
3332
- return [...decoratorNames].filter((name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name));
3858
+ return [...decoratorNames].filter(
3859
+ (name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name)
3860
+ );
3333
3861
  }
3334
3862
  function isCssClassNameChar(char) {
3335
3863
  const codePoint = char.charCodeAt(0);
@@ -3603,6 +4131,23 @@ function isNavigationLandmark(tagName, attributes) {
3603
4131
  function hasNavigationLabel(attributes) {
3604
4132
  return Boolean(getJsxAttribute(attributes, "aria-label", "aria-labelledby", "title"));
3605
4133
  }
4134
+ function getJsxAttributeOwner(attribute) {
4135
+ const attributes = attribute.parent;
4136
+ if (!attributes || !ts.isJsxAttributes(attributes)) return null;
4137
+ const owner = attributes.parent;
4138
+ if (ts.isJsxOpeningElement(owner) || ts.isJsxSelfClosingElement(owner)) {
4139
+ return owner;
4140
+ }
4141
+ return null;
4142
+ }
4143
+ function isSafeJsonLdDangerouslySetInnerHtml(attribute) {
4144
+ const owner = getJsxAttributeOwner(attribute);
4145
+ if (!owner || getJsxTagName(owner) !== "script") return false;
4146
+ const typeValue = getJsxAttributeLiteralValue(getJsxAttribute(owner.attributes, "type"))?.trim().toLowerCase();
4147
+ if (typeValue !== "application/ld+json") return false;
4148
+ const sourceText = attribute.getText();
4149
+ return /\bJSON\.stringify\s*\(/.test(sourceText) && /\\u003c/i.test(sourceText);
4150
+ }
3606
4151
  function hasInsecureFormAction(attributes) {
3607
4152
  const actionValue = getJsxAttributeLiteralValue(getJsxAttribute(attributes, "action"));
3608
4153
  const normalized = actionValue?.trim().toLowerCase() ?? "";
@@ -4485,7 +5030,7 @@ function isHeaderClearValue(node) {
4485
5030
  }
4486
5031
  function countAuthGuardSignals(code) {
4487
5032
  const patterns = [
4488
- /\b(?:ProtectedRoute|AuthGuard|RequireAuth|withAuth|requireAuth|ensureAuth|useRequireAuth)\b/,
5033
+ /\b(?:ProtectedRoute|AuthGuard|RequireAuth|withAuth|requireAuth|requireAdmin|ensureAuth|ensureAdmin|useRequireAuth)\b/,
4489
5034
  /\b(?:useAuth|useSession|getServerSession|authGuard|isAuthenticated|isSignedIn)\b/,
4490
5035
  /\b(?:redirect|navigate|push|replace)\s*\(\s*['"`]\/(?:auth|login|log-?in|sign-?in|sign-?up|register|forgot-password|reset-password)[^'"`]*['"`]/i,
4491
5036
  /\bNextResponse\.redirect\s*\(/,
@@ -9125,7 +9670,7 @@ function analyzeAstSignals(filePath, code) {
9125
9670
  signals.inlineStyleAttributeCount += 1;
9126
9671
  }
9127
9672
  }
9128
- if (isPropertyNamed(node.name, "dangerouslySetInnerHTML")) {
9673
+ if (isPropertyNamed(node.name, "dangerouslySetInnerHTML") && !isSafeJsonLdDangerouslySetInnerHtml(node)) {
9129
9674
  signals.dangerousHtmlCount += 1;
9130
9675
  }
9131
9676
  if (isPropertyNamed(node.name, "href", "to") && node.initializer && ts.isJsxExpression(node.initializer) && expressionContainsOpenRedirectSource(
@@ -10755,10 +11300,7 @@ function critiqueSource({
10755
11300
  })
10756
11301
  );
10757
11302
  }
10758
- const dangerousHtmlCount = Math.max(
10759
- astSignals.dangerousHtmlCount,
10760
- /dangerouslySetInnerHTML\s*=/.test(code) ? 1 : 0
10761
- );
11303
+ const dangerousHtmlCount = astSignals.dangerousHtmlCount;
10762
11304
  const rawHtmlInjectionCount = Math.max(
10763
11305
  astSignals.rawHtmlInjectionCount,
10764
11306
  /\binnerHTML\s*=|\binsertAdjacentHTML\s*\(|\bdocument\.write\s*\(/.test(code) ? 1 : 0
@@ -11385,10 +11927,13 @@ async function critiqueFile(filePath, projectRoot) {
11385
11927
  });
11386
11928
  }
11387
11929
  export {
11930
+ EVIDENCE_BUNDLE_SCHEMA_URL,
11388
11931
  INTERACTION_SIGNALS,
11389
11932
  VERIFICATION_SCHEMA_URLS,
11390
11933
  auditBuiltDist,
11391
11934
  auditProject,
11935
+ createContractAssertions,
11936
+ createEvidenceBundle,
11392
11937
  critiqueFile,
11393
11938
  critiqueSource,
11394
11939
  emptyRuntimeAudit,