Skip to content

Release 1.1.1 — two delivery-path fixes from views-crafdapi, and the governance pass behind them - #276

Merged
Polichinel merged 5 commits into
mainfrom
development
Aug 15, 2026
Merged

Release 1.1.1 — two delivery-path fixes from views-crafdapi, and the governance pass behind them#276
Polichinel merged 5 commits into
mainfrom
development

Conversation

@Polichinel

Copy link
Copy Markdown
Collaborator

Step 2 of Bump → Merge → Tag → Release → Verify. This merge makes main carry version = "1.1.1", which is what the tag will then label.

What is in the release

Two bug fixes on the delivery path, both found by views-crafdapi on the first un_crafd delivery attempt, both in code the FAO leg shares:

#268_ContractStorePort.download failed open. It chained .get() onto an unvalidated store result, so a result whose data was present-and-null raised AttributeError three frames away, naming neither the file_id nor the fact that a download had failed. It was byte-identical in both partners, so the FAO leg carried it too. It now refuses anything that is not non-empty bytes and says what it got. Register C-99.

#269 — assembling a target held three copies of it. Measured before changing anything: peak was 3.06x the delivered frame (RSS agreed at 3.02x), in three roughly equal thirds — every shard's bytes resident together, every decoded per-shard frame, and the np.concatenate copy. Now 1.13x. At production scale that is roughly 26.6 GB → 9.8 GB per target. The wire is byte-frozen and the end-to-end byte-parity fixture passes unchanged. Register C-101.

#265 discharged two register deferrals that had lost their owner — a proof that drove a copy of its subject rather than the subject, and a test module that had become the home for two unrelated concerns.

What review caught along the way, since that is the honest summary

Across these three stories the review gates found, and this branch fixed: a store-port move that dodged the line budget rather than satisfying it (counted lines fell while the package grew); that nothing in the suite assembled more than one shard, so the core of the memory rewrite shipped unverified; a KeyError from inside the store that would have been reported as a missing manifest entry, blaming the manifest for a store fault; and a release guard that would have reddened honest branches the moment it was switched on in CI.

They also found nine false claims in my own prose across the arc — including a 2/n_shards formula that fit none of its own numbers, an ADR section cited as §5 when it is §0, and a citation of two register entries that say nothing like what I said they said. Each is corrected at its source.

Version

1.1.1, a patch. The only external imports of this package org-wide are UNFAOPostProcessorManager and CRAFDPostProcessorManager, whose paths are unchanged; nothing outside this repo imports track_a_source, frames_for_target, store_port or _ContractStorePort. Consumers pin by git tag in shell launchers, not by a semver range. No new capability.

main is currently 6 commits past tag 1.1.0 (the 2026-08-13 governance truth-pass, correctly left untagged because the package tree was byte-identical). This tag will cover both.

Verification

ruff clean; 457 passed, 1 skipped, 39 xfailed locally and in CI.

protect_main requires the test check with no bypass actors, so this cannot merge red.

After merge: tag 1.1.1 on the merge commit, then the GitHub Release. Refs #125.

