4.4 I. ellenőrző pont: megismételhető kutatói munkaasztal

4.4.1 Helyzet és cél: a munkaasztal mint átadott termék

Egy kolléga a Northstar tanulóprojektet adja át neked: a lab/ mappát, benne a determinisztikus adatgenerátorral, a kezdő elemző notebookkal, a src/northstar modullal és a manifesttel. A te feladatod tiszta mappában rekonstruálni a környezetet, legenerálni vagy ellenőrizni az adatot, lefuttatni a notebookot, és a futást visszakereshető jegyzékben dokumentálni. A kolléga egyetlen belépő parancsot kér: aki klónozza a repót, annak ne kelljen fejben tartania a lépések sorrendjét.

A megfigyelési egység itt nem ügyfél és nem rendelés, hanem egy ellenőrző futás egy adott gépen. A bemenet a projektfájl-készlet és a gép környezete; a kimenet a futási jegyzék, az újra futtatott notebook és a kontrollösszeg-eredmény. Az előzetes eredménybecslés: ha az átadott repo konzisztens, akkor a manifestben rögzített hashvizsgálat failures=0 értékkel zár, a quickcheck a szerződésben rögzített sorszámokat adja vissza (300 ügyfél, 2400 rendelés, 4796 tételsor az alaps seed esetén), és a git-munkaállapot tiszta marad. A tényleges hashértékeket nem találgathatjuk előre; azokat a labor manifest.json fájlja tartalmazza.

4.4.2 Fogalmi alapok: hat réteg, egyetlen belépő paranccsal

A reprodukálható munkaasztal hat rétegből áll, és a belépő szkript feladata az, hogy ezeket sorban, hiba esetén korán álljon meg.

4.4.2.1 Projektgyökér: a parancsok vonatkoztatási pontja

A projektgyökér az a mappa, ahol a pyproject.toml található. Minden relatív útvonal innen értendő: scripts/entrypoint.sh, src/northstar, manifest.json. Ha más munkakönyvtárból indítod a parancsot, akkor nem a szkript hibás, hanem a hívási kontextus.

A belépő szkript ezért az első soraiban explicit horgonyt ellenőriz:

if [[ ! -f pyproject.toml ]]; then
  echo "Run this script from the lab project root." >&2
  exit 2
fi

A szabály: egy parancs akkor hordozható, ha a meghívás helyét a projektgyökér rögzíti, és a hívó nem támaszkodik rejtett, gépenként eltérő környezeti változóra.

4.4.2.2 Interpreter és függőségek: uv-run mint egyetlen Python-bejárat

A labor nem feltételez előre feloldott uv.lock fájlt. Az első tiszta futtatáskor a felbontást el kell végezni, utána viszont a lock már rögzített állapotot jelent. A uv projekt-sync dokumentációjának szerepe szerint a --locked kapcsoló azt ellenőrzi, hogy a lock és a pyproject.toml összhangban van-e; a --frozen ennél szigorúbb, és aktualitásvizsgálatot sem végez.

A belépő szkript logikája:

  1. ha nincs uv.lock, készítsd el;
  2. uv sync --locked;
  3. minden további Python-parancs uv run-on keresztül fusson, hogy a projekt .venv-je legyen az interpreter.

A lokális python3 parancs nem azonos a projekt interpreterével. Az átadás során mindig a projekt .venv/bin/python (Windowson .venv\Scripts\python.exe) az irányadó.

4.4.2.3 Kernel: a notebook is egy Python-folyamat

A 3.4 alfejezetben már láttuk, hogy a Jupyter-kernel egy külön folyamat, amelynek argv[0] eleme a Python-útvonal. A notebook akkor reprodukálható, ha ez az útvonal pontosan a projekt .venv-ére mutat. A belépő szkript ezért regisztrál egy kernelspecet, és az nbconvert végrehajtásnál ezt a kernelnevet adja meg:

uv run python -m ipykernel install --user \
  --name northstar-clean-handoff \
  --display-name "Northstar clean handoff"

A regisztráció önmagában nem futtat semmit; a notebook végrehajtása csak ezután következhet. A kernelválasztás és a Restart and Run All a JupyterLab felületén is ugyanezt a kernelt kell, hogy használja.

4.4.2.4 Git: a változás nyoma, nem a futási bizonyíték

