BXUP Track progress & test yourself — free
Zero-to-Hero Spine · Five Real Projects · ~25 min read
ZERO-TO-HERO SPINE · FIVE REAL PROJECTS

Team project on inherited broken codebase

The scene

Trupti Salunke joins a small logistics firm in Bhiwandi on a Monday. Her first task is a delivery-delay model that an intern built and left behind. The handover meeting was booked for Friday. The intern's last day moved up, so it never happened. What she has is a repo, thirteen commits, and one page of notes written in a hurry. The README says the model is 94 percent accurate. Nobody left in the office has ever run it. Her lead gives her three teammates and two weeks.

Why this matters to you
  • Most of your first year will be spent on code you did not write. Nobody teaches this, and it is what separates a fresher who can be handed real work from one who cannot.
  • This is the only project graded as a team, and teams fail on talking far more often than on code. The git habits here are the ones your first employer assumes you already have.
  • The honest number you produce here is your strongest interview story. A candidate who can explain why a score fell from 0.98 to 0.73, and why that is good news, has answered half of a panel's questions.
The idea

Every project so far started with an empty folder. This one starts with somebody else's code, which is what a first job actually looks like. You will not design this system. You will inherit it, understand it, prove what is wrong, fix it safely, and hand it over better than you found it.

A word on hours before you plan. The header's 24 hours are per person in a team of four; working solo, 40 to 60 hours is honest, and stage 3's out-of-scope list is where you buy time back.

The repo is a delivery-delay predictor from a Bhiwandi logistics firm. An intern built it over three months and left. It carries thirteen commits, a README that describes software which does not exist, and a note written on his last day. Some of what that note says is true. Some of it is not, and telling those apart is the whole of stage one.

Start with a rule that will feel slow and will save you days. You may not call anything broken until you can show it breaking. So stage one is archaeology: run it, read it, read the history, and change nothing. Your output is a findings register — one row per defect, holding what you saw, how a reviewer repeats it, the risk, the effort, and your decision. A row with no evidence is an opinion, and opinions cannot be ranked.

One file in this repo is a trap. It has one-letter argument names, magic numbers, forty lines of nested branches, and no comments at all. The note you inherit says its tier logic is wrong. It is not wrong. It matches the promised delivery window on all 18,000 rows, and a team that rewrites it drops two seasonal rules that nobody ever wrote down.

Wrong way: rewrite the unreadable file on day one. Right way: characterise it, prove it, then decide — and the same choice again when the score falls
Wrong way: rewrite the unreadable file on day one. Right way: characterise it, prove it, then decide — and the same choice again when the score falls

Stage two is the safety net, and it is the stage teams skip. A characterisation test — a test that records what the code does today, wrong behaviour included, so any change that moves the output turns it red. It does not check that the answers are right. It checks that you notice when they change. Without it you cannot tell a fix from a regression, and every number you report after stage two is a guess about which change caused what.

Then comes triage, with the whole team in one room. Rank by risk first and effort second, and write down what you agree to leave alone. Split the work by file rather than by who feels like doing what, because two people in one file on Monday is a lost afternoon on Friday. Every choice goes into the decision log — a dated file naming what you decided, who decided it, and the option you rejected. A team that argues twice about the same thing does not have one.

Stage four is where the number falls. The pipeline you inherited reports an accuracy of 0.9864. Two of its columns record what happened after delivery, and together they carry 76 percent of the model. That is target leakage — a feature the model cannot know at the moment it has to predict. Remove those two columns, split by pickup date rather than at random, and the honest score is 0.7278 accuracy with a ROC-AUC of 0.7534.

Nothing broke on the day that number appeared. Something was measured for the first time. The honest figure is the deliverable, and a fall is the normal result of a first real measurement. Tell your manager in that order: what changed, why the old number existed, and what the real one means for the business. WR-189 is a team that got this conversation wrong.

Six stages of taking over code you did not write, with the two that change nothing marked, and the honest re-measurement at the end
Six stages of taking over code you did not write, with the two that change nothing marked, and the honest re-measurement at the end

The honest table also tells you what to do next, and that is the part students miss. Recall on the repaired model is 0.0985, so it catches under a tenth of the late shipments. Split the same score by courier and one row shouts: on the courier whose late rate rose from 0.325 to 0.686, accuracy is 0.366. The model is worst exactly where the business changed. An average over five couriers cannot see that, which is why you never report one number alone.

Three roles rotate every stage, and each of you takes each one. The integrator — the single person who merges to main this stage, one pull request at a time, and never their own work. The reviewer reads every diff and asks for evidence rather than opinions. The scribe keeps the decision log and the findings register current on the day, not later. Everybody writes code as well; these roles sit on top of that.

A review comment earns its place when it names a line, states a risk in terms of behaviour, and says what evidence would close it. "Looks good" is not a review, and a note about a variable name on a one-line fix costs a round trip and buys nothing. When two of you disagree, run the smallest experiment that settles it and put the result in the log. If no experiment can settle it, the integrator decides for this stage and the log records who was overruled. Team projects fail on silence far more often than on code.

Some of you have no team, and the project still works. You play all three roles yourself across the six stages, one at a time, in writing. Open a pull request against your own repository, leave written review comments on your own diff, and merge only after you have answered them. Manufacture one merge conflict on purpose in stage five and resolve it deliberately. The marks are the same and so are the artefacts.

Stage six is the clone test — a stranger clones your repo into an empty folder, follows your README, and reaches a working system without asking you anything. Time it, watch where they stop, and fix that. Then write the handover note you wish you had been given, listing every defect you did not fix. This project is also the richest interview material you will own, and Phase 6 spends it. Module 6.4 wants the numbers, 6.6 wants the defence, and 6.8 wants three resume lines that each carry a figure.

Watch me do it

Six artefacts, not a tutorial. Nothing below is a new technique — module 0.2 gave you branches and pull requests, module 4.5 gave you tests, module 1.7 gave you leakage and split discipline. What follows is the standard your work is measured against, and the repo you are about to inherit. Read all six before you type a command.

Artefact 1 — the generator that writes your inheritance. Save this as make_inherited_repo.py, run it once in an empty folder, and never edit it. Standard library only: no pandas, no network, no model. It writes delay-predictor/ — the shipment file, the intern's source, his README, his handover note, and a real git history with thirteen commits including the one where he added credentials and the later one where he "removed" them. Beside it, it writes p5_sealed/, which holds the true defect list. Do not open the sealed file until your stage 1 findings register is submitted. Opening it early is the one way to fail stage 1 outright.

"""make_inherited_repo.py - run once, then never edit. Writes delay-predictor/ and p5_sealed/.

You are taking over a half-finished 'delivery delay predictor' from an intern who has
left. This script builds that repo exactly as he left it: the shipment file, his source
files, his README, his handover note, and his git history including the commit where he
added credentials and the later one where he 'removed' them. Seeded, so every team in
the batch inherits the identical mess.

Standard library only - no pandas, no network, no model. The REPO it writes needs pandas
and scikit-learn; that is the intern's problem, and soon yours.
"""
import csv
import json
import os
import random
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path

RNG = random.Random(20260420)
REPO = Path("delay-predictor")
SEALED = Path("p5_sealed")
N_ROWS = 18000
START = datetime(2024, 9, 1, 6, 0)
END = datetime(2026, 3, 31, 22, 0)
REGIME = datetime(2025, 11, 15)        # the week GatiRoad fell apart
COURIERS = ["Shreeji-Surface", "GatiRoad", "NexusExpress", "Bhiwandi-Direct", "IndiaPost-Cargo"]
HUBS = ["Bhiwandi-A", "Bhiwandi-B", "Nashik-Feeder"]
DEST = [("Mumbai", 1), ("Pune", 1), ("Surat", 1), ("Nagpur", 2), ("Indore", 2),
        ("Aurangabad", 2), ("Kolhapur", 2), ("Jalgaon", 3), ("Ratnagiri", 3),
        ("Bhusawal", 3), ("Latur", 3), ("Gondia", 3), ("Bhilai", 3), ("Cuttack", 3)]
DIST = {"Mumbai": 45, "Pune": 160, "Surat": 280, "Nagpur": 720, "Indore": 590,
        "Aurangabad": 310, "Kolhapur": 400, "Jalgaon": 380, "Ratnagiri": 330,
        "Bhusawal": 420, "Latur": 470, "Gondia": 890, "Bhilai": 1080, "Cuttack": 1580}
COURIER_RISK = {"NexusExpress": -0.75, "Shreeji-Surface": -0.35, "Bhiwandi-Direct": 0.0,
                "GatiRoad": 0.25, "IndiaPost-Cargo": 1.00}

