@bounded-systems/conformance-kit 0.4.0 → 0.5.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/README.md CHANGED
@@ -14,7 +14,7 @@ hardcodes `robertdelanghe.dev`, `bounded.tools`, an account, or an email.
14
14
  integrity/ verify-site · verify (sigstore) · gen-sitemanifest · gen-provenance · structure-audit · http-probe
15
15
  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) · jargon-gate (plain-language) · readability-gate · commonmark-runner · semantic (lone)
16
16
  gates/conformance/ conformance-report — lone's conformance() projection (Node port of jsr:@bounded-systems/lone@0.4) + a generic HTML renderer
17
- generators/ gen-cid (IPFS UnixFS) · gen-identity (did:web + VC) · gen-snapshots (reader/markdown) · openapi (static-API helper core)
17
+ generators/ gen-cid (IPFS UnixFS) · gen-identity (did:web + VC) · gen-snapshots (reader/markdown) · gen-print-snapshots (PDF) · openapi (static-API helper core)
18
18
  emitters/ reprDigest (RFC 9530) · securityTxt (RFC 9116) · webManifest · markdown-sibling headers
19
19
  lib/ schema-validate (zero-dep JSON Schema) · config (env/arg helpers)
20
20
  fixtures/ test/ isolated verification of the generic logic
@@ -98,6 +98,7 @@ criteria are reported + summarised per area but never widen the headline claim.
98
98
  | `gen-cid.mjs` | `DIST=dist node …/gen-cid.mjs` | `$DIST`. Walks the `site.sha256` file set (or `dist`), computes the IPFS UnixFS dir CIDv1 with no daemon, records it into `$DIST/provenance.json`. |
99
99
  | `gen-identity.mjs` | `IDENTITY_DOMAIN=… IDENTITY_REPO=owner/repo node …/gen-identity.mjs` | `$IDENTITY_DOMAIN`, `$IDENTITY_REPO` (cert-identity regexp), `$IDENTITY_SUBJECT` (the credentialSubject JSON, default `$DIST/resume.json`), optional `$IDENTITY_SUBJECT_SCHEMA`, `$IDENTITY_VC_NAME/DESCRIPTION`, `$IDENTITY_VALID_FROM_PATH`. Emits `did.json` + a W3C VC 2.0. |
100
100
  | `gen-snapshots.mjs` | `node …/gen-snapshots.mjs [distDir]` | `$SNAPSHOT_DIST` (default `dist`). Optional `$SNAPSHOT_PAGES`, `$SNAPSHOT_BASE_URL` (recorded as `source` in the front-matter), `$SNAPSHOT_SUFFIX` (default `.reader`). For every built page, runs **@mozilla/readability** (the Firefox/Safari Reader engine, via `linkedom` — headless, no browser) and writes a clean reader **`<page>.reader.html`** + an analysis-friendly **`<page>.reader.md`** (YAML front-matter + Markdown via `turndown`). The Markdown is the durable, diffable twin of the page — far easier to run NLP/LLM analysis over than scraping live HTML — and doubles as the AI-readable Markdown sibling. (The printed/PDF view needs a print-CSS renderer and is a separate generator.) |
101
+ | `gen-print-snapshots.mjs` | `node …/gen-print-snapshots.mjs [distDir]` | `$PRINT_DIST` (default `dist`). Optional `$PRINT_PAGES`, `$PRINT_RENDERER` (default `tezcatl`, or a `"cmd {url} {out}"` template), `$PRINT_WAIT` (default `600`), `$PRINT_SUFFIX` (default `.print`). The print/PDF twin of `gen-snapshots`: serves `dist` over an ephemeral origin (so assets resolve) and renders each page's `@media print` view to **`<page>.print.pdf`** via **tezcatl** (macOS-native WebKit — no Chromium). A LOCAL / macOS-deploy artifact: on a host without the renderer (e.g. a Linux CI runner) it **SKIPS** with a note. |
101
102
  | `openapi.mjs` | `import { sortKeys, writeApiFile, embedSchema, jsonResponse, validateOpenapi }` | The **generic core** of a static-API generator. The per-endpoint projection of a site's contracts (profile/posts/corpus/VC, etc.) stays in the site's build; this module provides deterministic JSON output, schema embedding, and OpenAPI 3.1/3.2 well-formedness validation. Pair with `lib/schema-validate.mjs` to self-check emitted docs. |