A Git a forráskód, a notebook és a szkriptek verzióját őrzi. A .venv/ és a run/ mappa a .gitignore-ba tartozik, mert gépfüggő vagy újragenerálható állapot. A belépő szkript a végén git status --porcelain=v1 kimenetet rögzít, hogy a fogadó gép naplójában látható legyen: a futtatás nem hagyott-e váratlan módosítást a követett fájlokban.

A porcelain=v1 formátum azért hasznos, mert stabil, scriptből elemezhető kimenetet ad. A naplóba kerülő Git-állapot egy ellenőrző jel, nem a tudományos eredmény.

4.4.2.5 Futási jegyzék: kereshető, nem képernyőkép

A futási jegyzék nem más, mint a parancsok és kimeneteik időbélyegzett naplója. A minimálisan érdemes mezők:

Mező Példa Mit ellenőriz?
run_id 20261020T083011Z egyedi futtatás
python_executable .../lab/.venv/bin/python projekt interpreter
kernel_name northstar-clean-handoff notebook kernel
hash_result checked=6 failures=0 fájl integritás

A naplót a run/ mappába írjuk, a belépő szkript pedig tee-vel egyszerre mutatja és rögzíti a kimenetet. A jegyzéket akkor lehet hatékonyan használni, ha egy óra múlva grep-pelhető, nem akkor, ha egy képernyőképen zöld pipa látszik.

4.4.2.6 Kontrollösszeg és a matematikai híd

A kontrollösszeg itt SHA-256 üzenetkivonat (hash). Egy fájl bájtsorozatához rendelünk egy 64 hexadecimális jegyből álló szöveget:

H=SHA256(f1,f2,…,fN), H = \mathrm{SHA256}(f_1, f_2, \dots, f_N),

ahol fif_i a fájl ii-edik bájtja, NN a fájl mérete bájtban, HH pedig a kimeneti kivonat. A kimenet 64 hexa jegy, tehát 64/2=3264/2 = 32 bájt, azaz 32⋅8=25632 \cdot 8 = 256 bit. Egy hexa jegy négy bitet kódol: például 0xF=11112=15100xF = 1111_2 = 15_{10}, egy bájt pedig két hexa jegyből áll, 0x000x00-tól 0xFF0xFF-ig.

A hash nem „összeg” a hétköznapi értelemben. Egy egyszerű bájtsorrend-összegnél az 1,21,2 és a 2,12,1 sorozat azonos eredményt adhat, így a sorrendhiba láthatatlan marad. A SHA-256 konstrukciójának célja éppen az, hogy egyetlen bájt megváltozása is előrejelezhetetlenül más kimenetet adjon. Ez nem abszolút matematikai garancia az ütközésmentességre; a mi kontrollünkben az integritásvizsgálat egy manifesthez rögzített referenciaértékhez hasonlít, és a manifest maga is megbízhatósági kérdés.

4.4.3 Teljes minimális átadási munkamenet

Az itteni futási napló minden idő- és verzióadata szemléltető, nem a te gépeden mért kimenet. A tényleges futtatást a tanulói labor végzi; a hashértékeket kizárólag a labor manifestje tartalmazza.

A labor gyökerében az alábbi szerkezetet feltételezzük a 4.3 átadásából:

lab/
├── pyproject.toml
├── src/northstar/metrics.py
├── notebooks/explore.ipynb
├── generate_data.py
├── quickcheck.py
├── manifest.json
├── scripts/
│   ├── entrypoint.sh
│   └── verify_hashes.py
└── run/

A verify_hashes.py egyetlen feladatot lát el: a manifestben rögzített elvárt SHA-256 értékeket összehasonlítja a tényleges fájlokkal.

"""Verify files against the SHA-256 hashes recorded in manifest.json.

An equal digest means the bytes are almost certainly identical, but the
statement is only as trustworthy as the manifest itself.
"""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path

CHUNK = 1024 * 1024


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        while chunk := handle.read(CHUNK):
            digest.update(chunk)
    return digest.hexdigest()


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--manifest", type=Path, default=Path("manifest.json"))
    parser.add_argument("--root", type=Path, default=Path("."))
    args = parser.parse_args()

    payload = json.loads(args.manifest.read_text(encoding="utf-8"))
    expected = payload.get("files")
    if not isinstance(expected, dict):
        raise ValueError("manifest requires a 'files' object")

    failures = 0
    checked = 0
    for relative, expected_digest in expected.items():
        target = (args.root / relative).resolve()
        checked += 1
        if not target.exists():
            print(f"{relative}: MISSING")
            failures += 1
            continue
        actual_digest = sha256(target)
        if actual_digest != expected_digest:
            print(f"{relative}: MISMATCH")
            failures += 1
            continue
        print(f"{relative}: OK")
    print(f"checked={checked} failures={failures}")
    return 0 if failures == 0 else 1