# --------------------------------------------------------------------------- the SLA rule
# This exact text becomes src/sla.py. The generator imports it to build promised_ts, so
# the function IS the ground truth of this world. It is unreadable and it is correct.
SLA_SRC = '''"""ops sheet v3. do not touch without asking ops. -gp"""


def calc(a, b, c, d):
    if b == "express":
        if a == 1:
            h = 12 if c < 60 else (24 if c < 150 else (36 if c < 600 else 48))
        elif a == 2:
            h = 18 if c < 60 else (36 if c < 150 else (48 if c < 600 else 60))
        else:
            h = 24 if c < 60 else (48 if c < 150 else (60 if c < 600 else 72))
    else:
        if a == 1:
            h = 24 if c < 60 else (48 if c < 150 else (72 if c < 600 else 96))
        elif a == 2:
            h = 30 if c < 60 else (60 if c < 150 else (84 if c < 600 else 108))
        else:
            h = 36 if c < 60 else (72 if c < 150 else (96 if c < 600 else 120))
    if d in (6, 7, 8, 9):
        if a == 3:
            h = h + 12
    if d in (10, 11):
        if b == "standard":
            h = h + 24
    if c > 1400:
        h = h + 24
    return h
'''
_ns = {}
exec(SLA_SRC, _ns)
calc = _ns["calc"]

# --------------------------------------------------------------------------- the shipments
rows = []
span = int((END - START).total_seconds())
stamps = sorted(START + timedelta(seconds=RNG.randint(0, span)) for _ in range(N_ROWS))
for n, pickup in enumerate(stamps, 1):
    city, tier = RNG.choice(DEST)
    dist = DIST[city] + RNG.randint(-25, 25)
    service = RNG.choices(["express", "standard"], weights=[38, 62])[0]
    weight = round(RNG.choices([RNG.uniform(0.2, 3), RNG.uniform(3, 18), RNG.uniform(18, 60)],
                               weights=[52, 36, 12])[0], 2)
    items = RNG.choices([1, 2, 3, 5], weights=[62, 22, 11, 5])[0]
    pay = RNG.choices(["cod", "prepaid"], weights=[58, 42])[0]
    courier = RNG.choice(COURIERS)
    hub = RNG.choice(HUBS)
    monsoon = 1 if pickup.month in (6, 7, 8, 9) else 0
    backlog = RNG.randint(40, 260) + (90 if monsoon else 0) + (140 if pickup.month in (10, 11) else 0)
    promised_hours = calc(tier, service, dist, pickup.month)

    # the real, learnable signal - honest, and nothing like 0.94 accuracy
    z = -3.70 + COURIER_RISK[courier]
    z += 0.55 * (tier - 1)
    z += 0.00095 * dist
    z += 0.0042 * (backlog - 150)
    z += 0.45 * monsoon
    z += 0.60 if pickup.month in (10, 11) else 0.0
    z += 0.30 if pay == "cod" else 0.0
    z += 0.38 if service == "express" else 0.0
    z += 0.28 if weight > 25 else 0.0
    z += 0.32 if pickup.hour >= 18 else 0.0
    z += 0.55 if (tier == 3 and monsoon) else 0.0
    if pickup >= REGIME and courier == "GatiRoad":
        z += 2.60                       # the regime change a random split cannot see
    if pickup >= REGIME and tier == 3:
        z += 0.50
    z += RNG.gauss(0, 0.20)
    p = 1.0 / (1.0 + 2.718281828459045 ** (-z))
    late = 1 if RNG.random() < p else 0

    if late:
        transit = promised_hours * RNG.uniform(1.04, 1.85)
    else:
        transit = promised_hours * RNG.uniform(0.32, 0.96)
    transit = round(transit, 2)
    attempts = 1
    if late and pay == "cod":
        attempts = RNG.choices([1, 2, 3], weights=[30, 50, 20])[0]
    elif late:
        attempts = RNG.choices([1, 2], weights=[70, 30])[0]
    delivered = pickup + timedelta(hours=transit)
    rows.append(dict(
        shipment_id=f"BHW{n + 300000}", pickup_ts=pickup.isoformat(sep=" ", timespec="seconds"),
        promised_ts=(pickup + timedelta(hours=promised_hours)).isoformat(sep=" ", timespec="seconds"),
        delivered_ts=delivered.isoformat(sep=" ", timespec="seconds"),
        origin_hub=hub, dest_city=city, dest_tier=tier, courier=courier,
        service_level=service, distance_km=dist, weight_kg=weight, items=items,
        payment_mode=pay, monsoon=monsoon, hub_backlog=backlog,
        actual_transit_hours=transit, delivery_attempts=attempts, is_late=late))

# --------------------------------------------------------------------------- the source files
FILES = {}

FILES["config.py"] = '''# paths for the delay model. update if you move the folder.
DATA_PATH = "/Users/ganeshambekar/Desktop/delay_project/data/shipments.csv"
MODEL_PATH = "/Users/ganeshambekar/Desktop/delay_project/models/model.pkl"
RANDOM_STATE = 42
TEST_SIZE = 0.2
'''

FILES["requirements.txt"] = "pandas\nscikit-learn\njoblib\n"

FILES["src/__init__.py"] = ""

FILES["src/sla.py"] = SLA_SRC

FILES["src/load.py"] = '''import pandas as pd

from config import DATA_PATH


def load_shipments():
    df = pd.read_csv(DATA_PATH)
    df["pickup_ts"] = pd.to_datetime(df["pickup_ts"])
    df["promised_ts"] = pd.to_datetime(df["promised_ts"])
    df["delivered_ts"] = pd.to_datetime(df["delivered_ts"])
    return df
'''

FILES["src/features.py"] = '''import pandas as pd


def promised_window_hours(df):
    """how many hours we promised the customer"""
    out = []
    for i in range(len(df)):
        gap = df["promised_ts"].iloc[i] - df["pickup_ts"].iloc[i]
        out.append(gap.seconds / 3600.0)
    return out


def courier_late_rate(df):
    """share of this courier's shipments that ran late"""
    couriers = df["courier"].tolist()
    if "is_late" not in df.columns:
        return [0.5] * len(couriers)
    late = df["is_late"].tolist()
    rates = []
    for i in range(len(couriers)):
        hits = 0
        total = 0
        for j in range(len(couriers)):
            if couriers[j] == couriers[i]:
                total += 1
                hits += late[j]
        rates.append(hits / total)
    return rates


def build(df):
    df = df.copy()
    df["promised_window_hours"] = promised_window_hours(df)
    df["courier_late_rate"] = courier_late_rate(df)
    df["pickup_hour"] = df["pickup_ts"].dt.hour
    df["pickup_dow"] = df["pickup_ts"].dt.dayofweek
    df["pickup_month"] = df["pickup_ts"].dt.month
    return df
'''

FILES["src/prep.py"] = '''NUM_COLS = ["distance_km", "weight_kg", "items", "hub_backlog", "promised_window_hours",
            "courier_late_rate", "pickup_hour", "pickup_dow", "pickup_month",
            "actual_transit_hours", "delivery_attempts"]
CAT_COLS = ["dest_tier", "service_level", "payment_mode", "courier", "origin_hub"]


def scale(df, cols=None):
    df = df.copy()
    for c in (cols or NUM_COLS):
        m = df[c].mean()
        s = df[c].std(ddof=0)
        df[c] = (df[c] - m) / (s if s else 1.0)
    return df


def encode(df, cols=None):
    df = df.copy()
    for c in (cols or CAT_COLS):
        df[c] = df[c].astype("category").cat.codes
    return df


def prepare(df):
    df = scale(df)
    df = encode(df)
    return df[NUM_COLS + CAT_COLS]
'''

FILES["src/train.py"] = '''import joblib
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.model_selection import train_test_split

from config import MODEL_PATH, RANDOM_STATE, TEST_SIZE
from src.features import build
from src.load import load_shipments
from src.prep import prepare


def main():
    df = load_shipments()
    df = build(df)
    X = prepare(df)
    y = df["is_late"]
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=TEST_SIZE, random_state=RANDOM_STATE, shuffle=True)
    model = RandomForestClassifier(n_estimators=200, random_state=RANDOM_STATE, n_jobs=-1)
    model.fit(X_train, y_train)
    pred = model.predict(X_test)
    print("holdout accuracy :", round(accuracy_score(y_test, pred), 4))
    print("holdout roc auc  :", round(roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]), 4))
    joblib.dump(model, MODEL_PATH)
    print("saved model to", MODEL_PATH)


if __name__ == "__main__":
    main()
'''

