Sync main: epic #181, the CRAF'd producer, and the guards that carry them - #215
Merged
Conversation
…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
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.
Brings
maincurrent 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,68installs this repository with:Merging is the release. The next FAO run picks up whatever
mainpoints at. views-models#294 already carries the scar: on 2026-07-31@mainwas 208 commits behind, carried no wire modules, and would have run green while delivering the legacy artifact.This repo's own
CLONING.mdwarns "do not pin an installer to@mainwithout 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
_validatereplica 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
sha256sum -c SHA256SUMSall 5 OK;contract_versionstill1.5data/gaul_lookup.parquetlookup_versionkeycontract/enrichment.pyrewrite (pandas → numpy/pyarrow)90fc105confirmed an ancestor of views-appwrite'smainvia the API — the #196 failure mode does not recurviews-frames >=1.0,<2→>=1.10.2,<2.poetry check --lockclean. pyarrow and pipeline-core pins byte-identical tomain<3.0.0and the lock resolves 2.3.0publish_package.ymlfires onrelease: publishedonly. No tag, no deploy, no notificationKnown and registered, not fixed here
main'sprotect_mainruleset has an empty ref list, so a red CI would not block a merge. Both are operator decisions.views-models/postprocessors/has noun_crafddirectory yet, so CRAF'd's has never been resolved.Verification
Register 82/22/60, integrity-guarded. Every new or changed guard mutation-proven.