if __name__ == "__main__":
    raise SystemExit(main())

A files kulcs egy lehetséges manifestszerződés; ha a labor manifest.json szerkezete más kulcsneveket használ, a hívó oldalon ahhoz kell igazítani. A hash-eredmény összehasonlítása pontos egyezést kér: nincs numerikus tolerancia, mert bájtszintű integritásról van szó.

A belépő szkript Linux/macOS bash környezetre:

#!/usr/bin/env bash
# One-command handoff check for the Northstar lab.
# Run from the project root: bash scripts/entrypoint.sh
set -euo pipefail

if [[ ! -f pyproject.toml ]]; then
  echo "Run this script from the lab project root." >&2
  exit 2
fi

RUN_DIR="run"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
LOG="${RUN_DIR}/handoff-${STAMP}.log"
mkdir -p "${RUN_DIR}"

log() { echo "$(date -u +%H:%M:%SZ) $*" | tee -a "${LOG}"; }

log "python outside project: $(command -v python3 || echo missing)"
log "uv: $(command -v uv || echo missing)"

if [[ ! -f uv.lock ]]; then
  uv lock | tee -a "${LOG}"
fi
uv sync --locked | tee -a "${LOG}"

uv run python -m ipykernel install --user \
  --name northstar-clean-handoff \
  --display-name "Northstar clean handoff" | tee -a "${LOG}"

uv run python generate_data.py | tee -a "${LOG}"
uv run python scripts/verify_hashes.py --manifest manifest.json | tee -a "${LOG}"

uv run python quickcheck.py | tee -a "${LOG}"
uv run python -m northstar.metrics --help | tee -a "${LOG}"

uv run jupyter nbconvert --to notebook --execute notebooks/explore.ipynb \
  --output explore.executed.ipynb \
  --output-dir run \
  --ExecutePreprocessor.kernel_name=northstar-clean-handoff \
  --ExecutePreprocessor.timeout=120 | tee -a "${LOG}"

uv run python - <<'PY' | tee -a "${LOG}"
import platform, sys
import numpy, pandas

print("python", sys.version.split()[0])
print("executable", sys.executable)
print("platform", platform.platform())
print("numpy", numpy.__version__)
print("pandas", pandas.__version__)
PY

if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  git status --porcelain=v1 >> "${LOG}"
fi

log "HANDOFF OK"

A parancs a projektgyökérből:

bash scripts/entrypoint.sh

A várható belépési folyamat tehát egyetlen sor a fogadó oldalon. A szkripten belül minden lépés külön fut, és a set -euo pipefail miatt az első hibánál megáll. A --ExecutePreprocessor.kernel_name explicit kernelválasztása azért fontos, mert különben az nbconvert a notebook metaadatában tárolt régi kernelt használhatná.

4.4.4 Eredmény értelmezése

A sikeres futás után a napló szerkezete ilyen lehet; a konkrét idők, útvonalak és verziók a laborban futás közben derülnek ki:

2026-10-20T08:30:11Z python outside project: /usr/bin/python3
2026-10-20T08:30:11Z uv: /home/ada/.local/bin/uv
2026-10-20T08:31:02Z checked=6 failures=0
2026-10-20T08:31:14Z converted notebooks/explore.ipynb to run/explore.executed.ipynb
2026-10-20T08:31:20Z executable: /home/ada/work/lab/.venv/bin/python
2026-10-20T08:31:21Z HANDOFF OK

A failures=0 azt jelenti, hogy a hat ellenőrzött CSV bájtjai egyeznek a manifestben rögzített elvárt hash-ekkel. Ez nem jelenti automatikusan, hogy a notebook tartalma lefutott; a notebook végrehajtását az nbconvert sora és a létrejött run/explore.executed.ipynb igazolja. A python outside project sorban a python3 lehet a rendszerinterpreter, de a fontos sor a uv run által kiírt executable: annak a .venv/bin/python útvonalra kell mutatnia.

