global-merge and the single task now write global-summary.candidate.json, run a table-driven gate (src/gate.rs, 20 checks in two tiers, "may replace" and "may display"; the thresholds are in the README) against the published summary and the hourly owned database, write global-summary.gate.json, and promote the candidate only if it passes. A refusal fails the run and prints a metric line for the alarms, and the published summary stays. Checkpoints only write the candidate. The hourly run evaluates the same table on the summary it reads, using its own database as the owned counts. Unless nothing fails, it hides the share and gives the reason; there is no live-count fallback any more. Windows are now exactly 30/90/365 days ending on the snapshot's day, and future-dated days fall in none of them. The old "30d" spanned 31 dates: for 21:47Z the 30d share goes from 1.199% (88,339 denominator) to 1.239% (85,462). The card shows only the 30d share, with a range and the fresh coverage. TALLY_SHOW_SHARE=false removes the card. check_publishable, the share_*_365 and objects_* fields are removed. TALLY_GLOBAL_MIN_DEFAULT_RATIO now applies per 30d/90d window, and its default changes from 0.4 to 0.6.
To verify, I replayed both summaries with tally global-gate, using S3 versions fetched read-only and a copy of the audit's hourly index. 21:47Z over 19:56Z: may replace, may not display (fails not-partial, site-scope and fresh-coverage). Its owned-db check (all branches, to Oct 24) gives owner 6,230 vs 5,978 (1.042) and agents 146 vs 146. The audit's "152 vs 178 agent lag" comes from the snapshot day being cut short, so the check now leaves that day out. 00:56Z over 21:47Z: refused. scope-ratios are 30d 0.25 / 0.00; universe moves −20.5%; owned-db has default owner 425 vs 5,624 and agents 0 vs 195. The tests cover every check, both replays (as fixtures) and each hourly refusal path. fmt, clippy (aws), node and py_compile pass. With today's live summary the page reads "under review · partial snapshot".
Round 2 is rebased onto #46. The gate now reads #46's own rules_hash, rules.hash and held_for_review (#46's single HeldRepo type), and refuses a rules_hash that disagrees with rules.hash. A new display check, share-holds, refuses while any repo held for its share of a window is unreviewed. This build's rule set hashes to b41949e3… (3a78278d… once #47 lands), and the published summary has no hash, so the first post-#46 promotion needs TALLY_GATE_APPROVED_RULES_HASH=<that hash>. rules::windows now uses the same 30 dates as the page, not 31; the hash is unchanged and RULES_VERSION isn't bumped. Coverage still owes repos_fresh and missing_changes_30d (a number or {low,high}); without repos_fresh nothing is displayed. Replays after the rebase: same verdicts, plus share-holds failing on both because those older summaries have no held_for_review. Round 3: the owned side of the owner check now collapses rewritten commits with #46's key, shared as logical_key_sql! between the export and tangled_daily_counts_by, so both sides count alike. In the hourly DB that removes 240 rewrite groups and moves the owner from 6,229 to 5,978 (−4.0%). The pre-#46 replay summaries still count every rewritten copy, which is why they now read 1.04; a #46 summary collapses the same way. A new test covers the case this protects against: 6% rewritten copies would push an 85-vs-90 comparison (0.944) down to 85 vs 96 (0.885). #47 hasn't landed, so the expected hash is still b41949e3…. All 141 tests pass. Deploy after the infra PR (https://github.com/jmeickle/permadeath-aws-org/pull/39), which sets TALLY_GATE_OWNED_DB; until that lands every candidate is refused. Screenshots: main on the same data (shows the partial all-branch summary), shown (21:47Z with partial=false and repos_fresh set by hand, all-branch site; illustrative only), and hidden (the live summary).
🤖 Generated with Claude Code