Release 1.1.1 — two delivery-path fixes from views-crafdapi, and the governance pass behind them - #276
Merged
Conversation
…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>
Closed
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 2 of Bump → Merge → Tag → Release → Verify. This merge makes
maincarryversion = "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_crafddelivery attempt, both in code the FAO leg shares:#268 —
_ContractStorePort.downloadfailed open. It chained.get()onto an unvalidated store result, so a result whosedatawas present-and-null raisedAttributeErrorthree frames away, naming neither thefile_idnor 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.concatenatecopy. 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
KeyErrorfrom 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_shardsformula 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
UNFAOPostProcessorManagerandCRAFDPostProcessorManager, whose paths are unchanged; nothing outside this repo importstrack_a_source,frames_for_target,store_portor_ContractStorePort. Consumers pin by git tag in shell launchers, not by a semver range. No new capability.mainis currently 6 commits past tag1.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
ruffclean; 457 passed, 1 skipped, 39 xfailed locally and in CI.protect_mainrequires thetestcheck with no bypass actors, so this cannot merge red.After merge: tag
1.1.1on the merge commit, then the GitHub Release. Refs #125.