From 3e07e86187615e62d7ad72a6a9d032ec977f0699 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 4 Jun 2026 06:33:27 -0700 Subject: [PATCH] feat: review transition core (proposed -> approved) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per sub-project-2 plan Task 11 / spec §8.1. proposed_records filter and approve() return an approved copy via dataclasses.replace (no in-place mutation), with optional coordinate/rationale override and an injected reviewed_at timestamp. Pure, no I/O, no clock. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_review.py | 59 ++++++++++++++++++++++++++++++++++++++++++++ tools/review.py | 34 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 tests/test_review.py create mode 100644 tools/review.py diff --git a/tests/test_review.py b/tests/test_review.py new file mode 100644 index 0000000..5fb9d2a --- /dev/null +++ b/tests/test_review.py @@ -0,0 +1,59 @@ +from hef.catalog import Record, validate +from hef.selection import Coordinate +from tools.review import approve, proposed_records + + +def make_record(**o): + base = dict( + id="a", + title="t", + source_url="u", + source_archive="nasa", + license="public_domain", + mode="video", + left=0, + right=0, + dark=0, + light=0, + duration_s=1, + file_path="p", + ) + base.update(o) + return Record(**base) + + +def test_proposed_records_filters(): + a = make_record(id="a", review_status="proposed") + b = make_record(id="b", review_status="approved") + assert [r.id for r in proposed_records([a, b])] == ["a"] + + +def test_approve_sets_status_and_timestamp(): + r = make_record(review_status="proposed") + out = approve(r, reviewed_at="2026-06-04T13:00:00+00:00") + assert out.review_status == "approved" + assert out.reviewed_at == "2026-06-04T13:00:00+00:00" + + +def test_approve_can_override_coordinates(): + r = make_record(left=0, right=0, dark=0, light=0, review_status="proposed") + out = approve(r, reviewed_at="t", coordinate=Coordinate(4, 1, 2, 3)) + assert (out.left, out.right, out.dark, out.light) == (4, 1, 2, 3) + + +def test_approve_does_not_mutate_input(): + r = make_record(review_status="proposed") + approve(r, reviewed_at="t") + assert r.review_status == "proposed" and r.reviewed_at is None + + +def test_approve_can_set_rationale(): + r = make_record(review_status="proposed", rationale="auto") + out = approve(r, reviewed_at="t", rationale="human note") + assert out.rationale == "human note" + + +def test_approved_record_revalidates(): + r = make_record(review_status="proposed") + out = approve(r, reviewed_at="t", coordinate=Coordinate(2, 2, 1, 1)) + validate(out) # must not raise diff --git a/tools/review.py b/tools/review.py new file mode 100644 index 0000000..90467b9 --- /dev/null +++ b/tools/review.py @@ -0,0 +1,34 @@ +"""Review state-transition core (pure, tested): proposed -> approved. + +No I/O, no clock — the timestamp is injected so the logic stays deterministic. +The CLI persists the result via hef.catalog.save_catalog after re-validating. +""" + +from __future__ import annotations + +from dataclasses import replace + + +def proposed_records(records): + """Records still awaiting review.""" + return [r for r in records if r.review_status == "proposed"] + + +def approve(record, *, reviewed_at, coordinate=None, rationale=None): + """Return an approved copy of `record` (the input is not mutated). + + review_status -> 'approved' and reviewed_at is stamped. Optionally override + the four coordinates with a human correction and/or replace the rationale. + The caller re-validates before saving. + """ + changes = {"review_status": "approved", "reviewed_at": reviewed_at} + if coordinate is not None: + changes.update( + left=coordinate.left, + right=coordinate.right, + dark=coordinate.dark, + light=coordinate.light, + ) + if rationale is not None: + changes["rationale"] = rationale + return replace(record, **changes)