Skip to content

docs: add bounded exhaustive model for NIP-RS manual-unread encoding - #1958

Closed
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/nip-rs-unread-model
Closed

docs: add bounded exhaustive model for NIP-RS manual-unread encoding#1958
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/nip-rs-unread-model

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Bounded exhaustive model comparing two candidate CRDT encodings for a manual mark-as-unread override layer within NIP-RS read state: lexicographic operation register (A) vs two grow-only counters + baseline (B).

  • BFS transition-system explorer over 7,129 canonical states per tie policy (14,258 total) with 2 upgraded + 1 legacy device, 2 contexts, interleaved actions/deliveries/compaction/legacy rewrite/reinstall
  • Directed deep-history parameter cube (672 points) proving compaction never permits stale-replay resurrection or loss of a genuinely-live override, across compact → local counter reuse → delayed stale delivery, both tie policies
  • Compaction uses a tombstone floor (RegB(0, max(S,C), 0)): a register that ever had S>0 or C>0 is never fully deleted, blocking counter reuse; only virgin (never-set) registers compact to None
  • Mandatory canonical publication: serialization always emits the canonical form of every register, closing a dead+dead resurrection path under merge (I5e: 45,074 ordered publish-merge pairs checked, 156² + 144² + 2 directed witnesses)
  • Atomic slot-grouping transport rule (normative, same weight as canonical publication): a context's frontier entry and ALL of its ov_* sibling entries must travel in the same slot. Grouping derives one logical identity for both key classes — frontier wire keys are unescaped via unescape_context_key() before grouping — so a partial register is structurally impossible from a compliant publisher, including for opaque context IDs that collide with reserved prefixes (e.g. raw ov_s:evil). Verified with interleaved receive → canonical re-publish → receive-rest → re-publish delivery tests and an old/new slot-coordinate mixture regression
  • Collision-safe reserved key namespace (ov_/esc: escaping) so a pre-existing opaque NIP-RS context ID cannot be misparsed as override control state; verified with an adversarial round-trip test
  • All invariants proven under both tie policies (clear-wins, set-wins) with policy-dependent compaction documented; Candidate A is exercised for I1/I7/I9 only (it fails I7, making further A coverage moot)
  • Mutation harness: 9/9 mutants caught via DeviceB subclass injection with recorded counterexamples, including directed regressions for the tombstone-floor resurrection witness (M6), raw serialization without canonicalization (M7), per-entry slot splitting (M8), and escaped-key grouping (M9)
  • Sizing rebuilt with realistic UUID/hex64 context IDs, covering both live-override and permanent-tombstone byte costs; packed 16/16 variant removed (not merge-preserving); >256-byte key drop hazard documented
  • Candidate A fails legacy rewrite (overrides erased by legacy client); candidate B passes all exercised invariants
  • Recommendation: candidate B with clear-wins tie policy, mandatory canonical publication, and the atomic slot-grouping rule

New files in docs/formal/nip-rs-unread/: model.py, exhaustive.py, mutation.py, NOTE.md. Both scripts are deterministic python3 and exit 0. Also adds __pycache__/ and *.pyc to the repo .gitignore.

@wpfleger96
wpfleger96 force-pushed the duncan/nip-rs-unread-model branch 3 times, most recently from 56e5c8a to 62f97c2 Compare July 17, 2026 18:58
…d model

Replaces delete-on-dominance compaction with a tombstone-floor rule
(RegB(0, max(S,C), 0)) so dead/dominated registers never lose the
counter ceiling that blocks stale-replay resurrection; only virgin
(never-set) registers still compact to None. Adds a directed
deep-history parameter cube (2,016 points) covering compact -> local
set/clear counter reuse -> delayed stale delivery (single- and
2-slot-split) for the same-device replay shape, proving the tombstone
floor holds where the depth-4 BFS explorer structurally cannot reach.
Reworks the M4 mutant to revert to the old delete-on-dominance rule
and directly reproduce Thufir's exact resurrection witness
(RegB(3,0,10) -> None -> reuse -> RegB(3,2,20), override_is_set=True).

Adds a collision-safe reserved-key escape (ov_/esc: prefixes) so a
pre-existing opaque NIP-RS context ID cannot be misparsed as override
control state, with an adversarial round-trip test.

