Skip to content

Sync main: epic #181, the CRAF'd producer, and the guards that carry them - #215

Merged
Polichinel merged 25 commits into
mainfrom
development
Aug 3, 2026
Merged

Sync main: epic #181, the CRAF'd producer, and the guards that carry them#215
Polichinel merged 25 commits into
mainfrom
development

Conversation

@Polichinel

Copy link
Copy Markdown
Collaborator

Brings main current with 25 commits: epic #181 ("Every claim checkable"), the CRAF'd producer package, and three review iterations of pre-flight corrections.

⚠ Read this first: merging deploys

views-models/postprocessors/un_fao/run.sh:61,68 installs this repository with:

pip install git+https://.../views-postprocessing.git@main

Merging is the release. The next FAO run picks up whatever main points at. views-models#294 already carries the scar: on 2026-07-31 @main was 208 commits behind, carried no wire modules, and would have run green while delivering the legacy artifact.

This repo's own CLONING.md warns "do not pin an installer to @main without checking what is on it." Its only consumer does exactly that, to it. Pinning that launcher to a tag is the fix, and it is a two-repo change — flagged, not made.

What lands

Epic #181 — eleven stories. Entry validators log before they raise; the register's closing conditions are machine-checkable; the coordinate registry has a drift detector; the lookup declares its own version; the þing-01 redaction guard was repaired; the _validate replica was retired and the gate tested where it fires; living docs no longer describe deleted code. Closed with ADR-014, for the rules no test carries.

The CRAF'd producer (views_postprocessing/crafd/) — a second partner package, upload interlock closed (UPLOAD_ENABLED = False). Its arrival showed that eight guards were scoped to one partner by name, so it landed exempt from all of them; that is fixed and the partner list is now declared once and checked against the filesystem.

