Skip to content

release: prepare content-addressable 0.1.1 (identity/classification layer) - #86

Merged
hartsock merged 1 commit into
mainfrom
release/0.1.1-identity-layer
Aug 17, 2026
Merged

release: prepare content-addressable 0.1.1 (identity/classification layer)#86
hartsock merged 1 commit into
mainfrom
release/0.1.1-identity-layer

Conversation

@hartsock

Copy link
Copy Markdown
Owner

What this PR does

#85 is already on main (dbb94c3). This cuts the 0.1.1 bugfix release and
closes two documentation gaps found while re-verifying it. No code behavior
changes.

Why re-verify at all

#85's green matrix was measured against 534d96d. main then took serde
1.0.229, serde_json 1.0.151, pyo3 0.29.2 and thiserror 2.0.20 underneath it, and
#85 merged without a textual conflict — which is not the same thing as a
validated tree. So the first thing I did was re-run everything on dbb94c3.

Result: green, and the dep bumps are inert here. Details in the test plan.

The invariants hold as enforced code, not just prose:

invariant how it is enforced
WellFormedCid ≅ ContentId ⊎ RawContentId ⊎ ForeignCid ForeignCid's every constructor (new, TryFrom<Cid>, FromStr, from_bytes, Deserialize) rejects a recognized profile; ClassifiedCid::from_cid is total. Pairwise disjoint, jointly total.
variant cannot be spoofed Deserialize re-derives the variant from wire bytes rather than trusting a tag
mint narrowly, recognize broadly two mintable profiles only; both validate version + codec + hash code + digest length
classification ≠ verification ClassifiedCid proves profile membership; content verification stays verify / ensure_content_id / VerifiedStore
legacy edge-only default-off, parse-only, Rust-only; canonical FromStr never learns a dialect
identity is not digest equality both profiles are newtypes over the whole Cid with derived Eq/Hash/Ord — no cross-type PartialEq, no From either way

Gap 1 — a default-off surface with no stability statement

docs/STABILITY.md is the crate's answer to "may I persist this?" — the whole
reason it has a What is NOT frozen section. That section covered
unstable-merkle and unstable-store but not the two features #85 added.
IdentityMigration appeared nowhere in the document at all; its non-frozen
status lived only in a Cargo.toml comment and a module doc.

That silence is expensive in the wrong direction. The record is itself
content-addressed, so its field names are load-bearing for its own id — a
consumer who read the silence as "settled" would persist identity claims that a
later rename invalidates.

Both features now have subsections stating what is unfrozen (API for legacy;
API and wire bytes for migration) and, for migration, the condition that
would freeze it: golden migration vectors making its content-addressed
representation explicit and reproducible — the same bar vectors.json /
raw_vectors.json already meet.

Guarded, not just written: tests/stability_doc.rs fails if any declared
unstable-* feature is unmentioned under that heading. Against main's shipped
text it names exactly ["unstable-legacy", "unstable-migration"]. It
deliberately does not judge what the prose says — a test cannot; wording
stays a review concern. Its parser has its own test (comments, other tables,
non-list values) so the guard cannot pass by parsing nothing.

Gap 2 — one survivor of the VerifiedCidClassifiedCid rename

Cargo.toml's unstable-legacy comment still said VerifiedCid. Small, but it
reintroduces exactly the ambiguity the rename removed: this crate already has a
VerifiedStore that does check content against its id.

Test plan

Run on merged main (dbb94c3) and again on this branch:

  • just check — fmt · clippy --all-features -D warnings · cargo test +
    cargo test --all-features · doc -D warnings · leaf guard ✅
  • MSRV 1.85 (just msrv) ✅
  • Pythonmaturin develop + pytest -q: 105 passed
  • Feature matrix (the gap CI leaves — CI runs only default and
    --all-features): each of unstable-merkle / unstable-store /
    unstable-legacy / unstable-migration alone, plus no-features — test
    and clippy -D warnings for every one ✅
  • serde/adversarial — in-module: foreign_cannot_alias_a_recognized_profile_through_any_door
    (both serde flavors), crafted/self IdentityMigration records refused in JSON
    and dag-cbor, cidv0_is_foreign, adversarial store backends ✅
  • CI on the merge commit dbb94c330 — success ✅
  • just verify-release — Rust 0.1.1 == PEP 440 0.1.1
  • Python surface audited: exports ContentId, RawContentId, content_id,
    to/from_canonical_dagcborno legacy, no migration leak

Out of scope

  • Tagging. Pushing v0.1.1 triggers publication to crates.io and PyPI,
    which is irreversible — it stays a human act per RELEASING.md.
  • No change to ContentId's frozen profile, the profile law, the classifier
    algebra, the legacy posture, or IdentityMigration's validation. The code is
    feat(identity): RawContentId + ClassifiedCid, the profile law, legacy adapters, IdentityMigration #85's as merged.
  • Not filed: an issue tracking the golden migration vectors. Nothing open
    tracks them today, so item 6's hold has no recorded exit condition outside
    this document — flagged rather than filed unilaterally.

…ayer)

#85 is on `main` (`dbb94c3`) and its full matrix re-verified green there. This
cuts the bugfix release and closes two documentation gaps found while
re-verifying it. **No code behavior changes.**

WHAT WAS VERIFIED ON MERGED `main` FIRST

#85's green matrix was measured against `534d96d`; `main` then took serde
1.0.229, serde_json 1.0.151, pyo3 0.29.2 and thiserror 2.0.20 underneath it and
the PR merged without textual conflict — which is not the same as a validated
tree. Re-ran everything on `dbb94c3`: `just check`, MSRV 1.85, `pytest` 105,
each `unstable-*` feature ALONE (test + clippy) plus no-features and
all-features, and CI on the merge commit. All green; the dep bumps are inert
here.

The invariants also hold as enforced code, not just prose: `ForeignCid`'s every
constructor rejects a recognized profile and `ClassifiedCid::from_cid` is total,
so the three variants are pairwise disjoint and jointly total; `Deserialize`
re-derives the variant from wire bytes; both mintable profiles validate version
+ codec + hash code + digest length; identity is the whole `Cid` newtype, never
the digest; the Python surface exports neither legacy nor migration.

GAP 1 — a default-off surface with no stability statement

`docs/STABILITY.md` is the crate's answer to "may I persist this?", which is
why it has a **What is NOT frozen** section. That section covered
`unstable-merkle` and `unstable-store` but NOT the two features #85 added.
`IdentityMigration` appeared nowhere in the document at all; its non-frozen
status lived only in a `Cargo.toml` comment and a module doc.

That inference is expensive in the wrong direction. The record is itself
content-addressed, so its field names are load-bearing for its own id — a
consumer who read the silence as "settled" would persist identity claims that a
later rename invalidates. Both features now have subsections stating what is
unfrozen (API for legacy; API *and* wire bytes for migration) and, for
migration, the condition that would freeze it: golden migration vectors making
its content-addressed representation explicit and reproducible, the same bar
`vectors.json` / `raw_vectors.json` already meet.

Regression guard, not just prose: `tests/stability_doc.rs` fails if any
declared `unstable-*` feature is unmentioned under that heading. Verified
non-vacuous — against `main`'s shipped text it names exactly
`["unstable-legacy", "unstable-migration"]`. It deliberately does not judge
what the prose *says*; a test cannot, and wording stays a review concern. Its
parser is covered by a second test (comments, other tables, non-list values)
so the guard cannot pass by parsing nothing.

GAP 2 — one survivor of the `VerifiedCid` → `ClassifiedCid` rename

`Cargo.toml`'s `unstable-legacy` comment still said `VerifiedCid`. Small, but it
reintroduces exactly the name ambiguity the rename removed — this crate already
has a `VerifiedStore` that *does* check content against its id, and two meanings
of "verified" in one surface is what the rename existed to prevent.

RELEASE

Three version strings bumped in lockstep (0.1.0 → 0.1.1); `just verify-release`
confirms Rust '0.1.1' == PEP 440 '0.1.1'. CHANGELOG entry describes the release
as additive: the frozen `0.1.0` core is untouched, every golden vector is
byte-identical, and identifiers minted under `0.1.0` remain valid.

Tagging is deliberately NOT done here — pushing `v0.1.1` triggers the publish
workflow to crates.io and PyPI, which is irreversible, so it stays a human act
per RELEASING.md.

Co-authored-by: Claude Opus 5 (1M context) (Claude Code) <309460085+newt-agent@users.noreply.github.com>
@hartsock
hartsock merged commit ba2eb59 into main Aug 17, 2026
8 checks passed
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