@ksmv/ui-checks 0.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/color.js ADDED
@@ -0,0 +1,192 @@
1
+ import { converter, parse } from "culori";
2
+ import valueParser from "postcss-value-parser";
3
+ const toRgb = converter("rgb");
4
+ const unsupportedFunctions = new Set(["color-mix", "light-dark"]);
5
+ const literalColorFunctions = new Set([
6
+ "color",
7
+ "hsl",
8
+ "hsla",
9
+ "hwb",
10
+ "lab",
11
+ "lch",
12
+ "oklab",
13
+ "oklch",
14
+ "rgb",
15
+ "rgba",
16
+ ]);
17
+ export function isColorBearingProperty(property) {
18
+ const cssProperty = property
19
+ .replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
20
+ .toLowerCase();
21
+ return (cssProperty.startsWith("--") ||
22
+ cssProperty === "color" ||
23
+ cssProperty.endsWith("-color") ||
24
+ cssProperty === "background" ||
25
+ cssProperty === "background-image" ||
26
+ cssProperty === "border" ||
27
+ /^(?:border-(?:top|right|bottom|left|block|inline)(?:-(?:start|end))?)$/.test(cssProperty) ||
28
+ cssProperty === "border-image" ||
29
+ cssProperty === "border-image-source" ||
30
+ cssProperty === "outline" ||
31
+ cssProperty === "box-shadow" ||
32
+ cssProperty === "text-shadow" ||
33
+ cssProperty === "fill" ||
34
+ cssProperty === "stroke" ||
35
+ cssProperty === "filter" ||
36
+ cssProperty === "text-decoration" ||
37
+ cssProperty === "text-emphasis" ||
38
+ cssProperty === "column-rule");
39
+ }
40
+ function parsedColor(value) {
41
+ const parsed = parse(value);
42
+ const color = parsed ? toRgb(parsed) : undefined;
43
+ if (!color ||
44
+ !Number.isFinite(color.r) ||
45
+ !Number.isFinite(color.g) ||
46
+ !Number.isFinite(color.b) ||
47
+ (color.alpha !== undefined && !Number.isFinite(color.alpha))) {
48
+ return undefined;
49
+ }
50
+ return {
51
+ ...color,
52
+ alpha: color.alpha === undefined
53
+ ? undefined
54
+ : Math.min(1, Math.max(0, color.alpha)),
55
+ b: Math.min(1, Math.max(0, color.b)),
56
+ g: Math.min(1, Math.max(0, color.g)),
57
+ r: Math.min(1, Math.max(0, color.r)),
58
+ };
59
+ }
60
+ function stringifyNodes(nodes) {
61
+ return valueParser.stringify(nodes).trim();
62
+ }
63
+ function splitVarArguments(nodes) {
64
+ const comma = nodes.findIndex((node) => node.type === "div" && node.value === ",");
65
+ return {
66
+ fallback: comma < 0 ? undefined : stringifyNodes(nodes.slice(comma + 1)),
67
+ name: stringifyNodes(comma < 0 ? nodes : nodes.slice(0, comma)),
68
+ };
69
+ }
70
+ export function containsUnsupportedColorFunction(value) {
71
+ let unsupported = false;
72
+ valueParser(value).walk((node) => {
73
+ if (node.type === "function" &&
74
+ unsupportedFunctions.has(node.value.toLowerCase())) {
75
+ unsupported = true;
76
+ return false;
77
+ }
78
+ return undefined;
79
+ });
80
+ return unsupported;
81
+ }
82
+ export function containsLiteralColor(value) {
83
+ let literal = false;
84
+ valueParser(value).walk((node) => {
85
+ if (node.type === "function" && node.value.toLowerCase() !== "var") {
86
+ if (literalColorFunctions.has(node.value.toLowerCase()) ||
87
+ parsedColor(valueParser.stringify(node))) {
88
+ literal = true;
89
+ return false;
90
+ }
91
+ }
92
+ if (node.type === "word" &&
93
+ node.value.toLowerCase() !== "currentcolor" &&
94
+ node.value.toLowerCase() !== "transparent" &&
95
+ parsedColor(node.value)) {
96
+ literal = true;
97
+ return false;
98
+ }
99
+ return undefined;
100
+ });
101
+ return literal;
102
+ }
103
+ export function referencedTokens(value) {
104
+ const references = new Set();
105
+ valueParser(value).walk((node) => {
106
+ if (node.type === "function" && node.value === "var") {
107
+ const { name } = splitVarArguments(node.nodes);
108
+ if (name.startsWith("--")) {
109
+ references.add(name);
110
+ }
111
+ }
112
+ return undefined;
113
+ });
114
+ return [...references];
115
+ }
116
+ export function unresolvedTokenReferences(value, tokens, stack = new Set()) {
117
+ const unresolved = new Set();
118
+ valueParser(value).walk((node) => {
119
+ if (node.type !== "function" || node.value !== "var") {
120
+ return undefined;
121
+ }
122
+ const { fallback, name } = splitVarArguments(node.nodes);
123
+ if (!name.startsWith("--")) {
124
+ return false;
125
+ }
126
+ if (stack.has(name)) {
127
+ unresolved.add(name);
128
+ return false;
129
+ }
130
+ const declared = tokens.get(name);
131
+ if (declared !== undefined) {
132
+ for (const reference of unresolvedTokenReferences(declared, tokens, new Set([...stack, name]))) {
133
+ unresolved.add(reference);
134
+ }
135
+ }
136
+ else if (fallback !== undefined) {
137
+ for (const reference of unresolvedTokenReferences(fallback, tokens, new Set([...stack, name]))) {
138
+ unresolved.add(reference);
139
+ }
140
+ }
141
+ else {
142
+ unresolved.add(name);
143
+ }
144
+ return false;
145
+ });
146
+ return [...unresolved];
147
+ }
148
+ export function resolveColor(value, tokens, stack = new Set()) {
149
+ if (!value || containsUnsupportedColorFunction(value)) {
150
+ return { status: "uncovered" };
151
+ }
152
+ const parsedValue = valueParser(value.trim());
153
+ const meaningful = parsedValue.nodes.filter((node) => node.type !== "space" && node.type !== "comment");
154
+ if (meaningful.length === 1 &&
155
+ meaningful[0].type === "function" &&
156
+ meaningful[0].value === "var") {
157
+ const { fallback, name } = splitVarArguments(meaningful[0].nodes);
158
+ if (stack.has(name)) {
159
+ return { status: "uncovered" };
160
+ }
161
+ const next = tokens.get(name) ?? fallback;
162
+ return resolveColor(next, tokens, new Set([...stack, name]));
163
+ }
164
+ const color = parsedColor(value.trim());
165
+ return color ? { color, status: "resolved" } : { status: "uncovered" };
166
+ }
167
+ function composite(foreground, background) {
168
+ const alpha = foreground.alpha ?? 1;
169
+ return {
170
+ b: foreground.b * alpha + background.b * (1 - alpha),
171
+ g: foreground.g * alpha + background.g * (1 - alpha),
172
+ mode: "rgb",
173
+ r: foreground.r * alpha + background.r * (1 - alpha),
174
+ };
175
+ }
176
+ function luminance(color) {
177
+ const channel = (value) => value <= 0.04045
178
+ ? value / 12.92
179
+ : ((value + 0.055) / 1.055) ** 2.4;
180
+ return (0.2126 * channel(color.r) +
181
+ 0.7152 * channel(color.g) +
182
+ 0.0722 * channel(color.b));
183
+ }
184
+ export function contrastRatio(foreground, background) {
185
+ if ((background.alpha ?? 1) < 1) {
186
+ return undefined;
187
+ }
188
+ const composed = composite(foreground, background);
189
+ const [lighter, darker] = [luminance(composed), luminance(background)].sort((left, right) => right - left);
190
+ const ratio = (lighter + 0.05) / (darker + 0.05);
191
+ return Number.isFinite(ratio) ? ratio : undefined;
192
+ }
@@ -0,0 +1,23 @@
1
+ import type { ContractSource } from "./contracts.js";
2
+ import type { Suppression } from "./result.js";
3
+ export interface ComputedEvidenceRef {
4
+ file: string;
5
+ theme: string;
6
+ }
7
+ export interface ThemeConfig {
8
+ computedEvidence?: ComputedEvidenceRef;
9
+ files: string[];
10
+ name: string;
11
+ selector: string;
12
+ }
13
+ export interface ChecksConfig {
14
+ baseline?: string;
15
+ contractSource?: ContractSource;
16
+ exclude?: string[];
17
+ failOnUncovered?: boolean;
18
+ include: string[];
19
+ root?: string;
20
+ suppressions?: Suppression[];
21
+ themes?: ThemeConfig[];
22
+ }
23
+ export declare function defineConfig<const Config extends ChecksConfig>(config: Config): Config;
package/dist/config.js ADDED
@@ -0,0 +1,3 @@
1
+ export function defineConfig(config) {
2
+ return config;
3
+ }
@@ -0,0 +1,40 @@
1
+ export declare const tokenNamePattern: RegExp;
2
+ export type ContractSource = "bundled" | "@ksmv/ui-react";
3
+ export interface ThemeContract {
4
+ brandComposition: string[];
5
+ contractVersion: string;
6
+ derived: Record<string, string>;
7
+ prefix: "--ui-";
8
+ required: string[];
9
+ schemaVersion: 1;
10
+ }
11
+ export interface ContrastPair {
12
+ background: string;
13
+ context: string;
14
+ foreground: string;
15
+ minimum: number;
16
+ }
17
+ export interface ContrastManifest {
18
+ contractVersion: string;
19
+ pairs: ContrastPair[];
20
+ schemaVersion: 1;
21
+ }
22
+ export interface ContractIdentity {
23
+ contractVersion: string;
24
+ sha256: string;
25
+ }
26
+ export interface ContractSet {
27
+ contrast: ContrastManifest;
28
+ identities: {
29
+ contrast: ContractIdentity;
30
+ theme: ContractIdentity;
31
+ };
32
+ source: ContractSource;
33
+ theme: ThemeContract;
34
+ }
35
+ export declare function parseThemeContract(content: string): ThemeContract;
36
+ export declare function parseContrastManifest(content: string): ContrastManifest;
37
+ export declare function resolveContractSet({ root, source, }: {
38
+ root: string;
39
+ source: ContractSource;
40
+ }): Promise<ContractSet>;
@@ -0,0 +1,138 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { resolve } from "node:path";
5
+ export const tokenNamePattern = /^--ui-[a-z0-9]+(?:-[a-z0-9]+)*$/;
6
+ const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
7
+ function exactKeys(value, keys) {
8
+ return Object.keys(value).sort().join(",") === [...keys].sort().join(",");
9
+ }
10
+ function isRecord(value) {
11
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ function isTokenList(value) {
14
+ return Array.isArray(value) && value.every((token) => typeof token === "string" && tokenNamePattern.test(token));
15
+ }
16
+ export function parseThemeContract(content) {
17
+ let value;
18
+ try {
19
+ value = JSON.parse(content);
20
+ }
21
+ catch {
22
+ throw new Error("Unsupported theme contract.");
23
+ }
24
+ if (!isRecord(value) ||
25
+ !exactKeys(value, [
26
+ "brandComposition",
27
+ "contractVersion",
28
+ "derived",
29
+ "prefix",
30
+ "required",
31
+ "schemaVersion",
32
+ ]) ||
33
+ value.schemaVersion !== 1 ||
34
+ value.prefix !== "--ui-" ||
35
+ typeof value.contractVersion !== "string" ||
36
+ !semverPattern.test(value.contractVersion) ||
37
+ !isTokenList(value.required) ||
38
+ new Set(value.required).size !== value.required.length ||
39
+ !isTokenList(value.brandComposition) ||
40
+ new Set(value.brandComposition).size !== value.brandComposition.length ||
41
+ !isRecord(value.derived) ||
42
+ Object.entries(value.derived).some(([token, fallback]) => !tokenNamePattern.test(token) ||
43
+ typeof fallback !== "string" ||
44
+ !tokenNamePattern.test(fallback))) {
45
+ throw new Error("Unsupported theme contract.");
46
+ }
47
+ return value;
48
+ }
49
+ export function parseContrastManifest(content) {
50
+ let value;
51
+ try {
52
+ value = JSON.parse(content);
53
+ }
54
+ catch {
55
+ throw new Error("Unsupported contrast manifest.");
56
+ }
57
+ if (!isRecord(value) ||
58
+ !exactKeys(value, ["contractVersion", "pairs", "schemaVersion"]) ||
59
+ value.schemaVersion !== 1 ||
60
+ typeof value.contractVersion !== "string" ||
61
+ !semverPattern.test(value.contractVersion) ||
62
+ !Array.isArray(value.pairs)) {
63
+ throw new Error("Unsupported contrast manifest.");
64
+ }
65
+ const identities = [];
66
+ for (const pair of value.pairs) {
67
+ if (!isRecord(pair) ||
68
+ !exactKeys(pair, ["background", "context", "foreground", "minimum"]) ||
69
+ typeof pair.background !== "string" ||
70
+ !tokenNamePattern.test(pair.background) ||
71
+ typeof pair.foreground !== "string" ||
72
+ !tokenNamePattern.test(pair.foreground) ||
73
+ typeof pair.context !== "string" ||
74
+ pair.context.length === 0 ||
75
+ typeof pair.minimum !== "number" ||
76
+ !Number.isFinite(pair.minimum) ||
77
+ pair.minimum <= 0) {
78
+ throw new Error("Unsupported contrast manifest.");
79
+ }
80
+ identities.push(`${pair.context}\u0000${pair.foreground}\u0000${pair.background}`);
81
+ }
82
+ if (new Set(identities).size !== identities.length) {
83
+ throw new Error("Unsupported contrast manifest.");
84
+ }
85
+ return value;
86
+ }
87
+ function identity(content, contractVersion) {
88
+ return {
89
+ contractVersion,
90
+ sha256: `sha256:${createHash("sha256").update(content).digest("hex")}`,
91
+ };
92
+ }
93
+ async function reactContractPaths(root) {
94
+ const requireFromRoot = createRequire(resolve(root, "package.json"));
95
+ try {
96
+ return {
97
+ contrast: requireFromRoot.resolve("@ksmv/ui-react/contrast-contract.json"),
98
+ theme: requireFromRoot.resolve("@ksmv/ui-react/theme-contract.json"),
99
+ };
100
+ }
101
+ catch {
102
+ throw new Error("Unable to resolve the configured contract source.");
103
+ }
104
+ }
105
+ export async function resolveContractSet({ root, source, }) {
106
+ try {
107
+ const paths = source === "bundled"
108
+ ? {
109
+ contrast: new URL("./contrast-contract.json", import.meta.url),
110
+ theme: new URL("./theme-contract.json", import.meta.url),
111
+ }
112
+ : await reactContractPaths(root);
113
+ const [contrastContent, themeContent] = await Promise.all([
114
+ readFile(paths.contrast, "utf8"),
115
+ readFile(paths.theme, "utf8"),
116
+ ]);
117
+ const contrast = parseContrastManifest(contrastContent);
118
+ const theme = parseThemeContract(themeContent);
119
+ return {
120
+ contrast,
121
+ identities: {
122
+ contrast: identity(contrastContent, contrast.contractVersion),
123
+ theme: identity(themeContent, theme.contractVersion),
124
+ },
125
+ source,
126
+ theme,
127
+ };
128
+ }
129
+ catch (error) {
130
+ if (error instanceof Error &&
131
+ (error.message === "Unsupported theme contract." ||
132
+ error.message === "Unsupported contrast manifest." ||
133
+ error.message === "Unable to resolve the configured contract source.")) {
134
+ throw error;
135
+ }
136
+ throw new Error("Unable to resolve the configured contract source.");
137
+ }
138
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "contractVersion": "1.0.0",
3
+ "schemaVersion": 1,
4
+ "pairs": [
5
+ { "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg", "minimum": 4.5, "context": "normal-text" },
6
+ { "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg-hover", "minimum": 4.5, "context": "normal-text-hover" },
7
+ { "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg-active", "minimum": 4.5, "context": "normal-text-active" },
8
+ { "foreground": "--ui-action-secondary-text", "background": "--ui-action-secondary-bg", "minimum": 4.5, "context": "normal-text" },
9
+ { "foreground": "--ui-action-secondary-text", "background": "--ui-action-secondary-bg-hover", "minimum": 4.5, "context": "normal-text-hover" },
10
+ { "foreground": "--ui-action-secondary-text", "background": "--ui-action-secondary-bg-active", "minimum": 4.5, "context": "normal-text-active" },
11
+ { "foreground": "--ui-action-danger-text", "background": "--ui-action-danger-bg", "minimum": 4.5, "context": "normal-text" },
12
+ { "foreground": "--ui-action-danger-text", "background": "--ui-action-danger-bg-hover", "minimum": 4.5, "context": "normal-text-hover" },
13
+ { "foreground": "--ui-action-danger-text", "background": "--ui-action-danger-bg-active", "minimum": 4.5, "context": "normal-text-active" },
14
+ { "foreground": "--ui-action-ghost-text", "background": "--ui-surface", "minimum": 4.5, "context": "normal-text" },
15
+ { "foreground": "--ui-action-ghost-text", "background": "--ui-action-ghost-bg-hover", "minimum": 4.5, "context": "normal-text-hover" },
16
+ { "foreground": "--ui-action-ghost-text", "background": "--ui-action-ghost-bg-active", "minimum": 4.5, "context": "normal-text-active" },
17
+ { "foreground": "--ui-status-success-text", "background": "--ui-status-success-bg", "minimum": 4.5, "context": "status-text" },
18
+ { "foreground": "--ui-status-success", "background": "--ui-status-success-bg", "minimum": 3, "context": "status-indicator" },
19
+ { "foreground": "--ui-status-warning-text", "background": "--ui-status-warning-bg", "minimum": 4.5, "context": "status-text" },
20
+ { "foreground": "--ui-status-warning", "background": "--ui-status-warning-bg", "minimum": 3, "context": "status-indicator" },
21
+ { "foreground": "--ui-status-danger-text", "background": "--ui-status-danger-bg", "minimum": 4.5, "context": "status-text" },
22
+ { "foreground": "--ui-status-danger", "background": "--ui-status-danger-bg", "minimum": 3, "context": "status-indicator" },
23
+ { "foreground": "--ui-status-info-text", "background": "--ui-status-info-bg", "minimum": 4.5, "context": "status-text" },
24
+ { "foreground": "--ui-status-info", "background": "--ui-status-info-bg", "minimum": 3, "context": "status-indicator" },
25
+ { "foreground": "--ui-status-success", "background": "--ui-surface", "minimum": 3, "context": "status-indicator-on-surface" },
26
+ { "foreground": "--ui-status-warning", "background": "--ui-surface", "minimum": 3, "context": "status-indicator-on-surface" },
27
+ { "foreground": "--ui-status-danger", "background": "--ui-surface", "minimum": 3, "context": "status-indicator-on-surface" },
28
+ { "foreground": "--ui-status-info", "background": "--ui-surface", "minimum": 3, "context": "status-indicator-on-surface" },
29
+ { "foreground": "--ui-status-info", "background": "--ui-border", "minimum": 3, "context": "loading-indicator" },
30
+ { "foreground": "--ui-status-danger-text", "background": "--ui-surface", "minimum": 4.5, "context": "field-error-text" },
31
+ { "foreground": "--ui-status-danger", "background": "--ui-surface", "minimum": 3, "context": "invalid-control-boundary" },
32
+ { "foreground": "--ui-status-info", "background": "--ui-surface-subtle", "minimum": 3, "context": "progress-indicator" },
33
+ { "foreground": "--ui-text-inverse", "background": "--ui-surface-inverse", "minimum": 4.5, "context": "inverse-text" },
34
+ { "foreground": "--ui-border-control", "background": "--ui-surface", "minimum": 3, "context": "control-boundary" },
35
+ { "foreground": "--ui-focus-ring", "background": "--ui-surface", "minimum": 3, "context": "focus-indicator" },
36
+ { "foreground": "--ui-focus-ring", "background": "--ui-focus-ring-offset", "minimum": 3, "context": "focus-indicator" }
37
+ ]
38
+ }
@@ -0,0 +1,24 @@
1
+ import type { ThemeConfig } from "./config.js";
2
+ import { type ContractIdentity, type ContractSet } from "./contracts.js";
3
+ export interface ThemeEvidence {
4
+ capturedAt: string;
5
+ contracts: {
6
+ contrast: ContractIdentity;
7
+ theme: ContractIdentity;
8
+ };
9
+ schemaVersion: 1;
10
+ selectors: string[];
11
+ sourceHash: string;
12
+ themes: Record<string, {
13
+ selector: string;
14
+ tokens: Record<string, string>;
15
+ }>;
16
+ }
17
+ export declare function computeThemeSourceHash(root: string, files: string[]): Promise<string>;
18
+ export declare function loadComputedEvidence({ contracts, evidenceByPath, now, root, themes, }: {
19
+ contracts: ContractSet;
20
+ evidenceByPath?: Map<string, ThemeEvidence>;
21
+ now?: Date;
22
+ root: string;
23
+ themes: ThemeConfig[];
24
+ }): Promise<Record<string, Record<string, string>>>;
@@ -0,0 +1,132 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve } from "node:path";
4
+ import { tokenNamePattern, } from "./contracts.js";
5
+ function normalizedFiles(root, files) {
6
+ return [...new Set(files.map((file) => file.replaceAll("\\", "/")))]
7
+ .sort()
8
+ .map((file) => {
9
+ const absolute = resolve(root, file);
10
+ const fromRoot = relative(root, absolute).replaceAll("\\", "/");
11
+ if (isAbsolute(file) ||
12
+ fromRoot === ".." ||
13
+ fromRoot.startsWith("../")) {
14
+ throw new Error("Invalid computed theme evidence.");
15
+ }
16
+ return { absolute, path: fromRoot };
17
+ });
18
+ }
19
+ export async function computeThemeSourceHash(root, files) {
20
+ const hash = createHash("sha256");
21
+ for (const file of normalizedFiles(root, files)) {
22
+ hash.update(file.path);
23
+ hash.update("\0");
24
+ hash.update(await readFile(file.absolute));
25
+ hash.update("\0");
26
+ }
27
+ return `sha256:${hash.digest("hex")}`;
28
+ }
29
+ function exactKeys(value, keys) {
30
+ return Object.keys(value).sort().join(",") === [...keys].sort().join(",");
31
+ }
32
+ function isRecord(value) {
33
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
34
+ }
35
+ function sameIdentity(actual, expected) {
36
+ return isRecord(actual) &&
37
+ exactKeys(actual, ["contractVersion", "sha256"]) &&
38
+ actual.contractVersion === expected.contractVersion &&
39
+ actual.sha256 === expected.sha256;
40
+ }
41
+ function requiredEvidenceTokens(contracts) {
42
+ return new Set([
43
+ ...contracts.theme.required,
44
+ ...Object.keys(contracts.theme.derived),
45
+ ...Object.values(contracts.theme.derived),
46
+ ...contracts.contrast.pairs.flatMap(({ background, foreground }) => [
47
+ background,
48
+ foreground,
49
+ ]),
50
+ ]);
51
+ }
52
+ function validateEvidenceShape(value, contracts) {
53
+ if (!isRecord(value) ||
54
+ !exactKeys(value, [
55
+ "capturedAt",
56
+ "contracts",
57
+ "schemaVersion",
58
+ "selectors",
59
+ "sourceHash",
60
+ "themes",
61
+ ]) ||
62
+ value.schemaVersion !== 1 ||
63
+ typeof value.capturedAt !== "string" ||
64
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value.capturedAt) ||
65
+ typeof value.sourceHash !== "string" ||
66
+ !/^sha256:[a-f0-9]{64}$/.test(value.sourceHash) ||
67
+ !Array.isArray(value.selectors) ||
68
+ value.selectors.some((selector) => typeof selector !== "string") ||
69
+ !isRecord(value.contracts) ||
70
+ !exactKeys(value.contracts, ["contrast", "theme"]) ||
71
+ !sameIdentity(value.contracts.contrast, contracts.identities.contrast) ||
72
+ !sameIdentity(value.contracts.theme, contracts.identities.theme) ||
73
+ !isRecord(value.themes)) {
74
+ return false;
75
+ }
76
+ return Object.values(value.themes).every((theme) => isRecord(theme) &&
77
+ exactKeys(theme, ["selector", "tokens"]) &&
78
+ typeof theme.selector === "string" &&
79
+ isRecord(theme.tokens) &&
80
+ Object.entries(theme.tokens).every(([token, tokenValue]) => tokenNamePattern.test(token) &&
81
+ typeof tokenValue === "string" &&
82
+ tokenValue.trim().length > 0 &&
83
+ !/var\s*\(/i.test(tokenValue)));
84
+ }
85
+ export async function loadComputedEvidence({ contracts, evidenceByPath, now = new Date(), root, themes, }) {
86
+ try {
87
+ const configured = themes.filter((theme) => theme.computedEvidence);
88
+ const grouped = new Map();
89
+ for (const theme of configured) {
90
+ const path = theme.computedEvidence.file.replaceAll("\\", "/");
91
+ grouped.set(path, [...(grouped.get(path) ?? []), theme]);
92
+ }
93
+ const result = {};
94
+ const requiredTokens = requiredEvidenceTokens(contracts);
95
+ for (const [path, group] of grouped) {
96
+ const injected = evidenceByPath?.get(path);
97
+ const evidence = injected ?? JSON.parse(await readFile(resolve(root, path), "utf8"));
98
+ if (!validateEvidenceShape(evidence, contracts)) {
99
+ throw new Error("shape");
100
+ }
101
+ const capturedAt = new Date(evidence.capturedAt);
102
+ if (!Number.isFinite(capturedAt.valueOf()) || capturedAt > now) {
103
+ throw new Error("time");
104
+ }
105
+ const selectors = [...new Set(group.map(({ selector }) => selector))].sort();
106
+ if ([...new Set(evidence.selectors)].sort().join("\0") !== selectors.join("\0")) {
107
+ throw new Error("selectors");
108
+ }
109
+ const files = [...new Set(group.flatMap(({ files }) => files))];
110
+ if (evidence.sourceHash !== await computeThemeSourceHash(root, files)) {
111
+ throw new Error("source");
112
+ }
113
+ for (const theme of group) {
114
+ const name = theme.computedEvidence.theme;
115
+ const computedTheme = evidence.themes[name];
116
+ if (!computedTheme || computedTheme.selector !== theme.selector) {
117
+ throw new Error("theme");
118
+ }
119
+ for (const token of requiredTokens) {
120
+ if (computedTheme.tokens[token] === undefined) {
121
+ throw new Error("tokens");
122
+ }
123
+ }
124
+ result[theme.name] = { ...computedTheme.tokens };
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+ catch {
130
+ throw new Error("Invalid computed theme evidence.");
131
+ }
132
+ }
@@ -0,0 +1,9 @@
1
+ import type { ChecksConfig } from "./config.js";
2
+ import type { CheckReport, Violation } from "./result.js";
3
+ export declare function resolveReportExitCode(report: CheckReport, failOnUncovered: boolean): 0 | 1 | 2;
4
+ export declare function applyConfiguredSuppressions(report: CheckReport, config: ChecksConfig): {
5
+ issues: Violation[];
6
+ valid: boolean;
7
+ };
8
+ export declare function applyConfiguredBaseline(report: CheckReport, config: ChecksConfig, issues: Violation[]): Promise<void>;
9
+ export declare function applyDebtControls(report: CheckReport, config: ChecksConfig): Promise<void>;