@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.
@@ -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
- /** Load tokens + stylesheets → extract ∪ declared matrix + palette evaluation. */
201
- export async function runPairingExtractor({ tokens, css = [], declared = null, defaultBackground = null }) {
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 report = await runPairingExtractor({ tokens, css, declared: process.env.PAIRING_DECLARED || null });
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}`);
@@ -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
- // Report-only unless `gate:true`.
65
- return m.gate ? r : { ...r, passed: true, gated: false };
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" ? `${s.total} pair(s), ${s.failing} failing${m.gated === false ? " (report-only)" : ""}`
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.6.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",