From afc57ddb0ca4d3dab07ed7a2778707ecbc84a530 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 28 Aug 2026 23:44:47 -0400 Subject: [PATCH 1/2] fix(build): generate the license surface and make build-metrics staleness fail (TIN-161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TIN-161, the "ship truthful performance/license surfaces" half. Two static pages in this repo described the build and were allowed to go on describing a build that no longer existed, because nothing checked them. **`THIRD-PARTY-LICENSES.md` was hand-maintained and had drifted badly.** As of `origin/main` it listed `shiki 3.22.0` against a resolution of 4.2.0, `vite 6.4.1` against 8.0.14, `svelte 5.50.1` against 5.55.4, `sharp 0.33.5` against 0.34.5, carried `mermaid` as a RUNTIME dependency it has not been for some time, and published 18 of 42 direct dependencies. It is now generated. `scripts/truth-surfaces.mjs` derives every row from `package-lock.json`: the package and its declared range from `package.json`, the resolved version and the `license` string from the lockfile entry for that exact resolution. The npm lockfile is the right source here because it is current with `package.json` and the pnpm resolutions AND carries a per-package license field, which `pnpm-lock.yaml` does not. Regenerate with `node scripts/truth-surfaces.mjs --write`. A package whose lockfile entry declares no license prints as `UNDECLARED` and is never guessed — five resolved packages are in that state and the document now names them instead of quietly implying MIT. The page also publishes the license histogram for the full 1,150-package resolved tree. **`docs/build-metrics.md` was a February snapshot titled "Baseline".** `main` had already added a prose "Superseded" note (#264); this makes the staleness machine-checked instead of remembered. The page now declares a `Status`, a capture date, and the toolchain it was captured against — `vite ^6.4.1`, `svelte ^5.19.0`, `@sveltejs/kit ^2.16.0`, read from `9aac474:package.json`, the commit that added the file, so the comparison is declared-range against declared-range rather than range against resolution. The check fails in BOTH directions: a toolchain bump without a recapture must carry STALE, and a page that carries STALE once the toolchain matches again must drop it. No permanent warning that nobody reads. `scripts/truth-surfaces.test.mts` covers it, and half the cases exist to prove the guard can go red — a check only ever observed passing is an unfailable assertion. `BUILD.bazel`'s `vitest_srcs` and `scripts/run-vitest-bazel.mjs` gain the two documents so they reach the Vitest sandbox next to `package-lock.json`. Verification (plain node 22; `node_modules` is not installed on this machine so vitest itself was not run here — the test's fixtures and assertions were ported 1:1 to a harness and executed): $ node scripts/truth-surfaces.mjs truth surfaces OK 12/12 harness cases pass, including: a lockfile version bump with a stale document goes red; an undeclared license renders UNDECLARED and NOT MIT; a moved toolchain without STALE fails; an unjustified STALE marker fails; a metrics page that will not name its toolchain fails; and the real tree publishes a row for all 42 direct dependencies with none unresolved. **Out of scope here, and why.** The other TIN-161 bullets — mermaid prerender, `sharp`-unreadable images, Pagefind skipping `/` and `/consultancy.html`, and the large-chunk/post-import warnings — all need a real build to triage honestly, and this machine cannot run one. Two of them look to have moved since the audit and want re-derivation rather than a fix: `puppeteer` is now a direct devDependency (`^24.43.1`), `scripts/render-mermaid.mts` carries an explicit "Cannot find package 'puppeteer'" degradation path, and there is a `puppeteer_chromium_smoke` bazel target. Stated as observations, not as a closure. CI pending. --- BUILD.bazel | 5 + THIRD-PARTY-LICENSES.md | 160 ++++++++++++++------ docs/build-metrics.md | 37 ++++- scripts/run-vitest-bazel.mjs | 4 + scripts/truth-surfaces.mjs | 256 ++++++++++++++++++++++++++++++++ scripts/truth-surfaces.test.mts | 203 +++++++++++++++++++++++++ 6 files changed, 615 insertions(+), 50 deletions(-) create mode 100644 scripts/truth-surfaces.mjs create mode 100644 scripts/truth-surfaces.test.mts diff --git a/BUILD.bazel b/BUILD.bazel index 9992fed9..c0e1a9ea 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -21,6 +21,11 @@ filegroup( allow_empty = True, ) + [ ".npmrc", + # Truth surfaces checked by scripts/truth-surfaces.test.mts (TIN-161): + # the test compares these documents against package-lock.json, so they + # have to reach the Vitest sandbox alongside it. + "THIRD-PARTY-LICENSES.md", + "docs/build-metrics.md", "package-lock.json", "package.json", "pnpm-lock.yaml", diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md index 38454497..9e1ffc25 100644 --- a/THIRD-PARTY-LICENSES.md +++ b/THIRD-PARTY-LICENSES.md @@ -1,55 +1,123 @@ # Third-Party Licenses -This project (transscendsurvival.org) is dedicated to the public domain -under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). + -The site is built with open-source software. Below is an attribution of -all direct dependencies and their licenses. +This project (transscendsurvival.org) is dedicated to the public domain under +[CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). + +Every figure below is derived from `package-lock.json` — the package name and +its declared range from `package.json`, the resolved version and the license +string from the lockfile entry for that exact resolution. Nothing here is +hand-entered, so nothing here can drift without `scripts/truth-surfaces.test.mts` +failing. + +Regenerate with: + +```sh +node scripts/truth-surfaces.mjs --write +``` + +A dependency whose lockfile entry declares no license is printed as +`UNDECLARED`. It is never guessed from the package name, the organisation, or +a sibling version. + +The previous revision of this file was hand-maintained and had drifted: it +listed `shiki 3.22.0`, `vite 6.4.1`, `svelte 5.50.1` and `sharp 0.33.5` +against resolutions of 4.2.0, 8.0.14, 5.55.4 and 0.34.5, carried `mermaid` as +a runtime dependency it no longer is, and omitted most of the direct set. ## Runtime Dependencies -| Package | Version | License | Repository | -|---------|---------|---------|------------| -| @skeletonlabs/skeleton | 5.0.0 | MIT | https://github.com/skeletonlabs/skeleton | -| @skeletonlabs/skeleton-svelte | 5.0.0 | MIT | https://github.com/skeletonlabs/skeleton | -| mdsvex | 0.12.6 | MIT | https://github.com/pngwn/MDsveX | -| mermaid | 11.12.2 | MIT | https://github.com/mermaid-js/mermaid | -| shiki | 3.22.0 | MIT | https://github.com/shikijs/shiki | -| tailwindcss | 4.1.18 | MIT | https://github.com/tailwindlabs/tailwindcss | +| Package | Declared | Resolved | License | +| --- | --- | --- | --- | +| @fontsource/crimson-pro | `^5.2.8` | 5.2.8 | OFL-1.1 | +| @fontsource/fira-code | `^5.2.7` | 5.2.7 | OFL-1.1 | +| @fontsource/inter | `^5.2.8` | 5.2.8 | OFL-1.1 | +| @fontsource/raleway | `^5.2.8` | 5.2.8 | OFL-1.1 | +| @skeletonlabs/skeleton | `5.0.0` | 5.0.0 | MIT | +| @skeletonlabs/skeleton-svelte | `5.0.0` | 5.0.0 | MIT | +| @tummycrypt/tinyvectors | `https://github.com/tinyland-inc/tinyvectors/archive/refs/tags/v0.3.5.tar.gz` | 0.3.5 | Zlib | +| dompurify | `^3.4.13` | 3.4.13 | (MPL-2.0 OR Apache-2.0) | +| flexsearch | `^0.8.212` | 0.8.212 | Apache-2.0 | +| marked | `^4.3.0` | 4.3.0 | MIT | +| mdsvex | `^0.12.7` | 0.12.7 | MIT | +| shiki | `^4.2.0` | 4.2.0 | MIT | +| tailwindcss | `^4.2.2` | 4.2.2 | MIT | ## Build / Dev Dependencies -| Package | Version | License | Repository | -|---------|---------|---------|------------| -| @sveltejs/adapter-static | 3.0.10 | MIT | https://github.com/sveltejs/kit | -| @sveltejs/enhanced-img | 0.10.0 | MIT | https://github.com/sveltejs/kit | -| @sveltejs/kit | 2.50.2 | MIT | https://github.com/sveltejs/kit | -| @sveltejs/vite-plugin-svelte | 6.2.4 | MIT | https://github.com/sveltejs/vite-plugin-svelte | -| @tailwindcss/vite | 4.1.18 | MIT | https://github.com/tailwindlabs/tailwindcss | -| pagefind | 1.4.0 | MIT | https://github.com/CloudCannon/pagefind | -| sharp | 0.33.5 | Apache-2.0 | https://github.com/lovell/sharp | -| svelte | 5.50.1 | MIT | https://github.com/sveltejs/svelte | -| svelte-check | 4.3.6 | MIT | https://github.com/sveltejs/language-tools | -| typescript | 5.9.3 | Apache-2.0 | https://github.com/microsoft/TypeScript | -| vite | 6.4.1 | MIT | https://github.com/vitejs/vite | - -## Notable Transitive Dependencies - -| Package | License | Notes | -|---------|---------|-------| -| chevrotain | Apache-2.0 | Parser toolkit (mermaid dependency) | -| d3 | ISC / BSD-3-Clause | Visualization library (mermaid dependency) | -| dompurify | MPL-2.0 OR Apache-2.0 | HTML sanitizer (mermaid dependency) | -| @iconify/utils | MIT | Icon utilities (skeleton dependency) | -| @floating-ui/dom | MIT | Positioning runtime (Skeleton/Zag dependency) | - -## License Summary - -All 243 production packages break down as: - -- **MIT**: 190 -- **ISC**: 34 -- **Apache-2.0**: 8 -- **BSD-3-Clause**: 6 -- **MPL-2.0 OR Apache-2.0**: 1 -- **Unlicense**: 1 +| Package | Declared | Resolved | License | +| --- | --- | --- | --- | +| @eslint/js | `^9.39.2` | 9.39.4 | MIT | +| @mermaid-js/mermaid-cli | `^11.15.0` | 11.15.0 | MIT | +| @playwright/test | `^1.59.1` | 1.59.1 | Apache-2.0 | +| @sveltejs/adapter-node | `^5.5.7` | 5.5.7 | MIT | +| @sveltejs/adapter-static | `^3.0.10` | 3.0.10 | MIT | +| @sveltejs/kit | `^2.61.1` | 2.61.1 | MIT | +| @sveltejs/vite-plugin-svelte | `^7.1.2` | 7.1.2 | MIT | +| @tailwindcss/vite | `^4.2.2` | 4.2.2 | MIT | +| @tummycrypt/vite-plugin-a11y | `^0.2.2` | 0.2.2 | Zlib AND LicenseRef-Tinyland-Proprietary | +| @types/node | `^25.2.3` | 25.5.0 | MIT | +| @vitest/coverage-v8 | `^4.1.4` | 4.1.4 | MIT | +| eslint | `^9.39.2` | 9.39.4 | MIT | +| eslint-config-prettier | `^10.1.8` | 10.1.8 | MIT | +| eslint-plugin-svelte | `^3.17.0` | 3.17.0 | MIT | +| pagefind | `^1.5.2` | 1.5.2 | MIT | +| prettier | `^3.8.1` | 3.8.1 | MIT | +| prettier-plugin-svelte | `^3.4.1` | 3.5.1 | MIT | +| puppeteer | `^24.43.1` | 24.43.1 | Apache-2.0 | +| rehype-slug | `^6.0.0` | 6.0.0 | MIT | +| rollup-plugin-visualizer | `^6.0.5` | 6.0.11 | MIT | +| serve | `^14.2.5` | 14.2.6 | MIT | +| sharp | `^0.34.5` | 0.34.5 | Apache-2.0 | +| svelte | `^5.55.4` | 5.55.4 | MIT | +| svelte-check | `^4.4.5` | 4.4.5 | MIT | +| tsx | `^4.21.0` | 4.21.0 | MIT | +| typescript | `^5.9.3` | 5.9.3 | Apache-2.0 | +| typescript-eslint | `^8.58.2` | 8.58.2 | MIT | +| vite | `^8.0.14` | 8.0.14 | MIT | +| vitest | `^4.1.4` | 4.1.4 | MIT | + +## Full Resolved Tree + +1150 third-party packages are resolved into the +lockfile in total (direct and transitive). Their declared licenses: + +| License | Packages | +| --- | --- | +| MIT | 794 | +| Apache-2.0 | 205 | +| ISC | 64 | +| BSD-3-Clause | 29 | +| MPL-2.0 | 12 | +| BSD-2-Clause | 10 | +| LGPL-3.0-or-later | 10 | +| UNDECLARED | 5 | +| OFL-1.1 | 4 | +| Apache-2.0 AND LGPL-3.0-or-later | 3 | +| (BSD-2-Clause OR MIT OR Apache-2.0) | 1 | +| (CC-BY-4.0 AND OFL-1.1 AND MIT) | 1 | +| (MIT AND Zlib) | 1 | +| (MIT OR CC0-1.0) | 1 | +| (MPL-2.0 OR Apache-2.0) | 1 | +| (WTFPL OR MIT) | 1 | +| 0BSD | 1 | +| Apache-2.0 AND LGPL-3.0-or-later AND MIT | 1 | +| BlueOak-1.0.0 | 1 | +| EPL-2.0 | 1 | +| Python-2.0 | 1 | +| Unlicense | 1 | +| Zlib | 1 | +| Zlib AND LicenseRef-Tinyland-Proprietary | 1 | + +5 resolved package(s) declare no license field in the lockfile: + +- `@better-fetch/fetch` +- `@blog/agent` +- `@blog/pulse-client` +- `@blog/pulse-core` +- `khroma` + +Workspace-local packages are covered by this repository's CC0 dedication; any +other name on that list is an unreviewed license and is a follow-up, not a +silent MIT. diff --git a/docs/build-metrics.md b/docs/build-metrics.md index c2f1be01..dc8edfd5 100644 --- a/docs/build-metrics.md +++ b/docs/build-metrics.md @@ -1,9 +1,25 @@ # Build Metrics Baseline -> Superseded 2026-08-28: Vite 8 shipped (`package.json` pins `^8.0.14`). The -> numbers below are the Vite 6.4.1 baseline, kept for comparison only. +- **Status:** STALE — superseded 2026-08-28. Vite 8 shipped and the toolchain + below no longer exists in this repo. The numbers are kept as the Vite 6.4.1 + comparison point, not as a description of the current build. +- **Captured:** 2026-02-11 on branch `feature/sprint3-week8` (commit `9aac474`) +- **Checked by:** `scripts/truth-surfaces.mjs`. The toolchain table below is + machine-compared against `package.json`. If the versions match again this + page must drop the STALE marker; while they differ it must carry it. A + metrics page cannot silently keep presenting itself as current (TIN-161). -Captured: 2026-02-11 | Branch: `feature/sprint3-week8` | Vite 6.4.1 +## Toolchain At Capture + +Declared ranges read from `9aac474:package.json`, the commit that added this +file — not the resolved versions, so the comparison is like-for-like against +`package.json` today. + +| Package | Declared at capture | +| --- | --- | +| `vite` | ^6.4.1 | +| `svelte` | ^5.19.0 | +| `@sveltejs/kit` | ^2.16.0 | ## Build Time @@ -49,4 +65,17 @@ All heavy chunks are lazy-loaded — they don't affect first-load performance. ## Vite 8 / Rolldown Status -**NOGO** — Vite 8.0.0-beta.13 (Feb 2026). Dead code elimination broken for `esm-env` conditions. No stable SvelteKit support. See [issue #16](https://github.com/Jesssullivan/jesssullivan.github.io/issues/16). +**NOGO at capture** — Vite 8.0.0-beta.13 (Feb 2026). Dead code elimination +broken for `esm-env` conditions. No stable SvelteKit support. See +[issue #16](https://github.com/Jesssullivan/jesssullivan.github.io/issues/16). +**Overtaken by events:** `package.json` declares `vite ^8.0.14` as of +2026-08-28, so this row records what was true in February and not a current +posture. + +## Recapture + +There is no measured replacement for this page yet. Producing one needs a real +build, which is the rest of TIN-161; until then the STALE marker and the +machine check are what keep the page honest. When it is recaptured, update the +toolchain table in the same commit as the numbers — the check pairs them +deliberately, so metrics cannot land without the toolchain that produced them. diff --git a/scripts/run-vitest-bazel.mjs b/scripts/run-vitest-bazel.mjs index 173c30fd..3ae98773 100644 --- a/scripts/run-vitest-bazel.mjs +++ b/scripts/run-vitest-bazel.mjs @@ -48,6 +48,10 @@ function copyInputsToBuildRoot() { for (const file of [ '.npmrc', + // TIN-161 truth surfaces: scripts/truth-surfaces.test.mts checks these + // documents against package-lock.json, so they travel with it. + 'THIRD-PARTY-LICENSES.md', + 'docs/build-metrics.md', 'package-lock.json', 'package.json', 'pnpm-lock.yaml', diff --git a/scripts/truth-surfaces.mjs b/scripts/truth-surfaces.mjs new file mode 100644 index 00000000..66dc6cad --- /dev/null +++ b/scripts/truth-surfaces.mjs @@ -0,0 +1,256 @@ +#!/usr/bin/env node + +/** + * Truth surfaces (TIN-161). + * + * Two static surfaces in this repo claimed to describe the build and lied + * quietly as the build moved underneath them: + * + * - `THIRD-PARTY-LICENSES.md` was hand-maintained. By 2026-08-28 it listed + * `shiki 3.22.0` (actual 4.2.0), `vite 6.4.1` (actual 8.0.14), + * `svelte 5.50.1` (actual 5.55.4), `sharp 0.33.5` (actual 0.34.5), + * `mermaid` as a RUNTIME dependency it no longer is, and omitted roughly + * two thirds of the direct dependency set. Nothing failed, because nothing + * checked. + * - `docs/build-metrics.md` was captured once on 2026-02-11 against Vite + * 6.4.1 on a branch that no longer exists, and was titled "Baseline" with + * no marker saying the tree it measured is gone. + * + * The fix is not a fresher hand-edit. It is to derive the license surface from + * a checked-in machine-readable source and to make staleness FAIL rather than + * accumulate. + * + * Derivation source: `package-lock.json` (npm lockfile v3). It is current with + * `package.json` and with the pnpm resolutions, and — unlike `pnpm-lock.yaml` + * — it carries a `license` field per package. Every figure this module emits + * comes from there; nothing is inferred, and a dependency whose lockfile entry + * declares no license is printed as UNDECLARED rather than guessed. + * + * Deliberately dependency-free so it runs under plain node, without an install + * step, in a pre-commit hook, in CI, or from the vitest suite. + */ + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +export const LICENSES_DOC = 'THIRD-PARTY-LICENSES.md'; +export const BUILD_METRICS_DOC = 'docs/build-metrics.md'; +export const GENERATED_MARKER = ''; + +/** Toolchain packages `docs/build-metrics.md` must name at capture time. */ +export const BUILD_METRICS_TOOLCHAIN = ['vite', 'svelte', '@sveltejs/kit']; + +const readJson = (root, relative) => JSON.parse(readFileSync(join(root, relative), 'utf-8')); + +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** + * Direct dependencies with the version and license the lockfile actually + * resolved, split into the two groups the document publishes. + */ +export function buildLicenseInventory(root) { + const manifest = readJson(root, 'package.json'); + const lock = readJson(root, 'package-lock.json'); + const entries = lock.packages ?? {}; + + const collect = (group) => + Object.keys(manifest[group] ?? {}) + .sort() + .map((name) => { + const entry = entries[`node_modules/${name}`]; + return { + name, + specifier: manifest[group][name], + version: entry?.version ?? 'UNRESOLVED', + license: entry?.license ?? 'UNDECLARED', + resolved: entry?.resolved ?? null, + }; + }); + + const thirdParty = Object.entries(entries).filter(([path]) => path.startsWith('node_modules/')); + const histogram = new Map(); + for (const [, entry] of thirdParty) { + const license = entry.license ?? 'UNDECLARED'; + histogram.set(license, (histogram.get(license) ?? 0) + 1); + } + + return { + runtime: collect('dependencies'), + build: collect('devDependencies'), + resolvedPackageCount: thirdParty.length, + licenseHistogram: [...histogram.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])), + undeclared: thirdParty + .filter(([, entry]) => entry.license === undefined) + .map(([path]) => path.replace(/^node_modules\//, '')) + .sort(), + }; +} + +const row = ({ name, specifier, version, license }) => `| ${name} | \`${specifier}\` | ${version} | ${license} |`; + +const table = (rows) => + ['| Package | Declared | Resolved | License |', '| --- | --- | --- | --- |', ...rows.map(row)].join('\n'); + +/** Render the whole document. Pure: same inventory in, same bytes out. */ +export function renderLicensesDoc(inventory) { + const histogram = inventory.licenseHistogram + .map(([license, count]) => `| ${license} | ${count} |`) + .join('\n'); + + return `# Third-Party Licenses + +${GENERATED_MARKER} + +This project (transscendsurvival.org) is dedicated to the public domain under +[CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). + +Every figure below is derived from \`package-lock.json\` — the package name and +its declared range from \`package.json\`, the resolved version and the license +string from the lockfile entry for that exact resolution. Nothing here is +hand-entered, so nothing here can drift without \`scripts/truth-surfaces.test.mts\` +failing. + +Regenerate with: + +\`\`\`sh +node scripts/truth-surfaces.mjs --write +\`\`\` + +A dependency whose lockfile entry declares no license is printed as +\`UNDECLARED\`. It is never guessed from the package name, the organisation, or +a sibling version. + +The previous revision of this file was hand-maintained and had drifted: it +listed \`shiki 3.22.0\`, \`vite 6.4.1\`, \`svelte 5.50.1\` and \`sharp 0.33.5\` +against resolutions of 4.2.0, 8.0.14, 5.55.4 and 0.34.5, carried \`mermaid\` as +a runtime dependency it no longer is, and omitted most of the direct set. + +## Runtime Dependencies + +${table(inventory.runtime)} + +## Build / Dev Dependencies + +${table(inventory.build)} + +## Full Resolved Tree + +${inventory.resolvedPackageCount} third-party packages are resolved into the +lockfile in total (direct and transitive). Their declared licenses: + +| License | Packages | +| --- | --- | +${histogram} + +${ + inventory.undeclared.length === 0 + ? 'Every resolved package declares a license.' + : `${inventory.undeclared.length} resolved package(s) declare no license field in the lockfile:\n\n${inventory.undeclared + .map((name) => `- \`${name}\``) + .join('\n')}\n\nWorkspace-local packages are covered by this repository's CC0 dedication; any\nother name on that list is an unreviewed license and is a follow-up, not a\nsilent MIT.` +} +`; +} + +/** Metadata `docs/build-metrics.md` must declare about its own capture. */ +export function readBuildMetricsHeader(root) { + const body = readFileSync(join(root, BUILD_METRICS_DOC), 'utf-8'); + const field = (label) => { + const match = body.match(new RegExp(`^- \\*\\*${label}:\\*\\* (.+)$`, 'm')); + return match ? match[1].trim() : null; + }; + const toolchain = {}; + for (const name of BUILD_METRICS_TOOLCHAIN) { + const match = body.match(new RegExp(`^\\| \`${escapeRegExp(name)}\` \\| ([^|]+) \\|`, 'm')); + toolchain[name] = match ? match[1].trim() : null; + } + return { body, status: field('Status'), capturedAt: field('Captured'), toolchain }; +} + +/** + * The freshness rule, stated so it can fail in both directions: + * + * - the document must declare a Status, a capture date, and the version of + * every toolchain package it was captured against; + * - if any of those versions is not what `package.json` declares today, the + * Status must say STALE. Bumping vite without recapturing turns the page + * red; recapturing on the current toolchain and updating the versions lets + * it go green again. + */ +export function checkBuildMetricsFreshness(root) { + const violations = []; + const manifest = readJson(root, 'package.json'); + const declared = { ...manifest.dependencies, ...manifest.devDependencies }; + const { status, capturedAt, toolchain } = readBuildMetricsHeader(root); + + if (!status) violations.push(`${BUILD_METRICS_DOC}: no "- **Status:** ..." line.`); + if (!capturedAt) violations.push(`${BUILD_METRICS_DOC}: no "- **Captured:** ..." line.`); + + const drifted = []; + for (const name of BUILD_METRICS_TOOLCHAIN) { + const capturedVersion = toolchain[name]; + if (!capturedVersion) { + violations.push( + `${BUILD_METRICS_DOC}: does not record the \`${name}\` version it was captured against. ` + + 'A metrics page that will not say which toolchain produced it cannot be checked for staleness.', + ); + continue; + } + if (capturedVersion !== declared[name]) { + drifted.push( + `${name} captured at ${capturedVersion}, package.json ` + + (declared[name] === undefined ? 'no longer declares it' : `declares ${declared[name]}`), + ); + } + } + + if (drifted.length > 0 && !/STALE/.test(status ?? '')) { + violations.push( + `${BUILD_METRICS_DOC}: the toolchain moved (${drifted.join('; ')}) but Status does not say STALE. ` + + 'Either recapture the metrics against the current toolchain and update the recorded versions, ' + + 'or mark the page STALE — it may not keep presenting itself as current.', + ); + } + if (drifted.length === 0 && /STALE/.test(status ?? '')) { + violations.push( + `${BUILD_METRICS_DOC}: Status says STALE but the recorded toolchain matches package.json. ` + + 'Drop the marker rather than leaving a permanent warning nobody reads.', + ); + } + + return violations; +} + +/** Everything, as a violation list. Empty means both surfaces are truthful. */ +export function checkTruthSurfaces(root) { + const violations = []; + const expected = renderLicensesDoc(buildLicenseInventory(root)); + const actual = readFileSync(join(root, LICENSES_DOC), 'utf-8'); + if (actual !== expected) { + violations.push( + `${LICENSES_DOC} is out of date with package-lock.json. ` + + 'Run `node scripts/truth-surfaces.mjs --write`.', + ); + } + violations.push(...checkBuildMetricsFreshness(root)); + return violations; +} + +/* CLI: `--write` regenerates the license doc; no flag checks both surfaces. */ +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const root = process.cwd(); + if (process.argv.includes('--write')) { + const { writeFileSync } = await import('node:fs'); + writeFileSync(join(root, LICENSES_DOC), renderLicensesDoc(buildLicenseInventory(root))); + process.stdout.write(`wrote ${LICENSES_DOC}\n`); + } else { + const violations = checkTruthSurfaces(root); + if (violations.length === 0) { + process.stdout.write('truth surfaces OK\n'); + } else { + for (const violation of violations) process.stderr.write(`- ${violation}\n`); + process.exitCode = 1; + } + } +} diff --git a/scripts/truth-surfaces.test.mts b/scripts/truth-surfaces.test.mts new file mode 100644 index 00000000..0bb9874d --- /dev/null +++ b/scripts/truth-surfaces.test.mts @@ -0,0 +1,203 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { + BUILD_METRICS_DOC, + LICENSES_DOC, + buildLicenseInventory, + checkBuildMetricsFreshness, + checkTruthSurfaces, + renderLicensesDoc, +} from './truth-surfaces.mjs'; + +/** + * TIN-161 — the two static surfaces that claimed to describe the build. + * + * `THIRD-PARTY-LICENSES.md` was hand-maintained and had drifted years of + * versions; `docs/build-metrics.md` was a February snapshot titled "Baseline". + * Neither could fail, because nothing checked them. These tests are the check, + * and half of them exist to prove the check can go red: a guard that has only + * ever been seen passing is an unfailable assertion. + */ + +const temps: string[] = []; +afterEach(() => { + while (temps.length > 0) { + rmSync(temps.pop() as string, { recursive: true, force: true }); + } +}); + +type Fixture = { + dependencies?: Record; + devDependencies?: Record; + lock?: Record; + metrics?: string; + licensesDoc?: string; +}; + +const METRICS_TEMPLATE = (status: string, vite: string, svelte: string, kit: string) => `# Build Metrics Baseline + +- **Status:** ${status} +- **Captured:** 2026-02-11 on branch \`feature/sprint3-week8\` + +## Toolchain At Capture + +| Package | Declared at capture | +| --- | --- | +| \`vite\` | ${vite} | +| \`svelte\` | ${svelte} | +| \`@sveltejs/kit\` | ${kit} | +`; + +function fixture({ dependencies = {}, devDependencies = {}, lock = {}, metrics, licensesDoc }: Fixture): string { + const root = mkdtempSync(join(tmpdir(), 'truth-surfaces-')); + temps.push(root); + + writeFileSync(join(root, 'package.json'), JSON.stringify({ dependencies, devDependencies }, null, 2)); + writeFileSync( + join(root, 'package-lock.json'), + JSON.stringify( + { + lockfileVersion: 3, + packages: Object.fromEntries(Object.entries(lock).map(([name, entry]) => [`node_modules/${name}`, entry])), + }, + null, + 2, + ), + ); + + mkdirSync(join(root, 'docs'), { recursive: true }); + writeFileSync(join(root, BUILD_METRICS_DOC), metrics ?? METRICS_TEMPLATE('CURRENT', '^6.4.1', '^5.19.0', '^2.16.0')); + writeFileSync( + join(root, LICENSES_DOC), + licensesDoc ?? renderLicensesDoc(buildLicenseInventory(root)), + ); + return root; +} + +describe('license surface is derived, not maintained', () => { + it('reads version and license from the lockfile entry for the resolved version', () => { + const root = fixture({ + dependencies: { shiki: '^4.2.0' }, + devDependencies: { vite: '^8.0.14' }, + lock: { + shiki: { version: '4.2.0', license: 'MIT' }, + vite: { version: '8.0.14', license: 'MIT' }, + }, + }); + const inventory = buildLicenseInventory(root); + expect(inventory.runtime).toEqual([ + { name: 'shiki', specifier: '^4.2.0', version: '4.2.0', license: 'MIT', resolved: null }, + ]); + expect(inventory.build[0]).toMatchObject({ name: 'vite', version: '8.0.14' }); + }); + + it('prints UNDECLARED rather than guessing when the lockfile declares no license', () => { + const root = fixture({ + dependencies: { khroma: '^2.1.0' }, + lock: { khroma: { version: '2.1.0' } }, + }); + const inventory = buildLicenseInventory(root); + expect(inventory.runtime[0].license).toBe('UNDECLARED'); + expect(inventory.undeclared).toEqual(['khroma']); + expect(renderLicensesDoc(inventory)).toContain('UNDECLARED'); + // The failure mode this replaces: a hand-written MIT for a package + // nobody read the license of. + expect(renderLicensesDoc(inventory)).not.toContain('| khroma | `^2.1.0` | 2.1.0 | MIT |'); + }); + + it('goes red when a resolved version moves and the document does not', () => { + const root = fixture({ + dependencies: { shiki: '^4.2.0' }, + // The build-metrics half of the check shares this fixture root, so the + // toolchain it records has to be declared here too — otherwise a + // build-metrics violation would mask the license one being asserted. + devDependencies: { vite: '^6.4.1', svelte: '^5.19.0', '@sveltejs/kit': '^2.16.0' }, + lock: { shiki: { version: '4.2.0', license: 'MIT' } }, + }); + expect(checkTruthSurfaces(root)).toEqual([]); + + // Bump the lockfile the way a dependency PR would, leaving the + // committed document behind — the exact drift that produced + // `shiki 3.22.0` in a tree resolving 4.2.0. + writeFileSync( + join(root, 'package-lock.json'), + JSON.stringify({ + lockfileVersion: 3, + packages: { 'node_modules/shiki': { version: '4.3.0', license: 'MIT' } }, + }), + ); + expect(checkTruthSurfaces(root).join('\n')).toMatch(/THIRD-PARTY-LICENSES\.md is out of date/); + }); + + it('is deterministic: rendering twice produces identical bytes', () => { + const root = fixture({ + dependencies: { marked: '^4.3.0' }, + lock: { marked: { version: '4.3.0', license: 'MIT' } }, + }); + const inventory = buildLicenseInventory(root); + expect(renderLicensesDoc(inventory)).toBe(renderLicensesDoc(buildLicenseInventory(root))); + }); +}); + +describe('build-metrics freshness fails in both directions', () => { + const deps = { vite: '^6.4.1', svelte: '^5.19.0', '@sveltejs/kit': '^2.16.0' }; + + it('passes when the recorded toolchain matches and the page does not claim to be stale', () => { + const root = fixture({ devDependencies: deps }); + expect(checkBuildMetricsFreshness(root)).toEqual([]); + }); + + it('fails when the toolchain moved and the page still presents itself as current', () => { + const root = fixture({ devDependencies: { ...deps, vite: '^8.0.14' } }); + expect(checkBuildMetricsFreshness(root).join('\n')).toMatch(/the toolchain moved .* but Status does not say STALE/s); + }); + + it('passes once the page is marked STALE against a moved toolchain', () => { + const root = fixture({ + devDependencies: { ...deps, vite: '^8.0.14' }, + metrics: METRICS_TEMPLATE('STALE — superseded', '^6.4.1', '^5.19.0', '^2.16.0'), + }); + expect(checkBuildMetricsFreshness(root)).toEqual([]); + }); + + it('fails when the page keeps a STALE marker the toolchain no longer justifies', () => { + const root = fixture({ + devDependencies: deps, + metrics: METRICS_TEMPLATE('STALE — superseded', '^6.4.1', '^5.19.0', '^2.16.0'), + }); + expect(checkBuildMetricsFreshness(root).join('\n')).toMatch(/Status says STALE but the recorded toolchain matches/); + }); + + it('fails when the page will not say which toolchain produced it', () => { + const root = fixture({ + devDependencies: deps, + metrics: '# Build Metrics Baseline\n\n- **Status:** CURRENT\n- **Captured:** 2026-02-11\n', + }); + const violations = checkBuildMetricsFreshness(root).join('\n'); + expect(violations).toMatch(/does not record the `vite` version/); + expect(violations).toMatch(/does not record the `@sveltejs\/kit` version/); + }); + + it('fails when the page carries no Status line at all', () => { + const root = fixture({ devDependencies: deps, metrics: '# Build Metrics Baseline\n\nnumbers\n' }); + expect(checkBuildMetricsFreshness(root).join('\n')).toMatch(/no "- \*\*Status:\*\* \.\.\." line/); + }); +}); + +describe('this repository satisfies both surfaces', () => { + it('has no truth-surface violations', () => { + expect(checkTruthSurfaces(process.cwd())).toEqual([]); + }); + + it('publishes a license row for every direct dependency, none of them guessed', () => { + const inventory = buildLicenseInventory(process.cwd()); + const declared = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8')); + const directCount = + Object.keys(declared.dependencies ?? {}).length + Object.keys(declared.devDependencies ?? {}).length; + expect(directCount).toBeGreaterThan(0); + expect(inventory.runtime.length + inventory.build.length).toBe(directCount); + expect(inventory.runtime.concat(inventory.build).filter((row) => row.version === 'UNRESOLVED')).toEqual([]); + }); +}); From 28c8539bd045f94388f454ba39f30ded5e4ecd61 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Sat, 29 Aug 2026 01:27:13 -0400 Subject: [PATCH 2/2] fix(build): run the license guard, generate the published page, and tie build-metrics to its capture commit (TIN-161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #270 found the guard did not run and the page it was meant to protect was untouched. Seven findings, all applied. BLOCKER — the guard never executed. `vitest.bazel.config.ts` is an exact-path allow-list and `scripts/truth-surfaces.test.mts` matched none of its five patterns, so `//:vitest_unit_tests` — the only test authority CI has — never loaded it. Grep found zero invocation sites anywhere in the tree. The test is now in the include list, and `.github/workflows/ci.yml` gains a dependency-free `node scripts/truth-surfaces.mjs` step, which is also the only place the new provenance check can run (the Vitest sandbox is a copied tree with no .git). BLOCKER — the published page was still lying. `/THIRD-PARTY-LICENSES` is prerendered and linked from the footer, and it was a hand-written duplicate carrying every defect the PR set out to fix: mermaid as a runtime dependency, shiki 3.22.0, vite 6.4.1, `@sveltejs/enhanced-img` which is not a dependency at all, and "All 243 production packages ... Apache-2.0 (8)" against a real runtime figure of 106. `truth-surfaces.mjs --write` now also emits `src/lib/data/third-party-licenses.json`, the route renders from it, and the byte-comparison covers it — so the page and the Markdown cannot disagree. MAJOR — the freshness check verified the label, not the measurement. Editing the three "Toolchain At Capture" cells and the Status line turned the page green without recapturing a number, which made falsifying provenance the cheapest path to green. `checkBuildMetricsProvenance` now validates that row against `:package.json`; forging it requires moving the capture commit, which is a claim the metrics were retaken. Verified red on the exact edit the review demonstrated. MAJOR — "1150 third-party packages" was an install-path count. npm records a package once per place it lands (`@opentelemetry/core` 21 times), and the filter also swept in the three `link: true` workspace entries — so 3 of the 5 names published as "declare no license" were this repository's own code. Tree figures now deduplicate on name@version and drop workspace links: 1150 installs, 1078 distinct third-party packages, 361 runtime, 2 genuinely undeclared. MINOR — the production-only breakdown was deleted, not replaced. Runtime and full histograms are now emitted separately and labelled, so LGPL-3.0-or-later and EPL-2.0 stop appearing on the attribution surface of a static site that ships neither. MINOR — `optionalDependencies`/`peerDependencies` were silently omitted, and the test computed its expected count from the same two keys, so it could never observe the omission. All four groups are read; the test derives the count from every `*ependencies` key in the manifest. MINOR — `resolved` was collected and never rendered. It is now a Source column, which is the only field that reveals a package that did not come from the registry (`@tummycrypt/tinyvectors` resolves to a GitHub tarball). NIT — "rendering twice produces identical bytes" asserted f(x) === f(x) over a pure module and stayed green with every `.sort()` deleted. It now renders from reversed manifest and lockfile key order and compares bytes. NIT — a missing surface threw ENOENT instead of reporting a violation. Guarded. 28 tests, all failable: every fix above was mutated to confirm it goes red, including unwiring the provenance check from `checkTruthSurfaces`. --- .github/workflows/ci.yml | 7 + THIRD-PARTY-LICENSES.md | 164 ++++--- scripts/truth-surfaces.mjs | 368 ++++++++++++--- scripts/truth-surfaces.test.mts | 333 +++++++++++-- src/lib/data/third-party-licenses.json | 466 +++++++++++++++++++ src/routes/THIRD-PARTY-LICENSES/+page.svelte | 185 +++++--- vitest.bazel.config.ts | 1 + 7 files changed, 1306 insertions(+), 218 deletions(-) create mode 100644 src/lib/data/third-party-licenses.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d20d580b..ac38ec62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,6 +185,13 @@ jobs: node-version-file: '.nvmrc' cache: 'npm' + # TIN-161. The vitest suite covers this too, but it runs in a copied + # sandbox with no .git, so the build-metrics provenance check is only + # reachable here — at a real checkout with history. Dependency-free by + # design: no install step required. + - name: Truth surfaces + run: node scripts/truth-surfaces.mjs + - run: npm ci - name: Static production dependency audit diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md index 9e1ffc25..4f1a1ce7 100644 --- a/THIRD-PARTY-LICENSES.md +++ b/THIRD-PARTY-LICENSES.md @@ -6,9 +6,9 @@ This project (transscendsurvival.org) is dedicated to the public domain under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). Every figure below is derived from `package-lock.json` — the package name and -its declared range from `package.json`, the resolved version and the license -string from the lockfile entry for that exact resolution. Nothing here is -hand-entered, so nothing here can drift without `scripts/truth-surfaces.test.mts` +its declared range from `package.json`, the resolved version, license string +and source URL from the lockfile entry for that exact resolution. Nothing here +is hand-entered, so nothing here can drift without `scripts/truth-surfaces.mjs` failing. Regenerate with: @@ -17,6 +17,10 @@ Regenerate with: node scripts/truth-surfaces.mjs --write ``` +That command writes this file and `src/lib/data/third-party-licenses.json`, which +`src/routes/THIRD-PARTY-LICENSES/+page.svelte` renders — the published page and +this document are the same data, so they cannot disagree. + A dependency whose lockfile entry declares no license is printed as `UNDECLARED`. It is never guessed from the package name, the organisation, or a sibling version. @@ -28,73 +32,100 @@ a runtime dependency it no longer is, and omitted most of the direct set. ## Runtime Dependencies -| Package | Declared | Resolved | License | -| --- | --- | --- | --- | -| @fontsource/crimson-pro | `^5.2.8` | 5.2.8 | OFL-1.1 | -| @fontsource/fira-code | `^5.2.7` | 5.2.7 | OFL-1.1 | -| @fontsource/inter | `^5.2.8` | 5.2.8 | OFL-1.1 | -| @fontsource/raleway | `^5.2.8` | 5.2.8 | OFL-1.1 | -| @skeletonlabs/skeleton | `5.0.0` | 5.0.0 | MIT | -| @skeletonlabs/skeleton-svelte | `5.0.0` | 5.0.0 | MIT | -| @tummycrypt/tinyvectors | `https://github.com/tinyland-inc/tinyvectors/archive/refs/tags/v0.3.5.tar.gz` | 0.3.5 | Zlib | -| dompurify | `^3.4.13` | 3.4.13 | (MPL-2.0 OR Apache-2.0) | -| flexsearch | `^0.8.212` | 0.8.212 | Apache-2.0 | -| marked | `^4.3.0` | 4.3.0 | MIT | -| mdsvex | `^0.12.7` | 0.12.7 | MIT | -| shiki | `^4.2.0` | 4.2.0 | MIT | -| tailwindcss | `^4.2.2` | 4.2.2 | MIT | +| Package | Declared | Resolved | License | Source | +| --- | --- | --- | --- | --- | +| @fontsource/crimson-pro | `^5.2.8` | 5.2.8 | OFL-1.1 | | +| @fontsource/fira-code | `^5.2.7` | 5.2.7 | OFL-1.1 | | +| @fontsource/inter | `^5.2.8` | 5.2.8 | OFL-1.1 | | +| @fontsource/raleway | `^5.2.8` | 5.2.8 | OFL-1.1 | | +| @skeletonlabs/skeleton | `5.0.0` | 5.0.0 | MIT | | +| @skeletonlabs/skeleton-svelte | `5.0.0` | 5.0.0 | MIT | | +| @tummycrypt/tinyvectors | `https://github.com/tinyland-inc/tinyvectors/archive/refs/tags/v0.3.5.tar.gz` | 0.3.5 | Zlib | | +| dompurify | `^3.4.13` | 3.4.13 | (MPL-2.0 OR Apache-2.0) | | +| flexsearch | `^0.8.212` | 0.8.212 | Apache-2.0 | | +| marked | `^4.3.0` | 4.3.0 | MIT | | +| mdsvex | `^0.12.7` | 0.12.7 | MIT | | +| shiki | `^4.2.0` | 4.2.0 | MIT | | +| tailwindcss | `^4.2.2` | 4.2.2 | MIT | | ## Build / Dev Dependencies -| Package | Declared | Resolved | License | -| --- | --- | --- | --- | -| @eslint/js | `^9.39.2` | 9.39.4 | MIT | -| @mermaid-js/mermaid-cli | `^11.15.0` | 11.15.0 | MIT | -| @playwright/test | `^1.59.1` | 1.59.1 | Apache-2.0 | -| @sveltejs/adapter-node | `^5.5.7` | 5.5.7 | MIT | -| @sveltejs/adapter-static | `^3.0.10` | 3.0.10 | MIT | -| @sveltejs/kit | `^2.61.1` | 2.61.1 | MIT | -| @sveltejs/vite-plugin-svelte | `^7.1.2` | 7.1.2 | MIT | -| @tailwindcss/vite | `^4.2.2` | 4.2.2 | MIT | -| @tummycrypt/vite-plugin-a11y | `^0.2.2` | 0.2.2 | Zlib AND LicenseRef-Tinyland-Proprietary | -| @types/node | `^25.2.3` | 25.5.0 | MIT | -| @vitest/coverage-v8 | `^4.1.4` | 4.1.4 | MIT | -| eslint | `^9.39.2` | 9.39.4 | MIT | -| eslint-config-prettier | `^10.1.8` | 10.1.8 | MIT | -| eslint-plugin-svelte | `^3.17.0` | 3.17.0 | MIT | -| pagefind | `^1.5.2` | 1.5.2 | MIT | -| prettier | `^3.8.1` | 3.8.1 | MIT | -| prettier-plugin-svelte | `^3.4.1` | 3.5.1 | MIT | -| puppeteer | `^24.43.1` | 24.43.1 | Apache-2.0 | -| rehype-slug | `^6.0.0` | 6.0.0 | MIT | -| rollup-plugin-visualizer | `^6.0.5` | 6.0.11 | MIT | -| serve | `^14.2.5` | 14.2.6 | MIT | -| sharp | `^0.34.5` | 0.34.5 | Apache-2.0 | -| svelte | `^5.55.4` | 5.55.4 | MIT | -| svelte-check | `^4.4.5` | 4.4.5 | MIT | -| tsx | `^4.21.0` | 4.21.0 | MIT | -| typescript | `^5.9.3` | 5.9.3 | Apache-2.0 | -| typescript-eslint | `^8.58.2` | 8.58.2 | MIT | -| vite | `^8.0.14` | 8.0.14 | MIT | -| vitest | `^4.1.4` | 4.1.4 | MIT | - -## Full Resolved Tree - -1150 third-party packages are resolved into the -lockfile in total (direct and transitive). Their declared licenses: +| Package | Declared | Resolved | License | Source | +| --- | --- | --- | --- | --- | +| @eslint/js | `^9.39.2` | 9.39.4 | MIT | | +| @mermaid-js/mermaid-cli | `^11.15.0` | 11.15.0 | MIT | | +| @playwright/test | `^1.59.1` | 1.59.1 | Apache-2.0 | | +| @sveltejs/adapter-node | `^5.5.7` | 5.5.7 | MIT | | +| @sveltejs/adapter-static | `^3.0.10` | 3.0.10 | MIT | | +| @sveltejs/kit | `^2.61.1` | 2.61.1 | MIT | | +| @sveltejs/vite-plugin-svelte | `^7.1.2` | 7.1.2 | MIT | | +| @tailwindcss/vite | `^4.2.2` | 4.2.2 | MIT | | +| @tummycrypt/vite-plugin-a11y | `^0.2.2` | 0.2.2 | Zlib AND LicenseRef-Tinyland-Proprietary | | +| @types/node | `^25.2.3` | 25.5.0 | MIT | | +| @vitest/coverage-v8 | `^4.1.4` | 4.1.4 | MIT | | +| eslint | `^9.39.2` | 9.39.4 | MIT | | +| eslint-config-prettier | `^10.1.8` | 10.1.8 | MIT | | +| eslint-plugin-svelte | `^3.17.0` | 3.17.0 | MIT | | +| pagefind | `^1.5.2` | 1.5.2 | MIT | | +| prettier | `^3.8.1` | 3.8.1 | MIT | | +| prettier-plugin-svelte | `^3.4.1` | 3.5.1 | MIT | | +| puppeteer | `^24.43.1` | 24.43.1 | Apache-2.0 | | +| rehype-slug | `^6.0.0` | 6.0.0 | MIT | | +| rollup-plugin-visualizer | `^6.0.5` | 6.0.11 | MIT | | +| serve | `^14.2.5` | 14.2.6 | MIT | | +| sharp | `^0.34.5` | 0.34.5 | Apache-2.0 | | +| svelte | `^5.55.4` | 5.55.4 | MIT | | +| svelte-check | `^4.4.5` | 4.4.5 | MIT | | +| tsx | `^4.21.0` | 4.21.0 | MIT | | +| typescript | `^5.9.3` | 5.9.3 | Apache-2.0 | | +| typescript-eslint | `^8.58.2` | 8.58.2 | MIT | | +| vite | `^8.0.14` | 8.0.14 | MIT | | +| vitest | `^4.1.4` | 4.1.4 | MIT | | + +## Resolved Tree + +`package-lock.json` records 1150 installs under `node_modules/`. +That is a count of install paths, not of packages: npm records a package once +per place it lands, so a dependency it cannot hoist appears many times over. +Deduplicated on `name@version`, the tree is **1078** distinct +third-party packages, of which **361** are reachable +outside `devDependencies`. + +### Runtime Packages + +This is the set that governs attribution: the site is a prerendered static +build, so only these licenses travel to a reader. + +| License | Packages | +| --- | --- | +| MIT | 225 | +| Apache-2.0 | 106 | +| BSD-3-Clause | 11 | +| ISC | 10 | +| OFL-1.1 | 4 | +| (MPL-2.0 OR Apache-2.0) | 1 | +| 0BSD | 1 | +| BSD-2-Clause | 1 | +| UNDECLARED | 1 | +| Zlib | 1 | + +### Full Resolved Tree + +Development-only packages included. Licenses appearing here but not above are +build-time only and are not distributed. | License | Packages | | --- | --- | -| MIT | 794 | -| Apache-2.0 | 205 | -| ISC | 64 | +| MIT | 769 | +| Apache-2.0 | 162 | +| ISC | 63 | | BSD-3-Clause | 29 | | MPL-2.0 | 12 | | BSD-2-Clause | 10 | | LGPL-3.0-or-later | 10 | -| UNDECLARED | 5 | | OFL-1.1 | 4 | | Apache-2.0 AND LGPL-3.0-or-later | 3 | +| UNDECLARED | 2 | | (BSD-2-Clause OR MIT OR Apache-2.0) | 1 | | (CC-BY-4.0 AND OFL-1.1 AND MIT) | 1 | | (MIT AND Zlib) | 1 | @@ -110,14 +141,13 @@ lockfile in total (direct and transitive). Their declared licenses: | Zlib | 1 | | Zlib AND LicenseRef-Tinyland-Proprietary | 1 | -5 resolved package(s) declare no license field in the lockfile: +## Undeclared Licenses + +2 resolved third-party package(s) declare no license field in the lockfile: + +- `@better-fetch/fetch@1.1.21` +- `khroma@2.1.0` -- `@better-fetch/fetch` -- `@blog/agent` -- `@blog/pulse-client` -- `@blog/pulse-core` -- `khroma` +Each one is an unreviewed license and a follow-up, not a silent MIT. -Workspace-local packages are covered by this repository's CC0 dedication; any -other name on that list is an unreviewed license and is a follow-up, not a -silent MIT. +The lockfile also carries 3 workspace link(s) — `@blog/agent`, `@blog/pulse-client`, `@blog/pulse-core` — which are this repository's own code under its CC0 dedication. They are not third party and are excluded from every figure above. diff --git a/scripts/truth-surfaces.mjs b/scripts/truth-surfaces.mjs index 66dc6cad..924a6e23 100644 --- a/scripts/truth-surfaces.mjs +++ b/scripts/truth-surfaces.mjs @@ -3,7 +3,7 @@ /** * Truth surfaces (TIN-161). * - * Two static surfaces in this repo claimed to describe the build and lied + * Three static surfaces in this repo claimed to describe the build and lied * quietly as the build moved underneath them: * * - `THIRD-PARTY-LICENSES.md` was hand-maintained. By 2026-08-28 it listed @@ -12,13 +12,20 @@ * `mermaid` as a RUNTIME dependency it no longer is, and omitted roughly * two thirds of the direct dependency set. Nothing failed, because nothing * checked. + * - `src/routes/THIRD-PARTY-LICENSES/+page.svelte` is the copy the public + * actually reads — it is prerendered and linked from the site footer — and + * it carried the same drift plus a "243 production packages" summary that + * understated Apache-2.0 by two orders of magnitude. Regenerating only the + * Markdown would have left the published page lying, so the page is now + * rendered from `src/lib/data/third-party-licenses.json`, which this module + * generates and byte-compares alongside the Markdown. * - `docs/build-metrics.md` was captured once on 2026-02-11 against Vite * 6.4.1 on a branch that no longer exists, and was titled "Baseline" with * no marker saying the tree it measured is gone. * - * The fix is not a fresher hand-edit. It is to derive the license surface from - * a checked-in machine-readable source and to make staleness FAIL rather than - * accumulate. + * The fix is not a fresher hand-edit. It is to derive the license surfaces + * from a checked-in machine-readable source and to make staleness FAIL rather + * than accumulate. * * Derivation source: `package-lock.json` (npm lockfile v3). It is current with * `package.json` and with the pnpm resolutions, and — unlike `pnpm-lock.yaml` @@ -27,76 +34,170 @@ * declares no license is printed as UNDECLARED rather than guessed. * * Deliberately dependency-free so it runs under plain node, without an install - * step, in a pre-commit hook, in CI, or from the vitest suite. + * step, in a pre-commit hook, in CI, or from the vitest suite. The one + * exception is the provenance check, which shells out to `git` and is + * therefore only run where a `.git` directory exists; CI runs this module at + * the repository root for exactly that reason. */ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; export const LICENSES_DOC = 'THIRD-PARTY-LICENSES.md'; +export const LICENSES_DATA = 'src/lib/data/third-party-licenses.json'; export const BUILD_METRICS_DOC = 'docs/build-metrics.md'; export const GENERATED_MARKER = ''; /** Toolchain packages `docs/build-metrics.md` must name at capture time. */ export const BUILD_METRICS_TOOLCHAIN = ['vite', 'svelte', '@sveltejs/kit']; +/** + * Every manifest key that declares a direct dependency. All four are read even + * though this repository currently uses two: a surface that silently ignores + * `optionalDependencies` would publish an incomplete attribution the first time + * one was added, and nothing would say so. + */ +export const DEPENDENCY_GROUPS = [ + { key: 'dependencies', field: 'runtime', heading: 'Runtime Dependencies' }, + { key: 'devDependencies', field: 'build', heading: 'Build / Dev Dependencies' }, + { key: 'optionalDependencies', field: 'optional', heading: 'Optional Dependencies' }, + { key: 'peerDependencies', field: 'peer', heading: 'Peer Dependencies' }, +]; + const readJson = (root, relative) => JSON.parse(readFileSync(join(root, relative), 'utf-8')); const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const NODE_MODULES = 'node_modules/'; + +/** `node_modules/a/node_modules/@s/b` is an install of `@s/b`, not of `a`. */ +const packageNameFromInstallPath = (path) => path.slice(path.lastIndexOf(NODE_MODULES) + NODE_MODULES.length); + +const histogram = (packages) => { + const counts = new Map(); + for (const { license } of packages) { + const key = license ?? 'UNDECLARED'; + counts.set(key, (counts.get(key) ?? 0) + 1); + } + return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])); +}; + /** * Direct dependencies with the version and license the lockfile actually - * resolved, split into the two groups the document publishes. + * resolved, plus honest aggregate figures for the resolved tree. + * + * `lock.packages` is keyed by INSTALL PATH, not by package: a package that npm + * cannot hoist to the root is recorded once per place it lands + * (`@opentelemetry/core` has 21 such entries here). Counting keys therefore + * overstates the package count, so the tree figures deduplicate on + * `name@version` and drop workspace links, which are this repository's own code + * and carry neither a version nor a license. */ export function buildLicenseInventory(root) { const manifest = readJson(root, 'package.json'); const lock = readJson(root, 'package-lock.json'); const entries = lock.packages ?? {}; - const collect = (group) => - Object.keys(manifest[group] ?? {}) + const groups = DEPENDENCY_GROUPS.map(({ key, field, heading }) => ({ + key, + field, + heading, + rows: Object.keys(manifest[key] ?? {}) .sort() .map((name) => { - const entry = entries[`node_modules/${name}`]; + const entry = entries[`${NODE_MODULES}${name}`]; return { name, - specifier: manifest[group][name], + specifier: manifest[key][name], version: entry?.version ?? 'UNRESOLVED', license: entry?.license ?? 'UNDECLARED', resolved: entry?.resolved ?? null, }; + }), + })).filter((group) => group.rows.length > 0); + + const installs = Object.entries(entries).filter(([path]) => path.startsWith(NODE_MODULES)); + const packages = new Map(); + for (const [path, entry] of installs) { + const name = packageNameFromInstallPath(path); + const key = `${name}@${entry.version ?? 'UNRESOLVED'}`; + const existing = packages.get(key); + if (!existing) { + packages.set(key, { + name, + version: entry.version ?? null, + license: entry.license ?? null, + link: entry.link === true, + dev: entry.dev === true, }); - - const thirdParty = Object.entries(entries).filter(([path]) => path.startsWith('node_modules/')); - const histogram = new Map(); - for (const [, entry] of thirdParty) { - const license = entry.license ?? 'UNDECLARED'; - histogram.set(license, (histogram.get(license) ?? 0) + 1); + continue; + } + // One package installed in several places is still one package, and it + // is a runtime package if ANY of those installs is outside the dev tree. + existing.dev = existing.dev && entry.dev === true; + existing.license ??= entry.license ?? null; } + const resolved = [...packages.values()]; + const thirdParty = resolved.filter((entry) => !entry.link); + const workspace = resolved.filter((entry) => entry.link); + const runtime = thirdParty.filter((entry) => !entry.dev); + return { - runtime: collect('dependencies'), - build: collect('devDependencies'), - resolvedPackageCount: thirdParty.length, - licenseHistogram: [...histogram.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])), - undeclared: thirdParty - .filter(([, entry]) => entry.license === undefined) - .map(([path]) => path.replace(/^node_modules\//, '')) - .sort(), + groups, + ...Object.fromEntries(groups.map((group) => [group.field, group.rows])), + tree: { + installCount: installs.length, + packageCount: thirdParty.length, + runtimePackageCount: runtime.length, + licenseHistogram: histogram(thirdParty), + runtimeLicenseHistogram: histogram(runtime), + undeclared: thirdParty + .filter((entry) => entry.license === null) + .map((entry) => `${entry.name}@${entry.version}`) + .sort(), + workspacePackages: workspace.map((entry) => entry.name).sort(), + }, }; } -const row = ({ name, specifier, version, license }) => `| ${name} | \`${specifier}\` | ${version} | ${license} |`; +const row = ({ name, specifier, version, license, resolved }) => + `| ${name} | \`${specifier}\` | ${version} | ${license} | ${resolved ? `<${resolved}>` : '—'} |`; const table = (rows) => - ['| Package | Declared | Resolved | License |', '| --- | --- | --- | --- |', ...rows.map(row)].join('\n'); + [ + '| Package | Declared | Resolved | License | Source |', + '| --- | --- | --- | --- | --- |', + ...rows.map(row), + ].join('\n'); + +const histogramTable = (counts) => + ['| License | Packages |', '| --- | --- |', ...counts.map(([license, count]) => `| ${license} | ${count} |`)].join( + '\n', + ); /** Render the whole document. Pure: same inventory in, same bytes out. */ export function renderLicensesDoc(inventory) { - const histogram = inventory.licenseHistogram - .map(([license, count]) => `| ${license} | ${count} |`) - .join('\n'); + const { tree } = inventory; + const sections = inventory.groups.map((group) => `## ${group.heading}\n\n${table(group.rows)}`).join('\n\n'); + + const undeclaredSection = + tree.undeclared.length === 0 + ? 'Every resolved third-party package declares a license.' + : `${tree.undeclared.length} resolved third-party package(s) declare no license field in the lockfile:\n\n${tree.undeclared + .map((name) => `- \`${name}\``) + .join('\n')}\n\nEach one is an unreviewed license and a follow-up, not a silent MIT.`; + + const workspaceSection = + tree.workspacePackages.length === 0 + ? '' + : `\nThe lockfile also carries ${tree.workspacePackages.length} workspace link(s) — ${tree.workspacePackages + .map((name) => `\`${name}\``) + .join( + ', ', + )} — which are this repository's own code under its CC0 dedication. They are not third party and are excluded from every figure above.\n`; return `# Third-Party Licenses @@ -106,9 +207,9 @@ This project (transscendsurvival.org) is dedicated to the public domain under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). Every figure below is derived from \`package-lock.json\` — the package name and -its declared range from \`package.json\`, the resolved version and the license -string from the lockfile entry for that exact resolution. Nothing here is -hand-entered, so nothing here can drift without \`scripts/truth-surfaces.test.mts\` +its declared range from \`package.json\`, the resolved version, license string +and source URL from the lockfile entry for that exact resolution. Nothing here +is hand-entered, so nothing here can drift without \`scripts/truth-surfaces.mjs\` failing. Regenerate with: @@ -117,6 +218,10 @@ Regenerate with: node scripts/truth-surfaces.mjs --write \`\`\` +That command writes this file and \`${LICENSES_DATA}\`, which +\`src/routes/THIRD-PARTY-LICENSES/+page.svelte\` renders — the published page and +this document are the same data, so they cannot disagree. + A dependency whose lockfile entry declares no license is printed as \`UNDECLARED\`. It is never guessed from the package name, the organisation, or a sibling version. @@ -126,31 +231,53 @@ listed \`shiki 3.22.0\`, \`vite 6.4.1\`, \`svelte 5.50.1\` and \`sharp 0.33.5\` against resolutions of 4.2.0, 8.0.14, 5.55.4 and 0.34.5, carried \`mermaid\` as a runtime dependency it no longer is, and omitted most of the direct set. -## Runtime Dependencies +${sections} + +## Resolved Tree -${table(inventory.runtime)} +\`package-lock.json\` records ${tree.installCount} installs under \`node_modules/\`. +That is a count of install paths, not of packages: npm records a package once +per place it lands, so a dependency it cannot hoist appears many times over. +Deduplicated on \`name@version\`, the tree is **${tree.packageCount}** distinct +third-party packages, of which **${tree.runtimePackageCount}** are reachable +outside \`devDependencies\`. -## Build / Dev Dependencies +### Runtime Packages -${table(inventory.build)} +This is the set that governs attribution: the site is a prerendered static +build, so only these licenses travel to a reader. -## Full Resolved Tree +${histogramTable(tree.runtimeLicenseHistogram)} -${inventory.resolvedPackageCount} third-party packages are resolved into the -lockfile in total (direct and transitive). Their declared licenses: +### Full Resolved Tree -| License | Packages | -| --- | --- | -${histogram} +Development-only packages included. Licenses appearing here but not above are +build-time only and are not distributed. -${ - inventory.undeclared.length === 0 - ? 'Every resolved package declares a license.' - : `${inventory.undeclared.length} resolved package(s) declare no license field in the lockfile:\n\n${inventory.undeclared - .map((name) => `- \`${name}\``) - .join('\n')}\n\nWorkspace-local packages are covered by this repository's CC0 dedication; any\nother name on that list is an unreviewed license and is a follow-up, not a\nsilent MIT.` +${histogramTable(tree.licenseHistogram)} + +## Undeclared Licenses + +${undeclaredSection} +${workspaceSection}`; } -`; + +/** + * The same inventory as JSON, for the prerendered route. Written by `--write` + * and byte-compared by `checkTruthSurfaces`, so the published page is covered + * by the same guard as the Markdown. + */ +export function renderLicensesData(inventory) { + return `${JSON.stringify( + { + generatedBy: 'scripts/truth-surfaces.mjs', + source: 'package-lock.json', + groups: inventory.groups.map(({ key, heading, rows }) => ({ key, heading, rows })), + tree: inventory.tree, + }, + null, + '\t', + )}\n`; } /** Metadata `docs/build-metrics.md` must declare about its own capture. */ @@ -165,7 +292,14 @@ export function readBuildMetricsHeader(root) { const match = body.match(new RegExp(`^\\| \`${escapeRegExp(name)}\` \\| ([^|]+) \\|`, 'm')); toolchain[name] = match ? match[1].trim() : null; } - return { body, status: field('Status'), capturedAt: field('Captured'), toolchain }; + const commit = body.match(/^- \*\*Captured:\*\* .*\(commit `([0-9a-f]{7,40})`\)/m); + return { + body, + status: field('Status'), + capturedAt: field('Captured'), + captureCommit: commit ? commit[1] : null, + toolchain, + }; } /** @@ -177,8 +311,17 @@ export function readBuildMetricsHeader(root) { * Status must say STALE. Bumping vite without recapturing turns the page * red; recapturing on the current toolchain and updating the versions lets * it go green again. + * + * On its own this checks the label rather than the measurement, which is why + * `checkBuildMetricsProvenance` exists: without it the cheapest way to green is + * to retype the toolchain row, and the guard would reward falsifying the one + * part of the page that records historical fact. */ export function checkBuildMetricsFreshness(root) { + if (!existsSync(join(root, BUILD_METRICS_DOC))) { + return [`${BUILD_METRICS_DOC}: missing. The build metrics surface may not be deleted to silence its check.`]; + } + const violations = []; const manifest = readJson(root, 'package.json'); const declared = { ...manifest.dependencies, ...manifest.devDependencies }; @@ -222,28 +365,127 @@ export function checkBuildMetricsFreshness(root) { return violations; } -/** Everything, as a violation list. Empty means both surfaces are truthful. */ +/** Read `:` out of the repository at `root`. */ +export const gitFileReader = + (root) => + (commit, path) => { + const result = spawnSync('git', ['-C', root, 'show', `${commit}:${path}`], { encoding: 'utf-8' }); + return result.status === 0 ? result.stdout : null; + }; + +/** + * The toolchain row is provenance, not a field. + * + * `checkBuildMetricsFreshness` compares the recorded toolchain against + * `package.json` today, which means the cheapest path to green after a Vite + * bump is to retype those three cells and drop the STALE marker — without + * recapturing a single number. That turns the guard into an incentive to + * falsify history. + * + * So the row is validated against the commit the page names as its capture: + * the recorded ranges must be what `:package.json` declared. + * Editing the row now requires moving the capture commit, and moving the + * capture commit is a claim that the measurements were retaken. + * + * `readAtCommit` is injected so this is testable without a git repository; the + * default reads the real one. + */ +export function checkBuildMetricsProvenance(root, readAtCommit = gitFileReader(root)) { + if (!existsSync(join(root, BUILD_METRICS_DOC))) { + return [`${BUILD_METRICS_DOC}: missing. The build metrics surface may not be deleted to silence its check.`]; + } + + const { captureCommit, toolchain } = readBuildMetricsHeader(root); + if (!captureCommit) { + return [ + `${BUILD_METRICS_DOC}: the "- **Captured:** ..." line does not name the commit it was captured at ` + + '(expected a trailing "(commit ``)"). Without it the toolchain table is an unverifiable claim ' + + 'that can be retyped to make the freshness check pass.', + ]; + } + + const source = readAtCommit(captureCommit, 'package.json'); + if (source === null) { + return [ + `${BUILD_METRICS_DOC}: cannot read package.json at the declared capture commit \`${captureCommit}\`. ` + + 'The page names a commit that this repository does not have.', + ]; + } + + let declaredAtCapture; + try { + const parsed = JSON.parse(source); + declaredAtCapture = { ...parsed.dependencies, ...parsed.devDependencies }; + } catch { + return [`${BUILD_METRICS_DOC}: package.json at capture commit \`${captureCommit}\` is not valid JSON.`]; + } + + const violations = []; + for (const name of BUILD_METRICS_TOOLCHAIN) { + const recorded = toolchain[name]; + // A missing row is already a freshness violation; do not double-report. + if (recorded === null) continue; + const actual = declaredAtCapture[name]; + if (recorded !== actual) { + violations.push( + `${BUILD_METRICS_DOC}: records \`${name}\` at ${recorded} "at capture", but ` + + `\`${captureCommit}:package.json\` declares ` + + `${actual === undefined ? 'no such dependency' : actual}. ` + + 'The toolchain row is provenance — it must be what the capture commit declared. ' + + 'Recapture the metrics and name the new commit rather than editing the row.', + ); + } + } + return violations; +} + +/** Everything, as a violation list. Empty means every surface is truthful. */ export function checkTruthSurfaces(root) { const violations = []; - const expected = renderLicensesDoc(buildLicenseInventory(root)); - const actual = readFileSync(join(root, LICENSES_DOC), 'utf-8'); - if (actual !== expected) { - violations.push( - `${LICENSES_DOC} is out of date with package-lock.json. ` + - 'Run `node scripts/truth-surfaces.mjs --write`.', - ); + const inventory = buildLicenseInventory(root); + + for (const [surface, expected] of [ + [LICENSES_DOC, renderLicensesDoc(inventory)], + [LICENSES_DATA, renderLicensesData(inventory)], + ]) { + const path = join(root, surface); + if (!existsSync(path)) { + violations.push(`${surface}: missing. Run \`node scripts/truth-surfaces.mjs --write\`.`); + continue; + } + if (readFileSync(path, 'utf-8') !== expected) { + violations.push( + `${surface} is out of date with package-lock.json. Run \`node scripts/truth-surfaces.mjs --write\`.`, + ); + } } + violations.push(...checkBuildMetricsFreshness(root)); + // The provenance check needs git history. The Vitest sandbox is a copied + // tree with no `.git`, so CI runs this module at the repository root as + // well — see the "Truth surfaces" step in .github/workflows/ci.yml. + if (existsSync(join(root, '.git'))) { + violations.push(...checkBuildMetricsProvenance(root)); + } + return violations; } -/* CLI: `--write` regenerates the license doc; no flag checks both surfaces. */ +/* CLI: `--write` regenerates the license surfaces; no flag checks all three. */ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const root = process.cwd(); if (process.argv.includes('--write')) { - const { writeFileSync } = await import('node:fs'); - writeFileSync(join(root, LICENSES_DOC), renderLicensesDoc(buildLicenseInventory(root))); - process.stdout.write(`wrote ${LICENSES_DOC}\n`); + const { mkdirSync, writeFileSync } = await import('node:fs'); + const { dirname } = await import('node:path'); + const inventory = buildLicenseInventory(root); + for (const [surface, contents] of [ + [LICENSES_DOC, renderLicensesDoc(inventory)], + [LICENSES_DATA, renderLicensesData(inventory)], + ]) { + mkdirSync(dirname(join(root, surface)), { recursive: true }); + writeFileSync(join(root, surface), contents); + process.stdout.write(`wrote ${surface}\n`); + } } else { const violations = checkTruthSurfaces(root); if (violations.length === 0) { diff --git a/scripts/truth-surfaces.test.mts b/scripts/truth-surfaces.test.mts index 0bb9874d..1c82c6b6 100644 --- a/scripts/truth-surfaces.test.mts +++ b/scripts/truth-surfaces.test.mts @@ -1,24 +1,30 @@ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { BUILD_METRICS_DOC, + DEPENDENCY_GROUPS, + LICENSES_DATA, LICENSES_DOC, buildLicenseInventory, checkBuildMetricsFreshness, + checkBuildMetricsProvenance, checkTruthSurfaces, + renderLicensesData, renderLicensesDoc, } from './truth-surfaces.mjs'; /** - * TIN-161 — the two static surfaces that claimed to describe the build. + * TIN-161 — the static surfaces that claimed to describe the build. * * `THIRD-PARTY-LICENSES.md` was hand-maintained and had drifted years of - * versions; `docs/build-metrics.md` was a February snapshot titled "Baseline". - * Neither could fail, because nothing checked them. These tests are the check, - * and half of them exist to prove the check can go red: a guard that has only - * ever been seen passing is an unfailable assertion. + * versions; `src/routes/THIRD-PARTY-LICENSES/+page.svelte` is the copy the + * public actually reads and had drifted the same way; `docs/build-metrics.md` + * was a February snapshot titled "Baseline". None could fail, because nothing + * checked them. These tests are the check, and most of them exist to prove the + * check can go red: a guard that has only ever been seen passing is an + * unfailable assertion. */ const temps: string[] = []; @@ -28,18 +34,25 @@ afterEach(() => { } }); +type LockEntry = { version?: string; license?: string; resolved?: string; dev?: boolean; link?: boolean }; + type Fixture = { + manifest?: Record; dependencies?: Record; devDependencies?: Record; - lock?: Record; + /** Keyed by install path; a bare name is shorthand for `node_modules/`. */ + lock?: Record; metrics?: string; licensesDoc?: string; + licensesData?: string; }; +const CAPTURE_COMMIT = '9aac474'; + const METRICS_TEMPLATE = (status: string, vite: string, svelte: string, kit: string) => `# Build Metrics Baseline - **Status:** ${status} -- **Captured:** 2026-02-11 on branch \`feature/sprint3-week8\` +- **Captured:** 2026-02-11 on branch \`feature/sprint3-week8\` (commit \`${CAPTURE_COMMIT}\`) ## Toolchain At Capture @@ -50,47 +63,77 @@ const METRICS_TEMPLATE = (status: string, vite: string, svelte: string, kit: str | \`@sveltejs/kit\` | ${kit} | `; -function fixture({ dependencies = {}, devDependencies = {}, lock = {}, metrics, licensesDoc }: Fixture): string { +/** A `readAtCommit` that answers for one commit only, like git history does. */ +const commitReader = (commit: string, manifest: Record) => (asked: string, path: string) => + asked === commit && path === 'package.json' ? JSON.stringify(manifest) : null; + +const write = (root: string, relative: string, contents: string) => { + mkdirSync(dirname(join(root, relative)), { recursive: true }); + writeFileSync(join(root, relative), contents); +}; + +function fixture({ + manifest, + dependencies = {}, + devDependencies = {}, + lock = {}, + metrics, + licensesDoc, + licensesData, +}: Fixture): string { const root = mkdtempSync(join(tmpdir(), 'truth-surfaces-')); temps.push(root); - writeFileSync(join(root, 'package.json'), JSON.stringify({ dependencies, devDependencies }, null, 2)); - writeFileSync( - join(root, 'package-lock.json'), + write(root, 'package.json', JSON.stringify(manifest ?? { dependencies, devDependencies }, null, 2)); + write( + root, + 'package-lock.json', JSON.stringify( { lockfileVersion: 3, - packages: Object.fromEntries(Object.entries(lock).map(([name, entry]) => [`node_modules/${name}`, entry])), + packages: Object.fromEntries( + Object.entries(lock).map(([path, entry]) => [ + path.startsWith('node_modules/') ? path : `node_modules/${path}`, + entry, + ]), + ), }, null, 2, ), ); - mkdirSync(join(root, 'docs'), { recursive: true }); - writeFileSync(join(root, BUILD_METRICS_DOC), metrics ?? METRICS_TEMPLATE('CURRENT', '^6.4.1', '^5.19.0', '^2.16.0')); - writeFileSync( - join(root, LICENSES_DOC), - licensesDoc ?? renderLicensesDoc(buildLicenseInventory(root)), - ); + write(root, BUILD_METRICS_DOC, metrics ?? METRICS_TEMPLATE('CURRENT', '^6.4.1', '^5.19.0', '^2.16.0')); + const inventory = buildLicenseInventory(root); + write(root, LICENSES_DOC, licensesDoc ?? renderLicensesDoc(inventory)); + write(root, LICENSES_DATA, licensesData ?? renderLicensesData(inventory)); return root; } describe('license surface is derived, not maintained', () => { - it('reads version and license from the lockfile entry for the resolved version', () => { + it('reads version, license and source from the lockfile entry for the resolved version', () => { const root = fixture({ dependencies: { shiki: '^4.2.0' }, devDependencies: { vite: '^8.0.14' }, lock: { - shiki: { version: '4.2.0', license: 'MIT' }, - vite: { version: '8.0.14', license: 'MIT' }, + shiki: { version: '4.2.0', license: 'MIT', resolved: 'https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz' }, + vite: { version: '8.0.14', license: 'MIT', dev: true }, }, }); const inventory = buildLicenseInventory(root); expect(inventory.runtime).toEqual([ - { name: 'shiki', specifier: '^4.2.0', version: '4.2.0', license: 'MIT', resolved: null }, + { + name: 'shiki', + specifier: '^4.2.0', + version: '4.2.0', + license: 'MIT', + resolved: 'https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz', + }, ]); expect(inventory.build[0]).toMatchObject({ name: 'vite', version: '8.0.14' }); + // `resolved` is the only field that reveals a package that did not come + // from the registry, so the document has to print it. + expect(renderLicensesDoc(inventory)).toContain(''); }); it('prints UNDECLARED rather than guessing when the lockfile declares no license', () => { @@ -100,13 +143,120 @@ describe('license surface is derived, not maintained', () => { }); const inventory = buildLicenseInventory(root); expect(inventory.runtime[0].license).toBe('UNDECLARED'); - expect(inventory.undeclared).toEqual(['khroma']); + expect(inventory.tree.undeclared).toEqual(['khroma@2.1.0']); expect(renderLicensesDoc(inventory)).toContain('UNDECLARED'); // The failure mode this replaces: a hand-written MIT for a package // nobody read the license of. expect(renderLicensesDoc(inventory)).not.toContain('| khroma | `^2.1.0` | 2.1.0 | MIT |'); }); + it('publishes every dependency group the manifest declares, not just the two this repo uses', () => { + const root = fixture({ + manifest: { + dependencies: { shiki: '^4.2.0' }, + devDependencies: { vite: '^8.0.14' }, + optionalDependencies: { fsevents: '^2.3.3' }, + peerDependencies: { svelte: '^5.55.4' }, + }, + lock: { + shiki: { version: '4.2.0', license: 'MIT' }, + vite: { version: '8.0.14', license: 'MIT' }, + fsevents: { version: '2.3.3', license: 'MIT' }, + svelte: { version: '5.55.4', license: 'MIT' }, + }, + }); + const inventory = buildLicenseInventory(root); + expect(inventory.optional).toEqual([ + { name: 'fsevents', specifier: '^2.3.3', version: '2.3.3', license: 'MIT', resolved: null }, + ]); + expect(inventory.peer[0]).toMatchObject({ name: 'svelte', version: '5.55.4' }); + // An omitted group used to be invisible: the package simply never + // appeared, and no figure moved. + const rendered = renderLicensesDoc(inventory); + expect(rendered).toContain('## Optional Dependencies'); + expect(rendered).toContain('| fsevents |'); + expect(rendered).toContain('## Peer Dependencies'); + }); + + it('omits the heading for a dependency group the manifest does not declare', () => { + const root = fixture({ dependencies: { shiki: '^4.2.0' }, lock: { shiki: { version: '4.2.0', license: 'MIT' } } }); + expect(renderLicensesDoc(buildLicenseInventory(root))).not.toContain('Optional Dependencies'); + }); + + it('counts a package installed at several paths once, not once per path', () => { + const root = fixture({ + dependencies: { a: '^1.0.0' }, + lock: { + a: { version: '1.0.0', license: 'MIT' }, + 'node_modules/a/node_modules/dup': { version: '2.0.0', license: 'ISC' }, + 'node_modules/b/node_modules/dup': { version: '2.0.0', license: 'ISC' }, + 'node_modules/c/node_modules/dup': { version: '3.0.0', license: 'ISC' }, + }, + }); + const { tree } = buildLicenseInventory(root); + // Four install paths, three packages: `dup@2.0.0` lands twice. + expect(tree.installCount).toBe(4); + expect(tree.packageCount).toBe(3); + expect(tree.licenseHistogram).toEqual([ + ['ISC', 2], + ['MIT', 1], + ]); + }); + + it("excludes this repository's own workspace links from the third-party figures", () => { + const root = fixture({ + dependencies: { a: '^1.0.0' }, + lock: { + a: { version: '1.0.0', license: 'MIT' }, + '@blog/pulse-core': { link: true }, + '@blog/pulse-client': { link: true }, + }, + }); + const { tree } = buildLicenseInventory(root); + expect(tree.packageCount).toBe(1); + expect(tree.workspacePackages).toEqual(['@blog/pulse-client', '@blog/pulse-core']); + // Workspace links carry no license field. Counting them as third party + // published this repo's own code as an unlicensed dependency. + expect(tree.undeclared).toEqual([]); + }); + + it('separates the runtime histogram from the dev-inclusive one', () => { + const root = fixture({ + dependencies: { a: '^1.0.0' }, + devDependencies: { b: '^1.0.0' }, + lock: { + a: { version: '1.0.0', license: 'MIT' }, + b: { version: '1.0.0', license: 'LGPL-3.0-or-later', dev: true }, + }, + }); + const { tree } = buildLicenseInventory(root); + expect(tree.runtimePackageCount).toBe(1); + expect(tree.runtimeLicenseHistogram).toEqual([['MIT', 1]]); + // A copyleft build tool is not distributed by a prerendered static + // site, and blending it into one number made the attribution useless. + expect(tree.licenseHistogram).toEqual([ + ['LGPL-3.0-or-later', 1], + ['MIT', 1], + ]); + }); + + it('treats a package as runtime when any of its installs is outside the dev tree', () => { + const root = fixture({ + dependencies: { a: '^1.0.0' }, + lock: { + a: { version: '1.0.0', license: 'MIT' }, + 'node_modules/tool/node_modules/shared': { version: '1.0.0', license: 'ISC', dev: true }, + 'node_modules/a/node_modules/shared': { version: '1.0.0', license: 'ISC' }, + }, + }); + const { tree } = buildLicenseInventory(root); + expect(tree.runtimePackageCount).toBe(2); + expect(tree.runtimeLicenseHistogram).toEqual([ + ['ISC', 1], + ['MIT', 1], + ]); + }); + it('goes red when a resolved version moves and the document does not', () => { const root = fixture({ dependencies: { shiki: '^4.2.0' }, @@ -131,13 +281,45 @@ describe('license surface is derived, not maintained', () => { expect(checkTruthSurfaces(root).join('\n')).toMatch(/THIRD-PARTY-LICENSES\.md is out of date/); }); - it('is deterministic: rendering twice produces identical bytes', () => { + it('goes red when the data the published page renders drifts, not just the Markdown', () => { const root = fixture({ - dependencies: { marked: '^4.3.0' }, - lock: { marked: { version: '4.3.0', license: 'MIT' } }, + dependencies: { shiki: '^4.2.0' }, + devDependencies: { vite: '^6.4.1', svelte: '^5.19.0', '@sveltejs/kit': '^2.16.0' }, + lock: { shiki: { version: '4.2.0', license: 'MIT' } }, }); - const inventory = buildLicenseInventory(root); - expect(renderLicensesDoc(inventory)).toBe(renderLicensesDoc(buildLicenseInventory(root))); + expect(checkTruthSurfaces(root)).toEqual([]); + + // The original defect: the Markdown was regenerated and the prerendered + // route — the copy the public reads — was left behind. + writeFileSync(join(root, LICENSES_DATA), '{"generatedBy":"a hand"}\n'); + expect(checkTruthSurfaces(root).join('\n')).toMatch(/third-party-licenses\.json is out of date/); + }); + + it('reports a missing surface as a violation instead of throwing', () => { + const root = fixture({ dependencies: { shiki: '^4.2.0' }, lock: { shiki: { version: '4.2.0', license: 'MIT' } } }); + rmSync(join(root, LICENSES_DOC)); + rmSync(join(root, LICENSES_DATA)); + const violations = checkTruthSurfaces(root).join('\n'); + expect(violations).toMatch(/THIRD-PARTY-LICENSES\.md: missing/); + expect(violations).toMatch(/third-party-licenses\.json: missing/); + }); + + it('renders the same bytes when the manifest and lockfile key order is reversed', () => { + // The claim worth testing is stability across input order, not that a + // pure function called twice agrees with itself — that cannot fail. + const dependencies = { shiki: '^4.2.0', marked: '^4.3.0', dompurify: '^3.4.13' }; + const lock = { + shiki: { version: '4.2.0', license: 'MIT' }, + marked: { version: '4.3.0', license: 'MIT' }, + dompurify: { version: '3.4.13', license: '(MPL-2.0 OR Apache-2.0)' }, + }; + const reverse = (value: Record) => Object.fromEntries(Object.entries(value).reverse()); + + const forward = renderLicensesDoc(buildLicenseInventory(fixture({ dependencies, lock }))); + const backward = renderLicensesDoc( + buildLicenseInventory(fixture({ dependencies: reverse(dependencies), lock: reverse(lock) })), + ); + expect(backward).toBe(forward); }); }); @@ -184,9 +366,69 @@ describe('build-metrics freshness fails in both directions', () => { const root = fixture({ devDependencies: deps, metrics: '# Build Metrics Baseline\n\nnumbers\n' }); expect(checkBuildMetricsFreshness(root).join('\n')).toMatch(/no "- \*\*Status:\*\* \.\.\." line/); }); + + it('reports a deleted metrics page as a violation instead of throwing', () => { + const root = fixture({ devDependencies: deps }); + rmSync(join(root, BUILD_METRICS_DOC)); + expect(checkBuildMetricsFreshness(root).join('\n')).toMatch(/docs\/build-metrics\.md: missing/); + }); }); -describe('this repository satisfies both surfaces', () => { +describe('the toolchain row is provenance, not a field', () => { + const capture = { devDependencies: { vite: '^6.4.1', svelte: '^5.19.0', '@sveltejs/kit': '^2.16.0' } }; + + it('passes when the recorded row is what the capture commit declared', () => { + const root = fixture({ devDependencies: capture.devDependencies }); + expect(checkBuildMetricsProvenance(root, commitReader(CAPTURE_COMMIT, capture))).toEqual([]); + }); + + it('fails when the row is retyped to whatever makes the freshness check green', () => { + // Without this, the cheapest path to green after a Vite bump is to edit + // three cells and drop the STALE marker, without recapturing a single + // number — the guard would reward falsifying the provenance. + const root = fixture({ + devDependencies: { vite: '^8.0.14', svelte: '^5.55.4', '@sveltejs/kit': '^2.61.1' }, + metrics: METRICS_TEMPLATE('CURRENT', '^8.0.14', '^5.55.4', '^2.61.1'), + }); + expect(checkBuildMetricsFreshness(root)).toEqual([]); + const violations = checkBuildMetricsProvenance(root, commitReader(CAPTURE_COMMIT, capture)).join('\n'); + expect(violations).toMatch(/records `vite` at \^8\.0\.14 "at capture", but `9aac474:package\.json` declares \^6\.4\.1/); + expect(violations).toMatch(/records `@sveltejs\/kit` at \^2\.61\.1/); + }); + + it('fails when the page will not name the commit it was captured at', () => { + const root = fixture({ + devDependencies: capture.devDependencies, + metrics: METRICS_TEMPLATE('CURRENT', '^6.4.1', '^5.19.0', '^2.16.0').replace(/ \(commit `[0-9a-f]+`\)/, ''), + }); + expect(checkBuildMetricsProvenance(root, commitReader(CAPTURE_COMMIT, capture)).join('\n')).toMatch( + /does not name the commit it was captured at/, + ); + }); + + it('fails when the named commit is not in this repository', () => { + const root = fixture({ + devDependencies: capture.devDependencies, + metrics: METRICS_TEMPLATE('CURRENT', '^6.4.1', '^5.19.0', '^2.16.0').replace(CAPTURE_COMMIT, 'deadbee'), + }); + expect(checkBuildMetricsProvenance(root, commitReader(CAPTURE_COMMIT, capture)).join('\n')).toMatch( + /cannot read package\.json at the declared capture commit `deadbee`/, + ); + }); + + it('is wired into checkTruthSurfaces wherever git history is available', () => { + // Proving the wiring, not just the function: the Vitest sandbox is a + // copied tree with no `.git`, so an unwired provenance check would be + // invisible to every other case here. This fixture has a `.git`, so + // checkTruthSurfaces must reach for history — and fail, because the + // fixture is not a real repository. + const root = fixture({ devDependencies: capture.devDependencies }); + mkdirSync(join(root, '.git'), { recursive: true }); + expect(checkTruthSurfaces(root).join('\n')).toMatch(/cannot read package\.json at the declared capture commit/); + }); +}); + +describe('this repository satisfies every surface', () => { it('has no truth-surface violations', () => { expect(checkTruthSurfaces(process.cwd())).toEqual([]); }); @@ -194,10 +436,31 @@ describe('this repository satisfies both surfaces', () => { it('publishes a license row for every direct dependency, none of them guessed', () => { const inventory = buildLicenseInventory(process.cwd()); const declared = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8')); - const directCount = - Object.keys(declared.dependencies ?? {}).length + Object.keys(declared.devDependencies ?? {}).length; + // Derived from the manifest rather than restating the two keys the + // implementation reads: a group the implementation ignores has to show + // up as a count mismatch, not as silence. + const directCount = Object.entries(declared) + .filter(([key, value]) => key.endsWith('ependencies') && value !== null && typeof value === 'object') + .reduce((total, [, value]) => total + Object.keys(value as Record).length, 0); + const published = inventory.groups.reduce((total, group) => total + group.rows.length, 0); expect(directCount).toBeGreaterThan(0); - expect(inventory.runtime.length + inventory.build.length).toBe(directCount); - expect(inventory.runtime.concat(inventory.build).filter((row) => row.version === 'UNRESOLVED')).toEqual([]); + expect(published).toBe(directCount); + expect(inventory.groups.flatMap((group) => group.rows).filter((row) => row.version === 'UNRESOLVED')).toEqual([]); + }); + + it('knows about every dependency group the manifest could declare', () => { + const declared = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8')); + const known = new Set(DEPENDENCY_GROUPS.map((group) => group.key)); + const unknown = Object.keys(declared).filter((key) => key.endsWith('ependencies') && !known.has(key)); + expect(unknown).toEqual([]); + }); + + it('renders the published route from the generated inventory rather than hand-written rows', () => { + const page = readFileSync(join(process.cwd(), 'src/routes/THIRD-PARTY-LICENSES/+page.svelte'), 'utf-8'); + expect(existsSync(join(process.cwd(), LICENSES_DATA))).toBe(true); + expect(page).toContain("import inventory from '$lib/data/third-party-licenses.json'"); + // The defect this replaces: 30-odd hand-written rows that drifted + // independently of the Markdown they claimed to summarise. + expect(page).not.toMatch(/]*>\s*\d+\.\d+\.\d+\s*<\/td>/); }); }); diff --git a/src/lib/data/third-party-licenses.json b/src/lib/data/third-party-licenses.json new file mode 100644 index 00000000..3e703512 --- /dev/null +++ b/src/lib/data/third-party-licenses.json @@ -0,0 +1,466 @@ +{ + "generatedBy": "scripts/truth-surfaces.mjs", + "source": "package-lock.json", + "groups": [ + { + "key": "dependencies", + "heading": "Runtime Dependencies", + "rows": [ + { + "name": "@fontsource/crimson-pro", + "specifier": "^5.2.8", + "version": "5.2.8", + "license": "OFL-1.1", + "resolved": "https://registry.npmjs.org/@fontsource/crimson-pro/-/crimson-pro-5.2.8.tgz" + }, + { + "name": "@fontsource/fira-code", + "specifier": "^5.2.7", + "version": "5.2.7", + "license": "OFL-1.1", + "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.7.tgz" + }, + { + "name": "@fontsource/inter", + "specifier": "^5.2.8", + "version": "5.2.8", + "license": "OFL-1.1", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz" + }, + { + "name": "@fontsource/raleway", + "specifier": "^5.2.8", + "version": "5.2.8", + "license": "OFL-1.1", + "resolved": "https://registry.npmjs.org/@fontsource/raleway/-/raleway-5.2.8.tgz" + }, + { + "name": "@skeletonlabs/skeleton", + "specifier": "5.0.0", + "version": "5.0.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton/-/skeleton-5.0.0.tgz" + }, + { + "name": "@skeletonlabs/skeleton-svelte", + "specifier": "5.0.0", + "version": "5.0.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton-svelte/-/skeleton-svelte-5.0.0.tgz" + }, + { + "name": "@tummycrypt/tinyvectors", + "specifier": "https://github.com/tinyland-inc/tinyvectors/archive/refs/tags/v0.3.5.tar.gz", + "version": "0.3.5", + "license": "Zlib", + "resolved": "https://github.com/tinyland-inc/tinyvectors/archive/refs/tags/v0.3.5.tar.gz" + }, + { + "name": "dompurify", + "specifier": "^3.4.13", + "version": "3.4.13", + "license": "(MPL-2.0 OR Apache-2.0)", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz" + }, + { + "name": "flexsearch", + "specifier": "^0.8.212", + "version": "0.8.212", + "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.8.212.tgz" + }, + { + "name": "marked", + "specifier": "^4.3.0", + "version": "4.3.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" + }, + { + "name": "mdsvex", + "specifier": "^0.12.7", + "version": "0.12.7", + "license": "MIT", + "resolved": "https://registry.npmjs.org/mdsvex/-/mdsvex-0.12.7.tgz" + }, + { + "name": "shiki", + "specifier": "^4.2.0", + "version": "4.2.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz" + }, + { + "name": "tailwindcss", + "specifier": "^4.2.2", + "version": "4.2.2", + "license": "MIT", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz" + } + ] + }, + { + "key": "devDependencies", + "heading": "Build / Dev Dependencies", + "rows": [ + { + "name": "@eslint/js", + "specifier": "^9.39.2", + "version": "9.39.4", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz" + }, + { + "name": "@mermaid-js/mermaid-cli", + "specifier": "^11.15.0", + "version": "11.15.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-cli/-/mermaid-cli-11.15.0.tgz" + }, + { + "name": "@playwright/test", + "specifier": "^1.59.1", + "version": "1.59.1", + "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz" + }, + { + "name": "@sveltejs/adapter-node", + "specifier": "^5.5.7", + "version": "5.5.7", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.7.tgz" + }, + { + "name": "@sveltejs/adapter-static", + "specifier": "^3.0.10", + "version": "3.0.10", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz" + }, + { + "name": "@sveltejs/kit", + "specifier": "^2.61.1", + "version": "2.61.1", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.61.1.tgz" + }, + { + "name": "@sveltejs/vite-plugin-svelte", + "specifier": "^7.1.2", + "version": "7.1.2", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz" + }, + { + "name": "@tailwindcss/vite", + "specifier": "^4.2.2", + "version": "4.2.2", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz" + }, + { + "name": "@tummycrypt/vite-plugin-a11y", + "specifier": "^0.2.2", + "version": "0.2.2", + "license": "Zlib AND LicenseRef-Tinyland-Proprietary", + "resolved": "https://registry.npmjs.org/@tummycrypt/vite-plugin-a11y/-/vite-plugin-a11y-0.2.2.tgz" + }, + { + "name": "@types/node", + "specifier": "^25.2.3", + "version": "25.5.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz" + }, + { + "name": "@vitest/coverage-v8", + "specifier": "^4.1.4", + "version": "4.1.4", + "license": "MIT", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz" + }, + { + "name": "eslint", + "specifier": "^9.39.2", + "version": "9.39.4", + "license": "MIT", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz" + }, + { + "name": "eslint-config-prettier", + "specifier": "^10.1.8", + "version": "10.1.8", + "license": "MIT", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz" + }, + { + "name": "eslint-plugin-svelte", + "specifier": "^3.17.0", + "version": "3.17.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.17.0.tgz" + }, + { + "name": "pagefind", + "specifier": "^1.5.2", + "version": "1.5.2", + "license": "MIT", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz" + }, + { + "name": "prettier", + "specifier": "^3.8.1", + "version": "3.8.1", + "license": "MIT", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz" + }, + { + "name": "prettier-plugin-svelte", + "specifier": "^3.4.1", + "version": "3.5.1", + "license": "MIT", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz" + }, + { + "name": "puppeteer", + "specifier": "^24.43.1", + "version": "24.43.1", + "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz" + }, + { + "name": "rehype-slug", + "specifier": "^6.0.0", + "version": "6.0.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz" + }, + { + "name": "rollup-plugin-visualizer", + "specifier": "^6.0.5", + "version": "6.0.11", + "license": "MIT", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.11.tgz" + }, + { + "name": "serve", + "specifier": "^14.2.5", + "version": "14.2.6", + "license": "MIT", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz" + }, + { + "name": "sharp", + "specifier": "^0.34.5", + "version": "0.34.5", + "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz" + }, + { + "name": "svelte", + "specifier": "^5.55.4", + "version": "5.55.4", + "license": "MIT", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz" + }, + { + "name": "svelte-check", + "specifier": "^4.4.5", + "version": "4.4.5", + "license": "MIT", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz" + }, + { + "name": "tsx", + "specifier": "^4.21.0", + "version": "4.21.0", + "license": "MIT", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz" + }, + { + "name": "typescript", + "specifier": "^5.9.3", + "version": "5.9.3", + "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" + }, + { + "name": "typescript-eslint", + "specifier": "^8.58.2", + "version": "8.58.2", + "license": "MIT", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz" + }, + { + "name": "vite", + "specifier": "^8.0.14", + "version": "8.0.14", + "license": "MIT", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz" + }, + { + "name": "vitest", + "specifier": "^4.1.4", + "version": "4.1.4", + "license": "MIT", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz" + } + ] + } + ], + "tree": { + "installCount": 1150, + "packageCount": 1078, + "runtimePackageCount": 361, + "licenseHistogram": [ + [ + "MIT", + 769 + ], + [ + "Apache-2.0", + 162 + ], + [ + "ISC", + 63 + ], + [ + "BSD-3-Clause", + 29 + ], + [ + "MPL-2.0", + 12 + ], + [ + "BSD-2-Clause", + 10 + ], + [ + "LGPL-3.0-or-later", + 10 + ], + [ + "OFL-1.1", + 4 + ], + [ + "Apache-2.0 AND LGPL-3.0-or-later", + 3 + ], + [ + "UNDECLARED", + 2 + ], + [ + "(BSD-2-Clause OR MIT OR Apache-2.0)", + 1 + ], + [ + "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + 1 + ], + [ + "(MIT AND Zlib)", + 1 + ], + [ + "(MIT OR CC0-1.0)", + 1 + ], + [ + "(MPL-2.0 OR Apache-2.0)", + 1 + ], + [ + "(WTFPL OR MIT)", + 1 + ], + [ + "0BSD", + 1 + ], + [ + "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + 1 + ], + [ + "BlueOak-1.0.0", + 1 + ], + [ + "EPL-2.0", + 1 + ], + [ + "Python-2.0", + 1 + ], + [ + "Unlicense", + 1 + ], + [ + "Zlib", + 1 + ], + [ + "Zlib AND LicenseRef-Tinyland-Proprietary", + 1 + ] + ], + "runtimeLicenseHistogram": [ + [ + "MIT", + 225 + ], + [ + "Apache-2.0", + 106 + ], + [ + "BSD-3-Clause", + 11 + ], + [ + "ISC", + 10 + ], + [ + "OFL-1.1", + 4 + ], + [ + "(MPL-2.0 OR Apache-2.0)", + 1 + ], + [ + "0BSD", + 1 + ], + [ + "BSD-2-Clause", + 1 + ], + [ + "UNDECLARED", + 1 + ], + [ + "Zlib", + 1 + ] + ], + "undeclared": [ + "@better-fetch/fetch@1.1.21", + "khroma@2.1.0" + ], + "workspacePackages": [ + "@blog/agent", + "@blog/pulse-client", + "@blog/pulse-core" + ] + } +} diff --git a/src/routes/THIRD-PARTY-LICENSES/+page.svelte b/src/routes/THIRD-PARTY-LICENSES/+page.svelte index 9fd7a424..5e2f0e25 100644 --- a/src/routes/THIRD-PARTY-LICENSES/+page.svelte +++ b/src/routes/THIRD-PARTY-LICENSES/+page.svelte @@ -1,3 +1,19 @@ + + Third-Party Licenses | transscendsurvival.org @@ -10,87 +26,150 @@

This site is dedicated to the - public domain (CC0 1.0). - It is built with the following open-source software. + public domain (CC0 1.0). It is built with the following open-source software.

-

Runtime Dependencies

-
- - - - - - - - - - - - - - - - -
PackageVersionLicense
@skeletonlabs/skeleton5.0.0MIT
@skeletonlabs/skeleton-svelte5.0.0MIT
mdsvex0.12.6MIT
mermaid11.12.2MIT
shiki3.22.0MIT
tailwindcss4.1.18MIT
-
+

+ Every row below is derived from {inventory.source} — the declared range from + package.json, the resolved version, the license string and the source URL from the lockfile entry for + that exact resolution. Nothing on this page is hand-entered. A package whose lockfile entry declares no license + reads UNDECLARED and is never guessed. +

+ + {#each inventory.groups as group (group.key)} +

{group.heading}

+
+ + + + + + + + + + + + {#each group.rows as row (row.name)} + + + + + + + + {/each} + +
PackageDeclaredResolvedLicenseSource
+ {row.name} + {row.specifier}{row.version}{row.license} + {#if sourceLabel(row.resolved)} + {sourceLabel(row.resolved)} + {:else} + — + {/if} +
+
+ {/each} + +

Resolved Tree

+

+ The lockfile records {inventory.tree.installCount} installs under node_modules/. That counts install + paths, not packages — npm records a package once per place it lands. Deduplicated on name@version, + the tree is {inventory.tree.packageCount} distinct third-party packages, of which + {inventory.tree.runtimePackageCount} are reachable outside + devDependencies. +

-

Build Dependencies

+

Runtime packages

+

+ This is the set that governs attribution: the site is a prerendered static build, so only these licenses travel to + a reader. +

- - + - - - - - - - - - - - + {#each inventory.tree.runtimeLicenseHistogram as [license, count] (license)} + + + + + {/each}
PackageVersion LicensePackages
@sveltejs/adapter-static3.0.10MIT
@sveltejs/enhanced-img0.10.0MIT
@sveltejs/kit2.50.2MIT
@sveltejs/vite-plugin-svelte6.2.4MIT
@tailwindcss/vite4.1.18MIT
pagefind1.4.0MIT
sharp0.33.5Apache-2.0
svelte5.50.1MIT
svelte-check4.3.6MIT
typescript5.9.3Apache-2.0
vite6.4.1MIT
{license}{count}
-

Notable Transitive Dependencies

+

Full resolved tree

+

+ Development-only packages included. Licenses appearing here but not above are build-time only and are not + distributed. +

- - + - - - - - + {#each inventory.tree.licenseHistogram as [license, count] (license)} + + + + + {/each}
Package LicenseNotesPackages
chevrotainApache-2.0Parser toolkit (mermaid dep)
d3ISC / BSD-3-ClauseVisualization (mermaid dep)
dompurifyMPL-2.0 OR Apache-2.0HTML sanitizer (mermaid dep)
@iconify/utilsMITIcon utilities (skeleton dep)
@floating-ui/domMITPositioning runtime (Skeleton/Zag dep)
{license}{count}
-

License Summary

-

- All 243 production packages: MIT (190), ISC (34), - Apache-2.0 (8), BSD-3-Clause (6), - MPL-2.0 OR Apache-2.0 (1), Unlicense (1). -

+

Undeclared licenses

+ {#if inventory.tree.undeclared.length === 0} +

+ Every resolved third-party package declares a license. +

+ {:else} +

+ {inventory.tree.undeclared.length} resolved third-party package(s) declare no license field in the lockfile. Each + is an unreviewed license and a follow-up, not a silent MIT. +

+
    + {#each inventory.tree.undeclared as name (name)} +
  • {name}
  • + {/each} +
+ {/if} + + {#if inventory.tree.workspacePackages.length > 0} +

+ The lockfile also carries {inventory.tree.workspacePackages.length} workspace link(s) — + {inventory.tree.workspacePackages.join(', ')} — which are this repository's own code under its CC0 dedication. + They are not third party and are excluded from every figure above. +

+ {/if}

- Full machine-readable SBOM available at - THIRD-PARTY-LICENSES.md - in the source repository. + This page and + THIRD-PARTY-LICENSES.md + are generated from the same inventory by scripts/truth-surfaces.mjs, which fails if either drifts from + {inventory.source}.

diff --git a/vitest.bazel.config.ts b/vitest.bazel.config.ts index 5f73fd61..71d03e67 100644 --- a/vitest.bazel.config.ts +++ b/vitest.bazel.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ include: [ 'src/**/*.test.ts', 'scripts/gf-reapi-bazel-credential-helper.test.mts', + 'scripts/truth-surfaces.test.mts', 'scripts/wayback-utils.test.mts', 'packages/pulse-core/test/**/*.test.ts', 'packages/pulse-client/test/**/*.test.ts',