Compare commits

...
20 changed files with 5249 additions and 18 deletions
+53 -17
View File
@@ -1407,16 +1407,6 @@ impl Workflow {
.color(style::MUTED),
);
}
content = content.push(
text(format!(
"{} changes · {} required boundary choices · {} unresolved conflicts · {} modeling warnings",
d.plan.changes.len(),
d.plan.conflicts.iter().filter(|c| d.boundary_choice(p, c)).count(),
d.plan.conflicts.iter().filter(|c| !d.boundary_choice(p, c)).count(),
d.plan.warnings.len()
))
.size(12),
);
}
}
let enabled = match self {
@@ -1433,12 +1423,50 @@ impl Workflow {
.color(iced::Color::from_rgb8(178, 54, 58)),
);
}
column![
scrollable(content.padding(iced::Padding {
right: 14.,
..Default::default()
}))
.height(480),
let mut footer = column![].spacing(8);
if let Self::Transition(d) = self {
let boundary_choices = d
.plan
.conflicts
.iter()
.filter(|c| d.boundary_choice(p, c))
.count();
footer = footer.push(
text(format!(
"{} reviewed items · {} boundary choices · {} conflicts · {} warnings",
d.plan.changes.len(),
boundary_choices,
d.plan.conflicts.len() - boundary_choices,
d.plan.warnings.len()
))
.size(12),
);
let guidance = if d.destination_error(p).is_some() {
Some("Choose a compatible diagram in Show result in, above the mappings.".into())
} else if boundary_choices > 0 {
Some("Choose the counterpart kind in each mapping that needs a system boundary decision.".into())
} else if let Some(field) = d.plan.conflicts.iter().find_map(|c| c.field) {
let field = match field {
TransitionField::Name => "Name",
TransitionField::Description => "Description",
};
Some(format!(
"Review {field} below the mappings: choose Keep target, Take source, or merged text."
))
} else if !d.plan.conflicts.is_empty() {
Some("Review the conflict messages below the mappings and complete the required decisions.".into())
} else {
None
};
if let Some(guidance) = guidance {
footer = footer.push(
text(guidance)
.size(12)
.color(iced::Color::from_rgb8(178, 54, 58)),
);
}
}
footer = footer.push(
row![
Space::new().width(Fill),
button(text("Cancel").size(12))
@@ -1458,7 +1486,15 @@ impl Workflow {
.padding(10)
.on_press_maybe(enabled.then_some(Message::Workflow(Event::Apply)))
]
.spacing(10)
.spacing(10),
);
column![
scrollable(content.padding(iced::Padding {
right: 14.,
..Default::default()
}))
.height(480),
footer
]
.spacing(18)
.into()
@@ -0,0 +1,55 @@
# Closed lifecycle checker and footer gates
Run `python verify.py` with Python 3.10 or newer. Verification uses only these
package files and the standard library; it does not execute archived source,
binaries, Cargo, Git or network requests. `prepare.py [CHECKOUT]` reproducibly
packages the explicitly named raw captures if those local originals are present.
The input capsules are source snapshots, not a hermetic toolchain distribution.
Ten closed Cargo runs are preserved: footer-format-1, footer-windows-build-1,
footer-windows-build-2, footer-clippy-1, lifecycle-producer-build-1,
lifecycle-producer-generate-1, lifecycle-producer-build-2, footer-windows-build-3,
footer-format-2 and lifecycle-producer-format-1. Each remains bound
to its original command, source inventory, helper, logs and receipts. The first
footer build has no JSON executable artifact records; the second captures them.
Recorded HEAD d0b7c28 does not identify uncommitted footer/producer source bytes;
the archived per-run input manifests do.
The three final gates use the same 715-file manifest, SHA-256
`b5a62817a1b5df936b9d759241bdfc4e8283901c1d4b540b9e494995e1544d8d`.
Compared with producer build2's captured inputs, only the footer literal changes
from "changes" to "reviewed items". The verifier checks that exact byte delta.
The final GUI build and both format checks pass. Earlier lint and oracle captures
retain their original input scope; they are not presented as reruns of the final
wording. The checker source is unchanged by that label refinement.
Producer build1 and its generation succeeded, but that older checker incorrectly
rejected expected engineering warnings during saved-file admission. Build2
contains the corrected admission logic. The 40 PASS falsifier trials are bound
to build2's exact checker artifact receipt and producer source, not to build1.
Build success alone does not demonstrate checker correctness. The earlier native
admission failure is outside these selected captures, not fabricated as a Cargo
failure or silently converted into successful native evidence.
The oracle archive retains all original fixtures, repeated generation outputs,
mutated specimens, identity receipts, copied build evidence, runner/source,
stdout/stderr and per-trial results. The 40 expected outcomes include accepted
original/deleted/recreated/relinked states and deliberate rejection cases for
retained prose/geometry corruption, source/trace identity drift, missing persisted
IDs, absent identity receipts and receipt overwrite. These are synthetic checker
tests, not native interaction. A PASS summary includes expected negative exits.
`manifest.json` maps every logical raw path to a stored member. Only byte-identical
inputs.json/inputs.zip files are deduplicated; exact run mapping is retained.
`files.json` pins outer files including the reproducible packager. Outer text is
UTF-8 LF; all raw archived bytes are preserved. The verifier checks exact safe ZIP
membership, hashes, nested input contents/counts, helpers, timestamps, receipt and
binary-pin relationships, repeated fixture equality, watched specimen identities,
all 40 outcomes and their recorded rejection text. It rereads evidence rather
than rerunning the model checker, and does not authenticate historical execution.
Native trial1 lacks a closing exit receipt and is excluded. No successful native
deletion, Undo, relinking, save/reopen, or full lifecycle acceptance is claimed.
No runtime binaries, credentials or whole user profile are included. Recorded
absolute paths are historical labels; verification maps them to archive members
without accessing the original machine. Binary hashes remain receipt observations.
@@ -0,0 +1,22 @@
{
"evidence.zip": {
"bytes": 14254708,
"sha256": "663bf83db4ead276063b321fd5e9857e29d2305e6046c2e87dbbac87a366b19c"
},
"manifest.json": {
"bytes": 79071,
"sha256": "5d2be3d68206871a560dd1a83064ff779e8aab6e11302d09aaf1a655502dd38e"
},
"README.md": {
"bytes": 3696,
"sha256": "0e33fe1a85a1ee17ebde57ed7994280dcb95d3a7c4338b78c1f7e0ae9fdcbb7d"
},
"verify.py": {
"bytes": 10908,
"sha256": "1e8ba1cc2759fbe76e14636a37925c9dfce9a6f14bf63569acb76498bd8380ad"
},
"prepare.py": {
"bytes": 3572,
"sha256": "147a63f26b7131076df52057f1cc263dfefdf99d937b2c2a819217a46c56105f"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
"""Repackage the explicitly closed captures; reads raw evidence, never runs Cargo."""
from pathlib import Path
import hashlib, json, sys, zipfile
OUT=Path(__file__).resolve().parent
ROOT=Path(sys.argv[1]).resolve() if len(sys.argv)>1 else OUT.parents[3]
GATES=ROOT/'.local/transition-gates'
ORACLE=ROOT/'.local/transition-native/oracle-falsifiers-1'
RUNS=['footer-format-1','footer-windows-build-1','footer-windows-build-2','footer-clippy-1',
'lifecycle-producer-build-1','lifecycle-producer-generate-1','lifecycle-producer-build-2',
'footer-windows-build-3','footer-format-2','lifecycle-producer-format-1']
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
def main():
payload={};logical={};run_specs={}
def add(name,b):
# Deduplicate only byte-identical input manifests and source archives.
stored='capsules/'+pin(b)['sha256']+Path(name).suffix if Path(name).name in ('inputs.json','inputs.zip') else name
if stored in payload:assert payload[stored]==b
payload[stored]=b;logical[name]=stored
for run in RUNS:
result=json.loads((GATES/run/'result.json').read_bytes())
assert 'finish' in result and result['inputsUnchanged'] is True,run
for f in sorted((GATES/run).iterdir()):
assert f.is_file(),f
add('cargo/'+run+'/'+f.name,f.read_bytes())
helpers=[f for f in GATES.glob('*.py') if pin(f.read_bytes())==result['helper']]
assert helpers,run+' helper unavailable'
helper='helpers/'+result['helper']['sha256']+'.py'
add(helper,helpers[0].read_bytes())
run_specs[run]={k:result[k] for k in ('command','head','exitCode','inputCount')}
run_specs[run]['helper']=helper
for f in sorted(ORACLE.rglob('*')):
if f.is_file():add('oracle/'+f.relative_to(ORACLE).as_posix(),f.read_bytes())
summary=json.loads((ORACLE/'summary.json').read_bytes())
assert summary['status']=='PASS' and summary['trialCount']==40
manifest={'schema':1,'captureCheckout':'d0b7c285018a516c6dc34c196d1359a861ddaeb3',
'cargo':run_specs,'oracleExpectedTrials':summary['trials'],
'oracleOriginalDirectory':str(ORACLE).replace('\\','/'),
'logicalFiles':dict(sorted(logical.items())),
'members':{n:pin(b) for n,b in sorted(payload.items())}}
with zipfile.ZipFile(OUT/'evidence.zip','w',compression=zipfile.ZIP_DEFLATED,compresslevel=9) as z:
for name,b in sorted(payload.items()):
info=zipfile.ZipInfo(name,date_time=(2026,9,13,0,0,0));info.compress_type=zipfile.ZIP_DEFLATED
z.writestr(info,b)
(OUT/'manifest.json').write_bytes((json.dumps(manifest,indent=2)+'\n').encode())
for name in ('README.md','verify.py','prepare.py'):
(OUT/name).write_bytes((OUT/name).read_bytes().replace(b'\r\n',b'\n'))
files={name:pin((OUT/name).read_bytes()) for name in ('evidence.zip','manifest.json','README.md','verify.py','prepare.py')}
(OUT/'files.json').write_bytes((json.dumps(files,indent=2)+'\n').encode())
with zipfile.ZipFile(OUT/'evidence.zip') as z:
assert all(z.read(logical[n])==b for n,b in (
[('cargo/'+r+'/'+f.name,f.read_bytes()) for r in RUNS for f in sorted((GATES/r).iterdir())]+
[('oracle/'+f.relative_to(ORACLE).as_posix(),f.read_bytes()) for f in sorted(ORACLE.rglob('*')) if f.is_file()]))
print(json.dumps(files,indent=2))
print('Exact raw fidelity:',len(logical),'logical files;',len(payload),'stored members')
if __name__=='__main__':main()
@@ -0,0 +1,132 @@
"""Read-only standard-library verification; no archived source/binary execution."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import hashlib, io, json, zipfile
ROOT=Path(__file__).resolve().parent
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
def need(ok,msg):
if not ok:raise ValueError(msg)
def safe(n):return not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n
def members(z,expected):
names=z.namelist()
need(len(names)==len(set(names)) and set(names)==set(expected),'archive membership')
need(all(safe(n) for n in names),'unsafe archive member')
def time_order(*values):return list(map(datetime.fromisoformat,values))==sorted(map(datetime.fromisoformat,values))
def main():
files=json.loads((ROOT/'files.json').read_bytes())
need(set(files)=={'evidence.zip','manifest.json','README.md','verify.py','prepare.py'},'outer inventory')
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
m=json.loads((ROOT/'manifest.json').read_bytes())
with zipfile.ZipFile(ROOT/'evidence.zip') as z:
members(z,m['members'])
for n,p in m['members'].items():need(pin(z.read(n))==p,'member hash '+n)
logical=m['logicalFiles']
need(all(safe(n) for n in logical) and set(logical.values())==set(m['members']),'logical mapping')
def raw(n):return z.read(logical[n])
def obj(n):return json.loads(raw(n))
def directory(prefix):return {n[len(prefix):]:pin(raw(n)) for n in logical if n.startswith(prefix)}
pairs=set()
for run,spec in m['cargo'].items():
p='cargo/'+run+'/'
start=obj(p+'started.json');result=obj(p+'result.json');process=obj(p+'process.json')
need(all(result[k]==v for k,v in start.items()),run+' start/result')
need(result['command']==process['command']==spec['command'],run+' commands')
need(result['head']==spec['head'] and result['exitCode']==spec['exitCode'],run+' recorded outcome')
need(result['inputsUnchanged'] is True and result['seconds']>=0,run+' source/duration')
need(process['pid']>0 and time_order(start['start'],process['observed'],result['finish']),run+' process times')
for field,n in [('inputs','inputs.json'),('archive','inputs.zip'),('stdout','stdout.log'),('stderr','stderr.log')]:need(result[field]==pin(raw(p+n)),run+' '+field)
need(result['helper']==pin(raw(spec['helper'])),run+' helper')
inputs=obj(p+'inputs.json')
need(len(inputs)==result['inputCount']==spec['inputCount'],run+' input count')
need(sum(n.startswith('crates/vendor/iced_widget/') for n in inputs)==71,run+' vendor inventory')
pair=(logical[p+'inputs.json'],logical[p+'inputs.zip'])
if pair not in pairs:
with zipfile.ZipFile(io.BytesIO(raw(p+'inputs.zip'))) as source:
members(source,inputs)
for n,expected in inputs.items():need(pin(source.read(n))==expected,run+' source '+n)
pairs.add(pair)
artifacts=[]
for line in raw(p+'stdout.log').decode('utf-8',errors='replace').splitlines():
try:item=json.loads(line)
except json.JSONDecodeError:continue
if isinstance(item,dict) and item.get('reason')=='compiler-artifact' and item.get('executable'):artifacts.append(item)
need(artifacts==[a['cargo'] for a in result['artifacts']],run+' artifact relationships')
print(run,'exit',result['exitCode'],'inputs',len(inputs),'artifact records',len(artifacts))
# Successful build1/generation are not the corrected checker admission.
older=obj('cargo/lifecycle-producer-build-1/inputs.json')
newer=obj('cargo/lifecycle-producer-build-2/inputs.json')
need(older==obj('cargo/lifecycle-producer-generate-1/inputs.json'),'generation uses older producer inputs')
source_path='harness/experiments/transition-native-lifecycle-20260913/producer/src/main.rs'
need(older[source_path]!=newer[source_path],'distinct producer revisions')
final_runs=('footer-windows-build-3','footer-format-2','lifecycle-producer-format-1')
final_inputs=obj('cargo/footer-windows-build-3/inputs.json')
final_pin='b5a62817a1b5df936b9d759241bdfc4e8283901c1d4b540b9e494995e1544d8d'
for run in final_runs:
p='cargo/'+run+'/'
need(pin(raw(p+'inputs.json'))['sha256']==final_pin and obj(p+'inputs.json')==final_inputs,run+' final source manifest')
need(obj(p+'result.json')['exitCode']==0 and len(final_inputs)==715,run+' final successful gate')
for run in final_runs[1:]:
need(not raw('cargo/'+run+'/stdout.log') and not raw('cargo/'+run+'/stderr.log'),run+' empty format streams')
final_build=obj('cargo/footer-windows-build-3/result.json')
need(len(final_build['artifacts'])==1 and final_build['artifacts'][0]['cargo']['target']['name']=='arcadia-studio','final GUI artifact')
changed={n for n in set(newer)|set(final_inputs) if newer.get(n)!=final_inputs.get(n)}
need(changed=={'crates/oa-desktop/src/workflow.rs'},'final wording-only file scope')
with zipfile.ZipFile(io.BytesIO(raw('cargo/lifecycle-producer-build-2/inputs.zip'))) as before, zipfile.ZipFile(io.BytesIO(raw('cargo/footer-windows-build-3/inputs.zip'))) as after:
path='crates/oa-desktop/src/workflow.rs';a=before.read(path);b=after.read(path)
old='{} changes · {} boundary choices · {} conflicts · {} warnings'.encode()
new='{} reviewed items · {} boundary choices · {} conflicts · {} warnings'.encode()
need(a.count(old)==1 and a.replace(old,new,1)==b,'exact final footer label delta')
build=obj('cargo/lifecycle-producer-build-2/result.json')
artifact=[a for a in build['artifacts'] if a['cargo']['target']['name']=='transition-native-lifecycle']
need(len(artifact)==1,'corrected checker artifact')
# Oracle copied build capsule must match the independently included gate.
for name in ('result.json','started.json','process.json','inputs.json','inputs.zip','stdout.log','stderr.log'):
need(raw('oracle/build-capture/'+name)==raw('cargo/lifecycle-producer-build-2/'+name),'oracle build capsule '+name)
need(pin(raw('oracle/build-capture/build-helper.py'))==build['helper'],'oracle build helper')
start=obj('oracle/started.json');summary=obj('oracle/summary.json')
need(summary['status']=='PASS' and summary['trialCount']==40,'oracle completion')
need(all(summary['context'][k]==v for k,v in start.items() if k!='start'),'oracle context')
need(start['checker']==artifact[0]['readback'] and start['buildReceipt']==pin(raw('cargo/lifecycle-producer-build-2/result.json')),'oracle checker chain')
need(start['runner']==pin(raw('oracle/oracle-falsifiers.py')),'oracle runner pin')
for path,p in directory('oracle/producer/').items():need(p==newer['harness/experiments/transition-native-lifecycle-20260913/producer/'+path],'oracle producer source '+path)
fixtures=directory('oracle/fixtures/')
need(fixtures==start['fixtureInput']==directory('oracle/repeat-generation-a/')==directory('oracle/repeat-generation-b/'),'deterministic repeated fixtures')
need(directory('oracle/specimens/')==summary['specimens'],'mutation inventory')
rows=summary['trials']
need(rows==m['oracleExpectedTrials'] and len(rows)==len({r['label'] for r in rows})==40,'forty distinct trials')
actual_labels={n.split('/')[2] for n in logical if n.startswith('oracle/trials/')}
need(actual_labels=={r['label'] for r in rows},'exact trial membership')
# Original absolute names are mapped to archive members, never opened.
original=m['oracleOriginalDirectory'].rstrip('/')+'/'
def mapped(path):
norm=path.replace('\\','/')
need(norm.startswith(original),'unmapped trial input path')
rel=norm[len(original):];need(safe(rel),'unsafe trial input mapping')
return 'oracle/'+rel
for row in rows:
label=row['label'];p='oracle/trials/'+label+'/'
r=obj(p+'result.json')
need(r['label']==label and r['exitCode']==r['expectedExitCode']==row['exitCode']==row['expectedExitCode'],label+' outcome')
need(time_order(start['start'],r['start'],r['finish'],summary['finish']),label+' time order')
need(r['checkerBefore']==r['checkerAfter']==start['checker'] and r['runner']==start['runner'] and r['buildReceipt']==start['buildReceipt'],label+' provenance')
for stream in ('stdout','stderr'):need(r[stream]==pin(raw(p+stream+'.log')),label+' '+stream)
need(r['watchedBefore']==r['watchedAfter'],label+' unchanged inputs')
for path,expected in r['watchedBefore'].items():need(pin(raw(mapped(path)))==expected,label+' watched input')
cmd=r['command'];need(cmd[0]==artifact[0]['cargo']['executable'],label+' executable command')
if cmd[1]=='check':
need(mapped(cmd[3]) in logical,label+' check entry')
opts=cmd[4:];need(len(opts)%2==0,label+' option arity')
for flag,path in zip(opts[::2],opts[1::2]):
need(flag in ('--identities','--write-identities') and mapped(path) in logical,label+' identity option')
if r['exitCode']==0:
report=obj(p+'stdout.log')
need(report['status']=='PASS' and report['comparison']=='complete Project equality' and report['state']==cmd[2] and report['entry']==cmd[3],label+' positive report')
if cmd[2].endswith(('-reopened','-noop')):need(report['trace_identity_pinned'] is True and '--identities' in opts,label+' continuity receipt')
if '--write-identities' in opts:need(obj(p+'written-identities.json')==report['new_ids']==obj(mapped(opts[opts.index('--write-identities')+1])),label+' identity write')
else:need(cmd[1]=='generate' and len(cmd)==3,label+' generation command')
stderr=raw(p+'stderr.log').decode('utf-8',errors='replace')
if r['expectedStderrFragment']:need(r['expectedStderrFragment'] in stderr,label+' rejection reason')
elif r['exitCode']==0:need(not stderr,label+' successful stderr')
print('PASS:',len(m['cargo']),'closed Cargo runs; 40 recorded oracle trials;',len(logical),'logical raw files;',len(m['members']),'stored members;',len(pairs),'source capsule pairs.')
print('Offline consistency only: no binaries/source executed; no native lifecycle completion claim.')
if __name__=='__main__':main()
@@ -0,0 +1,45 @@
# Closed native transition lifecycle captures
Keep this directory beside `../checks/`. Run `python verify.py` with Python 3.10
or newer. Verification requires the exact pinned `../checks/evidence.zip` and
these native package files, uses only the standard library, and executes no
archived helper, application, checker, Cargo command or network request.
`prepare.py [CHECKOUT]` rebuilds this archive from the named closed originals.
Trials lifecycle-2/3 capture OA deletion, recreation, first relink, a separate
reopening and no-op check. Trials lifecycle-4/5 capture reverse SA deletion,
Undo/Redo, recreation, first relink, reopening and no-op. All four GUI receipts
close successfully with empty stdout/stderr and unchanged fixture/binary pins.
Trial1 is excluded entirely because it lacked a closing process receipt. This
package does not turn that interrupted history into successful evidence.
Trials 2/3 use footer-windows-build-2; trials 4/5 use footer-windows-build-3 with
the refined "reviewed items" footer. Snapshot checks use the corrected producer
build2. Exact build receipts, helpers, artifact records and complete source
capsules are read from the pinned sibling checks archive; source proof is not
self-contained in the native archive alone. Runtime binaries are excluded.
Every original frame, adjacent metadata, trial file, accepted identity receipt,
saved source snapshot and helper is retained without editing. The launch-pinned
captureHelper refers to the corresponding capture-lifecycle-N.js source. Each
trial's separately recorded capture-function.js is the serialized function used
in the persistent UI session; it is different bytes and was not that launch pin.
The files document the distinction rather than asserting byte equivalence or
authenticated execution. Frame labels/timestamps/application paths record states,
not a complete action invocation log or independent PID attestation. Image/window
dimensions vary; these captures are not all minimum-window checks.
The verifier checks exact safe archive membership, hashes, linked build/source
capsules, trial/helper/stream relationships, frame membership and timestamps,
snapshot helper and corrected-checker provenance, and the recorded complete
Project comparison results. Accepted first-relink IDs match later reopen/no-op
receipts in each direction, and both later saved source bundles are byte-identical
to their first-relink bundle. The producer's full-model result is recorded evidence;
the portable verifier does not rerun its Rust comparisons or derive model equality
from screenshots. Snapshot receipts record start time but no completion timestamp;
only their start can be located in the GUI process interval.
These finite workflows do not establish exhaustive usability/accessibility,
performance, deployment readiness, or behavior on another platform. No credentials,
whole user profile, or runtime binaries are packaged. Outer text is UTF-8 LF;
archived raw bytes and historical local-path strings remain unchanged.
@@ -0,0 +1,22 @@
{
"evidence.zip": {
"bytes": 6888298,
"sha256": "b53253fbe9b98db6a0028ed3629426460e7086c8943be0d13ab5ec540e7e4b68"
},
"manifest.json": {
"bytes": 56348,
"sha256": "f6bd0bb5e3d29d5e47f1dfb8f1cb7747a3c2e12aa62b6e4e81b68e68ce5ccf95"
},
"README.md": {
"bytes": 3019,
"sha256": "01aed34547156bede9c2fea75162c8fecda18203fa575c4cf81e28e440130a95"
},
"verify.py": {
"bytes": 10474,
"sha256": "5f93ff8b266c0c7c3e64630f64a522ff5f8f8bd41d7d0a14cc4856c16a529306"
},
"prepare.py": {
"bytes": 2861,
"sha256": "c1884b10724cc618b58abd84b93c03623ea9e80dbda4e5a8b866fff499e93fca"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
"""Package only closed lifecycle trials 2-5; never runs a GUI or checker."""
from pathlib import Path
import hashlib,json,sys,zipfile
OUT=Path(__file__).resolve().parent
ROOT=Path(sys.argv[1]).resolve() if len(sys.argv)>1 else OUT.parents[3]
SRC=ROOT/'.local/transition-native'
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
def main():
payload={};runs={}
for number in (2,3,4,5):
run=f'lifecycle-{number}';folder=SRC/run
result=json.loads((folder/'result.json').read_bytes())
assert result['exitCode']==0 and 'finish' in result,run+' not closed successfully'
for f in sorted(folder.rglob('*')):
if f.is_file():payload['native/'+f.relative_to(SRC).as_posix()]=f.read_bytes()
helpers={}
for field,name in [('runner',f'run-lifecycle-{number}.py'),('launchCapture',f'capture-lifecycle-{number}.js'),('snapshot',f'check-lifecycle-snapshot-{number}.py')]:
helpers[field]='helpers/'+name;payload[helpers[field]]=(SRC/name).read_bytes()
captures=[n for n,b in payload.items() if n.startswith('native/'+run+'/') and n.endswith('.json') and isinstance(json.loads(b),dict) and 'images' in json.loads(b)]
snapshots={f.name:json.loads((f/'result.json').read_bytes())['command'][2] for f in sorted((folder/'snapshots').iterdir()) if f.is_dir()}
runs[run]={'helpers':helpers,'guiBuild':'footer-windows-build-2' if number<4 else 'footer-windows-build-3','captures':captures,'snapshots':snapshots}
for f in sorted((SRC/'lifecycle-input-1').rglob('*')):
if f.is_file():payload['fixtures/'+f.relative_to(SRC/'lifecycle-input-1').as_posix()]=f.read_bytes()
checks=OUT.parent/'checks'
payload['references/checks-manifest.json']=(checks/'manifest.json').read_bytes()
manifest={'schema':1,'checksEvidence':pin((checks/'evidence.zip').read_bytes()),
'checksRelativePath':'../checks/evidence.zip','originalNativeDirectory':str(SRC).replace('\\','/'),
'runs':runs,'members':{n:pin(b) for n,b in sorted(payload.items())}}
with zipfile.ZipFile(OUT/'evidence.zip','w',compression=zipfile.ZIP_DEFLATED,compresslevel=9) as z:
for n,b in sorted(payload.items()):
info=zipfile.ZipInfo(n,date_time=(2026,9,22,0,0,0));info.compress_type=zipfile.ZIP_DEFLATED;z.writestr(info,b)
(OUT/'manifest.json').write_bytes((json.dumps(manifest,indent=2)+'\n').encode())
for n in ('README.md','verify.py','prepare.py'):(OUT/n).write_bytes((OUT/n).read_bytes().replace(b'\r\n',b'\n'))
files={n:pin((OUT/n).read_bytes()) for n in ('evidence.zip','manifest.json','README.md','verify.py','prepare.py')}
(OUT/'files.json').write_bytes((json.dumps(files,indent=2)+'\n').encode())
print(json.dumps(files,indent=2));print('Native raw members:',len(payload))
if __name__=='__main__':main()
@@ -0,0 +1,115 @@
"""Read-only offline verification. Requires the pinned sibling checks/evidence.zip."""
from pathlib import Path,PurePosixPath
from datetime import datetime
import hashlib,io,json,zipfile
ROOT=Path(__file__).resolve().parent
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
def need(ok,msg):
if not ok:raise ValueError(msg)
def safe(n):return not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n
def members(z,expected):
names=z.namelist();need(len(names)==len(set(names)) and set(names)==set(expected),'exact ZIP membership');need(all(safe(n) for n in names),'safe ZIP members')
def ordered(*times):return list(map(datetime.fromisoformat,times))==sorted(map(datetime.fromisoformat,times))
def main():
files=json.loads((ROOT/'files.json').read_bytes())
need(set(files)=={'evidence.zip','manifest.json','README.md','verify.py','prepare.py'},'outer inventory')
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
m=json.loads((ROOT/'manifest.json').read_bytes())
need(m['checksRelativePath']=='../checks/evidence.zip','fixed sibling evidence location')
checks_bytes=(ROOT/'../checks/evidence.zip').read_bytes()
need(pin(checks_bytes)==m['checksEvidence'],'pinned sibling checks archive')
with zipfile.ZipFile(ROOT/'evidence.zip') as z,zipfile.ZipFile(io.BytesIO(checks_bytes)) as checks:
members(z,m['members'])
for n,p in m['members'].items():need(pin(z.read(n))==p,'native member hash '+n)
def raw(n):return z.read(n)
def obj(n):return json.loads(raw(n))
def directory(prefix):return {n[len(prefix):]:pin(raw(n)) for n in m['members'] if n.startswith(prefix)}
cm=obj('references/checks-manifest.json');members(checks,cm['members'])
def craw(logical):
n=cm['logicalFiles'][logical];b=checks.read(n);need(pin(b)==cm['members'][n],'checks member '+n);return b
def cobj(n):return json.loads(craw(n))
# Source capsules and receipt helper chains are independently checked here.
builds={};artifacts={};pairs=set()
for run in ('footer-windows-build-2','footer-windows-build-3','lifecycle-producer-build-2'):
p='cargo/'+run+'/';r=cobj(p+'result.json');start=cobj(p+'started.json');process=cobj(p+'process.json')
need(all(r[k]==v for k,v in start.items()) and r['exitCode']==0 and r['inputsUnchanged'] is True,run+' build closure')
need(r['command']==process['command'] and ordered(r['start'],process['observed'],r['finish']),run+' build process')
for field,n in [('inputs','inputs.json'),('archive','inputs.zip'),('stdout','stdout.log'),('stderr','stderr.log')]:need(r[field]==pin(craw(p+n)),run+' receipt '+field)
need(r['helper']==pin(craw(cm['cargo'][run]['helper'])),run+' build helper')
inputs=cobj(p+'inputs.json');need(len(inputs)==r['inputCount'] and sum(n.startswith('crates/vendor/iced_widget/') for n in inputs)==71,run+' input inventory')
pair=(r['inputs']['sha256'],r['archive']['sha256'])
if pair not in pairs:
with zipfile.ZipFile(io.BytesIO(craw(p+'inputs.zip'))) as source:
members(source,inputs)
for n,expected in inputs.items():need(pin(source.read(n))==expected,run+' source '+n)
pairs.add(pair)
records=[]
for line in craw(p+'stdout.log').decode('utf-8',errors='replace').splitlines():
try:item=json.loads(line)
except json.JSONDecodeError:continue
if isinstance(item,dict) and item.get('reason')=='compiler-artifact' and item.get('executable'):records.append(item)
need(records==[a['cargo'] for a in r['artifacts']],run+' artifact records')
expected='transition-native-lifecycle' if run.startswith('lifecycle') else 'arcadia-studio'
selected=[a for a in r['artifacts'] if a['cargo']['target']['name']==expected];need(len(selected)==1,run+' unique artifact')
builds[run]=r;artifacts[run]=selected[0]
original=m['originalNativeDirectory'].rstrip('/')+'/'
def mapped(path):
norm=path.replace('\\','/');need(norm.startswith(original),'snapshot path outside captured native root')
rel=norm[len(original):];need(safe(rel),'unsafe snapshot path');return 'native/'+rel
fixtures=directory('fixtures/');snapshots={};reports={};capture_count=image_count=0
need(set(m['runs'])=={'lifecycle-2','lifecycle-3','lifecycle-4','lifecycle-5'},'closed trial membership')
for run,spec in m['runs'].items():
p='native/'+run+'/';r=obj(p+'result.json');start=obj(p+'started.json');process=obj(p+'process.json')
build=builds[spec['guiBuild']];artifact=artifacts[spec['guiBuild']]
need(all(r[k]==v for k,v in start.items()),run+' start/result relationship')
need(r['exitCode']==0 and r['inputUnchanged'] is True and r['binary']==r['binaryAfter']==artifact['readback'],run+' native closure and binary pin')
need(r['buildReceipt']==pin(craw('cargo/'+spec['guiBuild']+'/result.json')) and r['buildHead']==build['head'],run+' GUI build relationship')
need(r['command']==[artifact['cargo']['executable']] and process['pid']>0,run+' native command/PID')
need(ordered(build['finish'],r['start'],process['observed'],r['finish']),run+' native times')
need(r['helper']==pin(raw(spec['helpers']['runner'])) and r['captureHelper']==pin(raw(spec['helpers']['launchCapture'])),run+' launch helper pins')
need(p+'capture-function.js' in m['members'],run+' actual capture-function record')
need(r['inputs']==pin(raw(p+'inputs.json')) and obj(p+'inputs.json')==fixtures,run+' unchanged fixture bytes')
for stream in ('stdout','stderr'):need(not raw(p+stream+'.log') and r[stream]==pin(raw(p+stream+'.log')),run+' empty '+stream)
images=[]
actual_captures=[n for n in m['members'] if n.startswith(p) and n.endswith('.json') and isinstance(obj(n),dict) and 'images' in obj(n)]
need(set(actual_captures)==set(spec['captures']) and len(actual_captures)==len(spec['captures']),run+' exact metadata coverage')
for n in spec['captures']:
frame=obj(n);need(ordered(r['start'],frame['time'],r['finish']),n+' frame timestamp')
need(frame['window']['app'].replace('\\','/')=='process:'+r['command'][0].replace('\\','/'),n+' window executable path')
for image in frame['images']:
name=p+image['file'];b=raw(name)
need(image['width']>0 and image['height']>0 and (b.startswith(b'\xff\xd8') or b.startswith(b'\x89PNG')),n+' image record')
images.append(name)
actual_images={n for n in m['members'] if n.startswith(p) and n.endswith(('.jpg','.png'))}
need(len(images)==len(set(images)) and set(images)==actual_images,run+' all original images')
capture_count+=len(spec['captures']);image_count+=len(images)
actual_snapshots={n.split('/')[3] for n in m['members'] if n.startswith(p+'snapshots/')}
need(actual_snapshots==set(spec['snapshots']),run+' snapshot inventory')
for label,state in spec['snapshots'].items():
q=p+'snapshots/'+label+'/';s=obj(q+'result.json');ss=obj(q+'started.json');report=obj(q+'stdout.log')
checker=artifacts['lifecycle-producer-build-2']
need(all(s[k]==v for k,v in ss.items()) and s['exitCode']==0 and s['binaryUnchanged'] is True and s['workingSourceUnchanged'] is True,label+' snapshot closure')
need(ordered(r['start'],s['time'],r['finish']),label+' snapshot start during trial')
need(s['binary']==checker['readback'] and s['buildReceipt']==pin(craw('cargo/lifecycle-producer-build-2/result.json')),label+' corrected checker build')
need(s['helper']==pin(raw(spec['helpers']['snapshot'])),label+' snapshot helper pin')
need(s['source']==directory(q+'source/'),label+' exact saved source bytes')
for stream in ('stdout','stderr'):need(s[stream]==pin(raw(q+stream+'.log')),label+' '+stream)
need(not raw(q+'stderr.log'),label+' empty snapshot stderr')
cmd=s['command'];need(cmd[:3]==[checker['cargo']['executable'],'check',state] and mapped(cmd[3])==q+'source/original.arcadia',label+' checker command')
need(report['status']=='PASS' and report['comparison']=='complete Project equality' and report['state']==state and report['entry']==cmd[3],label+' accepted model oracle')
opts=cmd[4:];need(len(opts)%2==0,label+' option pairs')
for flag,path in zip(opts[::2],opts[1::2]):
need(flag in ('--identities','--write-identities'),label+' identity-only options')
need(obj(mapped(path))==report['new_ids'],label+' exact identity receipt')
if state.endswith(('-reopened','-noop')):need('--identities' in opts and report['trace_identity_pinned'] is True,label+' continuity identity pin')
if state.endswith('-relinked'):need('--write-identities' in opts and mapped(report['identity_receipt'])==mapped(opts[opts.index('--write-identities')+1]),label+' initial accepted identities')
snapshots[(run,label)]=s['source'];reports[(run,label)]=report
print(run,'captures',len(spec['captures']),'images',len(images),'accepted snapshots',len(spec['snapshots']))
for side,first,later in [('oa','lifecycle-2','lifecycle-3'),('sa','lifecycle-4','lifecycle-5')]:
base=snapshots[(first,side+'-relinked')];ids=reports[(first,side+'-relinked')]['new_ids']
for phase in ('reopened','noop'):
key=(later,side+'-'+phase)
need(snapshots[key]==base and reports[key]['new_ids']==ids,side+' saved bytes and identity continuity '+phase)
print('PASS: 4 closed trials;',len(snapshots),'accepted full-model receipts;',capture_count,'capture records;',image_count,'original images;',len(pairs),'linked source capsules.')
print('No UI history authentication or checker re-execution; snapshot receipts lack completion timestamps. Frame dimensions vary.')
if __name__=='__main__':main()
@@ -0,0 +1,434 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "num-bigint"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "oa-core"
version = "0.2.0"
dependencies = [
"libc",
"num-bigint",
"num-integer",
"num-traits",
"same-file",
"serde",
"serde_json",
"tempfile",
"thiserror",
"uuid",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]]
name = "transition-native-lifecycle"
version = "0.1.0"
dependencies = [
"oa-core",
"serde",
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "uuid"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
"getrandom",
"js-sys",
"serde_core",
"sha1_smol",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.119",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -0,0 +1,12 @@
[package]
name = "transition-native-lifecycle"
version = "0.1.0"
edition = "2024"
publish = false
[workspace]
[dependencies]
oa-core = { path = "../../../../crates/oa-core" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -0,0 +1,154 @@
# Native transition lifecycle fixture and oracle
This isolated producer/checker prepares native OA↔SA acceptance work. It does
not automate the application. The project owns no production code. It has its
own Cargo workspace and depends on this checkout's `oa-core` by relative path.
## Commands
From the repository root, with the configured Rust toolchain and compiler gate:
```powershell
cargo run --manifest-path harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml -- generate .local/transition-native/lifecycle-input-1
cargo run --manifest-path harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml -- check original .local/transition-native/lifecycle-input-1/original.arcadia
```
The parent owns compilation, execution and native verification. These commands
are instructions, not evidence that they have run. Generation refuses to
overwrite an existing deliverable; use a new output directory for a new run.
All fixture identities are reproducible UUIDv5 values of fixed aliases in one
fixed namespace. Generating into a second directory must reproduce file bytes.
`generate OUTDIR` validates and round-trips each expected project, then emits:
- `original.arcadia`, `original.json`, and `manifest.json` with owned removal IDs.
- `oa-deleted.arcadia` / `.json` and `sa-deleted.arcadia` / `.json`.
- `oa-recreated.arcadia` / `.json` and `sa-recreated.arcadia` / `.json`.
- `oa-recreate-root.snippet` and `sa-recreate-root.snippet`.
The deleted/recreated files are independent expected artifacts. Opening them
does not establish that the native deletion, Undo or editing operation works.
Each snippet contains only its replacement root declaration, with the original
alias, name and description, a deterministic new UUID, no children, and no
placement. Generation verifies that inserting it immediately before the deleted
project's final closing brace compiles to the exact expected recreated Project.
`check STATE SAVED_ENTRY [--placements PATH] [--identities PATH]
[--write-identities PATH]` loads the actual saved entry and its reachable
includes through the public disk loader/compiler. It rejects any source bundle
changed by compilation, including declarations with missing UUIDs. An identity
inserted only into the compiler's memory cannot prove a saved identity. States:
| State | Required saved model |
| --- | --- |
| `original` | Entire original Project, including every original UUID |
| `oa-deleted`, `sa-deleted` | Explicit source-owned removals; opposite design and shared definitions retained exactly |
| `oa-recreated`, `sa-recreated` | Deleted state plus only the original source root under its pinned replacement UUID |
| `oa-relinked`, `sa-relinked` | First relink: recreated state plus exactly one new refinement trace, with both existing prose values acknowledged |
| `oa-reopened`, `sa-reopened` | Same relinked model; requires `--identities` with its previously accepted trace UUID |
| `oa-noop`, `sa-noop` | Same relinked model after unchanged retry; requires `--identities` with its previously accepted trace UUID |
The checker compares every Project field, including IDs, aliases, prose, typed
bodies, ownership, item/port contracts, relations, trace baselines, diagram
geometry/routes and artifacts. A mismatch reports its first differing JSON path.
No deletion impact helper, transition planner, or observed post-deletion state
constructs the expected removal set. Relinking expectations construct a new
Trace directly. The source UUID is always the deterministic snippet UUID unless
an explicit `--identities` file supplies another source UUID. Only the first
`*-relinked` check can observe a fresh trace UUID. Use `--write-identities PATH`
on that successful check to write a receipt, then pass that exact receipt using
`--identities PATH` for reopening and no-op checks. Those stages refuse to run
without a pinned trace UUID and reject any replacement of source, trace or
declared placement identities. Receipt writes refuse overwrite and happen only
after complete-model comparison succeeds.
An identity receipt is a JSON object with `source` and, after relinking, `trace`
UUID strings. Optional declared placements add `placement:ALIAS` UUID entries.
An explicitly authored source-only identity file can establish a different
GUI-created root UUID for initial recreation/relinking. Do not re-extract it
from later saves. The checker rejects unused identity entries and reports whether
trace identity was pinned; the first relink establishes identity evidence but
does not itself prove preservation across save/reopen.
Example using the already compiled producer executable:
```powershell
transition-native-lifecycle check oa-relinked saved-first-relink.arcadia --write-identities first-relink.identities.json
transition-native-lifecycle check oa-reopened saved-after-reopen.arcadia --identities first-relink.identities.json
transition-native-lifecycle check oa-noop saved-after-noop.arcadia --identities first-relink.identities.json
```
## Native workflow
| Layer | Root to select | Alias | Diagrams |
| --- | --- | --- | --- |
| OA | Coordinate distress response | `oa_response` | `oa_work`, `oa_overview` |
| SA | Deliver coordinated response | `sa_response` | `sa_work`, `sa_overview` |
Both roots own two work children with an internal report exchange. The SA
children own typed report input/output ports. Shared report definitions have
Flow mechanisms and String payload fields. OA intentionally has no ports.
Both layers have an expanded decomposition and overview diagram, authored
placements, exchange routing/labels, and artifacts on roots, children,
exchanges, placements and shared items. The initial correspondence and its
artifact must disappear when either root is deleted. Shared item definitions,
their fields/evidence, diagrams and the independent System remain.
1. Open `original.arcadia`. Save a native snapshot and check `original`.
2. Graphically delete **Coordinate distress response** (`oa_response`). Save a
snapshot and check `oa-deleted`. Inspect the complete retained SA design.
3. Native Undo, save another snapshot and check `original`. This checks the
actual saved result, including original IDs and all owned records.
4. Delete OA again. In the native Textual model editor, insert the inspected
`oa-recreate-root.snippet` immediately before the project's final `}`. Apply,
save, and check `oa-recreated`. Do not open the generated recreated fixture
as a substitute for this editing step.
5. Select the newly recreated `oa_response` and start OA→SA transition. Choose
existing `sa_response` explicitly. Select **Keep target** for both Name and
Description; do not request child/required dependency creation. The existing
target already has placements. Apply, save, and check `oa-relinked` with
`--write-identities first-relink.identities.json`.
6. Reopen the saved result and save a fresh snapshot; check `oa-reopened` with
`--identities first-relink.identities.json`.
Re-preview unchanged and inspect reuse/no changes. Applying any no-op and
saving must pass `oa-noop` with the same original identity receipt. Do not
substitute another unpinned `oa-relinked` observation for these later checks.
7. For the reverse workflow, start again from `original.arcadia`, delete
`sa_response`, use the SA snippet, and explicitly relink to retained
`oa_response`, using the corresponding `sa-*` checker states.
These instructions separate source recreation in the text editor from relinking
through the graphical transition dialog, avoiding unpredictable canvas placement
side effects. The newly recreated root deliberately has no children or placement.
## Optional explicitly authored source placement
If a separate GUI recreation task intentionally creates a placement, pass
`--placements` with an expectation file shaped like `oa-placement.example.json`. Set its diagram, alias,
position, size and collapsed state to the intended interaction inputs. Only the
new placement UUID is observed during the first check; all its other fields are
compared with the authored expectation. It must reference the new source root
and use the existing source-layer work/overview diagram. Undeclared placements,
changed target geometry and extra artifacts fail comparison. The snippet
workflow requires no placement expectation file. Later checks must supply the
same placement specifications and the saved identity receipt; the placement UUID
is then pinned rather than re-learned from the new saved model.
## Explicit incompleteness and limits
The architecture fixture deliberately omits performer allocations and
process/chain/scenario contexts. Admission requires exactly `unallocated_leaf`
(`allocation`) and `work_without_behavior_context` (no field) on each surviving
leaf work UUID, and `exchange_without_behavior_context` (no field) on each
surviving work exchange UUID. Errors, extra warnings, wrong warning owners and
duplicate warnings fail. System and data contracts are modeled explicitly.
The disk loader and source compiler may report these same engineering warnings.
Their diagnostic multisets must exactly match the validated model warnings by
severity, code, object UUID and field. Additional loading warnings, missing or
duplicate warnings, and all source-loading errors remain failures.
Whole-model comparison and source/JSON round trips do not prove usability,
authored comment/whitespace preservation, native Undo without saving, or HTTP
retry behavior. The parent must record actual native interactions, executable
provenance, saved snapshots and checker results. This tool does not perform
native interaction or publish an acceptance result itself.
@@ -0,0 +1,9 @@
[
{
"diagram": "oa_work",
"alias": "view_coordinate_distress_response",
"position": { "x": 60.0, "y": 60.0 },
"size": { "width": 224.0, "height": 94.0 },
"collapsed": false
}
]
@@ -0,0 +1,914 @@
use oa_core::{
language, source_files,
source_project::{ProjectLimits, SourceBundle, compile_bundle},
validation::{self, Severity},
*,
};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fs, io::Write, path::Path};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn id(alias: &str) -> Id {
Id::new_v5(
&Id::from_u128(0xc1192026_0913_5000_8000_000000000028),
alias.as_bytes(),
)
}
#[derive(Serialize)]
struct Side {
prefix: String,
root: Id,
elements: Vec<Id>,
exchange: Id,
placements: Vec<(Id, Id)>,
artifacts: Vec<Id>,
}
struct Fixture {
project: Project,
oa: Side,
sa: Side,
}
fn add(
p: &mut Project,
kind: Kind,
layer: Layer,
alias: &str,
name: &str,
parent: Option<Id>,
) -> Id {
let key = id(alias);
assert!(
p.elements
.insert(
key,
Element {
id: key,
alias: alias.into(),
name: name.into(),
description: String::new(),
parent,
body: ElementBody::new(kind, layer).unwrap(),
}
)
.is_none()
);
key
}
fn artifact(p: &mut Project, owner: Id, alias: &str, name: &str) -> Id {
let key = id(alias);
p.artifact_links.insert(
key,
ArtifactLink {
id: key,
alias: alias.into(),
name: name.into(),
description: "Reviewed evidence must survive opposite-layer deletion.".into(),
owner,
relation: ArtifactRelation::References,
uri: "git:response/design.md".into(),
revision: Some("review-17".into()),
fragment: Some("response-handling".into()),
},
);
key
}
fn port(p: &mut Project, alias: &str, owner: Id, item: Id, output: bool) -> Id {
let key = id(alias);
p.elements.insert(
key,
Element {
id: key,
alias: alias.into(),
name: if output {
"Report output"
} else {
"Report input"
}
.into(),
description: "Retain the typed report contract.".into(),
parent: Some(owner),
body: ElementBody::shared_in(
Layer::Sa,
SharedBody::Port {
contract: if output {
PortContract::FunctionOutput {
outgoing_items: vec![item],
}
} else {
PortContract::FunctionInput {
incoming_items: vec![item],
}
},
legacy: None,
},
),
},
);
key
}
fn side(p: &mut Project, layer: Layer) -> Side {
let (prefix, kind, root_name, send_name, receive_name, description) = match layer {
Layer::Oa => (
"oa",
Kind::Activity,
"Coordinate distress response",
"Report distress",
"Assess incident",
"Coordinate responders while preserving operational responsibility.",
),
Layer::Sa => (
"sa",
Kind::Function,
"Deliver coordinated response",
"Transmit report",
"Classify incident",
"Technical design: preserve independent processing and report contracts.",
),
};
let root = add(
p,
kind,
layer,
&format!("{prefix}_response"),
root_name,
None,
);
p.elements.get_mut(&root).unwrap().description = description.into();
let send = add(
p,
kind,
layer,
&format!("{prefix}_send"),
send_name,
Some(root),
);
let receive = add(
p,
kind,
layer,
&format!("{prefix}_receive"),
receive_name,
Some(root),
);
p.elements.get_mut(&send).unwrap().description =
"Keep the detailed sending responsibility.".into();
p.elements.get_mut(&receive).unwrap().description =
"Keep the detailed receiving responsibility.".into();
let item = add(
p,
Kind::ExchangeItem,
layer,
&format!("{prefix}_report"),
"Incident report",
None,
);
*p.elements.get_mut(&item).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
kind: ExchangeItemKind::Information,
mechanism: ExchangeMechanism::Flow,
supertype: None,
};
add(
p,
Kind::DataField,
layer,
&format!("{prefix}_report_text"),
"Report text",
Some(item),
);
let mut elements = vec![root, send, receive];
let body = if layer == Layer::Sa {
let output = port(p, "sa_send_output", send, item, true);
let input = port(p, "sa_receive_input", receive, item, false);
elements.extend([output, input]);
RelationBody::FunctionalExchange {
source: send,
target: receive,
source_port: output,
target_port: input,
items: vec![item],
}
} else {
RelationBody::Interaction {
source: send,
target: receive,
items: vec![item],
}
};
let exchange_alias = format!("{prefix}_report_delivery");
let exchange = id(&exchange_alias);
p.relations.insert(
exchange,
Relation {
id: exchange,
alias: exchange_alias,
name: "Deliver incident report".into(),
description: "Preserve direction, item identity and endpoint contracts.".into(),
body,
},
);
let mut placements = vec![];
let mut artifacts = vec![
artifact(
p,
root,
&format!("{prefix}_root_evidence"),
"Response rationale",
),
artifact(
p,
send,
&format!("{prefix}_send_evidence"),
"Sending requirement",
),
artifact(
p,
exchange,
&format!("{prefix}_exchange_evidence"),
"Report contract evidence",
),
];
for (suffix, title, projection) in [
(
"work",
"Response decomposition",
ProjectionKind::Decomposition,
),
("overview", "Response overview", ProjectionKind::Overview),
] {
let alias = format!("{prefix}_{suffix}");
let diagram = id(&alias);
let mut nodes = BTreeMap::new();
for (index, (element, short)) in [(root, "root"), (send, "send"), (receive, "receive")]
.into_iter()
.enumerate()
{
let node_alias = format!("{prefix}_{suffix}_{short}");
let node_id = id(&node_alias);
nodes.insert(
node_id,
NodePlacement {
id: node_id,
alias: node_alias,
element,
position: match index {
0 => Position { x: 40., y: 40. },
1 => Position { x: 80., y: 120. },
_ => Position { x: 400., y: 120. },
},
size: if index == 0 {
Size {
width: 660.,
height: 240.,
}
} else {
Size {
width: 224.,
height: 94.,
}
},
collapsed: false,
port_anchors: vec![],
},
);
placements.push((diagram, node_id));
if index == 0 {
artifacts.push(artifact(
p,
node_id,
&format!("{prefix}_{suffix}_layout_evidence"),
"Reviewed layout",
));
}
}
p.diagrams.insert(
diagram,
Diagram {
id: diagram,
alias,
name: format!("{} {title}", layer),
description: "Inspect the response root, its children and their original exchange."
.into(),
layer,
projection,
view_options: Default::default(),
nodes,
routes: vec![EdgeRoute {
edge: exchange,
waypoints: vec![Position { x: 360., y: 167. }],
label: Some(EdgeLabel {
fraction: 0.55,
offset: Position { x: 0., y: -18. },
}),
}],
},
);
}
artifact(
p,
item,
&format!("{prefix}_item_evidence"),
"Shared report definition",
);
Side {
prefix: prefix.into(),
root,
elements,
exchange,
placements,
artifacts,
}
}
fn fixture() -> Fixture {
let mut project = Project::empty();
project.id = id("response_lifecycle");
project.alias = "response_lifecycle".into();
project.name = "Distress response: transition lifecycle".into();
project.description =
"Native acceptance fixture for preserving opposite-layer engineering work.".into();
add(
&mut project,
Kind::System,
Layer::Sa,
"response_system",
"Response support system",
None,
);
let oa = side(&mut project, Layer::Oa);
let sa = side(&mut project, Layer::Sa);
let key = id("response_refinement");
project.traces.insert(
key,
Trace {
id: key,
alias: "response_refinement".into(),
name: "Reviewed response refinement".into(),
description: "Operational and technical prose are intentionally different.".into(),
oa: oa.root,
sa: sa.root,
kind: TraceKind::Refinement,
baseline: Some(TraceBaseline {
oa: project.properties(oa.root).unwrap(),
sa: project.properties(sa.root).unwrap(),
semantic: None,
}),
},
);
artifact(
&mut project,
key,
"response_trace_evidence",
"Review decision",
);
Fixture { project, oa, sa }
}
// The removal oracle enumerates fixture-owned records. No deletion command,
// impact helper, transition planner or observed post-deletion model is used.
fn deleted(f: &Fixture, side: &Side) -> Project {
let mut p = f.project.clone();
for key in &side.elements {
assert!(p.elements.remove(key).is_some());
}
assert!(p.relations.remove(&side.exchange).is_some());
assert!(p.traces.remove(&id("response_refinement")).is_some());
for key in side
.artifacts
.iter()
.copied()
.chain([id("response_trace_evidence")])
{
assert!(p.artifact_links.remove(&key).is_some());
}
for (diagram, node) in &side.placements {
assert!(
p.diagrams
.get_mut(diagram)
.unwrap()
.nodes
.remove(node)
.is_some()
);
}
for suffix in ["work", "overview"] {
let diagram = p
.diagrams
.get_mut(&id(&format!("{}_{suffix}", side.prefix)))
.unwrap();
assert_eq!(diagram.routes.len(), 1);
assert_eq!(diagram.routes[0].edge, side.exchange);
diagram.routes.clear();
}
p
}
fn accepted(p: &Project) -> Result<Vec<validation::Issue>> {
// Intentional isolated architecture: no allocations, processes or scenarios.
// Require the exact warning multiset by record UUID, code and field.
let issues = validation::validate(p);
if issues.iter().any(|i| i.severity != Severity::Warning) {
return Err(format!("Structural validation failed: {issues:?}").into());
}
let mut expected = vec![];
for e in p.elements.values().filter(|e| {
matches!(e.kind(), Kind::Activity | Kind::Function)
&& !p
.elements
.values()
.any(|child| child.parent == Some(e.id) && child.kind() == e.kind())
}) {
expected.push(("unallocated_leaf", Some(e.id), Some("allocation")));
expected.push(("work_without_behavior_context", Some(e.id), None));
}
for r in p.relations.values().filter(|r| {
matches!(
r.kind(),
RelationKind::Interaction | RelationKind::FunctionalExchange
)
}) {
expected.push(("exchange_without_behavior_context", Some(r.id), None));
}
let mut actual = issues
.iter()
.map(|i| (i.code.as_str(), i.element, i.field.as_deref()))
.collect::<Vec<_>>();
expected.sort();
actual.sort();
if expected != actual {
return Err(
format!("Unexpected warnings: expected {expected:?}; actual {actual:?}").into(),
);
}
Ok(issues)
}
fn compile(source: String) -> Result<Project> {
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([("project.arcadia".into(), source)]),
};
let result = compile_bundle(&bundle, ProjectLimits::default())
.map_err(|e| format!("Source compile: {e:?}"))?;
Ok(result.decoded.project)
}
fn round_trip(p: &Project) -> Result<String> {
accepted(p)?;
let bytes = serde_json::to_vec_pretty(p)?;
compare(p, &persistence::decode(&bytes)?)?;
let source = language::format(p);
compare(p, &compile(source.clone())?)?;
Ok(source)
}
fn recreated(f: &Fixture, side: &Side, new_id: Id) -> Project {
assert!(!f.project.contains(new_id));
let mut p = deleted(f, side);
let mut root = f.project.elements[&side.root].clone();
root.id = new_id;
p.elements.insert(new_id, root);
p
}
fn snippet(f: &Fixture, side: &Side) -> String {
let e = &f.project.elements[&side.root];
let keyword = if side.prefix == "oa" {
"oa_activity"
} else {
"sa_function"
};
format!(
" {keyword} {} {} id \"{}\" {{\n description {};\n }}\n",
e.alias,
serde_json::to_string(&e.name).unwrap(),
id(&format!("{}_recreated_response", side.prefix)),
serde_json::to_string(&e.description).unwrap()
)
}
fn generate(out: &Path) -> Result<()> {
let f = fixture();
let mut files: Vec<(String, Vec<u8>)> = vec![
(
"original.arcadia".into(),
round_trip(&f.project)?.into_bytes(),
),
(
"original.json".into(),
serde_json::to_vec_pretty(&f.project)?,
),
(
"manifest.json".into(),
serde_json::to_vec_pretty(&serde_json::json!({
"fixture": "transition-native-lifecycle-20260913", "oa": &f.oa, "sa": &f.sa,
"trace": id("response_refinement"), "trace_artifact": id("response_trace_evidence"),
"recreated_oa_id": id("oa_recreated_response"), "recreated_sa_id": id("sa_recreated_response")
}))?,
),
];
for side in [&f.oa, &f.sa] {
let removal = deleted(&f, side);
let removal_source = round_trip(&removal)?;
let replacement = recreated(&f, side, id(&format!("{}_recreated_response", side.prefix)));
let replacement_source = round_trip(&replacement)?;
let fragment = snippet(&f, side);
let (body, tail) = removal_source
.rsplit_once('}')
.ok_or("Missing project closing brace")?;
compare(&replacement, &compile(format!("{body}{fragment}}}{tail}"))?)?;
files.extend([
(
format!("{}-deleted.json", side.prefix),
serde_json::to_vec_pretty(&removal)?,
),
(
format!("{}-deleted.arcadia", side.prefix),
removal_source.into_bytes(),
),
(
format!("{}-recreated.json", side.prefix),
serde_json::to_vec_pretty(&replacement)?,
),
(
format!("{}-recreated.arcadia", side.prefix),
replacement_source.into_bytes(),
),
(
format!("{}-recreate-root.snippet", side.prefix),
fragment.into_bytes(),
),
]);
}
if files.iter().any(|(name, _)| out.join(name).exists()) {
return Err(
"Generation refuses to overwrite fixture files; use a fresh output directory".into(),
);
}
fs::create_dir_all(out)?;
for (name, bytes) in files {
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(out.join(name))?
.write_all(&bytes)?;
}
println!(
"Generated and round-trip verified both directions in {}",
out.display()
);
Ok(())
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PlacementExpectation {
diagram: String,
alias: String,
position: Position,
size: Size,
collapsed: bool,
}
fn first_difference(
expected: &serde_json::Value,
actual: &serde_json::Value,
path: &str,
) -> Option<String> {
use serde_json::Value;
if expected == actual {
return None;
}
match (expected, actual) {
(Value::Object(a), Value::Object(b)) => {
for key in a.keys().chain(b.keys()) {
match (a.get(key), b.get(key)) {
(Some(x), Some(y)) => {
if let Some(diff) = first_difference(x, y, &format!("{path}.{key}")) {
return Some(diff);
}
}
_ => {
return Some(format!(
"{path}.{key}: expected presence {}, actual presence {}",
a.contains_key(key),
b.contains_key(key)
));
}
}
}
}
(Value::Array(a), Value::Array(b)) if a.len() == b.len() => {
for (index, (x, y)) in a.iter().zip(b).enumerate() {
if let Some(diff) = first_difference(x, y, &format!("{path}[{index}]")) {
return Some(diff);
}
}
}
_ => return Some(format!("{path}: expected {expected}, actual {actual}")),
}
Some(format!("{path}: values differ"))
}
fn compare(expected: &Project, actual: &Project) -> Result<()> {
if expected != actual {
return Err(first_difference(
&serde_json::to_value(expected)?,
&serde_json::to_value(actual)?,
"Project",
)
.unwrap_or_else(|| "Project differs".into())
.into());
}
Ok(())
}
fn check(
state: &str,
entry: &Path,
placement_path: Option<&Path>,
identity_path: Option<&Path>,
receipt_path: Option<&Path>,
) -> Result<()> {
let f = fixture();
let identity_expectations: Option<BTreeMap<String, Id>> = identity_path
.map(|path| -> Result<_> { Ok(serde_json::from_slice(&fs::read(path)?)?) })
.transpose()?;
let disk = source_files::load_project(entry, ProjectLimits::default())?;
if disk
.diagnostics
.iter()
.any(|located| located.diagnostic.severity != Severity::Warning)
{
return Err(format!("Source loading errors: {:?}", disk.diagnostics).into());
}
let disk_bundle = disk.reachable_bundle();
let compiled = compile_bundle(&disk_bundle, ProjectLimits::default())
.map_err(|e| format!("Saved source compile: {e:?}"))?;
if compiled.bundle != disk_bundle {
return Err("Saved source is not identity-stable: compilation inserted UUIDs or changed source; save explicit identities before checking".into());
}
let actual = &compiled.decoded.project;
let validated_warnings = accepted(actual)?;
let mut expected_diagnostics = validated_warnings
.iter()
.map(|issue| {
(
issue.severity == Severity::Warning,
issue.code.as_str(),
issue.element,
issue.field.as_deref(),
)
})
.collect::<Vec<_>>();
expected_diagnostics.sort();
// Loading/compilation may return the fixture's intentional engineering
// warnings. Admit exactly the independently validated warning multiset,
// retaining owner, field and severity checks; no extra discovery warning or
// error is silently discarded. Source location spelling is not semantic.
for (stage, diagnostics) in [
("loading", &disk.diagnostics),
("compilation", &compiled.diagnostics),
] {
let mut actual_diagnostics = diagnostics
.iter()
.map(|located| {
let diagnostic = &located.diagnostic;
(
diagnostic.severity == Severity::Warning,
diagnostic.code.as_str(),
diagnostic.object_id,
diagnostic.field.as_deref(),
)
})
.collect::<Vec<_>>();
actual_diagnostics.sort();
if actual_diagnostics != expected_diagnostics {
return Err(format!("Unexpected source {stage} diagnostics: expected {expected_diagnostics:?}; actual {actual_diagnostics:?}").into());
}
}
let mut generated_ids = BTreeMap::<String, Id>::new();
let expected = if state == "original" {
if placement_path.is_some() || identity_path.is_some() || receipt_path.is_some() {
return Err("original does not accept placement/identity options".into());
}
f.project.clone()
} else {
let (prefix, phase) = state
.split_once('-')
.ok_or("State must be original or oa-/sa-deleted/recreated/relinked/reopened/noop")?;
let side = match prefix {
"oa" => &f.oa,
"sa" => &f.sa,
_ => return Err("Unknown source layer".into()),
};
if phase == "deleted" {
if placement_path.is_some() || identity_path.is_some() || receipt_path.is_some() {
return Err("deleted does not accept placement/identity options".into());
}
deleted(&f, side)
} else if matches!(phase, "recreated" | "relinked" | "reopened" | "noop") {
if matches!(phase, "reopened" | "noop")
&& identity_expectations
.as_ref()
.and_then(|ids| ids.get("trace"))
.is_none()
{
return Err("Reopened/noop checks require --identities with the first accepted relink trace UUID".into());
}
let original_root = &f.project.elements[&side.root];
let new_source = match &identity_expectations {
Some(ids) => *ids
.get("source")
.ok_or("Identity expectations must explicitly include source")?,
None => id(&format!("{prefix}_recreated_response")),
};
let roots = actual
.elements
.values()
.filter(|e| e.alias == original_root.alias)
.collect::<Vec<_>>();
if roots.len() != 1 || roots[0].id != new_source || f.project.contains(new_source) {
return Err(
"Expected exactly one recreated root with the original alias and the pinned new UUID (snippet UUID unless --identities supplied)"
.into(),
);
}
generated_ids.insert("source".into(), new_source);
let mut candidate = recreated(&f, side, new_source);
if let Some(path) = placement_path {
let specs: Vec<PlacementExpectation> = serde_json::from_slice(&fs::read(path)?)?;
for spec in specs {
if ![format!("{prefix}_work"), format!("{prefix}_overview")]
.contains(&spec.diagram)
{
return Err("A new source placement must use its existing source-layer work/overview diagram".into());
}
let diagram = id(&spec.diagram);
let nodes = actual
.diagrams
.get(&diagram)
.ok_or("Expected diagram is missing")?
.nodes
.values()
.filter(|node| node.alias == spec.alias)
.collect::<Vec<_>>();
if nodes.len() != 1 || f.project.contains(nodes[0].id) {
return Err(
"Expected one newly identified declared source placement".into()
);
}
let receipt_key = format!("placement:{}", spec.alias);
let key = match &identity_expectations {
Some(ids) => *ids.get(&receipt_key).ok_or(
"Identity receipt lacks an explicitly declared placement UUID",
)?,
None => nodes[0].id,
};
if nodes[0].id != key {
return Err(
"Source placement UUID changed from its identity expectation".into(),
);
}
if candidate
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.insert(
key,
NodePlacement {
id: key,
alias: spec.alias.clone(),
element: new_source,
position: spec.position,
size: spec.size,
collapsed: spec.collapsed,
port_anchors: vec![],
},
)
.is_some()
{
return Err("Duplicate placement expectation".into());
}
generated_ids.insert(receipt_key, key);
}
}
if phase != "recreated" {
if actual.traces.len() != 1 {
return Err("Relinking must create exactly one trace".into());
}
let actual_key = actual.traces.values().next().unwrap().id;
let key = identity_expectations
.as_ref()
.and_then(|ids| ids.get("trace"))
.copied()
.unwrap_or(actual_key);
if actual_key != key {
return Err(
"Refinement trace UUID changed from its accepted identity receipt".into(),
);
}
if f.project.contains(key) || candidate.contains(key) {
return Err("Expected a fresh trace UUID".into());
}
let (oa, sa) = if prefix == "oa" {
(new_source, f.sa.root)
} else {
(f.oa.root, new_source)
};
candidate.traces.insert(
key,
Trace {
id: key,
alias: "trace_oa_response_sa_response".into(),
name: format!(
"{} ↔ {}",
candidate.elements[&oa].name, candidate.elements[&sa].name
),
description: String::new(),
oa,
sa,
kind: TraceKind::Refinement,
baseline: Some(TraceBaseline {
oa: candidate.properties(oa).unwrap(),
sa: candidate.properties(sa).unwrap(),
semantic: None,
}),
},
);
generated_ids.insert("trace".into(), key);
}
candidate
} else {
return Err("Unknown lifecycle phase".into());
}
};
// No actual body, prose, ownership, geometry, alias or baseline is copied
// into the expected model: only explicitly permitted fresh identities.
compare(&expected, actual)?;
round_trip(&expected)?;
if let Some(ids) = &identity_expectations {
for (label, expected_id) in ids {
if generated_ids.get(label) != Some(expected_id) {
return Err(format!("Unused or mismatching identity expectation: {label}").into());
}
}
}
if let Some(path) = receipt_path {
// Publish a receipt only after complete-model validation succeeds. Never
// replace earlier accepted identity evidence with a new observation.
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?
.write_all(&serde_json::to_vec_pretty(&generated_ids)?)?;
}
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"status": "PASS", "state": state, "entry": entry,
"comparison": "complete Project equality", "new_ids": generated_ids,
"trace_identity_pinned": identity_expectations.as_ref().is_some_and(|ids| ids.contains_key("trace")),
"identity_receipt": receipt_path,
"recreated_id_matches_snippet": generated_ids.get("source").map(|key|
*key == id(if state.starts_with("oa-") { "oa_recreated_response" } else { "sa_recreated_response" }))
}))?
);
Ok(())
}
fn run() -> Result<()> {
let args = std::env::args().skip(1).collect::<Vec<_>>();
match args.as_slice() {
[command, out] if command == "generate" => generate(Path::new(out)),
[command, state, entry, options @ ..] if command == "check" => {
if options.len() % 2 != 0 { return Err("Every check option requires a path".into()); }
let (mut placements, mut identities, mut receipt) = (None, None, None);
for pair in options.chunks_exact(2) {
let slot = match pair[0].as_str() {
"--placements" => &mut placements,
"--identities" => &mut identities,
"--write-identities" => &mut receipt,
_ => return Err(format!("Unknown check option: {}", pair[0]).into()),
};
if slot.replace(Path::new(&pair[1])).is_some() { return Err("Duplicate check option".into()); }
}
check(state, Path::new(entry), placements, identities, receipt)
},
_ => Err("Usage: transition-native-lifecycle generate OUTDIR | check STATE SAVED_ENTRY [--placements PATH] [--identities PATH] [--write-identities PATH]".into()),
}
}
fn main() {
if let Err(error) = run() {
eprintln!("FAIL: {error}");
std::process::exit(1);
}
}
@@ -0,0 +1,102 @@
# Independent bounded transition review
Reviewer: `transition_picker_review`, 2026-09-22. This review inspected code,
original native screenshots and closed-run/snapshot receipts. The reviewer did
not operate the application, build it, rerun the checker, or alter the specimens.
Only this review note was written. The parent owns execution and publication.
The reviewed production baseline is `d0b7c285018a516c6dc34c196d1359a861ddaeb3`
plus the captured, initially uncommitted footer changes in
`crates/oa-desktop/src/workflow.rs`. HEAD alone does not identify those changes.
Lifecycle trials 2 and 3 identify GUI SHA-256
`3813e79d84cf4a83f2290639b93c3daaa8de427b0268b288f87e4f535fa1aea2`;
trials 4 and 5 identify final-footer GUI SHA-256
`f0bb021e350bcc55435d29e3daf6a4b3dd7389620df99f5d169f9cb669fd53d0`.
Exact build/source inventories belong to the accompanying evidence packages.
## Code and evaluator findings
The picker shares the core layer/category/kind predicate, preserves selected
identity independently of search results, and keeps query state outside the
request and recovery schema. Query events return before replanning. Compatibility
does not imply that dependencies or the complete transition plan are valid.
Distinct traced IDs, rather than trace-record count, determine inferred reuse;
ambiguous traces remain an explicit decision. Manual tracing remains separate.
The desktop tests compare the whole request, plan and recovery snapshot during
search, including generated preview identities. The initial second-query test
used a target ID rather than a second rendered source card; native corrected
picker frame 17 separately demonstrates two visible source cards with independent
queries. These checks do not prove keyboard access or every focus transition.
The core lifecycle tests and [native producer/checker](producer/src/main.rs)
construct expected deletions from fixture-owned identities, not a deletion
impact helper or observed deletion result. Expected recreation adds only the
replacement root. Expected relinking adds a directly constructed refinement
trace and reviewed baseline while preserving target prose, bodies, ownership,
geometry, routes and artifacts. Whole-Project equality detects incidental changes.
First-relink admission may observe the fresh trace UUID; reopen/no-op admission
requires the previously written identity receipt and rejects replacement IDs.
Source loading rejects compiler-inserted identities. Round trips compare the
decoded model; they do not promise original comments or formatting preservation.
No blocking conservation-oracle defect was found in the inspected paths.
## Native observations
Trial/frame labels below refer to original `*-0.jpg` files and matching JSON
under `.local/transition-native/` at capture time. Receipt evidence is distinct
from what screenshots visibly establish. Capture helpers and checksums provide
provenance context, not authenticated historical action logs.
| Originals inspected | Visible observation |
| --- | --- |
| `picker-baseline-1` 10–11 | Activity menu includes incompatible ports; selecting one yields a kind conflict. |
| `picker-corrected-2` 06–07, 10–12, 15, 17–19, 22–23 | Six compatible Functions are sorted; alias filtering preserves current selection until explicit selection; zero results preserve the selected target; two cards retain independent queries; field review remains required; clearing search retains Keep target; Cancel returns to the saved-status view. |
| `lifecycle-2` 05, 11–12 | OA deletion is visible; a replacement root declaration is entered and applied through Textual model. |
| `lifecycle-2` 17, 21, 27–30 | Recreation initially offers Create rather than silently relinking. Selecting the retained SA target requires Name and Description decisions. Sticky guidance advances from Name/two conflicts to Description/one, then clears after both Keep target decisions. Seven warnings do not block Apply. |
| `lifecycle-2` 31–32 | Apply displays the retained SA root, children and exchange; subsequent frame reports all changes saved. |
| `lifecycle-3` 02, 06–08 | A fresh process opens the saved model; unchanged transition infers reuse without field conflicts; Apply and save return to the retained SA design. |
| `lifecycle-4` 12, 14, 16, 22–23 | SA deletion, Undo restoration, Redo deletion and replacement-root entry/application are visible. |
| `lifecycle-4` 28, 30–38 | Reverse transition defaults to Create; alias search offers the retained OA Activity; explicit selection requires both prose decisions. Persistent guidance follows two/one/zero conflicts. Apply displays the retained OA decomposition and save reports completion. The final `reviewed items` wording is visible. |
| `lifecycle-5` 02, 07–09 | A fresh process opens the retained OA design; unchanged SA-to-OA review infers the accepted counterpart without conflicts; Apply and save retain the visible OA design. |
Closed GUI receipts for lifecycle trials 2–5 record exit 0, empty stdout/stderr,
and unchanged executable hashes. Snapshot receipts record exit 0 for OA deletion,
recreation and relink; OA reopen/no-op; and SA deletion, original-state Undo,
deletion-state Redo, recreation, relink and subsequent reopen/no-op. Checks operate on saved source copies
and report the working source unchanged. SA Redo's source hash equals deletion's.
OA relink, reopen and no-op snapshots have identical hashes for both
`original.arcadia` and `traceability.arcadia`. Reopen/no-op use source
`8eb83649-2810-58e1-94cd-71e25422658e` and accepted trace
`856eb758-33fc-4ebf-9a87-22ad40a89952`. SA first relink establishes source
`074eab66-7092-5e7e-8b83-b31b95afdbcb` and trace
`acc7969b-08c3-43f4-a31d-9140ca873ffe`. Trial 5 passes that exact first-relink
receipt to both saved-source checks. Its reopen and no-op source hashes match the
first relink for both files (`original.arcadia`: `e75b4cb55e5b205bd5784a1f35573931f1e64491b646f4ffb241f90288447b5d`;
`traceability.arcadia`: `5046e2d2731c5d8fcbcb7fe2deede0677ec61de03b85890852fa20f778c57552`).
This supports conservation and persisted identity across these finite reopen and
unchanged-review workflows in both directions. No new blocking finding arose
from the reviewed lifecycle evidence.
## Feedback correction and remaining limits
Corrected picker frames 12/15 initially left Apply disabled while its explanation
was below the fold. The reviewed footer change moves counts outside the scrolling
body and derives guidance from the same conflicts and destination validity used
by Apply. It adds no persisted state and weakens no validation. Generic guidance
was corrected from “adjust mapping or scope” because operation-omission rationale
and component-port decisions require other controls. Final native trial 4 shows
the Name/Description guidance and `reviewed items` count. Destination, boundary
and general-conflict guidance were checked against code, not exercised in these
native lifecycle frames.
All inspected originals are 1442×932. Resize attempts did not establish a smaller
viewport, regardless of frame filenames; minimum-window behavior is unverified.
The inherited picker is pointer-dependent. This review does not establish full
keyboard/screen-reader accessibility, packaged launch, other platforms, arbitrary
models, implicit creation applied natively, or relink Undo/Redo. Recreated roots
were authored through Textual model and intentionally have no new placement;
the empty source canvas is consistent with that fixture. Screenshots alone do
not establish complete model conservation: the independent saved-source comparisons
provide that bounded evidence.
+80 -1
View File
@@ -52,4 +52,83 @@ The corrected GUI trial ran 2026-09-13 15:22:22–15:27:13 UTC on `d46854c`. It
Independent read-only reviews found no blocking defect in the shared predicate, selection/query handling, recovery behavior, or lifecycle conservation evaluator. Two refinements were incorporated: empty-query feedback no longer asks to clear an already empty search, and query-isolation tests use two actual OA source cards. A separate review of original native frames confirmed the bounded observations above without a blocking visual regression. Remaining friction: disabled Apply can have its required field-review explanation below the fold, requiring a scroll. Keyboard-only picker operation and broader transition-dialog feedback remain incomplete.
The native trial establishes this finite Windows selection/review/cancel workflow. It does not establish native endpoint deletion/recreation/relink, saving an applied transition, packaged launch, or whole-project equality from screenshots. Those lifecycle operations have the core evidence above; the broader C1 native acceptance and integration/release decision remain open.
That picker-only trial establishes the finite Windows selection/review/cancel workflow. The applied lifecycle follow-up below supplies separate native and saved-model evidence; neither trial is a packaged-release acceptance claim.
## Applied native lifecycle follow-up, 2026-09-22
The original disabled-Apply observation led to a small presentation correction:
transition counts and the next blocking decision now remain beside Apply, outside
the scrolling mappings. Guidance identifies destination, boundary-kind, field or
other conflict decisions; it does not weaken Apply validation. The count says
`reviewed items` because a reused item need not change the model. No new persisted
state or transition semantics are introduced.
The [isolated producer/checker](experiments/transition-native-lifecycle-20260913/producer/README.md)
defines a deterministic two-layer fixture and independently constructs expected
deletion, recreation and relink states. It compares complete Projects, exact
engineering-warning multisets, JSON/source round trips and persisted identities.
An independent critic exposed two potential false passes: learning replacement
identities again after reopening, and accepting identities inserted only into
compiler memory. Both were corrected. First relink records accepted identities;
reopen/reapply require that unchanged receipt. Forty synthetic falsification
trials include deliberate prose/geometry corruption, identity drift, omitted
persisted IDs and receipt misuse. They are checker evidence, not GUI actions.
The [closed Cargo and oracle package](experiments/transition-native-lifecycle-20260913/checks/README.md)
retains ten successful invocations, including strict all-target Clippy, final
desktop build and both format checks. Earlier checker build1 incorrectly rejected
the fixture's expected engineering warnings during native admission; corrected
build2 accepts only the exact expected diagnostic multiset. The original failed
admission and an interrupted first GUI session were not counted as successful
native trials. The final build differs from the linted footer only in the
`changes` to `reviewed items` literal; the package verifies that exact delta.
Four fresh, closed Windows sessions provide the
[native evidence](experiments/transition-native-lifecycle-20260913/native/README.md):
| Flow | Native operations and saved-source acceptance |
| --- | --- |
| OA source, trials 2–3 | Delete OA root; recreate only that root through Textual model; search `@sa_response`; explicitly select the retained SA function; keep target Name and Description; apply/save; restart/open/save; unchanged review/apply/save. Five full-Project checks pass. |
| SA source, trials 4–5 | Delete SA root; Undo/save restores the complete original; Redo/save repeats the deletion; recreate only that root through Textual model; search `@oa_response`; explicitly select the retained OA activity; keep both prose fields; apply/save; restart/open/save; unchanged review/apply/save. Seven full-Project checks pass. |
The retained opposite-layer design includes children, internal exchanges, typed
SA ports, shared contracts, authored diagram geometry/routes and artifact links.
Recreated roots intentionally have no children or placement. The relink adds one
trace and its reviewed baseline; it does not recreate deleted design work.
For each direction, both saved source files remain byte-identical through relink,
restart/save and unchanged reapply/save, with the first accepted source/trace IDs
pinned. This is evidence about these concrete files, not a general promise to
preserve arbitrary authored comments or whitespace.
All four processes exited zero with empty stdout/stderr and unchanged executable
hashes. Trials 2–3 used GUI SHA256
`3813e79d84cf4a83f2290639b93c3daaa8de427b0268b288f87e4f535fa1aea2`;
trials 4–5 used the final 117,090,816-byte GUI SHA256
`f0bb021e350bcc55435d29e3daf6a4b3dd7389620df99f5d169f9cb669fd53d0`.
Build receipts record HEAD `d0b7c28` plus captured uncommitted input bytes;
the 715-file final inventory, rather than that old HEAD alone, identifies the
tested source. All 715 captured inputs were checked against exact staged Git
blobs before source commit `05e4c83054dc8b2f156e50e7731a50ac19ae8767`.
The [independent review](experiments/transition-native-lifecycle-20260913/review.md)
separates original-frame observations from saved-model comparisons and found no
new blocking defect in this bounded workflow. Native frames show the persistent
Name/Description instruction advancing with two, one and zero conflicts, while
expected modeling warnings remain nonblocking. Destination, boundary and generic
conflict guidance were reviewed in code but not exercised by these native trials.
All main-window captures are 1442×932; attempted resizing did not establish
minimum-window behavior. Keyboard/screen-reader accessibility, native implicit
creation, relink Undo/Redo, arbitrary models and packaged launch remain outside
this acceptance. Linux checks remain paused by the user. Integration and release
status belong to issue 28; this is branch-specific evidence.
Run the read-only evidence verifiers with Python 3.10+:
```text
python harness/experiments/transition-native-lifecycle-20260913/checks/verify.py
python harness/experiments/transition-native-lifecycle-20260913/native/verify.py
```
These verify archived bytes and recorded relationships without executing archived
code or authenticating historical interaction. The producer README gives the
commands and native steps to reproduce behavior independently.