docs(engine): retire the ECS prose, and guard the trees that let it survive - #554
Conversation
Nothing named SystemECS, Entity or ComponentSystem survives in any src/main tree, and there is no ecs sub-package. The prose kept saying otherwise: the adapters "talk to the ECS-based engine", a watermark "is not an ECS entity", metadata "does not participate in the ECS layout pipeline", and render-pdf's package-info sent readers to an ecs sub-package they can never open. The contributing guide routed people around an "engine ECS" that is not there. Each sentence now says what it was actually for. The watermark and header/footer notes were drawing a real distinction -- these are document-level configuration rather than nodes in the document tree -- so they keep it in words that match the code. The package-info records the removal instead of pointing at where the thing supposedly went.
The ECS prose survived a full release line under a green architecture guard, and the reason is structural rather than an oversight: the retired-token scan reads markdown only, and the one scan that reads main sources is scoped to document/**. Every false claim lived in engine and render-backend javadoc -- outside every root any guard looked at. This scan reads .md and .java together across core, the three render backends and templates, matching on word boundaries so SPECS and RECS do not trip it. It found ROADMAP.md's "Entity-Component-System" spelling, which a case-sensitive grep for the acronym had missed. Two files are allowlisted by name because they name the architecture as removed, which is the one thing such prose may still do: ROADMAP's per-GA sections record what each release dropped, and baselines/COMPARISON.md is a dated benchmark log measured while that engine existed -- rewriting it would falsify the record it exists to keep. Listed rather than pattern-matched, since a guard that recognised "gone" would pass any sentence containing the word.
The roadmap called 2.1.0 the current stable line after 2.1.1 shipped. It is the page a reader opens to decide whether the project is maintained, so a superseded version there says the opposite of what the release history does. The section now names 2.1.1 and says what that release was -- tooling and Javadoc-gate fixes, no authoring API moved -- so the headline stays with 2.1.0 where it belongs. A guard beside the README's release-status check holds it: the bolded version in "Current stable" must be a published one. Pinned to published rather than latest, because between a GA and the next cut the poms already name the version under development while the roadmap correctly still names the shipped one.
…eady is The roadmap guard added in this branch reads the pom to decide which version ROADMAP must name, and the cut bumps every pom in Step 1 but ran `mvnw clean verify` in Step 5 without touching ROADMAP. A 2.2.0 cut would therefore have failed its own gate, mid-flight, with every pom already rewritten and the tree dirty to unwind by hand -- a guard breaking the release it exists to protect. Update-RoadmapCurrentStable rewrites the bolded version in the '## Current stable' section and nothing else: the maintainer's prose and every per-release section below stay put. It runs under the same $isFinalRelease branch as the README release-status block, for the same reason -- a pre-release never reaches Central, so the section keeps naming the last version a reader can resolve. Step 0's post-mutation checks gained the matching Test-RoadmapCurrentStable, and ROADMAP.md joins $commitFiles so the rewrite reaches the release commit instead of being stranded. Both no-op cases throw rather than pass quietly: a missing section, or one with no bolded version to replace, would otherwise surface as a verify-gate failure after the bump. Verified by dry-run at 2.2.0: the script reports the rewrite and lists ROADMAP.md in the staged set.
…e rest Two false descriptions outlived the first pass, each for a reason the scan itself had: The post-2.0 engineering roadmap still described layout as resolving on a live Entity / EntityManager model with EntityBounds geometry, and listed retiring it as Deferred. None of those types is declared anywhere in any src/main tree, and none is referenced from testing either -- 2.0 did the work this entry planned. It now records that. The scan had skipped the whole of docs/roadmaps/ as archival, but the root roadmap links this file as committed engineering direction, so it is a live document sitting under an archival prefix; the ECS scan no longer skips that directory. render-pdf/pom.xml described the render tree as including "the ecs system + handlers". A module description is what a reader sees browsing the artifact on Central, and no guard had ever read a pom. The scan now covers .xml alongside .md and .java, walking whole modules rather than their source roots -- with target/ excluded, since a stale generated copy would report a violation with no source to fix. The ROADMAP allowlist is narrowed from the whole file to the exact historical sentences, in ROADMAP and two others. Exempting a file would have permitted a fresh claim in the live "Current stable" section, which is the sentence a reader trusts most.
Inserting the roadmap test above the method put its javadoc between that comment and its subject, so java bound the comment to the new test and acceptableTargets lost its own -- the same orphaning this branch's sibling work fixed in the PPTX render environment. The test moves above the comment; no behaviour changes.
DemchaAV
commented
Aug 14, 2026
All three confirmed and fixed. The Blocker was mine to own: I added a guard keyed to the pom version without reading the script that moves it. Blocker — the cut would have failed its own gate. Verified exactly as described: Step 1 bumps every pom, Step 5 runs
Proven by dry-run at 2.2.0:
Major — both sites fixed, and both blind spots with them.
The allowlist is narrowed from whole files to the exact historical sentences — three of them, in ROADMAP and the benchmark log. Your reasoning applies directly: a file-level exemption would have permitted a fresh claim in the live "Current stable" section. Minor — corrected. Inserting the test above Full reactor |
Swapping the bolded version is the whole edit within a line and the wrong edit across one. The section's heading carries the line and its prose describes that line's headline feature, so a 2.1 -> 2.2 cut would have written "## Current stable - 2.1" above "**2.2.0** is the current release": self-contradictory, and green under every guard here, because each asked only whether the version was published. Such a cut now stops with the edit a maintainer has to make -- new heading, new prose, old text moved down -- since no rewrite can produce a paragraph about a release line that has not been described yet. Within a line the swap runs as before. A section already naming the target is a no-op rather than a throw. The first cut of this function reported "names no bolded X.Y.Z version to update" for a correctly-prepared section, which is the exact state the line-crossing path now tells maintainers to leave behind. The version check gains the heading: on its own it accepts the contradiction above, so it holds the line named in "## Current stable - X.Y" against the version in the body, catching by hand what the script now refuses to write. The rehearsal version in release-script-check.yml is derived from the roadmap's current line instead of being hardcoded -- a fixed 9.9.9 would now be refused, and any fixed value breaks the job the day the project moves lines, which is when a working rehearsal matters most. A second step asserts the refusal itself, so the negative half is covered by running the script rather than by reading it.
…ds it The exemptions were matched anywhere. Any live document could have quoted one of the three permitted sentences and walked through the scan -- the same over-broad shape as the whole-file allowlist they replaced, one step smaller. Keyed by path, a sentence is exempt where it IS the record and nowhere else. release-process.md was describing the release flow this branch changed: it said the script does not rewrite the README 'Latest stable' block (it does, and has since that block stopped being a hand-edit), and its staging list omitted ROADMAP.md. Step 3b now also states what a line-crossing cut refuses to do, which is the one release-time behaviour a maintainer has to plan around.
DemchaAV
commented
Aug 14, 2026
Blocker confirmed on both halves, and both reproduced before fixing. The contradiction: heading The version is now the base of the edit, not the edit. Within a line the swap runs as before. Across one the cut stops, because no rewrite can write a paragraph about a line nobody has described yet: All three of your scenarios, run against the live roadmap:
The guard gained the heading check you implied by naming the contradiction — on its own the version check passes Real script tests, in the repo's own idiom — Minors. Full reactor |
The negative step ran the cut across release lines, the script refused as designed, and the step failed anyway: the refusal is a `throw`, and `*>&1` redirects the message without stopping the termination. So the check failed for observing exactly the behaviour it exists to observe. Caught, with LASTEXITCODE reset, so the assertion runs against the message. Verified both ways locally: the crossing cut is refused and the step exits 0, the on-line rehearsal still completes.
The line-crossing refusal was correct and unsurvivable. Preparing what it asked for was impossible three ways: committing the new version into 'Current stable' failed VersionConsistencyGuardTest, which holds that section to what is published; editing it uncommitted failed the cut's own clean-tree preflight; and preparing nothing failed the cut in Step 1, after every pom and README had already been rewritten. The negative case was proven and the positive one did not exist. A '## Upcoming - X.Y' section is the state that was missing. It says what the next line leads with while 'Current stable' goes on naming the release that is actually on Central, so develop stays truthful and green for the whole cycle. The cut promotes it: 'Upcoming' becomes 'Current stable' and the line it replaces becomes 'Previously'. The compatibility check moves to Step 0, before the first write. A cut that cannot describe itself now refuses with a clean tree instead of handing back a half-bumped one. A staged section is guarded too, or it becomes the same stale claim about 'next' that the roadmap was making about 'now': when present it must name the line the poms are on, and must differ from the published line, so a copy left behind after promotion fails. Verified end to end in an isolated clone -- a real cut at 2.2.0 with 2.2 staged produced 'Current stable - 2.2' over '**2.2.0**' with 2.1 demoted to 'Previously'. The workflow now covers all three paths: the in-line cut, the refusal (asserting git status stays empty), and the crossing cut after staging, checked against a real run rather than a dry one.
DemchaAV
commented
Aug 14, 2026
Confirmed on all three horns, and you named the real fault: I built a refusal without building the state it demands. The negative case was proven and the positive one did not exist. The missing state is The check moved to Step 0, before the first write. Your third horn was the sharpest: the refusal sat next to the edit in Step 1, so it handed back a half-bumped tree for a condition knowable before anything was touched. Verified end to end, in a bare-cloned repo so no commit or tag reached the real one. A real cut ( The workflow now covers all three paths you listed:
The staged section is guarded too, or it becomes the same stale claim about "next" that the roadmap was making about "now":
Full reactor One note on the rehearsal I ran: it needed the script's uncommitted state, and my first attempt cloned the committed one — so the first run "refused" a correctly staged section and briefly looked like a bug in the fix. It was a stale clone. Re-run against the working copy, it promoted correctly. |
… the cut honestly Two gaps in the promotion, both leaving a half-rewritten tree. A staged section was accepted on its heading alone. With no bolded version in the body the promotion's substitution finds nothing to replace and silently does nothing, so the roadmap is written naming no release and the cut fails at the Step 2b re-check -- after every pom and README has moved. Step 0 now parses the body: it requires a bolded X.Y.Z and requires it to sit on the line being cut. The Java guard holds the same contract, where a maintainer staging the section meets it first. The promotion result is checked in memory before Set-Content -- section present, naming the release, on the right line, staged heading consumed -- so a state neither check anticipated still cannot reach disk. The positive rehearsal ran the real cut against the workflow checkout, which actions/checkout leaves on a detached HEAD, and the preflight rejected it for exactly the reason it exists. Relaxing that check for a test would have removed the thing being tested, so the rehearsal now builds a bare remote in the runner and works on a branch of its own: named branch, clean tree, in sync with its origin, all satisfied honestly. It also runs last, since it leaves the checkout on that branch with a release commit -- the earlier ordering would have handed the following steps a repository pointing somewhere else. A malformed staged section gets its own negative case, asserting both the refusal and that nothing but the file the step wrote is dirty. The position check now means what its message says: the staged section must be the one immediately above 'Current stable', not merely somewhere above it.
DemchaAV
commented
Aug 14, 2026
All three fixed. You were right that the rehearsal, not the preflight, was the thing to change. Blocker. The positive test ran the real cut against the workflow checkout, and It also runs last now. It leaves the checkout on that branch with a release commit and Major. Confirmed: the heading was accepted as preparation while the body decided the outcome. Step 0 now parses the whole staged section, requires a bolded New negative case asserts both halves: refusal, and nothing dirty but the file the step wrote itself. Nit. The position check now means what its message says — the staged section must be the one immediately above Rehearsed locally in the exact CI shape (bare remote, own branch, real cut, And the malformed case refuses with Full reactor |
The malformed-section check failed while the script did exactly what it was being tested for. PowerShell wraps a thrown message across lines and prefixes each continuation with '| ', so 'carries no bolded version for the cut to promote' arrived split mid-phrase and the match missed it. Both refusal assertions now collapse the continuation markers and the wrapping before matching. Same failure as the try/catch one two commits back: the script was right and the test read its output wrong.
…und it The positive rehearsal never got as far as the thing it was testing. It pushed a staging commit to a bare remote the runner's shallow checkout could not accept -- actions/checkout fetches depth 1, and a shallow push is rejected -- and the unchecked push let the step limp on to a secondary preflight error. Fixing that would only have moved the wall: the real cut reaches Step 4, which builds the example catalogue through mvnw.cmd, and the job runs on Ubuntu. None of that git and Maven machinery is what the promotion does; all of it was standing between CI and ever running it. So the promotion is now exercised directly, in the idiom this workflow already uses for Get-NextSnapshotVersion: lift the shipped functions and call them. They are lifted by AST rather than by scanning for a closing brace, so the extraction cannot pick up half a body, and each case gets a scratch roadmap of its own. Five cases, and three of them are the refusals that used to need a throwaway repository: nothing staged, staged with no bolded version, staged with a version off the line being cut, staged correctly (the file is really rewritten and satisfies the script's own post-check), and a re-run that must be a no-op rather than a throw. The refusal-before-mutation check keeps running the real script against the real checkout, since what it asserts is that git status stays clean. Not addressed here, and worth its own change: the script hardcodes mvnw.cmd, so it cannot run on Linux or macOS at all. Nothing in this workflow needs it now, and fixing it is a change to release tooling rather than to the roadmap contract this PR is about.
DemchaAV
commented
Aug 14, 2026
Both confirmed, and your second point decided the fix: repairing the shallow clone would only have moved the wall to Step 4. I dropped the git harness instead of repairing it. The promotion is now exercised directly against the shipped functions, in the idiom this workflow already uses for That removes the shallow-push, the bare remote, Five cases, each on a scratch roadmap of its own:
Run locally, verbatim from the workflow: The refusal-before-mutation check still runs the real script against the real checkout, because what it asserts is that On the Full reactor |
The line-mismatch case staged $major.9.9, which is only off the line being cut until the project reaches 2.8. At that point it becomes the very line the cut releases, the refusal it asserts stops happening, and the case starts proving the opposite of its name -- silently, since a passing test says nothing about which branch it took. Derived from the current line instead, the same way the rehearsal version above it already is. Checked against a roadmap on 2.8, where the literal would have flipped.
DemchaAV
commented
Aug 14, 2026
Nit fixed in The off-line version is now derived as Checked against a roadmap on 2.8, the line where the literal would have flipped: Still 3 refusals, so the case is still taking its own branch there. |
Uh oh!
There was an error while loading. Please reload this page.
Why
Two pieces of drift a release audit surfaced, with one cause between them: nothing was reading the places they lived.
The engine is described as something it is not. No
SystemECS,EntityorComponentSystemsurvives in anysrc/maintree, and there is noecssub-package — yet the javadoc told readers the adapters "talk to the ECS-based engine", a watermark "is not an ECS entity", metadata "does not participate in the ECS layout pipeline", andrender-pdf'spackage-infosent them to anecssub-package they can never open. The contributing guide routed contributors around an "engine ECS" that is not there.The roadmap named a superseded release. It called 2.1.0 the current stable line after 2.1.1 shipped — on the page a reader opens to decide whether the project is maintained.
Both survived a green
Architecture and Documentation Guardsjob, and that is the part worth fixing. The retired-token scan reads markdown only; the one scan that reads main sources is scoped todocument/**. Every false claim lived in engine and render-backend javadoc — outside every root any guard looked at.What changed
Prose. Seven sites corrected. The watermark and header/footer notes were drawing a real distinction — document-level configuration rather than nodes in the document tree — so they keep it in words that match the code;
package-inforecords the removal instead of pointing at where the thing supposedly went.A scan that reads both.
nothingShouldDescribeTheEngineAsEntityComponentSystemwalks.mdand.javaacross core, the three render backends and templates, matching\bECS\bandentity-component-systemon word boundaries soSPECSandRECSdo not trip it. It earned its place immediately: it caughtROADMAP.md'sEntity-Component-Systemspelling, which a case-sensitive grep for the acronym had missed.Three sentences are allowlisted, each keyed to the file that records it, because naming the architecture as removed is the one thing such prose may still do —
ROADMAP.md's per-GA section, the post-2.0 roadmap's opening paragraph, andbaselines/COMPARISON.md, a dated benchmark log measured while that engine existed. Keyed by path rather than matched globally, so a live document cannot quote one and walk through; listed rather than pattern-matched, since a guard that recognised "gone" would pass any sentence containing the word.Roadmap. Names 2.1.1 and says what it was (tooling and Javadoc-gate fixes; no authoring API moved), so the 2.1 headline stays with 2.1.0 where it belongs.
roadmapCurrentStableSectionNamesAPublishedVersionholds it, beside the README's existing release-status check — pinned to a published version rather than the latest, because between a GA and the next cut the poms already name the version under development while the roadmap correctly still names the shipped one.The cut carries the roadmap, and refuses to fake it. The guard reads the pom to decide which version ROADMAP must name, and the cut bumps every pom in Step 1 before running
mvnw clean verifyin Step 5 — so without this the 2.2.0 cut would have failed its own gate mid-flight.Update-RoadmapCurrentStableswaps the version within the line the section already describes, and stops a cut that crosses one: the heading and prose describe the old line, so rewriting a single token would leave## Current stable — 2.1above**2.2.0** is the current release. A section already naming the target is a no-op.ROADMAP.mdjoins$commitFiles; the version guard now also holds the heading against the body, catching by hand what the script refuses to write.Verification
./mvnw clean verifyover the full reactor — 13/13 modules,BUILD SUCCESS.git diff --checkclean; no baselines, examples or rendered output touched.Both guards checked against the state they exist to catch:
nothingShouldDescribeTheEngineAsEntityComponentSystemWatermarkConfigis restored to "not an ECS entity" — fails naming that fileroadmapCurrentStableSectionNamesAPublishedVersion**2.1.0**— fails naming both the claim and the published setDeliberately untouched: ADR-0016, released
CHANGELOG.mdsections, and a test fixture's CV text. Those are historical records, and the ADR and changelog are already exempt by prefix.Not in scope, from the same audit: the open DOCX issues (#527–#531), backend-neutral measurement, and the BOM / ADR-0017 work.