WolfDavid's picture
feat(05-05): Reality Anchor pipeline + tests (D-ANCHOR-04 launch gate)
32f9152
Raw
History Blame Contribute Delete
4.53 kB
"""Phase 5 plan 05-05: interactive label confirmation → reality_anchor.parquet.
Reads JSONL from ``export_dogfood.py``; for each row, surfaces the v1.0.0
classifier's predicted ``top_class`` and asks the owner to confirm or correct.
Writes a Parquet file matching the ``data/eval.parquet`` column schema PLUS a
``consent_level`` column (D-ANCHOR-03).
Reality Anchor parquet column schema (mirrors eval.parquet + consent_level):
ts : double
predicted_class : string
true_class : string
consent_level : string
schema_version : string
telemetry_json : string (the full opted-in telemetry window)
verdict_json : string (the v1.0.0 verdict at diagnosis time)
The columns intentionally do NOT mirror eval.parquet's per-feature columns
(``rssi_dbm``, ``bssid``, etc) verbatim — eval.parquet stores one row per
telemetry frame, whereas the Reality Anchor stores one row per *diagnosis*
(a whole window of frames). The eval-pipeline schema verified at task-1 step 1
informs the auxiliary lints; the Reality-Anchor file is the diagnosis-grained
projection ``eval_reality_anchor.py`` needs.
Exit codes:
- 0 — parquet written
- 2 — input JSONL missing OR empty (no rows to label)
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import pyarrow as pa
import pyarrow.parquet as pq
# Canonical class slug list — mirrors model.features.CLASSES (Phase 2 D-CAL-08).
# Kept in lockstep with eval_reality_anchor.CLASSES.
CLASSES = [
"auth_8021x_eap_fail",
"ap_roam_rekey_fail",
"radius_timeout",
"captive_portal_expiry",
"mac_randomization_reject",
"dhcp_lease_churn",
"dns_resolver_fail",
"driver_power_save_wake",
"rf_sticky_client",
"isp_upstream_fail",
]
def _prompt_label(predicted: str, headline: str) -> str:
print(f"\n predicted: {predicted}")
print(f" narrator: {headline[:120]}")
ans = input(" accept predicted? [Y/n/? to list classes]: ").strip().lower()
if ans in ("?", "list", "show"):
for i, c in enumerate(CLASSES):
print(f" [{i}] {c}")
idx = input(" enter class index: ").strip()
return CLASSES[int(idx)]
if ans in ("", "y", "yes"):
return predicted
print(" classes:")
for i, c in enumerate(CLASSES):
print(f" [{i}] {c}")
idx = input(" enter class index: ").strip()
return CLASSES[int(idx)]
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(
description="Label opted-in real diagnoses → data/reality_anchor.parquet"
)
ap.add_argument("--in", dest="inp", required=True, type=Path,
help="Input JSONL from export_dogfood.py")
ap.add_argument("--out", required=True, type=Path,
help="Output Parquet path (e.g. data/reality_anchor.parquet)")
ap.add_argument("--non-interactive", action="store_true",
help="Accept every predicted top_class as the label (CI smoke).")
args = ap.parse_args(argv)
if not args.inp.exists():
print(f"input JSONL not found: {args.inp}", file=sys.stderr)
return 2
rows: list[dict] = []
with args.inp.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
raw = json.loads(line)
v = json.loads(raw["verdict_json"])
predicted = v.get("top_class", "")
headline = v.get("headline", "")
if args.non_interactive:
label = predicted
else:
label = _prompt_label(predicted, headline)
rows.append(
{
"ts": float(raw["ts"]),
"predicted_class": predicted,
"true_class": label,
"consent_level": raw["consent_level"],
"schema_version": raw["schema_version"],
"telemetry_json": raw["telemetry_json"],
"verdict_json": raw["verdict_json"],
}
)
if not rows:
print("no rows to label (input JSONL was empty)", file=sys.stderr)
return 2
table = pa.Table.from_pylist(rows)
args.out.parent.mkdir(parents=True, exist_ok=True)
pq.write_table(table, args.out)
print(f"wrote {len(rows)} rows to {args.out}")
return 0
if __name__ == "__main__":
sys.exit(main())