@@ -92,6 +92,17 @@ def test_source_and_ledger_schemas_remain_closed_and_read_only(self):
9292 self .assertFalse (source_schema ["additionalProperties" ])
9393 self .assertFalse (ledger_schema ["additionalProperties" ])
9494 self .assertEqual (source_schema ["properties" ]["schema_version" ]["const" ], "qsl_m0_research_source_snapshot.v1" )
95+ self .assertEqual (source_schema ["properties" ]["source_id" ]["pattern" ], "^[A-Za-z0-9._:/-]{1,128}$" )
96+ self .assertEqual (source_schema ["allOf" ][0 ]["then" ]["properties" ]["errors" ], {"maxItems" : 0 })
97+ provenance_variants = source_schema ["$defs" ]["m0Hypothesis" ]["properties" ]["provenance" ]["oneOf" ]
98+ self .assertEqual (len (provenance_variants ), 2 )
99+ self .assertEqual (provenance_variants [0 ]["properties" ]["source_schema_version" ], {"const" : "5" })
100+ self .assertEqual (provenance_variants [0 ]["properties" ]["source_input_digest" ], {"type" : "null" })
101+ self .assertEqual (provenance_variants [1 ]["properties" ]["source_schema_version" ], {"const" : "6" })
102+ self .assertEqual (
103+ provenance_variants [1 ]["properties" ]["source_input_digest" ],
104+ {"type" : "string" , "pattern" : "^[0-9a-f]{64}$" },
105+ )
95106 self .assertEqual (ledger_schema ["properties" ]["policy" ]["properties" ]["no_order" ], {"const" : True })
96107 self .assertEqual (
97108 ledger_schema ["properties" ]["policy" ]["properties" ]["permitted_next_step" ],
@@ -126,9 +137,11 @@ def test_aggregation_deduplicates_subject_and_source_and_flags_horizon_conflict(
126137 "stale_observation_count" : 0 ,
127138 "unknown_observation_count" : 0 ,
128139 "horizon_conflict_count" : 1 ,
140+ "historical_stale_horizon_drift_count" : 0 ,
129141 })
130142 subject = ledger ["subjects" ][0 ]
131143 self .assertEqual (subject ["horizon_conflict" ], {"status" : "conflict" , "primary_horizons" : ["long" , "medium" ]})
144+ self .assertEqual (subject ["historical_stale_horizon_drift" ], {"status" : "none" , "primary_horizons" : []})
132145 self .assertEqual (subject ["observations" ][0 ]["source_ids" ], ["quant-advisor-research" , "research-mirror" ])
133146
134147 def test_expired_or_stale_source_is_visible_but_cannot_become_fresh (self ):
@@ -143,11 +156,36 @@ def test_expired_or_stale_source_is_visible_but_cannot_become_fresh(self):
143156 self .assertEqual (ledger ["data_status" ], "stale" )
144157 self .assertEqual (ledger ["summary" ]["fresh_observation_count" ], 0 )
145158 self .assertEqual (ledger ["summary" ]["stale_observation_count" ], 2 )
159+ self .assertEqual (ledger ["summary" ]["horizon_conflict_count" ], 0 )
160+ self .assertEqual (ledger ["summary" ]["historical_stale_horizon_drift_count" ], 0 )
146161 self .assertEqual (
147162 {entry ["freshness" ]["status" ] for item in ledger ["subjects" ] for entry in item ["observations" ]},
148163 {"stale" },
149164 )
150165
166+ def test_historical_stale_horizon_drift_does_not_create_a_current_conflict (self ):
167+ fresh = self ._snapshot (self ._hypothesis (primary_horizon = "medium" ), data_status = "ready" )
168+ historical = self ._snapshot (
169+ self ._hypothesis (
170+ report_digest = "d" * 64 ,
171+ entry_digest = "e" * 64 ,
172+ hypothesis_id = "m0r-long-history" ,
173+ primary_horizon = "long" ,
174+ ),
175+ data_status = "stale" ,
176+ )
177+ ledger = m0_research_ledger .aggregate_m0_research_sources (
178+ [fresh , historical ], now = "2026-08-21T12:00:00Z"
179+ )
180+ subject = ledger ["subjects" ][0 ]
181+ self .assertEqual (subject ["horizon_conflict" ], {"status" : "none" , "primary_horizons" : ["medium" ]})
182+ self .assertEqual (
183+ subject ["historical_stale_horizon_drift" ],
184+ {"status" : "drift" , "primary_horizons" : ["long" ]},
185+ )
186+ self .assertEqual (ledger ["summary" ]["horizon_conflict_count" ], 0 )
187+ self .assertEqual (ledger ["summary" ]["historical_stale_horizon_drift_count" ], 1 )
188+
151189 def test_m0_authority_execution_escape_and_source_digest_mismatch_fail_closed (self ):
152190 for mutate , message in (
153191 (lambda value : value .update (authority = "shadow_only" ), "authority_invalid" ),
@@ -165,6 +203,53 @@ def test_m0_authority_execution_escape_and_source_digest_mismatch_fail_closed(se
165203 with self .assertRaisesRegex (m0_research_ledger .M0ResearchLedgerValidationError , "source_report_digest_mismatch" ):
166204 m0_research_ledger .validate_m0_research_source_snapshot (snapshot )
167205
206+ def test_ready_source_errors_and_non_qar_identifier_fail_closed (self ):
207+ snapshot = self ._snapshot (self ._hypothesis ())
208+ snapshot ["errors" ] = ["upstream_timeout" ]
209+ with self .assertRaisesRegex (m0_research_ledger .M0ResearchLedgerValidationError , "ready_source_errors_invalid" ):
210+ m0_research_ledger .validate_m0_research_source_snapshot (snapshot )
211+
212+ hypothesis = self ._hypothesis ()
213+ hypothesis ["subject" ]["identifier" ] = "SOXX=leveraged"
214+ with self .assertRaisesRegex (m0_research_ledger .M0ResearchLedgerValidationError , "subject_identifier_invalid" ):
215+ m0_research_ledger .validate_m0_research_hypothesis (hypothesis )
216+
217+ def test_v5_v6_provenance_pairing_matches_the_closed_schema (self ):
218+ v5 = self ._hypothesis ()
219+ v5 ["provenance" ].update (
220+ source_schema_version = "5" ,
221+ source_contract_version = "model_recommendations.v5" ,
222+ source_input_digest = None ,
223+ )
224+ m0_research_ledger .validate_m0_research_hypothesis (v5 )
225+
226+ invalid_v6 = self ._hypothesis ()
227+ invalid_v6 ["provenance" ]["source_input_digest" ] = None
228+ with self .assertRaisesRegex (m0_research_ledger .M0ResearchLedgerValidationError , "source_input_digest_invalid" ):
229+ m0_research_ledger .validate_m0_research_hypothesis (invalid_v6 )
230+
231+ invalid_v5 = copy .deepcopy (v5 )
232+ invalid_v5 ["provenance" ]["source_contract_version" ] = "model_recommendations.v6"
233+ with self .assertRaisesRegex (m0_research_ledger .M0ResearchLedgerValidationError , "source_contract_version_invalid" ):
234+ m0_research_ledger .validate_m0_research_hypothesis (invalid_v5 )
235+
236+ def test_future_source_metadata_is_omitted_fail_closed (self ):
237+ for field , value in (
238+ ("generated_at" , "2026-08-22T12:00:00Z" ),
239+ ("computed_at" , "2026-08-22T12:00:00Z" ),
240+ ):
241+ with self .subTest (field = field ):
242+ snapshot = self ._snapshot (self ._hypothesis ())
243+ snapshot [field ] = value
244+ if field == "generated_at" :
245+ snapshot ["computed_at" ] = "2026-08-22T12:01:00Z"
246+ ledger = m0_research_ledger .aggregate_m0_research_sources (
247+ [snapshot ], now = "2026-08-21T12:00:00Z"
248+ )
249+ self .assertEqual (ledger ["data_status" ], "unavailable" )
250+ self .assertEqual (ledger ["summary" ]["observation_count" ], 0 )
251+ self .assertEqual (ledger ["errors" ], ["m0_source_future_timestamp" ])
252+
168253 def test_same_subject_and_source_with_different_payloads_is_omitted_fail_closed (self ):
169254 first = self ._hypothesis ()
170255 collision = self ._hypothesis (entry_digest = "f" * 64 , hypothesis_id = "m0r-collision" )
0 commit comments