logs.gokuls.in

1 pull request merged across 1 repo

Offline-Protocol/offline-protocol-sdk

CLAUDE.md had grown to 61 KB in a handful of paragraph-long bullets. It was unreadable by people, unreviewable in a diff, and the only home for knowledge that several parts of the codebase depend on. CHANGELOG.md had the same problem for a different reason: 2,425 lines with no archiving story.

This relocates the durable knowledge into documents that can be reviewed, linked and cited, and cuts CLAUDE.md to repository instructions plus a pointer table: 61,107 → 7,606 bytes.

New reference material

PathScope
docs/spec/Implementation-independent protocol specification: identity and address derivation, the message model with both encodings and the frozen binary layout, the reserved prefix registry with the signing gate and its two exemption classes, the three encryption envelopes, the group protocol, capability negotiation
docs/security/threat-model.mdAssets, six adversary classes, five trust boundaries, a control-to-attack table, and residual risks R1 through R8 stated plainly
docs/state-machines/Delivery and acknowledgement, outbox and retries, session lifecycle, group message lifecycle, transport lifecycle, with 10 Mermaid diagrams
docs/adr/Fifteen decisions that are expensive to reverse or easy to undo by accident, each with a "what would undo this" note
docs/bridges/The Rust to Swift/Kotlin/Python/TypeScript contract, every rule of which fails silently when violated

Changelog archiving

CHANGELOG.md now holds unreleased changes plus the current release; older releases move to docs/changelog/, one file per minor series with its own release table.

All 31 release sections are accounted for, none duplicated and none lost, verified by diffing the old and new release bodies section by section. Content is otherwise byte-identical apart from a blank line at the split boundary and three docs/UPGRADING.md links in the 0.19 and 0.20 sections, which had to become ../UPGRADING.md to keep resolving from their new depth. No CI job or script parses the changelog, so the split is safe.

This adds a step to the release cut: after cutting, move the now-previous release's section into docs/changelog/<major>.<minor>.md and update both archive tables. The procedure is written down in the archive index.

Related issues

None.

Type of change

  • docs — documentation only

Checklist

  • cargo fmt --all -- --check passes
  • cargo clippy --workspace -- -D warnings passes
  • cargo test --workspace passes (2,132 tests, 0 failures, doctests included)
  • cargo-deny unaffected (no dependency, license or manifest changes)
  • Commits follow Conventional Commits
  • Docs / CHANGELOG.md updated
  • No new unsafe
  • UDL unchanged, so no binding regeneration needed

Breaking changes

None. No behavioural change of any kind. The non-markdown edits are all comments: five Rust doc comments that pointed at CLAUDE.md sections which no longer exist there and now name the specific document, plus one TypeScript and one Kotlin comment in 174016b that named a guard test by a stale name.

Notes for reviewers

Deliberately not carried over: the per-feature Coverage: test_x, test_y, ... lists. They were the most staleness-prone part of the old file, and enumerating test names in prose recreates exactly the problem this change is addressing. The docs name the *behaviour* that is pinned instead, and grep finds the tests. Happy to add a generated test index if reviewers want that back, but it should not be hand-maintained prose.

One claim in the old CLAUDE.md was already stale, which is itself an argument for the move: it described the wire v1 extension TLV registry as having only tag 1, but wire.rs has tag 2 (reply context) as well. The spec documents both, along with the rule that distinguishes them: tag 1 shipped in v1's first release so its absence would truncate content, which fixes the constraint that no future v1 tag may change meaning by being ignored.

Where to focus review. The docs are long, but the load-bearing parts are the ones that describe shapes which are correct only as a whole, since those are what a future change is most likely to undo by accident:

  • the six pieces of the deferred-acknowledgement atom (state machine, ADR 0005)
  • the desync classification gating the re-key rather than the acknowledgement (ADR 0006)
  • the inverted logical-identifier marking rules on the mesh versus relay group paths (ADR 0011)
  • the fail-open rule in membership enforcement (ADR 0009)
  • the four-source commit walk in the leaf identity binding (ADR 0010)

Corrections welcome on any of these: the docs are an attempt to state the invariants precisely, and if one is stated wrong it is better caught now than cited later.

Every relative link and anchor in the new and edited files was verified to resolve.

---

Review corrections (174016b)

Every load-bearing claim in the new documents was verified against the code at HEAD. The review found roughly two dozen statements that did not hold, all now fixed in 174016b. The classes worth calling out, since they say something about where this kind of document goes wrong:

Three stated pre-fix behaviour as current design. The parked-DM section said a parked message "cannot settle on its own", which was true before the offer-plus-settle pair landed; the delivery machine said the desync arm was unreachable from the drain, where it is in fact the routine case; the session machine said confirmation triggers only on a successful decrypt, when the joiner confirms on the Welcome itself. Prose that describes the bug rather than the fix is the failure mode this restructure is most exposed to, because the fix and the document were written at different times.

One security property was stated backwards in three places. Permanent refusals have two dispositions: a policy refusal is acknowledged, a security refusal is silent and unmarks the identifier. ADR 0005, ADR 0006 and the delivery state machine each carried only the first half, which would have licensed exactly the change the interception exists to prevent. The delivery document's own media section had it right, so the file contradicted itself.

ADR 0003's tiebreaker rule was wrong for three of its four named sites. Only session ownership compares hash bytes; leave election, admin promotion and fork leader sort rendered strings. Each converges on its own, so the correct invariant is per tiebreaker, and the hazard the ADR should have named is harmonizing one site onto the other order.

A bridge contract named a guard test that does not exist. react_native_mesh_wake_task_key_matches_native is really react_native_mesh_wake_wiring_is_present; the stale name was in two source comments as well, so those are fixed in the same commit. Also in that family: swift test does not cover error mapping (the suite is excluded from the package manifest), the FFI error match is no longer compiler-exhaustive now that the engine error types are #[non_exhaustive], and the Android local-test workaround now lives in docs/bridges/kotlin.md instead of pointing at a guide that never contained it.

Two of the findings are code defects rather than documentation defects, and are documented as such rather than papered over:

  • On the mesh path a security-refused group frame never releases its group-level dedup entry (the relay path does), so a verbatim replay is absorbed by the duplicate branch and acknowledged, which is the liveness confirmation the refusal exists to withhold. docs/spec/group-protocol.md states the conforming behaviour and marks the current mesh behaviour a defect.
  • mark_key_package_synced deletes a key package record without purging its provider key, which ADR 0012 presented as a removed leak. It is now recorded as a surviving exception with the reasoning for and against.

Both want their own issues; neither belongs in a docs-only PR.

No behavioural change in this PR. cargo fmt, RUSTDOCFLAGS="-D warnings" cargo doc, the 24 React Native pinning guards, and the TypeScript typecheck all pass on the corrections.