Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ hardcodes `robertdelanghe.dev`, `bounded.tools`, an account, or an email.

```
integrity/ verify-site · verify (sigstore) · gen-sitemanifest · gen-provenance · structure-audit · http-probe
gates/ sbom (gen + completeness) · shacl-runner · seo-gate · axe-gate (axe-core a11y) · vuln-gate (npm audit) · html-validator-gate (vnu) · readability-gate · commonmark-runner · semantic (lone)
gates/ sbom (gen + completeness) · shacl-runner · seo-gate · axe-gate (axe-core a11y) · vuln-gate (npm audit) · html-validator-gate (vnu) · baseline-gate (web-features) · readability-gate · commonmark-runner · semantic (lone)
gates/conformance/ conformance-report — lone's conformance() projection (Node port of jsr:@bounded-systems/lone@0.4) + a generic HTML renderer
generators/ gen-cid (IPFS UnixFS) · gen-identity (did:web + VC) · openapi (static-API helper core)
emitters/ reprDigest (RFC 9530) · securityTxt (RFC 9116) · webManifest · markdown-sibling headers
Expand Down Expand Up @@ -69,6 +69,7 @@ in-process verifier). The Deno semantic runner pins its imports in
| `axe-gate.mjs` | `node …/axe-gate.mjs [distDir]` | `$DIST`. Optional `$AXE_PAGES` (comma list, default: every `*.html` in dist), `$AXE_TAGS` (default `wcag2a,wcag2aa,wcag21a,wcag21aa,wcag22aa`), `$AXE_IMPACT_THRESHOLD` (`minor`/`moderate`/`serious`/`critical`, default `serious`), `$AXE_RUNNER` (`playwright` (CI, needs `playwright` + `@axe-core/playwright` + `npx playwright install chromium`) \| `tezcatl` (macOS WebKit, local)), `$AXE_REPORT` (write the JSON report). Serves dist over an ephemeral origin (so assets resolve), runs **axe-core** per page, and **fails closed** on any violation at/above the threshold. The emitted report's `axe: { serious, critical }` envelope is exactly what `conformance-report`'s `a11y.axe-serious-critical` criterion consumes — a clean run is what lets a site honestly assert it. |
| `vuln-gate.mjs` | `node …/vuln-gate.mjs [projectDir]` | `$VULN_ROOT` (lockfile lives here, default `.`). Optional `$VULN_OMIT_DEV` (`true`→production deps only, default `true`), `$VULN_THRESHOLD` (highest tolerated known critical/high, default `0`), `$VULN_REPORT` (write the JSON report). Runs **`npm audit`** and **fails closed** when the known critical/high count exceeds the threshold. The report's `vulns: { knownCriticalOrHighVulns }` envelope is what `conformance-report`'s `security.no-critical-vulns` criterion consumes. |
| `html-validator-gate.mjs` | `node …/html-validator-gate.mjs [distDir]` | `$HTML_DIST`. Optional `$HTML_PAGES` (comma list, default: every `*.html`), `$HTML_THRESHOLD` (default `0`), `$HTML_REPORT`. Runs **vnu** (the Nu Html Checker, a self-contained Java jar — needs a JRE) `--errors-only` over the built pages and **fails closed** above the threshold. The report's `htmlValidator: { errors }` envelope is what `conformance-report`'s `html.validator-clean` criterion consumes. |
| `baseline-gate.mjs` | `node …/baseline-gate.mjs [cssGlob]` | `$BASELINE_CSS` (default `dist/**/*.css`). Optional `$BASELINE_TARGET` (`widely`/`newly`, default `widely`), `$BASELINE_REPORT`. Maps the shipped CSS to **web-features Baseline** data (via `stylelint-plugin-use-baseline` — headless, no browser) and **fails closed** when the site-wide status is below target. A feature behind an `@supports` query is a tested fallback and doesn't count against it. The report's `baseline: { status, fallbackTested }` envelope is what `conformance-report`'s `compatibility.baseline` criterion consumes. |
| `readability-gate.mjs` | `node …/readability-gate.mjs <corpus.json> [--strict]` | **The corpus is an input** the site assembles from its copy: a JSON array of `{id,text}` or an `{id:text}` map. Optional `$READABILITY_THRESHOLDS`, `$READABILITY_MIN_WORDS`, `$READABILITY_KNOWN_ACRONYMS`. WARN-only unless `--strict`. |
| `commonmark-runner.mjs` | `node …/commonmark-runner.mjs <renderer.mjs> [fixtures.json]` | **The site's markdown renderer module** (export `renderMarkdown`, or set `$COMMONMARK_RENDER_EXPORT`). Default fixtures pin a safe CommonMark subset + 4 hostile-HTML escapes; a site with a different renderer supplies its own `fixtures.json`. |
| `semantic/gate.ts` | `deno run --allow-read --allow-net …/gate.ts` | Built HTML in `$SEMANTIC_DIR` (default `dist/blog`); `$SEMANTIC_SELECTOR` (subject node, default `article`). Imports `jsr:@bounded-systems/lone`; any error-severity finding fails CI. |
Expand Down
3 changes: 3 additions & 0 deletions fixtures/baseline/bad.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* :has() is newly available; ::selection is limited — both below Widely Available. */
.card:has(img) { outline: 1px solid; }
::selection { background: rebeccapurple; }
3 changes: 3 additions & 0 deletions fixtures/baseline/good.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* Only Baseline Widely Available features. */
.card { display: flex; gap: 1rem; color: rebeccapurple; }
.card > p { margin: 0; }
112 changes: 112 additions & 0 deletions gates/baseline-gate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env node
// Baseline-availability gate — turns "our CSS is interoperable" into a
// CONTINUOUSLY-ENFORCED member of the conformance contract. It maps a project's
// SHIPPED CSS to web-features Baseline data (via stylelint-plugin-use-baseline —
// headless, no browser) and FAILS CLOSED (exit 1) when the site-wide status is
// below a configurable target (default: widely). The machine-readable result is
// exactly the shape lone's conformance() model consumes for `compatibility.baseline`
// (`{ status, fallbackTested }`), so a clean run lets a site honestly assert that
// criterion — and a regression to a newer/limited feature turns CI red.
//
// node gates/baseline-gate.mjs [cssGlob] # build gate (exit 1 below target)
//
// HONEST, NOT ASPIRATIONAL: the gate reports the MEASURED status whatever it is.
// The site-wide status is the WORST feature used (a feature guarded behind an
// `@supports` query is a tested fallback and does not count against it):
// • 0 features below "widely" -> "widely"
// • some below "widely" but none below "newly" -> "newly"
// • any feature below "newly" -> "limited"
//
// Config-driven; NOTHING about any one site is hard-coded:
// argv[2] / $BASELINE_CSS glob of CSS to scan (default: "dist/**/*.css")
// $BASELINE_TARGET lowest acceptable status (widely|newly, default: widely)
// $BASELINE_REPORT path to write the JSON report (default: none)
//
// The pure classify/threshold functions are exported for unit testing.
import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";

// ── Pure core (unit-testable) ────────────────────────────────────────────────

export const STATUS_ORDER = ["limited", "newly", "widely"]; // worst → best

/** Site-wide status from the two stylelint passes (counts of features below each bar). */
export function classify(belowWidely, belowNewly) {
if (belowWidely === 0) return "widely";
return belowNewly > 0 ? "limited" : "newly";
}

/** Whether `status` is at or above the `target` bar. */
export function meetsTarget(status, target) {
return STATUS_ORDER.indexOf(status) >= STATUS_ORDER.indexOf(target);
}

/** Build the report from a measured status. Pure: (status, target, offenders) → report. */
export function evaluateBaseline(status, target = "widely", offenders = []) {
return {
passed: meetsTarget(status, target),
target,
status,
offenders,
// The envelope lone's conformance() consumes for `compatibility.baseline`.
baseline: { status, fallbackTested: false },
};
}

// ── Impure runner (stylelint; deterministic, no browser/network) ─────────────

async function violationsAt(files, available) {
const stylelint = (await import("stylelint")).default;
const res = await stylelint.lint({
files,
config: {
plugins: ["stylelint-plugin-use-baseline"],
rules: { "plugin/use-baseline": [true, { available }] },
},
});
const feats = [];
for (const r of res.results) for (const w of r.warnings) feats.push(w.text.replace(/\s+plugin\/use-baseline$/, ""));
return feats;
}

/** Scan → classify → evaluate → report. Exposed for programmatic use and the test. */
export async function runBaselineGate({ css, target = "widely" }) {
const belowWidely = await violationsAt(css, "widely");
let status = "widely";
if (belowWidely.length > 0) {
const belowNewly = await violationsAt(css, "newly");
status = classify(belowWidely.length, belowNewly.length);
}
return evaluateBaseline(status, target, belowWidely);
}

// ── CLI ──────────────────────────────────────────────────────────────────────

async function main() {
const css = (process.argv[2] && !process.argv[2].startsWith("--") ? process.argv[2] : process.env.BASELINE_CSS || "dist/**/*.css");
const target = (process.env.BASELINE_TARGET || "widely").trim();
if (target !== "widely" && target !== "newly") {
console.error(`✗ baseline-gate: $BASELINE_TARGET must be "widely" or "newly" (got "${target}")`);
process.exit(2);
}

const report = await runBaselineGate({ css: resolve(process.cwd(), css), target });
if (process.env.BASELINE_REPORT) {
await writeFile(resolve(process.env.BASELINE_REPORT), JSON.stringify(report, null, 2) + "\n");
}

const line = `baseline-gate: shipped CSS is Baseline "${report.status}" (${report.offenders.length} feature(s) below widely) · target "${target}"`;
if (!report.passed) {
console.error(`✗ ${line}`);
for (const o of report.offenders) console.error(` · ${o}`);
console.error(` guard newer features behind an @supports feature query (the tested fallback), or lower $BASELINE_TARGET.`);
process.exit(1);
}
console.log(`✓ ${line}`);
for (const o of report.offenders) console.log(` · ${o}`);
}

// Only run the CLI when invoked directly (not when imported by a test).
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((e) => { console.error("✗ baseline-gate: error —", e.stack || e.message); process.exit(1); });
}
Loading
Loading