The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
80 lines
3.0 KiB
JavaScript
80 lines
3.0 KiB
JavaScript
// Generates CHANGELOG.md from frontend/src/content/release-notes/en.json — the same entries the app
|
|
// shows under Release Notes, so a self-hoster deciding whether to upgrade reads the same text an
|
|
// in-app user does, without having to install the app first (DX-14).
|
|
//
|
|
// Why derive rather than hand-write: two lists of "what changed in 0.80.0" WILL drift, and the
|
|
// in-app one is the one that gets written during a release. This makes the file a build product of it.
|
|
//
|
|
// node scripts/gen-changelog.mjs # write CHANGELOG.md
|
|
// node scripts/gen-changelog.mjs --check # exit 1 if CHANGELOG.md is stale (used by `siftlode release`)
|
|
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import path from "node:path";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const SOURCE = path.join(root, "frontend", "src", "content", "release-notes", "en.json");
|
|
const TARGET = path.join(root, "CHANGELOG.md");
|
|
|
|
async function loadEntries() {
|
|
const entries = JSON.parse(await readFile(SOURCE, "utf8"));
|
|
if (!Array.isArray(entries) || entries.length === 0) {
|
|
throw new Error(`${SOURCE} is not a non-empty array of release notes.`);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function section(title, items) {
|
|
if (!items || items.length === 0) return "";
|
|
return `### ${title}\n\n${items.map((line) => `- ${line}`).join("\n")}\n\n`;
|
|
}
|
|
|
|
function render(entries) {
|
|
const head = [
|
|
"# Changelog",
|
|
"",
|
|
"Every released version of Siftlode, newest first.",
|
|
"",
|
|
"**Generated from `frontend/src/content/release-notes/en.json` by `scripts/gen-changelog.mjs` — do not edit by hand;**",
|
|
"edit the release notes and re-run the generator.",
|
|
"",
|
|
"Upgrading, pinning a version and rolling back: [docs/self-hosting.md](docs/self-hosting.md#upgrading).",
|
|
"",
|
|
"",
|
|
].join("\n");
|
|
|
|
const body = entries
|
|
.map((e) => {
|
|
const sha = e.sha ? ` (\`${e.sha}\`)` : "";
|
|
let out = `## ${e.version} — ${e.date}${sha}\n\n`;
|
|
out += section("New", e.new);
|
|
out += section("Fixed", e.fixed);
|
|
// A release that carried only internal work (the owner's rule says none should).
|
|
if (e.new.length === 0 && e.fixed.length === 0) out += "Internal changes only.\n\n";
|
|
return out;
|
|
})
|
|
.join("");
|
|
|
|
return `${head}${body}`.replace(/\n{3,}$/, "\n");
|
|
}
|
|
|
|
const entries = await loadEntries();
|
|
const rendered = render(entries);
|
|
|
|
if (process.argv.includes("--check")) {
|
|
const current = await readFile(TARGET, "utf8").catch(() => null);
|
|
if (current !== rendered) {
|
|
console.error(
|
|
`CHANGELOG.md is out of date with the release notes (newest entry: ${entries[0].version}).\n` +
|
|
"Run: node scripts/gen-changelog.mjs — then commit the result.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log(
|
|
`CHANGELOG.md is current (${entries.length} releases, newest ${entries[0].version}).`,
|
|
);
|
|
} else {
|
|
await writeFile(TARGET, rendered, "utf8");
|
|
console.log(`Wrote CHANGELOG.md — ${entries.length} releases, newest ${entries[0].version}.`);
|
|
}
|