From 9933125feaa86eb2765c1e638392ff26a5342da4 Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers Date: Mon, 14 Sep 2026 08:56:09 +0200 Subject: [PATCH 1/3] test: hold README and CONTRIBUTING to the repository they describe Extract every npm script, backtick path, coverage percentage and Node version the two documents name, and check each against package.json, the tree, vitest.config.ts and .nvmrc. The link lint already resolves Markdown links and anchors; this covers the claims that never sit inside one. README's Node and coverage lines were vague enough to dodge drift by never stating a number. Made them concrete so there is something to hold, and corrected the coverage sentence to say verify enforces the ratchet rather than test. --- README.md | 6 +- scripts/lint-docs.ts | 219 +++++++++++++++++++++++ test/docs-contract.test.ts | 347 +++++++++++++++++++++++++++++++++++++ 3 files changed, 570 insertions(+), 2 deletions(-) create mode 100644 scripts/lint-docs.ts create mode 100644 test/docs-contract.test.ts diff --git a/README.md b/README.md index ee13883..dae14d7 100644 --- a/README.md +++ b/README.md @@ -79,13 +79,15 @@ business. The parked direction work is in ## Local checks ```bash -nvm use # the exact Node version in .nvmrc +nvm use # Node 24.21.0, pinned in .nvmrc npm ci npm run verify # lint, format, typecheck, ADR contract, tests + coverage ``` `npm run lint:adrs` alone runs the decision-record contract, and `npm test` -runs the suite without the coverage thresholds. +runs the suite without enforcing coverage. `npm run test:coverage` (part of +`npm run verify`) enforces the ratchet in `vitest.config.ts`: statements +96.9%, branches 86.52%, functions 100%, lines 96.59%. ## Conventions diff --git a/scripts/lint-docs.ts b/scripts/lint-docs.ts new file mode 100644 index 0000000..f6526bf --- /dev/null +++ b/scripts/lint-docs.ts @@ -0,0 +1,219 @@ +// Docs contract: holds README.md and CONTRIBUTING.md to the repository they +// describe. Prose goes stale silently, and each of the four claims below +// reads exactly as well wrong as right: a renamed script, a coverage +// threshold that moved, a pinned Node version that changed, a path that no +// longer exists. +// +// The link lint (lint-links.ts) already proves that a Markdown link and its +// heading anchor resolve. This gate covers what that one cannot see: names, +// numbers and versions, most of which are never inside a link at all. +// +// A library first: tests call lintDocs() in-process against fixture trees, +// and `node scripts/lint-docs.ts [root]` is the command. +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { isEntrypoint } from "./lib/entrypoint.ts"; +import { processOutput, type GateOutput } from "./lib/output.ts"; + +const REPOSITORY = join(import.meta.dirname, ".."); + +// The documents this gate holds to the repository. Any other file that +// quotes a script, a number or a path is somebody else's problem. +const DOCUMENTS = ["README.md", "CONTRIBUTING.md"] as const; + +const COVERAGE_METRICS = [ + "statements", + "branches", + "functions", + "lines", +] as const; +type CoverageMetric = (typeof COVERAGE_METRICS)[number]; + +export type ClaimKind = "script" | "number" | "version" | "path"; + +/** How many claims of each kind the documents made, across both files. */ +export type ClaimCounts = Readonly>; + +export interface DocsLintResult { + readonly claims: number; + readonly byKind: ClaimCounts; + readonly errors: readonly string[]; +} + +/** Every `npm run