Skip to content

Commit dd5722f

Browse files
Pigbibicodex
andcommitted
feat: add versioned artifact metadata
Co-Authored-By: Codex <noreply@openai.com>
1 parent f7dd58b commit dd5722f

4 files changed

Lines changed: 45 additions & 3 deletions

File tree

src/research_signal_context_pipelines/schema.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class SignalValidationError(ValueError):
2828
ALLOWED_REGIMES = frozenset({"risk_on", "risk_off", "neutral", "mixed", "unknown"})
2929
ALLOWED_BIAS_VALUES = frozenset({"positive", "negative", "neutral", "watch", "avoid"})
3030
REQUIRED_SIGNAL_HORIZON = "1-3 years"
31+
SUPPORTED_SCHEMA_VERSIONS = frozenset({"1", "2"})
3132

3233

3334
def _require_mapping(value: Any, name: str) -> Mapping[str, Any]:
@@ -82,11 +83,15 @@ def validate_signal(payload: Mapping[str, Any]) -> None:
8283
if missing:
8384
raise SignalValidationError(f"missing required keys: {', '.join(missing)}")
8485

85-
if _require_string(payload["schema_version"], "schema_version") != "1":
86-
raise SignalValidationError("schema_version must be '1'")
86+
schema_version = _require_string(payload["schema_version"], "schema_version")
87+
if schema_version not in SUPPORTED_SCHEMA_VERSIONS:
88+
raise SignalValidationError("schema_version must be '1' or '2'")
8789
_require_iso_date(payload["as_of"], "as_of")
8890
_require_iso_datetime(payload["generated_at"], "generated_at")
8991
_require_iso_date(payload["expires_at"], "expires_at")
92+
if schema_version == "2":
93+
_require_string(payload.get("model_version"), "model_version")
94+
_require_string(payload.get("scoring_version"), "scoring_version")
9095

9196
if payload["mode"] != "shadow":
9297
raise SignalValidationError("mode must be 'shadow'")

src/research_signal_context_pipelines/theme_momentum.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
THEME_MOMENTUM_HORIZON = "medium"
2626
THEME_MOMENTUM_HORIZON_WINDOW = "2-12 weeks"
2727
THEME_MOMENTUM_HORIZON_WINDOW_ZH = "2-12周"
28+
THEME_MOMENTUM_MODEL_VERSION = "theme-momentum-v1"
29+
THEME_MOMENTUM_SCORING_VERSION = "theme-momentum-rules-v1"
30+
THEME_MOMENTUM_EXPIRY_DAYS = 84
2831

2932

3033
def utc_now_iso() -> str:
@@ -218,9 +221,12 @@ def build_theme_momentum_snapshot(
218221

219222
taxonomy_versions = sorted({theme.taxonomy_version for theme in themes.values() if theme.taxonomy_version})
220223
return {
221-
"schema_version": "1",
224+
"schema_version": "2",
222225
"as_of": snapshot_as_of,
223226
"generated_at": (generated_at or dt.datetime.now(dt.UTC)).isoformat().replace("+00:00", "Z"),
227+
"expires_at": (parse_price_date(snapshot_as_of) + dt.timedelta(days=THEME_MOMENTUM_EXPIRY_DAYS)).isoformat(),
228+
"model_version": THEME_MOMENTUM_MODEL_VERSION,
229+
"scoring_version": THEME_MOMENTUM_SCORING_VERSION,
224230
"mode": "theme_momentum_snapshot",
225231
"artifact_type": THEME_MOMENTUM_ARTIFACT_TYPE,
226232
"horizon": THEME_MOMENTUM_HORIZON,

tests/test_signal_validation.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,33 @@ def test_example_signal_is_valid() -> None:
2121
assert payload["horizon"] == "1-3 years"
2222

2323

24+
def test_v2_signal_requires_versioned_model_metadata() -> None:
25+
payload = load_example()
26+
payload.update(
27+
{
28+
"schema_version": "2",
29+
"model_version": "shadow-v2",
30+
"scoring_version": "rules-v2",
31+
}
32+
)
33+
34+
validate_signal(payload)
35+
36+
37+
def test_v1_signal_remains_readable_without_v2_metadata() -> None:
38+
payload = load_example()
39+
40+
validate_signal(payload)
41+
42+
43+
def test_v2_signal_requires_model_and_scoring_versions() -> None:
44+
payload = load_example()
45+
payload["schema_version"] = "2"
46+
47+
with pytest.raises(SignalValidationError, match="model_version"):
48+
validate_signal(payload)
49+
50+
2451
def test_signal_requires_long_horizon_contract() -> None:
2552
payload = load_example()
2653
payload["horizon"] = "1-3 months"

tests/test_theme_momentum.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def test_theme_momentum_ranks_strong_broad_theme_first() -> None:
5858

5959
ranked = snapshot["theme_ranks"]
6060
assert snapshot["artifact_type"] == "medium_horizon_theme_context"
61+
assert snapshot["schema_version"] == "2"
62+
assert snapshot["expires_at"] == "2025-12-30"
63+
assert snapshot["model_version"] == "theme-momentum-v1"
64+
assert snapshot["scoring_version"] == "theme-momentum-rules-v1"
6165
assert snapshot["horizon"] == "medium"
6266
assert snapshot["horizon_window"] == "2-12 weeks"
6367
assert snapshot["horizon_window_label"] == "2-12周"

0 commit comments

Comments
 (0)