mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-06 12:52:04 +00:00
feat(cross-impl): Phase L proves foreign data survives a full A->B->A cycle
The round-trip conformance check from spec section 8.6, now enforced in CI:
a vault holding a foreign record (unknown kind, nested foreign data, and
trap fields) plus an unknown top-level index key goes through an interleaved
Python -> TypeScript -> Python read-modify-write cycle, and every foreign
byte must come back exactly as planted. Allocation must skip past the
foreign id, and TS must refuse to reveal the record rather than guess.
Landing the check forced the normalization decision it existed to force.
Python's _normalize_entry_defaults ran its legacy renames on every entry,
foreign ones included -- and those renames are migrations for OUR old
shapes: the negative control shows the old code turning a foreign
"blacklisted" string into our "archived" flag, which is not backfill but
reinterpretation of another application's data. The normalizer now skips
unknown kinds entirely, so both implementations carry foreign records
byte-for-byte untouched and produce identical canonical state -- the
property sync convergence is defined by. Entries with neither kind nor
type are pre-kind legacy Python data and still normalize as before.
The spec's gap ledger entry for the divergence is resolved, and the Python
tolerance tests are tightened from field-subset to exact equality, plus a
trap-field test pinning that blacklisted/website/words on a foreign record
mean whatever the other application says they mean.
Verified: with the normalizer gate stashed, Phase L fails on the exact
mangling ("archived": "foreign meaning, not our archive flag"); with it in
place, 38/38 cross-impl checks and 1183 Python tests pass.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8
This commit is contained in:
1 parent
e434b0abc1
commit
cdd74f379f
5 files changed
+139
-20
No files matched your search
@@ -985,6 +985,84 @@ def phase_k(tmp: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def phase_l(tmp: Path) -> None:
|
||||
print("\nPhase L: foreign data round-trip — unknown kinds/fields survive A->B->A")
|
||||
# Spec section 8.6: for a vault holding records neither side fully
|
||||
# understands (a future BitLogin kind, unknown fields, an unknown
|
||||
# top-level index key), an interleaved Python -> TS -> Python
|
||||
# read-modify-write cycle must preserve every foreign byte. The trap
|
||||
# fields (blacklisted/website/words) are names Python's legacy
|
||||
# migrations act on for ITS shapes; touching them on a foreign record
|
||||
# would be reinterpretation, which the spec forbids.
|
||||
app_dir = tmp / "l"
|
||||
app_dir.mkdir()
|
||||
fp = py_create_profile(app_dir, SEED_A, PASSWORD)
|
||||
env = {"SEEDPASS_MNEMONIC": SEED_A}
|
||||
|
||||
foreign_entry = {
|
||||
"kind": "bitlogin_org",
|
||||
"type": "bitlogin_org",
|
||||
"label": "Acme Corporation",
|
||||
"modified_ts": 1700000123,
|
||||
"blacklisted": "foreign meaning, not our archive flag",
|
||||
"website": "foreign meaning, not our label alias",
|
||||
"words": ["foreign", "list"],
|
||||
"bitlogin": {"admins": ["npub1aaaa"], "roles": {"sales": ["npub1bbbb"]}, "policy_rev": 7},
|
||||
}
|
||||
foreign_top_level = {"spec": "bitlogin-v1", "org_count": 1}
|
||||
|
||||
# Python plants the foreign data at a fresh id beside the native entries.
|
||||
vault, em, _cfg = _py_vault(app_dir, fp, SEED_A)
|
||||
data = vault.load_index()
|
||||
pre_count = len(data["entries"])
|
||||
foreign_id = str(max((int(k) for k in data["entries"]), default=-1) + 1)
|
||||
data["entries"][foreign_id] = dict(foreign_entry)
|
||||
data["bitlogin_meta"] = dict(foreign_top_level)
|
||||
vault.save_index(data)
|
||||
|
||||
# Python read-modify-write on a neighbour.
|
||||
em2 = _py_vault(app_dir, fp, SEED_A)[1]
|
||||
em2.add_entry("native-python", 16)
|
||||
|
||||
# TS read-modify-write: create and edit, both of which re-validate and
|
||||
# re-encrypt the whole index.
|
||||
run_cli(app_dir, "entry", "add", "key-value", "native-ts", "k", "v", env_extra=env)
|
||||
run_cli(app_dir, "entry", "modify", "0", "--notes", "touched", env_extra=env)
|
||||
|
||||
# Python reopens: every foreign byte must be exactly as planted.
|
||||
_seed, final = py_open_profile(app_dir, fp, PASSWORD)
|
||||
check(
|
||||
"foreign entry survives Python->TS->Python byte-for-byte",
|
||||
final["entries"].get(foreign_id) == foreign_entry,
|
||||
json.dumps(final["entries"].get(foreign_id), sort_keys=True)[:200],
|
||||
)
|
||||
check(
|
||||
"foreign top-level index key survives",
|
||||
final.get("bitlogin_meta") == foreign_top_level,
|
||||
json.dumps(final.get("bitlogin_meta"), sort_keys=True),
|
||||
)
|
||||
labels = {e.get("label") for e in final["entries"].values()}
|
||||
check(
|
||||
"native entries from both sides coexist with the foreign record",
|
||||
{"native-python", "native-ts"} <= labels,
|
||||
str(sorted(str(l) for l in labels)),
|
||||
)
|
||||
check(
|
||||
"allocation skipped past the foreign id (no collisions, no reuse)",
|
||||
len(final["entries"]) == pre_count + 3
|
||||
and max(int(k) for k in final["entries"]) == int(foreign_id) + 2,
|
||||
str(sorted(final["entries"].keys(), key=int)),
|
||||
)
|
||||
# And the foreign record is invisible to secret materialization rather
|
||||
# than guessed at: TS reveal must refuse it.
|
||||
try:
|
||||
run_cli(app_dir, "entry", "reveal", f"sp://entry/{foreign_id}", env_extra=env)
|
||||
refused = False
|
||||
except Exception:
|
||||
refused = True
|
||||
check("TS refuses to reveal the foreign record", refused)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--skip-relay", action="store_true")
|
||||
@@ -1008,6 +1086,7 @@ def main() -> int:
|
||||
phase_i(tmp)
|
||||
phase_j(tmp)
|
||||
phase_k(tmp)
|
||||
phase_l(tmp)
|
||||
if not args.skip_relay:
|
||||
phase_f(tmp)
|
||||
finally:
|
||||
|
||||
Reference in new issue
Block a user