v0.81.0 — the roadmap becomes a machine
v0.80.0 fixed nine inconsistencies between the roadmap and the specs it points at, by hand, by reading fourteen projects against the changelog and the tags. Five of the nine are exactly what a staleness query catches automatically. This release builds the machine, and then moves 133 documents into it.
The other half of the ask is older and simpler: the people now involved in
development do not use VSCode and do not read the repo. A folder per feature is
a wall of folders to them. So the planning ledger stays in the repo — in
frontmatter, in the same commit as the code, reviewable in a diff — and the
audience surface becomes a generated dashboard at help.purplecat.games.
Spec: Roadmap System · 21 tasks, one commit each. 1–4 are the system, 5–8 are the migration, 9–10 are the gates, in that order, so that a malformed page fails a check rather than being discovered six folders later.
Two things the re-spec established that shrink the work: ursa already parses
frontmatter, emits a sibling .json per page, renders a frontmatter table, and
supports hydrated .mdx — so what gets built here is a validator and one
manifest, not a renderer. And visibility stops being a frontmatter field: a
docs/internal/ folder in a repo going open source protects nothing, so private
means a different repository.
1 — prove the UI substrate
-
[x] 1a — Spike
.mdxhydration in ursa, and decide. It cannot be relied on here, so the dashboard is plainindex.mdwith a<script>— the fallback the task named, taken on the evidence rather than on taste.ursa 0.88.0 does implement `.mdx` with `hydrate: true`. Its *browser* half is right: the compiled MDX takes `React` as a parameter and ursa passes its own. Its *server* half calls `mdx-bundler/client`'s `getMDXComponent`, which supplies whichever React resolves from `mdx-bundler` — and **`mdx-bundler` declares no react at all**, neither dependency nor peer. So that `require` escapes its isolated tree and lands on pnpm's hoisted `.pnpm/node_modules/react`, which is a property of the install and not of this repository. React 18 builds `Symbol(react.element)`; React 19's `renderToString` takes only `Symbol(react.transitional.element)`, so on the losing half of that coin every `.mdx` page fails, including one containing nothing but a heading. **And it is a coin.** *(Corrected after the fact — see below.)* The same committed lockfile hoists React 18.3.1 on one machine and 19.2.8 on another, because `apps/web` is on 18 while ursa depends on `^19.2.4` and both are in the store. On the second machine `.mdx` renders perfectly. That is not a reprieve: combined with the first finding below — a failed render does not fail the build — a dashboard built on `.mdx` would be correct or silently broken depending on who last ran `pnpm install`. The fallback stands for a better reason than the spike originally found. **Two findings the spike was not looking for.** A failed `.mdx` render **does not fail the build** — ursa catches it and ships a yellow banner over the raw markdown — so 10a's gate cannot lean on the build to notice. And ursa publishes **only `.html`** static files (`allStaticFiles = allSourceFilenamesThatAreHtml`), so the spec's claim that a `roadmap.json` beside the page "lands at `/roadmap/roadmap.json` and is fetchable" is wrong: it lands nowhere. A build step publishes it until ursa changes, and 4a inherits it. The dashboard renders five rows from a hand-written manifest, derives its filter options from the data, and filters — verified against the built page in a DOM, with no console errors. A guard fails the moment `.mdx` becomes worth re-testing. **The guard's first form was wrong, and it is worth saying how.** It compared ursa's resolved React against `mdx-bundler`'s and demanded they differ — one machine's outcome, asserted as if it were a fact about the code. It passed where it was written and failed the whole build on the next checkout, where nothing was broken. It now asserts the *ambiguity* instead: `mdx-bundler` pins no react, and more than one React major is in the store. Both halves are properties of the lockfile, both were checked against synthetic inputs to confirm they fire rather than merely pass, and the condition lifts exactly when the workspace settles on one React major or somebody upstream pins the resolution.
2 — the schema and its validator
-
[x] 2a — The frontmatter schema, and a validator that fails the build. Fifteen fields, four shapes, and the three conditional rules:
shipped_inrequired when shipped,blocked_onrequired when a committed entry has notarget_version,target_versionforbidden unless committed or in-progress. An unknown field is an error too — nothing readstargetversion, and without that rule it would validate clean and appear nowhere.**It parses the frontmatter itself rather than using ursa's `yaml`.** That package is ursa's dependency, not this one's, and reaching for it would give `apps/help` its first runtime dependency — the property that lets these scripts run in a fresh checkout and keeps them clear of the ursa version situation. Task 1a is that argument in miniature. So the parser reads the subset the schema uses and **rejects anything else with a file and a line**, rather than misparsing it quietly. Sixteen fixtures, each rule passing and failing. Two of them exist because of bugs this repo has already shipped: a trailing `#` comment is stripped while a `#` inside a URL is not (v0.80.0's L.6), and a nested map — a construct the parser does not read — is an error rather than a silent drop. -
[x] 2b — An effort scale anchored to this project's velocity.
commit/release/multi-release, defined as units of this repo's release machinery — a commit with its own tests, a version branch of 8–30 tasks, or more than one branch because the phases ship separately — and each point carries anchors that actually shipped, which is the only calibration available. A definition without them is a definition everyone reads differently.CLAUDE.md already sizes work by what Claude Code can do rather than a human, so a scale borrowed from sprint planning is wrong here by about an order of magnitude. The validator prints the meanings rather than the labels when it rejects a value, because the failure mode is reading `release` as a quarter.
3 — the checks
-
[x] 3a — Identity integrity. Unique ids, every
prerequisitesandepicreference resolves, and an id cannot leave the tree while anything still references it — implemented from the other side, as "every reference resolves", which makes deleting a referenced page and renaming a page the same event with only one of them an error. Plus one rule the spec did not list: nothing is its own prerequisite. Fails the build, and the message says which of the two things happened, because the fix differs.Checks run only over pages that passed the schema: an entry missing its `id` would otherwise be reported once as a schema error and again as a dangling reference. -
[x] 3b — Ordering, cycles, and the cross-check against
ROADMAP.md. Nothing ships before what it needs — including the case the spec did not name, a slotted entry whose prerequisite has no slot and has not shipped, which is a release plan that cannot happen. Cycles are reported as the loop (a → b → c → a) rather than as their existence, because "there is a cycle" is not actionable.The cross-check runs in both directions, and **they are not equally enforceable yet**. A `target_version` `ROADMAP.md` has never heard of is an error: frontmatter cannot invent a release. A planned release no page claims **warns** until task 9b — task 5 lands 133 documents over several commits, so that state is the expected one throughout the move, and failing on it would break the build in the middle for a reason that resolves itself. 9b is where `ROADMAP.md` states its relationship to the tree, which is when the rule becomes true and enforceable. It reports five warnings today, one per planned release, which is an accurate description of a tree with nothing migrated into it. -
[x] 3c — Staleness.
git log --oneline <reviewed_at>..HEAD -- <touches>, recording the commit count and the newest offending commit, because the output is the evidence and a boolean is not actionable. Badges, does not fail: a schema violation breaks the machine, a stale document is information about a document, and a release that cannot ship until every spec is re-read is a release that does not ship.**A document with no baseline or no `touches` is `unknown`, not `fresh`** — the lie a two-state field would have to tell about an idea that has never been checked. So would a resolution failure: a `reviewed_at` git cannot find is reported with the ref in the message, not thrown. Verified against real history rather than only fixtures: a page pointing at `useAutoArtwork.ts` from `v0.79.0` reports one commit, `17c16c48`, which is v0.80.0 — the release that moved it.
4 — the manifest and the dashboard
-
[x] 4a — Generate
roadmap.json. One manifest, sorted by id, carrying only declared fields with the empty ones dropped, written where the hoist runs. Committed, not gitignored, with the regenerate-and-diff guard inpnpm --filter @purple-cat/help test.**The spec asked for two things that cannot both be true**: the committed manifest carries each entry's staleness, *and* it regenerates byte-identical. Staleness is `git log <reviewed_at>..HEAD`, so it moves with every commit to a touched path — the file would show a diff on unrelated work, and the guard would cry wolf until somebody deleted it. So the manifest is what the tree says, and **staleness is computed at build time into the published copy**. The dashboard keeps its badges; the guard keeps its meaning. A tree that does not validate produces **no manifest at all** rather than a partial one: the dashboard would render the valid half and nothing would say the rest went missing. And ursa still does not publish the file — the build step task 1a wrote now regenerates it with staleness instead of copying. -
[x] 4b — The dashboard. Six sortable columns, seven facets derived from the data,
shippedanddroppedhidden behind one checkbox — the plan is what has not happened yet — and a facet with only one value is not offered, because a select with one option states that a choice exists when it does not. Asking for a hidden status by name overrides the default, or "filter to shipped" would return nothing and read as a bug. It reads the manifest and never the tree.**It is a module, not an inline `<script>`**, so the parts that can be quietly wrong — deriving facets, combining them, sorting — are exported functions with nine fixtures. One of them pins `v0.9.0` sorting before `v0.81.0`, which is the bug every version list ships once. Freshness reads as a sentence rather than a state name: *needs review (4 commits since v0.79.0)*, *current as of v0.80.0*, *not checked*. The audience is someone who has never seen the repository. **`render-frontmatter: true` is now required by the schema** rather than left to each author. It is what puts the status table on the page, ursa owns the default, and a page that omits it looks identical in the tree and blank on the site. Verified against the built page: the default view hides the shipped entry, the checkbox reveals it, filtering to `shipped` narrows to one, column sort works, links point at each entry's own page, and a missing manifest produces a sentence rather than an empty table.
5 — the migration, 133 documents
One commit per source tree, each carrying its disposition list, so deleting
things is a reviewable decision rather than a gesture. Triage with the tools
tasks 1–4 just built rather than by re-reading: the staleness query is the
evidence for "has this already shipped", and quarter buckets come from
git log --diff-filter=A, because filesystem mtimes are meaningless after a
clone.
-
[x] 5a —
docs/committed/**→ roadmap pages. 40 files and 14 projects out, 47 entries in, because six projects could not be described by one status: anepicpage plus one entry per independently shippable phase, each with its own.ROADMAP.md's 47 links were repointed in the same commit rather than left dangling.**The spec was wrong about the legal drafts, and the tree said so.** It listed the Privacy Policy and Terms of Service as *superseded — already published*. What is published is a **17-line placeholder whose first line says it is not the policy**, against 646- and 393-line drafts that are the real documents. They are `committed`, not `dropped`. This is the ninth such inconsistency in two releases and the first one the machine was used to find. **`v0.80.0` was never tagged here.** Every `reviewed_at` resolved to nothing and all 47 staleness queries failed. Local `main` is still at v0.79.0: v0.80.0's merge and tag were made in a codespace and only the `version/0.81.0` branch was ever pushed, so the release commit survives solely as this branch's base. The tag is restored on `17c16c48` — task L.2 retro-tagged four releases for this exact reason, and it turns out the release that did it shipped untagged itself. **Real data broke the dashboard's Version column.** It rendered `blocked: ${reason}`, which was fine against 4b's five hand-written rows and is not fine against 23 of 47 real ones whose reasons run 61 to 167 characters. The cell now says `blocked` and carries the reason as its `title`; the widest Version cell went from 167 characters to 16. Two dispositions worth naming. The **style guide is not a plan item** — a ratified reference document has no honest `status` on a roadmap — so 2.2.0, its frozen 2.1.0 archive and the four design assets beside them moved to `docs/help/design/` instead. And **`architecture/` and `23-roadmap-system/` are not in this commit**: the first belongs to task 7b, and the second is the live spec for the release doing the migrating, so it leaves at 10a. Historical changelog citations into `docs/committed/` **degrade to their own link text rather than 404** — `hoistChangelog.mjs` already did that — and its self-test fixtures were repointed at a document that still exists. -
[x] 5b —
docs/shipped/**specs → dated roadmap pages. 45 files, 23 entries, in five quarter buckets from2025-Q4to2026-Q3.**Bucketing by `--diff-filter=A` puts everything in two quarters, and both are wrong.** A `git mv` into `docs/shipped/` registers as an add, so the date recovered is the date a document was *filed away*. `--follow` recovers the date it was written, and the buckets go from two to five — the oldest being 2025-12-08, two months before `CHANGELOG.md` exists at all. Which is why **`v0.1.0` is `shipped_in` for four documents that predate every recorded release**: v0.1.0 opens "for some reason, I didn't have a CHANGELOG.md file before, so we'll start here", so it is the first release that exists and earlier work is in it by definition. **`docs/shipped/` is not all shipped, so `status: shipped` is not the answer for all 45.** The Character Sheet Overhaul reads 33 of 68 in one task list and 0 of 47 in another; the Map Generator reads 84 of 98 while `ROADMAP.md` separately carries it as "revisit procedural maps". **And the unticked boxes are not a backlog either** — `phase-2-validation.md` declares itself `status: stub`, and the validation work it describes demonstrably shipped in v0.49.0 and v0.52.0 under the changelog's own task list. So the Character Sheet Overhaul is an epic with three shipped phases and one that needs re-reading rather than a task list, and the Map Generator is `committed` with `shipped_in` recording what did land. Reading the three navigation passes in order — v0.1.0's context panels, v0.42.0's global nav, v0.36.0–v0.39.0's style-guide sweep — is the only way the current design reads as deliberate, so each entry says where it sits in that sequence. -
[x] 5c —
docs/ideas/**→ one page each. 14 files, 48 entries, because three of the fourteen were dumps:ideas.mdheld eleven unrelated ideas under one heading,TODO TBD ideas.mdseventeen at four different levels of readiness, andTODO beta.mdeleven.alphaandGAstay whole — the spec named three dumps and those two are milestone wishlists whose value is the grouping.**The ability score wizard is the case this was built for.** It was specified, built, and deliberately scrapped during the Character Sheet Overhaul because it collided with inline editing — and the v0.75.0 playtest then asked for it again, scheduled as v0.82.0's P.4. It is `dropped` with the reason attached, so the re-proposal meets its own prior rejection instead of starting over. **A fifth already-shipped document**: the selected-versus-impersonated click model was implemented in v0.35.0 and its note was never ticked. Kept as `shipped` rather than deleted, because a plain click impersonating rather than selecting is the thing people get wrong in that code. **Two dashboard defects that only 118 real entries could show.** Ideas are now 45 of the 87 rows in the default view, which is fine — the default sort is by version, so slotted work leads and unslotted ideas sink. But `versionKey` read `shipped_in` as a slot even on unfinished work, so three unscheduled leftovers carrying old releases sorted **above the in-flight release** — a plan that appears to start at v0.25.0. `shipped_in` now counts as a position only when the entry actually shipped. -
[x] 5d —
docs/temp_to_be_analyzed/, use it or lose it. 32 files: 20 entries, 3 reference documents handed to task 7b, 5 design assets moved, 5 deleted outright. No "keep for later" outcome, which is what the directory already was.**The folder had already triaged itself.** `analysis_result.md` held a disposition list — Keep, Analysis Required, Archive as Shipped, Archive as No Longer Interested, Archive as Obsolete, Delete — and this commit follows it, resolving the four items it left open rather than re-deriding the whole thing: - **Data tables** — the delta it asked for is real and now written down. `EntityList` uses an infinite window rather than pagination, and `X-Total-Count` is unreadable cross-origin while production serves the SPA and API from different origins. So the header half of the contract does not work where it matters. Kept as `committed`. - **TODO_TESTING** — 16 of 118 ticked and written a year before v0.67.0 put `apps/web`'s 8,635 tests into CI and v0.68.0 added the ratchets and `pnpm verify`. It does not reflect the current state, so `dropped`, per the triage's own condition. - **Recommended models** — filed as obsolete, actually **shipped** in v0.78.0's curated model tiers. - **Map layers** and **monster character sheets** — likewise filed as obsolete and likewise shipped, the latter with its §2 never built, which is worth knowing before citing it as a description of the code. Two constraints held: `ROADMAP.md` stays because `HelpTabs.tsx:36` still imports it with `?raw` until task 9a, and `architecture/` stays for 7b — now three documents larger, because the module author guide, module template and character-import architecture overview are **reference, not plan**, and a reference document has no honest `status` on a roadmap. The same call as the style guide in 5a.
6 — the changelog becomes authored
-
[x] 6a —
docs/shipped/changelog/→docs/help/changelog/. 132 notes, un-gitignored,titleandrender-frontmatter: falseon each.docs/helpis now literally the record of truth for everything in it.**The move is not a `git mv`: the links only worked because the hoist was rewriting them every build.** Baking that in permanently is what "becomes authored" means, and doing it by hand surfaced two things. **A repo-root-relative link never resolved.** A good number of notes write `docs/committed/17-…/index.md` rather than `../../committed/…`, and from the note's own directory that points at nothing — most of the "~130 links already broken in the repo" the hoist's own header describes. Resolving those against the repository root before giving up turned **133 dangling links into 26**: 112 now reach the roadmap pages phase 5 made for them, 23 are sibling notes, 4 reach the index. **v0.52.0's notes never existed.** `CHANGELOG.md` has linked `v0.52.0.md` since the release shipped as `d8340156`, and the file was never written — the hoist unlinked it silently every build, so the gap was invisible. Recovered from that commit's own `CHANGELOG.md`, 326 lines, and committed with the other 132. Two mechanical corrections. The hoist's `rm -rf` of its output directory would now delete 132 authored files, so it removes only what it writes. And `classifyTarget` excluded the whole of `docs/help/changelog/` from being treated as wiki — right while every file there was its own output, and wrong the moment they were not: it unlinked **all 133** of the index's links to the notes, publishing a list of every release with none of them clickable. `HelpChangelog.contract.test.ts` pinned the directory as gitignored, which this reverses. Verified rather than assumed, as the task said: it failed, and now asserts the index alone is ignored. -
[x] 6b — Collapse
hoistChangelog.mjs. Both jobs are gone — 6a made the notes authored, task 5 made the specs roadmap pages — so the crawl, the flat/ref/output topology and the two-sources-one-page guard go with them. 349 lines of script become 238, and 187 of self-test become 128.**Not the clean deletion the task predicted, and the difference is worth naming.** What survives is not "one line": it is `CHANGELOG.md` copied to `docs/help/changelog/index.md` **with its links rewritten**, and that rewriting is the part that can be quietly wrong. So the self-test is trimmed to what still exists rather than deleted — 6a is the proof it earns its keep, where one wrong exclusion unlinked all 133 links in the published index and nothing failed. The `ref/` tree was down to **5 files** before this commit, of which two were linked. Both are repository documents that are deliberately not published — `ROADMAP.md` and the live spec — so they now degrade to their own text, which is what a public reader with no checkout can use anyway.
7 — the API trees converge
-
[x] 7a — Restructure
docs/help/api/. The 22-page Action API tree keeps its audience untouched,index.mdbecomes a hub that sends each of the two audiences the right way, anddeveloper/indexes all ten API surfaces plus the eight documented inside one of them.**It indexes them by what is true in the code, not by what a spec says.** The contract specs arrive in 7b; this page stands on `MODULE_API_VERSION`, the route modules and the type definitions, so it is useful before them and does not go stale behind them. Numbers corrected against the code: there are **58** route modules under `packages/server-core/src/routes/`, not the "~90" both old indexes state, and the schema defines **48** tables, not 47 — the count 7b's `data-index.md` owes a row each. *(Corrected in 7b: this entry first said both indexes were also wrong about `MODULE_API_VERSION`. Only the shipped one was, at 1.5.0. The master index said 1.35.0 and was right, because a guard in `packages/shared` holds it to the constant — and finding that guard is what corrected this.)* -
[x] 7b — Collapse the four architecture trees into it. 25 documents and the fixture set, out of
committed,shipped,ideasandtemp_to_be_analyzed, intodocs/help/api/developer/.docs/now holds nothing buthelp/, the changelog, and the three things tasks 8–10 own.**The two rival `api-index.md` files are one page**, and the merge settled which was right: the master index tracked `MODULE_API_VERSION` correctly at **1.35.0** because a guard in `packages/shared` held it there; the shipped one had no guard and still said **1.5.0**, twenty-two minor versions behind. That guard read the file this commit deleted, so it now reads the published index — and its failure is what caught the merge. **`data-index.md` is generated from `dbSchema.ts`**, one row per table, so a new table appears as *no spec yet* rather than not appearing. **48 tables, 22 with no specification** — an invisible gap converted into a countable one. The ~35 entity specs stay deferred behind the actor model, and the page says why. **The specs lost their version suffixes.** `rest-api-0.1.md` is `rest-api.md`: the version is stated in the document and in the index, and `module-api-1.6.md` had been naming a version it did not match for a year. **A third stale status, found in the moving.** The chat-invocation RFC still opened *"Nothing here is built"* — it shipped as v0.73.0's 28 tasks. The original wording is kept beneath the correction rather than edited away, because that sentence outliving the build is the exact failure this release exists to stop. Destinations for the documents that are not API contracts, named rather than discovered: **styling architecture** → `docs/help/design/`, beside the style guide it implements; **release tiers and LTS** → a roadmap idea, because it is a plan with four open questions; **the library classifier note** → `developer/`, with the dozen lines of search transcript it opened with removed.
8 — legacy-migrations leaves docs/
-
[x] 8a — →
packages/server-core/legacy-migrations/. 53 quarantined SQL files and their README, out ofdocs/and into the package whose tests read them.docs/now contains only documentation, which is what makes task 10a's allowlist a rule rather than an aspiration.**Kept, because eleven test files read these as source-of-truth contracts** — each asserts that the runtime DDL in `db.ts` still matches what the migration declared. That is fixture data, and it had been sitting in the one tree a cleanup deletes things from. All eleven read repo-root-relative paths, so the change is mechanical; the twelfth reference was a relative path in a doc comment that had been **wrong since it was written** (`../../packages/…` from inside `packages/server-core/src/`) and is now correct. The README gains the reason it lives here, next to the reason it must never move back into `migrations/`: a `.sql` file in a drizzle folder that is absent from the journal reads like an executable migration and is not one. The historical changelog notes still say `docs/legacy-migrations/`. That is where the files were in v0.32.0, and rewriting history to match the present is the opposite of what those notes are for.
9 — retire the other two roadmaps
-
[x] 9a — The app's Roadmap tab points at the wiki. One panel, one config constant, and 58 lines out of
HelpTabs.tsx— the bundled?rawimport, the accordion,parseMarkdownSectionsand the line-at-a-time renderer it fed. The Changelog tab was already the pattern.**The rendering was the smaller problem.** That file was one of *three* roadmaps, it had not been edited since June, and it was **the one users saw**. `docs/help/roadmap/` is generated from validated frontmatter, so the tab now shows the plan CI checks. Verified live in a browser against the built site: 94 rows, seven facets, correct ordering, no console errors. **Both contract tests had to be loosened, and both for the same reason** — they matched source text that incidental prose also matches. `not.toContain('parseMarkdownSections')` failed on the file's own header explaining what the *changelog* tab stopped doing in v0.74.0, and the changelog contract pinned `import { HELP_CHANGELOG_PAGE }` as an exact string, which a second constant in the same import breaks. Both now match code rather than prose, which is the standing trap with this repo's ~590 source-text guards. -
[x] 9b —
ROADMAP.mdstays, and states its relationship. A header saying what it is — the ordered pipeline, in prose, because sequencing is an argument and a table cannot hold one — and what it is not: the list of work, which is the 139 pages it links into. The project index is reframed as a closed redirect table, so an old reference still resolves and nothing new is added to it.**And task 3b's second direction is now an error.** A release `ROADMAP.md` plans that no page claims fails the build, as the reverse already did. Both halves of "the two roadmaps agree" are enforced, which is what makes the header's claim true rather than aspirational — verified by planning a fake v0.99.0 and watching the build exit 1 with a message that names both fixes. **Promoting it broke four fixtures, and the reason is worth keeping.** "Every planned release is claimed" is a property of the *complete* set of entries; run it over a two-page fixture directory and every release looks unclaimed. It was invisible while the finding was a warning. The cross-check is now scoped to the real tree — a partial directory still gets every check that is a property of the pages themselves — and a test pins that. The dead warning channel goes with it: nothing emits warnings any more, and its message still said "task 5 is still migrating".
10 — the gates
-
[x] 10a — A
docs/allowlist guard. CI assertsdocs/contains onlyhelp/, and it does — the last four directories went with this commit, including this project's own spec, synthesised into its roadmap page exactly as the other 133 documents were.**Nothing about the migration stopped it happening again**, which is the whole argument for fifteen lines: a new folder under `docs/` is one `mkdir`, fails no check, and by the time anyone reads it the documents inside are a year old. That is the entire history of `temp_to_be_analyzed/`. The error message says where things go instead — a plan is a page with frontmatter, documentation of code lives beside the code, and **anything private is a different repository, not a folder here**. Dot-files are deliberately ignored: `.DS_Store` appeared under `docs/` twice during this release, and failing the build on it would teach people to distrust the check. Five self-tests, and the ones that matter assert it still **fails** — on a new folder and on a loose file. A guard that has quietly stopped catching anything is worse than none, because it reads like coverage. -
[x] 10b — Pre-open-source history audit. 3,552 blobs across 603 historical paths under
docs/, plus a repository-wide credential sweep, against the five categories named in advance. Recorded as an entry; it gates the repository going public, not this release.**It found one thing that does not wait for open-sourcing, and the details are deliberately not in this file** — writing the location of a credential exposure into a published changelog is the exposure. They are in a private findings document handed over separately, with rotation as step one. Of the rest: **no financial or pricing analysis exists** (every match was the word "churn" applied to objects and diffs), **no personal information**, and no key material anywhere in `docs/` history. What is left is material that is *already published*, and one judgement call nobody has made — this changelog documents its own security defects in detail, which is good practice for a hosted service and **an exploit guide for every self-hosted deployment older than the release that fixed one**. LTS for older versions is an explicit goal, so that population is real. **Two follow-ups filed rather than left in prose.** The still-open finding is now [#68](https://github.com/purple-cat-games/platform/issues/68) — the codebase tells users their API keys are "never sent to server" while five call sites send them as `x-ai-*` headers. And the LTS question the audit surfaced has a committed strategy behind it: release tiers and long-term support, **committed in principle and unspecified in the numbers**, because a support window is a promise to other people rather than an engineering choice. **The audit also found what dropping `visibility` cost.** An earlier draft of this project marked four things `internal` and carried a frontmatter field to enforce it. Dropping the field was right — a `docs/internal/` in a repo going open source protects nothing. Not re-triaging the four things it had been protecting was the gap, and phases 5 to 7 published them.