Skip to content

docs(engine): retire the ECS prose, and guard the trees that let it survive - #554

Merged
DemchaAV merged 14 commits into
developfrom
chore/retire-ecs-prose-and-roadmap-drift
Aug 14, 2026
Merged

docs(engine): retire the ECS prose, and guard the trees that let it survive#554
DemchaAV merged 14 commits into
developfrom
chore/retire-ecs-prose-and-roadmap-drift

Conversation

@DemchaAV

@DemchaAVDemchaAV commented Aug 14, 2026

Copy link
Copy Markdown
Owner

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, Entity or ComponentSystem survives in any src/main tree, and there is no ecs sub-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", and render-pdf's package-info sent them to an ecs sub-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 Guards job, and that is the part worth fixing. The retired-token scan reads markdown only; 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.

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-info records the removal instead of pointing at where the thing supposedly went.

A scan that reads both.nothingShouldDescribeTheEngineAsEntityComponentSystem walks .md and .java across core, the three render backends and templates, matching \bECS\b and entity-component-system on word boundaries so SPECS and RECS do not trip it. It earned its place immediately: it caught ROADMAP.md's Entity-Component-System spelling, 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, and baselines/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. roadmapCurrentStableSectionNamesAPublishedVersion holds 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 verify in Step 5 — so without this the 2.2.0 cut would have failed its own gate mid-flight. Update-RoadmapCurrentStable swaps 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.1 above **2.2.0** is the current release. A section already naming the target is a no-op. ROADMAP.md joins $commitFiles; the version guard now also holds the heading against the body, catching by hand what the script refuses to write.

Verification

./mvnw clean verify over the full reactor — 13/13 modules, BUILD SUCCESS. git diff --check clean; no baselines, examples or rendered output touched.

Both guards checked against the state they exist to catch:

GuardRed when
nothingShouldDescribeTheEngineAsEntityComponentSystemWatermarkConfig is restored to "not an ECS entity" — fails naming that file
roadmapCurrentStableSectionNamesAPublishedVersionROADMAP is restored to **2.1.0** — fails naming both the claim and the published set

Deliberately untouched: ADR-0016, released CHANGELOG.md sections, 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.

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

Copy link
Copy Markdown
OwnerAuthor

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 mvnw clean verify, and nothing touched ROADMAP.md. The guard would have read 2.2.0 from the pom and found 2.1.1 in the section, aborting mid-flight with the tree already rewritten.

Update-RoadmapCurrentStable now rewrites the bolded version in ## Current stable and nothing else — prose and every per-release section below stay put. It sits under the same $isFinalRelease branch as Update-ReadmeReleaseStatus, so a pre-release keeps naming the last version a reader can resolve on Central. Test-RoadmapCurrentStable joins the Step 0 post-mutation checks beside Test-ReadmeLatestStable, and ROADMAP.md is in $commitFiles. Both no-op cases throw rather than pass quietly — a missing section, or one with no bolded version — because either would otherwise resurface as a verify-gate failure after the bump.

Proven by dry-run at 2.2.0:

[DRY RUN] ROADMAP.md 'Current stable' -> 2.2.0
[DRY RUN] git add … README.md ROADMAP.md CHANGELOG.md …

ReleaseScriptRoadmapGuardTest (3 cases) holds the wiring: the function exists, Step 1 calls it, the pre-flight verifies it, and the file reaches the commit. It deliberately does not assert the call sits inside $isFinalRelease — matching that from outside pins block layout rather than behaviour, and the failure that costs a cut is the call going missing entirely.

Major — both sites fixed, and both blind spots with them.

post-2.0-engineering.md claimed layout still resolves on a live Entity / EntityManager model with EntityBounds, marked Deferred. Checked: zero declarations of any of the three in any src/main tree, zero references from testing/. 2.0 already did the work that entry planned, so it now records that. The scan skipped all of docs/roadmaps/ as archival — but the root roadmap links this file as committed engineering direction, so it is a live document under an archival prefix. That directory is no longer skipped by this scan.

render-pdf/pom.xml said "the ecs system + handlers". You are right that nothing had ever read a pom: the scan now covers .xml and walks whole modules rather than source roots, with target/ excluded so a stale generated copy cannot report a violation with no source to fix.

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 acceptableTargets() put my javadoc between that method's comment and its subject, so java bound the comment to the new test. The same orphaning this branch's sibling work fixed in the PPTX render environment, reintroduced two files over. The test moved above the comment.

