@bounded-systems/conformance-kit 0.6.0 → 0.8.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/gates/likeness-gate.mjs +15 -5
- package/gates/pairing-extractor.mjs +74 -3
- package/gates/token-a11y.mjs +12 -4
- package/package.json +1 -1
package/gates/likeness-gate.mjs
CHANGED
|
@@ -99,8 +99,16 @@ export function evaluateLikeness({ tokenMap = {}, groups = [], thresholds = DEFA
|
|
|
99
99
|
});
|
|
100
100
|
const collapseCount = categorical.reduce((n, c) => n + c.count, 0);
|
|
101
101
|
|
|
102
|
+
// Identical-value pairs (ΔE 0) are intentional ALIASES — two semantic tokens
|
|
103
|
+
// deliberately mapped to one value (e.g. `card`/`white` → one #FFFFFF). The single
|
|
104
|
+
// value is defined once; the alias is by design, NOT redundancy. Only NEAR-but-
|
|
105
|
+
// DISTINCT pairs (0 < ΔE < dupDeltaE) are real redundancy: two almost-the-same
|
|
106
|
+
// colours defined separately, which probably should be one.
|
|
107
|
+
const aliasPairs = near.duplicates.filter((d) => d.identical).length;
|
|
108
|
+
const redundantTokens = near.count - aliasPairs;
|
|
109
|
+
|
|
102
110
|
const dupIsError = t.dupSeverity === "error";
|
|
103
|
-
const errors = (dupIsError ?
|
|
111
|
+
const errors = (dupIsError ? redundantTokens : 0) + collapseCount;
|
|
104
112
|
|
|
105
113
|
return {
|
|
106
114
|
passed: errors === 0,
|
|
@@ -108,7 +116,8 @@ export function evaluateLikeness({ tokenMap = {}, groups = [], thresholds = DEFA
|
|
|
108
116
|
summary: {
|
|
109
117
|
tokens: Object.keys(tokenMap).length,
|
|
110
118
|
nearDuplicates: near.count,
|
|
111
|
-
identicalPairs:
|
|
119
|
+
identicalPairs: aliasPairs,
|
|
120
|
+
redundantTokens,
|
|
112
121
|
categoricalGroups: categorical.length,
|
|
113
122
|
categoricalCollapses: collapseCount,
|
|
114
123
|
},
|
|
@@ -117,7 +126,8 @@ export function evaluateLikeness({ tokenMap = {}, groups = [], thresholds = DEFA
|
|
|
117
126
|
// Envelope a future lone `likeness.*` criterion can consume.
|
|
118
127
|
likeness: {
|
|
119
128
|
distinctCategoricals: collapseCount === 0,
|
|
120
|
-
|
|
129
|
+
// Intentional identical-value aliases don't count — only near-but-distinct dups.
|
|
130
|
+
noRedundantTokens: redundantTokens === 0,
|
|
121
131
|
},
|
|
122
132
|
};
|
|
123
133
|
}
|
|
@@ -156,8 +166,8 @@ async function main() {
|
|
|
156
166
|
if (process.env.LIKENESS_REPORT) await writeFile(resolve(process.env.LIKENESS_REPORT), JSON.stringify(report, null, 2) + "\n");
|
|
157
167
|
|
|
158
168
|
const s = report.summary;
|
|
159
|
-
const line = `likeness-gate: ${s.tokens} token(s) — ${s.
|
|
160
|
-
const dupLine = (d) => ` · ${d.identical ? "identical" : "near-dup"}: ${d.a} ≈ ${d.b} (${d.aHex}/${d.bHex}) ΔE ${d.deltaE}`;
|
|
169
|
+
const line = `likeness-gate: ${s.tokens} token(s) — ${s.redundantTokens} redundant near-dup(s) + ${s.identicalPairs} intentional alias(es), ${s.categoricalCollapses} categorical collapse(s)`;
|
|
170
|
+
const dupLine = (d) => ` · ${d.identical ? "alias (identical, ok)" : "near-dup"}: ${d.a} ≈ ${d.b} (${d.aHex}/${d.bHex}) ΔE ${d.deltaE}`;
|
|
161
171
|
const colLine = (c) => ` · collapse: ${c.a} vs ${c.b} (${c.aHex}/${c.bHex}) under ${c.condition} — ΔE ${c.deltaE} < ${c.min}`;
|
|
162
172
|
if (!report.passed) {
|
|
163
173
|
console.error(`✗ ${line}`);
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
//
|
|
38
38
|
// INPUTS / ENV:
|
|
39
39
|
// $PAIRING_DECLARED optional pairings.json to UNION in (declared ∪ extracted).
|
|
40
|
+
// --allowlist | $PAIRING_ALLOWLIST=1 CLOSED-WORLD: `declared` is the opt-in
|
|
41
|
+
// allowlist — every extracted pairing must be declared (else an
|
|
42
|
+
// `undeclared` violation) and every declared pairing must pass.
|
|
40
43
|
// $PAIRING_MATRIX write the Markdown matrix here (else stdout).
|
|
41
44
|
// $PAIRING_REPORT write the full JSON report here.
|
|
42
45
|
// $PAIRING_GATE "1" → also run the palette gate over the union and exit 1 on
|
|
@@ -197,13 +200,68 @@ export function renderMatrixMarkdown(matrix) {
|
|
|
197
200
|
// 4. Full run
|
|
198
201
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
199
202
|
|
|
200
|
-
/**
|
|
201
|
-
|
|
203
|
+
/** Resolve a declared spec's pairings to {fg,bg,fgHex,bgHex,kind,name,key}. */
|
|
204
|
+
function resolveDeclared(map, spec) {
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const d of spec.pairings || []) {
|
|
207
|
+
try {
|
|
208
|
+
const fgHex = resolveColor(map, d.fg), bgHex = resolveColor(map, d.bg), kind = d.kind || "text";
|
|
209
|
+
const key = `${toHex(parseHex(fgHex)).toLowerCase()}|${toHex(parseHex(bgHex)).toLowerCase()}|${kind}`;
|
|
210
|
+
out.push({ fg: d.fg, bg: d.bg, fgHex, bgHex, kind, name: d.name, key, source: "declared", confidence: "declared" });
|
|
211
|
+
} catch { /* unresolvable token → skip */ }
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
const pairKey = (p) => `${toHex(parseHex(p.fgHex)).toLowerCase()}|${toHex(parseHex(p.bgHex)).toLowerCase()}|${p.kind}`;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Load tokens + stylesheets → extract ∪ declared → matrix + palette evaluation.
|
|
219
|
+
*
|
|
220
|
+
* `allowlist:true` switches to CLOSED-WORLD: the `declared` set is the opt-in
|
|
221
|
+
* allowlist and the gate enforces BOTH directions —
|
|
222
|
+
* 1. every DECLARED pairing must pass contrast, and
|
|
223
|
+
* 2. every pairing the CSS actually produces must be DECLARED; any extracted
|
|
224
|
+
* pairing absent from the allowlist is an `undeclared` violation.
|
|
225
|
+
* The palette envelope is evaluated over the declared (vetted) set, so surface
|
|
226
|
+
* mis-extractions can't poison it — they surface as `undeclared` to either
|
|
227
|
+
* declare (and pass) or fix in the CSS.
|
|
228
|
+
*/
|
|
229
|
+
export async function runPairingExtractor({ tokens, css = [], declared = null, defaultBackground = null, allowlist = false }) {
|
|
202
230
|
const map = typeof tokens === "string" ? await loadTokens(tokens) : tokens;
|
|
203
231
|
const cssTexts = await Promise.all((Array.isArray(css) ? css : [css]).map((c) => (c.includes("{") ? c : readFile(c, "utf8"))));
|
|
204
232
|
const rules = cssTexts.flatMap((t) => parseRules(t));
|
|
205
233
|
const ext = extractPairings(rules, map, { defaultBackground });
|
|
206
234
|
|
|
235
|
+
if (allowlist) {
|
|
236
|
+
const spec = declared
|
|
237
|
+
? (typeof declared === "string" ? JSON.parse(await readFile(declared, "utf8")) : declared)
|
|
238
|
+
: { pairings: [] };
|
|
239
|
+
const declaredPairs = resolveDeclared(map, spec);
|
|
240
|
+
const allowed = new Set(declaredPairs.map((p) => p.key));
|
|
241
|
+
const undeclared = ext.pairings.filter((p) => !allowed.has(pairKey(p)));
|
|
242
|
+
const matrix = buildMatrix(declaredPairs);
|
|
243
|
+
const palette = evaluatePalette({ pairings: declaredPairs.map((p) => ({ ...p })) });
|
|
244
|
+
const failingDeclared = matrix.filter((m) => !m.passed);
|
|
245
|
+
return {
|
|
246
|
+
passed: palette.passed && undeclared.length === 0,
|
|
247
|
+
mode: "allowlist",
|
|
248
|
+
summary: {
|
|
249
|
+
declared: declaredPairs.length,
|
|
250
|
+
extracted: ext.pairings.length,
|
|
251
|
+
undeclared: undeclared.length,
|
|
252
|
+
failingDeclared: failingDeclared.length,
|
|
253
|
+
surfaces: ext.surfaces.length,
|
|
254
|
+
total: declaredPairs.length,
|
|
255
|
+
failing: failingDeclared.length + undeclared.length,
|
|
256
|
+
},
|
|
257
|
+
undeclared,
|
|
258
|
+
defaultSurface: ext.defaultSurface,
|
|
259
|
+
pairings: declaredPairs,
|
|
260
|
+
matrix,
|
|
261
|
+
palette,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
207
265
|
// Union declared pairings (resolved) in.
|
|
208
266
|
const extractedCount = ext.pairings.length;
|
|
209
267
|
let union = ext.pairings;
|
|
@@ -245,7 +303,8 @@ async function main() {
|
|
|
245
303
|
console.error("✗ pairing-extractor: usage: pairing-extractor.mjs <tokens.(json|css)> <style1.css> [style2.css …]");
|
|
246
304
|
process.exit(2);
|
|
247
305
|
}
|
|
248
|
-
const
|
|
306
|
+
const allowlist = process.argv.includes("--allowlist") || process.env.PAIRING_ALLOWLIST === "1";
|
|
307
|
+
const report = await runPairingExtractor({ tokens, css, declared: process.env.PAIRING_DECLARED || null, allowlist });
|
|
249
308
|
if (process.env.PAIRING_REPORT) await writeFile(resolve(process.env.PAIRING_REPORT), JSON.stringify(report, null, 2) + "\n");
|
|
250
309
|
|
|
251
310
|
const md = renderMatrixMarkdown(report.matrix);
|
|
@@ -253,6 +312,18 @@ async function main() {
|
|
|
253
312
|
else console.log(md);
|
|
254
313
|
|
|
255
314
|
const s = report.summary;
|
|
315
|
+
if (allowlist) {
|
|
316
|
+
// Closed-world: fail on undeclared OR failing declared pairings.
|
|
317
|
+
const line = `pairing-extractor [allowlist]: ${s.declared} declared, ${s.undeclared} undeclared, ${s.failingDeclared} failing declared`;
|
|
318
|
+
if (!report.passed) {
|
|
319
|
+
console.error(`✗ ${line}`);
|
|
320
|
+
for (const u of report.undeclared) console.error(` · UNDECLARED: ${u.fg}/${u.bg} [${u.kind}] — declare it or fix the CSS`);
|
|
321
|
+
for (const m of report.matrix) if (!m.passed) console.error(` · FAILS: ${m.fg}/${m.bg} [${m.kind}] WCAG ${m.wcag}:1`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
console.error(`✓ ${line}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
256
327
|
const line = `pairing-extractor: ${s.total} pair(s) (${s.extracted} extracted + ${s.declaredAdded} declared) — ${s.failing} failing`;
|
|
257
328
|
if (process.env.PAIRING_GATE === "1" && !report.passed) {
|
|
258
329
|
console.error(`✗ ${line}`);
|
package/gates/token-a11y.mjs
CHANGED
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
// CONFIG (every member is OPTIONAL — only declared members run):
|
|
18
18
|
// { "tokens": "brand/tokens/tokens.css", // default token map (path or map)
|
|
19
19
|
// "palette": { "pairings":[…], "categorical":[…], "thresholds":{…} } | "pairings.json",
|
|
20
|
-
// "pairing": { "css":["a.css","b.css"], "declared":"pairings.json", "gate":true
|
|
20
|
+
// "pairing": { "css":["a.css","b.css"], "declared":"pairings.json", "gate":true,
|
|
21
|
+
// "allowlist":true }, // closed-world: `declared` is the opt-in
|
|
22
|
+
// // allowlist; any pairing the CSS produces that ISN'T declared is a
|
|
23
|
+
// // violation, and declared pairings must pass. Palette envelope is
|
|
24
|
+
// // evaluated over the vetted declared set (no surface false-positives).
|
|
21
25
|
// "typography":{ "tokens":"…", "body":["body"], "thresholds":{…} } | "typo.json",
|
|
22
26
|
// "targetSize":{ "targets":[…], "thresholds":{…} } | "targets.json",
|
|
23
27
|
// "opacity": { "usages":[…], "opacityTokens":{…} } | "usages.json",
|
|
@@ -60,9 +64,11 @@ export async function runTokenA11y(config, base = ".") {
|
|
|
60
64
|
const r = await runPairingExtractor({
|
|
61
65
|
tokens: tokens(m), css: (m.css || []).map((c) => rel(base, c)),
|
|
62
66
|
declared: m.declared ? rel(base, m.declared) : null,
|
|
67
|
+
allowlist: m.allowlist || false,
|
|
63
68
|
});
|
|
64
|
-
//
|
|
65
|
-
|
|
69
|
+
// Allowlist (closed-world) gates by default — undeclared pairings are
|
|
70
|
+
// violations. Extraction-only mode is report-only unless `gate:true`.
|
|
71
|
+
return (m.allowlist || m.gate) ? r : { ...r, passed: true, gated: false };
|
|
66
72
|
});
|
|
67
73
|
}
|
|
68
74
|
if (config.typography) {
|
|
@@ -97,7 +103,9 @@ function memberLine(name, m) {
|
|
|
97
103
|
const s = m.summary || {};
|
|
98
104
|
const tail =
|
|
99
105
|
name === "palette" ? `${s.pairs} pair(s), ${s.failingPairs} failing, ${s.categoricalCollapses} collapse(s)`
|
|
100
|
-
: name === "pairing" ?
|
|
106
|
+
: name === "pairing" ? (m.mode === "allowlist"
|
|
107
|
+
? `${s.declared} declared, ${s.undeclared} undeclared, ${s.failingDeclared} failing`
|
|
108
|
+
: `${s.total} pair(s), ${s.failing} failing${m.gated === false ? " (report-only)" : ""}`)
|
|
101
109
|
: name === "typography" ? `${s.styles} style(s), ${s.errors} error(s), ${s.warnings} warn(s)`
|
|
102
110
|
: name === "targetSize" ? `${s.targets} target(s), ${s.belowAA} below AA${m.coverage === "none" ? " (none declared)" : ""}`
|
|
103
111
|
: name === "opacity" ? `${s.usages} usage(s), ${s.failing} failing`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bounded-systems/conformance-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Standalone, site-agnostic web-conformance toolkit: integrity tooling + build gates + provenance generators, all parameterized so a site vendors one kit instead of duplicating scripts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|