01 - Pass-rate with Wilson 95% CI¶

Computes per-(variant, branch, suite, audience) pass-rate over rolling audience windows (24h / 7d / 30d) from docs/results/*.json. Each pass-rate is reported with a Wilson score 95% confidence interval and an FMEA catalog of known failure modes for the estimator.

Output: docs/data/derived/trust_window_pass_rate.json, conforming to schemas/v2/derived-metric.schema.json.

Estimator: Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. JASA 22(158):209-212.

In [1]:
import hashlib
import json
import os
from datetime import datetime, timedelta, timezone
from importlib.metadata import version, PackageNotFoundError
from pathlib import Path

import jsonschema
from statsmodels.stats.proportion import proportion_confint

REPO_ROOT = Path.cwd()
if (REPO_ROOT / 'notebooks').exists() is False and (REPO_ROOT.parent / 'notebooks').exists():
    REPO_ROOT = REPO_ROOT.parent
RESULTS_DIR = REPO_ROOT / 'docs' / 'results'
DERIVED_DIR = REPO_ROOT / 'docs' / 'data' / 'derived'
SCHEMA_PATH = REPO_ROOT / 'schemas' / 'v2' / 'derived-metric.schema.json'
DERIVED_DIR.mkdir(parents=True, exist_ok=True)
print('repo root:', REPO_ROOT)
repo root: /home/runner/work/lab/lab

Inputs¶

Load every docs/results/*.json except downstream consumer runs (bluefin-aurora-*, bluefin-bazzite-*). Compute a deterministic inputs_sha256 over the byte-sorted concatenation of each input file's sha256.

In [2]:
EXCLUDE_PREFIXES = ('bluefin-aurora-', 'bluefin-bazzite-')

raw_inputs = []  # list of (path, bytes, sha256_hex, parsed)
for path in sorted(RESULTS_DIR.glob('*.json')):
    if any(path.name.startswith(p) for p in EXCLUDE_PREFIXES):
        continue
    data = path.read_bytes()
    h = hashlib.sha256(data).hexdigest()
    raw_inputs.append((path, data, h, json.loads(data)))

concat = ''.join(sorted(h for _, _, h, _ in raw_inputs)).encode('ascii')
inputs_sha256 = hashlib.sha256(concat).hexdigest()
print('loaded', len(raw_inputs), 'input files')
print('inputs_sha256:', inputs_sha256)
loaded 20 input files
inputs_sha256: b9eb1d56966106532e190b9e7063d518a364af8e77466676efa963484947ac5f

Estimator¶

wilson_ci(passes, n) returns (p_hat, lo, hi) using the Wilson score interval at the 95% confidence level. When n == 0, returns (None, None, None).

in_window(run_date, now, hours) returns True iff run_date is within hours of now. now is the maximum run_date observed across all loaded inputs, which makes the output deterministic given the same inputs.

In [3]:
AUDIENCE_WINDOWS = [
    ('contributor', 24),
    ('user', 24 * 7),
    ('downstream', 24 * 30),
]

def wilson_ci(passes: int, n: int):
    if n <= 0:
        return None, None, None
    lo, hi = proportion_confint(count=passes, nobs=n, alpha=0.05, method='wilson')
    return passes / n, float(lo), float(hi)

def parse_dt(s: str) -> datetime:
    return datetime.fromisoformat(s.replace('Z', '+00:00')).astimezone(timezone.utc)

def in_window(run_dt: datetime, now: datetime, hours: int) -> bool:
    return (now - run_dt) <= timedelta(hours=hours) and run_dt <= now

def history_date(h):
    return h.get('run_date') or h.get('run')

all_dates = []
for _, _, _, parsed in raw_inputs:
    for h in parsed.get('history', []) or []:
        d = history_date(h)
        if d:
            all_dates.append(parse_dt(d))
now_ref = max(all_dates)
print('reference now (max run_date):', now_ref.isoformat())
reference now (max run_date): 2026-07-07T04:00:00+00:00

Compute¶

Parse the variant string into (variant, branch) by stripping the -testing or -stable suffix. Emit one record per (variant, branch, suite, audience).

In [4]:
def split_variant_branch(s: str):
    if s.endswith('-testing'):
        return s[:-len('-testing')], 'testing'
    if s.endswith('-stable'):
        return s[:-len('-stable')], 'stable'
    return s, 'unknown'

RAW_INPUT_URL_PREFIX = 'https://github.com/projectbluefin/testing-lab/blob/main/'

metrics = []
for path, _, file_sha, parsed in raw_inputs:
    variant_str = parsed.get('variant')
    suite = parsed.get('suite')
    if not variant_str or not suite:
        continue
    variant, branch = split_variant_branch(variant_str)
    history = parsed.get('history', []) or []
    rel_path = f'docs/results/{path.name}'
    evidence = [{
        'kind': 'raw-input',
        'url': RAW_INPUT_URL_PREFIX + rel_path,
        'path': rel_path,
        'sha256': file_sha,
    }]

    for audience, hours in AUDIENCE_WINDOWS:
        in_win = [h for h in history if history_date(h) and in_window(parse_dt(history_date(h)), now_ref, hours)]
        total_scenarios = sum(int(h.get('scenarios', 0)) for h in in_win)
        total_failed = sum(int(h.get('failed', 0)) for h in in_win)
        passes = total_scenarios - total_failed
        p_hat, lo, hi = wilson_ci(passes, total_scenarios)
        n = total_scenarios
        confidence = 'high' if n >= 30 else 'medium' if n >= 10 else 'low'
        state = 'fresh' if len(in_win) > 0 else 'unknown'

        record = {
            'id': f'trust_window_pass_rate.{variant}.{branch}.{suite}.{audience}',
            'label': f'T pass-rate ({audience} window) - {variant}/{branch}/{suite}',
            'value': p_hat,
            'ci_lower': lo,
            'ci_upper': hi,
            'ci_level': 0.95,
            'n': n,
            'unit': 'proportion',
            'method': 'wilson_score_95',
            'method_citation': 'Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. JASA 22(158):209-212.',
            'window': {'type': 'rolling_hours', 'size': hours, 'audience': audience},
            'confidence': confidence,
            'state': state,
            'context': {
                'variant': variant,
                'branch': branch,
                'suite': suite,
                'audience': audience,
            },
            'formula': 'p_hat = passes/n; CI = Wilson score interval at 95%',
            'numerator': passes,
            'denominator': n,
            'failure_modes': [],  # filled in next cell
            'evidence': evidence,
        }
        # Hero metric contract: skip emissions that would be confidence=low (n<10).
        # See tests/unit/test_derived_contract.py::test_confidence_low_metrics_not_in_hero_set.
        if n < 10:
            continue
        metrics.append(record)

print('built', len(metrics), 'metric records')
built 28 metric records

Failure modes (FMEA)¶

Three declared ways this number can be wrong:

  • small_sample: n<30 -> CI width may exceed 10 percentage points.
  • noniid_scenarios: Wilson assumes independent Bernoulli trials. Scenarios in the same run share infrastructure (VM, kernel, network) and are not independent. The CI may undercount.
  • flake_masking_regression: a scenario that passes intermittently inflates pass-rate even when a real regression is present. Threshold reserved for future KM survival AUC<0.7 check.
In [5]:
for r in metrics:
    n = r['n']
    r['failure_modes'] = [
        {
            'id': 'small_sample',
            'description': 'n<30; CI width may exceed 10 percentage points',
            'active': n < 30,
            'threshold': 'n<30',
            'evidence_url': None,
        },
        {
            'id': 'noniid_scenarios',
            'description': (
                'Wilson assumes independent Bernoulli trials. Scenarios in the same '
                'run share infrastructure (same VM, same kernel, same network) and '
                'are not independent. CI may undercount.'
            ),
            'active': True,
            'threshold': None,
            'evidence_url': None,
        },
        {
            'id': 'flake_masking_regression',
            'description': (
                'A scenario that passes intermittently inflates pass-rate even when a '
                'real regression is present'
            ),
            'active': False,
            'threshold': 'future: KM survival AUC<0.7',
            'evidence_url': None,
        },
    ]
print('failure modes attached to', len(metrics), 'records')
failure modes attached to 28 records

Output¶

Assemble the generator block, validate against schemas/v2/derived-metric.schema.json, and write docs/data/derived/trust_window_pass_rate.json.

In [6]:
LIB_NAMES = ['statsmodels', 'scipy', 'pandas', 'jsonschema', 'nbformat', 'nbconvert', 'papermill', 'jupyter']
library_versions = {}
for name in LIB_NAMES:
    try:
        library_versions[name] = version(name)
    except PackageNotFoundError:
        pass

output = {
    'schema_version': 'v2',
    'generated_at': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
    'generator': {
        'notebook': 'notebooks/01_pass_rate_wilson.ipynb',
        'notebook_url': 'https://factory.projectbluefin.io/methods/01_pass_rate_wilson.html',
        'workflow_run_url': os.environ.get('GITHUB_WORKFLOW_RUN_URL') or None,
        'commit_sha': os.environ.get('GITHUB_SHA') or None,
        'inputs_sha256': inputs_sha256,
        'library_versions': library_versions,
    },
    'metrics': metrics,
}

schema = json.loads(SCHEMA_PATH.read_text())
jsonschema.validate(instance=output, schema=schema)

out_path = DERIVED_DIR / 'trust_window_pass_rate.json'
out_path.write_text(json.dumps(output, indent=2, sort_keys=False) + '\n')
print('wrote', out_path, 'with', len(metrics), 'metrics')
wrote /home/runner/work/lab/lab/docs/data/derived/trust_window_pass_rate.json with 28 metrics