diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 987a97699..3c502c527 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -21997,3 +21997,70 @@ So `test_the_script_prefers_its_own_repo_over_an_earlier_path_entry` supplies th **Verification:** 8 passed in `tests/test_webconsole_seam_snapshot.py`. Mutation check run rather than argued -- with the `sys.path.insert` line deleted, the decoy test reds naming the decoy import, and the by-path digest test **stays green**, which is the luck described above measured rather than predicted. Anchor restored, 8 passed again. **Adjacent and NOT fixed here, named rather than numbered.** `docs/WEBCONSOLE-PACKAGE.md`'s seam-refresh procedure is stale in three steps left behind by #1220: it says to bump `ENGINE_UI_SEAM` by hand (`1` to `2`) when the value is a derived digest, it says to update curated lists in this script that #1220 retired, and its step 5 prescribes `python scripts/webconsole_seam_snapshot.py > tests/golden/...`, the shell redirect this script's own docstring forbids because PowerShell's `>` writes UTF-16LE with a BOM into a file the test reads as UTF-8. That is doc drift with its own cause and it wants its own item; folding a documentation rewrite into a `sys.path` fix would make both harder to review. + +> **It got one: #1443**, filed and fixed 2026-09-04, in the commit that carries this line. Two of the three descriptions above were sharpened by measurement while it was built: the redirect is wrong in every shell rather than only under PowerShell (a redirect can write just one of the two files `--write` writes, so it leaves the constant stale even where the encoding survives), and the drift was not confined to the numbered steps -- the section heading two screens earlier called the seam an integer and typed it `int`. See #1443's LIMB 2 and LIMB 4. + +## 1443. WEBCONSOLE-PACKAGE.md's seam-refresh procedure teaches three steps #1220 retired, one of which corrupts the golden + +> ๐Ÿ”ข **Filed 2026-09-04 - FIXED IN THIS COMMIT, not yet landed.** Found 2026-09-03 while building #1439, which deliberately left it alone so a `sys.path` fix and a documentation rewrite stayed separately reviewable. The wider census in LIMB 4 is why the fix is not confined to the three steps the finding named. +> **Scored at filing 2026-09-04.** Value **3/10** ยท Difficulty **2/10** ยท _fill-in_. Scored here rather than left for a later pass, because nothing in CI requires a score at filing and an item that enters unranked stays unranked until a pass reaches it -- five items filed since 2026-09-03 already carry none. Value is above the bottom of the table but not by much: there is no engine, PHI or deployment axis, and the whole population is developers changing the console contract, which is rare. It is not a 1 because the defect is not cosmetic staleness -- LIMB 2 measures the old procedure walking a reader into a state where the headline gate PASSES on a fabricated seam, and a procedure that manufactures a false green is worth more to remove than one that merely fails. Difficulty 2 prices the REMAINDER, which is what this table prices and nothing else: a section rewrite in one page, a docstring and its duplicate in the generator, one line in the package release checklist, and this row -- four small prose edits on existing seams, no code path, no test, no CI leg, which is where #1381 sits at the same digit for five citation rewrites plus an ADR amendment. **The measurement that made the prose honest is deliberately NOT priced here.** An earlier draft of this line did price it, and that was a rubric error worth recording rather than silently fixing: investigation already done is not remainder, and counting it would make a well-investigated item cost more than a careless one, inverting the scale. What the controls bought belongs in LIMB 2, which carries it. Correction raised by a peer session on 2026-09-04 and verified against the table's own rule at docs/BACKLOG.md:203, "the scores below price only the remainder". + +**Cluster:** repository tooling. **Priority:** P3. **Verdict:** build. +**Severity:** no engine effect, no PHI axis, and **no deployment axis (sec. 0)** -- this is a developer procedure for a repository gate, and `docs/` and `scripts/` reach no wheel. The cost is bounded to a developer's session and it is the same shape #1439 recorded: **a corrupted golden, then debugging the gate that reported it.** Nothing is mis-shipped, because the seam the engine actually ships is derived by the tool and pinned by the tests either way. + +**What:** [`docs/WEBCONSOLE-PACKAGE.md`](WEBCONSOLE-PACKAGE.md)'s "Bumping the seam on an intentional contract change" still described the pre-#1220 world, in which `ENGINE_UI_SEAM` was a hand-picked incrementing integer over five hand-maintained tuples. #1220 made it a **16-hex-character SHA-256 digest of a DISCOVERED surface**. Three of the procedure's seven steps had no meaning left. + +| Step | Said | Shipped behaviour | +|---|---|---| +| 2 | bump `ENGINE_UI_SEAM` by hand, "e.g. `1` -> `2`" | the value is derived; `test_the_stored_seam_equals_the_derived_digest` fails any hand-written one | +| 4 | if the change touched the *curated* surface, update the list in `webconsole_seam_snapshot.py` | #1220 retired all five tuples; `scripts/seam_discovery.py` discovers the surface and there is no list | +| 5 | `python scripts/webconsole_seam_snapshot.py > tests/golden/webconsole_seam.snapshot` | the script's own docstring forbids exactly this; `--write` is the command, and it rewrites the constant AND the golden together | + +**LIMB 1 -- THE PROCEDURE IS UNFOLLOWABLE, NOT MERELY MISLABELLED, AND THAT IS THE STRONGEST THING AGAINST IT.** Under a derived seam the value does not exist until the change has been made and regenerated. Old step 2 asked the reader to write it, and old step 3 asked them to copy it into the console before old step 5 ever ran the generator. **There is no order in which the old steps can be executed**, so a reader who follows them faithfully cannot finish, and the first thing they will doubt is the gate rather than the page. + +**LIMB 2 -- STEP 5 FAILS IN THE WORST DIRECTION: FOLLOW THE OLD PROCEDURE AND THE HEADLINE GATE GOES GREEN ON A FABRICATED SEAM.** This is the finding that matters, and it is not the one the script's docstring names. + +`build_snapshot()` prints whatever `ENGINE_UI_SEAM` is imported, while `contract_digest()` never reads it. So old step 2 (hand-write the value) followed by old step 5 (regenerate the golden by redirect) produces a golden that AGREES with the hand-written value. Reproduced 2026-09-04 in an isolated copy of `messagefoundry/`, `messagefoundry_webconsole/`, `scripts/` and the golden, with the repository untouched and an assertion that the generator read the copy rather than the real tree: + +``` +hand-written seam in source : deadbeefdeadbeef +seam recorded in the golden : deadbeefdeadbeef +test_webconsole_seam_snapshot_matches_golden : PASS +test_the_stored_seam_equals_the_derived_digest : FAIL (derived=266cbfd342b22819 stored=deadbeefdeadbeef) +``` + +**The gate whose name says it compares the snapshot accepts `deadbeefdeadbeef`.** Exactly one test refuses it. A procedure that walks a developer into that state is worse than one that simply does not work. + +**THE ENCODING STORY IS REAL BUT NARROWER THAN THE DOCSTRING CLAIMS, AND I HAD IT WRONG ONCE BEFORE CORRECTING IT.** Measured on this box, one string redirected by each shell, then read back the way `tests/test_webconsole_seam_snapshot.py:94` reads the golden: + +| Shell | Bytes written | `read_text(encoding="utf-8")` | +|---|---|---| +| Windows PowerShell 5.1 | `ff fe 78 00 0d 00 0a 00` -- UTF-16LE with a BOM | `UnicodeDecodeError: invalid start byte` | +| `pwsh` 7 | UTF-8, no BOM, CRLF | **decodes, and compares EQUAL** | +| Git Bash | UTF-8, no BOM, LF | decodes clean | + +The `pwsh` 7 row is the correction: an earlier draft of this item and of the page claimed CRLF would diff every line. It does not. `read_text` performs universal-newline translation, so a CRLF golden and an LF golden compare equal, verified directly. The claim was plausible, wrong, and would have sent a reader hunting line endings; an adversarial verification pass caught it before it landed. + +So only the 5.1 arm damages the file, and that arm is still the nastiest: the golden is read as UTF-8 with no guard, so the developer gets a decode traceback instead of `_FAILURE_HINT` -- the gate's one mechanism for teaching the repair, bypassed exactly when it is needed. **But the encoding is not the common failure.** The common failure, in every shell including the `bash` the fence advertised, is that a redirect writes one of the two files `--write` writes. + +**LIMB 3 -- THE REMAINING BY-HAND EDIT IS STATED BY THE TOOL, SO THE PAGE LINKS TO IT RATHER THAN RESTATING IT (SDS-3.5).** `--write` deliberately does **not** touch `messagefoundry_webconsole.SUPPORTED_ENGINE_SEAMS`; its own docstring gives the reason, that a tool writing both halves would turn the handshake into a self-consistent tautology and remove the one place a human states this console build matches this engine contract. So the console-side edit stays a step, but its **literal text is printed by `--write` with the new value already in it**, and the page now points at that output. The page keeps the *why* -- one value (#279), a test that reds CI on the same commit, and what widening the set would cost -- because none of that is in the tool's output. + +**LIMB 4 -- THE DRIFT WAS WIDER THAN THE THREE STEPS, AND THE REST OF IT WAS IN THE SECTION THAT DEFINES THE TERM.** Fixing only the numbered list would have left the page contradicting itself two screens earlier: + +- the heading read **"`ENGINE_UI_SEAM` -- the handshake integer"**, and the paragraph under it typed the constant `int` and the console's set `frozenset[int]`. Shipped: `ENGINE_UI_SEAM: str` (`api/_ui_seam.py`) and `SUPPORTED_ENGINE_SEAMS: frozenset[str]` (`messagefoundry_webconsole/__init__.py:48`). A reader who trusted the annotation would write an integer and be refused by mypy before the gate ever spoke. +- the implementation list said **"Three files implement it"** and described the generator as emitting "a **curated** list" plus "**live-introspected**" DTO fields. `scripts/seam_discovery.py` is a fourth file and the whole surface is discovered. The description also predated two capture classes the generator has carried since #1220: enum member sets and `Literal` value sets. That omission is load-bearing rather than tidy -- the console indexes `_SCOPE_NOTES[data.scope]`, so a renamed literal `KeyError`s at runtime while a field-name-only snapshot stays byte-identical, which is precisely the failure the section claims to be the sole backstop against. +- three further sites carried "unbumped" / "seam bump" / "seam-bumping", each implying a number somebody types. + +**ONE RULE WAS ADDED THAT NO VERSION OF THE PAGE EVER CARRIED: on a merge conflict over the seam, NEITHER SIDE IS CORRECT.** When two branches both move the contract surface, all three files holding the value conflict, and the merged surface derives a **third** digest matching neither. Taking either side ships a value describing a tree that does not exist, and it looks exactly like an ordinary conflict resolution. This is #1220's central property read from the other direction -- `api/_ui_seam.py` states it as the reason a digest cannot collide the way the old integer did, but nothing told a developer standing in the conflict what to do. The page now does: clear the markers to anything, rerun `--write`, then set the console side from its output. Reported by a peer session that hit it live on 2026-09-04 (branch `d4ae2ce03d5723a7`, main `266cbfd342b22819`, merged `767521d4399d1ef4`); the mechanism is deterministic from the digest's own construction, so it is recorded here as design rather than as one session's luck. + +**Two things were DELIBERATELY not added.** A `PYTHONPATH` pin before the generator runs, because #1439 anchored the script on `__file__` and that test's `_ANCHOR_HINT` forbids the call-site pin by name, citing this very by-hand procedure as the reason. And the new digest's value, anywhere in prose -- the section's own rule, older than this item, is that a value quoted in prose goes stale silently, which is how it came to say "currently 1" until seam 11. + +**Verification:** `python scripts/webconsole_seam_snapshot.py --digest` returns `266cbfd342b22819`, matching both `ENGINE_UI_SEAM` and `SUPPORTED_ENGINE_SEAMS`, so the tree is self-consistent and the rewritten page describes the state it is in. The redirect table above was measured rather than argued, in a scratch directory outside the repository, with the repository untouched. + +**ONE SIBLING WAS FIXED, BECAUSE IT IS THE TERMINUS OF THIS PROCEDURE.** The page's last step sends the reader to `packaging/messagefoundry-webconsole/RELEASE.md`, whose opening said to "keep the range and the seam **integers** honest". A reader who follows the corrected procedure to its end would have landed on the retired vocabulary one line after leaving it. That file now says digests, and names #1220. A repo-wide sweep found the shell redirect surviving in exactly ONE place -- the page this item fixes -- so there is no second copy of the procedure anywhere, `.github/` included (checked with a positive control, since a pattern that finds nothing is indistinguishable from a clean tree). + +**Adjacent and NOT fixed here, named rather than numbered.** `tests/test_webconsole_seam_snapshot.py:106-127` still says "A seam bump has always required editing BOTH constants" and "on the commit that bumps the engine", and its assertion message opens "Bumping ENGINE_UI_SEAM requires updating...". That prose is not false -- the constant does change -- but it carries the same chosen-number connotation this item removed from the page, and `_FAILURE_HINT` in the same file already teaches the correct repair. It is test prose rather than the procedure a developer follows, so it is left for whoever next edits that file. + +**ONE MORE SIBLING WAS TAKEN, ON A HANDOVER, AND THE ARGUMENT FOR IT IS SDS-3.4 RATHER THAN TIDINESS.** `scripts/webconsole_seam_snapshot.py:41` gave the redirect's reason as the PowerShell encoding alone. That is accurate and it is the wrong thing to publish: a reader on Git Bash consults it, correctly concludes it does not apply to their shell, runs the redirect and silently does half the job -- the exact case measured in LIMB 2, licensed by the warning meant to prevent it. Asking what a reader would DO with the sentence is the SDS-3.4 test, and it fails. The docstring now leads with the one-of-two-files failure and the false green, and demotes the encoding to a shell-specific second hazard; the duplicate at `:303` becomes a pointer rather than a second copy (SDS-3.5). Raised by the session that authored the line under #1439, which declined to open a competing branch for two comment lines and handed it over; taken because this diff is the one that cites the behaviour. It changes no code path -- the derived digest is `266cbfd342b22819` before and after. + +Three more, all in the #1220 class and all deliberately out of this diff so the procedure stays separately reviewable: `messagefoundry_webconsole/__init__.py:29` is a truncated leftover comment ending mid-sentence on the word "the" and naming an integer seam identity, sitting directly above the constant it purports to explain; `packaging/messagefoundry-webconsole/CHANGELOG.md:16` and `:25` make a PRESENT-state claim in `[Unreleased]` that the supported seam is `2, 3`, which is wrong on both the type and on #279's one-value rule; and `docs/adr/0143`'s "curated into the webconsole seam snapshot" names the retired mechanism, though an accepted ADR records what was true when written and is better left dated than edited. Also surfaced and entirely unrelated: `docs/LEDGER-GATE.md:132` credits `install-git-hooks.ps1` with installing the ledger gate, which its own synopsis says it no longer does, and `:164`'s two anchors into `ledger_check.py` have drifted to `:376` and `:465`. diff --git a/docs/WEBCONSOLE-PACKAGE.md b/docs/WEBCONSOLE-PACKAGE.md index 5782a24e0..10e11ece7 100644 --- a/docs/WEBCONSOLE-PACKAGE.md +++ b/docs/WEBCONSOLE-PACKAGE.md @@ -56,10 +56,14 @@ console package, so `import messagefoundry.api.app` still succeeds with the cons (`add_auth_routes` runs unconditionally and returns an `AdminHandlers` built from this leaf, so its concrete type must live engine-side). -### `ENGINE_UI_SEAM` โ€” the handshake integer - -`api/_ui_seam.ENGINE_UI_SEAM: int` is the contract version the engine ships โ€” **read the current value from that module**; it is deliberately not restated here, because a number quoted in prose goes stale silently (this line said "currently 1" until seam 11). The -console declares `messagefoundry_webconsole.SUPPORTED_ENGINE_SEAMS: frozenset[int]` and refuses a skew +### `ENGINE_UI_SEAM` โ€” the handshake digest + +`api/_ui_seam.ENGINE_UI_SEAM: str` is the contract identity the engine ships: a 16-hex-character +SHA-256 digest of the contract surface, **derived and never chosen** (BACKLOG #1220). **Read the +current value from that module**; it is deliberately not restated here, because a value quoted in +prose goes stale silently (this line said "currently 1" back when the seam was a hand-picked +integer). The console declares `messagefoundry_webconsole.SUPPORTED_ENGINE_SEAMS: frozenset[str]` +and refuses a skew at startup via `assert_engine_seam(engine_seam)`, which raises `UiSeamMismatch` with a clear message rather than a raw `TypeError`. The handshake is **three-layered** and fails loud at every layer: @@ -70,7 +74,7 @@ rather than a raw `TypeError`. The handshake is **three-layered** and fails loud surfaces as `UiSeamMismatch`, not a kwargs `TypeError`. A second identical assert at the top of `mount_ui` is belt-and-suspenders. 3. **CI** โ€” the package suite runs against the supported engine seam(s); the engine repo's snapshot gate - (below) fails on an unbumped incompatible change. + (below) fails on an incompatible change the seam did not follow. ### `mount_ui(app, deps)` and the injected bundle @@ -91,8 +95,8 @@ Handler fields are typed `Callable[..., Awaitable[Any]]` (engine/gate params ins The auth dep factories โ€” `require`, `require_step_up`, `require_reauth_only`, `get_auth`, `authorize_ws`, `ws_token` โ€” are **not** injected; the console imports them directly from `messagefoundry.api.security` -(leaf-safe). Their public surface is part of the seam's compat scope even so (a re-signature there is -seam-bumping) and is captured by the snapshot gate. +(leaf-safe). Their public surface is part of the seam's compat scope even so (a re-signature there +moves the seam) and is captured by the snapshot gate. ### The `app.state` hooks @@ -116,56 +120,105 @@ Independent versioning is exactly what makes runtime skew possible, so the engin **contract-snapshot gate**. Its purpose: a silent, incompatible change to the injected contract โ€” a renamed handler field, a re-signatured `api.security` dep or `AuthService` method, or a renamed field on a **DTO the console renders** (which breaks render, not import, so `mypy` alone misses it) โ€” must fail -CI until `ENGINE_UI_SEAM` is bumped. +CI until `ENGINE_UI_SEAM` is regenerated. + +**The surface is DISCOVERED, not enumerated** (BACKLOG #1220). It used to be five hand-maintained +tuples in the generator, and three of them had drifted: a list like that cannot detect its own +omissions, because the gate's coverage *is* the list. Nothing below asks you to keep one current. -Three files implement it: +Four files implement it: +- [`scripts/seam_discovery.py`](../scripts/seam_discovery.py) โ€” walks the console's own imports and + uses to derive the surface it depends on: the `api.security` deps, the `AuthService` members + (methods **and** properties), the `app.state` attributes, and the DTOs it renders, closed over + nested models. An idiom the walk cannot resolve exactly raises `SeamDiscoveryError` rather than + being skipped, so a new blind spot is loud instead of silent. - [`scripts/webconsole_seam_snapshot.py`](../scripts/webconsole_seam_snapshot.py) โ€” emits a stable, - deterministic text serialization of the contract: `ENGINE_UI_SEAM`; the `UiDeps` / `CoreHandlers` / - `AdminHandlers` dataclass field names; a **curated** list of the cross-seam surface consumed outside - the bundle (the `api.security` deps' names+signatures, the `AuthService` methods, the `app.state` - attributes); and the **live-introspected** field sets of the `api.models` / `api.auth_models` DTOs the - console renders (so a rename on exactly those DTOs changes the snapshot). + deterministic text serialization of that surface: `ENGINE_UI_SEAM`; the `UiDeps` / `CoreHandlers` / + `AdminHandlers` dataclass field names; the discovered cross-seam surface consumed outside the + bundle; the DTO field sets, closed over nested models; and the enum member sets and `Literal` value + sets those DTOs expose. Field names alone are not the contract โ€” the console indexes a dict by + `UploadedFileList.scope`, so a renamed literal would `KeyError` at runtime while a + field-name-only snapshot stayed byte-identical. - [`tests/golden/webconsole_seam.snapshot`](../tests/golden/webconsole_seam.snapshot) โ€” the checked-in golden. - [`tests/test_webconsole_seam_snapshot.py`](../tests/test_webconsole_seam_snapshot.py) โ€” regenerates the - snapshot and diffs it against the golden, failing with an actionable hint on any drift. + snapshot and diffs it against the golden, failing with an actionable hint on any drift. It also + pins the digest itself: `test_the_stored_seam_equals_the_derived_digest` fails any value that was + not derived from the current surface. -This gate is the **sole backstop** against a *future* engine's unbumped, render-breaking DTO rename -(the package CI matrix only covers engines that exist at package-CI time), so it must stay comprehensive -and blocking. +This gate is the **sole backstop** against a *future* engine's render-breaking DTO rename that the +seam did not follow (the package CI matrix only covers engines that exist at package-CI time), so it +must stay comprehensive and blocking. -### Bumping the seam on an intentional contract change +### Refreshing the seam on an intentional contract change + +**You cannot know the new value in advance, and you never type one.** The seam is a digest of the +surface `seam_discovery.py` finds, so it exists only once the change is made and regenerated, and +`test_the_stored_seam_equals_the_derived_digest` fails any hand-written value. When you deliberately change the injected contract (add/rename a `CoreHandlers`/`AdminHandlers` field, -change an `api.security` dep signature, rename a rendered DTO field, add/remove an `app.state` hook or a -consumed `AuthService` method): +change an `api.security` dep signature, rename a rendered DTO field or one of its `Literal` values, +add/remove an `app.state` hook or a consumed `AuthService` member): 1. Make the contract change in the engine (and the matching consumer change in the package). -2. Bump `ENGINE_UI_SEAM` in [`messagefoundry/api/_ui_seam.py`](../messagefoundry/api/_ui_seam.py) - (e.g. `1` โ†’ `2`). -3. Update `messagefoundry_webconsole.SUPPORTED_ENGINE_SEAMS` in - [`messagefoundry_webconsole/__init__.py`](../messagefoundry_webconsole/__init__.py) to the NEW seam - alone โ€” it holds exactly one value (BACKLOG #279). A test asserts - `SUPPORTED_ENGINE_SEAMS == {ENGINE_UI_SEAM}`, so forgetting this step fails CI on the bump commit - rather than at a deployment's startup. Widening it back to a range requires landing the cross-seam - CI matrix in the same change. -4. If the change touched the *curated* surface (an `api.security` symbol, an `AuthService` method, an - `app.state` attribute, or which DTOs the console renders), update the corresponding list in - `scripts/webconsole_seam_snapshot.py`. -5. Refresh the golden (write UTF-8, no BOM): +2. Regenerate the engine side: ```bash - python scripts/webconsole_seam_snapshot.py > tests/golden/webconsole_seam.snapshot + python scripts/webconsole_seam_snapshot.py --write ``` -6. Confirm green: `python -m pytest tests/test_webconsole_seam_snapshot.py -q`. -7. At release, update the compat range on both sides (the engine `[webconsole]` extra and the package's + That rewrites **both** `ENGINE_UI_SEAM` in + [`messagefoundry/api/_ui_seam.py`](../messagefoundry/api/_ui_seam.py) and the golden + [`tests/golden/webconsole_seam.snapshot`](../tests/golden/webconsole_seam.snapshot). + + **Use `--write`, never a shell redirect over the golden**, and note that this fails in the worst + direction. A redirect writes the golden and never the constant, in every shell. The golden *embeds* + whatever `ENGINE_UI_SEAM` currently says, so redirecting makes the golden agree with a stale or + hand-typed value and `test_webconsole_seam_snapshot_matches_golden` goes **green** โ€” measured + against a fabricated `deadbeefdeadbeef`. Exactly one test refuses it, + `test_the_stored_seam_equals_the_derived_digest`. (Windows PowerShell 5.1 also emits UTF-16LE with + a BOM there, which the test cannot decode as UTF-8 at all; `pwsh` 7 does not.) +3. Set the console side **by hand, in the same commit**. `--write` deliberately leaves it alone โ€” a + tool that wrote both halves would turn the handshake into a self-consistent tautology, removing + the one place a human states that this console build matches this engine contract. Its output + prints the exact one-line edit, with the new value already in it; paste that. + + `messagefoundry_webconsole.SUPPORTED_ENGINE_SEAMS` in + [`messagefoundry_webconsole/__init__.py`](../messagefoundry_webconsole/__init__.py) holds exactly + one value (BACKLOG #279), and a test asserts `SUPPORTED_ENGINE_SEAMS == {ENGINE_UI_SEAM}`, so + forgetting this step reds CI on the same commit rather than becoming a hard startup refusal at a + deploying site. Widening it back to a range requires landing the cross-seam CI matrix in the same + change. +4. Confirm green: `python -m pytest tests/test_webconsole_seam_snapshot.py -q`. +5. At release, update the compat range on both sides (the engine `[webconsole]` extra and the package's `messagefoundry>=X,`` emits UTF-16LE with a -BOM, which corrupts a file the test reads as UTF-8. +Use ``--write``, never a shell redirect over the golden. A redirect writes the golden and never +``ENGINE_UI_SEAM``, in EVERY shell, and it fails GREEN rather than red: ``build_snapshot`` prints +whatever seam is imported, so a redirected golden AGREES with a stale or hand-typed constant and +``test_webconsole_seam_snapshot_matches_golden`` passes. Measured against a fabricated +``deadbeefdeadbeef``, only ``test_the_stored_seam_equals_the_derived_digest`` refused it. + +The encoding damage is a SECOND hazard layered on that one, and it is shell-specific, so it must not +be read as the reason: Windows PowerShell 5.1's ``>`` emits UTF-16LE with a BOM, which the test +cannot decode as UTF-8 at all, while ``pwsh`` 7 and Git Bash write a clean file and still leave the +constant stale. An earlier version of this note gave only the encoding reason, which licensed the +redirect for anyone not on 5.1 -- the failure it was warning against. """ from __future__ import annotations @@ -300,8 +309,8 @@ def write_seam_and_golden() -> str: # Rebuild with the NEW value in scope. The digest itself does not depend on the seam (see # contract_sections), but the snapshot's first section prints it. globals()["ENGINE_UI_SEAM"] = digest - # Written explicitly rather than via a shell redirect: PowerShell's `>` emits UTF-16LE with a - # BOM, which corrupts a golden the test reads as UTF-8. + # Written explicitly rather than via a shell redirect, which would write this file and never the + # constant rewritten above it. See the module docstring for why that fails green. _GOLDEN_FILE.write_text(build_snapshot(), encoding="utf-8", newline="\n") return digest