5252
5353from __future__ import annotations
5454
55+ import contextlib
5556import copy
57+ import io
5658import json
5759import os
5860import sys
61+ import tempfile
5962from typing import Any
6063
6164sys .path .insert (0 , os .path .join (os .path .dirname (__file__ ), ".." ))
@@ -231,12 +234,24 @@ def _plan() -> tuple[dict[str, tuple[str, str]], dict[str, dict[str, Any]],
231234 return plan , refusal_entries , sorted (artifact_names ), problems
232235
233236
234- def _foreign_engines (golden_path : str ) -> list [dict [str , Any ]]:
237+ def _foreign_engines (golden_path : str ,
238+ rewriting : bytes | None = None ) -> list [dict [str , Any ]]:
235239 """The engine captures already committed in an artifact that this side did
236240 NOT author. `--write` reads them back and carries them through, because an
237241 engine writes only its own entry: an artifact where one implementation
238242 authored another's capture would be a comparison of one thing against
239- itself."""
243+ itself.
244+
245+ `rewriting` is the input the artifact is being REWRITTEN for (`--write`
246+ only). A committed foreign capture attests the bytes that engine read; when
247+ the input has moved since, that capture is of a different document, and
248+ carrying it through yields an artifact that cannot verify — so the writer
249+ used to refuse, the port's writer refuses until the reference has written
250+ (it demands the artifact's bytes equal the file's), and the documented
251+ two-pass order could not start. A capture of other bytes is not carried:
252+ it is dropped with its reason, exactly like a pre-v3 one, and the port
253+ re-runs and authors its own. Verify mode passes nothing and drops nothing,
254+ so a stale artifact still reads as stale."""
240255 if not os .path .exists (golden_path ):
241256 return []
242257 try :
@@ -245,6 +260,16 @@ def _foreign_engines(golden_path: str) -> list[dict[str, Any]]:
245260 except (OSError , json .JSONDecodeError ):
246261 return []
247262 carried , dropped = carry_foreign (committed .get ("engines" ))
263+ if rewriting is not None :
264+ identity = hash_bytes (rewriting )
265+ fresh = [e for e in carried if e .get ("consumed" ) == identity ]
266+ dropped += [
267+ f"the { e .get ('id' )!r} entry attests other bytes "
268+ f"({ str (e ['consumed' ].get ('digest' ))[:12 ]} … ≠ { identity ['digest' ][:12 ]} …): "
269+ f"the input moved since that engine ran, so its capture is of a "
270+ f"different document"
271+ for e in carried if e .get ("consumed" ) != identity ]
272+ carried = fresh
248273 for reason in dropped :
249274 print (f"NOTE: { os .path .basename (golden_path )} : { reason } . Regenerate it "
250275 f"with OWN_SHADOW_WRITE=1 cargo test -p own-shadow --test engine." )
@@ -1081,6 +1106,55 @@ def _carry_controls() -> list[tuple[str, str]]:
10811106 "this engine's OWN entry was carried forward instead of "
10821107 "re-authored — an engine writes only its own entry, and "
10831108 "carrying its own would replay a stale capture" ))
1109+ fails += _moved_input_controls ()
1110+ return fails
1111+
1112+
1113+ def _moved_input_controls () -> list [tuple [str , str ]]:
1114+ """The writer meets a committed artifact whose INPUT has moved.
1115+
1116+ No committed fixture can reach this: an artifact and its input are always
1117+ regenerated together, so the state only exists between the two halves of a
1118+ regeneration — which is exactly when it deadlocked. Driven on a real
1119+ artifact projected here, with a foreign entry that attests the old bytes."""
1120+ fails : list [tuple [str , str ]] = []
1121+ before = b'{"module": "Moved", "components": []}'
1122+ after = b'{"module": "Moved", "components": []}\n '
1123+ foreign = {"id" : "rust-own-bridge" , "consumed" : hash_bytes (before ),
1124+ "derived" : {"sarif" : {}}, "layers" : []}
1125+ directory = tempfile .mkdtemp ()
1126+ path = os .path .join (directory , "moved.repro.json" )
1127+ try :
1128+ with open (path , "w" , encoding = "utf-8" ) as f :
1129+ json .dump ({"engines" : [{"id" : ENGINE_PYTHON , "layers" : []}, foreign ]}, f )
1130+ with contextlib .redirect_stdout (io .StringIO ()) as noted :
1131+ unchanged = _foreign_engines (path , rewriting = before )
1132+ moved = _foreign_engines (path , rewriting = after )
1133+ verifying = _foreign_engines (path )
1134+ finally :
1135+ os .unlink (path )
1136+ os .rmdir (directory )
1137+ if unchanged != [foreign ]:
1138+ fails .append (("carry-moved-input" ,
1139+ "a foreign capture of the SAME bytes was not carried through "
1140+ "a rewrite — an unchanged input must keep the port's entry" ))
1141+ if moved :
1142+ fails .append (("carry-moved-input" ,
1143+ "a foreign capture of OTHER bytes was carried into a rewritten "
1144+ "artifact — it cannot verify there, so the writer refuses and "
1145+ "the port cannot write until the reference has: a regeneration "
1146+ "that cannot start" ))
1147+ if "attests other bytes" not in noted .getvalue ():
1148+ fails .append (("carry-moved-input" ,
1149+ "a foreign capture was dropped without saying why" ))
1150+ if verifying != [foreign ]:
1151+ fails .append (("carry-moved-input" ,
1152+ "VERIFY dropped a foreign capture — only a rewrite may; in "
1153+ "verify a moved input must still read as a stale artifact" ))
1154+ remaining = verify_repro (project_repro (after , moved ))
1155+ if remaining :
1156+ fails .append (("carry-moved-input" ,
1157+ f"the rewritten artifact does not verify: { remaining } " ))
10841158 return fails
10851159
10861160
@@ -1532,7 +1606,8 @@ def write() -> int:
15321606 print (f"wrote { DIGESTS } ({ len (plan )} documents)" )
15331607 for case in artifact_names :
15341608 out = os .path .join (FIXDIR , f"{ case } .repro.json" )
1535- artifact = project_repro (_read (plan [case ][1 ]), _foreign_engines (out ))
1609+ raw = _read (plan [case ][1 ])
1610+ artifact = project_repro (raw , _foreign_engines (out , rewriting = raw ))
15361611 remaining = verify_repro (artifact )
15371612 if remaining :
15381613 print (f"ERROR: { case } : refusing to write an artifact that does not "
0 commit comments