fix(rectification): anchor candidate windows to civil dates across midnight
Carry explicit local date intervals instead of inferring the day from clock order. Cluster width, delivery, adoption, and reports keep the actual civil date; adopted date is stored separately from the reported birth_date. Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation thresholds, and Skill version are unchanged. Isolated Linux final-3 gates passed; four pre-existing Python failures remain. This is not a production release.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent-process native A/B and dated-window goldens; not an accuracy benchmark."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def load(root: Path):
|
||||
for name in list(sys.modules):
|
||||
if name == "scripts" or name.startswith("scripts."):
|
||||
del sys.modules[name]
|
||||
sys.path.insert(0, str(root))
|
||||
importlib.invalidate_caches()
|
||||
from scripts.rectification.api_service import score_candidates
|
||||
from scripts.research.probe_supply_after_six import request_from_case
|
||||
from scripts.rectification.decision_policy import indistinguishable_width_minutes
|
||||
return score_candidates, request_from_case, indistinguishable_width_minutes
|
||||
|
||||
|
||||
def canonical(value):
|
||||
return json.dumps(value, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()
|
||||
|
||||
|
||||
def projection(result, width):
|
||||
new_fields = {"candidate_id", "candidate_date", "window_index", "window_offset_minutes", "segment_index", "cluster_intervals"}
|
||||
return {
|
||||
"candidate_scores": result["candidate_scores"],
|
||||
"matrix": result["event_contribution_matrix"],
|
||||
"decisions": [{key: value for key, value in row.items() if key not in new_fields} for row in result["candidate_decisions"]],
|
||||
"width": width(result["candidate_decisions"]),
|
||||
"confirmation_allowed": result["confirmation_allowed"],
|
||||
"selection_allowed": result["selection_allowed"],
|
||||
"representative_time": result["decision_receipt"]["representative_time"],
|
||||
}
|
||||
|
||||
|
||||
def worker(root: Path, dated: bool):
|
||||
score, make_request, width = load(root)
|
||||
dataset = ROOT / "references/real_case_calibration/minute_rectification_holdout_v3.json"
|
||||
result = []
|
||||
for case in json.loads(dataset.read_text(encoding="utf-8"))["cases"][:3]:
|
||||
request = make_request(case)
|
||||
if dated:
|
||||
request["candidate_intervals"] = [{"start_at": f'{request["birth_date"]}T{request["start_time"]}', "end_at": f'{request["birth_date"]}T{request["end_time"]}'}]
|
||||
result.append(projection(score(request), width))
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
def independent(root: Path, dated: bool):
|
||||
completed = subprocess.run([sys.executable, str(Path(__file__).resolve()), "--worker", str(root), *( ["--dated"] if dated else [])],
|
||||
cwd=root, check=True, capture_output=True, text=True, encoding="utf-8")
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def main():
|
||||
if "--worker" in sys.argv:
|
||||
worker(Path(sys.argv[sys.argv.index("--worker") + 1]), "--dated" in sys.argv)
|
||||
return
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--baseline", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, default=ROOT / "artifacts/midnight-date-anchor")
|
||||
parser.add_argument("--golden", action="store_true")
|
||||
parser.add_argument("--golden-path", type=Path, default=ROOT / "frontend/tests/fixtures/rectification-midnight-date-anchor.native.json")
|
||||
args = parser.parse_args()
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
dataset = ROOT / "references/real_case_calibration/minute_rectification_holdout_v3.json"
|
||||
cases = json.loads(dataset.read_text(encoding="utf-8"))["cases"][:3]
|
||||
assert all(case["birth"]["source"]["rodden_rating"] == "AA" for case in cases)
|
||||
old = independent(args.baseline, False)
|
||||
current = independent(ROOT, True)
|
||||
comparisons = []
|
||||
for case, before, after in zip(cases, old, current):
|
||||
fields = {key: canonical(before[key]) == canonical(after[key]) for key in before}
|
||||
comparisons.append({"case_id": case["case_id"], "source": case["birth"]["source"], "fields": fields,
|
||||
"before_sha256": hashlib.sha256(canonical(before)).hexdigest(), "after_sha256": hashlib.sha256(canonical(after)).hexdigest(),
|
||||
"candidate_minutes": len(after["candidate_scores"]), "public_clusters": len(after["decisions"]), "width": after["width"]})
|
||||
print("current", case["case_id"], fields, flush=True)
|
||||
report = {"baseline": str(args.baseline), "dataset_sha256": hashlib.sha256(dataset.read_bytes()).hexdigest(),
|
||||
"same_machine_independent_processes": True, "python_executable": sys.executable, "numeric_tolerance": 0, "comparisons": comparisons,
|
||||
"excluded_fields": "new date/ordinal metadata; result/candidate IDs intentionally change with algorithm identity"}
|
||||
with (args.output / "independent-process-aa-ab.json").open("x", encoding="utf-8") as stream:
|
||||
json.dump(report, stream, indent=2)
|
||||
assert all(all(row["fields"].values()) for row in comparisons), "same-day native A/B changed"
|
||||
if args.golden:
|
||||
score, _, _ = load(ROOT)
|
||||
# Explicitly fictional birth/event facts; response is produced only by the unmocked native engine.
|
||||
request = {"birth_date": "2000-03-01", "start_time": "23:58", "end_time": "00:02", "lat": 0.0, "lon": 0.0, "tz": 0.0,
|
||||
"candidate_intervals": [{"start_at": "2000-02-29T23:58", "end_at": "2000-03-01T00:02"}],
|
||||
"events": [{"id": "00000000-0000-4000-8000-000000000001", "domain": "career", "event_kind": "career_entry",
|
||||
"date_start": "2020-01-01", "date_end": "2020-01-01", "precision": "day", "summary": "Fictional career entry for date-contract testing"}]}
|
||||
from scripts.rectification.contracts import normalize_rectification_request
|
||||
response = score(normalize_rectification_request(request))
|
||||
target = args.golden_path
|
||||
with target.open("x", encoding="utf-8") as stream:
|
||||
json.dump({"provenance": {"kind": "unmocked_native_engine", "facts": "explicitly_fictional", "generator": "scripts/research/midnight_date_anchor_regression.py --baseline <baseline-worktree> --golden", "algorithm": response["algorithm_version"]}, "request": request, "response": response}, stream, ensure_ascii=False, indent=2)
|
||||
print("golden", target, flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,8 +30,8 @@ from scripts.research.sealed_holdout_rerun import (
|
||||
OFFSETS = (-30, -20, -15, -10, -8, -5, -3, 0, 3, 5, 8, 10, 15, 20, 30)
|
||||
RADII = (15, 30, 60)
|
||||
MINUTE_STEP = 1
|
||||
FREEZE = ROOT / "docs/research/reported_offset_cross_midnight_2026_09_20.final.freeze.json"
|
||||
REPORT = ROOT / "docs/research/reported_offset_cross_midnight_2026_09_20.json"
|
||||
FREEZE = ROOT / "docs/research/reported_offset_midnight_anchor_2026_09_21.freeze.json"
|
||||
REPORT = ROOT / "docs/research/reported_offset_midnight_anchor_2026_09_21.json"
|
||||
LEGACY_REPORT = ROOT / "docs/research/reported_offset_2026_09_20.json"
|
||||
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ from scripts.minute_rectification_feature_facts_v4 import build_feature_fact_row
|
||||
from scripts.minute_rectification_holdout_validator import validate
|
||||
|
||||
DATASET = ROOT / "references/real_case_calibration/minute_rectification_holdout_v3.json"
|
||||
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_cross_midnight_2026_09_20.final.freeze.json"
|
||||
REPORT = ROOT / "docs/research/sealed_holdout_rerun_cross_midnight_2026_09_20.json"
|
||||
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_midnight_anchor_2026_09_21.freeze.json"
|
||||
REPORT = ROOT / "docs/research/sealed_holdout_rerun_midnight_anchor_2026_09_21.json"
|
||||
LEGACY_REPORT = ROOT / "docs/research/sealed_holdout_rerun_2026_09_20.json"
|
||||
ARCHIVE = ROOT / "docs/research/history/rectification_pre_cross_midnight_2026_09_20"
|
||||
PRODUCTION_FILES = [
|
||||
@@ -38,6 +38,10 @@ PRODUCTION_FILES = [
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
]
|
||||
RESEARCH_FILES = [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
|
||||
Reference in New Issue
Block a user