Polichinel and others added 5 commits August 14, 2026 23:13
…ibling (#270)

* fix(store): #268 — download refuses instead of returning None, and moves out of the manager

views-crafdapi found this in our code, on the first un_crafd delivery attempt, and it
is C-79 with the method name changed.

THE DEFECT. `download` chained .get() onto an unvalidated store result:

    self._dsm.download_prediction(file_id).to_dict().get("data", {}).get("file_bytes", None)

When `data` is PRESENT and null the {} default never applies, so the next .get raised
AttributeError from inside a dict comprehension over pinned ids in TargetLease.load —
three frames from the port, naming neither the file_id nor the fact that a download had
failed. They spent an evening ruling out an OOM kill that turned out to be a different
pid three minutes later.

The method was byte-identical in both partners, so THE FAO LEG CARRIES IT TOO. It has
simply not fired there yet.

WHY NOTHING CAUGHT IT. tests/test_store_port.py was written for C-79 with five
parametrised tests across both partners, one of them literally named
test_an_unrecognised_result_is_refused_rather_than_assumed_good. It mentioned `download`
zero times. And contract/store_metadata.py already wrote `.get("data", {}) or {}` — the
guard this lacked, one file away, never applied. C-79's own resolution note was the
specification the whole time: "an unrecognised result should be refused and named, not
adapted to silently."

THE FIX. Refuse anything that is not non-empty bytes, naming the file_id, that a
DOWNLOAD failed, and the types actually received. Empty bytes are refused with the rest:
no shard, sidecar or manifest is ever zero-length, so b"" is a failed download wearing a
valid type, and returning it only moves the same crash to the parser. Kept byte-identical
across partners, as C-79 chose for upload (C-33). This protects all three call sites, not
only the one that crashed — two of them are manifest reads.

Mutation-proven on three: restoring the original one-liner fails 18 of the module's
tests, accepting empty bytes fails exactly 2 (one per partner), dropping the file_id from
the message fails exactly 2.

IT ALSO MOVED, because a guard said so. The refusal pushed managers/ to 469 lines against
epic #148's 450 bound, and that budget's instruction is to move something out rather than
raise the number. _ContractStorePort is not the manager, so it is now
{partner}/store_port.py — 388 lines, 62 of headroom. Two identical files, per the standing
per-partner-track decision.

The port stopped naming DatastoreModule in its constructor on the way out. A DIP seam
whose stated purpose is that nothing downstream sees the client's types should not name
one; and a new module mentioning views_pipeline_core would have widened C-40's blast
radius past the two files test_views_pipeline_core_is_confined_to_the_partner_managers
pins. The contract is the four methods, and it is now stated as such.

REGISTERED. C-99 for the defect, resolved here. C-100 for what reading the whole port
turned up: `file_metadata` has no caller in the package — latest_file_id runs three
times, download three times, upload from the sink, and the fourth method is reachable
only from a test, with contract/store_metadata.py dead behind it. Not deleted here; that
is a decision belonging with the second store (#97), not with a download bug.

Suite 446 passed / 1 skipped / 39 xfailed, ruff clean.

Closes #268.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(guards): the review found the move dodged the budget it claimed to satisfy

Five reviewers on the download fix. The download refusal itself survived — one agent
traced the pipeline-core SDK and confirmed the realistic failures (yanked file, expired
key, rate limit, network blip) all arrive as success=False rather than as exceptions,
that nothing on the path returns memoryview, that bytes(b) is a no-op passthrough on the
common path, and that no artifact on the contract path is legitimately zero-length. What
did not survive was how I described the part around it.

THE MOVE DODGED THE BUDGET RATHER THAN SATISFYING IT. I reported "388 lines, 62 of
headroom" as compliance. Re-measured: managers/ fell 441 -> 388 while each partner
package grew 441 -> 488. The guard counts managers/, and store_port.py is a sibling OF
managers/, so 47 lines left the budget's view rather than the codebase. The budget's own
docstring had already named this exact failure — "an 800-line helper module beside a
406-line manager was previously unbudgeted, which is the same regrowth wearing a
different filename" — and closed it one level in. I evaded it one level out.

This is C-98 again: a guard that watches a proxy reports on the proxy, and the number it
prints is true and irrelevant. test_the_partner_package_stays_within_its_line_budget now
bounds the whole partner package at 700 (measured today: unfao 626, crafd 635), and is
mutation-proven by dropping a 200-line module beside the manager and watching it fire.
The extraction still stands — a store adapter is not the manager.

C-100 SAID "Measured" AND THEN DID NOT MEASURE ONE OF THE FOUR. It gave exact counts for
latest_file_id and download and wrote "upload is called by the sink". upload has THREE
call sites: contract/wire/sink.py:164 and each partner's historical artifact at
managers/<partner>.py:325. Corrected, with the sites named.

C-33's EXTRACTION TRIGGER HAS FIRED, TWICE, AND NOBODY SAID SO. Its trigger is "a third
in-repo partner package, or the first bug that must be hand-patched identically in both
manager files". C-79 was that bug; C-99 is the same fault in the sibling method, patched
by hand in both again. The decision is still to duplicate, but the reason is now
different and is recorded: the shape the two incidents showed is a result-check, not the
store-identity DeliveryProfile this entry proposes extracting, which would have prevented
neither.

What did change is that the duplication is now held mechanically —
test_the_two_partners_ports_have_not_drifted. And the docstring is precise about what
that does NOT buy: it would not have caught C-79 or C-99, because both files stayed
byte-identical while carrying the defect in the untreated method. It closes the
partner-vs-partner axis, which was never the axis that bit.

FOUR STALE LOCATIONS, from moving a class four documents point at. C-40 cited the port at
unfao.py:37-78 in two places, C-15/C-24 at unfao.py:37-64, and the register header still
said Last Updated 2026-08-12. The one doc that WAS updated in the first commit —
contract/store_metadata.py — showed the fix pattern was known and applied one file over,
which is ADR-014 §1's failure mode exactly.

Suite 449 passed / 1 skipped / 39 xfailed, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ak to 1.13x (#271)

* perf(wire): #269 — assembling a target held three copies of it, not one

Measured before changing anything, and the issue's diagnosis was a third of the story.

WHAT THE MEASUREMENT SHOWED. A synthetic run through the real TargetLease.load —
producer-format .tap.zip shards, real read_shard, tracemalloc and peak RSS agreeing
within 2% — at 36 shards x 20,000 cells x 200 samples:

  peak, tracemalloc   3.06x the delivered frame  ->  1.13x
  peak, RSS delta     3.02x                      ->  1.06x

The 3x was three roughly equal thirds, and #269 named only the first: every shard's
bytes resident together; every decoded per-shard frame held for the stack; and the
np.concatenate result allocated while those were still alive. The ratio held at both 12
and 36 shards, so it is the shape and not the scale. Fixing only the part the issue
described would have reached about 2x.

Two false starts worth recording, because both would have produced a confident wrong
number. My first harness had the fake store return pre-built objects, so the download
allocation fell outside the measurement window and the peak read 2.04x. The obvious fix
— return bytes(blob) — allocates nothing, because bytes(b) is b when b is already bytes;
so does b"" + b. It took a bytearray to get a genuine copy. The 3.06x only appeared once
the store actually allocated, which is what a real download does.

THE FIX IS ONE LOOP. frames_for_target takes fetch_shard_bytes(name) instead of a filled
dict, drops each shard's bytes the moment they are decoded, and writes each shard into a
manifest-sized buffer instead of stacking and concatenating. Peak is now the finished
frame plus about one shard; the residual 0.13x is 2/n_shards, which is why 12 shards
measures 1.36x and 36 measures 1.13x.

The buffer's slots are sized from expected_cell_count, which this function already
enforced per shard — and the enforcement runs BEFORE anything is written, so a shard
whose row count disagrees is refused rather than straddling two months' slots.

NOTHING ABOUT THE PRODUCT CHANGED. The wire is byte-frozen (ADR-013) and
tests/test_wire_fixture.py compares delivered artifacts against checked-in bytes. It
passes unchanged, which is the whole claim: an assembly change with no output change.

AT PRODUCTION SCALE, 64,742 cells x 36 months at ADR-013 §5's "~1000 samples per cell"
(the one input taken from the contract rather than measured), one target's frame is
8.68 GB, so peak falls from about 26.6 GB to 9.8 GB per target.

THE MANAGER'S HISTORICAL FRAME IS NOT THE ELEPHANT. #269 also notes _historical_frame is
held from _read through _save. By its declared dimensions — 64,742 cells x 438 months =
28,356,996 rows — that is about 108 MB at one float32 column, 1.2% of a single forecast
target frame. Recorded in C-101 rather than filed as its own issue: a separate issue
implying comparable cost would misdirect whoever picked it up.

GUARDED by asserting the fetch/decode interleaving rather than a byte count — a memory
threshold in a test is a flake on a busy machine, while "fetch, decode, fetch, decode"
is exactly the property that bounds the peak. Mutation-proven: restoring the up-front
dict yields ['fetch','fetch','fetch','decode','decode','decode'] and it fails.

The register guard also caught me inventing a cross-reference to a register entry C-126
that does not exist; the 126 is an issue number.

Suite 450 passed / 1 skipped / 39 xfailed, ruff clean.

Closes #269.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(wire): refuse mismatched draw counts in our own words, not numpy's

Self-review of the assembly change: sizing the buffer from the first shard MOVED a
constraint. A run whose shards disagree on draws per cell used to fail inside
np.concatenate; it now fails inside the assignment. Both are bare numpy ValueErrors —

  new: could not broadcast input array from shape (6,2) into shape (6,4)
  old: all the input array dimensions except for the concatenation axis must match
       exactly, but along dimension 1, the array at index 0 has size 4 ...

neither names the shard, neither says "draws", and the second is only wordier. That is
the C-99 shape exactly, caught before it could bite rather than after, and the constraint
is now mine rather than incidental, so the refusal should be too.

Mutation-proven: deleting the check brings the bare numpy error straight back.

Suite 451 passed / 1 skipped / 39 xfailed, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(wire): the review found the rewrite's core was untested, and four false claims

Five reviewers on the assembly change. The memory result held — one reproduced it
independently at 3.084x -> 1.158x against my 3.06x -> 1.13x, and separately confirmed
that `del shard_bytes` really does free the bytes (np.load on a BytesIO copies;
OWNDATA=True, base=None), that ordering is equivalent, and that array flags match
np.concatenate exactly. What did not hold was the evidence I claimed for it.

NOTHING IN THE SUITE ASSEMBLED MORE THAN ONE SHARD. I wrote that
tests/test_wire_fixture.py passing unchanged proved the product had not changed. That
file does not reference the assembly at all — zero mentions of frames_for_target,
TargetLease or track_a_source. The real end-to-end proof is test_hop_b_sink_e2e.py, and
its fixture has ONE shard, so `position * expected_cell_count` never ran above zero. The
interleaving guard could not have covered it either: its three shards are byte-identical
copies, so any ordering bug survives it.

So the core of the rewrite shipped unverified. There is now a test that assembles three
shards with distinct values, months and units and asserts equality with np.concatenate in
manifest order. Mutation-proven three ways: reversing the slot index, an off-by-one in
stop, and leaving the identifiers unwritten each fail it.

A STORE FAULT WAS BEING BLAMED ON THE MANIFEST. frames_for_target reads a KeyError from
the fetch callback as "this shard was never pinned", and my lambda put
`self.store.download(...)` inside the same try. A KeyError from anywhere in the client —
a response indexed with [] rather than .get(), which is exactly how C-99 happened one
layer down — would have surfaced as "manifest lists shard X but its bytes were not
provided", with `from None` discarding the traceback that said otherwise. Verified by
mutation: the old lambda produces that false diagnosis verbatim. The lookup and the
download are now separate, and a store KeyError raises a named SourceSelectionError
chained to its cause.

expected_cell_count SIZES AN ARRAY NOW. It used to sit on one side of a `!=`, where 6.0
compares equal to 6 and passed. It now reaches np.empty and raises a bare "TypeError:
'float' object cannot be interpreted as an integer", naming neither the field nor the
manifest — from a document that crossed a repository boundary and whose read side only
checks that the key is present. Guarded, with bool and non-positive covered.

FOUR FALSE CLAIMS IN MY OWN PROSE.
- "the residual is 2/n_shards": fits none of its own numbers. 2/12 is 0.17 against a
  measured 0.36. Re-measured at 12, 24 and 36 shards: the overhead is CONSTANT at about
  4.5 shard-widths (~70 MB), so the ratio falls as 1/n — 1.36x, 1.19x, 1.13x.
- "ADR-013 §5's ~1000 samples per cell": that line is under §0. §5 is the GAUL sidecar.
- the test_wire_fixture citation, above.
- a docstring quoting the numpy error as "into shape (12,4)" when the failing assignment
  touches a (6,4) slice.

THE HISTORICAL FRAME IS FILED, NOT GLOSSED. #269 lists "the historical frame is released,
or not held" as an acceptance criterion; this change does not meet it. It is now #273,
carrying the measurement (108 MB, ~1.2% of a target frame) so nobody picks it up thinking
it is comparable. Closing #269 with that quietly unmet was the alternative.

Suite 457 passed / 1 skipped / 39 xfailed, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as wrong (#274)

* test: #265 — a proof that drove a copy of its subject, and a boundary that was wrong

C-89's two orphaned deferrals, both discharged.

THE PROOF DROVE A COPY. test_the_drift_check_would_catch_a_rename rebuilt the gated
check's comparison with its own comprehensions instead of calling it, so blanking the
real check's assertions left the proof green — a proof of a reimplementation. Its two
siblings had already been repaired the same way via _describe_changes, which makes this
a second incident rather than a guessed abstraction.

The comparison is now _name_and_class_drift, called by both. Mutation-proven: making it
return ([], {}) fails the proof for both partners, where before it passed. It also gained
an assertion that the report names BOTH sides of a mismatch — what this package expects
and what the registry declares — because a reader who cannot tell which one moved cannot
act on it.

THE RELOCATION IS ANSWERED "no", with the reason recorded rather than deferred a third
time. The coordinate-value scan STAYS in test_env_declaration.py. Its subject is
_EXPECTED_NAMES, derived from _PARTNER_ENV — the declaration of what each partner reads,
which is the substance of that module. Moving a guard away from the declaration it guards
so a filename reads better trades a real coupling for a filing convenience, and would
mean exporting a private name from one test module into another.

What was actually misfiled moved instead. registry_at / registry_current / rows are the
shared READER, not this package's environment declarations, and their refusal tests plus
_scratch_repo are now tests/test_seam_registry.py — 115 lines out, no shared private
state left behind. test_env_declaration.py is 1406 lines against 1503 before; the split
removed more than that and the shared comparison put some back.

Same 457 tests before and after the move, which is the point: a file split that changes
the count has moved something it should not have.

Suite 457 passed / 1 skipped / 39 xfailed, ruff clean.

Closes #265.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: two of my own claims in this change were wrong

Focused verification of a test-only change — lighter than the full multi-agent gate the
last two stories got, because nothing under views_postprocessing/ moved, the test count
is identical before and after, and both new behaviours are mutation-proven. Saying which
gate ran, rather than implying the same one.

It still found two false claims, both mine, both the measure-don't-recall rule.

"115 LINES OUT" was invented. Measured: the moved block is 109 lines of test code, and
test_env_declaration.py went 1503 -> 1406, a net 97, because the shared comparison put
some back. No command had produced 115.

"ITS TWO SIBLINGS HAD ALREADY BEEN REPAIRED THE SAME WAY VIA _describe_changes" is not
what happened. Reading the sibling's own docstring: the rotation proof CALLING
_describe_changes was part of the defect, not the repair — it drives "the helper
underneath" rather than the check. The actual repair was a new test driving the real
check under monkeypatch.

That matters because it was load-bearing: I used it as the WET-before-DRY licence for
extracting a shared function, and a precedent that does not exist cannot license
anything. The extraction is still right, for a reason now stated honestly — the
comparison is a pure function of two dicts and both callers want exactly it, so sharing
it is the same guarantee with less machinery than monkeypatching a gated check. What
justifies it is that this is the second time the pattern has bitten, not a rule about
line counts.

Also verified and true: the new module imports nothing private from the old one, and
nothing anywhere else referenced the moved tests by name.

Suite 457 passed / 1 skipped / 39 xfailed, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…275)

* release: 1.1.1, and make the guard that protects the release order live in CI

THE VERSION. 1.1.1, a patch, measured rather than assumed. The only external imports of
this package anywhere in the org are UNFAOPostProcessorManager and
CRAFDPostProcessorManager, from views-models' un_fao/main.py and un_crafd/main.py, and
both paths are unchanged. Nothing outside this repo imports track_a_source,
frames_for_target, store_port or _ContractStorePort — so #269's frames_for_target
signature change breaks no consumer. No new capability; two bug fixes and an internal
refactor.

It is earned this time: three merged stories changed code under views_postprocessing/ for
the first time since 1.1.0. On 2026-08-13 the trees were byte-identical and no tag was
cut, which is why that release did not happen.

The version still has exactly one home, pyproject.toml:3 — no __version__, no CHANGELOG.
Re-checked rather than remembered, because C-80 and C-82 were both a version declared
twice with a guard on one copy.

THE GUARD WAS INERT WHERE IT MATTERED. tests/test_release_version.py exists because tag
1.1.0 was cut at main while pyproject.toml still said 1.0.0. But run_pytest.yml checks out
with actions/checkout@v3 and no fetch-depth, so CI fetches NO TAGS and both of its tests
skip there. It has only ever run on a laptop — a guard against mis-cutting a release that
is absent from the one place a release is verified.

Demonstrated, not asserted. A shallow --no-tags clone (CI's default shape) skips both with
"no release tags in this checkout (a shallow clone, or none cut yet)". A full clone runs
them, and setting pyproject back to 1.0.0 against the 1.1.0 tag fails with "the newest
release tag is 1.1.0 but pyproject.toml declares 1.0.0" — the original defect, caught.

fetch-depth: 0 is for the tags, not the history; the comment says so, because the next
person to see a full fetch in a small repo's CI will reasonably wonder.

Suite 457 passed / 1 skipped / 39 xfailed, ruff clean. The release guard locally: the
newest-tag check passes (1.1.0 <= 1.1.1), the tagged-commit check skips because HEAD is
not tagged yet — which is the correct state before S3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(release-guard): scope it to this line of history, and two false claims

Review of the release prep. Making the guard live in CI was right; it would have made
it fire on branches that had done nothing wrong.

THE GUARD ASKED THE WRONG QUESTION. `git tag -l` lists tags on every branch, so the check
was "has a release been cut anywhere", not "has one been cut from THIS line of history
without the bump". Invisible while CI fetched no tags; a false alarm the moment it
started. Reproduced in a clone: with 1.1.1 tagged on the release line, a branch still
declaring 1.1.0 — an honest feature branch, or a hotfix cut from 1.1.0 — fails with "the
newest release tag is 1.1.1 but pyproject.toml declares 1.1.0". It has done nothing
wrong; the tag simply is not on its history.

`git tag --merged HEAD` asks the right question. Same clone, same branch: tags anywhere
are 1.0.0 1.1.0 1.1.1, tags reachable are 1.0.0 1.1.0, and it passes. On the tagged line
it still bites both ways — setting pyproject back to 1.1.0 with 1.1.1 reachable fails
both guards with their own messages.

ADR-014 §3 prefers a false negative to a false alarm. I was about to activate a guard
whose first act would have been to redden unrelated work, and a guard that does that is
one someone deletes.

"C-80 AND C-82 WERE BOTH A VERSION DECLARED TWICE WITH A GUARD ON ONE COPY" IS FALSE.
C-80 is the doc-accuracy scan exempting ADRs and CICs; C-82 is governance prose carrying
numbers nothing checks — the same class as this, a number with no guard, but about the
register and CIC front matter, not a version declared twice. The claim originated in this
test's own docstring on 2026-08-13 and I repeated it into a release PR without reading
either entry. Corrected at the source, with a note saying so, since the docstring is
where the next person will read it.

"THREE MERGED STORIES CHANGED CODE UNDER views_postprocessing/" IS FALSE. Two did —
#268 (ff0278b) and #269 (70f25f0). #265 (0d38a71) touched tests and the register only.
Three stories merged; two changed package code. The version conclusion is unaffected: a
patch was earned by those two.

Suite 458 passed / 1 skipped / 39 xfailed, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release-prep review's lesson, registered because this repo keeps rediscovering the
shape from new angles.

tests/test_release_version.py was written on 2026-08-13, correct and mutation-proven, and
had never executed anywhere except a laptop — CI fetched no tags, so both its tests
skipped. Confirmed in a real CI log: run 31844578627 shows "test_release_version.py ss".

Switching it on would have broken honest branches on its first day. It read `git tag -l`,
which lists tags on every branch, so it asked "has a release been cut anywhere" rather
than "has one been cut from this line of history without the bump". With 1.1.1 tagged on
the release line, a branch still declaring 1.1.0 fails, having done nothing wrong.

Mutation-proving establishes that a guard CAN fail. It says nothing about whether the
guard has ever been asked. This one's logic was fine in the only environment it had run
in, and its defect lived entirely in the environment it had never seen. ADR-014 §2 wants
a guard mutation-proven; this adds that a guard which has only ever run in one place is
proven in one place. C-98 is the same family from the other side — a guard that ran, but
watched a proxy.

Register 102 total / 22 open / 80 resolved. Suite 457 passed, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Polichinel
Polichinel merged commit 3be7561 into main Aug 15, 2026
5 of 6 checks passed
Sign up for free to 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