Adds a cross-device compaction-transparency check: a tombstone's
counter ceiling can one-shot suppress an unrelated device's concurrent
fresh set (never resurrect it, per a structural merge-monotonicity
lemma over 1,728 points), recoverable by one more local action. This
requalifies I5c's original "compaction never changes the semantic
outcome" claim, which only holds for same-device replay of a device's
own pre-compaction ancestor; NOTE.md states the narrower proven
property and cites the pre-existing clear-wins false-negative tradeoff
this shares with an uncompacted stale clear.

Makes canonical publication mandatory rather than advisory:
DeviceB.publish_blob() now canonicalizes every override against the
device's own effective frontier at serialization time, unconditionally
-- live unchanged (3 keys), dead folded to the tombstone floor (1 key,
ov_c: only), virgin omitted (0 keys) -- regardless of whether
do_compact was ever called locally first. This closes a CRITICAL found
in independent review: two individually-dead but *uncompacted*
published registers from different device histories could
componentwise-max-merge into a live resurrection, because publication
previously depended on an unstated compact-before-publish discipline.
do_compact remains a separate, optional local storage-GC transition,
decoupled from publication correctness. Adds
test_published_merge_closure: Thufir's exact witness pair under both
tie policies plus a general search over a 300-point-per-policy cube of
independently-dead published states pairwise-joined in both delivery
orders, including a one-hop relay-republish path for delayed/multi-hop
delivery (45,074 pairs checked, 0 violations). Adds mutant M7
(publish-without-canonicalization), caught directly via the witness
reproduction.

Fixes the tombstone wire shape to match the sizing claims: a dead
override now serializes as exactly one ov_c: key instead of three
zero-padded keys, closing an IMPORTANT finding that the schema and
storage-budget tables assumed a 1-key tombstone while the serializer
emitted 3. Adds an exact wire-shape regression (live=3 keys,
tombstone=1 key, virgin=0 keys, both tie policies) and extends
I8/test_bounded_growth to check the tombstone shape after 100
set/clear round trips.

Documents (rather than silently carries) the model's legacy
drop-on-receive simplification: DeviceB(is_legacy=True) does not
forward ov_* sibling keys it receives, unlike production's
sanitizeContexts pass-through -- explained with the reason the BFS's
own convergence/compaction verdicts are unaffected (legacy devices
never act on overrides in the explorer). States canonical publication
in NOTE.md as a protocol requirement (spec-amendment implication:
production clients MUST canonicalize before publish) and scopes the
reserved-namespace escape codec explicitly as a backward-compatibility
limitation, not a collision-safe migration of pre-existing unescaped
ov_-prefixed legacy data.

BFS explorer: 7,129 states/policy; published-state merge closure:
45,074 pairs/both policies; both suites (exhaustive.py, mutation.py)
exit 0, 7/7 mutants caught.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 2 commits July 25, 2026 00:41
…nread model

Resolves Thufir's final-pass NOT CLEAR findings on PR #1958:

CRITICAL (transport atomicity): encode atomic slot-grouping rule.
A context's frontier entry and all ov_s:/ov_c:/ov_b: sibling entries
MUST travel in the same slot. Adds DeviceB.split_blob_into_slots
(model.py) which round-robins per-context groups, not per-entry.
Updates _ancestor_split_ctx_dicts and test_multi_slot_union to use
grouped splits. Adds test_interleaved_delivery_grouping exercising
both slot orders and delayed transient re-publication to a third
observer (Thufir's exact witness). Adds mutant_m8 (M8_PerEntrySplit)
which reverts to per-entry splitting and confirms the new test catches
the false-clear counterexample. Documents the rule in NOTE.md under
Multi-slot union with same normative weight as canonical publication.

IMPORTANT (Candidate A coverage): NOTE.md invariant table marks I2,
I3, I4, I6 as 'not exercised' for A instead of incorrect PASS.

IMPORTANT (I5e cube-size wording): '300 points per tie policy'
corrected to '156 clear-wins + 144 set-wins = 300 total'; 45,074 =
156^2 + 144^2 + 2 directed witnesses.

MINOR (mutation section): count updated from 6 to 8; M7 and M8 added
to table and narrative; M1-M6 subclass list updated to include M7,M8.

Cosmetic MINOR: restore Candidate-comparison heading consumed by
prior I5e insertion.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…e rewrite, M9, cleanup

CRITICAL: fix escaped-context-ID identity mismatch in split_blob_into_slots
(model.py). Frontier wire keys are escaped (e.g. 'esc:ov_s:evil') while ov_*
sibling keys use the raw suffix ('ov_s:evil'). Without unescaping the frontier
before grouping, the two entries land in different slots — a compliant publisher
can still produce a split register for any context whose raw ID starts with a
reserved prefix. Old/new slot-coordinate mixtures across publication cycles then
reproduce the round-1 partial-reconstruction poison. Fix: derive group identity
via unescape_context_key(wire_key) for frontier keys. Add M9 mutant reverting to
escaped-key grouping; add test_escaped_context_slot_grouping regression (two
publication cycles, old/new slot-coordinate mixture, canonical re-pub in loop).

IMPORTANT: rewrite test_interleaved_delivery_grouping with correct oracle. Prior
version never delivered first_slot to final_a/final_b and never performed the
receive-rest → re-publish step (substituted source's full blob). Rewritten to
exactly: partial_obs receives first_slot → publishes transient_1 → receives
second_slot → publishes transient_2; three representative delivery orders for
third-party finals covering the toxic path and both orderings. Re-validated: M8
caught in clear-wins toxic branch (3 violations), zero false violations under
set-wins.

IMPORTANT: fix NOTE prose vs executable coverage.
- NOTE:178 'Both candidates converge under all tested delivery permutations'
  rephrased as algebraic property with note that I2-I4/I6 are exercised for
  Candidate B only.
- NOTE:615 'Both pass all merge invariants' scoped to Candidate B (I2-I8
  exhaustively verified; A exercised only on I1/I7/I9).
- NOTE:656-658 distinguished abstract grouping model (modeled by
  split_blob_into_slots, including unescape fix) from production TypeScript
  splitter (not modeled).
- Added unescape-before-group rule and M9 to Multi-slot section and mutation
  table; updated count 8→9.

IMPORTANT: delete docs/formal/nip-rs-unread/__pycache__/*.cpython-314.pyc
(107,366 bytes, embedded absolute worktree path). Add __pycache__/ and *.pyc
to .gitignore to prevent recurrence.

MINOR: collapse deep-history delivery shapes 3→1 (2,016→672 points). The
split_fwd/split_rev shapes became semantically identical to single once the
atomic-grouping rule made a single-context compliant split whole-register+empty;
removed _ancestor_split_ctx_dicts (now dead code). Update NOTE cube-size figure.
Fix M8 witness narrative: block comment said 'ov_s+ov_b' in one slot; actual
M8 puts frontier+ov_s in slot 0, ov_b+ov_c in slot 1.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 marked this pull request as ready for review July 31, 2026 14:55
@wpfleger96
wpfleger96 requested a review from a team as a code owner July 31, 2026 14:55
wpfleger96 added a commit that referenced this pull request Jul 31, 2026
…nread model

Resolves Thufir's final-pass NOT CLEAR findings on PR #1958:

CRITICAL (transport atomicity): encode atomic slot-grouping rule.
A context's frontier entry and all ov_s:/ov_c:/ov_b: sibling entries
MUST travel in the same slot. Adds DeviceB.split_blob_into_slots
(model.py) which round-robins per-context groups, not per-entry.
Updates _ancestor_split_ctx_dicts and test_multi_slot_union to use
grouped splits. Adds test_interleaved_delivery_grouping exercising
both slot orders and delayed transient re-publication to a third
observer (Thufir's exact witness). Adds mutant_m8 (M8_PerEntrySplit)
which reverts to per-entry splitting and confirms the new test catches
the false-clear counterexample. Documents the rule in NOTE.md under
Multi-slot union with same normative weight as canonical publication.

IMPORTANT (Candidate A coverage): NOTE.md invariant table marks I2,
I3, I4, I6 as 'not exercised' for A instead of incorrect PASS.

IMPORTANT (I5e cube-size wording): '300 points per tie policy'
corrected to '156 clear-wins + 144 set-wins = 300 total'; 45,074 =
156^2 + 144^2 + 2 directed witnesses.

MINOR (mutation section): count updated from 6 to 8; M7 and M8 added
to table and narrative; M1-M6 subclass list updated to include M7,M8.

Cosmetic MINOR: restore Candidate-comparison heading consumed by
prior I5e insertion.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96

Copy link
Copy Markdown
Member Author

🤖 Consolidated into #2864 per Will's request — all four model files and the .gitignore entries are now in the duncan/nip-rs-unread-spec branch at head 48cc940fc. Closing this PR; the duncan/nip-rs-unread-model branch is retained until #2864 merges.

@wpfleger96 wpfleger96 closed this Jul 31, 2026
@wpfleger96
wpfleger96 deleted the duncan/nip-rs-unread-model branch July 31, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant