From 3d5c34491cfe58d493c3ab609c759e509288469b Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 4 Jun 2026 06:27:37 -0700 Subject: [PATCH] feat: heuristic coordinate proposer (drafting) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per sub-project-2 plan Task 7 / spec §7. HeuristicProposer seeds the brain plane from archive priors and the mood plane from title/description keyword nudges, clamps to 0..4, and emits a one-line rationale. Deterministic, no I/O, no ML (honors design §11) — only a DRAFT a human reviews. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_drafting.py | 79 ++++++++++++++++++++++++++++++++++++ tools/drafting.py | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 tests/test_drafting.py create mode 100644 tools/drafting.py diff --git a/tests/test_drafting.py b/tests/test_drafting.py new file mode 100644 index 0000000..e28bbe6 --- /dev/null +++ b/tests/test_drafting.py @@ -0,0 +1,79 @@ +from hef.selection import Coordinate +from tools.drafting import Draft, HeuristicProposer, Signals + + +def _sig(**o): + base = dict( + title="", + description="", + source_archive="nasa", + mode="video", + duration_s=600, + ) + base.update(o) + return Signals(**base) + + +def test_returns_draft_with_coordinate_and_rationale(): + d = HeuristicProposer().propose(_sig()) + assert isinstance(d, Draft) + assert isinstance(d.coordinate, Coordinate) + assert d.rationale and "\n" not in d.rationale + + +def test_librivox_seeds_left(): + d = HeuristicProposer().propose( + _sig(title="Meditations", source_archive="librivox", mode="audio") + ) + assert d.coordinate.left >= 3 and d.rationale + + +def test_music_archives_seed_right(): + for arch in ("musopen", "fma", "nasa", "freesound"): + d = HeuristicProposer().propose(_sig(source_archive=arch, mode="audio")) + assert d.coordinate.right >= 3, arch + + +def test_internet_archive_seeds_left(): + d = HeuristicProposer().propose(_sig(source_archive="internet_archive")) + assert d.coordinate.left >= 3 + + +def test_storm_seeds_dark(): + d = HeuristicProposer().propose( + _sig(title="Thunderstorm at Night", source_archive="nasa") + ) + assert d.coordinate.dark >= 2 + + +def test_sunrise_seeds_light(): + d = HeuristicProposer().propose( + _sig(title="Sunrise over the Garden", source_archive="nasa") + ) + assert d.coordinate.light >= 2 + + +def test_coordinates_clamped_to_0_4(): + d = HeuristicProposer().propose( + _sig( + title="storm night war funeral decay requiem minor", + description="noir death grief", + source_archive="librivox", + ) + ) + for v in (d.coordinate.left, d.coordinate.right, d.coordinate.dark, d.coordinate.light): + assert 0 <= v <= 4 + + +def test_rationale_cites_a_signal(): + d = HeuristicProposer().propose( + _sig(title="Storm", source_archive="librivox", mode="audio") + ) + assert "librivox" in d.rationale.lower() + + +def test_deterministic(): + s = _sig(title="Storm at Dawn", source_archive="nasa") + a = HeuristicProposer().propose(s) + b = HeuristicProposer().propose(s) + assert a == b diff --git a/tools/drafting.py b/tools/drafting.py new file mode 100644 index 0000000..83ff174 --- /dev/null +++ b/tools/drafting.py @@ -0,0 +1,90 @@ +"""Heuristic coordinate proposer — a deterministic, rule-based DRAFT seed. + +The four curatorial coordinates are a human act (design §11: no automatic ML +coordinate tagging). This proposer only suggests a starting point written as +review_status='proposed'; a human blesses every coordinate before selection. +There is no ML model here — just archive priors (brain plane) and keyword +nudges (mood plane), grounded in the §8 sourcing→axis map. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + +from hef.selection import Coordinate + +COORD_MIN, COORD_MAX = 0, 4 + +# Brain plane (left = verbal/spoken/educational, right = music/wordless/abstract). +_ARCHIVE_PRIORS = { + "librivox": (4, 0), # spoken readings + "internet_archive": (3, 1), # educational / industrial / Prelinger + "musopen": (0, 4), # classical music + "fma": (0, 4), # music + "nasa": (0, 4), # wordless awe + "freesound": (0, 4), # abstract field recordings +} +_ARCHIVE_DEFAULT = (2, 2) + +# Mood plane keywords (substring match so "thunderstorm" trips "storm"). +_DARK_WORDS = ( + "storm", "night", "noir", "requiem", "minor", "war", "funeral", + "decay", "death", "grief", "dusk", "winter", "mourning", "shadow", +) +_LIGHT_WORDS = ( + "sunrise", "dawn", "garden", "spring", "joy", "hope", "major", + "bright", "bloom", "summer", "smile", "light", "morning", +) + + +def _clamp(v: int) -> int: + return max(COORD_MIN, min(COORD_MAX, v)) + + +@dataclass +class Signals: + title: str + description: str + source_archive: str + mode: str + duration_s: int + + +@dataclass +class Draft: + coordinate: Coordinate + rationale: str + + +class Proposer(Protocol): + def propose(self, signals: Signals) -> Draft: ... + + +class HeuristicProposer: + def propose(self, signals: Signals) -> Draft: + left, right = _ARCHIVE_PRIORS.get( + signals.source_archive, _ARCHIVE_DEFAULT + ) + + text = f"{signals.title} {signals.description}".lower() + dark_hits = [w for w in _DARK_WORDS if w in text] + light_hits = [w for w in _LIGHT_WORDS if w in text] + dark = _clamp(len(dark_hits)) + light = _clamp(len(light_hits)) + + coordinate = Coordinate(_clamp(left), _clamp(right), dark, light) + + # Rationale: name the dominant brain prior + any mood keywords. + brain = ( + f"{signals.source_archive} → " + + ("strong left" if left >= right else "strong right") + ) + mood_bits = [] + if dark_hits: + mood_bits.append(f"{dark_hits[0]!r} → dark") + if light_hits: + mood_bits.append(f"{light_hits[0]!r} → light") + rationale = "; ".join([brain, *mood_bits]) + + return Draft(coordinate=coordinate, rationale=rationale)