Full reactor clean verify — 13/13, BUILD SUCCESS. Guards still red on the state each exists to catch.

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

Copy link
Copy Markdown
OwnerAuthor

Blocker confirmed on both halves, and both reproduced before fixing.

The contradiction: heading ## Current stable — 2.1, body all 2.1 prose, and a rewrite that touches one token. The idempotency bug I reproduced live — a section prepared at **2.2.0** threw names no bolded X.Y.Z version to update, which is the section having exactly what was asked for.

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:

ROADMAP.md '## Current stable' describes the 2.1 line, but this cut is 2.2.0 (2.2).
Rewriting the version alone would leave the heading and the prose describing 2.1.
Prepare the section before cutting: update the heading to '## Current stable — 2.2',
write what this line leads with, and move the 2.1 text down to a 'Previously' section.
Then re-run the cut — a section already naming 2.2.0 is accepted as-is.

All three of your scenarios, run against the live roadmap:

ScenarioResult
patch 2.1.5[DRY RUN] ROADMAP.md 'Current stable' -> 2.1.5
minor 2.2.0, unpreparedrefused with the message above
minor 2.2.0, section preparedROADMAP.md 'Current stable' already names 2.2.0

The guard gained the heading check you implied by naming the contradiction — on its own the version check passes — 2.1 above **2.2.0**. Red when the heading is desynced: "heading says line 2.2 while the section names 2.1.1 — one of the two is describing the wrong release, and a reader has no way to tell which".

Real script tests, in the repo's own idiom — release-script-check.yml runs the script rather than reading it, so the tests go there. One catch worth naming: the stricter check would have failed the existing -Version 9.9.9 rehearsal, since 9.9 is not the roadmap's line. Hardcoding any replacement just moves the breakage to the day the project changes lines — when a working rehearsal matters most — so the rehearsal version is now derived from the roadmap heading, and a second step asserts the line-crossing refusal. ReleaseScriptRoadmapGuardTest holds the source-level markers so neither half can vanish from the script while CI rehearses a version that happens to sit on the current line.

Minors.ENGINE_HISTORY_SENTENCES is now Map<path, fragments> — you were right that the global form was the whole-file allowlist one step smaller, and a live document could have quoted its way through. release-process.md Step 3b and the staging list corrected (the script does own the README block, and ROADMAP is in $commitFiles), Step 3b now also states what a line-crossing cut refuses. VersionConsistencyGuardTest's "nothing rewrites it" corrected. PR description updated for the keyed allowlist and the new cut behaviour.

Full reactor clean verify — 13/13, BUILD SUCCESS; git diff --check clean.

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

Copy link
Copy Markdown
OwnerAuthor

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 ## Upcoming — X.Y. It says what the next line leads with while Current stable goes on naming what is actually on Central — so develop stays truthful and green for the whole cycle, and none of the three deadlocks applies. The cut promotes it: UpcomingCurrent stable, and the line it replaces → Previously.

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 (-SkipPush -SkipVerify) at 2.2.0 with 2.2 staged:

ROADMAP.md promoted 'Upcoming — 2.2' to 'Current stable' (2.2.0); 2.1 moved to 'Previously'
[chore/retire-ecs-prose-and-roadmap-drift b998870b] Release v2.2.0
## Current stable — 2.2
**2.2.0** is the current release. …
## Previously — 2.1
## Previously — 2.0

The workflow now covers all three paths you listed:

StepAsserts
in-line cutrehearsal at X.Y.99, derived from the roadmap heading
crossing cut, unstagedrefused andgit status --porcelain empty afterwards
crossing cut, stagedpromotion checked after a real run, not a dry one — Current stable — X.(Y+1), old line demoted, no Upcoming heading left

The staged section is guarded too, or it becomes the same stale claim about "next" that the roadmap was making about "now": aPreparedUpcomingSectionNamesTheLineUnderDevelopment requires it to name the line the poms are on and to differ from the published line, so a copy left behind after promotion fails. Absent is the normal state and passes. Red on a stale one: "stages an '## Upcoming — 2.9' section while the poms are on 2.2.0-SNAPSHOT".

