feat(features): per-feature freshness spec — JIT vs batched (#61)

Each ml/features/*.py now declares freshness, source, and fallback per
feature. ProfileFeature gains ttl_sec (mirrored from registry.ts),
freshness="batched", source, and fallback. context.py adds
ContextFeatureSpec + CONTEXT_FEATURES for the three JIT features
(hour_of_day, day_of_week, tasks). CI test parses ttlSec from registry.ts
to catch drift. ml/README updated with split JIT/batched feature contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 17:02:55 +00:00
parent bd3ea1b8b1
commit 45416000f9
6 changed files with 218 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
"""Tests for ml/features/context.py"""
import pytest
import sys, os; sys.path.insert(0, os.path.dirname(__file__))
from context import build_context, TaskSignal, PromptContext
from context import build_context, TaskSignal, PromptContext, CONTEXT_FEATURES
def test_empty_tasks():
@@ -62,3 +62,30 @@ def test_due_date_none_preserved():
tasks = [TaskSignal(id="x", content="No due", due_date=None)]
ctx = build_context(tasks)
assert ctx.tasks[0]["due_date"] is None
# ── CONTEXT_FEATURES spec tests (issue #61) ──────────────────────────────────
def test_context_features_expected_names():
names = {f.name for f in CONTEXT_FEATURES}
assert names == {"hour_of_day", "day_of_week", "tasks"}
def test_context_features_all_jit():
for f in CONTEXT_FEATURES:
assert f.freshness == "jit", f"{f.name}: expected freshness='jit', got {f.freshness!r}"
def test_context_features_source_set():
for f in CONTEXT_FEATURES:
assert f.source, f"{f.name}: source must not be empty"
def test_context_features_fallback_set():
for f in CONTEXT_FEATURES:
assert f.fallback, f"{f.name}: fallback must not be empty"
def test_context_features_no_duplicates():
names = [f.name for f in CONTEXT_FEATURES]
assert len(names) == len(set(names)), f"duplicate names: {names}"