Equal tab counts after an update cannot establish preservation: account assignments or drafts may change while the count remains equal. I turned questions from actual retention checks into a small synthetic snapshot comparator. It reads declared before/after data and collects nothing from the original app.

Start with a deliberately small comparison contract

Inputs have four groups: tabs, projects, settings and payload. A tab contains project, profile and location aliases, pinned state and a supplied draft SHA-256. The comparator receives no actual URL or manuscript and does not automatically anonymize raw exports.

Projects retain a default-profile field; settings contain locale and panel width; payload maps file aliases to supplied hashes. Unknown fields and invalid forms are refused. Titles, loading and timestamps sit outside the contract. The result concerns this declared projection, not every original app state. An operational adaptation needs a trusted collector and a comparison policy before using the function.

The same count of different tabs is not preservation

Tabs, projects and payload first compare identity sets. Replacing tab_a with tab_b fails despite an equal count. Every accepted field also compares, exposing an account-only change or a changed draft fingerprint. A visually similar screen cannot substitute for retained relationships.

Dictionary order is not identity, so reordering otherwise identical records passes. An app that must retain visual tab order needs that property added to its contract and tests. This example does not inspect the original browser's display order. Reuse depends on understanding both included and excluded properties rather than assigning whole-app coverage to a small passing result.

An intended change must match both exact values

An intended change names section, ID and field with exact before/after values. Only a match enters accepted_intended_changes. A wrong starting value or an unperformed change fails. This is not an ignore list accepting any difference.

An update can declare exact old/new payload hashes, but that plan cannot conceal another changed profile. A missing identity cannot be excused by a field-change plan. Intended changes should be a criterion chosen before comparison, not an unlimited explanation invented afterward. The module compares the declarations; it does not apply an update or decide whether a planned business change was authorized.

Explore the implementation

Complete independent synthetic implementation

"""Compare declared synthetic snapshots. Never collect or restore application data."""
from copy import deepcopy
import re

def alias(x):
    return type(x) is str and re.fullmatch(r'[a-z][a-z0-9_-]{0,39}',x) is not None

def digest(x):
    return type(x) is str and re.fullmatch(r'[0-9a-f]{64}',x) is not None

def snapshot(raw):
    if type(raw) is not dict or set(raw)!={'tabs','projects','settings','payload'}:raise ValueError('SNAPSHOT_FIELDS')
    d=deepcopy(raw)
    for section in ['tabs','projects','payload']:
        rows=d[section]
        if type(rows) is not dict or len(rows)>100 or not all(alias(k) for k in rows):raise ValueError('ROW_IDS')
    for r in d['tabs'].values():
        if type(r) is not dict or set(r)!={'project','profile','location','pinned','draft_sha'}:raise ValueError('TAB_FIELDS')
        if not all(alias(r[k]) for k in ['project','profile','location']) or type(r['pinned']) is not bool or not digest(r['draft_sha']):raise ValueError('TAB_VALUE')
        if r['project'] not in d['projects']:raise ValueError('PROJECT_REFERENCE')
    for r in d['projects'].values():
        if type(r) is not dict or set(r)!={'default_profile'} or (r['default_profile'] is not None and not alias(r['default_profile'])):raise ValueError('PROJECT_FIELDS')
    if type(d['settings']) is not dict or set(d['settings'])!={'locale','panel_width'}:raise ValueError('SETTINGS_FIELDS')
    s=d['settings']
    if type(s['locale']) is not str or s['locale'] not in {'ko','en'} or type(s['panel_width']) is not int or not 200<=s['panel_width']<=1200:raise ValueError('SETTINGS_VALUE')
    if not all(digest(v) for v in d['payload'].values()):raise ValueError('PAYLOAD_DIGEST')
    return d

def flatten(d):
    rows={}
    for section in ['tabs','projects']:
        for id,r in d[section].items():
            for field,value in r.items():rows[(section,id,field)]=value
    for id,value in d['payload'].items():rows[('payload',id,'sha256')]=value
    for field,value in d['settings'].items():rows[('settings','app',field)]=value
    return rows

def compare(before, after, intended_changes=None):
    """All accepted fields compare. An intended change must match both exact values."""
    a,b=snapshot(before),snapshot(after)
    expected=intended_changes if intended_changes is not None else []
    if type(expected) is not list or len(expected)>100:raise ValueError('CHANGES_LIST')
    aa,bb=flatten(a),flatten(b);changes={}
    for row in expected:
        if type(row) is not dict or set(row)!={'section','id','field','before','after'}:raise ValueError('CHANGE_FIELDS')
        key=(row['section'],row['id'],row['field'])
        if not all(type(v) is str for v in key) or key not in aa or key not in bb:raise ValueError('CHANGE_TARGET')
        if key in changes:raise ValueError('DUPLICATE_CHANGE')
        if type(row['before']) is not type(aa[key]) or type(row['after']) is not type(bb[key]):raise ValueError('CHANGE_TYPE')
        if row['before']==row['after']:raise ValueError('UNCHANGED_PLAN')
        changes[key]=(deepcopy(row['before']),deepcopy(row['after']))
    differences=[];accepted=[];unused=[]
    for section in ['tabs','projects','payload']:
        if set(a[section])!=set(b[section]):differences.append({'kind':'identity_set_changed','section':section,'removed':sorted(set(a[section])-set(b[section])),'added':sorted(set(b[section])-set(a[section]))})
    for key in sorted(set(aa)&set(bb)):
        if aa[key]!=bb[key]:
            item=dict(section=key[0],id=key[1],field=key[2],before=aa[key],after=bb[key])
            if key in changes and changes[key]==(aa[key],bb[key]):accepted.append(item)
            else:differences.append(dict(kind='unexpected_value_change',**item))
    for key,values in changes.items():
        if aa[key]==bb[key] or values!=(aa[key],bb[key]):unused.append(dict(section=key[0],id=key[1],field=key[2]))
    return dict(preservation_matches_declared_projection=not differences and not unused,
                differences=differences,accepted_intended_changes=accepted,unused_or_mismatched_plans=unused,
                actual_authentication_verified=False,installer_executed=False,durable_backup_restored=False,
                arbitrary_data_redacted=False,excluded_from_contract=['title','loading','timestamps'])

Passing still leaves authentication and restoration unverified

Matching snapshots do not prove a valid login. Actual_authentication_verified, installer_executed and durable_backup_restored always remain false. Comparing equal digest strings also does not mean reading visitor files and calculating their fingerprints. Trust in the supplier of the projection is an additional requirement.

Thirty independent tests cover replaced identities with equal counts, profile/draft changes, intended navigation and mismatched plans, missing/changed payload and invalid inputs. Five records come from actual local comparisons; the selector reads only those results. Tests run again after ZIP extraction without installing the original browser or reading accounts. A small comparison success does not become success of a larger operation.

Transfer the comparison policy together with the code

Code, tests and record generation permit modification and reuse. Extend fields for another app only after defining original versus copy, dynamic values and intended changes. Removing fields merely to obtain a passing result removes the question rather than adapting the tool.

The package has no collection, URL normalization, secret removal, actual login, installation, backup or restoration. It is not a reproduction of every original retention tool. It shares an explicit criterion for sameness and allowed difference. Reusability means the next operator can read the reason for comparison as well as its result, and can identify the extra evidence needed before making a broader preservation claim.

A read-only viewer of synthetic local records
Download code, tests and records
Python standard library, synthetic data, modification and reuse permitted