@bounded-systems/conformance-kit 0.6.0 → 0.7.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/pairing-extractor.mjs +74 -3
- package/gates/token-a11y.mjs +12 -4
- package/package.json +1 -1
|
@@ -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.7.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",
|