Skip to content

Commit 1e5d7fd

Browse files
Pigbibicodex
andcommitted
feat: add retry-safe weekly period lock contract
Co-Authored-By: Codex <noreply@openai.com>
1 parent 7c4104d commit 1e5d7fd

3 files changed

Lines changed: 393 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# `pert.weekly.period_lock.v1`
2+
3+
This pure contract freezes the producer's original weekly period and source
4+
snapshot for retry/rerun validation. It is not an Actions artifact acquisition
5+
implementation and does not read the wall clock.
6+
7+
- `period_start` is a Monday; `period_end_exclusive` is the next Monday;
8+
`as_of` is the preceding Sunday.
9+
- `source_attempt` is exactly `1`, identifying the original dispatch evidence.
10+
A retry/rerun compares its expected lock to that original value and never
11+
derives a new period from `run_attempt` or current time.
12+
- `producer_ref`, source snapshot id/digest/provenance, and canonical sorted
13+
source artifact path/sha256/row-count entries are part of the lock.
14+
- `generated_at` is intentionally absent: a later producer build may use a new
15+
actual UTC build timestamp without changing period or source identity.
16+
- Wire is exact/canonical JSON with duplicate-key rejection and stable sanitized
17+
errors. Unknown, missing, noncanonical, unsafe integer, invalid UTC-week,
18+
tampered, or source-snapshot-mismatched values fail closed.
19+
20+
The next integration slice must acquire the original lock and immutable input
21+
snapshot before invoking the producer. Missing evidence must not fall back to
22+
wall-clock derivation.
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
"""Pure immutable period/input lock for retry-safe weekly production."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import re
7+
import unicodedata
8+
from collections.abc import Mapping
9+
from dataclasses import dataclass
10+
from datetime import date, timedelta
11+
from pathlib import PurePosixPath
12+
13+
LOCK_VERSION = "pert.weekly.period_lock.v1"
14+
CALENDAR = "utc_iso_week_monday_sunday"
15+
MAX_SAFE_JSON_INTEGER = 2**53 - 1
16+
17+
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
18+
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
19+
_SHA1_RE = re.compile(r"^[0-9a-f]{40}$")
20+
_RUN_ID_RE = re.compile(r"^[1-9][0-9]*$")
21+
_PATH_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*$")
22+
_IDENTIFIER_RE = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$")
23+
_WORKFLOW_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/@-]*$")
24+
_LOCK_KEYS = frozenset(
25+
{
26+
"lock_version",
27+
"calendar",
28+
"period_start",
29+
"period_end_exclusive",
30+
"as_of",
31+
"workflow_ref",
32+
"source_run_id",
33+
"source_attempt",
34+
"producer_ref",
35+
"source_snapshot_id",
36+
"source_snapshot_digest",
37+
"source_provenance",
38+
"source_artifacts",
39+
}
40+
)
41+
_ARTIFACT_KEYS = frozenset({"path", "sha256", "row_count"})
42+
43+
44+
class PeriodLockError(ValueError):
45+
"""Stable, sanitized period-lock contract error."""
46+
47+
def __init__(self, code: str) -> None:
48+
self.code = code
49+
super().__init__(code)
50+
51+
52+
def _invalid(code: str) -> PeriodLockError:
53+
return PeriodLockError(code)
54+
55+
56+
def _date(value: object) -> date:
57+
if type(value) is not str or not _DATE_RE.fullmatch(value):
58+
raise _invalid("period_date_invalid")
59+
try:
60+
return date.fromisoformat(value)
61+
except ValueError:
62+
raise _invalid("period_date_invalid") from None
63+
64+
65+
def _safe_int(value: object, code: str) -> int:
66+
if type(value) is not int or not 0 <= value <= MAX_SAFE_JSON_INTEGER:
67+
raise _invalid(code)
68+
return value
69+
70+
71+
def _identifier(value: object, code: str) -> str:
72+
if type(value) is not str or not value.isascii() or unicodedata.normalize("NFC", value) != value or not _IDENTIFIER_RE.fullmatch(value):
73+
raise _invalid(code)
74+
return value
75+
76+
77+
def _path(value: object) -> str:
78+
if type(value) is not str or not value.isascii() or unicodedata.normalize("NFC", value) != value or not _PATH_RE.fullmatch(value):
79+
raise _invalid("source_snapshot_path_invalid")
80+
parsed = PurePosixPath(value)
81+
if parsed.is_absolute() or value != str(parsed) or value.endswith("/") or "//" in value or any(part in {"", ".", ".."} for part in parsed.parts):
82+
raise _invalid("source_snapshot_path_invalid")
83+
return value
84+
85+
86+
def _workflow_ref(value: object) -> str:
87+
if type(value) is not str or not value.isascii() or unicodedata.normalize("NFC", value) != value or not _WORKFLOW_REF_RE.fullmatch(value):
88+
raise _invalid("workflow_ref_invalid")
89+
return value
90+
91+
92+
@dataclass(frozen=True, slots=True)
93+
class SourceSnapshotArtifact:
94+
path: str
95+
sha256: str
96+
row_count: int
97+
98+
def __post_init__(self) -> None:
99+
_path(self.path)
100+
if type(self.sha256) is not str or not _SHA256_RE.fullmatch(self.sha256):
101+
raise _invalid("source_snapshot_artifact_invalid")
102+
_safe_int(self.row_count, "source_snapshot_artifact_invalid")
103+
104+
105+
@dataclass(frozen=True, slots=True)
106+
class PoliticalEventWeeklyPeriodLockV1:
107+
period_start: date
108+
period_end_exclusive: date
109+
as_of: date
110+
workflow_ref: str
111+
source_run_id: str
112+
source_attempt: int
113+
producer_ref: str
114+
source_snapshot_id: str
115+
source_snapshot_digest: str
116+
source_provenance: str
117+
source_artifacts: tuple[SourceSnapshotArtifact, ...]
118+
119+
def __post_init__(self) -> None:
120+
if type(self.period_start) is not date or type(self.period_end_exclusive) is not date or type(self.as_of) is not date:
121+
raise _invalid("period_date_invalid")
122+
try:
123+
expected_end = self.period_start + timedelta(days=7)
124+
except OverflowError:
125+
raise _invalid("period_boundary_invalid") from None
126+
if self.period_start.weekday() != 0 or self.period_end_exclusive != expected_end or self.as_of != expected_end - timedelta(days=1):
127+
raise _invalid("period_mismatch")
128+
_workflow_ref(self.workflow_ref)
129+
if type(self.source_run_id) is not str or not _RUN_ID_RE.fullmatch(self.source_run_id):
130+
raise _invalid("source_run_id_invalid")
131+
if type(self.source_attempt) is not int or self.source_attempt != 1:
132+
raise _invalid("source_attempt_invalid")
133+
if type(self.producer_ref) is not str or not _SHA1_RE.fullmatch(self.producer_ref):
134+
raise _invalid("producer_ref_invalid")
135+
_identifier(self.source_snapshot_id, "source_snapshot_id_invalid")
136+
if type(self.source_snapshot_digest) is not str or not _SHA256_RE.fullmatch(self.source_snapshot_digest):
137+
raise _invalid("source_snapshot_digest_invalid")
138+
_identifier(self.source_provenance, "source_provenance_invalid")
139+
if not isinstance(self.source_artifacts, tuple) or not self.source_artifacts or any(
140+
not isinstance(item, SourceSnapshotArtifact) for item in self.source_artifacts
141+
):
142+
raise _invalid("source_snapshot_artifact_invalid")
143+
ordered = tuple(sorted(self.source_artifacts, key=lambda item: item.path))
144+
if len({item.path for item in ordered}) != len(ordered):
145+
raise _invalid("source_snapshot_duplicate")
146+
object.__setattr__(self, "source_artifacts", ordered)
147+
148+
149+
def parse_period_lock(value: Mapping[str, object]) -> PoliticalEventWeeklyPeriodLockV1:
150+
if not isinstance(value, Mapping) or set(value) != _LOCK_KEYS:
151+
raise _invalid("period_lock_shape_invalid")
152+
if value["lock_version"] != LOCK_VERSION or value["calendar"] != CALENDAR:
153+
raise _invalid("period_lock_version_invalid")
154+
artifacts_value = value["source_artifacts"]
155+
if not isinstance(artifacts_value, list) or not artifacts_value:
156+
raise _invalid("source_snapshot_artifact_invalid")
157+
artifacts: list[SourceSnapshotArtifact] = []
158+
for item in artifacts_value:
159+
if not isinstance(item, Mapping) or set(item) != _ARTIFACT_KEYS:
160+
raise _invalid("source_snapshot_artifact_invalid")
161+
artifacts.append(SourceSnapshotArtifact(item["path"], item["sha256"], item["row_count"]))
162+
return PoliticalEventWeeklyPeriodLockV1(
163+
_date(value["period_start"]),
164+
_date(value["period_end_exclusive"]),
165+
_date(value["as_of"]),
166+
_workflow_ref(value["workflow_ref"]),
167+
value["source_run_id"],
168+
_safe_int(value["source_attempt"], "source_attempt_invalid"),
169+
value["producer_ref"],
170+
value["source_snapshot_id"],
171+
value["source_snapshot_digest"],
172+
value["source_provenance"],
173+
tuple(artifacts),
174+
)
175+
176+
177+
def _payload(lock: PoliticalEventWeeklyPeriodLockV1) -> dict[str, object]:
178+
if not isinstance(lock, PoliticalEventWeeklyPeriodLockV1):
179+
raise _invalid("period_lock_type_invalid")
180+
payload: dict[str, object] = {
181+
"lock_version": LOCK_VERSION,
182+
"calendar": CALENDAR,
183+
"period_start": lock.period_start.isoformat(),
184+
"period_end_exclusive": lock.period_end_exclusive.isoformat(),
185+
"as_of": lock.as_of.isoformat(),
186+
"workflow_ref": lock.workflow_ref,
187+
"source_run_id": lock.source_run_id,
188+
"source_attempt": lock.source_attempt,
189+
"producer_ref": lock.producer_ref,
190+
"source_snapshot_id": lock.source_snapshot_id,
191+
"source_snapshot_digest": lock.source_snapshot_digest,
192+
"source_provenance": lock.source_provenance,
193+
"source_artifacts": [
194+
{"path": item.path, "sha256": item.sha256, "row_count": item.row_count} for item in lock.source_artifacts
195+
],
196+
}
197+
parse_period_lock(payload)
198+
return payload
199+
200+
201+
def serialize_period_lock(lock: PoliticalEventWeeklyPeriodLockV1) -> bytes:
202+
try:
203+
return json.dumps(_payload(lock), ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
204+
except (TypeError, ValueError, UnicodeError, OverflowError, RecursionError):
205+
raise _invalid("period_lock_serialization_invalid") from None
206+
207+
208+
def parse_period_lock_bytes(wire: bytes) -> PoliticalEventWeeklyPeriodLockV1:
209+
if type(wire) is not bytes:
210+
raise _invalid("period_lock_wire_invalid")
211+
212+
def pairs(items: list[tuple[str, object]]) -> dict[str, object]:
213+
result: dict[str, object] = {}
214+
for key, item in items:
215+
if key in result:
216+
raise _invalid("period_lock_duplicate_key")
217+
result[key] = item
218+
return result
219+
220+
try:
221+
value = json.loads(wire.decode("utf-8"), object_pairs_hook=pairs)
222+
except PeriodLockError:
223+
raise
224+
except (UnicodeError, json.JSONDecodeError, TypeError, ValueError, RecursionError):
225+
raise _invalid("period_lock_wire_invalid") from None
226+
lock = parse_period_lock(value)
227+
if serialize_period_lock(lock) != wire:
228+
raise _invalid("period_lock_noncanonical")
229+
return lock
230+
231+
232+
def assert_expected_period_lock(actual: PoliticalEventWeeklyPeriodLockV1, expected: PoliticalEventWeeklyPeriodLockV1) -> None:
233+
if not isinstance(actual, PoliticalEventWeeklyPeriodLockV1) or not isinstance(expected, PoliticalEventWeeklyPeriodLockV1):
234+
raise _invalid("period_lock_expected_invalid")
235+
if actual != expected:
236+
raise _invalid("period_lock_mismatch")

tests/test_weekly_period_lock.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from datetime import date
5+
6+
import pytest
7+
8+
from political_event_tracking_research.weekly_period_lock import (
9+
LOCK_VERSION,
10+
PeriodLockError,
11+
SourceSnapshotArtifact,
12+
PoliticalEventWeeklyPeriodLockV1,
13+
assert_expected_period_lock,
14+
parse_period_lock,
15+
parse_period_lock_bytes,
16+
serialize_period_lock,
17+
)
18+
19+
20+
def artifact_payload(path: str = "data/live/source_events.csv") -> dict[str, object]:
21+
return {"path": path, "sha256": "b" * 64, "row_count": 11}
22+
23+
24+
def payload(**overrides: object) -> dict[str, object]:
25+
value: dict[str, object] = {
26+
"lock_version": LOCK_VERSION,
27+
"calendar": "utc_iso_week_monday_sunday",
28+
"period_start": "2026-07-06",
29+
"period_end_exclusive": "2026-07-13",
30+
"as_of": "2026-07-12",
31+
"workflow_ref": "QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/rss_source_pipeline.yml@refs/heads/main",
32+
"source_run_id": "29396791050",
33+
"source_attempt": 1,
34+
"producer_ref": "a" * 40,
35+
"source_snapshot_id": "rss_source_snapshot_20260712",
36+
"source_snapshot_digest": "c" * 64,
37+
"source_provenance": "official_political_event_tracking_research_v1",
38+
"source_artifacts": [artifact_payload()],
39+
}
40+
value.update(overrides)
41+
return value
42+
43+
44+
def test_valid_lock_is_canonical_and_round_trips() -> None:
45+
lock = parse_period_lock(payload())
46+
wire = serialize_period_lock(lock)
47+
assert parse_period_lock_bytes(wire) == lock
48+
assert json.loads(wire) == payload()
49+
50+
51+
def test_source_artifacts_are_sorted_and_permutation_stable() -> None:
52+
items = [artifact_payload("z.csv"), artifact_payload("a.csv")]
53+
first = parse_period_lock(payload(source_artifacts=items))
54+
second = parse_period_lock(payload(source_artifacts=list(reversed(items))))
55+
assert first == second
56+
assert serialize_period_lock(first) == serialize_period_lock(second)
57+
assert [item.path for item in first.source_artifacts] == ["a.csv", "z.csv"]
58+
59+
60+
def test_direct_value_object_canonicalizes_artifacts() -> None:
61+
first = SourceSnapshotArtifact("z.csv", "a" * 64, 2)
62+
second = SourceSnapshotArtifact("a.csv", "b" * 64, 1)
63+
lock = PoliticalEventWeeklyPeriodLockV1(
64+
date(2026, 7, 6),
65+
date(2026, 7, 13),
66+
date(2026, 7, 12),
67+
"QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/rss_source_pipeline.yml@refs/heads/main",
68+
"29396791050",
69+
1,
70+
"a" * 40,
71+
"rss_source_snapshot_20260712",
72+
"c" * 64,
73+
"official_political_event_tracking_research_v1",
74+
(first, second),
75+
)
76+
assert [item.path for item in lock.source_artifacts] == ["a.csv", "z.csv"]
77+
78+
79+
@pytest.mark.parametrize(
80+
("field", "value"),
81+
[
82+
("period_end_exclusive", "2026-07-14"),
83+
("as_of", "2026-07-11"),
84+
("calendar", "utc_calendar_day"),
85+
("source_attempt", 2),
86+
("source_snapshot_digest", "not-a-digest"),
87+
("producer_ref", "A" * 40),
88+
],
89+
)
90+
def test_period_identity_and_original_attempt_are_strict(field: str, value: object) -> None:
91+
with pytest.raises(PeriodLockError):
92+
parse_period_lock(payload(**{field: value}))
93+
94+
95+
@pytest.mark.parametrize("value", [-1, 2**53, True, "1"])
96+
def test_source_attempt_rejects_unsafe_integer_shapes(value: object) -> None:
97+
with pytest.raises(PeriodLockError):
98+
parse_period_lock(payload(source_attempt=value))
99+
100+
101+
@pytest.mark.parametrize("path", ["", "/data/a.csv", "./a.csv", "a/../b.csv", "a//b.csv", "a\\b.csv", "é.csv", "a\n.csv"])
102+
def test_source_artifact_paths_are_canonical_safe_posix(path: str) -> None:
103+
with pytest.raises(PeriodLockError):
104+
parse_period_lock(payload(source_artifacts=[artifact_payload(path)]))
105+
106+
107+
def test_unknown_missing_and_duplicate_keys_fail_closed() -> None:
108+
unknown = payload(extra=True)
109+
with pytest.raises(PeriodLockError):
110+
parse_period_lock(unknown)
111+
missing = payload()
112+
del missing["source_snapshot_digest"]
113+
with pytest.raises(PeriodLockError):
114+
parse_period_lock(missing)
115+
duplicate = b'{"calendar":"utc_iso_week_monday_sunday","calendar":"utc_iso_week_monday_sunday"}'
116+
with pytest.raises(PeriodLockError):
117+
parse_period_lock_bytes(duplicate)
118+
119+
120+
def test_noncanonical_wire_and_generated_at_are_rejected() -> None:
121+
lock = parse_period_lock(payload())
122+
canonical = serialize_period_lock(lock)
123+
with pytest.raises(PeriodLockError):
124+
parse_period_lock_bytes(b" " + canonical)
125+
with pytest.raises(PeriodLockError):
126+
parse_period_lock(payload(generated_at="2026-07-13T00:00:00Z"))
127+
128+
129+
def test_expected_lock_requires_exact_original_evidence() -> None:
130+
actual = parse_period_lock(payload())
131+
assert_expected_period_lock(actual, actual)
132+
with pytest.raises(PeriodLockError):
133+
assert_expected_period_lock(actual, parse_period_lock(payload(source_run_id="29396791051")))
134+
with pytest.raises(PeriodLockError):
135+
assert_expected_period_lock(actual, parse_period_lock(payload(source_snapshot_digest="d" * 64)))

0 commit comments

Comments
 (0)