"""The verdict for "a Friday release is followed by another one sooner",
on a held-out population, with the window that does not measure the calendar.

Committed 2026-09-16 (session 66) before a single package of this population
was fetched. It is the direct successor to `research/friday-releases/`, run
the same evening, and it exists because that study's answer to the second half
of the folklore was VOID and the repair was found after the data.

WHAT HAPPENED FIRST, in one paragraph, because a replication is only honest if
it says what it is replicating. On a random 1,000 of PyPI's top 5,000 projects,
"a Friday release is followed within 72 hours by another release" came back at
a relative risk of 0.70 against Monday-to-Thursday — and the sealed negative
control (Tuesday, a day the folklore says nothing about) came back at 1.114
instead of 1, so the claim was recorded void. The cause: a 72-hour window from
a Monday contains three working days and from a Friday one, and the follow-up
rate tracks that count (1 day 35.7 %, 2 days 44.2 %, 3 days 46.6 %). A window
of 168 hours contains exactly five working days whichever day it starts on. On
that window, in that sample, the control was clean (1.027) and Friday's
relative risk was 1.065, interval 1.046 to 1.084.

That number is not a result. The window was promoted to primary after the data
and the sample is the one the promotion was chosen on. This file makes it a
claim, on a population that study never saw.

--------------------------------------------------------------------------
THE POPULATION — held out on purpose
--------------------------------------------------------------------------
The same public download ranking snapshot (2026-09-01,
https://hugovk.github.io/top-pypi-packages/top-pypi-packages.min.json), but
ranks 5001..15000, which the first study's frame excluded. No project in the
first sample can appear in this one; the script asserts it against the
committed `../friday-releases/sample.txt` and prints the overlap, which must
be zero.

THE SAMPLE. 1,500 projects drawn uniformly without replacement from those
10,000 ranks, `random.Random(20260917)` — a different seed from the first
study, because the same seed over a different frame is a coincidence waiting
to be explained.

Less popular projects release less often, so the sample is half again as large
and the expected release count is lower. The realised n is published, never
trimmed.

RELEASES AND THE FILTERS are identical to the first study, verbatim, and are
imported rather than retyped: a distinct non-pre-release version, dated by the
earliest upload among its files, inside [2016-01-01, 2026-09-01) UTC.

--------------------------------------------------------------------------
THE CLAIM
--------------------------------------------------------------------------
STATISTIC: p_d = P(the same project publishes another release within 168 h |
the release was on day group d), and RR = p_Friday / p_Monday..Thursday, with
a 95 % interval on log RR. Eligibility: the release is at least 168 h before
2026-09-01T00:00Z, so every release is watched for the same full window.

VERDICT — the bars, and why they sit where they do. The first study's estimate
was 1.065. A replication's job is to say whether the effect is there at all,
not to re-estimate it, so:

    survived      lo(RR) > 1.00  AND  RR >= 1.03
    killed        0.97 < lo(RR)  AND  hi(RR) < 1.03
    inconclusive  otherwise

The 1.03 floor is a judgement and it is stated as one: below a 3 % relative
difference I do not think the claim "Friday releases need fixing" is a thing
anybody means, whatever its p-value. The bars were chosen with knowledge of
the first study's point estimate — that is what an effect size from a prior
study is *for* — and they are not a window drawn around it: 1.065 is not
between them, it is above both.

WHAT IS DECIDABLE, computed now, before the fetch. With n1 Friday and n2
Monday-to-Thursday eligible releases at a base rate p,
    se(log RR) ~ sqrt((1-p)/(n1 p) + (1-p)/(n2 p))
At p = 0.55, n1 = 7,000, n2 = 28,000 this is 0.0117, so the interval is about
±2.3 %: the survived branch needs a true RR near 1.03 or above, and the killed
branch needs the truth within about 0.7 % of 1. At p = 0.55, n1 = 3,000,
n2 = 12,000 the half-width is ±3.6 % and THE KILLED BRANCH CANNOT FIRE AT ALL,
because an interval that wide cannot fit inside 0.97..1.03.

THE PRE-REGISTERED CONSEQUENCE, decided here rather than after the count:
if the realised half-width of the interval exceeds 3.0 % — i.e. the killed
branch is unreachable — a null result is reported as INCONCLUSIVE (floor not
reached) and the study is recorded as such. A rule that can only confirm is
not a rule ([[a-clause-that-cannot-fire]]).

NEGATIVE CONTROL, sealed and scored, as before: the same RR for TUESDAY
against Monday/Wednesday/Thursday, which must land inside 0.97..1.03 or cover
1. Outside that, the measurement sees weekday structure unrelated to the claim
and the verdict is VOID rather than scored — the same consequence as the first
study, which is the only reason that study is not publishing an artefact
tonight.

SECOND CONTROL, new and sealed: SATURDAY and SUNDAY against Monday-to-Thursday,
reported for direction only, not scored. A weekend release is disproportionately
an emergency, so if the instrument sees anything at all about urgency it should
see it there.

--------------------------------------------------------------------------
THE SHARPER PROXY, registered and scored SEPARATELY
--------------------------------------------------------------------------
"Another release" is a weak proxy for "it broke": feature work looks the same.
A follow-up that increments ONLY the patch component of a semantic version —
1.4.2 after 1.4.1, with the same major and minor — is what a fix looks like.

STATISTIC: RR_patch = P(followed within 168 h by a patch-level bump | Friday)
/ P(the same | Monday..Thursday), over releases whose own version and whose
follow-up's version both parse as semantic versions with three components.

Same bars, same control requirement, its own verdict line. It is registered as
a second scored claim and not as a robustness check, because it is a different
proxy for the same idea and it deserves to be able to fail on its own.

--------------------------------------------------------------------------
BRANCH ODDS, mine, before the data
--------------------------------------------------------------------------
    any follow-up    survived 0.45   killed 0.15   inconclusive 0.40
    patch follow-up  survived 0.35   killed 0.15   inconclusive 0.50

I believe there is a small real effect and I am not confident this sample can
resolve it; the large middle is a bet I expect to lose slowly rather than a
hedge. Written before the fetch.

--------------------------------------------------------------------------
REPORTED, registered, NOT scored
--------------------------------------------------------------------------
Friday's share of Monday-to-Friday releases in this population, with the FIRST
study's bars applied, as an out-of-sample look at claim A (18.99 % there); the
weekday histogram; the follow-up levels by day at 168 h; the same at 336 h
(two weeks, also balanced); the yank rate by weekday; the working-days-in-the-
window table that killed the first proxy, recomputed here; and the overlap
with the first sample, which must be zero.
"""

import math
import random

SEED = 20260917
FRAME_RANKS = (5000, 15000)   # half-open, zero-based: ranks 5001..15000
N_PROJECTS = 1500
WINDOW_H = 168
SECOND_WINDOW_H = 336
HORIZON_H = 168

SURVIVE_LO = 1.00
SURVIVE_MIN_RR = 1.03
KILL_BAND = (0.97, 1.03)
MAX_HALF_WIDTH = 0.030
CONTROL_BAND = (0.97, 1.03)


def rr_ci(k1, n1, k2, n2, z=1.96):
    if 0 in (k1, k2, n1, n2):
        return (float("nan"), float("nan"), float("nan"))
    p1, p2 = k1 / n1, k2 / n2
    rr = p1 / p2
    se = math.sqrt((1 - p1) / k1 + (1 - p2) / k2)
    return (rr, rr * math.exp(-z * se), rr * math.exp(z * se))


def half_width(rr, lo, hi):
    """The interval's half-width as a fraction of the point estimate."""
    return (hi - lo) / 2 / rr


def verdict(rr, lo, hi):
    if lo > SURVIVE_LO and rr >= SURVIVE_MIN_RR:
        return "survived"
    if KILL_BAND[0] < lo and hi < KILL_BAND[1]:
        return "killed"
    if half_width(rr, lo, hi) > MAX_HALF_WIDTH:
        return "inconclusive (floor not reached)"
    return "inconclusive"


def control_clean(rr, lo, hi):
    return (CONTROL_BAND[0] <= rr <= CONTROL_BAND[1]) or (lo < 1 < hi)


def sample_projects(rows):
    frame = [r["project"] for r in rows[FRAME_RANKS[0]:FRAME_RANKS[1]]]
    return random.Random(SEED).sample(frame, N_PROJECTS)


if __name__ == "__main__":
    import json
    import pathlib

    rows = json.load(open("top-pypi-packages.min.json"))["rows"]
    names = sample_projects(rows)
    first = set(
        (pathlib.Path(__file__).parent.parent / "friday-releases" / "sample.txt")
        .read_text().split()
    )
    overlap = sorted(set(names) & first)
    assert not overlap, f"held-out frame is not held out: {overlap[:5]}"
    print("\n".join(names))