release-process.md Step 3b now documents the staging commit as the prerequisite for a minor, with the markdown to copy.

Full reactor clean verify — 13/13, BUILD SUCCESS.

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

Copy link
Copy Markdown
OwnerAuthor

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 actions/checkout leaves a PR on a detached HEAD — so the preflight rejected it for precisely the reason it exists. Relaxing it would have deleted the thing under test. The rehearsal now builds a bare remote in the runner and works on a branch of its own, so named branch, clean tree and in sync with origin are all satisfied honestly, staging commit included.

It also runs last now. It leaves the checkout on that branch with a release commit and origin pointing at the bare repo, and five later steps use -Branch develop against the real remote — my ordering would have handed them a repository pointing somewhere else. That was a second failure waiting behind the first.

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 X.Y.Z, and requires it on the line being cut (a section headed 2.2 staging **2.3.0** is refused too). The Java guard carries the same contract, where a maintainer staging the section meets it first. And the promotion 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.

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 Current stable, verified by what follows its body, not merely by ordering.

Rehearsed locally in the exact CI shape (bare remote, own branch, real cut, -SkipPush -SkipVerify):

ROADMAP.md promoted 'Upcoming — 2.2' to 'Current stable' (2.2.0); 2.1 moved to 'Previously'
[release-rehearsal 08bef5c6] Release v2.2.0
## Current stable — 2.2
## Previously — 2.1
## Previously — 2.0

And the malformed case refuses with carries no bolded version for the cut to promote, leaving the tree clean.

Full reactor clean verify — 13/13, BUILD SUCCESS.

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

Copy link
Copy Markdown
OwnerAuthor

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 Get-NextSnapshotVersion — lift them and call them. Lifted by AST rather than by scanning for a closing brace, so the extraction cannot pick up half a body, and the step fails loudly if any function it needs has been renamed away.

That removes the shallow-push, the bare remote, mvnw.cmd and Step 4 in one go — and CI finally reaches the promotion, which as you note it never had.

Five cases, each on a scratch roadmap of its own:

CaseExpected
nothing stagedrefused, naming what to prepare
staged, no bolded versionrefused — the one that used to slip through
staged, version off the cut's linerefused
staged correctlyfile really rewritten; passes the script's own Test-RoadmapCurrentStable
re-run on the promoted fileno-op, not a throw

Run locally, verbatim from the workflow:

ROADMAP.md promoted 'Upcoming — 2.2' to 'Current stable' (2.2.0); 2.1 moved to 'Previously'
ROADMAP.md 'Current stable' already names 2.2.0
roadmap promotion: 3 refusals, 1 rewrite, idempotent on re-run.

The refusal-before-mutation check still runs the real script against the real checkout, because what it asserts is that git status stays clean — that one needs the actual cut.

On the mvnw.cmd hardcode: you are right that it is real — the script cannot run on Linux or macOS at all, and pwsh is the only cross-platform thing about it today. I have deliberately not fixed it here. Nothing in this workflow needs it now, and changing which wrapper the release tooling invokes is a change to the release path, not to the roadmap contract this PR is about. Happy to do it as its own change if you want it; it is a one-line selection plus a dry-run on both runners to prove it.

Full reactor clean verify — 13/13, BUILD SUCCESS.

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

Copy link
Copy Markdown
OwnerAuthor

Nit fixed in 77873260 — it was worth doing, because it is the second instance of the same fault in this file: the rehearsal version above it was hardcoded for exactly the same reason and broke for exactly the same one.

The off-line version is now derived as minor + 2. What made it worth catching is that the failure is silent in the wrong direction — at 2.8 the staged 2.9.9 becomes the line being cut, the refusal it asserts stops happening, and the case goes on passing while proving the opposite of its name.

Checked against a roadmap on 2.8, the line where the literal would have flipped:

ROADMAP.md promoted 'Upcoming — 2.9' to 'Current stable' (2.9.0); 2.8 moved to 'Previously'
roadmap promotion: 3 refusals, 1 rewrite, idempotent on re-run.

Still 3 refusals, so the case is still taking its own branch there.

@DemchaAV
DemchaAV merged commit b862b0b into developAug 14, 2026
14 checks passed
@DemchaAV
DemchaAV deleted the chore/retire-ecs-prose-and-roadmap-drift branch August 14, 2026 10:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@DemchaAV