2020import json
2121import re
2222from collections .abc import Iterable , Mapping
23- from datetime import UTC , datetime
23+ from datetime import UTC , datetime , timedelta
2424from pathlib import Path
2525from typing import Any
2626
@@ -55,6 +55,7 @@ def build_execution_evidence_source_snapshot(
5555 * ,
5656 source_id : str ,
5757 now : datetime | None = None ,
58+ max_report_age : timedelta = timedelta (hours = 36 ),
5859) -> dict [str , Any ]:
5960 """Project eligible runtime reports into the Worker source schema.
6061
@@ -63,7 +64,9 @@ def build_execution_evidence_source_snapshot(
6364 is copied to the output.
6465 """
6566 normalized_source_id = _identity (source_id , "source_id" )
66- computed_at = _timestamp (now or datetime .now (UTC ))
67+ computed_at_value = _normalize_now (now )
68+ if max_report_age < timedelta (minutes = 5 ) or max_report_age > timedelta (days = 7 ):
69+ raise ExecutionEvidenceProjectionError ("max_report_age is outside safe bounds" )
6770 latest_by_deployment : dict [str , tuple [datetime , dict [str , Any ]]] = {}
6871 errors : set [str ] = set ()
6972
@@ -73,18 +76,28 @@ def build_execution_evidence_source_snapshot(
7376 except ExecutionEvidenceProjectionError as exc :
7477 errors .add (str (exc ))
7578 continue
79+ if observed_at > computed_at_value + timedelta (minutes = 5 ):
80+ errors .add ("runtime_report_timestamp_future" )
81+ continue
82+ if observed_at < computed_at_value - max_report_age :
83+ errors .add ("runtime_report_stale" )
84+ continue
7685 previous = latest_by_deployment .get (deployment ["deployment_id" ])
7786 if previous is None or observed_at > previous [0 ]:
7887 latest_by_deployment [deployment ["deployment_id" ]] = (observed_at , deployment )
7988
80- deployments = [entry [1 ] for _ , entry in sorted (latest_by_deployment .items ())]
89+ selected = [entry for _ , entry in sorted (latest_by_deployment .items ())]
90+ deployments = [entry [1 ] for entry in selected ]
8191 if not deployments :
8292 errors .add ("runtime_report_no_eligible_records" )
8393 return {
8494 "schema_version" : SOURCE_SCHEMA_VERSION ,
8595 "source_id" : normalized_source_id ,
86- "generated_at" : computed_at ,
87- "computed_at" : computed_at ,
96+ # The Worker uses the older of generated_at/computed_at for freshness.
97+ # Preserve the oldest included observation so a fresh collection cannot
98+ # make an old platform report appear current.
99+ "generated_at" : _timestamp (min (entry [0 ] for entry in selected )) if selected else None ,
100+ "computed_at" : _timestamp (computed_at_value ),
88101 "data_status" : "ready" if deployments else "unavailable" ,
89102 "deployments" : deployments ,
90103 "errors" : sorted (errors )[:20 ],
@@ -219,6 +232,13 @@ def _timestamp(value: datetime) -> str:
219232 return value .astimezone (UTC ).replace (microsecond = 0 ).isoformat ().replace ("+00:00" , "Z" )
220233
221234
235+ def _normalize_now (value : datetime | None ) -> datetime :
236+ resolved = value or datetime .now (UTC )
237+ if resolved .tzinfo is None or resolved .utcoffset () is None :
238+ raise ExecutionEvidenceProjectionError ("now must be timezone-aware" )
239+ return resolved .astimezone (UTC )
240+
241+
222242def _reject_duplicate_keys (pairs : list [tuple [str , Any ]]) -> dict [str , Any ]:
223243 result : dict [str , Any ] = {}
224244 for key , value in pairs :
@@ -232,6 +252,12 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
232252 parser = argparse .ArgumentParser (description = __doc__ )
233253 parser .add_argument ("--source-id" , required = True , help = "stable non-sensitive source identity" )
234254 parser .add_argument ("--runtime-report" , action = "append" , default = [], help = "path to one runtime_report.v1 JSON document" )
255+ parser .add_argument (
256+ "--max-report-age-hours" ,
257+ type = float ,
258+ default = 36 ,
259+ help = "discard reports older than this bounded freshness window (default: 36)" ,
260+ )
235261 parser .add_argument ("--output" , required = True , help = "path for the generated source snapshot" )
236262 return parser .parse_args (argv )
237263
@@ -245,7 +271,11 @@ def main(argv: list[str] | None = None) -> int:
245271 reports .append (load_runtime_report (path ))
246272 except ExecutionEvidenceProjectionError as exc :
247273 input_errors .append (str (exc ))
248- snapshot = build_execution_evidence_source_snapshot (reports , source_id = args .source_id )
274+ snapshot = build_execution_evidence_source_snapshot (
275+ reports ,
276+ source_id = args .source_id ,
277+ max_report_age = timedelta (hours = args .max_report_age_hours ),
278+ )
249279 snapshot ["errors" ] = sorted (set ([* snapshot ["errors" ], * input_errors ]))[:20 ]
250280 output_path = Path (args .output )
251281 output_path .parent .mkdir (parents = True , exist_ok = True )
0 commit comments