Implement GH-DEC-2026-008: declared PDP-path intent, enforced at issue
Gate House ruled binding.pdp_digest is the binding correspondence on the GH-DEC-2026-003 path and is required there, having rejected a vocabulary mapping for the reasons we gave. It asked this engine to record the PDP digest at issue for approvals intended for that path, and to have the claim state which approvals those are rather than leaving it to the requester's memory. Schema v3 adds approvals.pdp_path. create() refuses pdp_path true without a pdp_digest, so an approval that would be unusable on the path fails at issue rather than at the protected side effect. The claim exposes binding.pdp_path, which makes it a guarantee rather than a hint: pdp_path true implies pdp_digest is non-null. Intent is declared and never inferred. A pdp_digest that happens to be present is not a declaration anybody made, so a recorded digest alone leaves pdp_path false, legacy rows migrate to false rather than being back-filled from their digests, and a successor inherits its predecessor's declaration. Approvals issued before the ruling stay usable by consumers in this engine's own vocabulary and are simply not usable on the PDP path -- the ruling's intended cost, stated as such. Schema, both published examples, a v2-to-v3 migration test asserting survivors keep their digest while declaring no path intent, and tests for refusal at issue, claim exposure, non-inference, and successor inheritance. 102 tests pass (8 new). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND Assistant: claude-code Assistant-Model: opus Assistant-Process: 411227@bnt-lap001 Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
This commit is contained in:
parent
6d0dfc8010
commit
7e756773de
10 changed files with 166 additions and 11 deletions
|
|
@ -3,9 +3,11 @@ import threading
|
|||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
from approval_engine.errors import Conflict
|
||||
import pytest
|
||||
|
||||
from approval_engine.errors import Conflict, Unprocessable
|
||||
from approval_engine.store import Engine
|
||||
from tests.conftest import FROZEN, approve
|
||||
from tests.conftest import FROZEN, approve, binding, validity
|
||||
|
||||
|
||||
def test_second_supersession_loses(engine):
|
||||
|
|
@ -136,3 +138,63 @@ def test_existing_database_migrates_consumption_columns():
|
|||
}
|
||||
assert {"consumed_digest", "consumed_decision_id", "consumed_at"} <= columns
|
||||
eng.close()
|
||||
|
||||
|
||||
def test_pdp_path_requires_a_digest_at_issue(engine):
|
||||
"""GH-DEC-2026-008: refuse at issue, not at the protected side effect."""
|
||||
with pytest.raises(Unprocessable):
|
||||
engine.create(binding(), validity(), pdp_path=True)
|
||||
|
||||
|
||||
def test_pdp_path_approval_states_itself_on_the_claim(engine):
|
||||
obj = engine.create(
|
||||
binding(), validity(), pdp_digest="sha256:" + "ab" * 32, pdp_path=True
|
||||
)
|
||||
engine.add_entry(obj.id, "user:alice")
|
||||
claim = engine.claim(obj.id)
|
||||
assert claim["binding"]["pdp_path"] is True
|
||||
assert claim["binding"]["pdp_digest"] == "sha256:" + "ab" * 32
|
||||
|
||||
|
||||
def test_a_recorded_digest_alone_does_not_declare_the_path(engine):
|
||||
"""Intent is declared, never inferred from an incidental digest."""
|
||||
obj = engine.create(binding(), validity(), pdp_digest="sha256:" + "cd" * 32)
|
||||
claim = engine.claim(obj.id)
|
||||
assert claim["binding"]["pdp_digest"] is not None
|
||||
assert claim["binding"]["pdp_path"] is False
|
||||
|
||||
|
||||
def test_successor_inherits_the_pdp_path_declaration(engine):
|
||||
obj = engine.create(
|
||||
binding(), validity(), pdp_digest="sha256:" + "ef" * 32, pdp_path=True
|
||||
)
|
||||
engine.add_entry(obj.id, "user:alice")
|
||||
result = engine.supersede(obj.id, None)
|
||||
successor = engine.get(result["successor_id"])
|
||||
assert successor.pdp_path is True
|
||||
|
||||
|
||||
def test_v2_database_migrates_to_v3_preserving_approvals():
|
||||
"""Schema v3 (GH-DEC-2026-008) must not disturb approvals issued under v2."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "v2.sqlite"
|
||||
eng = Engine(path, clock=lambda: FROZEN)
|
||||
obj = eng.create(binding(), validity(), pdp_digest="sha256:" + "12" * 32)
|
||||
eng.add_entry(obj.id, "user:alice")
|
||||
# simulate a store written before v3 existed
|
||||
eng._conn().execute("ALTER TABLE approvals DROP COLUMN pdp_path")
|
||||
eng._conn().execute("PRAGMA user_version=2")
|
||||
eng._conn().commit()
|
||||
eng.close()
|
||||
|
||||
upgraded = Engine(path, clock=lambda: FROZEN)
|
||||
version = int(upgraded._conn().execute("PRAGMA user_version").fetchone()[0])
|
||||
assert version == 3
|
||||
survivor = upgraded.get(obj.id)
|
||||
assert survivor.status == "approved"
|
||||
assert survivor.pdp_digest == "sha256:" + "12" * 32
|
||||
# a legacy row never declared the path; intent is not back-filled from
|
||||
# a digest that happens to be present
|
||||
assert survivor.pdp_path is False
|
||||
assert upgraded.claim(obj.id)["binding"]["pdp_path"] is False
|
||||
upgraded.close()
|
||||
|
|
|
|||
|
|
@ -39,3 +39,16 @@ def test_examples_cover_both_pdp_binding_states():
|
|||
json.loads(p.read_text())["binding"]["pdp_digest"] is None for p in EXAMPLES
|
||||
}
|
||||
assert states == {True, False}
|
||||
|
||||
|
||||
def test_examples_cover_both_pdp_path_declarations():
|
||||
states = {json.loads(p.read_text())["binding"]["pdp_path"] for p in EXAMPLES}
|
||||
assert states == {True, False}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", EXAMPLES, ids=lambda p: p.name)
|
||||
def test_pdp_path_examples_always_carry_a_digest(path):
|
||||
"""GH-DEC-2026-008: pdp_path true guarantees pdp_digest non-null."""
|
||||
binding = json.loads(path.read_text())["binding"]
|
||||
if binding["pdp_path"]:
|
||||
assert binding["pdp_digest"] is not None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue