feat(gate): graduated auto-publish gate + hard scope guard + deploy/heartbeat guardrails#28
Merged
Conversation
Two launch-gating changes: 1. Never run silently unscoped. LAUNCH_STATES must be explicitly set — ALL (all states, intentional) or a comma list; an unset value now RAISES instead of defaulting to all-states-all-time (the cron-without-inputs failure). launch_states() treats ALL as "no state filter"; a new scope_is_configured() backs the hard assertion in run(). 2. Graduated auto-publish gate (pipeline/gates.py). A record that passed dedupe (confidence >= review threshold) AUTO-publishes only if it is also non-minor, names no accused, is durably sourced (court/news_article/ press_release, not live-blog-only), and confidence >= 0.85. Everything else — minors, named accused, live-blog-only, the 0.80..0.84 band — is HELD in data/_needs_review/queue.json: never on the public site, carried over so a hold whose source rolls off the feed is not lost, re-split each run so a later court update can promote it. The ledger never settles a held doc, so it re-surfaces until a human resolves it. The staged review PR shows both streams, labelled, so nothing publishes unreviewed. Fixtures updated to a non-minor auto-eligible case (still demonstrates the sanitizer dropping a forbidden key + source-union dedup). Tests cover the gate criteria, minor/named-accused holds, needs-review carryover, and the scope-unset refusal.
…not be web-reachable) The Pages deploy rsynced all of data/ except _review, so the graduated gate's held records (data/_needs_review/queue.json — minors, named accused, live-blog-only) would be fetchable by URL even though they are not indexed. That would publish an unproven accused name without human review. Exclude _needs_review and _meta (the ledger) alongside _review.
Safety-critical fixes from an adversarial review of the auto-publish gate: - HIGH: a POCSO case the model flags minor_involved=false auto-published a minor's day-precise dates (no independent minor check). Add a deterministic POCSO-implies-minor cross-check: a POCSO category/section flagged non-minor is HELD (reason pocso_minor_mismatch), mirroring the two-layer scope defense. - HIGH: a published record that becomes held kept its id off-shard, so its freed serial was re-minted for a new case — fusing two distinct cases under one id. write_shards now takes a `reserve` set (the held records); their ids/serials/anchors are reserved so a fresh mint can never collide. - MEDIUM: three notions of "minor" diverged (sanitize `is True`, gate truthiness, dedupe `or`); a truthy non-bool flag was held but not age-projected. Coerce minor_involved to a strict bool before the last gate. - MEDIUM: in auto mode (no STAGED_DIR) a held record whose source rolled off was dropped. Reload the committed _needs_review/queue.json into dedupe in both modes so held records always re-enter and persist/promote. - MEDIUM: scope_is_configured() used bare truthiness, so LAUNCH_STATES="," or " " passed the guard yet resolved to all-states. Now it agrees with the real resolution (ALL or a non-empty set), so malformed values are refused. - MEDIUM: the ops heartbeat never surfaced held records. Add auto-published vs held counts, the needs-review queue depth (with a >25 warning), and count held in the in-scope total. Regression tests for every finding, incl. the exact id-fusion 3-run scenario and the auto-mode carryover. make check green.
…sed minor Second review round on the gate fixes found 6 more real defects; all fixed: - HIGH: merge_records dropped `id` when a court doc (no id) became primary over an already-published media record, so its freed public serial was re-minted for a distinct case (public id reuse). merge now fills `id` from the secondary, so an already-published case keeps its canonical id through a merge — and the reserve seeding then protects that serial. Regression test now exercises the court-beats-media path (media run 1, court run 2). - HIGH: a carryover held record with a non-bool minor kept day-precise detail in the queue (coercion ran only on fresh extractions). Coerce at the single write choke point (_write_needs_review) so every held record — fresh or carried — is age-projected. - MEDIUM: _coerce_minor mapped an absent/None minor flag to False (fail OPEN) — a minor case with an omitted flag could auto-publish day-precise detail. Now fail CLOSED: absent/None -> minor, and any POCSO signal -> minor (projected + held), never shipped as non-minor. - MEDIUM: a held POCSO-mismatch record kept day-precise detail in the queue. Forcing POCSO -> minor before sanitize projects it (subsumed by _coerce_minor). - LOW: reserve seeding also seeded anchor keys, letting a distinct case with a weak (empty-district) anchor reuse a held id. Reserve now reserves serials+ids only (seed_anchors=False). - MEDIUM: fix #4 double-loaded the held queue in staged mode (archive + main), spamming _review with self-matches. Load the main queue only in auto mode. make check green (193 tests, 100% on sanitize/pii_guard/gates).
_update_ledger classifies membership in the non-injective sanitised-URL space. A quarantined _review doc whose sanitised url collides with an on-main published record's was settled 'published' (existing_urls checked before the held check) and skipped forever — and carryover never restores _review, so the case was lost. Check the held/quarantine set FIRST: a doc quarantined this run never settles. A rare colliding published doc merely re-processes (safe direction).
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.
Items 2 + 3 of the launch directive. Builds on the merged record-loss fix (#27).
Scope safety (item 2)
LAUNCH_STATES=ALLmeans "all states"; an unset/malformed value now RAISES instead of silently running all-states-all-time.scope_is_configured()agrees with the real resolution (ALL or a non-empty set).Graduated auto-publish gate (item 3)
A record that passed dedupe AUTO-publishes only if: non-minor and no accused named and durably sourced (court/news/press, not live-blog-only) and confidence ≥ 0.85 and no POCSO→non-minor mismatch. Everything else is held in
data/_needs_review/queue.json— never on the public site, carried over (both modes) so it is never lost, re-split each run so a later court update can promote it.Deploy/ops guardrails
_needs_review+_meta(held records — incl. named accused — must never be web-reachable).Reviewed hard
Two adversarial review rounds. Round 1 found 6 real defects (a POCSO minor's day-precise detail auto-publishing; id-fusion between distinct cases; three diverging "minor" notions; auto-mode held-record loss; a scope-guard bypass; heartbeat blind to held) — all fixed with regression tests, incl. the exact id-fusion 3-run scenario. Round 2 verification in progress.
make checkgreen locally (193 tests, mypy --strict, 100% on sanitize/pii_guard/gates, validation + pii_guard clean).