FILES["scripts/report.py"] = '''"""weekly model health report for the ops dashboard."""
import joblib
import pandas as pd

from config import MODEL_PATH
from src.features import build
from src.load import load_shipments
from src.prep import prepare


def main():
    df = build(load_shipments())
    X = prepare(df)
    model = joblib.load(MODEL_PATH)
    df["pred"] = model.predict(X)
    acc = (df["pred"] == df["is_late"]).mean()
    print("MODEL HEALTH THIS WEEK")
    print("rows scored :", len(df))
    print("accuracy    :", round(acc, 4))
    by_courier = df.groupby("courier").apply(
        lambda g: pd.Series({"n": len(g), "acc": (g["pred"] == g["is_late"]).mean()}))
    print(by_courier.to_markdown())


if __name__ == "__main__":
    main()
'''

FILES["src/serve.py"] = '''"""tiny scorer the ops dashboard calls. one shipment at a time."""
import joblib
import pandas as pd

from config import MODEL_PATH
from src.features import build
from src.prep import prepare


def load_and_predict(payload):
    model = joblib.load(MODEL_PATH)
    df = pd.DataFrame(payload if isinstance(payload, list) else [payload])
    df["pickup_ts"] = pd.to_datetime(df["pickup_ts"])
    df["promised_ts"] = pd.to_datetime(df["promised_ts"])
    df = build(df)
    X = prepare(df)
    return model.predict_proba(X)[:, 1].tolist()
'''

FILES["config/secrets.env"] = '''DB_URL=postgresql://delay_app:Bh1w4ndi%40Ship2025@10.20.4.11:5432/delaydb
MAPS_API_KEY=AIzaSyD-6f4kQpLm2nT8xVr0wZbYc3JhEuKvNqA
SLACK_WEBHOOK=https://hooks.slack.com/services/T02ABCD9/B04KLMN7/xY9qWs2ZpRtVu6Ha
'''

FILES[".gitignore"] = "config/secrets.env\n__pycache__/\n*.pkl\n.venv/\n"

FILES["README.md"] = '''# delay-predictor

Predicts whether a Bhiwandi outbound shipment will miss its promised delivery time.

## What it does
Reads the shipment dump, builds features, trains a random forest, saves the model.
The ops dashboard calls `src/serve.py` for a live probability.

## Results
**Accuracy 0.94 on held-out data.** Beats the old rule sheet by a wide margin.
Features used: distance, weight, city tier, courier, service level, hub backlog.

## Run it

    pip install -r requirements.txt
    python -m src.train
    python -m src.serve --port 8000
    python scripts/report.py --week last

## Tests

    pytest tests/

## Notes
Model is retrained every Monday morning by the cron on the ops box.
'''

FILES["HANDOVER.md"] = '''# Handover - Ganesh Ambekar, last working day 27 March 2026

Sorry this is short, I am joining the new place on Monday. Calling out what I remember.

1. The model is basically done. 0.94 accuracy, ops are happy with it. The only thing
   left is putting it behind a proper API.
2. `src/sla.py` is a mess, I wrote it in one night from the ops sheet. The tier logic in
   there is definitely wrong for tier 3, I never got around to rewriting it. Please
   rewrite that file first, it is the worst thing in the repo.
3. `src/prep.py` scaling caused a dashboard problem in February. I reverted it, then put
   it back because the real issue was somewhere else. It is fine now.
4. Data comes from the ops team every Monday as a CSV. Path is in `config.py`.
5. There are no tests yet. The code is small enough to check by eye.
6. `courier_late_rate` is the strongest feature. Do not remove it.
7. Credentials were in the repo at one point, I removed them in a later commit so we
   are clean now.
8. `promised_window_hours` sometimes prints small numbers for long routes. I think the
   CSV export is bad. Ask ops for a fresh dump.

Good luck. My personal email is on the git commits if something is really stuck,
but I will be busy for the first month.
'''

# --------------------------------------------------------------------------- write the repo
if REPO.exists():
    print(f"{REPO} already exists. Delete it first if you want a clean inheritance.")
    sys.exit(1)
(REPO / "data").mkdir(parents=True)
(REPO / "models").mkdir()
for rel, text in FILES.items():
    p = REPO / rel
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(text, encoding="utf-8")

with (REPO / "data/shipments.csv").open("w", newline="", encoding="utf-8") as fh:
    w = csv.DictWriter(fh, fieldnames=list(rows[0]))
    w.writeheader()
    w.writerows(rows)

# --------------------------------------------------------------------------- the git history
GP = "Ganesh Ambekar"
GE = "ganesh.ambekar.intern@example.com"