A napló utolsó git status --porcelain blokkja akkor jó, ha üres. Ha a generálás módosítaná a követett CSV-ket, az azonnal látszana; ebben az esetben vagy a generátor viselkedését, vagy a fájlok követésének döntését kell felülvizsgálni. A hash-ellenőrzés és a Git-állapot két különböző réteg: az egyik a bájtok, a másik a verziózott források állapota.

Értékelési rubrika (20 pont). Környezet és interpreter azonosítása: 3 pont; forrásadat és hash/fájlméret rögzítése: 3 pont; megfigyelési egység és join-invariáns: 4 pont; friss kernelben teljes futás: 4 pont; értelmezhető hibajegyzék és javítás: 3 pont; átadható napló és README: 3 pont. A rubrika a készségre ad visszajelzést, nem csak a »lefutott« állapotra.

Kritikus kizáró hiba: előre nem ismert információ beemelése a predikcióba, többszörözött megfigyelési egység vagy az ellenőrzés hamis sikernek jelentése esetén a megoldás nem fogadható el teljesítettnek, még ha a részpontszám magas is. A javítás után új, tiszta futás szükséges.

4.4.5 Feladatok

  1. Értelmezés. A társ a következő naplórészletet küldi: checked=6 failures=0 és HANDOFF OK. Azt állítja, hogy a notebook is biztosan sikerült, mert a hash-ellenőrzés zöld. Miért hibás a következtetés? Nevezd meg, melyik naplósor vagy fájl bizonyítaná a notebook végrehajtását, és mit nem bizonyít a hash.

  2. Megvalósítás. A labor scripts/verify_hashes.py fájlja hiányzik. Írd meg a fenti szerződés szerint, majd teszteld egy háromsoros mini manifesten: sample.csv elvárt hash-ét a tényleges fájlból számold ki, futtasd le, változtass egyetlen bájtot, és futtasd újra. Rögzítsd mindkét kimenetet és a kilépési kódot.

  3. Hibadiagnózis. A társ gépen a uv run python -c "import northstar; print('ok')" parancs sikeres, de a notebook első cellája ModuleNotFoundError: No module named 'northstar' hibát ad. A notebookban a import sys; print(sys.executable) sor /usr/bin/python3 értéket ír ki. Add meg a hiba valószínű okát, a kimutatásához szükséges parancsot, és a javítási utat a kernelspec szintjén.

  4. Megváltozott feltétel. A fogadó gép Windowson fut, bash nem elérhető. Írd át a belépő munkamenet lényegi lépéseit PowerShellre a projektgyökérből, beleértve a lock, a sync, a hash-ellenőrzés és a Get-FileHash használatát. Nevezd meg, milyen bájtszintű kockázatot jelenthet a sorelválasztó eltérése, ha a generátor platformfüggő sortörést írna.

  5. Megváltozott kontraktus. A quickcheck.py egy új sémaszabályt ad hozzá: minden order_items sorra igaznak kell lennie a line_total_minor = quantity * unit_price_minor - discount_minor egyenlőségnek. A hash-ellenőrzés failures=0, de a quickcheck hibát jelez. Mi a diagnózis iránya, és miért nem mond ellent a két eredmény?

4.4.6 Megoldások és indoklás

  1. A hash-ellenőrzés a CSV-fájlok bájtjait hasonlítja a manifesthez. A notebook végrehajtását az nbconvert naplósora és a generált run/explore.executed.ipynb igazolja. A failures=0 tehát szükséges, de nem elégséges feltétele a teljes átadási célnak; a környezet, a kernel és a kód futtatása önálló bizonyítékot igényel.

  2. A fenti verify_hashes.py kód közvetlenül használható. A mini teszt menete Linuxon:

printf 'alpha\n' > sample.csv
sha256sum sample.csv
# a kapott 64 hexa karaktert írd a mini-manifest.json "files" objektumába
uv run python scripts/verify_hashes.py --manifest mini-manifest.json
# checked=1 failures=0
printf 'beta\n' > sample.csv
uv run python scripts/verify_hashes.py --manifest mini-manifest.json
# checked=1 failures=1, exit code 1

Az ellenőrző invariáns: azonos bemeneti bájtokhoz azonos digest, eltérő bájthoz a szkript failures=1 és nem nulla kilépési kód. A hashnél tűrés nincs.

  1. A tünet arra mutat, hogy a notebook a rendszer Pythonjához regisztrált kernelt használja, miközben a projekt környezete a .venv. A diagnózis:
