Skip to content

release: 1.4.0 to main - #317

Merged
Polichinel merged 10 commits into
mainfrom
development
Sep 29, 2026
Merged

Polichinel merged 10 commits into
mainfrom
development

Conversation

@Polichinel

Copy link
Copy Markdown
Collaborator

Promotes development to main for 1.4.0.

Fixes the defect behind today's unservable UN-FAO delivery: the findability guard checked 2 of 110 artefacts and could not see the sidecar by construction. It now verifies every uploaded object by the consumer's own resolution shape, order-independently.

A previously-passing delivery can now stop. Four refusal situations instead of two, no new exception types. CHANGELOG.md leads with that.

Reviewed by the views-models seat (non-hosting, approved), plus /code-review medium and /review-diff. Between them they caught three defects in the fix — the lookup key, a polarity bug that would have condemned a healthy delivery, and an ordering assumption that does not exist. All addressed, all mutation-proven.

This tag is the delivery mechanism, not housekeeping: the launcher installs from it.

526 passing, ruff clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ANY1CCy9Xo7zjMY4XJ69v9

Polichinel and others added 10 commits September 29, 2026 21:14
…y category (#312)

The first live UN-FAO delivery, 2026-09-29, uploaded 109 of 110 objects and
reported success. views-faoapi refused it. The missing object was the §5 GAUL
sidecar: its bytes matched the previous run's, the content-addressed store
correctly declined a second copy, update_document ran against the OLD file, and
_ContractStorePort.upload returned a real file id for the wrong document. The
consumer resolves by filename, found nothing, refused.

The C-94 guard did not catch it, and could not have. It checked two entries,
{"forecast": manifest_id, "historical": hist_id}, via
latest_file_id({"name": ..., "category": category}). Every wire object is
uploaded with category="forecast" (sink.py:227), so the newest forecast document
is always the manifest, uploaded last by design. Adding the sidecar to that dict
would have resolved the manifest and passed. The lookup key was wrong, not the
dict.

So the guard now asks two different questions, because neither implies the other:
  legs    — does the consumer's SELECTION land on this run? (unchanged)
  objects — does each artefact resolve by its own filename AT ALL? (new)

Per object rather than by count, and the decisive reason is not the one I started
with. Pooling upstream is deterministic — measured in views-models on 2026-09-29,
25 of 25 anchor cells byte-identical across an accidental re-pool — and
naming.py embeds the run id in every filename. A re-run therefore writes NEW
names over IDENTICAL bytes, so the dedup path that took the sidecar takes all 110
objects at once: the store creates the documents, the count is right, nothing is
servable. A count-plus-spot-check passes that. Re-running is what C-105 and C-22
tell an operator to do after a torn run, which makes it the realistic case rather
than the exotic one — our own remedy is the trigger for the worst version.

The query loop moved OUT of both partner managers into delivery/findability.py.
Not incidental: the partner packages are under a ratcheting line budget whose own
comment says the response to it binding is to move code out of the package rather
than raise the number, and crafd/ sat at 699 of 700. Delegating paid for the wider
call site exactly — both partners are unchanged at 690 and 699. It also leaves one
copy of the rule instead of two that can disagree (the C-75 shape).

An unqueryable filename degrades to UNVERIFIED, never to "invisible". filename is
a declared collection attribute (pipeline-core provisioning.py:101), but if a
store cannot be queried on it the honest answer is "could not ask" — a guard that
manufactured an outage on every delivery would be deleted within a week
(ADR-014 §3).

Five mutations, each caught: report only the first missing object; accept any id
as evidence; pass only the two legs (the shipped bug); stop exporting the sink's
ledger; swallow a failed query as not-found.

Complementary to views-pipeline-core's separate fix for the upload that reports
success having written nothing. Neither waits on the other: theirs stops the lie
at the source, this one stops us believing a lie from any source.

Noted, not acted on: this is the first bug requiring an identical hand-patch in
both partner managers, which is one of the two triggers C-33 names for extracting
the partner seam. Recorded on #312; extracting it here would be a different change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANY1CCy9Xo7zjMY4XJ69v9
…lookup (#312 review)

Review finding from the views-models seat, verified here before acting. The
per-object check queried `filename` directly. Three facts say it would not work:

  1. `Query.equal("filename", ...)` appears NOWHERE on the platform. The hits in
     pipeline-core, faoapi and crafdapi are `Query.equal("name", filename)`
     against the storage BUCKET — a different call on a different object.
  2. `filename` is declared in pipeline-core's collection schema
     (provisioning.py:101) and **no index is declared anywhere in that file**.
  3. views-faoapi — the consumer being modelled — deliberately does not query it.
     `resolve_artifact_file_ids` (prediction/manager.py:277-295) runs a
     type-scoped query on {category, type} and matches `filename` in Python.
     Its docstring says so, and that shape ran against the real store during the
     2026-09-29 incident.

Left as written, the guard would return UNVERIFIED on every delivery forever
while its presence read as coverage — a guard that structurally cannot fire
(views-models C-155), which is worse than the bug it replaces. The failure
direction was right and the query was wrong.

So the check now uses faoapi's shape: scope to {name, category, type}, match
filename in Python. Better on its own merits, index question aside — it asks the
question the consumer actually asks, which is C-94's thesis rather than an
approximation, and it costs one query per artefact TYPE rather than one per
object (110 -> 3).

Carrying it needed the sink's ledger to record doc_type, and the port to expose
`documents()` alongside `latest_file_id`.

Both partner packages came in UNDER where they started: unfao 690 -> 688,
crafd 699 -> 697. The guard's docstring and success log moved into
`findability.verify` with the rule they describe — one home per fact, and the
budget's own instruction is to move code out of the package rather than raise
the number. Moving `store_port.py` out was considered and rejected: it is
byte-identical in both partners and would free 111 lines each, but it is
referenced by six register entries, an anti-drift test and ADR-015, and that
blast radius does not belong inside an incident fix.

Six mutations, each caught: revert to a filename query; accept any id as
evidence; report only the first missing object; pass no objects (the shipped
bug); swallow a failed query as not-found; stop exporting the ledger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANY1CCy9Xo7zjMY4XJ69v9
…oved

Three findings from /review-diff on this branch, two of them self-inflicted by
the rework that answered the review.

1. WARNING — findability.py said "asked for that exact filename", "Why by
   filename and not by id", and "NOT FOUND by filename". After the rework the
   store is NOT queried on filename; it is a type-scoped query matched in
   Python, precisely because that attribute has no index. A reader would have
   concluded the opposite, and anyone simplifying would have restored the broken
   form. The test guards the filters; the prose was inviting the change.

2. WARNING — the CIC still said "After both legs are uploaded" and "Two distinct
   refusals". Both wrong: every artefact is checked now, and there are four
   refusals across two questions. Updated, with why the per-object half resolves
   the way faoapi does rather than by filename. Review date moved.

3. SUGGESTION — `objects` carried no annotation while `legs: dict` sat beside
   it. Annotated in verify and in both managers.

Checked and NOT findings, recorded so they are not re-raised: blank-line
hygiene is correct, `logger` remains used in both managers, and
`assert_findable` is still reachable through the legs loop.

Both partner packages remain under budget at 690 and 699.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANY1CCy9Xo7zjMY4XJ69v9
…(/code-review)

Six findings from /code-review medium on PR #314. It also traced the new query
down through pipeline-core to Appwrite and confirmed the mechanism holds:
the scope set matches faoapi's query exactly, newest-first is guaranteed by the
$createdAt sort so first-wins reproduces the consumer's selection rather than
approximating it, pagination is handled upstream (MAX_METADATA_PAGES 1000 x 100)
so 110 objects are not truncated, and pipeline-core's own comment at file.py:1216
independently corroborates that `filename` carries no index.

MEDIUM 1 — a polarity bug, and the worst kind. The parse sat OUTSIDE the try. If
the store returned objects rather than dicts, or renamed its keys, `doc.get`
raised, `seen` stayed empty, and EVERY object was reported not found — firing the
full "NOTHING in this run is servable" refusal on a healthy delivery. A failure of
the check must never quarantine the delivery; this module argues that twice, in
C-99's and C-103's words, and then did the opposite. The parse is now inside the
try. Mutation-proven: moving it back out fails the new test.

MEDIUM 2 — silent fallback. `summary.get("uploaded_objects", [])` would have let
the guard quietly shrink to the single historical object and log "preflight
passed" while 108 shards, the sidecar and the manifest went unasked — the precise
shape of the bug being fixed. The key is guaranteed on every path that reaches
the guard (the interlock-closed path returns before it), so its absence is a
defect and now reads as one (ADR-003).

LOW 3 — `documents()` had no test anywhere. It is the only method the per-object
check depends on, and the findability tests inject callables, so a pipeline-core
rename would have surfaced during a live delivery. Now asserted at the port,
which is the seam whose job is absorbing exactly that.

LOW 4 — the port documented itself as four-method; `documents` makes it five. A
datastore satisfying the documented contract would have failed with AttributeError
mid-delivery.

Also fixed, reported as not-a-finding: `unverified()` hardcoded the word "leg",
so a scope rendered as "'forecast/sampled_forecast_shard objects' leg". It now
takes the phrase. And the module docstring claimed "no store types" while verify
reads two document KEYS — the boundary is now stated honestly rather than
overclaimed.

LOW 5 (SEARCH_INCOMPLETE noise growing ~110 documents per run) and LOW 6 (the
budget-driven inline dict at the call site) are recorded on #312 rather than
changed here: 5 needs a C-94 note and no code, and 6 costs lines neither partner
package has.

522 passing. Both packages unchanged at 690 and 699.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANY1CCy9Xo7zjMY4XJ69v9
…#312 re-review)

The views-models seat could not find the `$createdAt` sort I had twice asserted
as fact. They were right and I was wrong, and the way I was wrong is the worse
part: an automated review asserted it, I relayed it to them twice without
checking, and it became a rationale I was about to put in the code.

Verified here. `search_files_by_metadata` builds `queries = []` and appends only
`Query.equal(attribute, value)` per filter, then `limit` + `offset`
(pipeline-core `modules/appwrite/file.py:1045-1050`). No `order_desc`, no
`order_asc`, no sort field. The only ordering calls in that module are at
`2643-2645`, inside `list_files` — a STORAGE listing, not a collection query —
where `order_field` defaults to None.

So document order is unspecified and "first match wins" was a coin flip dressed
as a tie-break.

Rather than record the caveat, the assumption is gone. `seen` now maps filename
to the SET of ids carried under it, and an object is findable when the id THIS
run uploaded is among them. That is the question we actually have, and it needs
no ordering guarantee at all. The refusal now shows every id the store holds
under a name instead of one, which is strictly more useful for an operator
auditing a bucket.

Two tests: the same delivery passes under three different document orders
including a stale same-filename document returned first, and order-independence
does not become permissiveness — a name carrying only other runs' ids is still
refused. Mutation-proven by restoring first-wins, which fails both.

NOT fixed here, because they are not ours, and both are now on #312: pipeline-core's
`get_latest_file_id` DOCUMENTS "the newest matching file based on creation
timestamp" and implements `files_list[0]` over that unsorted result — a stated
guarantee with no mechanism, and the C-94 leg check has depended on it since
August. views-faoapi's `resolve_artifact_file_ids` carries the same "Newest-first"
assumption in its docstring.

524 passing. Both partner packages unchanged at 690 and 699.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANY1CCy9Xo7zjMY4XJ69v9
…fact-by-name

fix(delivery): verify every uploaded artefact by name, not two legs by category (#312)
…ents() calls

Follow-up to #312. /code-review's finding 4 said the port documented itself as
four-method when `documents` made it five. I fixed the module header and missed
the thing that mattered: the CLASS docstring also enumerates the datastore methods
a caller must supply, and `get_predictions_by_metadata` was absent from it.

Measured rather than reasoned about — a double built to the documented contract:

    class DocumentedContract:
        def get_latest_file_id(self, filters): ...
        def get_file_metadata(self, fid): ...
        def download_prediction(self, fid): ...
        def upload_data(self, **kw): ...

    AttributeError: 'DocumentedContract' object has no attribute
    'get_predictions_by_metadata'

Mid-delivery, after the upload has happened — which is the one place this port
exists to prevent a surprise.

Found while verifying, for this repo rather than taking it on trust, that a
pipeline-core fix to `get_latest_file_id`'s missing sort would actually reach us.
It does: this repo vendors no copy of the Appwrite client, calls
`search_files_by_metadata` nowhere, and the port takes any duck-typed datastore
with `views_pipeline_core.modules.datastore.DatastoreModule` passed in production.
So the ordering fix reaches us with a pin bump and no change here. faoapi and
crafdapi each carry their own copy of that module and are not covered.

The guard is derived from the source, not a hardcoded list: it collects every
`self._dsm.<method>(` the port calls and asserts each appears in the module or
class docstring. Prose cannot drift from the code without failing, which is how
this one rotted. Mutation-proven by removing the fifth name, which fails both
partners. ADR-014 §2 — the guard also asserts it found calls at all, so a
refactor that renames `_dsm` cannot leave it silently scanning nothing.

526 passing. Both partner packages unchanged at 690 and 699.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANY1CCy9Xo7zjMY4XJ69v9
…tract

fix(port): the documented datastore contract omitted the method documents() calls
…s tagged

Bumps 1.3.0 -> 1.4.0. MINOR rather than PATCH by the same reasoning as 1.2.0: a
previously-passing delivery can now stop. The guard refuses in four situations
instead of two. That is a behaviour change a launcher must be told about, not a
patch.

Cut now rather than at convenience because the launcher installs from a git TAG
(views-models tools/launcher/postprocessor.sh:57), not from PyPI and not from
poetry.lock. Until this tag exists, the fix for today's unservable delivery is
merged, reviewed, tested — and reachable by nothing. 1.2.0 sat unpinned for three
weeks for exactly this reason.

The changelog entry leads with the consequence for someone still on an older pin:
a delivery can complete successfully and be unservable. Then what changed, then
why taking it promptly is worth it — the deterministic-re-run case, where the
dedup path that took one object takes all 110 and the documented remedy for a
torn run is what triggers it.

Recorded there as known-and-not-ours: pipeline-core's get_latest_file_id
documents "newest by creation timestamp" over an unsorted result, which the
SELECTION half of the guard has relied on since August. This release removes the
equivalent assumption from the per-object half. The upstream half is filed there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANY1CCy9Xo7zjMY4XJ69v9
release: 1.4.0 — the findability fix, which is unreachable until it is tagged
@Polichinel
Polichinel merged commit 8db8c9f into main Sep 29, 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