2020 write_campaign_corpus ,
2121)
2222from .compare import run_conformance
23+ from .evidence import prepare_evidence_workspace , write_evidence_manifest
2324from .external import AdapterProtocolError , ExternalProcessAdapterFactory
2425from .exit_codes import exit_code_for_artifact
2526from .junit import write_junit
2627from .minimize import minimize_failing_trace
27- from .model import ConformanceConfig , ConformanceError
28+ from .model import ConformanceConfig , ConformanceError , PinnedCandidateIdentity
2829from .qualification import _QualificationOutputReservation , run_qualification
2930from .reproduce import (
3031 discover_failure_metadata ,
@@ -54,6 +55,18 @@ def _add_config_arguments(parser: argparse.ArgumentParser) -> None:
5455
5556def _add_candidate_arguments (parser : argparse .ArgumentParser ) -> None :
5657 parser .add_argument ("--timeout" , type = float , default = 5.0 )
58+ parser .add_argument (
59+ "--candidate-name" ,
60+ help = "Task-pinned candidate name expected in the ready frame." ,
61+ )
62+ parser .add_argument (
63+ "--candidate-revision" ,
64+ help = "Task-pinned revision expected in the ready frame." ,
65+ )
66+ parser .add_argument (
67+ "--candidate-snapshot" ,
68+ help = "Task-pinned snapshot ID expected in the ready frame." ,
69+ )
5770 candidate = parser .add_mutually_exclusive_group (required = True )
5871 candidate .add_argument (
5972 "--candidate-cmd" ,
@@ -163,6 +176,23 @@ def _build_parser() -> argparse.ArgumentParser:
163176 reproduce .add_argument ("--junit-output" )
164177 _add_config_arguments (reproduce )
165178 _add_candidate_arguments (reproduce )
179+
180+ evidence_init = commands .add_parser (
181+ "evidence-init" ,
182+ help = "Create two clean roots for a captured qualification pair." ,
183+ )
184+ evidence_init .add_argument ("candidate_source" )
185+ evidence_init .add_argument ("--workspace" , required = True )
186+ evidence_init .add_argument ("--candidate-name" , required = True )
187+ evidence_init .add_argument ("--candidate-revision" , required = True )
188+ evidence_init .add_argument ("--expected-snapshot" )
189+
190+ evidence_verify = commands .add_parser (
191+ "evidence-verify" ,
192+ help = "Verify two captured qualification bundles and write one manifest." ,
193+ )
194+ evidence_verify .add_argument ("plan" )
195+ evidence_verify .add_argument ("--output" )
166196 return parser
167197
168198
@@ -174,7 +204,32 @@ def _candidate_factory(args) -> ExternalProcessAdapterFactory:
174204 command = command [1 :]
175205 if not command :
176206 raise ConformanceError ("--candidate requires a command" )
177- return ExternalProcessAdapterFactory (command , timeout_seconds = args .timeout )
207+ identity_values = (
208+ args .candidate_name ,
209+ args .candidate_revision ,
210+ args .candidate_snapshot ,
211+ )
212+ if any (value is not None for value in identity_values ) and not all (
213+ value is not None for value in identity_values
214+ ):
215+ raise ConformanceError (
216+ "--candidate-name, --candidate-revision, and --candidate-snapshot "
217+ "must be provided together"
218+ )
219+ expected_identity = (
220+ PinnedCandidateIdentity (
221+ name = args .candidate_name ,
222+ revision = args .candidate_revision ,
223+ snapshot_id = args .candidate_snapshot ,
224+ )
225+ if all (value is not None for value in identity_values )
226+ else None
227+ )
228+ return ExternalProcessAdapterFactory (
229+ command ,
230+ timeout_seconds = args .timeout ,
231+ expected_identity = expected_identity ,
232+ )
178233
179234
180235def _config (args ) -> ConformanceConfig :
@@ -253,6 +308,31 @@ def main(argv: Optional[List[str]] = None) -> int:
253308 print (f"Suite id: { suite .suite_id } " )
254309 print (f"Cases: { len (suite .cases )} " )
255310 return 0
311+ if args .command == "evidence-init" :
312+ plan_path = prepare_evidence_workspace (
313+ args .candidate_source ,
314+ args .workspace ,
315+ candidate_name = args .candidate_name ,
316+ candidate_revision = args .candidate_revision ,
317+ expected_snapshot = args .expected_snapshot ,
318+ )
319+ plan = json .loads (plan_path .read_text (encoding = "utf-8" ))
320+ print (f"Evidence plan written: { plan_path } " )
321+ print (f"Candidate snapshot: { plan ['candidate' ]['snapshot_id' ]} " )
322+ for run in plan ["runs" ]:
323+ print (
324+ f"{ run ['run_id' ]} : candidate={ run ['candidate_root' ]} "
325+ f"qualification={ run ['qualification_dir' ]} "
326+ )
327+ return 0
328+ if args .command == "evidence-verify" :
329+ manifest_path = write_evidence_manifest (args .plan , args .output )
330+ manifest = json .loads (manifest_path .read_text (encoding = "utf-8" ))
331+ print (f"Evidence manifest written: { manifest_path } " )
332+ print (f"Manifest: { manifest ['manifest_id' ]} " )
333+ print (f"Qualification: { manifest ['runs' ][0 ]['qualification_id' ]} " )
334+ print ("Evidence pair: PASS" )
335+ return 0
256336 if args .command == "run" :
257337 _require_distinct_paths (args .events , args .output , args .junit_output )
258338 single_report = run_conformance (
0 commit comments