Pre-flight (#214) — the runbook told operators there is no rollback when views-faoapi ships a deployed quarantine; a run has two selection entry points and withdrawing one leaves the other serving; README's delivered-schema table had the normative column order reversed and the wrong dtypes; a CIC named a collaborator its class has never called.

Delivery safety — verified, not assumed

check result
ADR-013 §10 golden fixture bytes untouched; sha256sum -c SHA256SUMS all 5 OK; contract_version still 1.5
data/gaul_lookup.parquet metadata-only change — all 10 columns byte-identical across all 64,742 rows; one added lookup_version key
contract/enrichment.py rewrite (pandas → numpy/pyarrow) all delivered values identical across a 5,003-row comparison of both revisions
Cross-repo pins 90fc105 confirmed an ancestor of views-appwrite's main via the API — the #196 failure mode does not recur
Dependency change one: views-frames >=1.0,<2>=1.10.2,<2. poetry check --lock clean. pyarrow and pipeline-core pins byte-identical to main
C-44 (pipeline-core 3.0.0 must not land yet) satisfied — the pin is <3.0.0 and the lock resolves 2.3.0
C-72 (pyarrow CVE) carried forward unchanged, not increased; the pin is the same on both branches
Publish side-effects none. publish_package.yml fires on release: published only. No tag, no deploy, no notification

Known and registered, not fixed here

  • C-81 — CI verifies 16 fewer tests than a full local run; the registry-drift detectors need a token for two private repos. And main's protect_main ruleset has an empty ref list, so a red CI would not block a merge. Both are operator decisions.
  • C-80 — ADRs and CICs are exempt from the doc-accuracy scan. That exemption is why most of the pre-flight findings existed.
  • C-77 — the historical leg names its document from the model path rather than the declared consumer name, for both partners. views-models/postprocessors/ has no un_crafd directory yet, so CRAF'd's has never been resolved.
  • Pre-Release Note 07 Topic B.2 tells FAO withdrawal is "not available today". It is available. The decision is real; the cost quoted is not.

Verification

402 collected
362 passed / 40 xfailed          local, all siblings present
348 passed / 16 skipped / 38 xf  CI (confirmed on #214)
ruff check .  ->  All checks passed!

Register 82/22/60, integrity-guarded. Every new or changed guard mutation-proven.

Polichinel and others added 25 commits August 1, 2026 19:44
…e guard cannot catch (#179)

Two entries sat under Open with their defects already fixed:

- C-63 (the silent config fork) was fixed by #149 on 2026-07-31 — the manager
  carries zero wire_contract branches and two launch_config asserts — but the
  entry never moved.
- C-47 (the stale reconciliation/ bytecode) was fixed by #177 hours ago.

Worth naming precisely: test_register_integrity.py did NOT catch either, and
correctly so. It asserts that no entry whose HEADING says RESOLVED sits under
Open. Neither heading said so — the code was fixed and the record was not. The
guard verifies internal consistency; only reading the tree catches this.

C-62's trigger had already fired. It read "when cutting this repo's first release
(#125)" — 1.0.0 was tagged on 2026-08-01 and the dependency drag shipped with it.
Rewritten to the half that is still ahead: the 3.0.0 bump, plus a re-check before
any PyPI publish, which is the point the footprint reaches someone else.

Cluster M added: five open concerns, one upstream publish. C-44, C-62, C-72,
C-73 and C-07 all resolve on the pipeline-core 3.0.0 pin bump and none can be
taken before it. They had been readable as five separate backlog items; they are
one blocked action. What this repo owes at the bump is a single verification —
confirm the delivery selects the run it expects — recorded in C-73's trigger.

Register: 73 concerns, 24 open, 49 resolved. Integrity guard green.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ve C-58 into Cluster M (#180)

Answering "are the things in other repos tracked by gh issues there?" — mostly
yes, two were not, and one turned out to be already fixed upstream.

ALREADY TRACKED, verified rather than assumed:
  C-13, C-28  timeouts        -> pipeline-core #248, #347
  C-73        stale run       -> pipeline-core #341 (closed)
  C-72        pyarrow CVE     -> vpp #174, faoapi #348, comment on #280
  C-62, C-44  dependency drag -> comment on pipeline-core #319
  C-43        area math       -> views-datafactory #387

GAPS, now filed:
  C-26 (Tier 1) -> pipeline-core #366. Our only Tier 1, open since 2026-06-12
       with no home in the repo that owns the code. Filed carrying the question
       this seat cannot answer: does get_feature_frame inherit the unconditional
       fillna(0.0), or does the frame path propagate NaN? That decides whether
       C-26 is live — run-0 shipped 28.4M historical rows through the frame path
       — or historical. Entry stays Tier 1 until answered, not downgraded on a
       guess.
  C-27 -> pipeline-core #367, cross-referenced to their #168 as the same defect
       class on a different call path.
  C-24 -> cross-referenced on faoapi #222 (their output-schema epic) asking
       directly whether the Topic-C rename is in scope, with an offer to close
       our entry pointing there if so. Open fourteen months without a home in
       the repo that owns the fix.

FOUND WHILE CHECKING: C-58's fix has already LANDED upstream. pipeline-core
#322 (write-path raise-by-default), #331 (relocate the four create_* sites) and
#332 (assert the delivery path does not import provisioning) are all closed —
that is þing-02's D5 implemented. But all three are on their 3.0.0, and we
resolve 2.3.0, so the defect is fixed upstream and LIVE here until the bump.
Same shape as C-73. C-58 joins Cluster M, which is now six entries — two of them
Tier 2 — resolving on one publish.

The integrity guard caught me writing "C-166" unnamespaced, which is my own
convention. Fixed before commit.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ver both entry validators (#193)

ADR-008:48 requires raised structural failures to be logged at ERROR; :51 that
"raising is not a substitute for logging." `assert_env_declared` raised
`EnvironmentError` naming every missing variable and wrote no log record, so an
operator reading logs after a refused run found nothing (register C-71).

The two-line fix matters less than why it was missing. `contract/launch_config.py`
was written in #149 by mirroring this module, inherited the flaw, was fixed in
review, and left the module it copied as the odd one out. The new test therefore
parametrises the ADR-008 obligation over BOTH validators rather than pinning one:
adding a third is one line, and the pair cannot drift again.

Deliberately NOT extracted into a shared helper. WET before DRY — they answer
different questions (did the launcher assemble the environment / did it declare
the delivery mode) against different sources (os.getenv / a passed dict). Named
trigger to revisit: a third entry-validation module. Recorded in epic #181.

Logging here is only safe because `missing` holds NAMES: membership is decided by
`os.getenv(name)` being falsy and the resolved value is never read. CONNECTION_ENV
carries the APPWRITE_DATASTORE_API_KEY secret slot, so a second test sets a
sentinel value and asserts it never reaches a record — a future "log the current
environment for debuggability" cannot land quietly.

Mutation-tested: removing the logger.error line fails 2 of the new tests.

Also registers C-74, found by this story's own review. tests/test_redaction_guard.py
pins the þing-01 #135 redaction audit over five module roots; #153 moved the
machinery from unfao/ to contract/ and four were never re-pointed. rglob on a
nonexistent directory yields nothing rather than raising, so the guard scans 6
files instead of ~16 and passes. Not a leak — the relocated modules are still
credential-blind — but C-57 and Cluster I both vouched for that guard in writing,
and both are corrected in place. Tracked as #192.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and make closing conditions machine-checkable (#194)

C-43, C-59 and C-61 were filed under Open with their own stated closing
conditions already met. C-43's body said it plainly — "C-43 closes when
tests/test_gaul_lookup_fidelity.py is committed and green" — and that file was
committed in #141 with 18 tests passing. C-59 and C-61 both recorded
"Mitigation — landed 2026-07-31" and named four guards that exist and pass.
Nothing was outstanding. The entries simply never moved.

All three moved to Resolved, each citing the test rather than the session, which
is what C-43 instructed. Its transcription-fidelity-vs-assignment-correctness
scope split is preserved verbatim: this closes faithful carriage of the
producer's answer, NOT whether that answer is right. The latter is
views-datafactory#387 and is not this repo's to close.

The guard this adds is the point of the story. test_register_integrity.py could
already catch a heading that SAYS resolved; it could not catch an entry saying
open while the thing it waited for had arrived — which turned out to be this
register's actual failure mode, six times in two days. Two new checks:

  - an Open entry naming a "closes when `tests/…`" artifact that exists
  - an Open entry recording "Mitigation — landed" without "Partial mitigation"

Mutation-tested against the real 2026-08-01 register: both fail, naming
'C-43 -> tests/test_gaul_lookup_fidelity.py' and ['C-59','C-61']. Restored, all
10 pass.

Deliberately narrow, and narrowed further in review. _ARTIFACT_ROOTS covers
tests/ and scripts/ only. For an executable artifact, existence is close to the
condition because the suite then runs it; for docs/ the two come apart entirely —
an ADR file has existed since July while the section an entry waits on may never
have been written. Including doc paths would fire on correctly-open entries, and
the guard would be deleted rather than fixed. That is the failure this check
exists to avoid, so it must not commit it.

Cluster K carries a closure note: one test file discharged three entries exactly
as its fix strategy predicted, and the cost was not the geography bug it was
opened for — it was that the engineering took one session and the record took two
more days. C-43's own residual said the forward-check needed attaching to
something the interpreter runs; the entries then made the identical error one
level up. A guarantee needs a check, including the register's own.

Header 25 -> 22 open, 49 -> 52 resolved. Cluster M heading corrected to six.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… ban the retired name (#195)

#158 renamed the cross-repo contract from PLATFORM-001 to "the Appwrite Seam
Contract" and was closed on 2026-08-01. Only the documentation half landed, and
the half that did land was applied as a blanket find-and-replace.

Four citations survived in .py. The one that mattered was inside the message
raised by appwrite_env.assert_env_declared — the text an operator reads when a
delivery refuses to launch, naming a retired contract in a repo they do not own,
on the day their run failed.

Worse, the find-and-replace rewrote the FILENAME INSIDE A URL. ADR-013 §7d
pointed at ".../platform/the Appwrite Seam Contract_identity_secrets_
configuration_contract.md" — a path containing spaces, which has never resolved.
The upstream file has since been renamed to appwrite_seam_contract.md
independently.

Both §7d links now point at views-appwrite b54928f rather than main. §7d claimed
"referenced by URL, never by copy", which was true in letter and false in effect
while an unpinned link could drift under it — and it did: the registry moved
1.3.0 to 1.4.0 on 2026-08-02, the same day. Recorded as Erratum E2 in the
post-adoption record; contract_version stays 1.5 and the wire is untouched.

The ban test is what stops this returning. Scoped to code — the package, the
tests, AND scripts/ — because docs/ADRs and reports/ legitimately name the old
contract when narrating what was decided under it. An ADR describing a 2026-07
decision correctly says what the thing was called in 2026-07; code speaks in the
present tense to whoever reads it now.

Review widened that scan from two roots to three. Two roots would have been
register C-74's defect committed again, ninety minutes after registering it: a
guard whose declared scope exceeds its actual scan, passing because the unscanned
part happens to be clean. Mutation-tested at both widths.

The ban also caught a parenthetical I had just written into appwrite_env.py's
docstring naming the retired name to explain its retirement. Removed; that
explanation belongs in the ADR, which now carries it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as an unmerged branch (#198)

S3 (#184/#195) pinned the Appwrite Seam Contract at a commit resolved with
`git -C ../views-appwrite rev-parse HEAD`. That checkout was sitting on the
unmerged `feat/s1-single-writer-rule` branch, so the pin captured a commit that
never reached `main` and has now been withdrawn (views-appwrite #30, #27). It
declared registry v1.4.0; main carries the ratified v1.3.0.

Verified before repinning, not taken on the issue's word: the withdrawn commit is
reachable only from that feature branch, 47172af is origin/main's tip, its
registry declares version = "1.3.0" (RATIFIED — þing-02), and both cited files
exist at it.

Four references repointed — appwrite_env.py's docstring and ADR-013 §7d's two
links — plus the version labels, the §7d correction paragraph, and Erratum E2,
all of which asserted a 1.3.0 -> 1.4.0 move that never landed.

The withdrawn sha is deliberately not repeated anywhere, including in the prose
narrating its withdrawal, so that grepping for it finds nothing. #196's
acceptance criteria are met literally.

The lesson is narrow and now recorded in both places a future contributor will
look: resolve a cross-repo pin from the tip of the other repo's main, never from
whatever its working copy has checked out. A pin is a claim about what the
contract says; HEAD answers a different question — what someone was working on.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ted; the ban list was never updated (#199)

Epic #148's story #154 ("correct ADR-012 and the manager CIC to describe what
the code is") was closed 2026-08-01 with ADR-012 done and the rest not. Ten live
falsehoods survived across four living onboarding documents.

The worst was views_postprocessing/unfao/managers/README.md, untouched by #154
and wrong in six places: it documented the Template Method as
`_transform → _append_metadata`, pointed readers at tests/test_append_metadata.py,
and claimed Appwrite settings have "no startup validation yet — tracked in #11"
when þing-01 #134 added fail-loud validation and #11 is about timeouts. It sits
inside the package, next to the code it misdescribed.

tests/test_doc_accuracy.py scans all four files. Its ban list carried only the
ADR-011 / C-39 symbols and nothing was added when #149 deleted a delivery path
and #153 moved the machinery out of unfao/. The guard was not wrong; it was not
updated. Extended 5 -> 19 entries, grouped by the PR that deleted each, with a
(?<!FAO_)PGMDataset lookbehind so views-faoapi's live class is spared.

Not every mention was a defect, and the difference mattered. role_and_seams.md
and the CIC were largely correct — they narrate retirements deliberately, and a
phase table with a gap is worse than one that says where the work went. Those
lines got the repo's existing line-scoped `legacy-ok` marker rather than edits,
and a new test pins that the marker stays line-scoped: one that leaked to a whole
file would silently disable the guard it excuses.

Review caught two things in my own work, both worth recording. The failure
message had been reduced to a basename while three living docs are named
README.md, so a failure could not say which. And I wrote an unverified claim into
the CIC — that forecast shards carry provenance in the run manifest. They do not:
the manifest's keys are contract_version, run_id, targets, shards,
expected_months, expected_cell_count, sidecar, and it carries no lookup_version,
region or unmapped_count. Writing an unchecked claim into a contract document, in
the story about unchecked claims in documents, is exactly the habit this epic is
against. Corrected and verified against run_manifest.py by script.

Mutation-tested: reverting the ban list to its five pre-S11 entries fails two of
the new tests.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…te where it fires (#200)

tests/test_validation.py defined its own validate_dataframe() and ran 43
parametrised cases against it. Forty-three tests, zero production code — a closed
loop that could not have failed if the real gate were deleted outright.

Its header claimed "the logic tested matches unfao.py:_validate() exactly". False
since #149, which stopped _validate null-gating entirely; its docstring now reads
"Neither payload is null-gated here." It also carried REQUIRED_METADATA_COLS as a
nine-element literal — the hand copy of the contract that C-70 was resolved to
make single-source.

Replaced by 14 tests of contract/historical.assert_metadata_complete, the code
that actually gates a delivery, parametrised over the IMPORTED METADATA_COLS so a
column added to gaul_schema.COLUMNS is gated automatically. Plus source-scan pins
that the gate has not drifted back into _validate and that the manager still calls
it. Mutation-tested: narrowing the gate to a single column fails 9 of the 14. The
old suite passed that same mutation untouched.

The behaviour C-01 fixed is preserved in full; it is simply asserted against the
function that performs it.

C-03 closes. Residual 1 (the replica) is gone. Residual 2 (the enrich->validate
end-to-end path) is RELOCATED to #18 by number, per the convention that a
relocation is not complete until the destination exists and is cited. Every leg is
covered — enrichment, artifact build, reader parity, invariants on primitives, the
wire e2e, and now the null-gate. What remains is the manager ORCHESTRATING them,
which needs pipeline-core and a production-like Appwrite environment that þing-02
D2 forbids. That gap already had two trackers; a third here was noise.

Three `assert False` campaign probes retired because the findings they assert are
discharged: 3.2's "validation uses replica not real code", 4.3's "filter_cols
change not detected", and 4.3's "enrichment test checks only 5 of 9" — the last
verified before removal, since test_enrichment.py asserts
list(lookup.columns) == METADATA_COLS, exact equality on all nine, then iterates
the full set twice more. C-36's own resolution predicted this: "the pure-assert
False probes don't test the live condition, so a fixed finding won't auto-flip."
A discharged probe must be retired by hand or it permanently asserts a defect that
no longer exists. Each removal leaves a dated note; the modules stay, because the
campaign record is the point.

Review caught the story closing C-03 while the CIC still cited it as open, next to
a now-false "replica tests" — the exact drift class, in the contract document.
Corrected to name #18 and þing-02 D2.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er inferring it (#201)

* feat(lookup): S5 #186 — the lookup declares its own version instead of the consumer inferring it

gaul_lookup.version() reconstructed the delivery's traceability stamp by
traversing three levels of views-datafactory's ingestion-ledger shape
(source_provenance -> land_gaul_region -> content_digest) inside a bare
`except (ValueError, AttributeError): pass`, returning the string "unknown" when
any level was absent or reshaped.

A rename upstream would therefore have made every delivery untraceable, silently,
in the one field C-15 exists to answer AFTER a suspect delivery. Nothing would
have noticed: a provenance record was still written; it just stopped meaning
anything. That is the declare-don't-infer violation ADR-003 forbids, at the
consumer.

The builder now composes the stamp — it is the only thing holding both the region
and the producer's ledger — and writes it as one flat `lookup_version` key. The
consumer reads that key and RAISES when it is absent, logging first (ADR-008). The
json import and every exception handler are gone from the module.

An untraceable build fails at BUILD, not at delivery. _provenance is best-effort
by design; a missing ledger used to surface as a placeholder in production, and
now surfaces where a human is present to fix it.

Review caught the fix reintroducing its own defect class one level up. The first
draft read `land_gaul_region or gaul_admin_area_majority`, and since _provenance
does not take a region, land_gaul_region is present for EVERY build — so
`--region all` would have been stamped `all@f74d3b2b`, the land_gaul region
definition's digest, on a global artifact. Authoritative-looking and wrong,
silently. Replaced by a declared `stamp_dataset(region)`: area-majority for `all`,
`<region>_region` otherwise, and a region whose entry is absent is refused rather
than substituted. Verified: all -> all@272cdb01, land_gaul -> land_gaul@f74d3b2b,
africa_me_legacy -> refused.

The committed artifact is rebuilt. Proven metadata-only: 64,742 rows, column order
and dtypes unchanged, a content hash over every column's values IDENTICAL, and the
stamp resolves to land_gaul@f74d3b2b — the same string the old traversal produced,
so no delivered provenance changes meaning. tests/test_gaul_lookup_fidelity.py
(now 26) still compares it value-for-value against the seven datafactory parquets.
ADR-013 is untouched: contract_version 1.5, fixtures byte-identical, and the
sidecar cannot inherit lookup metadata because build_sidecar constructs a fresh
table.

Also from review: the ledger is append-only and the LAST entry per dataset wins,
which was harmless while provenance was decorative and is load-bearing now that
the stamp raises — stated in _provenance rather than left in the loop. Digest
length is declared so producer and consumer agree on what a valid stamp is. And
the GaulLookupEnricher CIC still documented the "unknown" degrade this removes.

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

* chore(register): S5 #186 — resolve C-60, and record that the fix reproduced its own defect

The entry closes citing the tests, with the sidecar-stamping residual deferred
behind a named trigger (the next ADR-013 version bump) rather than a vague later.

Kept visible in the resolution: the first draft of the fix reintroduced C-60's own
defect class — a silent fallback that would have stamped `--region all` with the
land_gaul region's digest. The reflex that produced the concern reappeared while
fixing the concern, and that is worth more to a future reader than a clean
narrative.

Cluster K now has one entry left: C-46.

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

---------

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

The Appwrite Seam Contract's registry is the authority for the 13 environment names
this package declares, and it is deliberately referenced, never copied — one owner,
no duplicated values. That was always right. What C-57 named was the gap it left:
nothing mechanical could tell you the two had diverged.

appwrite_env now declares the edition it was verified against, SEAM_CONTRACT_VERSION
and SEAM_CONTRACT_COMMIT. A version string and a sha are not coordinate values; what
is recorded is WHICH EDITION WAS READ, and that is precisely what makes drift
detectable. Bumping them is not bookkeeping — it asserts someone re-checked.

Four checks, each mutation-proven with bytecode disabled:

  names + declared class     rename a coordinate here      -> 4 failures
  registry [meta] version    pretend v1.2.0                -> 1 failure
  commit reachable from main pin the withdrawn b54928f     -> 1 failure
  no value copied into code  add a constant/dict/kwarg     -> 1 failure each

The reachability check exists because existence was not enough. #196: S3 pinned a
commit resolved with `rev-parse HEAD` on a checkout sitting on an unmerged branch.
The commit existed, both cited files existed at it, and every check anyone had
written passed — but it had never reached main, declared a version never ratified,
and was withdrawn. A pin is a claim about what the contract SAYS; only reachability
supports that claim.

The value-copy check took two wrong narrowings before the right one. A substring
scan flagged three false positives — file_metadata is a FUNCTION NAME here, and the
other two appear only in refusal labels and docstrings. Narrowing to assignments and
defaults then caught neither a dict value nor a keyword argument, and the keyword
argument is the shape this repo would actually produce: AppwriteConfig(bucket_id=...)
is how every store is configured. The right axis was exact equality on string
constants, not statement shape — it catches all three real shapes while the false
positives fall out on their own. When a guard cries wolf, check whether the matching
is wrong before assuming the scope is.

Review also caught the expectation map deriving class from an _API_KEY suffix — the
exact inference the registry forbids, inside the test enforcing the registry. Now
written out, with a completeness check so an unclassified name fails.

tests/conftest.py holds the sibling-repo resolution S7 (#188, C-46) will adopt for
views-datafactory: declared env var, conventional sibling path, never an absolute
path to one machine, and a skip that names what to set so the test is runnable
rather than merely skippable.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Cluster K closes (#203)

Four sites resolved the producer's checkout four different ways, and one of them
was an absolute path to a single developer's home directory. The repo's only
cross-repo release gate had therefore never run anywhere but that laptop — which
is what register C-46 was open for, and why "it is skipped in CI" understated it.

All four now resolve the same way: $VIEWS_DATAFACTORY, else the conventional
directory beside this repo, never an absolute path to a particular machine. Every
skip names the variable AND the path, so a contributor can run the test rather
than watch it skip.

A fourth site turned up while doing the work — tests/test_delivery_coverage.py's
drift tripwire had its own parents[2] walk. The issue listed three. Leaving it
would have made a closure titled "one declared way" false.

The builder keeps its own resolver, deliberately and now argued in writing. A
script must not import from tests/ — that is the dependency direction backwards —
and the contracts genuinely differ: the script returns a Path even when the
checkout is absent so main can raise naming both the flag and the variable, while
the test helper returns None because a missing sibling is a normal skip. WET
before DRY. What is guarded is not the duplication but the property that matters:
that the two AGREE. A builder writing from one checkout while the tests verify
against another would report success on a lookup compared to a producer it was
not built from.

Review sharpened that guard. It first skipped when no checkout was present — which
is exactly when the fallback path is exercised, so the one case it existed for was
the case it declined to check, and it declined in CI where it matters most. The
fallback paths are now compared unconditionally; mutation-proven by pointing the
builder at a different directory.

Review also caught this change contradicting itself in the register: Cluster K's
resolution-scope line still called C-60 and C-46 "partial" eight lines above the
note closing them, and C-46's body still asserted a [backlog] state that ended on
resolution. Both fixed.

Cluster K closes: five entries, all full. Its own note records what the strategy
did not anticipate — that a cluster about "the artifact is trusted but unverified"
had a verification apparatus that was itself partly unverifiable.

The CI question is recorded as a residual with a recommendation and a named
trigger rather than implemented, because the issue put it out of scope: do not
couple this repo's per-PR CI to another repo's default branch; if wanted, a weekly
scheduled check that opens an issue on divergence.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… that vanishes now fails (#204)

tests/test_redaction_guard.py pins the #135 delivery-log redaction audit over five
module trees. Epic #148's #153 moved the machinery from unfao/ to contract/ and
four of the five roots were never re-pointed. The guard kept passing while
scanning 6 files instead of 17.

Re-pointing fixes today. The assertion that every declared root EXISTS fixes the
class, and that is the half worth having: rglob on a nonexistent directory yields
an empty iterator rather than raising, so a missing root and a clean root were
indistinguishable to the scan. A package move silently emptied it while the suite
stayed green. A third check bounds the count, so narrowing surfaces as a number
rather than as nothing.

Mutation-proven by restoring the pre-S10 roots: two failures, one naming all four
missing paths, one reporting "covers only 6 files". The guard is also now proven
to bite on the defect it was written for — a module reading
os.environ['APPWRITE_DATASTORE_API_KEY'] — which had never been demonstrated. An
unproven guard is what two silent days buys you.

Explicit list, not derivation, decided and recorded. test_clone_readiness.py
enumerates an overlapping set for a different question; folding them together
would couple two guards whose sets are free to diverge, since a module can be
partner-neutral without being credential-blind. What makes the list safe is the
existence assertion.

No leak occurred. The relocated modules were checked at registration and again
here: zero hits for os.environ, getenv, load_dotenv, API_KEY, credentials.

Two entries that had vouched for this guard are restored rather than quietly
reverted — C-57's "already mechanically enforced" and Cluster I's fix strategy —
because a claim that was false for two days is worth more as a record.

Also caught by the register's own integrity guard while writing this: the
extraction regex for C-74 used a `(?=^### )` boundary, and since C-74 was the last
entry in Open Concerns it swallowed the `## Disagreements` heading, filing three
D-entries under Open. Four integrity tests failed immediately. Fixed with a
section-aware boundary — the guard S2 built catching a defect in the tooling that
maintains the register it guards.

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

assert_env_declared has logged before raising since #193. The entry stayed under
Open while eight further stories shipped, in the epic whose entire thesis is that
a record should not outlive the thing it describes.

S2's guard did not catch it, and that is by design rather than by defect: it
matches two declarative phrasings, and this entry's closing language is prose —
"a two-line change; the natural place to take it is S8". That is the false
negative S2 deliberately accepted, and the cost came due within hours.

Widening the guard would be the wrong fix; no regex separates "describes work that
is done" from prose. The gap is that #182 was closed without disposing of the
entry it named in the same change. The generalisable rule — a story that names a
register entry disposes of it in the same PR — goes to S9's ADR question alongside
the CIC lagging three consecutive stories. Both are records updated by memory
rather than by the change that invalidates them.

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

C-22's trigger fired on 2026-07-27. Run-0 put 64,742 cells x 36 months into the
UN FAO's store, live, and the written procedure (#15, June) described disk caches
and shapefiles — both deleted with the runtime mapper. There was data in a
partner's store and no usable way to correct it.

Affected deliveries are identified by run_id and lookup_version, both present by
construction. That is why this story waited on S5: until C-60, lookup_version
could silently be the string "unknown", and a procedure whose identification step
rests on a field that can become a placeholder is not a procedure.

The wire mechanism is supersession, not retraction, and the document says so
rather than inventing one — manifest-last commit ordering means a run is replaced
by publishing a new complete run. It also states the consequence a reader would
otherwise find the hard way: views-faoapi selects the newest manifest over a broad
filter, so a correction is picked up because it is NEWER, not because it is
CORRECT, and a partial correction published to production is indistinguishable
from the real one. That is C-73, cited rather than re-solved.

Confirmation is offline against committed artifacts, because re-running the
pipeline to investigate changes the thing being investigated. And if the fidelity
suite passes while a value is still wrong, the fault is upstream — C-43's scope
split, restated where an operator will meet it at 22:00.

One step is written but NOT decided, and it is the step that reaches the partner:
who contacts the UN FAO and whether they expect retraction or supersession. Both
belong to the operator. The document states them verbatim and tells the reader to
stop and ask rather than improvise — an inconsistent first message to a partner is
harder to correct than a slow one.

C-22 closes because the procedure exists and says exactly where it stops. What
remains is a decision, not engineering, and leaving the entry open would read as
unfinished work rather than as a question awaiting an answer.

docs/CLONING.md carries it forward: answer your partner's correction questions
BEFORE first delivery. This repo did not.

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

* docs(ops): record the operator's correction decisions; put the FAO half to them formally

Simon Polichinel von der Maase is responsible for contacting the FAO when a
delivery is found wrong — direct email, not an automated alert, as soon as the
scope is established rather than after a correction is prepared.

The intended treatment of a bad delivery is WITHDRAWAL. What is implemented is
SUPERSESSION, and the procedure now states that gap in the section an operator
reads at 22:00, because believing a bad delivery becomes unretrievable when it
does not is worse than knowing it stays. Withdrawal has no wire mechanism: it
needs an ADR-013 amendment and matching views-faoapi work, and whether that is
worth building depends on FAO's answer about audit requirements.

Both are put to FAO as Pre-Release Note 07 Topic B, with a table of the interim
defaults marked as placeholders rather than policy — including the one that is
the option we are NOT recommending.

Recipients are deliberately not in this repository. It is public. Naming a
responsible person on our side is one thing; publishing an external
organisation's individual email addresses to a public repo as a side effect of
documenting a runbook is another. The contacts live in the FAO-02 project
materials. tests/test_doc_accuracy.py now refuses @fao.org anywhere in the repo,
mutation-proven.

The doc-accuracy guard fired on this change, which is what it was written for: it
required that removing the "not decided" marker be accompanied by the decision.
Its replacement guards the subtler failure — a half-answered question quietly
becoming a whole answer, so the outstanding half stops being asked.

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

* fix: the address guard fired on the sentence describing the address guard

Third instance in this epic of a guard catching the prose that explains it, after
S3's retired contract name and S5's ledger-schema docstring. The fix is the same
each time: name the thing without spelling it.

Worth keeping the note rather than silently rewording, because the alternative
each time was to weaken the guard, and three times would have made it worthless.

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

---------

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

Ten register entries closed, one cluster closed, open count 24 -> 15. Every
remaining entry is classified: six blocked on the pipeline-core 3.0.0 publish,
five owned by another repo, four deliberately deferred. None unexamined.

ADR-014 records what the epic learned. The argument for writing nothing was
genuinely strong for most of it: a contributor who violates the register's
closing-condition convention already gets a failure naming the exact strings and
the fix, at the moment of violation, which teaches better than an ADR nobody
opens. What that argument leaves unrecorded is the two clauses no test carries —
a deferral needs a trigger and an owner, and a change that names a record disposes
of that record in the same change. An ADR for the mechanisable rules would be
ceremony; one for the un-mechanisable ones is the only vehicle there is.

§5 says explicitly that no test enforces it and none is proposed, and records the
attempt so the next person does not spend the same afternoon on it. The evidence
that it lapses is the epic's own: C-71 fixed in the morning and filed open all day
while eight stories shipped; the CIC lagging three consecutive stories, caught by
review each time and by nothing mechanical.

The closeout states what was NOT done, at length, because a closeout reporting
only successes is the defect this epic exists to fix: Cluster M untouched and
correctly so; the CI question decided in writing but unowned; withdrawal of a bad
delivery chosen but deliberately unbuilt pending FAO's answer; two questions with
the partner; the deploy gate's xfail tuning left alone on purpose; and a local
Python below the declared floor that skips three checks.

The sharpest thing the epic produced is smaller than any of its stories:
existence is not reachability.

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

views-frames sits on the byte-producing path: wire/shard.py writes each Hop-B shard
through views_frames.io.arrow.save, and the §10 golden fixture is generated the same
way. A version bump there is a candidate contract event, so it was checked rather
than assumed.

Verified in a clean venv at pyarrow 16.1.0 (the pinned version, not this machine's
23.0.1) with views-frames 1.10.2: the fixture generator reproduces ALL FIVE canonical
artifacts byte-identically — arrow shard, tap zip, both manifests, sidecar. The
committed SHA256SUMS is unchanged and the fixture bytes are untouched.

The same venv runs the full suite at 312 passed, 0 failed. The five failures seen
locally are purely this machine's pyarrow 23.0.1 against the <17 pin (C-72); they
disappear at the pinned version, which is a useful confirmation that the known-failure
set really is what it has been claimed to be all along.

Lock diff is one package: 1.6.0 -> 1.10.2 plus a docs extra that appeared in the new
release's metadata. pyarrow stays at 16.1.0. views-frames 1.10.2 declares
pyarrow>=14,<20 for its arrow extra, so the ceiling is compatible.

Also corrects the fixture README, which named views_frames 1.0.0 as a pinned tool
version. That was stale — the lock had said 1.6.0 for weeks — and it conflated the two
dependencies. pyarrow is the version-sensitive one; views_frames is not, and the README
now says which is which and what evidence supports it.

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

* fix(seam): re-pin to registry v1.4.0 — the drift detector's first real firing

S6 (#187) built a check that fails when views-appwrite's coordinate registry moves
without this repo re-verifying against it. It fired on 2026-08-03, naming the
version and the action:

    the Appwrite Seam Contract's registry moved to v1.4.0; this repo declares
    v1.3.0. Re-verify appwrite_env's declaration against v1.4.0, then bump
    SEAM_CONTRACT_VERSION and SEAM_CONTRACT_COMMIT together.

A true positive on its first firing, and it turned development red rather than
letting the divergence sit unnoticed — which is the whole point.

The upstream change graduates four CRAFD coordinates from reserved to real and
records the CRAFD caller key as issued. Its own header says the bump "imposes no
new obligation on any consumer". Re-verified rather than taken on trust, because a
bumped pin ASSERTS that someone checked: all 13 names this repo declares are
present at v1.4.0 with unchanged class, and nothing was added or removed across
connection/target/secret. The claim holds for us mechanically.

Pin bumped as a pair — version and commit together, never one alone — to 20dfd0f,
confirmed an ancestor of views-appwrite main with both cited files present at it.
That reachability check is the one #196 taught us to make: the withdrawn b54928f
existed, its files existed at it, and it had never reached main.

ADR-013 §7d's two URLs follow the pin, with a dated erratum recording that the
detector worked.

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

* docs(adr): past-tense the superseded pin

Two erratum entries still read "both links now pin 47172af". They pin 20dfd0f as
of this change, so the record was asserting a superseded pin as current — the
exact stale-claim class ADR-014 exists for, in the document that records the last
time it happened.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same procedure as #212, same outcome: our 13 declared names are unchanged in
class and membership at v1.4.1, verified rather than assumed before the pair was
bumped. The upstream change is a register reframing that imposes nothing on
consumers.

Noted in ADR-013 rather than acted on: the registry moved twice today, and a pin
chased per-PR is a real cost. If views-appwrite stays this active, the check may
want a MAJOR/MINOR floor rather than an exact match. That is a decision, not a
fix, so it is recorded where the next person re-pinning will read it.

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

* feat(enrichment): S4 (#89) — numpy/pyarrow keyed gather, drop the pandas merge

Rewrite GaulLookupEnricher's lookup side to be pandas-free (epic #85):
- Read the 888 KB GAUL lookup with pyarrow; hold it as numpy arrays + a sorted
  key index. Attaching metadata is a keyed `searchsorted` gather, not a pandas
  merge — absent gids gather to None so the downstream null gate still fails
  loud. The artifact is no longer materialised as a pandas frame, and the join
  no longer needs it to carry pandas index metadata (which unblocks S5/#90's
  pyarrow-native builder).
- pandas becomes a TYPE_CHECKING-only import (it's in the enricher's interface —
  callers hand it DataFrames — not its implementation).
- test_enrichment: the `lookup` fixture loads the artifact directly (the
  enricher no longer exposes a private pandas frame), tolerant of both the
  current pandas-index shape and S5's index-free one.
- test_doc_accuracy: the pandas-importer guard now checks by AST, asserting
  ZERO runtime importers + type-only in enrichment.py (a regex couldn't tell a
  TYPE_CHECKING guard from a real import).

Recovered from an uncommitted working tree on this (correctly-named) branch —
complete, its 31 tests pass. (The repo's 5 wire shard/sidecar byte-parity +
toolchain-pin failures are pre-existing and unrelated to this change.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(enrichment): S4 review findings — refuse an empty lookup, and say what the dtype change costs

Four findings from the diff review, applied to the commit that preceded them.

An empty lookup raised IndexError from inside the gather. The guard read
`(len(self._keys) > 0) & (self._keys[clipped] == wanted)`, which LOOKS like a
guard and is not one: `&` evaluates both operands, so the index happened anyway.
Guard where the condition is knowable — __init__ now refuses an empty lookup with
a message about the lookup, rather than letting the delivery later complain about
missing metadata.

The parity proof passed check_dtype=False, check_categorical=False and compared
after reset_index — three flags that hide exactly what a merge-to-assign rewrite
is most likely to change. Re-checked without them. Two results worth having:

  - name columns went category -> object. Measured, not guessed: on a 200-row
    output, category is 1,795,191 bytes and object is 60,061, because a
    categorical carries the artifact's full 64,742-entry dictionary whatever the
    output size. Lighter for the frames this object actually sees. Recorded in the
    code beside the C-32 note it might otherwise be read as contradicting.

  - on EMPTY input the old merge replaced the input's RangeIndex with an
    object-dtype Index; the gather leaves it alone. The only behavioural
    difference found, and it is in the direction the CIC already claimed.

The CIC is corrected — fourth story running where it lagged the change, which is
the pattern ADR-014 §5 was written for, recurring in the first story after the ADR
landed. It now states the gather, the dtype change, the empty-lookup refusal, and
that pandas is interface-only.

Also dropped a dead alias and a duplicated row count.

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

* fix(enrichment): S4 review round 1 — a declared conversion, and the guards the claims needed

Six findings from /code-review max (five parallel reviewers), all reproduced before
fixing.

The blind `np.asarray(gids, dtype=np.int64)` was wrong in three ways, one of them a
regression against development:

  - a NaN gid became INT64_MIN with only a RuntimeWarning, and then the warning line
    itself raised `ValueError: cannot convert float NaN to integer` because it
    iterated the PRE-cast array. The path whose job is to report the problem
    gracefully was the path that crashed. A missing gid is ordinary data quality.
  - a non-integral float truncated SILENTLY: 54220.000000001 -> 54220, matching a
    real, different cell. Verified against the pandas merge this replaced — it
    returns null there, and even warns. A silent wrong match is worse than a crash.
  - pd.NA raised a bare TypeError with no log and no contract-shaped error.

Replaced by `_as_cell_ids`, which DECLARES what a cell id is and marks anything else
unusable rather than coercing it. Unusable ids gather to null exactly as unknown
gids do, so the downstream gate sees a hole rather than a guess. Verified case by
case against the old implementation: NaN, drifted float and object-with-None now
match it exactly, and pd.NA is strictly better (old crashed).

The lookup side had the same hole: a null key would take the same sentinel, collide
with an unusable query id, and report FOUND — handing back another cell's metadata.
That is the fabricated value this module's docstring forbids. __init__ now refuses
it.

The warning then named cell 0 for an unusable id — a cell nobody asked about. It now
names unknown cells and separately counts rows that carried no usable id, because an
id that is not an id has none to report.

The AST guard added in this same PR had the defect it was written to catch. It
walked the whole `if TYPE_CHECKING` node including `else:`, and substring-matched
the test, so `not TYPE_CHECKING` read as a guard. Both evasions reproduced, both now
caught, and pinned by a test rather than a comment — the `&`-not-short-circuiting
shape, committed twice in one pull request.

The CIC's guarantees were prose resting on a throwaway script. Committed: empty and
null-key refusals, the four unusable-id behaviours, row/order/index preservation
across five shapes plus a non-default index, and the object dtype. 29 tests.

ADR-012 updated — my own test docstring said "ADR-012 names the seam, so update it
rather than letting the claim drift", and the first draft did not. Three claims in
the CIC that PR #200 retired in the manager's CIC were still standing here.

Registered C-75: this class has no production caller and now holds a second copy of
the delivery path's keyed gather. Tier 3 — the verification path and the delivery
path implement the same algorithm twice, and the tests run through the copy that
does not ship.

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

* fix(enrichment): S4 review round 2 — bound the conversion, stop parsing, prove the mask

Round 1 removed one silent coercion and introduced two more. Round 2 found them.

The object branch used `float(value)`, which PARSES. It accepted the string "54220"
as a cell id — where the pandas merge this replaces raised "You are trying to merge
on object and int64 column". A string gid column is a declaration error and the old
path said so; parsing it is inference, and it is the drifted-float defect pointing
the other way. It now accepts only values that already ARE integers. `bool` is
excluded for the same reason: True is not cell 1.

Neither branch bounded int64. `uint64` max wrapped to -1 and `1e30` to INT64_MIN,
both flagged VALID — an id nobody wrote, marked usable. Harmless only because real
gids are 1..259,200, which is luck of the key range rather than a property the code
enforced. Both branches now bound explicitly.

`& usable` in `_gather` was the one new guard that survived its own removal: unusable
ids substitute to 0, and 0 is not a real gid, so the key comparison already failed.
It looked like decoration. It is not, and the case is now committed: with a lookup
containing gid 0, a NaN row silently receives cell 0's geography without the mask.
That is the fabricated value the docstring forbids, produced from an input the caller
never wrote.

The out-of-range test I wrote in round 2 built every case with dtype=object, so all
of them entered the element-wise branch and the int and float bounds it claimed to
cover were never executed — removing them left the suite green. Each case now enters
the branch it tests. Mutation matrix after: dropping `& usable`, the float bound, the
int bound, or the isinstance check each fails the suite.

`np.isfinite` is the exception and is now documented as such rather than left to be
rediscovered: inf fails the upper bound, NaN fails `arr == rint(arr)`, so the check
is subsumed and defensive, not load-bearing. Saying so is cheaper than the next
person mutation-testing it and wondering.

37 tests in test_enrichment.py; 337 in the suite. Wire untouched.

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

* fix(enrichment): S4 review round 3 — the float bound was off by one ULP, and my mutation proof was a replica

Second /code-review max pass. Three correctness defects and a vacuous test, all
reproduced before fixing.

The float upper bound admitted exactly what it was added to exclude. _INT64_MAX is
2**63-1, which is odd and NOT representable in float64, so comparing a float against
it promotes and rounds UP to 2**63. float(2**63) passed isfinite, integral and both
bounds, then wrapped to INT64_MIN and was flagged valid. Now bounded strictly below
2.0**63, which IS exact. _INT64_MIN needed no change: -2**63 is a power of two.

timedelta64 was accepted as a cell id, because isinstance(np.timedelta64(5,'ns'),
np.integer) is True — a numpy quirk, and datetime64 is not, so the hole was specific
to one type. A duration is not a cell id any more than True is cell 1.

The test I wrote to prove the AST guard bites was itself a replica. It defined a
local copy of the classifier and asserted against that; reintroducing the polarity
bug into the REAL guard left all 16 tests green. That is the defect S4 of epic #181
retired from test_validation.py two days ago — 43 tests against a function the file
defined itself — committed again by me, in the PR whose message boasts about
catching its cousin. The classifier is now one module-level function with two
callers, and both regressions now fail it.

And the fix for that had a sting: routing the out-of-range cases through the public
API, as the review recommended, LOST the proof. Publicly, "refused" and "accepted but
wrapped to an absent id" both produce null, so reverting the float bound left the
test green. The public assertion is the caller's guarantee; the mask assertion is the
mutation proof; both are needed and both are now present, with a comment saying why.

_gather returned four values, three of which existed only to build one log message.
The warning moved inside it, matching this file's own convention where __init__
co-locates detection and logging at every guard. It returns one dict.

My own error message credited the builder with refusing null keys. It does not: its
null check runs after the key becomes the index, and DataFrame.isna() does not
inspect an index. A null key is unreachable there only because an earlier
astype('int64') raises — protection by accident. Registered as C-76, along with the
empty-output case, which IS reachable and writes cells=0 without complaint.

Also: the pandas error message was misquoted, ADR-012 cited a path that does not
exist (unfao/wire/ -> contract/wire/), the CIC claimed eight input shapes where the
pinned class has six and named one it does not exercise, the artifact size was wrong
by 2% in four places, and a register quote was singular where C-40 is plural. C-75
gained the owner ADR-014 §4 requires and its first draft omitted while citing §4.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Stand up the views-postprocessing producer for the second consumer, CRAF'd
(views-crafdapi). Per CLONING.md this is "replace three, keep everything else":

- crafd/product.py — TARGETS = the same three GED fatality series (CRAF'd is FAO
  extended, same forecasts/geography; additional targets are a later Amendment A1
  edit), CONSUMER_DOCUMENT_NAME = "un_crafd" (ADR-034 §6), S_MIN = 2,
  UPLOAD_ENABLED = False (§11.4 interlock — staged locally, zero store calls,
  until the views-crafdapi selection guard is deployed).
- crafd/appwrite_env.py — CRAFD_ENV (APPWRITE_CRAFD_* coordinates, filled by the
  operator at views-crafdapi S9 / views-appwrite #38); connection + prod-forecasts
  env shared with FAO (same internal shelf source). Contract pinned at 1.3.0 /
  47172af (same edition as unfao; the CRAFD names are reserved there).
- crafd/managers/crafd.py — CRAFDPostProcessorManager: the pipeline-core seam,
  reads the shared production_forecasts shelf and delivers the same wire to
  crafd_bucket. Mechanical retarget of the FAO manager; no partner-neutral
  machinery touched.
- test_doc_accuracy: the pipeline-core-importer guard now expects BOTH partner
  managers (unfao + crafd) — a new partner manager is expected, not blast-radius
  creep; anything OUTSIDE the managers still fails.

VALIDATION: ruff-clean and AST-parses locally; the FULL suite is validated by CI,
because this machine (Python 3.13) cannot build the repo's pinned pyarrow 16.1.0
(no 3.13 wheel) — CI runs the correct 3.11/3.12 + 16.1.0 toolchain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`crafd/` landed exempt from eight guards, because each named `unfao` as a string.
Five review passes; each found defects in the previous pass's fixes.

The defect crafd shipped with
- `crafd/appwrite_env.py` pinned registry v1.3.0/47172af — an edition at which its
  own four APPWRITE_CRAFD_* coordinates had no values, while the file's docstring
  cited the PR that filled them. Re-pinned to v1.4.1/90fc105.

What was unguarded, and is now
- partner list declared ONCE (`tests/conftest.py:PARTNER_PACKAGES`), asserted
  against the filesystem, and consumed by every guard that needs it
- þing-01 dotenv borrow: covered 2 files of 30. A live `load_dotenv(find_dotenv())`
  in `unfao/appwrite_env.py` — the entry validator whose docstring says the borrow
  is dead — ran on import with the suite green. Now a package-wide AST walk, which
  sees imports and calls and cannot see the prose that legitimately describes it
- ADR-002 both legs: `contract/` could import `crafd`, and `delivery/ -> contract/`
  had no test at all. Both are now AST import walks plus a subprocess import-purity
  check. Regexes escaped three times (relative, bare-package, function-body) before
  being replaced
- line budget: the last hardcoded list; a 906-line third manager passed it. Derived,
  and applied to the whole `managers/` directory
- consumer document name: nothing asserted crafd's. Pinned, plus a gated cross-seam
  check reading views-crafdapi's own `APIPathManager` literal and its filter field
- `_MACHINERY` had no completeness assertion, so `contract/enrichment.py` was exempt
  from both purity guards

Production code
- `_ContractStorePort.upload`'s comment claimed the pipeline-core store "only LOGS"
  metadata failures. It returns `success=False, code="PARTIAL_SUCCESS"`. The comment
  was self-defeating: if it only logged, the guard it explains could never fire

Records disposed of in the same change (ADR-014 §5)
- ADR-012 ontology, CLONING.md, role_and_seams, the CIC, unfao/managers/README,
  README tree, `contract/__init__` — all said "the" partner or "only one" importer
- CLONING.md over-claimed þing-02 S24(5) as a flat prohibition; the verdict binds the
  repos cut from views-faoapi, not an in-repo partner package of the producer. It also
  dropped #146's named trigger and asserted a tracker state #146 did not have (commented)
- register: C-77, C-78, C-79 added; C-33, C-40, C-47, C-57 amended; 76/17/59 -> 79/19/60

Also
- retired `test_version_bumped_past_latest_tag`: it asserted a version is NOT tagged,
  so it went red whenever views-datafactory was in a clean released state. Its purpose
  is covered by `test_land_gaul_commit_is_in_a_release_tag`
- `sibling_repo` briefly raised on a bad override and turned a typo into
  "0 tests run". Moved to its own assertion
- C-33's measurement was wrong six times, twice by edits that shifted lines it had
  just counted. No entry states manager line numbers now — they name symbols, and
  the docs state the `diff` command instead of its result

361 passed, 40 xfailed, 0 failed (conda env, pipeline-core 3.0.0 editable, pyarrow
16.1.0); ruff clean. Every guard mutation-proven; ~25 mutations, all reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(crafd): CRAF'd producer package — clone of unfao/ (per CLONING.md)
…lback

Three review iterations on the development->main sync. Each found defects in the
previous iteration's fixes; the pattern held all three times.

The one that mattered
- docs/operations/correction_procedure.md said withdrawal "has no mechanism today"
  and "there is no rollback". Both false. views-faoapi ships an operator quarantine
  (f1a59bf, on its main): add a store document's file-id to
  APPWRITE_UNFAO_QUARANTINED_FILE_IDS and selection drops it, falling the consumer
  back to the previous run. Read at selection time, no redeploy, nothing deleted,
  reversible. The runbook instead sent an operator to rebuild and republish an entire
  corrected run, for hours, while wrong data stayed live.
- Iteration 2 found that fix incomplete: a run has TWO selection entry points. The
  historical artifact is a separate document selected by category="historical" and
  never consults the manifest. Quarantining the manifest alone leaves it serving —
  wrong way round for a geography fault, since lookup_version lives only there. §3
  now gives a per-leg table.
- The env vars are partner-specific. Setting FAO's name against a CRAF'd deployment
  is a silent no-op. Said in the runbook and in CLONING.md, which had told a new
  partner these steps "transfer unchanged".

The guard that protected the error
- test_the_procedure_distinguishes_intended_policy_from_what_is_implemented had been
  mutation-proven against the false claim: it asserted the doc must say withdrawal
  costs "an ADR-013 amendment". ADR-014 §2 asks whether a guard bites, never whether
  what it bites on is true. Replaced with one that pins the mechanism -- checkable
  against another repository rather than against our own prose.

Other corrections
- README's delivered-schema table had the NORMATIVE column order reversed and typed
  the four *_code columns int where the wire is float64. A consumer building a reader
  from it ships a broken one. Corrected and now guarded against gaul_schema.COLUMNS.
- README copied four real registry VALUES into a public file, two lines below the
  sentence saying they never are. The value-copy guard scanned only .py; it now scans
  tracked markdown, matching NAME=value assignments (a substring draft cried wolf on
  prose that merely names a store).
- The UNFAOPostProcessorManager CIC named GaulLookupEnricher as its enrichment
  collaborator in six places; the manager has zero references and a test asserts its
  absence. Its sibling CIC said the opposite. Five further CIC claims described
  deleted code.
- Runbook: run_id and lookup_version are on different artifacts and never co-occur;
  the fidelity count is 24, not 26; region-set equality is gated, not always-on.
- ADR-014 said "no test enforces this and none is proposed" while two partial guards
  shipped in the same branch.
- test_no_partner_contact_details_... scanned the repo root, so a CI sibling checkout
  could turn main red with a message naming another repo's file. Scoped to tracked
  files -- via a directory pathspec, because git's ** does not match top-level files
  and the first fix silently narrowed a security-adjacent scan.

CI
- run_pytest.yml checks out views-crafdapi. Recovers exactly ONE test. views-
  datafactory was tried and reverted: its tests need raw parquets absent from its git
  repo. views-appwrite and views-faoapi are private -- C-81.

Register: C-80, C-81, C-82 added; C-07, C-15, C-22, C-24, C-57, Cluster H, D-12
corrected. 79/19/60 -> 82/22/60.

402 collected; 362 passed / 40 xfailed locally, 347/17/38 in a bare checkout,
348/16/38 with the crafdapi sibling. ruff clean. Every new guard mutation-proven.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(docs): pre-main-sync — the runbook told operators there is no rollback
@Polichinel
Polichinel merged commit d77e204 into main Aug 3, 2026
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