@decantr/verifier 2.0.0 → 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/README.md CHANGED
@@ -16,7 +16,9 @@ npm install @decantr/verifier
16
16
  - `auditProject()` for project-level Decantr audits
17
17
  - `auditBuiltDist()` for built-output runtime verification against emitted HTML, assets, and route hints
18
18
  - `critiqueFile()` for file-level review against compiled review-pack contracts
19
- - schema-backed report types for project audits, Project Health, file critiques, and showcase verification
19
+ - `createContractAssertions()` for explicit route, shell, accessibility, context, and design-token assertions derived from Essence/context
20
+ - `createEvidenceBundle()` for privacy-redacted local evidence artifacts used by AI repair loops and CI
21
+ - schema-backed report types for project audits, Project Health, Evidence Bundles, Workspace Health, file critiques, and showcase verification
20
22
  - `ProjectHealthReport`, `ProjectHealthFinding`, and `ProjectHealthRemediation` types for the CLI's end-user health surface
21
23
  - published verifier report schemas are exercised by AJV-backed round-trip tests against real audit, critique, and shortlist-report outputs
22
24
  - project audits include runtime evidence when a built `dist/` output is present:
@@ -31,9 +33,16 @@ npm install @decantr/verifier
31
33
  ## Example
32
34
 
33
35
  ```ts
34
- import { auditProject, critiqueFile, type ProjectHealthReport } from '@decantr/verifier';
36
+ import {
37
+ auditProject,
38
+ createContractAssertions,
39
+ createEvidenceBundle,
40
+ critiqueFile,
41
+ type ProjectHealthReport,
42
+ } from '@decantr/verifier';
35
43
 
36
44
  const audit = await auditProject(process.cwd());
45
+ const assertions = createContractAssertions(process.cwd(), audit);
37
46
  const critique = await critiqueFile('./src/pages/overview.tsx', process.cwd());
38
47
 
39
48
  function isBlocking(report: ProjectHealthReport) {
@@ -46,6 +55,8 @@ function isBlocking(report: ProjectHealthReport) {
46
55
  - `@decantr/verifier/schema/verification-report.common.v1.json`
47
56
  - `@decantr/verifier/schema/project-audit-report.v1.json`
48
57
  - `@decantr/verifier/schema/project-health-report.v1.json`
58
+ - `@decantr/verifier/schema/evidence-bundle.v1.json`
59
+ - `@decantr/verifier/schema/workspace-health-report.v1.json`
49
60
  - `@decantr/verifier/schema/file-critique-report.v1.json`
50
61
  - `@decantr/verifier/schema/showcase-shortlist-report.v1.json`
51
62
 
package/dist/index.d.ts CHANGED
@@ -119,12 +119,14 @@ declare const VERIFICATION_SCHEMA_URLS: {
119
119
  readonly common: "https://decantr.ai/schemas/verification-report.common.v1.json";
120
120
  readonly projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json";
121
121
  readonly projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json";
122
+ readonly evidenceBundle: "https://decantr.ai/schemas/evidence-bundle.v1.json";
123
+ readonly workspaceHealth: "https://decantr.ai/schemas/workspace-health-report.v1.json";
122
124
  readonly fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json";
123
125
  readonly showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json";
124
126
  };
125
127
  type VerificationSeverity = 'error' | 'warn' | 'info';
126
128
  type ProjectHealthStatus = 'healthy' | 'warning' | 'error';
127
- type ProjectHealthFindingSource = 'audit' | 'check' | 'brownfield' | 'runtime' | 'pack' | 'interaction';
129
+ type ProjectHealthFindingSource = 'audit' | 'assertion' | 'browser' | 'check' | 'brownfield' | 'design-token' | 'runtime' | 'pack' | 'interaction';
128
130
  interface VerificationFinding {
129
131
  id: string;
130
132
  category: string;
@@ -248,6 +250,98 @@ interface ProjectHealthReport {
248
250
  };
249
251
  findings: ProjectHealthFinding[];
250
252
  }
253
+ type ContractAssertionCategory = 'route' | 'shell' | 'region' | 'interaction' | 'accessibility' | 'design-token' | 'context' | 'policy';
254
+ interface ContractAssertion {
255
+ id: string;
256
+ category: ContractAssertionCategory;
257
+ status: 'passed' | 'failed' | 'not_applicable';
258
+ severity: VerificationSeverity;
259
+ message: string;
260
+ evidence: string[];
261
+ target?: string;
262
+ rule: string;
263
+ suggestedFix?: string;
264
+ }
265
+ interface EvidenceProvenanceEntry {
266
+ path: string;
267
+ present: boolean;
268
+ hash: string | null;
269
+ generatedAt: string | null;
270
+ }
271
+ interface EvidenceBundle {
272
+ $schema: string;
273
+ generatedAt: string;
274
+ project: {
275
+ id: string;
276
+ rootLabel: string;
277
+ };
278
+ toolchain: {
279
+ verifierVersion: string | null;
280
+ };
281
+ privacy: {
282
+ localOnly: boolean;
283
+ redactedFields: string[];
284
+ screenshotsLocalOnly: boolean;
285
+ };
286
+ health: {
287
+ status: ProjectHealthStatus;
288
+ score: number;
289
+ errorCount: number;
290
+ warnCount: number;
291
+ infoCount: number;
292
+ findingCount: number;
293
+ };
294
+ provenance: {
295
+ essence: EvidenceProvenanceEntry;
296
+ packManifest: EvidenceProvenanceEntry;
297
+ reviewPack: EvidenceProvenanceEntry;
298
+ workspaceConfig?: EvidenceProvenanceEntry;
299
+ designTokens?: EvidenceProvenanceEntry;
300
+ };
301
+ assertions: ContractAssertion[];
302
+ findings: Array<{
303
+ id: string;
304
+ source: ProjectHealthFindingSource;
305
+ category: string;
306
+ severity: VerificationSeverity;
307
+ message: string;
308
+ evidence: string[];
309
+ target?: string;
310
+ rule?: string;
311
+ suggestedFix?: string;
312
+ remediationSummary: string;
313
+ commands: string[];
314
+ promptCommand: string;
315
+ }>;
316
+ browser?: {
317
+ enabled: boolean;
318
+ status: 'not_requested' | 'unavailable' | 'passed' | 'failed';
319
+ baseUrl: string | null;
320
+ screenshots: string[];
321
+ findings: string[];
322
+ };
323
+ designTokens?: {
324
+ source: string;
325
+ status: 'not_requested' | 'passed' | 'warning' | 'error';
326
+ compared: number;
327
+ matched: number;
328
+ missing: string[];
329
+ };
330
+ }
331
+ interface EvidenceBundleInput {
332
+ projectRoot: string;
333
+ report: ProjectHealthReport;
334
+ audit?: ProjectAuditReport | null;
335
+ assertions?: ContractAssertion[];
336
+ verifierVersion?: string | null;
337
+ workspaceConfigPath?: string | null;
338
+ designTokensPath?: string | null;
339
+ browser?: EvidenceBundle['browser'];
340
+ designTokens?: EvidenceBundle['designTokens'];
341
+ }
342
+ declare const EVIDENCE_BUNDLE_SCHEMA_URL = "https://decantr.ai/schemas/evidence-bundle.v1.json";
343
+ declare function createContractAssertions(projectRoot: string, audit?: ProjectAuditReport | null): ContractAssertion[];
344
+ declare function createEvidenceBundle(input: EvidenceBundleInput): EvidenceBundle;
251
345
  interface FileCritiqueReport {
252
346
  $schema: string;
253
347
  file: string;
@@ -362,4 +456,4 @@ declare function auditProject(projectRoot: string): Promise<ProjectAuditReport>;
362
456
  declare function critiqueSource({ filePath, code, reviewPack, packManifest, treatmentsCss, }: CritiqueSourceInput): FileCritiqueReport;
363
457
  declare function critiqueFile(filePath: string, projectRoot: string): Promise<FileCritiqueReport>;
364
458
 
365
- export { type BuiltDistAuditOptions, type CritiqueSourceInput, type FileCritiqueReport, INTERACTION_SIGNALS, type InteractionMissingFinding, type InteractionRequirement, type InteractionSignal, type PackManifest, type ProjectAuditReport, type ProjectHealthFinding, type ProjectHealthFindingSource, type ProjectHealthPackSummary, type ProjectHealthRemediation, type ProjectHealthReport, type ProjectHealthRouteSummary, type ProjectHealthStatus, type RuntimeAudit, type ShowcaseShortlistVerificationEntry, type ShowcaseShortlistVerificationReport, VERIFICATION_SCHEMA_URLS, type VerificationFinding, type VerificationScore, type VerificationSeverity, auditBuiltDist, auditProject, critiqueFile, critiqueSource, emptyRuntimeAudit, extractRouteHintsFromEssence, listKnownInteractions, verifyInteractionsInSource };
459
+ export { type BuiltDistAuditOptions, type ContractAssertion, type ContractAssertionCategory, type CritiqueSourceInput, EVIDENCE_BUNDLE_SCHEMA_URL, type EvidenceBundle, type EvidenceBundleInput, type EvidenceProvenanceEntry, type FileCritiqueReport, INTERACTION_SIGNALS, type InteractionMissingFinding, type InteractionRequirement, type InteractionSignal, type PackManifest, type ProjectAuditReport, type ProjectHealthFinding, type ProjectHealthFindingSource, type ProjectHealthPackSummary, type ProjectHealthRemediation, type ProjectHealthReport, type ProjectHealthRouteSummary, type ProjectHealthStatus, type RuntimeAudit, type ShowcaseShortlistVerificationEntry, type ShowcaseShortlistVerificationReport, VERIFICATION_SCHEMA_URLS, type VerificationFinding, type VerificationScore, type VerificationSeverity, auditBuiltDist, auditProject, createContractAssertions, createEvidenceBundle, critiqueFile, critiqueSource, emptyRuntimeAudit, extractRouteHintsFromEssence, listKnownInteractions, verifyInteractionsInSource };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/index.ts
2
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync } 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 { dirname, extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
5
+ import { basename, dirname, extname as extname2, isAbsolute, join as join2, relative, resolve } from "path";
5
6
  import { evaluateGuard, isV4, validateEssence } from "@decantr/essence-spec";
6
7
  import * as ts from "typescript";
7
8
 
@@ -730,9 +731,249 @@ var VERIFICATION_SCHEMA_URLS = {
730
731
  common: "https://decantr.ai/schemas/verification-report.common.v1.json",
731
732
  projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json",
732
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",
733
736
  fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
734
737
  showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
735
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
+ }
736
977
  var DEFAULT_FOCUS_AREAS = [
737
978
  "route-topology",
738
979
  "theme-consistency",
@@ -1463,7 +1704,13 @@ function guardViolationToFinding(violation) {
1463
1704
  }
1464
1705
  function countPages(essence) {
1465
1706
  if (!essence) return 0;
1466
- return essence.blueprint.sections.reduce((sum, section) => sum + section.pages.length, 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;
1467
1714
  }
1468
1715
  function makeFinding(input) {
1469
1716
  return input;
@@ -3884,6 +4131,23 @@ function isNavigationLandmark(tagName, attributes) {
3884
4131
  function hasNavigationLabel(attributes) {
3885
4132
  return Boolean(getJsxAttribute(attributes, "aria-label", "aria-labelledby", "title"));
3886
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
+ }
3887
4151
  function hasInsecureFormAction(attributes) {
3888
4152
  const actionValue = getJsxAttributeLiteralValue(getJsxAttribute(attributes, "action"));
3889
4153
  const normalized = actionValue?.trim().toLowerCase() ?? "";
@@ -9406,7 +9670,7 @@ function analyzeAstSignals(filePath, code) {
9406
9670
  signals.inlineStyleAttributeCount += 1;
9407
9671
  }
9408
9672
  }
9409
- if (isPropertyNamed(node.name, "dangerouslySetInnerHTML")) {
9673
+ if (isPropertyNamed(node.name, "dangerouslySetInnerHTML") && !isSafeJsonLdDangerouslySetInnerHtml(node)) {
9410
9674
  signals.dangerousHtmlCount += 1;
9411
9675
  }
9412
9676
  if (isPropertyNamed(node.name, "href", "to") && node.initializer && ts.isJsxExpression(node.initializer) && expressionContainsOpenRedirectSource(
@@ -11036,10 +11300,7 @@ function critiqueSource({
11036
11300
  })
11037
11301
  );
11038
11302
  }
11039
- const dangerousHtmlCount = Math.max(
11040
- astSignals.dangerousHtmlCount,
11041
- /dangerouslySetInnerHTML\s*=/.test(code) ? 1 : 0
11042
- );
11303
+ const dangerousHtmlCount = astSignals.dangerousHtmlCount;
11043
11304
  const rawHtmlInjectionCount = Math.max(
11044
11305
  astSignals.rawHtmlInjectionCount,
11045
11306
  /\binnerHTML\s*=|\binsertAdjacentHTML\s*\(|\bdocument\.write\s*\(/.test(code) ? 1 : 0
@@ -11666,10 +11927,13 @@ async function critiqueFile(filePath, projectRoot) {
11666
11927
  });
11667
11928
  }
11668
11929
  export {
11930
+ EVIDENCE_BUNDLE_SCHEMA_URL,
11669
11931
  INTERACTION_SIGNALS,
11670
11932
  VERIFICATION_SCHEMA_URLS,
11671
11933
  auditBuiltDist,
11672
11934
  auditProject,
11935
+ createContractAssertions,
11936
+ createEvidenceBundle,
11673
11937
  critiqueFile,
11674
11938
  critiqueSource,
11675
11939
  emptyRuntimeAudit,