uv run python -c "import sys; print(sys.executable)"

Ha ez .../lab/.venv/bin/python, a notebookban viszont /usr/bin/python3, akkor a kernelspec rossz. A javítás a projektkörnyezetből regisztrált kernel:

uv run python -m ipykernel install --user \
  --name northstar-lab \
  --display-name "Northstar lab"

Ezután a JupyterLabban a Kernel > Change Kernel menüben a Northstar lab kernelt válaszd, majd futtasd a Restart and Run All parancsot. Az ismételt sys.executable kiírásnak a .venv/bin/python útvonallal kell megegyeznie. A pontos útvonal gépfüggő, az invariáns a két parancs kimenetének egyezése.

  1. PowerShell-változat a projektgyökérből:
Set-Location C:\work\northstar-lab
if (-not (Test-Path uv.lock)) { uv lock }
uv sync --locked
uv run python scripts\verify_hashes.py --manifest manifest.json
Get-FileHash customers.csv -Algorithm SHA256
uv run python -m ipykernel install --user `
  --name northstar-lab `
  --display-name "Northstar lab"

A projekt interpreter itt C:\work\northstar-lab\.venv\Scripts\python.exe, tehát a kernel bejegyzés argv[0] eleme más elérési útvonal, mint Linuxon. A sorelválasztó kockázata: ha a generátor szöveges módban ír, Windowson \r\n, Linuxon \n kerülhet a fájlba, így a sorok logikailag azonosak, de a hash eltér, mert a hash a bájtokra vonatkozik. A bitazonossághoz a generátornak platformfüggetlen újsor-konvenciót kell írnia, vagy a manifestnek külön szerződnie kell a sortörésről.

  1. A hash-ellenőrzés a fájl és a manifest közötti bájtegyezést igazolja; a quickcheck a szemantikai sémakontraktust. A hiba iránya: a manifest hash-réteg hibátlanul frissült, de az új üzleti invariáns vagy a generátor logikája, vagy a quickcheck elvárása hibás. A két eredmény nem mond ellent egymásnak, mert különböző réteget vizsgál. Az adatfájl bájtjai épek lehetnek, miközben a bennük lévő sorok egy előírt egyenlőséget sértenek; ilyenkor a generálási logikát és a szerződést kell felülvizsgálni.

4.4.7 Gyakori tévedés ellenpéldával

A leggyakoribb hiba a képernyőkép mint „bizonyíték”. A társ küld egy felvételt a zöld pipáról, de utólag senki sem tudja megmondani, melyik kernel futott, mi volt a Python-útvonal, vagy hány hash-ellenőrzés ment át. Ha viszont a run/handoff-*.log szövegfájlt küldi, a grep, a diff és a kolléga is pontosan követni tudja a lépéseket.

Ugyanilyen félreértés a hash túlértékelése. A failures=0 nem jelenti, hogy a manifest helyes; ha valaki a manifestet hibásan frissítette, a rossz elvárt értékhez is lehet tökéletes egyezés. Ezért a hash-ellenőrzés mellé mindig kell legalább egy független sémakontroll, például a quickcheck.py invariánsai. A hash az integritás egy rétegét, nem a tudományos helyességet igazolja.

4.4.8 Önellenőrzés

Futtatás után kérdezd végig a következőket:

Ha bármelyik kérdésre nemleges a válasz, a futási jegyzékből azonosítani kell a réteget, és csak azt javítani; a következő futtatást újraindított, tiszta kernelrel kell elvégezni.

4.4.9 Rövid összefoglaló

A megismételhető kutatói munkaasztal nem egyetlen notebook, hanem hat együttműködő réteg: projektgyökér, project interpreter, regisztrált kernel, Git-állapot, futási jegyzék és kontrollösszeg-manifest. A belépő szkript a lock hiányát első futtatáskor feloldja, --locked módban szinkronizál, a notebookot a projekt .venv-hez regisztrált kernellel futtatja, a hash-ellenőrzést és a sémakontrollt elkülönítetten rögzíti. A failures=0 a fájlok integritását igazolja, de nem helyettesíti a notebook-végrehajtás bizonyítékát. A következő fejezetben NumPy-tömbökkel dolgozunk: shape, tengely és dtype válik a numerikus munka új ellenőrző szerződésévé.