@decantr/verifier 2.0.0 → 2.2.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 +14 -2
- package/dist/index.d.ts +103 -2
- package/dist/index.js +306 -18
- package/dist/index.js.map +1 -1
- package/package.json +15 -2
- package/schema/evidence-bundle.v1.json +197 -0
- package/schema/project-health-report.v1.json +11 -1
- package/schema/workspace-health-report.v1.json +140 -0
package/README.md
CHANGED
|
@@ -16,8 +16,11 @@ 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
|
-
-
|
|
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
|
|
23
|
+
- interaction findings now include scanned file counts, file line ranges, and expected signal evidence where available, so CLI health/check output can point agents at source-grounded remediation
|
|
21
24
|
- published verifier report schemas are exercised by AJV-backed round-trip tests against real audit, critique, and shortlist-report outputs
|
|
22
25
|
- project audits include runtime evidence when a built `dist/` output is present:
|
|
23
26
|
- root document
|
|
@@ -31,9 +34,16 @@ npm install @decantr/verifier
|
|
|
31
34
|
## Example
|
|
32
35
|
|
|
33
36
|
```ts
|
|
34
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
auditProject,
|
|
39
|
+
createContractAssertions,
|
|
40
|
+
createEvidenceBundle,
|
|
41
|
+
critiqueFile,
|
|
42
|
+
type ProjectHealthReport,
|
|
43
|
+
} from '@decantr/verifier';
|
|
35
44
|
|
|
36
45
|
const audit = await auditProject(process.cwd());
|
|
46
|
+
const assertions = createContractAssertions(process.cwd(), audit);
|
|
37
47
|
const critique = await critiqueFile('./src/pages/overview.tsx', process.cwd());
|
|
38
48
|
|
|
39
49
|
function isBlocking(report: ProjectHealthReport) {
|
|
@@ -46,6 +56,8 @@ function isBlocking(report: ProjectHealthReport) {
|
|
|
46
56
|
- `@decantr/verifier/schema/verification-report.common.v1.json`
|
|
47
57
|
- `@decantr/verifier/schema/project-audit-report.v1.json`
|
|
48
58
|
- `@decantr/verifier/schema/project-health-report.v1.json`
|
|
59
|
+
- `@decantr/verifier/schema/evidence-bundle.v1.json`
|
|
60
|
+
- `@decantr/verifier/schema/workspace-health-report.v1.json`
|
|
49
61
|
- `@decantr/verifier/schema/file-critique-report.v1.json`
|
|
50
62
|
- `@decantr/verifier/schema/showcase-shortlist-report.v1.json`
|
|
51
63
|
|
package/dist/index.d.ts
CHANGED
|
@@ -97,6 +97,13 @@ declare const INTERACTION_SIGNALS: Record<string, InteractionRequirement>;
|
|
|
97
97
|
interface InteractionMissingFinding {
|
|
98
98
|
interaction: string;
|
|
99
99
|
suggestion: string;
|
|
100
|
+
scannedFiles?: number;
|
|
101
|
+
scannedLocations?: Array<{
|
|
102
|
+
file: string;
|
|
103
|
+
startLine: number;
|
|
104
|
+
endLine: number;
|
|
105
|
+
}>;
|
|
106
|
+
expectedSignals?: string[];
|
|
100
107
|
}
|
|
101
108
|
/**
|
|
102
109
|
* Scan a source-tree map (file path → file contents) for evidence of each
|
|
@@ -119,12 +126,14 @@ declare const VERIFICATION_SCHEMA_URLS: {
|
|
|
119
126
|
readonly common: "https://decantr.ai/schemas/verification-report.common.v1.json";
|
|
120
127
|
readonly projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json";
|
|
121
128
|
readonly projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json";
|
|
129
|
+
readonly evidenceBundle: "https://decantr.ai/schemas/evidence-bundle.v1.json";
|
|
130
|
+
readonly workspaceHealth: "https://decantr.ai/schemas/workspace-health-report.v1.json";
|
|
122
131
|
readonly fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json";
|
|
123
132
|
readonly showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json";
|
|
124
133
|
};
|
|
125
134
|
type VerificationSeverity = 'error' | 'warn' | 'info';
|
|
126
135
|
type ProjectHealthStatus = 'healthy' | 'warning' | 'error';
|
|
127
|
-
type ProjectHealthFindingSource = 'audit' | 'check' | 'brownfield' | 'runtime' | 'pack' | 'interaction';
|
|
136
|
+
type ProjectHealthFindingSource = 'audit' | 'assertion' | 'browser' | 'check' | 'brownfield' | 'design-token' | 'runtime' | 'pack' | 'interaction';
|
|
128
137
|
interface VerificationFinding {
|
|
129
138
|
id: string;
|
|
130
139
|
category: string;
|
|
@@ -248,6 +257,98 @@ interface ProjectHealthReport {
|
|
|
248
257
|
};
|
|
249
258
|
findings: ProjectHealthFinding[];
|
|
250
259
|
}
|
|
260
|
+
type ContractAssertionCategory = 'route' | 'shell' | 'region' | 'interaction' | 'accessibility' | 'design-token' | 'context' | 'policy';
|
|
261
|
+
interface ContractAssertion {
|
|
262
|
+
id: string;
|
|
263
|
+
category: ContractAssertionCategory;
|
|
264
|
+
status: 'passed' | 'failed' | 'not_applicable';
|
|
265
|
+
severity: VerificationSeverity;
|
|
266
|
+
message: string;
|
|
267
|
+
evidence: string[];
|
|
268
|
+
target?: string;
|
|
269
|
+
rule: string;
|
|
270
|
+
suggestedFix?: string;
|
|
271
|
+
}
|
|
272
|
+
interface EvidenceProvenanceEntry {
|
|
273
|
+
path: string;
|
|
274
|
+
present: boolean;
|
|
275
|
+
hash: string | null;
|
|
276
|
+
generatedAt: string | null;
|
|
277
|
+
}
|
|
278
|
+
interface EvidenceBundle {
|
|
279
|
+
$schema: string;
|
|
280
|
+
generatedAt: string;
|
|
281
|
+
project: {
|
|
282
|
+
id: string;
|
|
283
|
+
rootLabel: string;
|
|
284
|
+
};
|
|
285
|
+
toolchain: {
|
|
286
|
+
verifierVersion: string | null;
|
|
287
|
+
};
|
|
288
|
+
privacy: {
|
|
289
|
+
localOnly: boolean;
|
|
290
|
+
redactedFields: string[];
|
|
291
|
+
screenshotsLocalOnly: boolean;
|
|
292
|
+
};
|
|
293
|
+
health: {
|
|
294
|
+
status: ProjectHealthStatus;
|
|
295
|
+
score: number;
|
|
296
|
+
errorCount: number;
|
|
297
|
+
warnCount: number;
|
|
298
|
+
infoCount: number;
|
|
299
|
+
findingCount: number;
|
|
300
|
+
};
|
|
301
|
+
provenance: {
|
|
302
|
+
essence: EvidenceProvenanceEntry;
|
|
303
|
+
packManifest: EvidenceProvenanceEntry;
|
|
304
|
+
reviewPack: EvidenceProvenanceEntry;
|
|
305
|
+
workspaceConfig?: EvidenceProvenanceEntry;
|
|
306
|
+
designTokens?: EvidenceProvenanceEntry;
|
|
307
|
+
};
|
|
308
|
+
assertions: ContractAssertion[];
|
|
309
|
+
findings: Array<{
|
|
310
|
+
id: string;
|
|
311
|
+
source: ProjectHealthFindingSource;
|
|
312
|
+
category: string;
|
|
313
|
+
severity: VerificationSeverity;
|
|
314
|
+
message: string;
|
|
315
|
+
evidence: string[];
|
|
316
|
+
target?: string;
|
|
317
|
+
rule?: string;
|
|
318
|
+
suggestedFix?: string;
|
|
319
|
+
remediationSummary: string;
|
|
320
|
+
commands: string[];
|
|
321
|
+
promptCommand: string;
|
|
322
|
+
}>;
|
|
323
|
+
browser?: {
|
|
324
|
+
enabled: boolean;
|
|
325
|
+
status: 'not_requested' | 'unavailable' | 'passed' | 'failed';
|
|
326
|
+
baseUrl: string | null;
|
|
327
|
+
screenshots: string[];
|
|
328
|
+
findings: string[];
|
|
329
|
+
};
|
|
330
|
+
designTokens?: {
|
|
331
|
+
source: string;
|
|
332
|
+
status: 'not_requested' | 'passed' | 'warning' | 'error';
|
|
333
|
+
compared: number;
|
|
334
|
+
matched: number;
|
|
335
|
+
missing: string[];
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
interface EvidenceBundleInput {
|
|
339
|
+
projectRoot: string;
|
|
340
|
+
report: ProjectHealthReport;
|
|
341
|
+
audit?: ProjectAuditReport | null;
|
|
342
|
+
assertions?: ContractAssertion[];
|
|
343
|
+
verifierVersion?: string | null;
|
|
344
|
+
workspaceConfigPath?: string | null;
|
|
345
|
+
designTokensPath?: string | null;
|
|
346
|
+
browser?: EvidenceBundle['browser'];
|
|
347
|
+
designTokens?: EvidenceBundle['designTokens'];
|
|
348
|
+
}
|
|
349
|
+
declare const EVIDENCE_BUNDLE_SCHEMA_URL = "https://decantr.ai/schemas/evidence-bundle.v1.json";
|
|
350
|
+
declare function createContractAssertions(projectRoot: string, audit?: ProjectAuditReport | null): ContractAssertion[];
|
|
351
|
+
declare function createEvidenceBundle(input: EvidenceBundleInput): EvidenceBundle;
|
|
251
352
|
interface FileCritiqueReport {
|
|
252
353
|
$schema: string;
|
|
253
354
|
file: string;
|
|
@@ -362,4 +463,4 @@ declare function auditProject(projectRoot: string): Promise<ProjectAuditReport>;
|
|
|
362
463
|
declare function critiqueSource({ filePath, code, reviewPack, packManifest, treatmentsCss, }: CritiqueSourceInput): FileCritiqueReport;
|
|
363
464
|
declare function critiqueFile(filePath: string, projectRoot: string): Promise<FileCritiqueReport>;
|
|
364
465
|
|
|
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 };
|
|
466
|
+
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 {
|
|
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
|
|
|
@@ -652,7 +653,7 @@ var INTERACTION_SIGNALS = {
|
|
|
652
653
|
},
|
|
653
654
|
"click-connect": {
|
|
654
655
|
interaction: "click-connect",
|
|
655
|
-
signals: [/onClick.*?port|connect/i, /connections?\s*[
|
|
656
|
+
signals: [/onClick.*?port|connect/i, /connections?\s*[:[=]/i],
|
|
656
657
|
suggestion: "Implement a two-click state machine: first click selects a port, second click on another port creates a connection. Store connections in component state."
|
|
657
658
|
},
|
|
658
659
|
"inline-edit": {
|
|
@@ -696,9 +697,11 @@ var INTERACTION_SIGNALS = {
|
|
|
696
697
|
suggestion: "Implement focus trap inside modal/dialog \u2014 listen for Tab key, cycle focus within the dialog, restore focus on close."
|
|
697
698
|
}
|
|
698
699
|
};
|
|
700
|
+
function sourceLineCount(source) {
|
|
701
|
+
return Math.max(1, source.replace(/\r?\n$/, "").split(/\r?\n/).length);
|
|
702
|
+
}
|
|
699
703
|
function verifyInteractionsInSource(interactions, sources) {
|
|
700
704
|
if (interactions.length === 0 || sources.size === 0) return [];
|
|
701
|
-
const combined = Array.from(sources.values()).join("\n\n");
|
|
702
705
|
const unique = Array.from(new Set(interactions));
|
|
703
706
|
const missing = [];
|
|
704
707
|
for (const interaction of unique) {
|
|
@@ -706,16 +709,30 @@ function verifyInteractionsInSource(interactions, sources) {
|
|
|
706
709
|
if (!requirement) {
|
|
707
710
|
continue;
|
|
708
711
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
712
|
+
let matched = false;
|
|
713
|
+
for (const source of sources.values()) {
|
|
714
|
+
matched = requirement.signals.some((signal) => {
|
|
715
|
+
if (typeof signal === "string") {
|
|
716
|
+
return source.includes(signal);
|
|
717
|
+
}
|
|
718
|
+
signal.lastIndex = 0;
|
|
719
|
+
return signal.test(source);
|
|
720
|
+
});
|
|
721
|
+
if (matched) break;
|
|
722
|
+
}
|
|
715
723
|
if (!matched) {
|
|
716
724
|
missing.push({
|
|
717
725
|
interaction,
|
|
718
|
-
suggestion: requirement.suggestion
|
|
726
|
+
suggestion: requirement.suggestion,
|
|
727
|
+
scannedFiles: sources.size,
|
|
728
|
+
scannedLocations: [...sources.entries()].slice(0, 8).map(([file, source]) => ({
|
|
729
|
+
file,
|
|
730
|
+
startLine: 1,
|
|
731
|
+
endLine: sourceLineCount(source)
|
|
732
|
+
})),
|
|
733
|
+
expectedSignals: requirement.signals.map(
|
|
734
|
+
(signal) => typeof signal === "string" ? signal : String(signal)
|
|
735
|
+
)
|
|
719
736
|
});
|
|
720
737
|
}
|
|
721
738
|
}
|
|
@@ -730,9 +747,249 @@ var VERIFICATION_SCHEMA_URLS = {
|
|
|
730
747
|
common: "https://decantr.ai/schemas/verification-report.common.v1.json",
|
|
731
748
|
projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json",
|
|
732
749
|
projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json",
|
|
750
|
+
evidenceBundle: "https://decantr.ai/schemas/evidence-bundle.v1.json",
|
|
751
|
+
workspaceHealth: "https://decantr.ai/schemas/workspace-health-report.v1.json",
|
|
733
752
|
fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
|
|
734
753
|
showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
|
|
735
754
|
};
|
|
755
|
+
var EVIDENCE_BUNDLE_SCHEMA_URL = "https://decantr.ai/schemas/evidence-bundle.v1.json";
|
|
756
|
+
function hashString(value) {
|
|
757
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
758
|
+
}
|
|
759
|
+
function hashFile(path) {
|
|
760
|
+
if (!existsSync2(path)) return null;
|
|
761
|
+
try {
|
|
762
|
+
return createHash("sha256").update(readFileSync2(path)).digest("hex");
|
|
763
|
+
} catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function provenanceEntry(projectRoot, relativePath) {
|
|
768
|
+
const path = join2(projectRoot, relativePath);
|
|
769
|
+
if (!existsSync2(path)) {
|
|
770
|
+
return {
|
|
771
|
+
path: relativePath,
|
|
772
|
+
present: false,
|
|
773
|
+
hash: null,
|
|
774
|
+
generatedAt: null
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
let generatedAt = null;
|
|
778
|
+
try {
|
|
779
|
+
generatedAt = statSync2(path).mtime.toISOString();
|
|
780
|
+
} catch {
|
|
781
|
+
generatedAt = null;
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
path: relativePath,
|
|
785
|
+
present: true,
|
|
786
|
+
hash: hashFile(path),
|
|
787
|
+
generatedAt
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function provenanceForPath(projectRoot, path) {
|
|
791
|
+
const rel = isAbsolute(path) ? relative(projectRoot, path).replace(/\\/g, "/") : path;
|
|
792
|
+
return provenanceEntry(projectRoot, rel && !rel.startsWith("..") ? rel : basename(path));
|
|
793
|
+
}
|
|
794
|
+
function redactEvidenceText(projectRoot, value) {
|
|
795
|
+
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
796
|
+
const normalized = value.replace(/\\/g, "/");
|
|
797
|
+
if (normalized.includes(normalizedRoot)) {
|
|
798
|
+
return normalized.split(normalizedRoot).join("<project>");
|
|
799
|
+
}
|
|
800
|
+
return normalized.replace(/\/Users\/[^/\s]+/g, "~").replace(/\/home\/[^/\s]+/g, "~");
|
|
801
|
+
}
|
|
802
|
+
function redactEvidenceList(projectRoot, evidence) {
|
|
803
|
+
return evidence.map((entry) => redactEvidenceText(projectRoot, entry));
|
|
804
|
+
}
|
|
805
|
+
function assertion(input) {
|
|
806
|
+
return {
|
|
807
|
+
status: "passed",
|
|
808
|
+
...input
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function createContractAssertions(projectRoot, audit) {
|
|
812
|
+
const assertions = [];
|
|
813
|
+
const essence = audit?.essence;
|
|
814
|
+
const v4 = essence && isV4(essence) ? essence : null;
|
|
815
|
+
const contextDir = join2(projectRoot, ".decantr", "context");
|
|
816
|
+
const packManifest = audit?.packManifest ?? null;
|
|
817
|
+
assertions.push(
|
|
818
|
+
assertion({
|
|
819
|
+
id: "contract.essence.present",
|
|
820
|
+
category: "context",
|
|
821
|
+
severity: "error",
|
|
822
|
+
rule: "essence-present",
|
|
823
|
+
status: essence ? "passed" : "failed",
|
|
824
|
+
message: essence ? "Decantr Essence contract is present." : "No Decantr Essence contract was found.",
|
|
825
|
+
evidence: [redactEvidenceText(projectRoot, join2(projectRoot, "decantr.essence.json"))],
|
|
826
|
+
suggestedFix: essence ? void 0 : "Run `decantr init` or restore decantr.essence.json."
|
|
827
|
+
})
|
|
828
|
+
);
|
|
829
|
+
assertions.push(
|
|
830
|
+
assertion({
|
|
831
|
+
id: "contract.essence.v4",
|
|
832
|
+
category: "context",
|
|
833
|
+
severity: "error",
|
|
834
|
+
rule: "essence-v4",
|
|
835
|
+
status: v4 ? "passed" : essence ? "failed" : "not_applicable",
|
|
836
|
+
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.",
|
|
837
|
+
evidence: [essence ? `version=${String(essence.version)}` : "essence missing"],
|
|
838
|
+
suggestedFix: v4 ? void 0 : "Run `decantr migrate --to v4` before reliability checks."
|
|
839
|
+
})
|
|
840
|
+
);
|
|
841
|
+
if (v4) {
|
|
842
|
+
const declaredRoutes = Object.keys(v4.blueprint.routes ?? {}).sort();
|
|
843
|
+
for (const route of declaredRoutes) {
|
|
844
|
+
const routeTarget = (v4.blueprint.routes ?? {})[route];
|
|
845
|
+
const section = v4.blueprint.sections.find((entry) => entry.id === routeTarget.section);
|
|
846
|
+
const page = section?.pages.find((entry) => entry.id === routeTarget.page);
|
|
847
|
+
assertions.push(
|
|
848
|
+
assertion({
|
|
849
|
+
id: `contract.route.${hashString(route)}`,
|
|
850
|
+
category: "route",
|
|
851
|
+
severity: page ? "info" : "warn",
|
|
852
|
+
rule: "declared-route-resolves",
|
|
853
|
+
status: page ? "passed" : "failed",
|
|
854
|
+
target: route,
|
|
855
|
+
message: page ? `Declared route ${route} resolves to a section page.` : `Declared route ${route} does not resolve to an existing section page.`,
|
|
856
|
+
evidence: [`section=${routeTarget.section}`, `page=${routeTarget.page}`],
|
|
857
|
+
suggestedFix: page ? void 0 : "Update blueprint.routes or add the missing section/page before running AI repair."
|
|
858
|
+
})
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
for (const section of v4.blueprint.sections) {
|
|
862
|
+
assertions.push(
|
|
863
|
+
assertion({
|
|
864
|
+
id: `contract.shell.${section.id}`,
|
|
865
|
+
category: "shell",
|
|
866
|
+
severity: section.shell && section.shell !== "inherit" ? "info" : "warn",
|
|
867
|
+
rule: "section-shell-concrete",
|
|
868
|
+
status: section.shell && section.shell !== "inherit" ? "passed" : "failed",
|
|
869
|
+
target: section.id,
|
|
870
|
+
message: section.shell && section.shell !== "inherit" ? `Section ${section.id} uses concrete shell ${section.shell}.` : `Section ${section.id} does not declare a concrete shell.`,
|
|
871
|
+
evidence: [`shell=${section.shell || "missing"}`],
|
|
872
|
+
suggestedFix: section.shell && section.shell !== "inherit" ? void 0 : "Resolve inherited shells during composition so AI agents get concrete layout intent."
|
|
873
|
+
})
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
assertions.push(
|
|
877
|
+
assertion({
|
|
878
|
+
id: "contract.accessibility.focus-visible",
|
|
879
|
+
category: "accessibility",
|
|
880
|
+
severity: v4.dna.accessibility.focus_visible ? "info" : "warn",
|
|
881
|
+
rule: "focus-visible-enabled",
|
|
882
|
+
status: v4.dna.accessibility.focus_visible ? "passed" : "failed",
|
|
883
|
+
message: v4.dna.accessibility.focus_visible ? "DNA requires visible focus states." : "DNA does not require visible focus states.",
|
|
884
|
+
evidence: [`focus_visible=${String(v4.dna.accessibility.focus_visible)}`],
|
|
885
|
+
suggestedFix: v4.dna.accessibility.focus_visible ? void 0 : "Set dna.accessibility.focus_visible=true for stronger UI reliability gates."
|
|
886
|
+
})
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
assertions.push(
|
|
890
|
+
assertion({
|
|
891
|
+
id: "contract.context.pack-manifest",
|
|
892
|
+
category: "context",
|
|
893
|
+
severity: packManifest ? "info" : "warn",
|
|
894
|
+
rule: "pack-manifest-present",
|
|
895
|
+
status: packManifest ? "passed" : "failed",
|
|
896
|
+
message: packManifest ? "Compiled execution pack manifest is present." : "Compiled execution pack manifest is missing.",
|
|
897
|
+
evidence: [redactEvidenceText(projectRoot, join2(contextDir, "pack-manifest.json"))],
|
|
898
|
+
suggestedFix: packManifest ? void 0 : "Run `decantr refresh` to regenerate context packs."
|
|
899
|
+
})
|
|
900
|
+
);
|
|
901
|
+
assertions.push(
|
|
902
|
+
assertion({
|
|
903
|
+
id: "contract.context.review-pack",
|
|
904
|
+
category: "context",
|
|
905
|
+
severity: audit?.reviewPack ? "info" : "warn",
|
|
906
|
+
rule: "review-pack-present",
|
|
907
|
+
status: audit?.reviewPack ? "passed" : "failed",
|
|
908
|
+
message: audit?.reviewPack ? "Compiled review pack is present." : "Compiled review pack is missing.",
|
|
909
|
+
evidence: [redactEvidenceText(projectRoot, join2(contextDir, "review-pack.json"))],
|
|
910
|
+
suggestedFix: audit?.reviewPack ? void 0 : "Run `decantr refresh` or hydrate the review pack from the registry."
|
|
911
|
+
})
|
|
912
|
+
);
|
|
913
|
+
const tokensPath = join2(projectRoot, "src", "styles", "tokens.css");
|
|
914
|
+
assertions.push(
|
|
915
|
+
assertion({
|
|
916
|
+
id: "contract.design-token.tokens-file",
|
|
917
|
+
category: "design-token",
|
|
918
|
+
severity: existsSync2(tokensPath) ? "info" : "warn",
|
|
919
|
+
rule: "tokens-file-present",
|
|
920
|
+
status: existsSync2(tokensPath) ? "passed" : "failed",
|
|
921
|
+
message: existsSync2(tokensPath) ? "Decantr token CSS file is present." : "Decantr token CSS file was not found.",
|
|
922
|
+
evidence: [redactEvidenceText(projectRoot, tokensPath)],
|
|
923
|
+
suggestedFix: existsSync2(tokensPath) ? void 0 : "Run `decantr refresh` or map Decantr tokens into the existing styling system."
|
|
924
|
+
})
|
|
925
|
+
);
|
|
926
|
+
return assertions;
|
|
927
|
+
}
|
|
928
|
+
function createEvidenceBundle(input) {
|
|
929
|
+
const assertions = input.assertions ?? createContractAssertions(input.projectRoot, input.audit);
|
|
930
|
+
let resolvedProjectRoot = input.projectRoot;
|
|
931
|
+
try {
|
|
932
|
+
resolvedProjectRoot = realpathSync(input.projectRoot);
|
|
933
|
+
} catch {
|
|
934
|
+
resolvedProjectRoot = resolve(input.projectRoot);
|
|
935
|
+
}
|
|
936
|
+
const projectId = `project_${hashString(resolvedProjectRoot)}`;
|
|
937
|
+
return {
|
|
938
|
+
$schema: EVIDENCE_BUNDLE_SCHEMA_URL,
|
|
939
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
940
|
+
project: {
|
|
941
|
+
id: projectId,
|
|
942
|
+
rootLabel: basename(input.projectRoot) || "project"
|
|
943
|
+
},
|
|
944
|
+
toolchain: {
|
|
945
|
+
verifierVersion: input.verifierVersion ?? null
|
|
946
|
+
},
|
|
947
|
+
privacy: {
|
|
948
|
+
localOnly: true,
|
|
949
|
+
redactedFields: [
|
|
950
|
+
"source",
|
|
951
|
+
"prompt",
|
|
952
|
+
"secret",
|
|
953
|
+
"environment",
|
|
954
|
+
"absolute_path",
|
|
955
|
+
"repository_name"
|
|
956
|
+
],
|
|
957
|
+
screenshotsLocalOnly: true
|
|
958
|
+
},
|
|
959
|
+
health: {
|
|
960
|
+
status: input.report.status,
|
|
961
|
+
score: input.report.score,
|
|
962
|
+
errorCount: input.report.summary.errorCount,
|
|
963
|
+
warnCount: input.report.summary.warnCount,
|
|
964
|
+
infoCount: input.report.summary.infoCount,
|
|
965
|
+
findingCount: input.report.summary.findingCount
|
|
966
|
+
},
|
|
967
|
+
provenance: {
|
|
968
|
+
essence: provenanceEntry(input.projectRoot, "decantr.essence.json"),
|
|
969
|
+
packManifest: provenanceEntry(input.projectRoot, ".decantr/context/pack-manifest.json"),
|
|
970
|
+
reviewPack: provenanceEntry(input.projectRoot, ".decantr/context/review-pack.json"),
|
|
971
|
+
...input.workspaceConfigPath ? { workspaceConfig: provenanceForPath(input.projectRoot, input.workspaceConfigPath) } : {},
|
|
972
|
+
...input.designTokensPath ? { designTokens: provenanceForPath(input.projectRoot, input.designTokensPath) } : {}
|
|
973
|
+
},
|
|
974
|
+
assertions,
|
|
975
|
+
findings: input.report.findings.map((finding) => ({
|
|
976
|
+
id: finding.id,
|
|
977
|
+
source: finding.source,
|
|
978
|
+
category: finding.category,
|
|
979
|
+
severity: finding.severity,
|
|
980
|
+
message: redactEvidenceText(input.projectRoot, finding.message),
|
|
981
|
+
evidence: redactEvidenceList(input.projectRoot, finding.evidence),
|
|
982
|
+
target: finding.target ? redactEvidenceText(input.projectRoot, finding.target) : void 0,
|
|
983
|
+
rule: finding.rule,
|
|
984
|
+
suggestedFix: finding.suggestedFix,
|
|
985
|
+
remediationSummary: finding.remediation.summary,
|
|
986
|
+
commands: finding.remediation.commands,
|
|
987
|
+
promptCommand: `decantr health --prompt ${finding.id}`
|
|
988
|
+
})),
|
|
989
|
+
...input.browser ? { browser: input.browser } : {},
|
|
990
|
+
...input.designTokens ? { designTokens: input.designTokens } : {}
|
|
991
|
+
};
|
|
992
|
+
}
|
|
736
993
|
var DEFAULT_FOCUS_AREAS = [
|
|
737
994
|
"route-topology",
|
|
738
995
|
"theme-consistency",
|
|
@@ -1448,6 +1705,11 @@ function buildRegistryContext(projectRoot) {
|
|
|
1448
1705
|
}
|
|
1449
1706
|
} catch {
|
|
1450
1707
|
}
|
|
1708
|
+
for (const id of ["content-section", "footer", "form-basic", "hero", "nav-header"]) {
|
|
1709
|
+
if (!patternRegistry.has(id)) {
|
|
1710
|
+
patternRegistry.set(id, { id, source: "bundled" });
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1451
1713
|
return { themeRegistry, patternRegistry };
|
|
1452
1714
|
}
|
|
1453
1715
|
function guardViolationToFinding(violation) {
|
|
@@ -1463,7 +1725,13 @@ function guardViolationToFinding(violation) {
|
|
|
1463
1725
|
}
|
|
1464
1726
|
function countPages(essence) {
|
|
1465
1727
|
if (!essence) return 0;
|
|
1466
|
-
|
|
1728
|
+
const blueprint = essence.blueprint;
|
|
1729
|
+
const sectionPages = Array.isArray(blueprint.sections) ? blueprint.sections.reduce(
|
|
1730
|
+
(sum, section) => sum + (Array.isArray(section.pages) ? section.pages.length : 0),
|
|
1731
|
+
0
|
|
1732
|
+
) : 0;
|
|
1733
|
+
const flatPages = Array.isArray(blueprint.pages) ? blueprint.pages.length : 0;
|
|
1734
|
+
return sectionPages + flatPages;
|
|
1467
1735
|
}
|
|
1468
1736
|
function makeFinding(input) {
|
|
1469
1737
|
return input;
|
|
@@ -2615,7 +2883,10 @@ function appendSourceAuditFindings(findings, sourceAudit, essence, reviewPack) {
|
|
|
2615
2883
|
})
|
|
2616
2884
|
);
|
|
2617
2885
|
}
|
|
2618
|
-
if (topology.hasAuthFeature && topology.primaryRoutes.length > 0 && sourceAudit.protectedSurfaceSignals.count > 0 && sourceAudit.authGuardSignals.count > 0 && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authGuardSignals) && !sourceAuditBucketsOverlap(
|
|
2886
|
+
if (topology.hasAuthFeature && topology.primaryRoutes.length > 0 && sourceAudit.protectedSurfaceSignals.count > 0 && sourceAudit.authGuardSignals.count > 0 && !sourceAuditBucketsOverlap(sourceAudit.protectedSurfaceSignals, sourceAudit.authGuardSignals) && !sourceAuditBucketsOverlap(
|
|
2887
|
+
sourceAudit.protectedSurfaceSignals,
|
|
2888
|
+
sourceAudit.authSessionSignals
|
|
2889
|
+
) && !sourceAuditProtectedSurfacesCoveredByGuardedNextLayouts(
|
|
2619
2890
|
sourceAudit.protectedSurfaceSignals,
|
|
2620
2891
|
sourceAudit.authGuardSignals,
|
|
2621
2892
|
topology
|
|
@@ -3884,6 +4155,23 @@ function isNavigationLandmark(tagName, attributes) {
|
|
|
3884
4155
|
function hasNavigationLabel(attributes) {
|
|
3885
4156
|
return Boolean(getJsxAttribute(attributes, "aria-label", "aria-labelledby", "title"));
|
|
3886
4157
|
}
|
|
4158
|
+
function getJsxAttributeOwner(attribute) {
|
|
4159
|
+
const attributes = attribute.parent;
|
|
4160
|
+
if (!attributes || !ts.isJsxAttributes(attributes)) return null;
|
|
4161
|
+
const owner = attributes.parent;
|
|
4162
|
+
if (ts.isJsxOpeningElement(owner) || ts.isJsxSelfClosingElement(owner)) {
|
|
4163
|
+
return owner;
|
|
4164
|
+
}
|
|
4165
|
+
return null;
|
|
4166
|
+
}
|
|
4167
|
+
function isSafeJsonLdDangerouslySetInnerHtml(attribute) {
|
|
4168
|
+
const owner = getJsxAttributeOwner(attribute);
|
|
4169
|
+
if (!owner || getJsxTagName(owner) !== "script") return false;
|
|
4170
|
+
const typeValue = getJsxAttributeLiteralValue(getJsxAttribute(owner.attributes, "type"))?.trim().toLowerCase();
|
|
4171
|
+
if (typeValue !== "application/ld+json") return false;
|
|
4172
|
+
const sourceText = attribute.getText();
|
|
4173
|
+
return /\bJSON\.stringify\s*\(/.test(sourceText) && /\\u003c/i.test(sourceText);
|
|
4174
|
+
}
|
|
3887
4175
|
function hasInsecureFormAction(attributes) {
|
|
3888
4176
|
const actionValue = getJsxAttributeLiteralValue(getJsxAttribute(attributes, "action"));
|
|
3889
4177
|
const normalized = actionValue?.trim().toLowerCase() ?? "";
|
|
@@ -9406,7 +9694,7 @@ function analyzeAstSignals(filePath, code) {
|
|
|
9406
9694
|
signals.inlineStyleAttributeCount += 1;
|
|
9407
9695
|
}
|
|
9408
9696
|
}
|
|
9409
|
-
if (isPropertyNamed(node.name, "dangerouslySetInnerHTML")) {
|
|
9697
|
+
if (isPropertyNamed(node.name, "dangerouslySetInnerHTML") && !isSafeJsonLdDangerouslySetInnerHtml(node)) {
|
|
9410
9698
|
signals.dangerousHtmlCount += 1;
|
|
9411
9699
|
}
|
|
9412
9700
|
if (isPropertyNamed(node.name, "href", "to") && node.initializer && ts.isJsxExpression(node.initializer) && expressionContainsOpenRedirectSource(
|
|
@@ -11036,10 +11324,7 @@ function critiqueSource({
|
|
|
11036
11324
|
})
|
|
11037
11325
|
);
|
|
11038
11326
|
}
|
|
11039
|
-
const dangerousHtmlCount =
|
|
11040
|
-
astSignals.dangerousHtmlCount,
|
|
11041
|
-
/dangerouslySetInnerHTML\s*=/.test(code) ? 1 : 0
|
|
11042
|
-
);
|
|
11327
|
+
const dangerousHtmlCount = astSignals.dangerousHtmlCount;
|
|
11043
11328
|
const rawHtmlInjectionCount = Math.max(
|
|
11044
11329
|
astSignals.rawHtmlInjectionCount,
|
|
11045
11330
|
/\binnerHTML\s*=|\binsertAdjacentHTML\s*\(|\bdocument\.write\s*\(/.test(code) ? 1 : 0
|
|
@@ -11666,10 +11951,13 @@ async function critiqueFile(filePath, projectRoot) {
|
|
|
11666
11951
|
});
|
|
11667
11952
|
}
|
|
11668
11953
|
export {
|
|
11954
|
+
EVIDENCE_BUNDLE_SCHEMA_URL,
|
|
11669
11955
|
INTERACTION_SIGNALS,
|
|
11670
11956
|
VERIFICATION_SCHEMA_URLS,
|
|
11671
11957
|
auditBuiltDist,
|
|
11672
11958
|
auditProject,
|
|
11959
|
+
createContractAssertions,
|
|
11960
|
+
createEvidenceBundle,
|
|
11673
11961
|
critiqueFile,
|
|
11674
11962
|
critiqueSource,
|
|
11675
11963
|
emptyRuntimeAudit,
|