102
103
 
103
104
  ### emitters/
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // Printed-view snapshot generator — the print/PDF twin of gen-snapshots (which does
3
+ // the headless reader/Markdown view). For every built page it renders the page's
4
+ // `@media print` view to a durable `<page>.print.pdf`, so the printed artifact is
5
+ // archived + content-addressable alongside the served bytes.
6
+ //
7
+ // Unlike the reader view, a PDF needs a real print-CSS renderer. The default is
8
+ // `tezcatl` (macOS-native WebKit, no Chromium download) — the same engine the kit's
9
+ // axe gate uses locally. So this generator is a LOCAL / macOS-deploy artifact: on a
10
+ // host without the renderer (e.g. a Linux CI runner) it SKIPS with a clear note.
11
+ //
12
+ // node generators/gen-print-snapshots.mjs [distDir]
13
+ //
14
+ // Config-driven; NOTHING about any one site is hard-coded:
15
+ // argv / $PRINT_DIST built output dir (default: "dist")
16
+ // $PRINT_PAGES comma list of page paths under dist (default: every *.html)
17
+ // $PRINT_RENDERER renderer command (default: "tezcatl")
18
+ // $PRINT_WAIT ms to let JS/layout settle (default: 600)
19
+ // $PRINT_SUFFIX output basename suffix (default: ".print")
20
+ //
21
+ // The pure path/arg functions are exported for unit testing without a renderer.
22
+ import { readdir, access, stat } from "node:fs/promises";
23
+ import { readFileSync } from "node:fs";
24
+ import { createServer } from "node:http";
25
+ import { spawn, spawnSync } from "node:child_process";
26
+ import { resolve, join, relative, dirname, basename, extname } from "node:path";
27
+
28
+ // ── Pure core (renderer-free; unit-testable) ─────────────────────────────────
29
+
30
+ const MIME = {
31
+ ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
32
+ ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
33
+ ".jpg": "image/jpeg", ".webp": "image/webp", ".woff2": "font/woff2",
34
+ ".woff": "font/woff", ".ico": "image/x-icon", ".txt": "text/plain",
35
+ };
36
+ export const mimeFor = (p) => MIME[extname(p).toLowerCase()] || "application/octet-stream";
37
+
38
+ /** Output PDF path for a built page: dist/blog/x.html → dist/blog/x.print.pdf. */
39
+ export function pdfOutPath(file, suffix = ".print") {
40
+ return join(dirname(file), basename(file, extname(file)) + suffix + ".pdf");
41
+ }
42
+
43
+ /** The renderer command + args. Pure: (renderer, url, out, wait) → [cmd, args]. */
44
+ export function rendererCommand(renderer, url, out, wait = 600) {
45
+ if (renderer === "tezcatl") return ["tezcatl", [url, `--pdf=${out}`, `--wait=${wait}`]];
46
+ // A custom $PRINT_RENDERER is a command template: "cmd {url} {out}".
47
+ const parts = renderer.split(/\s+/).map((t) => t.replace("{url}", url).replace("{out}", out).replace("{wait}", String(wait)));
48
+ return [parts[0], parts.slice(1)];
49
+ }
50
+
51
+ // ── Impure: static origin + renderer ─────────────────────────────────────────
52
+
53
+ /** Serve `root` over an ephemeral localhost origin so absolute asset paths resolve. */
54
+ function startServer(root) {
55
+ return new Promise((res) => {
56
+ const server = createServer(async (req, res2) => {
57
+ try {
58
+ let p = join(root, decodeURIComponent((req.url || "/").split("?")[0]));
59
+ try { if ((await stat(p)).isDirectory()) p = join(p, "index.html"); } catch { p = join(root, "404.html"); }
60
+ res2.writeHead(200, { "content-type": mimeFor(p) });
61
+ res2.end(readFileSync(p));
62
+ } catch { res2.writeHead(404); res2.end(); }
63
+ });
64
+ server.listen(0, "127.0.0.1", () => res({ origin: `http://127.0.0.1:${server.address().port}`, close: () => server.close() }));
65
+ });
66
+ }
67
+
68
+ function render(renderer, url, out, wait) {
69
+ const [cmd, args] = rendererCommand(renderer, url, out, wait);
70
+ return new Promise((res, rej) => {
71
+ const ch = spawn(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
72
+ let err = "";
73
+ ch.stderr.on("data", (d) => (err += d));
74
+ ch.on("error", (e) => rej(new Error(`renderer "${cmd}" not runnable (on PATH?): ${e.message}`)));
75
+ ch.on("close", (code) => (code === 0 ? res() : rej(new Error(`renderer "${cmd}" exit ${code}: ${err.trim().slice(0, 200)}`))));
76
+ });
77
+ }
78
+
79
+ async function walkHtml(dir) {
80
+ const out = [];
81
+ for (const e of await readdir(dir, { withFileTypes: true })) {
82
+ const p = join(dir, e.name);
83
+ if (e.isDirectory()) out.push(...await walkHtml(p));
84
+ else if (e.name.endsWith(".html")) out.push(p);
85
+ }
86
+ return out;
87
+ }
88
+
89
+ /** Render each page → PDF. Exposed for programmatic use. */
90
+ export async function genPrintSnapshots({ dist, pages, renderer = "tezcatl", wait = 600, suffix = ".print" }) {
91
+ const distAbs = resolve(dist);
92
+ const files = pages && pages.length ? pages.map((p) => resolve(distAbs, p)) : (await walkHtml(distAbs)).sort();
93
+ const { origin, close } = await startServer(distAbs);
94
+ const written = [];
95
+ try {
96
+ for (const file of files) {
97
+ const rel = relative(distAbs, file);
98
+ const out = pdfOutPath(file, suffix);
99
+ await render(renderer, `${origin}/${rel}`, out, wait);
100
+ written.push(relative(distAbs, out));
101
+ }
102
+ } finally { close(); }
103
+ return written;
104
+ }
105
+
106
+ // ── CLI ──────────────────────────────────────────────────────────────────────
107
+
108
+ async function main() {
109
+ const distArg = process.argv.slice(2).find((a) => !a.startsWith("--"));
110
+ const dist = resolve(distArg || process.env.PRINT_DIST || "dist");
111
+ const exists = async (p) => { try { await access(p); return true; } catch { return false; } };
112
+ if (!(await exists(dist))) { console.error(`✗ gen-print-snapshots: ${dist} not found — build first.`); process.exit(2); }
113
+
114
+ const renderer = (process.env.PRINT_RENDERER || "tezcatl").trim();
115
+ const wait = Number.parseInt(process.env.PRINT_WAIT ?? "600", 10);
116
+ const suffix = process.env.PRINT_SUFFIX || ".print";
117
+ const pages = (process.env.PRINT_PAGES || "").split(",").map((s) => s.trim().replace(/^\//, "")).filter(Boolean);
118
+
119
+ // Renderer present? If not, SKIP (this is a local/macOS-deploy artifact, not CI).
120
+ const cmd0 = rendererCommand(renderer, "", "", wait)[0];
121
+ if (spawnSync(cmd0, ["--help"], { stdio: "ignore" }).error) {
122
+ console.log(`✓ gen-print-snapshots: renderer "${cmd0}" not on PATH — SKIPPED (run on a host with it, e.g. macOS + tezcatl).`);
123
+ return;
124
+ }
125
+
126
+ const written = await genPrintSnapshots({ dist, pages, renderer, wait, suffix });
127
+ for (const w of written) console.log(` ✓ ${w}`);
128
+ console.log(`✓ gen-print-snapshots: ${written.length} printed PDF snapshot(s) via ${cmd0}.`);
129
+ }
130
+
131
+ if (import.meta.url === `file://${process.argv[1]}`) {
132
+ main().catch((e) => { console.error("✗ gen-print-snapshots: error —", e.stack || e.message); process.exit(1); });
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bounded-systems/conformance-kit",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",
@@ -28,7 +28,8 @@
28
28
  "ck-commonmark-runner": "gates/commonmark-runner.mjs",
29
29
  "ck-gen-cid": "generators/gen-cid.mjs",
30
30
  "ck-gen-identity": "generators/gen-identity.mjs",
31
- "ck-gen-snapshots": "generators/gen-snapshots.mjs"
31
+ "ck-gen-snapshots": "generators/gen-snapshots.mjs",
32
+ "ck-gen-print-snapshots": "generators/gen-print-snapshots.mjs"
32
33
  },
33
34
  "scripts": {
34
35
  "test": "node test/run.mjs"