untracked files on main: dceb101 feat(#63): IMP-34 R1 donor capacity measured bound (u1+u2)

This commit is contained in:
2026-05-21 22:07:41 +09:00
commit 8f085a28d3
3220 changed files with 985495 additions and 0 deletions
@@ -0,0 +1,347 @@
"""IMP-46 u3 — Fingerprint-based cache invalidation tests.
Scope (Stage 2 plan, u3):
* ``save_proposal`` persists ``fingerprints`` verbatim (u2 already covers
the round-trip; this suite re-asserts the read-side comparator).
* ``read_proposal`` accepts an optional ``fingerprints`` kwarg. When
supplied, the stored dict must equal the supplied dict EXACTLY (strict
equality). Mismatch — including missing keys, extra keys, or value
drift — returns ``None``.
* Default ``fingerprints=None`` performs no comparison (back-compat for
legacy callers).
* Fingerprint *computation* stays outside ``cache.py`` — these tests
treat the three declared shas (``contract_sha`` / ``partial_sha`` /
``catalog_sha``) as opaque hex strings, never recomputing them. The
cache layer is a content-addressed *comparator*, not a content
*hasher*.
All filesystem writes are scoped to ``tmp_path`` via
``monkeypatch.setattr`` on the module-level :data:`CACHE_ROOT`.
"""
from __future__ import annotations
import json
import pathlib
import pytest
from src.phase_z2_ai_fallback import cache as cache_mod
from src.phase_z2_ai_fallback.cache import (
KEY_DELIMITER,
read_proposal,
save_proposal,
)
from src.phase_z2_ai_fallback.schema import AiFallbackProposal, ProposalKind
_FRAME_ID = "1171281190"
_SIG_HASH = "f" * 64
_KEY = f"{_FRAME_ID}{KEY_DELIMITER}{_SIG_HASH}"
_FINGERPRINTS_BASELINE: dict[str, str] = {
"contract_sha": "c" * 64,
"partial_sha": "p" * 64,
"catalog_sha": "x" * 64,
}
def _proposal(payload: dict | None = None) -> AiFallbackProposal:
return AiFallbackProposal(
proposal_kind=ProposalKind.BUILDER_OPTIONS_PATCH,
payload=payload if payload is not None else {"item_parser": "bullet_v2"},
rationale="u3-test",
)
@pytest.fixture(autouse=True)
def _isolated_cache_root(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(cache_mod, "CACHE_ROOT", tmp_path / "frame_cache")
yield tmp_path / "frame_cache"
# -- save side: fingerprints persisted verbatim ---------------------------
def test_save_persists_fingerprints_verbatim(
_isolated_cache_root: pathlib.Path,
):
path = save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
fingerprints=_FINGERPRINTS_BASELINE,
)
stored = json.loads(path.read_text(encoding="utf-8"))["fingerprints"]
assert stored == _FINGERPRINTS_BASELINE
# -- read side: back-compat (no fingerprints kwarg) -----------------------
def test_read_without_fingerprints_kwarg_returns_proposal(
_isolated_cache_root: pathlib.Path,
):
"""Legacy read path (no kwarg) skips invalidation — round-trip succeeds."""
save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
fingerprints=_FINGERPRINTS_BASELINE,
)
loaded = read_proposal(_KEY)
assert loaded is not None
assert loaded.payload == {"item_parser": "bullet_v2"}
def test_read_without_fingerprints_kwarg_ignores_stored_mismatch(
_isolated_cache_root: pathlib.Path,
):
"""A caller that has not adopted fingerprint-aware lookup must still
see the proposal — invalidation only kicks in when explicitly asked."""
save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
fingerprints={"contract_sha": "old"},
)
loaded = read_proposal(_KEY)
assert loaded is not None
# -- read side: matching fingerprints -------------------------------------
def test_read_with_matching_fingerprints_returns_proposal(
_isolated_cache_root: pathlib.Path,
):
save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
fingerprints=_FINGERPRINTS_BASELINE,
)
loaded = read_proposal(_KEY, fingerprints=dict(_FINGERPRINTS_BASELINE))
assert loaded is not None
assert loaded.proposal_kind is ProposalKind.BUILDER_OPTIONS_PATCH
def test_read_with_empty_fingerprints_matches_empty_stored(
_isolated_cache_root: pathlib.Path,
):
"""Both sides empty is an exact match, not a special-case None."""
save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
# default fingerprints=None → stored as {}
)
loaded = read_proposal(_KEY, fingerprints={})
assert loaded is not None
# -- read side: invalidation on mismatch ----------------------------------
@pytest.mark.parametrize(
"drifted_axis",
["contract_sha", "partial_sha", "catalog_sha"],
)
def test_read_invalidates_on_single_axis_drift(
drifted_axis: str, _isolated_cache_root: pathlib.Path
):
save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
fingerprints=_FINGERPRINTS_BASELINE,
)
supplied = dict(_FINGERPRINTS_BASELINE)
supplied[drifted_axis] = "deadbeef" * 8 # 64-char distinct value
assert read_proposal(_KEY, fingerprints=supplied) is None
def test_read_invalidates_when_caller_supplies_extra_key(
_isolated_cache_root: pathlib.Path,
):
"""Strict equality — extra key on caller side is a mismatch."""
save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
fingerprints=_FINGERPRINTS_BASELINE,
)
supplied = dict(_FINGERPRINTS_BASELINE)
supplied["future_axis_sha"] = "z" * 64
assert read_proposal(_KEY, fingerprints=supplied) is None
def test_read_invalidates_when_caller_supplies_subset(
_isolated_cache_root: pathlib.Path,
):
"""Strict equality — subset on caller side is a mismatch."""
save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
fingerprints=_FINGERPRINTS_BASELINE,
)
subset = {"contract_sha": _FINGERPRINTS_BASELINE["contract_sha"]}
assert read_proposal(_KEY, fingerprints=subset) is None
def test_read_invalidates_when_entry_saved_without_fingerprints(
_isolated_cache_root: pathlib.Path,
):
"""A pre-invalidation cache entry (empty stored fingerprints) MUST NOT
satisfy a fingerprint-aware lookup — caller demands proof of freshness."""
save_proposal(
_KEY,
_proposal(),
visual_check_passed=True,
user_approved=True,
# default fingerprints=None → stored as {}
)
assert read_proposal(_KEY, fingerprints=_FINGERPRINTS_BASELINE) is None
def test_read_invalidates_when_stored_fingerprints_not_dict(
_isolated_cache_root: pathlib.Path,
):
"""Hand-corrupted payload (fingerprints serialized as non-dict) → None."""
path = _isolated_cache_root / _FRAME_ID / f"{_SIG_HASH}.json"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(
{
"schema_version": 1,
"proposal": _proposal().model_dump(mode="json"),
"slide_css": None,
"fingerprints": ["contract_sha", "c" * 64],
}
),
encoding="utf-8",
)
assert read_proposal(_KEY, fingerprints=_FINGERPRINTS_BASELINE) is None
def test_read_invalidates_when_stored_fingerprints_field_missing(
_isolated_cache_root: pathlib.Path,
):
"""Legacy payload (no ``fingerprints`` field at all) → None when caller
demands fingerprint comparison."""
path = _isolated_cache_root / _FRAME_ID / f"{_SIG_HASH}.json"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(
{
"schema_version": 1,
"proposal": _proposal().model_dump(mode="json"),
"slide_css": None,
# fingerprints field deliberately omitted
}
),
encoding="utf-8",
)
assert read_proposal(_KEY, fingerprints={"contract_sha": "c" * 64}) is None
def test_read_with_matching_fingerprints_still_loses_to_missing_file():
"""File missing takes precedence over fingerprint check — no false hit."""
assert read_proposal(_KEY, fingerprints=_FINGERPRINTS_BASELINE) is None
def test_read_with_matching_fingerprints_still_loses_to_corrupt_json(
_isolated_cache_root: pathlib.Path,
):
path = _isolated_cache_root / _FRAME_ID / f"{_SIG_HASH}.json"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("{not valid json", encoding="utf-8")
assert read_proposal(_KEY, fingerprints=_FINGERPRINTS_BASELINE) is None
# -- read side: input validation symmetry with save -----------------------
def test_read_rejects_non_dict_fingerprints():
with pytest.raises(TypeError):
read_proposal(_KEY, fingerprints=["contract_sha", "c" * 64]) # type: ignore[arg-type]
def test_read_rejects_non_dict_fingerprints_string():
with pytest.raises(TypeError):
read_proposal(_KEY, fingerprints="contract_sha=c" * 8) # type: ignore[arg-type]
def test_read_rejects_non_dict_fingerprints_int():
with pytest.raises(TypeError):
read_proposal(_KEY, fingerprints=42) # type: ignore[arg-type]
# -- isolation: cache.py never computes fingerprints ----------------------
def test_cache_module_has_no_fingerprint_computer():
"""Guardrail: cache.py is a *comparator*, not a *hasher*. The three
declared shas are computed outside this module (step 12 / pipeline
glue). Adding a fingerprint computer here would leak Phase Z runtime
knowledge into the cache layer and violate AI isolation."""
public_surface = [
name
for name in dir(cache_mod)
if not name.startswith("_") and callable(getattr(cache_mod, name))
]
forbidden_substrings = ("hash", "sha", "fingerprint")
leaks = [
name
for name in public_surface
if any(sub in name.lower() for sub in forbidden_substrings)
]
assert leaks == [], (
f"cache.py public surface leaks fingerprint computation: {leaks}; "
"computation must live outside cache.py per IMP-46 u3 contract."
)
# -- isolation across distinct fingerprint sets ---------------------------
def test_distinct_fingerprint_sets_isolated_per_signature(
_isolated_cache_root: pathlib.Path,
):
"""Two entries under different signature hashes keep their own
fingerprints; reading one with the other's fingerprints misses."""
key_a = f"{_FRAME_ID}{KEY_DELIMITER}{'a' * 64}"
key_b = f"{_FRAME_ID}{KEY_DELIMITER}{'b' * 64}"
fps_a = {"contract_sha": "a" * 64}
fps_b = {"contract_sha": "b" * 64}
save_proposal(
key_a,
_proposal(payload={"sig": "a"}),
visual_check_passed=True,
user_approved=True,
fingerprints=fps_a,
)
save_proposal(
key_b,
_proposal(payload={"sig": "b"}),
visual_check_passed=True,
user_approved=True,
fingerprints=fps_b,
)
# Crossed lookups miss.
assert read_proposal(key_a, fingerprints=fps_b) is None
assert read_proposal(key_b, fingerprints=fps_a) is None
# Aligned lookups hit.
a_hit = read_proposal(key_a, fingerprints=fps_a)
b_hit = read_proposal(key_b, fingerprints=fps_b)
assert a_hit is not None and a_hit.payload == {"sig": "a"}
assert b_hit is not None and b_hit.payload == {"sig": "b"}
@@ -0,0 +1,93 @@
"""IMP-46 u6 — repository layout coverage for the persistent frame cache.
This module is a *layout* contract test, not a runtime test. It asserts the
files committed to source control that make ``data/frame_cache/`` exist on a
fresh checkout while keeping cached JSON payloads ignored by git:
* ``data/frame_cache/.gitkeep`` is tracked (so the cache root exists for a
fresh clone before any AI fallback run materialises payloads).
* ``.gitignore`` ignores ``data/*`` broadly, re-includes the
``data/frame_cache/`` directory, ignores its contents, and re-includes
``data/frame_cache/.gitkeep`` so cache payloads under
``data/frame_cache/{frame_id}/{signature_hash}.json`` remain ignored.
If somebody removes the ``.gitkeep`` marker, drops the negation lines from
``.gitignore``, or commits a real cache payload, this test fails. The cache
module surface (cache.py) is exercised by ``test_cache.py`` /
``test_cache_invalidation.py`` and is intentionally *not* re-asserted here —
this file is the layout-only lock that Stage 2 u6 declared.
"""
from __future__ import annotations
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
GITIGNORE_PATH = REPO_ROOT / ".gitignore"
CACHE_ROOT = REPO_ROOT / "data" / "frame_cache"
GITKEEP_PATH = CACHE_ROOT / ".gitkeep"
def _gitignore_lines() -> list[str]:
assert GITIGNORE_PATH.is_file(), f".gitignore missing at {GITIGNORE_PATH}"
text = GITIGNORE_PATH.read_text(encoding="utf-8")
return [line.strip() for line in text.splitlines()]
def test_frame_cache_root_directory_exists() -> None:
"""``data/frame_cache/`` must exist on disk as the cache root."""
assert CACHE_ROOT.is_dir(), (
f"frame cache root missing: {CACHE_ROOT}. The directory must exist "
"for save_proposal to write JSON payloads without first conjuring a "
"parent on demand from outside the cache module."
)
def test_gitkeep_marker_is_tracked_file() -> None:
"""``data/frame_cache/.gitkeep`` is the marker that keeps the dir tracked."""
assert GITKEEP_PATH.is_file(), (
f".gitkeep marker missing: {GITKEEP_PATH}. Without it the cache root "
"would disappear on a fresh clone (everything under data/ is "
"ignored by default)."
)
@pytest.mark.parametrize(
"rule",
[
# Broad ignore for everything under data/ (cache payloads, runs/, etc.).
"data/*",
# Re-include the frame_cache directory itself so child negations work.
"!data/frame_cache/",
# Ignore everything inside frame_cache/ (cached JSON payloads).
"data/frame_cache/*",
# Re-include the .gitkeep marker only.
"!data/frame_cache/.gitkeep",
],
)
def test_gitignore_contains_frame_cache_exception(rule: str) -> None:
"""The four ignore rules together pin the 'track marker only' contract."""
lines = _gitignore_lines()
assert rule in lines, (
f".gitignore missing IMP-46 u6 rule: {rule!r}. The four-line block "
"(data/*, !data/frame_cache/, data/frame_cache/*, "
"!data/frame_cache/.gitkeep) together ensure the cache root is "
"tracked while cached payloads remain ignored."
)
def test_gitignore_rule_order_keeps_payloads_ignored() -> None:
"""Rule order matters: the ``data/frame_cache/*`` re-ignore must come
AFTER the ``!data/frame_cache/`` directory re-include, otherwise the
re-include would shadow it and cached JSON payloads would be tracked."""
lines = _gitignore_lines()
reinclude_dir = lines.index("!data/frame_cache/")
reignore_contents = lines.index("data/frame_cache/*")
reinclude_marker = lines.index("!data/frame_cache/.gitkeep")
assert reinclude_dir < reignore_contents < reinclude_marker, (
"gitignore IMP-46 u6 block out of order: expected "
"'!data/frame_cache/' < 'data/frame_cache/*' < "
"'!data/frame_cache/.gitkeep' so cached payloads stay ignored while "
"only the marker is tracked."
)
@@ -0,0 +1,184 @@
"""IMP-46 u1 — Frame cache signature builder tests.
Verifies:
* Determinism — identical inputs yield the same SHA256 digest.
* Axis-change sensitivity — every one of the 8 declared axes mutates the
digest when changed in isolation.
* Public surface — only the 8 declared axes are accepted (no
sample/section identifier leakage).
* char_count bucket boundaries (0-50, 51-150, 151-400, 401-1000, 1001+).
* source_shape enum equivalence (string and SourceShape inputs match).
* schema_version is part of the hashed payload (digest stable for fixture).
"""
from __future__ import annotations
import inspect
import pytest
from src.phase_z2_ai_fallback.signature import (
CHAR_COUNT_BUCKET_LABELS,
SCHEMA_VERSION,
SourceShape,
bucket_char_count,
build_signature,
)
def _base_kwargs() -> dict:
return dict(
frame_id="frame_03",
v4_label="light_edit",
cardinality=3,
source_shape=SourceShape.BULLET,
h3_count=2,
char_count_bucket="51-150",
layout_preset="sidebar-right",
zone_position="top",
)
def test_schema_version_is_one() -> None:
assert SCHEMA_VERSION == 1
def test_bucket_labels_match_spec() -> None:
assert CHAR_COUNT_BUCKET_LABELS == (
"0-50",
"51-150",
"151-400",
"401-1000",
"1001+",
)
def test_signature_is_deterministic() -> None:
a = build_signature(**_base_kwargs())
b = build_signature(**_base_kwargs())
assert a == b
assert len(a) == 64
@pytest.mark.parametrize(
"axis, new_value",
[
("frame_id", "frame_04"),
("v4_label", "restructure"),
("cardinality", 5),
("source_shape", SourceShape.PARAGRAPH),
("h3_count", 3),
("char_count_bucket", "151-400"),
("layout_preset", "two-column"),
("zone_position", "bottom_l"),
],
)
def test_signature_changes_for_each_axis(axis: str, new_value: object) -> None:
base = build_signature(**_base_kwargs())
kwargs = _base_kwargs()
kwargs[axis] = new_value
assert build_signature(**kwargs) != base
def test_signature_accepts_string_source_shape() -> None:
enum_sig = build_signature(**_base_kwargs())
kwargs = _base_kwargs()
kwargs["source_shape"] = "bullet"
assert build_signature(**kwargs) == enum_sig
def test_signature_rejects_unknown_source_shape() -> None:
kwargs = _base_kwargs()
kwargs["source_shape"] = "nonsense"
with pytest.raises(ValueError):
build_signature(**kwargs)
def test_signature_rejects_unknown_char_count_bucket() -> None:
kwargs = _base_kwargs()
kwargs["char_count_bucket"] = "999-1234"
with pytest.raises(ValueError):
build_signature(**kwargs)
def test_signature_handles_none_cardinality() -> None:
kwargs = _base_kwargs()
kwargs["cardinality"] = None
sig = build_signature(**kwargs)
assert len(sig) == 64
kwargs2 = _base_kwargs()
kwargs2["cardinality"] = 0
assert build_signature(**kwargs2) != sig
def test_signature_surface_only_8_declared_axes() -> None:
params = set(inspect.signature(build_signature).parameters)
expected = {
"frame_id",
"v4_label",
"cardinality",
"source_shape",
"h3_count",
"char_count_bucket",
"layout_preset",
"zone_position",
}
assert params == expected
def test_bucket_boundaries() -> None:
assert bucket_char_count(0) == "0-50"
assert bucket_char_count(50) == "0-50"
assert bucket_char_count(51) == "51-150"
assert bucket_char_count(150) == "51-150"
assert bucket_char_count(151) == "151-400"
assert bucket_char_count(400) == "151-400"
assert bucket_char_count(401) == "401-1000"
assert bucket_char_count(1000) == "401-1000"
assert bucket_char_count(1001) == "1001+"
assert bucket_char_count(10_000) == "1001+"
def test_bucket_rejects_negative() -> None:
with pytest.raises(ValueError):
bucket_char_count(-1)
def test_bucket_rejects_non_int() -> None:
with pytest.raises(TypeError):
bucket_char_count(3.14) # type: ignore[arg-type]
with pytest.raises(TypeError):
bucket_char_count(True) # type: ignore[arg-type]
def test_signature_stable_known_fixture() -> None:
"""Lock the digest for a known fixture so a silent payload-shape change
(e.g. a new axis sneaks in, or schema_version drifts) breaks this test.
"""
sig = build_signature(
frame_id="frame_03",
v4_label="light_edit",
cardinality=3,
source_shape=SourceShape.BULLET,
h3_count=2,
char_count_bucket="51-150",
layout_preset="sidebar-right",
zone_position="top",
)
import hashlib
import json
expected_payload = {
"schema_version": 1,
"frame_id": "frame_03",
"v4_label": "light_edit",
"cardinality": 3,
"source_shape": "bullet",
"h3_count": 2,
"char_count_bucket": "51-150",
"layout_preset": "sidebar-right",
"zone_position": "top",
}
expected = hashlib.sha256(
json.dumps(expected_payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
).hexdigest()
assert sig == expected