def git(*args, when=None):
    env = dict(os.environ, GIT_AUTHOR_NAME=GP, GIT_AUTHOR_EMAIL=GE,
               GIT_COMMITTER_NAME=GP, GIT_COMMITTER_EMAIL=GE)
    if when:
        env["GIT_AUTHOR_DATE"] = env["GIT_COMMITTER_DATE"] = when
    subprocess.run(["git", *args], cwd=REPO, env=env, check=True,
                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def commit(msg, paths, when, remove=()):
    for p in paths:
        git("add", "-f", p)
    for p in remove:
        git("rm", "--cached", "-q", p)
    git("commit", "-m", msg, when=when)


git("init", "-q", "-b", "main")
git("config", "user.name", GP)
git("config", "user.email", GE)

# earlier shapes of the two files whose history tells the story
PREP = FILES["src/prep.py"]
SCALE_FN = PREP[PREP.index("def scale("):PREP.index("def encode(")]
LEAK = ',\n            "actual_transit_hours", "delivery_attempts"]'
P_NOLEAK_NOSCALE = PREP.replace(LEAK, "]").replace("    df = scale(df)\n", "").replace(SCALE_FN, "")
P_LEAK_NOSCALE = PREP.replace("    df = scale(df)\n", "").replace(SCALE_FN, "")
P_LEAK_REVERT = PREP.replace("    df = scale(df)\n", "")
FEAT = FILES["src/features.py"]
CLR_FN = FEAT[FEAT.index("def courier_late_rate("):FEAT.index("def build(")]
F_NO_CLR = FEAT.replace(CLR_FN, "").replace(
    '    df["courier_late_rate"] = courier_late_rate(df)\n', "")
STAGES = {2: {"src/prep.py": P_NOLEAK_NOSCALE, "src/features.py": F_NO_CLR},
          4: {"src/prep.py": P_LEAK_NOSCALE},
          5: {"src/features.py": FEAT},
          6: {"src/prep.py": PREP},
          7: {"src/prep.py": P_LEAK_REVERT},
          8: {"src/prep.py": PREP}}

HIST = [
    ("first commit, shipment dump from ops", ["data/shipments.csv", "config.py"],
     "2026-01-09T19:40:00+05:30", ()),
    ("sla hours calc, matches the ops sheet exactly", ["src/__init__.py", "src/sla.py",
     "src/load.py"], "2026-01-14T21:05:00+05:30", ()),
    ("features + first model, 0.71 accuracy", ["src/features.py", "src/prep.py",
     "src/train.py"], "2026-01-21T23:15:00+05:30", ()),
    ("add db and maps config for the enrichment job", ["config/secrets.env"],
     "2026-01-28T18:20:00+05:30", ()),
    ("use transit hours as a feature, accuracy jumps to 0.94!!", ["src/prep.py"],
     "2026-02-03T22:50:00+05:30", ()),
    ("courier late rate feature", ["src/features.py"], "2026-02-11T20:30:00+05:30", ()),
    ("scale numeric cols before fit", ["src/prep.py"], "2026-02-18T21:10:00+05:30", ()),
    ("revert scaling, dashboard numbers went crazy", ["src/prep.py"],
     "2026-02-20T09:05:00+05:30", ()),
    ("put scaling back, dashboard issue was elsewhere", ["src/prep.py"],
     "2026-02-24T22:40:00+05:30", ()),
    ("remove creds from repo", [".gitignore"], "2026-03-02T11:30:00+05:30",
     ("config/secrets.env",)),
    ("weekly report script for ops", ["scripts/report.py"], "2026-03-09T20:15:00+05:30", ()),
    ("serve.py for the ops dashboard", ["src/serve.py"], "2026-03-17T21:45:00+05:30", ()),
    ("readme + handover", ["README.md", "HANDOVER.md"], "2026-03-27T17:55:00+05:30", ()),
]
for i, (msg, paths, when, rm) in enumerate(HIST):
    for rel, text in STAGES.get(i, {}).items():
        (REPO / rel).write_text(text, encoding="utf-8")
    commit(msg, paths, when, rm)
(REPO / "config/secrets.env").unlink()
(REPO / "config").rmdir()

# --------------------------------------------------------------------------- the sealed truth
DEFECTS = [
    dict(id="D01", kind="correctness", file="src/features.py", symbol="promised_window_hours",
         what="Uses timedelta.seconds instead of total_seconds(). Any promise of 24 hours "
              "or more wraps: a 72-hour promise becomes 0.0 and a 60-hour promise becomes "
              "12.0. Nothing crashes; the feature is silently wrong on most rows.",
         evidence="Take one row where promised_ts - pickup_ts is 72 hours and print the "
                  "feature. It is 0.0. Count rows where the value is smaller than the true gap.",
         decision="fix"),
    dict(id="D02", kind="leakage", file="src/prep.py", symbol="NUM_COLS",
         what="actual_transit_hours and delivery_attempts are outcomes, not inputs. Both are "
              "only known after the shipment finishes. is_late is transit > promised, so "
              "actual_transit_hours plus promised_window_hours IS the label.",
         evidence="Drop both columns and retrain. The reported score collapses.",
         decision="fix"),
    dict(id="D03", kind="leakage", file="src/features.py", symbol="courier_late_rate",
         what="Target encoding computed over the whole file, so every row's feature includes "
              "that row's own outcome and the outcomes of shipments that had not happened yet.",
         evidence="For a courier with few rows the feature moves when you flip one label. "
                  "Recompute it from past rows only and the score drops.",
         decision="fix"),
    dict(id="D04", kind="split", file="src/train.py", symbol="train_test_split",
         what="shuffle=True on a file sorted by pickup time. Test rows sit between training "
              "rows in time, so the score answers a question nobody asked.",
         evidence="Sort by pickup_ts, cut the last three months as the test set, retrain. "
                  "The score falls, and it falls most on GatiRoad.",
         decision="fix"),
    dict(id="D05", kind="evaluation", file="scripts/report.py", symbol="main",
         what="Scores the model on the full shipment file, which contains every training row. "
              "The weekly 'model health' number the ops team reads is a memory test.",
         evidence="Score only rows the model never saw and compare the two numbers.",
         decision="fix"),
    dict(id="D06", kind="train-serve-drift", file="src/prep.py", symbol="scale / encode",
         what="Scaling means and standard deviations, and category codes, are recomputed from "
              "whatever frame is passed. Training uses statistics of 12,000 rows; serving uses "
              "statistics of one row, where the standard deviation is 0 and the code is 0. The "
              "same shipment gets a different probability alone than it does in a batch.",
         evidence="Call load_and_predict on one shipment, then on the same shipment inside a "
                  "batch of 200. Compare the two probabilities.",
         decision="fix"),
    dict(id="D07", kind="train-serve-drift", file="src/features.py", symbol="courier_late_rate",
         what="At serve time there is no is_late column, so every shipment receives 0.5 for "
              "the feature the handover note calls the strongest one.",
         evidence="Print the feature frame inside serve for one request.",
         decision="fix"),
    dict(id="D08", kind="portability", file="config.py", symbol="DATA_PATH / MODEL_PATH",
         what="Absolute paths on the intern's laptop. The repo cannot run anywhere else.",
         evidence="Clone into a fresh folder and run train. It fails on the first read.",
         decision="fix"),
    dict(id="D09", kind="portability", file="requirements.txt", symbol="whole file",
         what="No versions pinned, and tabulate is missing although scripts/report.py calls "
              "to_markdown() which needs it.",
         evidence="Install from requirements.txt in a clean venv and run the report.",
         decision="fix"),
    dict(id="D10", kind="tests", file="(whole repo)", symbol="tests/",
         what="No tests exist. The README tells you to run pytest tests/.",
         evidence="ls tests/",
         decision="fix"),
    dict(id="D11", kind="documentation", file="README.md", symbol="whole file",
         what="Four claims the code does not support: accuracy 0.94 on held-out data (the "
              "split is not held out and the features leak); src/serve.py --port 8000 (serve.py "
              "has no CLI and no server); scripts/report.py --week last (no argument parsing); "
              "pytest tests/ (no tests). The feature list also omits the two leaked columns.",
         evidence="Run each command in the README exactly as written.",
         decision="fix"),
    dict(id="D12", kind="secret", file="config/secrets.env", symbol="git history",
         what="A database URL with a password, a maps API key and a Slack webhook were "
              "committed, then removed in a later commit. Deleting a file does not delete it "
              "from history; every clone still carries it.",
         evidence="git log --all -p -- config/secrets.env",
         decision="fix - and the fix starts with rotating the credentials, not with git"),
    dict(id="D13", kind="performance", file="src/features.py", symbol="courier_late_rate",
         what="A nested loop over all rows for every row. On 12,000 rows that is 144 million "
              "comparisons, and it runs on every training run and every request.",
         evidence="Time the build() call. Then time a version using one pass and a dictionary.",
         decision="fix"),
    dict(id="D14", kind="performance", file="src/serve.py", symbol="load_and_predict",
         what="joblib.load runs inside the request path, so the model is read from disk for "
              "every single shipment scored.",
         evidence="Time 50 calls. Then move the load to module import and time 50 again.",
         decision="fix"),
    dict(id="D15", kind="LEAVE IT", file="src/sla.py", symbol="calc",
         what="UGLY BUT CORRECT. Four-argument function with one-letter names, magic numbers "
              "and no comments. It is also exactly right, including the monsoon exception "
              "(tier 3 only, June to September, plus 12 hours), the festival exception "
              "(standard service only, October and November, plus 24 hours) and the "
              "long-haul rule above 1400 km. The handover note says the tier 3 logic is wrong. "
              "The handover note is wrong.",
         evidence="Run calc over all 12,000 rows and compare against promised_ts - pickup_ts. "
                  "It matches on every row, including the exceptions.",
         decision="LEAVE IT. Rename nothing, rewrite nothing. Wrap it in a characterisation "
                  "test and move on. A team that rewrites this loses the two exceptions and "
                  "nobody will notice until the next monsoon."),
]
SEALED.mkdir(exist_ok=True)
(SEALED / "DO_NOT_OPEN_UNTIL_STAGE_1_IS_SUBMITTED.json").write_text(
    json.dumps(dict(
        note="Opening this before your findings register is submitted is the one way to fail "
             "stage 1 outright. It is here so you can mark yourselves afterwards.",
        planted_defects=len([d for d in DEFECTS if d["kind"] != "LEAVE IT"]),
        leave_it_traps=len([d for d in DEFECTS if d["kind"] == "LEAVE IT"]),
        defects=DEFECTS), indent=2), encoding="utf-8")

# --------------------------------------------------------------------------- the receipt
def rate(sub):
    return f"{sum(r['is_late'] for r in sub) / max(1, len(sub)):.3f}"


late = sum(r["is_late"] for r in rows)
cut = rows[int(len(rows) * 0.8)]["pickup_ts"]
early = [r for r in rows if r["pickup_ts"] < cut]
recent = [r for r in rows if r["pickup_ts"] >= cut]
wrapped = sum(1 for r in rows
              if (datetime.fromisoformat(r["promised_ts"]) - datetime.fromisoformat(r["pickup_ts"])
                  ).total_seconds() / 3600.0 >= 24)
print("shipments              :", len(rows))
print("  date range           :", rows[0]["pickup_ts"][:10], "to", rows[-1]["pickup_ts"][:10])
print("  late                 :", late, f"(rate {late / len(rows):.3f})")
print("  ALWAYS-ON-TIME acc   :", round(1 - late / len(rows), 4), "<- beat this or you have nothing")
print("  first 80% by date    :", f"n={len(early)} late {rate(early)}")
print("  last  20% by date    :", f"n={len(recent)} late {rate(recent)}  <- the honest test set")
print("  late rate by courier, first 80% -> last 20%:")
for c in COURIERS:
    print(f"    {c:<17}", rate([r for r in early if r['courier'] == c]), "->",
          rate([r for r in recent if r['courier'] == c]))
print("  promise >= 24h       :", wrapped, f"({wrapped / len(rows):.3f}) <- rows D01 silently corrupts")
print("git commits            :", subprocess.run(["git", "rev-list", "--count", "HEAD"],
      cwd=REPO, capture_output=True, text=True).stdout.strip())
print("files in the repo      :", len(FILES) - 1, "(config/secrets.env is gone from disk, not from history)")
print("planted defects        :", len([d for d in DEFECTS if d["kind"] != "LEAVE IT"]),
      "| leave-it traps:", len([d for d in DEFECTS if d["kind"] == "LEAVE IT"]))
print("repo                   :", REPO.resolve())
print("sealed answers         :", (SEALED / "DO_NOT_OPEN_UNTIL_STAGE_1_IS_SUBMITTED.json").resolve())
print("DO NOT OPEN the sealed file until your stage 1 findings register is submitted.")

On CPython 3.14.6 with no third-party packages the receipt reads: shipments 18000; date range 2024-09-01 to 2026-03-31; late 4891 (rate 0.272); ALWAYS-ON-TIME acc 0.7283; first 80% by date n=14400 late 0.267; last 20% by date n=3600 late 0.291; late rate by courier from the first 80 percent to the last 20 percent — Shreeji-Surface 0.200 -> 0.157, GatiRoad 0.325 -> 0.686, NexusExpress 0.143 -> 0.107, Bhiwandi-Direct 0.248 -> 0.195, IndiaPost-Cargo 0.424 -> 0.325; promise >= 24h 17625 (0.979); git commits 13; files in the repo 13; planted defects 14 | leave-it traps: 1. Read those lines as findings and paste them into your own README. Three of them decide this project. ALWAYS-ON-TIME acc 0.7283 is the score you get by predicting that nothing is ever late, so any accuracy near it is worth nothing on its own. The GatiRoad 0.325 -> 0.686 row is a courier that fell apart in the final months, which is why a shuffled split flatters this model and a split by pickup date does not. And promise >= 24h 17625 counts the rows that one planted bug silently corrupts, which is 97.9 percent of the file.

Two things the receipt does not tell you, and you should confirm them yourself on day one. First, the repo does not run: python -m src.train on a fresh clone dies with FileNotFoundError: /Users/ganeshambekar/Desktop/delay_project/data/shipments.csv, because the paths belong to a laptop you will never see. Second, pip install -r requirements.txt succeeds and scripts/report.py still fails, with ImportError: Import tabulate failed — the requirements file is unpinned and incomplete. Both are stage-1 findings with one-line evidence, and both are cheap to fix. Neither is the interesting part.

Artefact 2 — the acceptance criteria. The whole marking scheme for the six stages, with nothing hidden behind it. Print it and tick rows.

# P5 ACCEPTANCE CRITERIA. A stage is DONE when every row is true.

STAGE 1 - ARCHAEOLOGY. CHANGE NOTHING.
 1.1  make_inherited_repo.py run unedited; its receipt pasted into
      docs/inherited_receipt.md with your Python version.
 1.2  Not one line of delay-predictor/ source is edited in this stage.
      Prove it: git status is clean and git log shows no new commits
      on the inherited history.
 1.3  docs/code_map.md - every file, what it does, what calls it, and
      what it reads or writes. A stranger must navigate from this page.
 1.4  docs/history_notes.md - what the 13 commits tell you that the
      current code does not. Name the commit that created the fake
      score and the commit that proves credentials exist in history.
 1.5  docs/handover_audit.md - every claim in HANDOVER.md copied out
      and marked verified, refuted or unverified, with the evidence.
 1.6  docs/findings.md - the findings register, artefact 3's columns.
      Every row carries evidence a reviewer can reproduce alone.
 1.7  The ugly-but-correct file is in the register with the decision
      LEAVE IT, and the evidence that earns it.
 1.8  A per-person page: which files each member read, and one
      question each could not answer from reading alone.

STAGE 2 - THE SAFETY NET. STILL NO FIXES.
 2.1  tests/ exists with a pytest run that is green on the BROKEN code.
 2.2  A characterisation test per public function, pinning today's
      output including the wrong output.
 2.3  At least one test runs over the full 18,000 rows, not a sample.
 2.4  The ugly-but-correct function is characterised against the data
      it must agree with, and the match count is in the test name or
      its assertion message.
 2.5  A serving test that captures today's train-serve behaviour: the
      same shipment alone and inside a batch, both values recorded.
 2.6  Every test names WHICH defect it will catch, in a docstring.
 2.7  Proof the suite works: break one line on a scratch branch, paste
      the red output, discard the branch.
 2.8  A one-command entry point: make test, or a documented pytest line.

STAGE 3 - TRIAGE AND PLAN
 3.1  Every register row scored for risk (1-5) and effort (1-5), with
      one sentence of reasoning on each score.
 3.2  A ranked list, risk first and effort second, agreed by the team.
 3.3  An explicit OUT OF SCOPE list. At least two items, with reasons.
 3.4  One owner and one branch name per in-scope defect. Branch names
      carry the defect id.
 3.5  Split by FILE, not by preference. No two people own defects in
      the same file in the same round.
 3.6  docs/decisions.md open with 5+ dated entries, each naming the
      option rejected.
 3.7  A written role roster for all six stages: integrator, reviewer,
      scribe, rotating so each member does each role.
 3.8  A dated plan a lead could read in two minutes.

STAGE 4 - FIX IN SMALL SAFE STEPS
 4.1  One defect, one branch, one pull request. No exceptions.
 4.2  Every fix PR contains the fix AND the test that proves it, and
      the PR body names the characterisation test that changed.
 4.3  When a characterisation test goes red, the PR explains WHY the
      change is correct and updates the pinned value deliberately.
 4.4  The honest re-measurement table, one row per change: inherited,
      leaks removed, split by date, courier rate rebuilt from the past,
      plus the always-on-time baseline on the same rows.
 4.5  The honest score is reported with the number of test rows and
      the date cutoff beside it, in the README.
 4.6  Per-segment scores by courier, not only the average.
 4.7  The credential finding is handled in the right order: rotation
      first, history second, prevention third, all three written down.
 4.8  Every merged PR has at least one review comment that changed
      the code, from someone other than the author.

STAGE 5 - COLLABORATE UNDER LOAD
 5.1  No branch older than one working day at any point, shown by
      git log --graph in your report.
 5.2  A real merge conflict, resolved deliberately, with the
      resolution in its own commit naming what was kept and why.
 5.3  The suite is run after every conflict resolution, before commit.
 5.4  A review checklist used on every PR, pasted in the repo.
 5.5  One recorded disagreement, the experiment that settled it, and
      the result - including the case where the experiment lost.
 5.6  The role roster was actually followed, evidenced by who merged
      which PR.
 5.7  decisions.md reaches 12+ dated entries.
 5.8  Nobody merged their own pull request.

STAGE 6 - HAND OVER
 6.1  The clone test, run by somebody outside the team, on a machine
      that has never seen the project, timed, with the transcript.
 6.2  A README that is TRUE: every command in it was run, in order,
      on that clean machine.
 6.3  requirements.txt pinned with ==, and complete.
 6.4  docs/runbook.md - retrain, score, roll back, and who to call.
 6.5  HANDOVER_FROM_US.md - what we fixed, what we did not, what we
      are unsure about, and the three things we would do next.
 6.6  Every unfixed defect from the register appears in that note.
 6.7  The sealed file is opened at last, and docs/self_marking.md
      lists what you found, what you missed, and what you called
      wrongly - including anything you marked broken that was not.
 6.8  Per-person defence rehearsed against artefact 6.

Row 1.2 is the row students argue with, and it is not negotiable: a repo edited during archaeology cannot produce evidence, because every later observation is confounded by your own change. Row 2.1 reads strangely on purpose — a green suite on broken code is exactly right, since the tests pin behaviour rather than judge it. Row 3.5 exists because WR-188 is one file and two owners. Row 4.3 is the one that separates engineers from students: a red characterisation test after a fix is not a failure, it is the moment you must state, in writing, that the new behaviour is the correct one. And row 6.7 is where honesty is graded, because the register entry you got wrong teaches your batch more than the twelve you got right.

Artefact 3 — the findings register. Six columns and nothing else. This file is stage 1's deliverable and stage 3's input, and a row that cannot fill every column is not ready.

# docs/findings.md    delay-predictor, inherited 2026-04-06
# One row per finding. Evidence must be reproducible by a reviewer alone.

| id  | defect (observable behaviour) | evidence (command + what you saw) | risk 1-5 | effort 1-5 | decision |

EXAMPLE OF A ROW THAT IS ACCEPTED
| F03 | promised_window_hours returns 0.0 for a 72-hour promise, so the
        feature is wrong on nearly every row and nothing raises an error
      | python -c "from src.features import *" over row 4117: CSV holds
        pickup 2025-06-14 09:00 and promised 2025-06-17 09:00, a 72-hour
        gap; the feature returns 0.0. 17,625 of 18,000 rows are affected.
      | 4 | 1 | FIX - own branch, own test |

EXAMPLE OF A ROW THAT IS REJECTED
| F09 | prep.py is badly written and probably causes problems
      | the code looks confusing
      | 5 | 3 | rewrite the whole file |
  Why rejected: "badly written" names no behaviour, "looks confusing" is
  not reproducible, and a risk of 5 cannot be defended when no
  consequence has been stated. Rewrite it as an observation plus a
  command, or delete the row.

EXAMPLE OF THE ROW EVERY TEAM GETS WRONG
| F11 | src/sla.py calc() is unreadable: four one-letter arguments,
        magic numbers, forty lines of nested branches, no comments
      | ran calc() over all 18,000 rows and compared with
        promised_ts - pickup_ts: 18,000 of 18,000 match, including
        the June-September tier-3 rows and the October-November
        standard rows
      | 1 | 4 | LEAVE IT - characterise, document the rules it
                encodes, do not rewrite |

THE FOUR RULES OF THIS FILE
  1. Evidence is a command and an observation, never an adjective.
  2. Risk is about consequence, not about how much the code annoys you.
  3. Effort is your estimate; write it before you know the answer and
     do not edit it afterwards. Compare at stage 6.
  4. A claim from HANDOVER.md enters as UNVERIFIED and leaves as
     VERIFIED or REFUTED. Never as a defect on the author's word.

The third example is the one to read twice. Its risk is 1 and its effort is 4, so it ranks last on every sensible ordering, and yet it is the item most teams start with — because unreadable code feels urgent in a way that a wrong number does not. On this repo that instinct costs two days and a business rule. The fourth rule exists because the note you inherit contains at least one claim that is false and at least one that is true, and a team that trusts it wholesale chases a clean CSV that was never dirty.

Artefact 4 — the characterisation test, shown on one function. Not a new testing technique; module 4.5's pytest, aimed at a different question. A unit test asks "is this right?". A characterisation test asks "is this the same as yesterday?" — which is the only question you can answer about code you did not write.

# tests/test_characterise_sla.py
# WHAT THIS CATCHES: any change to src/sla.py that alters a promised
# window. It does NOT claim calc() is correct. It claims calc() agrees
# with the promised_ts already stored in the shipment file, on every row.
import pandas as pd
import pytest

from src.sla import calc

DATA = "data/shipments.csv"


@pytest.fixture(scope="module")
def rows():
    df = pd.read_csv(DATA, parse_dates=["pickup_ts", "promised_ts"])
    df["true_hours"] = (df.promised_ts - df.pickup_ts).dt.total_seconds() / 3600.0
    return df


def test_calc_matches_every_promised_window(rows):
    """Pins the SLA rule against 18,000 rows of real promises."""
    got = [calc(r.dest_tier, r.service_level, r.distance_km, r.pickup_ts.month)
           for r in rows.itertuples()]
    mismatches = rows[[g != t for g, t in zip(got, rows.true_hours)]]
    assert len(mismatches) == 0, (
        f"{len(mismatches)} of {len(rows)} rows disagree; first offenders:\n"
        f"{mismatches.head()[['shipment_id', 'dest_tier', 'service_level', 'distance_km', 'true_hours']]}")


@pytest.mark.parametrize("tier,service,km,month,hours,rule", [
    (3, "standard",  380,  7,  108, "monsoon: tier 3, Jun-Sep, +12"),
    (3, "standard",  380,  4,   96, "same lane outside the monsoon"),
    (2, "standard",  310, 10,  108, "festival: standard, Oct-Nov, +24"),
    (2, "express",   310, 10,   48, "festival rule does NOT apply to express"),
    (3, "standard", 1580,  2,  144, "long haul above 1400 km, +24"),
])
def test_the_three_exceptions_are_pinned(tier, service, km, month, hours, rule):
    """Each row is a business rule that exists nowhere else in writing."""
    assert calc(tier, service, km, month) == hours, rule

Three details are doing the work here. The first test runs over the whole file rather than a sample, because a rule that fires only in four months of the year is invisible in a random hundred rows. The parametrised test then names each exception in plain words, so the test file becomes the documentation the original author never wrote — and if somebody deletes the monsoon branch, the failure message says which business rule they deleted. The assertion prints the first offending rows instead of a bare False, which is the difference between a five-minute diagnosis and an afternoon. Write one of these for every function you intend to touch, and one for every function somebody wants to rewrite, before stage 3 starts.

Artefact 5 — the team workflow contract. Paste this into your repo as docs/team_contract.md on day one and sign it with four names. Every line below is a rule that exists because a real team lost work without it.

# TEAM CONTRACT - delay-predictor takeover

ROLES, ROTATING EACH STAGE (write the roster in decisions.md)
  INTEGRATOR  merges to main, one PR at a time, in order. Never merges
              their own PR. Announces every merge with the files it
              touched. Runs the suite on main after each merge.
  REVIEWER    reads every diff. May not approve without either running
              the branch or naming the test that convinced them.
  SCRIBE      owns decisions.md and findings.md. Writes entries the day
              the decision happens, never at the end of the week.
  Everyone codes. These three jobs sit on top of coding, and they move
  every stage so all four of you do each one at least once.

BRANCHES
  one defect -> one branch -> one PR.  fix/D02-drop-leaked-columns
  Open the PR the day you start, in draft if it is not ready.
  No branch lives longer than one working day. If it must, say so in
  standup and name the file it touches.
  git pull --rebase origin main every morning, before you touch code.

BEFORE YOU OPEN A SHARED FILE
  Say the file name out loud in the group. Thirty seconds here removes
  the merge that eats an afternoon. If it is already owned this round,
  queue behind that person or take a different defect.

THE PR RULES
  Title:  fix(D02): drop post-outcome columns from NUM_COLS
  Body:   what defect, the evidence line from findings.md, which
          characterisation tests changed and why that is correct, the
          before and after numbers with test-row counts.
  Size:   if the diff is over ~150 lines, it is two PRs.
  A PR with no test is a PR with no evidence.

REVIEW CHECKLIST - the reviewer answers all six, in writing
  1. Does this PR fix exactly one defect from the register?
  2. Is there a test that fails without the fix? Did you see it fail?
  3. Did any characterisation test change? Is the new value explained?
  4. Does it touch a file somebody else owns this round?
  5. Any number in the PR body reproducible by me from this branch?
  6. What is the blast radius if this is wrong in production?
  Comment style: name the line, state the risk as behaviour, say what
  evidence closes it. "Looks good" is not a review. Taste comments go
  in as suggestions and never block a merge.

CONFLICTS
  Resolve with the other author present. Read both sides aloud and
  decide which BEHAVIOUR is correct - not which side is yours.
  Run the suite BEFORE committing the resolution.
  The resolution is its own commit: "resolve: keep X over Y because Z".
  Never resolve a conflict two hours before a demo. Postpone the merge.

DISAGREEMENT
  1. State both positions in one sentence each, in decisions.md.
  2. Name the smallest experiment that settles it. Run it. Record it.
  3. If no experiment can settle it, the integrator decides for this
     stage and the log records who was overruled. Revisit at stage 6.
  Nobody re-argues a logged decision. That is what the log is for.

STANDUP, 10 MINUTES, SAME TIME DAILY
  What I merged. What I am in now, and which FILE. What is blocking me.
  Anyone who has not pushed in 24 hours says why, without blame.

WORKING SOLO
  You play all three roles, one at a time, in writing, and the
  artefacts are identical. Open a PR against your own repo. Leave the
  six review answers as comments on your own diff, then answer them in
  a second comment before you merge. Keep the same role roster and
  write which hat you are wearing in decisions.md. In stage 5,
  manufacture a conflict on purpose: branch twice from main, edit the
  same lines of src/features.py both times, and merge both.

Two lines in this contract carry most of its value. "No branch lives longer than one working day" removes almost every painful conflict on a repo this small, and it is a habit rather than a tool. "Never resolve a conflict two hours before a demo" exists because WR-188 is that exact hour: a tired person takes their own side on every block, the build goes green, and a reviewed fix disappears for three days. The six review answers matter too, and question 3 is the one that catches real regressions — a characterisation test that changed value without an explanation is a behaviour change nobody decided to make.

Artefact 6 — the per-person defence rubric. One hundred marks, and every member is examined alone. The team's repo cannot rescue a member who cannot explain it, and one weak member does not pull down the others.

# P5 GRADING. Total 100. TEAM 45, INDIVIDUAL 55.

TEAM MARKS - 45, from the acceptance criteria above
  Stage 1  archaeology, register, handover audit ......  10
  Stage 2  characterisation suite ....................    8
  Stage 3  triage, ranking, ownership plan ...........    5
  Stage 4  fixes, tests, honest re-measurement .......   12
  Stage 5  git workflow under load, decision log .....    5
  Stage 6  clone test, README, runbook, handover .....    5

INDIVIDUAL DEFENCE - 55, live, 15 minutes each, no notes on screen,
teammates OUT OF THE ROOM and forbidden to answer for anyone.

  I1  THE WHOLE SYSTEM (10)
      Full: 90 seconds - what you inherited, what was wrong with it,
      what you changed, what the honest number is, what is still
      broken. Ends with an offer to go deeper.
      Half: correct but over three minutes, or misses "still broken".
      Zero: describes only the part you personally wrote.
  I2  YOUR OWN COMMITS (12)
      Full: open your PRs by name, read your own diff aloud, name the
      test that proves each fix, and state what would break if your
      change were reverted.
      Half: finds the files, cannot explain the test.
      Zero: cannot locate your own commits, or another member has to
      find them for you.
  I3  SOMEBODY ELSE'S FIX (10)
      Full: explain a defect you did NOT fix - what it was, how it was
      proved, how it was fixed, and which number it moved.
      Zero: "that was their part". This is the most common failure in
      this rubric and it is the one that matters at work.
  I4  THE NUMBERS (10)
      Full: inside three seconds each - row count, base rate, the
      always-on-time baseline, the inherited score, the honest score,
      the honest ROC-AUC, recall, and the courier where it is worst.
      Half: two lookups needed.
      Zero: cannot state the honest score or the baseline.
  I5  THE TRAP (5)
      Full: name the ugly-but-correct file, state the evidence that
      earned LEAVE IT, and name the business rules a rewrite would
      have deleted.
      Zero: your team rewrote it, or cannot say why it was kept.
  I6  LIVE MODIFICATION (8)
      One change, in the team's code, while watched. Examples: put a
      leaked column back and predict the score before running it;
      delete the monsoon branch in sla.py and predict which test goes
      red; change the split cutoff by a month and predict the
      direction. Full: names the file, the function, the number that
      will move AND its direction, edits, runs, reads the result, and
      says whether the prediction held.
      Half: correct reasoning, cannot complete the edit.
      Zero: asks for time, or describes the change in general words.

THE OVERRIDE, and it is not negotiable:
  A member who cannot explain the system and cannot modify the code
  scores ZERO individually, whatever the repo looks like and however
  much they contributed on paper. Team marks are released only after
  every member has attempted I1 to I6. This is ruling 2 of the course:
  the defence IS the assessment, because generated code cannot be
  detected and an owner can always be questioned.

  A SECOND OVERRIDE, specific to this project:
  A team that reports a score produced with any post-outcome column,
  or that reports a number it cannot reproduce live from its own repo
  in one command, caps at 40 overall. The honest number is the
  deliverable. There is no partial credit for a comfortable one.

SOLO STUDENTS
  Same 100 marks, same rubric, same 15 minutes. I3 becomes "explain
  the defect you fixed FIRST, from memory, and what you would tell a
  new joiner about it". Expect harder probing on your self-review
  comments and on your manufactured merge conflict, because those are
  the two artefacts a solo route weakens.

Read I3 twice, because it is the row that fails the most students and the one that predicts most about a first job. It is comfortable to stay inside your own files, and a team that never reviews each other produces four people who each own a quarter of a system and can defend none of it. The second override exists for the same reason as row 4.3: in a real firm, a number nobody can reproduce is a number that will be reproduced in front of a client, by somebody who has the data.

Now you do it

Six stages, six graded submissions, one repo. Work in teams of three or four; if you have no team, follow the solo column of the contract in artefact 5 and play every role yourself in writing. Fork your own repository on day one — delay-predictor-takeover — and commit the inherited repo into it as your first commit, unedited, so that every later diff is against what you actually received.

Where a Check states a number that comes from the generator, your number must match exactly. Where it states a score, a band is given, because your fixes are your own. Any check you cannot meet goes into the README as a known gap with your reasoning; an honest gap costs a mark and a hidden one costs the stage.

Everything runs on an ordinary 8 GB laptop with no GPU: Python, pandas, scikit-learn and pytest, nothing else. No paid service is needed anywhere in this project and using one earns no extra marks. Total working time is 24 hours per person; the stages below are sized for a team of four working in parallel, so plan your own hours from the ownership split in stage 3.

Exercise 1230 min●●○

Stage 1 — archaeology. Understand it, prove it, and change nothing. Run make_inherited_repo.py once and paste its full receipt into docs/inherited_receipt.md with your Python version. Now try to run the thing: record what happens on a clean clone, exactly, including the error text. Then read. Map every file into docs/code_map.md: what it does, what calls it, what it reads and writes. Read the history into docs/history_notes.md — git log --oneline, git log -p -- src/prep.py, and git log --all -p -- config/secrets.env. Copy every claim from HANDOVER.md into docs/handover_audit.md and mark each one verified, refuted or unverified with the evidence beside it. Then build docs/findings.md in artefact 3's six columns. Every row needs a command a reviewer can repeat without you. Split the reading across the team by directory, then swap and re-read somebody else's directory before you agree the register. Last, make the inheritance yours — this step is mandatory. Design one new defect of your own invention, a kind the intern could plausibly have left, in a file you now understand, and plant it in your fork on its own clearly named branch — never merged before its stage-4 fix, so the inherited history stays untouched and row 1.2 still holds. Enter it in the findings register like any other row, with evidence, risk, effort and a decision, and write its true description into docs/our_planted_defect.md as your own sealed note; it must be fixed, tested and self-marked by stage 6 like everything else. The reason is blunt: every team in the batch inherits the identical repo and would otherwise report the identical honest numbers, so this defect is what makes your register, your diffs and your defence yours alone — and the panel will ask its planter to defend it. Check: git status on the inherited repo is clean and no commit has been added to its history, and you show both; your receipt matches the generator exactly on shipments 18000, late 4891, ALWAYS-ON-TIME acc 0.7283, promise >= 24h 17625 and git commits 13; your findings register holds at least twelve rows and every single one carries a reproducible command plus what you observed; docs/history_notes.md names the commit whose own message announces the accuracy jump and the commit that proves credentials are in history, by short sha; your handover audit marks at least one note claim REFUTED with evidence and at least one VERIFIED, and neither is guessed; the ugly-but-correct file appears in your register with the decision LEAVE IT and evidence showing it agrees with the promised window on all 18,000 rows, and any team that has already rewritten it, renamed its arguments or deleted a branch inside it fails this stage; your own planted defect sits in the register with a reproducible evidence command, its true description lives in docs/our_planted_defect.md, and its branch has not touched the inherited history; and each member submits one question they could not answer from reading alone.

Exercise 2210 min●●●

Stage 2 — the safety net. Pin today's behaviour, wrong behaviour included. Still no fixes. Create tests/ and write characterisation tests that go green against the broken code. One per public function you intend to touch, each with a docstring naming the defect it will catch. Run at least one test across the whole 18,000-row file rather than a sample, because the seasonal rules only fire in four months of the year. Characterise the ugly function against the data it must agree with, following artefact 4, and pin its three exceptions as named parametrised cases. Characterise the serving path as well: record the probability of one shipment scored alone and the same shipment scored inside a batch of 200, and pin both numbers as they are today. Then prove the suite works — on a scratch branch, break one line, paste the red output into docs/suite_proof.md, and discard the branch. Check: pytest is green on completely unfixed code and you state in one sentence why that is the correct result; at least eight characterisation tests exist and each docstring names the defect it guards; one test covers all 18,000 rows and its assertion message prints the first offending rows rather than a bare failure; the ugly function's three exceptions are each a named case whose failure message states the business rule in words; your serving test records two different probabilities for the identical shipment and you paste both, which is the drift defect captured before anyone fixes it; docs/suite_proof.md shows a real red run for a deliberately broken line, with the test name that caught it; and the whole suite runs from one documented command in under two minutes.

Exercise 3150 min●●○

Stage 3 — triage and plan, out loud, together. Score every register row for risk 1-5 and effort 1-5, with a sentence of reasoning behind each score, then rank risk first and effort second. Argue this in one sitting with the whole team present. Produce an explicit out-of-scope list with reasons — you will not fix everything in 24 hours and pretending otherwise is how stage 4 collapses. Assign exactly one owner and one branch name per in-scope defect, splitting by file so that no two people own defects in the same file in the same round. Write the six-stage role roster: integrator, reviewer, scribe, rotating so every member does each job. Open docs/decisions.md and start it properly, one entry per decision, dated, naming the option you rejected. Check: every register row carries a risk score, an effort score and a sentence for each, and the ugly-but-correct row ranks in the bottom third with LEAVE IT recorded; your out-of-scope list has at least two items with reasons a lead would accept; the ownership table has one name and one branch name per defect, and no file has two owners in the same round — show the file-to-owner mapping as evidence; the role roster covers all six stages and gives every member each role at least once; decisions.md has at least five dated entries and each names the rejected option; your plan states which defect is being fixed first and why risk ranked it there, in one sentence; and every member can state, without looking, which files they own this round and which they must not touch.

Exercise 4300 min●●●

Stage 4 — fix in small safe steps, and re-measure honestly. One defect, one branch, one pull request, every time. Each fix PR carries the fix and a test that fails without it, and the body names any characterisation test whose pinned value changed and explains why the new value is correct. Fix in risk order. When you reach the leakage and the split, rebuild the evaluation properly: drop the post-outcome columns, sort by pickup date, hold out the last 20 percent, and rebuild the courier rate from past rows only. Then build the honest re-measurement table with one row per change and the always-on-time baseline on the same rows. Break the score down by courier as well as overall. Handle the credential finding in the right order — rotation first, history second, prevention third — and write all three steps down. Check: every merged PR fixes exactly one defect, contains a test, and carries at least one review comment from another member that changed the code; your honest table has a row for the inherited pipeline, for leaked columns removed, for the split by pickup date, for the courier rate rebuilt from the past, and for the always-on-time baseline, all on the same test rows with the row count stated; your honest score after the leakage and the split are corrected lands near accuracy 0.73 and ROC-AUC 0.75, against a baseline of 0.7094 — and you report the fall as the result, not as a problem, in one written paragraph a manager could read; your per-courier table shows the courier whose accuracy collapses and you state its true late rate in the test window; you report recall beside accuracy and say in one sentence what a recall near 0.10 means for the exceptions desk; the credential steps are documented in order with the rotation dated and named; and no reported number anywhere in your repo was produced with a post-outcome column, proven by a test that fails if one re-enters the feature list.

Exercise 5200 min●●●

Stage 5 — collaborate under load, and survive it. Run the workflow contract for real while the fixes are still landing. Keep every branch under one working day. Use the six-question review checklist on every pull request, in writing, and let nobody merge their own work. Produce a genuine merge conflict — two people editing src/features.py in the same round will do it, and solo students manufacture one deliberately — and resolve it with both authors present, running the suite before the resolution is committed. Record one real disagreement, the smallest experiment that would settle it, the result of running that experiment, and what you did when the result went against somebody. Keep decisions.md current on the day. Check: git log --graph --oneline shows short-lived branches merging steadily rather than three long-running branches landing at the end, and you paste it; every PR carries written answers to all six checklist questions from someone other than the author; the merge conflict resolution is its own commit whose message says what was kept and why, and you show the suite passing between the resolution and the commit; you name one review comment that changed code and one you decided to let go, with the reason for each; your recorded disagreement includes the experiment, its result and the decision, and it is honest about who was wrong; the role roster was followed, proven by which member merged which PR; decisions.md has at least twelve dated entries; and no member merged their own pull request at any point.

Exercise 6150 min●●○

Stage 6 — hand it over better than you found it, then defend it alone. Run the clone test properly: give your repo to somebody outside the team, on a machine that has never seen it, with a timer, and write down every place they stopped. Fix those places, then rewrite the README so that every command in it was actually run in order on that clean machine. Pin requirements.txt with == and make it complete. Write docs/runbook.md — retrain, score, roll back, who to call. Write HANDOVER_FROM_US.md: what you fixed, what you did not, what you are unsure about, and the three things you would do next. Only now open the sealed file, and write docs/self_marking.md honestly. Then mine the project for Phase 6: three resume lines that each carry a number, five viva answers for module 6.4, and a 90-second walkthrough for module 6.6. Check: the clone test was run by a named outsider, is timed, and the transcript shows what they hit; your README's commands all work on a clean machine and you say who verified that; requirements.txt is fully pinned and installs cleanly in a fresh virtual environment; HANDOVER_FROM_US.md lists every defect you did not fix, with your reason, and a reader can act on it without you; docs/self_marking.md states how many of the planted defects you found, which you missed, and anything you called broken that was not — and a team that marked the ugly-but-correct file as a defect says so plainly; each member gives the 90-second walkthrough from memory and can state the honest score, the baseline and the worst courier inside three seconds; each member completes one live modification, naming the file, the function and the direction of the number before editing; and your three resume lines each contain a figure you can defend for two minutes.

Working alone with no outsider to hand? Use the solo substitute defined in P1: run the clone test yourself in a clean container or a brand-new OS user account, follow your own README verbatim with the timer running, and record where you stopped — that transcript stands in for the outsider's.

When it breaks

Three failures that end inherited-code projects, all of them reachable from the stages above. Each has a War Room entry with the full diagnosis path:

  • WR-187 — the rewrite that quietly lost a business rule. A team rewrote an unreadable module they had never tested, the new version was cleaner and shorter, and it silently dropped a seasonal rule that lived nowhere else; the fix is a characterisation test over real data before any rewrite, a rules table mined from that test, and the discipline of separating "ugly" from "wrong".
  • WR-188 — the merge that deleted a teammate's week. Two people worked five days on separate branches touching one file, the second merge produced a conflict resolved under demo pressure by taking one whole side, and a reviewed fix vanished; the fix is short branches, a rotating integrator, talking before you open a shared file, and treating a conflict resolution as code that needs review.
  • WR-189 — the honest score that looked like failure. Removing the leakage dropped accuracy from 0.9864 to 0.7278 and a member wanted to restore the column for one slide; the fix is a before-and-after table with a baseline beside it, a guard test so the leak cannot return, and a way of telling a manager the drop that leads with the number instead of burying it.
Recap in 5 lines

This project hands you somebody else's half-working delivery-delay model, thirteen commits of history, and a handover note that is partly wrong. The first rule is that nothing gets called broken until you can show it breaking, so stage one produces a findings register and changes no code at all. Characterisation tests come next, because without them you cannot tell a fix from a regression, and one file in this repo is unreadable and completely correct. The reported accuracy of 0.9864 rests on two columns recorded after delivery, and the honest re-measurement is 0.7278 against a do-nothing baseline of 0.7094. Work in short branches with a rotating integrator, defend your own commits in person, and hand over a repo a stranger can clone and run.

Where this is used for real
  • IT services firms (TCS, Infosys, Wipro, Cognizant, LTIMindtree) earn much of their revenue from maintaining systems other people built. A fresher's first posting is usually a codebase older than they are, and the work is exactly this: read it, test it, change it without breaking it.
  • Logistics and e-commerce companies across India run delay, routing and refund models that were built fast during a growth year and inherited later. Couriers change, lanes change, and a model trained on last year is quietly wrong in a way no error message reports.
  • Product startups after a funding round or a resignation hand over half-finished pipelines with no tests and a README nobody updated. The person who can take that over calmly, prove what is wrong, and hand back something a stranger can run is the person who gets kept.
Check yourself

Answer these aloud before opening the gate assessment. Each question names its kind.

  1. type=interpretation — Your teammate reports that a function returns 0.0 for a 72-hour promised window and 12.0 for an 84-hour one, while the CSV holds the correct timestamps in both cases. Say what the pattern in those two numbers tells you about the code, and name the one-word change that fixes it.
  2. type=scenario-judgement — On day one a member proposes rewriting the unreadable SLA file, quoting the handover note that says its tier logic is wrong. Give your ruling, the single piece of evidence you would demand before anybody edits that file, and the register decision you would write in each outcome.
  3. type=debug — After a teammate speeds up a feature by replacing a nested loop with one pass, the reported accuracy falls by three points and two characterisation tests go red. Give the two most likely causes, name the files you open first, and give the one check that settles which it was.
  4. type=trade-off — With eight hours left, compare fixing the train-serve drift against tuning the model to lift recall. Give one concrete gain and one concrete cost of each, and name the number from your own runs that decides it.
  5. type=output-prediction — A student restores actual_transit_hours to the feature list and reruns the honest evaluation with the time-ordered split unchanged. Predict what happens to accuracy, to ROC-AUC and to recall, and say which of the three would make a reviewer suspicious first.
Explain it to a friend

Explain to a friend who does not code why a team that inherits somebody else's work should write tests that lock in the current wrong behaviour before fixing anything. Use an everyday comparison, such as photographing a rented flat on the day you move in, so that later you can prove which marks on the wall were yours and which were already there. Cover three things: that you cannot tell a repair from new damage without a record of the starting state, that the record has to be made before you touch anything, and that some of what looks damaged turns out to have been deliberate. Use at most 4 sentences and no word your friend would need to look up.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in