Design-docs architect review, seam skeletons, and SLICES.md#384
Merged
Conversation
The module map marked the implemented crypto core (primitives, keys, encryption, provenance, verify_asset, backup) as planned while a dozen docs described deferred networked/MLS surfaces in the present tense. Align every status claim with what actually ships: - module-map: drop stale (planned) markers; add the missing crypto::authority, lifecycle, validation, and cbor rows; re-home hardware-key adapters from capsule-sdk to capsule-core + the swift/kotlin harnesses; roster rows for capsule-core-ffi and capsule-vision plus a non-cargo package appendix; mark the E2E surface as contract-only until the first networked slice lands - principles: define the Implemented / Planned / Blocked status convention all docs now follow - keys: document the implemented write-authority seam (AlbumAuthority, ReferenceAuthority, SignedEpochLedger) that verify_asset actually consumes, with OpenMlsAuthority as the blocked drop-in - mls: add the upstream-blocker status note (0x004D has no IANA codepoint and no RustCrypto OpenMLS backend, openmls#1940) that MLS-dependent docs now link instead of implying live MLS - federation, peering, authentication, validation, organization, versioning, mls-resilience, device-enrollment, moderation: fix present-tense implemented-in claims for planned surfaces - filesystem/client: mark the cache-eviction sweep implemented (issue #23); import/pipeline: mark the streaming mode and free-space probe planned and note the legacy executor still writes the unsigned sidecar
The docs never stated which server surface speaks which transport, and contradicted each other where they tried: download-sync specified an HTTP GET /sync while the module map called capsule-api-sync gRPC, the threat-model handshake was HTTP-only across three transports, and the OpenAPI-generated SDK could never drive the GraphQL library API. Decide and document the split: REST/OpenAPI for every request/response surface (auth, upload, blob fetch, shares, drops, storage-verify), gRPC (capsule.sync.v1.SyncService) for the sync feed and federation pull, and no server surface at all for rich queries — they run client-side over library.sqlite, which is what the E2EE key-free server model demands. GraphQL (capsule-api-library) is frozen and retiring. - new owner doc design/api-surfaces.md: surface-to-transport map, REST-header to gRPC-metadata mapping, HTTP-status to gRPC-status rejection mapping with the error.* code as the cross-transport discriminator, and the GraphQL retirement rationale - download-sync: the feed is the gRPC Sync RPC (manifest travels as opaque canonical CBOR); blob fetch stays REST ranged GET; sync_seq typed u64; the cursor HMAC noted as a server-internal construction - federation: primitives table gains the transport column; capability rides gRPC authorization metadata - threat-model/validation: the universal gate is stated once in REST terms with the gRPC carriage delegated to api-surfaces - module-map: capsule-api-library marked legacy; E2E case 1 rewritten to auth -> gRPC sync -> client-side library query - principles/index: owner-table rows and overview entries for the new doc plus the previously unlisted web-upload, quota, storage-verification, and mls-resilience layers
Several contracts in the crypto/metadata SSoT docs were ambiguous or self-contradictory in ways an implementer could not code against: - metadata: provenance_chain_hash was defined as the hash of the latest provenance record — a structure that itself commits to a hash of the sidecar, i.e. a hash containing itself. Redefine it as the PRIOR chain head (always equal to the sealing manifest's prior_provenance_hash) and pin the author/seal/sign ordering in a new Provenance Binding and Sealing Order section - metadata: declare the single canonical asset identity (sidecar uuid = manifest file_id = asset_id = blob_id, one UUIDv7) plus the UUID version per identifier; enumerate the previously undeclared content_type and GpsSource closed value sets; bound caption_lww; define the add_id reseed source when local sidecars are absent; and state how the op-log path preserves _unknown byte-fidelity - provenance: metadata_blob_hash becomes Option with explicit presence-by-action; normative wire-presence rules (legacy options encode present-null, later v1 fields encode absent-key — absent key_mode means derived) since presence is signature-visible in canonical CBOR; DerivativeManifest gains the amk_version and protocol_version it needs to be authorization-verifiable; the record's duplicated prior_provenance_hash becomes a checked mirror invariant; chain-walk cost bounded via the cached verified head - provenance + filesystem/server: resolve the manifest's contradicted physical location — a small server-visible envelope object stored content-addressed beside fully opaque ciphertext blobs (never an embedded header), which is also what the keyless index rebuild walks; the server-side provenance chain is that envelope sequence, not an encrypted blob the no-key invariants could never read - encryption: normative ciphertext offset for ranged reads (chunk_index x 65,536 — the plaintext-stride confusion corrupts every ranged read); wrapped-key mode marked planned; the AlbumKeyDistribution message re-homed to its MLS owner - keys: verify_asset takes the ciphertext hash, never the bytes; sponsoree recovery matrix stated (all paths route through the sponsor); directory TOFU first-contact corroborated out-of-band; KDF formulas single-owned by encryption - primitives: crypto_suite_id vs MLS ciphersuite codepoint namespaces disambiguated - maintenance + client: rewrite bundles stage the displaced prior version aside so rollback restores rather than deletes; date buckets fixed at import (capture-date edits do not relocate files); one suite id governs both digests in lockstep - backup: deterministic export requires normalized tar headers - versioning: ceremony deadline evaluated on the server's trusted clock; frozen_state_hash follows the suite hash; MLS group naming deferred to the MLS layer
Security-contract and protocol defects across the networked docs, each either self-contradictory or exploitable as written: - backup-recovery: the 48-bit recovery-code floor wrapping the master key (offline-brute-forceable once the escrow blob leaks) is raised to >=128 bits; sub-floor codes are permitted only inside an attested rate-limiting enclave (SVR pattern) - share-links: the POST .../passphrase endpoint contradicted the contract's own the-server-never-receives-the-passphrase rule — it becomes a GET of the wrapped material with client-side unwrap; the 60s revocation cache is motivated (replicas of one home server) and stays fail-closed - web-upload: the write-gating passphrase gets a real possession proof (stored Argon2id verifier, KDF-derived proof, passphrase never transmitted); adoption's quota wording corrected to bulk-bytes-only; invariant-14-at-drop-finalization made explicit; drop-invariant anchor precision - quota: Grace-expired now exempts the provenance writes a delete or trash-restore itself produces (a user must be able to delete their way back under quota) and states exactly what it adds over Hard-exceeded - federation: capability token gains the referenced-but-undeclared iat claim; min_protocol_version disambiguated (the album pin); the nonstandard aud noted for verifier authors; revoked-jti pruning bounded; invariant 25 applies on pulls; read-derivative-only is now structurally enforceable via the per-role envelope objects; perspective check requires >=2 unanimous vantage points - moderation: 410-vs-404 divergence made an explicit per-surface rule; suspended users keep master-key-gated revoke_all_sessions (the DoS it defended against is already closed upstream, and a compromised suspended user needs it most); no-content-scanner scoped server-side with the client-side AI cross-link - mls-resilience: the lost-commit and local-ahead paths overlapped — a lost commit routes to re-submission and only budget exhaustion or a provable fork routes to discard-and-rebootstrap - device-enrollment + keys: hardware key generation claims corrected to what secure elements ship (classical half hardware-bound and P-256, PQ half software-sealed; the P-256 hybrid variant is the planned composition); the enrollment-code entropy/transcribability tension resolved (full-entropy QR, shorter rate-limited text fallback backstopped by the safety code) - peering: cross-device add/recovery mislink fixed to the right owners; device-key-as-TLS-certificate specified (classical half in the handshake, hybrid IK chain at the application layer); mDNS rotation interval bounded; X-Capsule-Protocol declared its own version space - authentication: the moved certificate gets a path and schema; access tokens are issued-against not derived; WebFinger opt-in record set bounded - ai: the vector-search section described pgvector (HNSW, <#>) for a client-local sqlite-vec index — rewritten for the actual engine; insert refusal targets unknown models, not superseded ones - download-sync: 403 treated as authorization change (re-sync membership) rather than durability loss; idle-notification interaction with session expiry noted - storage-verification: durable scoped to the home server; deep rate-limited and coalesced; the verify-to-release TOCTOU window bounded by re-verify + the GC grace period; endpoint-planned vs predicate-seam status split - pipeline: streaming mode's single-asset headroom floor and the default-album pointer's last-writer snapshot semantics stated - clients: the stale closed-enum list replaced by the blanket rule + owner-doc pointers; the forbidden-behavior tripwire scoped to capsule-core-mapped items; per-platform sandbox primitives named
The threat model's governing tables lagged the docs they govern: - validation: idempotency rows for the five stateful write surfaces the docs introduced without one (device enrollment, MLS re-keying, share/upload-link create + revoke, drop adoption) — the table's own rule says an unlisted surface must be designed before it ships; the universal-headers table becomes the census of the whole X-Capsule-* namespace (upload headers registered by pointer); every rejection now names its error.* catalog code, wiring the orphaned i18n error-code contract into the refuse-by-default surface - threat-model index: cross-reference rows for Share Links, Quota, Moderation, Device Enrollment, and MLS Resilience, which all carry scenario/invariant ties the navigation table omitted - scenarios: rows 40-42 (upload-link passphrase brute-force, capability min_protocol_version doctoring, share-link passphrase offline brute-force) - authentication: the .well-known/capsule/* namespace gets a single registry (server-info, moved certificate, revoked-jti, deprecation) instead of four docs declaring paths piecemeal - i18n: the error-code contract explicitly covers the validation invariants, with the flagship codes named - organization: smart-album definitions and scope overrides bound to the library-settings document metadata already declares (schema is its own slice); StackType's normative value set pinned to the capsule-core::domain enum
Mechanical consistency pass over the whole design set: - one term for the session store (Valkey, matching its owner doc) - ciphertext_sha256 -> the suite-governed ciphertext_hash (the stray name also hard-coded an algorithm against the crypto-agility rule) - versioning's duplicated negotiation-header table replaced by a pointer to the validation registry (one census, per the SSoT rule) - protocol_version consistently framed as date-based (no more 'bumps' / ordinal v_k phrasing, no LaTeX math in peering) - capability-token vs link-capability terminology note in federation - restatements pruned to owner-doc references: web-upload's Drop Key composition, STREAM constants, and opaque-id rule; device-enrollment's master-key and IK compositions - authorization's suite-pin claim aligned with what invariants 2 and 6 actually check; schema-rules' enum census extended to the newly declared value-set owners; failure-modes' sponsoree path count made honest and its AlbumKeyDistribution link re-homed to MLS - RFC 3339 spelling normalized in prose
The type-level half of the web-upload wrapped-key contract and the P-256 hardware seam, binding the slices that implement them (S-A1/S-A6 and S-A4 in the upcoming SLICES.md). Additive only — no behavior change, and the canonical CBOR of every existing signed structure is byte-identical. - ManifestCore gains key_mode (closed enum, Default = Derived), wrapped_file_key (byte-string newtype, following the Hash32 serde discipline so ciborium never emits an integer array), and metadata_blob_hash — all #[serde(default, skip_serializing_if)] so their defaults are ABSENT map keys: signing_bytes() re-serializes the struct for verification, so present-null defaults would have silently invalidated every previously signed manifest with no test catching it. A regression test pins the wire-absence contract and the byte-string encoding; the wire-presence rules are normative in the provenance design doc - DerivativeCore gains the protocol_version and amk_version a verifier needs to select the epoch write-tier key (wire-optional for pre-binding fixtures, required on real writes per the doc) - the asset-keywrap/v1 HKDF label lands beside its siblings in crypto::primitives::info with domain-separation assertions - crypto::keys::p256 is the hardware-composition seam: the ClassicalAlgorithm discriminator, the P256HybridSigningKey skeleton (todo!() until slice S-A4), and an #[ignore]d contract test that names the acceptance criteria (algorithm-tagged hybrid round-trip, Ed25519-verifier rejection, non-exportability) verify_asset behavior and structural_ok are deliberately untouched; presence-by-action enforcement lands with the wrapped-key slice. mise run check-rust passes end to end; all 291 existing core tests pass unchanged plus the two new wire-contract tests.
The two planned capsule-core modules the module map has carried as (planned) rows, materialized as compiling contracts so the implementing slices (S-A5 sharing, S-A6 drops in SLICES.md) bind to a compiler-checked surface instead of prose: - capsule_core::drop — the web-upload client halves: UploadLink / LinkCaps / DropDescriptor (the unsigned guest wire object, kem_ct as a CBOR byte string) / SealedDrop / PendingDrop types, the UploadLinkIssuer + DropAdopter seams lifecycle::Workspace will implement, and the seal_drop entry point (todo!() until S-A6, WASM target noted). Adoption returns the signed create manifest with key_mode = wrapped, closing the loop with the S1 contract types - capsule_core::sharing — ShareScope / ShareLink / ShareLinkId types and the ShareLinkIssuer seam per the share-links contract skeleton, with the client-side passphrase-unwrap rule in the type docs Each module carries #[ignore]d contract tests naming its slice's acceptance criteria (seal round-trip, adoption rewrap acceptance, opaque-id entropy, client-side passphrase unwrap). Compiles under --features ffi with no uniffi surface changes; all existing tests unchanged.
The remaining capsule-core seams DEFERRED.md described in prose, materialized as code contracts: - library::space — available_bytes() (the statvfs/GetDiskFreeSpaceEx probe; todo!() until slice S-B3, since it is real I/O) plus the pure streaming_recommended predicate, implemented and tested: the planner stays deterministic and the probe attaches at confirmation. The planner's total_size accounting is explicitly S-B3 work — the current planner emits per-candidate decisions and counts only - library::storage_verify — the verify-before-destroy gate's client types (StorageVerdict / BlobVerdict / BlobRole mirroring the POST /storage/verify response) and the pure release_is_safe conjunction, implemented and tested: durable AND per-blob re-check AND verify_asset acceptance, with the client never trusting the server's aggregate over details it can recompute. Fetching the verdict and wiring the destructive paths is S-C3/S-D4 work, named by the #[ignore]d contract tests
The planned REST surfaces from the module map, materialized as
oapi-annotated salvo stubs (the pattern the share route already
ships): compiling handlers with todo!() bodies whose request/response
DTOs and status semantics are the contract, mounted so they land in
the generated OpenAPI spec the SDK is built from.
- capsule-api-media routes/verify.rs — POST /storage/verify (slice
S-C3): the batched key-free durability verdict (stored / indexed /
retrievable per declared blob hash, deep re-hash opt-in), the
server half of the verify-before-destroy gate whose client
predicate landed in capsule-core
- capsule-api-media routes/drops.rs — the web-upload drop store
(slice S-C5): guest drop-session creation under /u/{opaque-id}/drop
(indistinguishable 404 for dead links, 403 for cap/quota refusals),
the owner inbox at /drops, atomic adoption at /drops/{id}/adopt,
and discard; mounts under /v1/storage, /v1/u, /v1/drops behind the
existing media feature with new storage/drops OpenAPI tags
- capsule-api-auth routes/devices.rs — the enrollment ceremony
surface (slice S-C7) at devices/enroll + devices/enroll/redeem,
deliberately distinct paths from the existing GET /auth/devices
session listing; issuance requires bearer auth (plus the fresh
local-authorization rule in the doc), redemption is
code-authenticated
- capsule-api-upload envelope.rs — the EnvelopeGate middleware seam
(slice S-C1) that wires the implemented, exhaustively-tested
capsule_core::validation invariants into the write path; unmounted
stub until the slice lands
Workspace builds and lints clean; no existing route or behavior
changes.
The gRPC half of the API-surface decision: a new capsule.sync.v1 package (slice S-C2 in SLICES.md) carrying the sync feed the download-sync design doc now specifies, beside the frozen legacy plaintext service. - proto/capsule/sync/v1/sync.proto — unary-paged Sync(cursor, page_size) with per-album strictly-increasing sync_seq, the closed ChangeKind enum, and per-role blob references. The load-bearing invariant is written into the proto: the signed AssetManifest travels as OPAQUE canonical-CBOR bytes, never re-modeled as proto fields (re-encoding would detach it from its signatures), and blob bytes never ride this service — ranged fetches stay REST - SyncFeedService stub (Status::unimplemented, the crate's accepted form) mounted at its explicit service path BEFORE the legacy catch-all so it wins matching - GrpcHandler genericized over the tonic service so both services share the one salvo bridge (behavior unchanged) - the legacy photolibrary.metadata.v1 service and proto carry the LEGACY-PLAINTEXT freeze marker pointing at retirement slice S-G2
- drop the unused axum workspace dependency (nothing in the workspace references it; the server is salvo) - remove the stale re-enable-when-production-ready comment on capsule-api-upload, which has shipped in the full feature set for some time - correct capsule-core-ffi's only-FFI-aware-crate claim: capsule-core exports its own uniffi surface on a different uniffi version, and the consolidation is tracked as slice S-F1 - convert the floating TODOs on the plaintext-era media/sync surfaces into greppable LEGACY-PLAINTEXT freeze markers pointing at their SLICES.md retirement/conformance slices
SLICES.md is the executable index of everything the design docs specify that is not yet implemented, decomposed into ~45 independently shippable slices across nine lanes (core-crypto, media/import, server, sdk/clients, federation/sharing, platform/FFI, legacy-retirement, ML, blocked-external, design follow-ups), each with a stable ID, its contract anchors, the in-tree seam it binds to, hard dependency edges, command-checkable done criteria, and a validation tier — plus the dependency DAG and a Baseline section recording what already ships. The slice IDs are load-bearing: every skeleton, #[ignore]d contract test, and LEGACY-PLAINTEXT freeze marker added on this branch names its slice, so rg S-xx finds a slice's whole footprint. DEFERRED.md is deleted — its implemented-baseline content moved into the Baseline section and every deferred item became a slice — and its eleven inbound references (core doc comments, harness READMEs, the TPM note) now point at SLICES.md.
Deploying capsule with
|
| Latest commit: |
e716804
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://65cf70fd.capsule-22k.pages.dev |
| Branch Preview URL: | https://design-architect-review.capsule-22k.pages.dev |
The commit-lint job installed bare convco, which mise 2026.7.0 (released today) no longer resolves — convco has never been in the mise registry under a short name, which is exactly why mise.toml pins it as cargo:convco = 0.6.4. Older mise releases resolved the short name anyway; the new registry does not, so the job died at tool install before linting anything. Use the fully-qualified name the pin declares.
Every design doc now carries a schema-validated `status: draft | stable` frontmatter field, starting at `draft`. The field exists because all design docs are queued for a manual re-review before v1: it maps 1:1 to that queue and flips to `stable` per doc as review passes, without affecting publication. Starlight's built-in `draft:` was deliberately not used — it unpublishes pages, which is the wrong semantic for review tracking.
The upload protocol is the contract three parallel slices (server hardening, SDK client, web drops) build against, so every behavior a client can observe is now written down: a full error taxonomy with machine-readable codes, a strictness table (unknown fields, empty chunks, wrong media types, and missing checksums are rejected — a buggy client fails loudly on its first bad request instead of corrupting state later), explicit edge cases, and a per-endpoint header census. Contract changes, each with its reason: - Chunk storage is append-only: one file per session, chunks appended in order. Sequential offsets were already mandatory, so the .part-file + reflink assembly step bought nothing and added a failure scenario (and a Linux-only fast path); it is gone from the design. - Session lifetime is a floor and a cap: >=1 hour guaranteed since last accepted chunk, 24 h maximum, and the server may discard in between as space needs reclaiming. A discarded session is a uniform 404 (no tombstones), matching the platform-wide indistinguishable-404 rule. - PATCH bodies are application/octet-stream (415 otherwise) — the payload is opaque ciphertext; TUS v2's application/partial-upload would claim a compatibility we don't implement. - X-Capsule-Checksum is required: the (upload_id, offset, chunk_hash) idempotency tuple that defends against garbage retries is undefined without it. - Chunk bounds [4 KiB, 16 MiB] become protocol surface; suggested sizes and adaptation tiers are explicitly non-normative client tuning, with the adaptive algorithm documented once and its enforcement locus stated (server rejects, capsule-sdk adapts). - Duplicate session creation is split by state (active -> return session; finalized -> 409 duplicate_blob + asset ref), fixing a self-contradiction between the invariants and the dedup section. - HEAD now reports state via X-Capsule-Upload-Status (a HEAD response cannot carry the JSON body the old text promised); session listing is uploader-scoped (the uploader is the party that resumes). - filename/date leave the wire request: plaintext metadata contradicted the 'no metadata smuggled' rule, and date was silently dropped anyway. Invariants 10 and 12 are amended in place (no renumbering; scenario tables keep their references), and the filesystem docs' .part/assembly mentions are updated in lockstep.
The upload contract mandates a machine-readable error.* code on every rejection, but the catalog only contained error.auth.*. This adds the full error.upload.* domain from the upload protocol's error taxonomy, plus three codes other docs already referenced without a catalog entry (error.protocol.version_unsupported, error.quota.exceeded, error.blob.pending_upload). Generated per-platform files are regenerated with 'mise run i18n'; the upload server slice (S-C1) can now reference compile-checked capsule_i18n::error_codes::UPLOAD_* constants, the same pattern capsule-api-auth already uses.
Brings the unhardened upload skeleton up to the contract the design doc now
freezes, so the hardening slice (S-C1) and the SDK client slice (S-D1) can
build against real types instead of guessing:
- Chunk storage is append-only: one incoming/{upload_id}.bin per session,
length cross-checked against the expected offset before every write,
fsync before ACK. The .part-file + Linux-only reflink assembly path (and
its libc dependency) is deleted — sequential offsets were already
enforced, so assembly bought nothing and added a failure scenario.
- The session record now carries everything finalization needs
(crypto_suite_id, protocol_version, blob_role, manifest_envelope,
intent_id, last_progress_at for the survival-floor anchor), and the wire
request matches the documented body exactly: deny_unknown_fields, and the
plaintext filename/date fields are gone (metadata rides the encrypted
metadata blob; date was silently dropped anyway).
- Silent leniencies removed: empty PATCH bodies are 400 (was: silent no-op
that skipped all validation), Content-Type must be
application/octet-stream (415), X-Capsule-Checksum is required and
verified against the body before any write, oversized chunks are 413,
and a duplicate content hash returns 409 duplicate_blob with the existing
asset id (was: swallowed into a 500).
- Every rejection now renders the {error, code} JSON shape with its
compile-checked capsule_i18n::error_codes constant, stale-offset 409s
carry the authoritative X-Capsule-Offset, HEAD reports state via the new
X-Capsule-Upload-Status header (a HEAD response cannot carry a body), and
the Pending->Uploading transition actually happens on the first accepted
chunk.
Deep envelope enforcement (invariants 1-8, 12's replay store, the CAS
finalize, the discard sweeper and startup scrub) remains S-C1, as indexed
in SLICES.md.
The standalone capsule-media crate had exactly one consumer (capsule-api-upload) and no dependency on capsule-core, so it was a crate boundary without an owner: not independently versioned, not independently shipped, and one more workspace member to wire. It is now the `media` module of capsule-core behind a non-default `media` feature — the default build (crypto data plane, CLI, FFI, future WASM) compiles none of the media stack or its dependencies, and capsule-api-upload opts in explicitly. Mechanical consequences: the media dependencies become optional deps of capsule-core; the crate's internal `fs` sub-feature is folded into `media` (no consumer used media-without-fs); the three examples move to capsule-core gated on the feature; and the PngImage #[ignore] test now carries its slice ID (S-B1) like every other contract-seam test. Module Map and SLICES.md updated to the new path.
progenitor consumes OpenAPI 3.0 only, so the SDK's client generation ran through a lossy 3.1->3.0 schema down-conversion script against a live server — a standing source of drift, and the reason the crate could not compile standalone (the macro's openapi.json input was gitignored and absent, which in turn forced '--exclude capsule-sdk' on every Rust gate). We do not downgrade schemas: the typed REST client will be generated from the OpenAPI 3.1 schema by spargen, our in-house generator (in development; slice S-D8). - progenitor, the generate_api! macro, the downconvert script, and the stale auth_flow example are gone; AuthenticatedClient and UploadClient are commented out (not deleted) as the shape S-D8 revives, per the staged-transition decision. - capsule-sdk now compiles standalone and re-enters every Rust gate (clippy/build/nextest/coverage — exclusions removed from mise.toml). Running its tests for the first time surfaced a broken test the exclusion had been hiding: test_adaptive_scaling_up asserted scaling after 3 chunks, but the warm-up rule (now normative in the upload doc) requires >=5 chunks or >=8 MiB — the test now exercises both sides of the warm-up boundary. Three never-linted format-string warnings fixed. - api-surfaces.md and module-map.md now name spargen (with the no-schema-downgrade rationale) instead of progenitor.
'Every native app works as a local gallery even with no server' was implied across four docs (principles' offline/online divide, api-surfaces' client- side queries, filesystem/client's local index, clients' duties) but owned by none — so it wasn't testable and couldn't accrete requirements. The new local-gallery.md is the owner: FRs (full gallery offline; never-signed-in is a valid mode; import offline; degradation visible never structural; rebuild without a server), security requirements (Recently-Deleted and Hidden views gated behind fresh biometric/credential re-auth with a short grace window; an honest per-platform at-rest posture matrix — mobile sandbox+FBE protects library bytes from other apps and casual file access, desktop is OS user permissions and we say so; no plaintext spillage outside the library root), and assertable NFRs (zero network I/O on read paths; an airplane-mode E2E case). Mechanism docs keep their content and link here for the contract.
The architect requirement is that every grouping operation — manual and automatic/AI — be idempotent and order-independent. Tags (OR-set) and caption/rating (LWW) already were; stack_membership was the one grouping structure with no defined merge rule (a plain optional field, silently last-write-wins by whole-sidecar rewrite). Since v1 has not shipped, the schema is fixed at the root: stack_membership becomes an LWW register over Option<StackMembership> — join, move, and leave (a stamped None) are all the same (ts, device_id)-ordered write, using the exact machinery caption and rating already use. A never-written register stays wire-absent, so existing stackless sidecars encode byte-identically (regression-tested); convergence and replay-idempotency have dedicated tests. Docs now state the requirement once as a normative table (metadata.md 'Grouping Convergence'): OR-set / LWW / computed-view / single-home-plus- ordered-lifecycle, each with why it converges. ai.md pins AI grouping as a pure function of (inputs, model_id, model_version) with sorted inputs and fixed seeds — clustering is otherwise order-sensitive — and fixes the sequencing contract for the deferred quality evaluations (best shot/ framing/exposure): they run strictly after grouping, keyed by (group_id, membership_hash, model_id, model_version), so any regroup invalidates them by key construction and the recompute is deterministic. That deferral gets a slice (S-H4) in the SLICES reconciliation commit.
Two asset attributes the schema could not express: - cull: the photographer's trinary review flag (pick | neutral | reject), deliberately separate from the numeric star rating — a reject can carry three stars, and tools that conflate the two force lossy workflows. The culling workflow (flag -> filter -> batch-act) is documented in organization.md; the review UX is indexed as a slice, but the schema and semantics freeze now so sidecars written today survive it unchanged. - hidden: excluded from default views (timeline/search/system views), visible only in a re-auth-gated Hidden view. View-layer only — never deletion or access control. Companion files (RAW+JPEG's JPEG, a Live Photo's video) stay handled by stack roles; hidden covers what stacking cannot express. Both are LWW registers with wire-absent defaults, following the exact key_mode/stack_membership discipline: existing sidecars encode byte-identically (regression-tested), concurrent edits converge under the grouping-convergence requirement, and round-trip + signing tests cover the set case.
… contracts Three sync-domain gaps closed, one commit because they interlock: - Staged uploads (download-sync.md): upload-direction tiering for low-data situations — traveling on a metered plan, the index of what exists escapes first (manifest+metadata w/ LQIP), then previews, then originals on Wi-Fi. If the phone drowns, the user knows exactly what was lost and holds a preview of it. The policy is deliberately CLIENT-SIDE SESSION ORDERING ONLY: same sessions, same bundle mechanics, zero server mode branches — the enumerated mode exists in the structure while both policies share one code path. The one contract change: visibility flips on manifest+metadata (was original+metadata), with original_held carried per sync entry and the derived awaiting-original state (transient error.blob.pending_upload, never 410; GC carve-out; verify-before-destroy untouched — nothing releases a local original until T2 is durable; staged x streaming rejected by the planner). 'Backup' is deliberately not the name — that term stays reserved for the export artifact. - networking.md (new owner doc): the connection-class taxonomy the docs kept naming but never defined (unmetered/metered/constrained/adverse/ offline, with behavioral promotion to adverse — no OS API reports the networks that need it most), three retry policy classes that existing per-doc ladders become instances of, and the adverse-network posture: assume mid-transfer resets and black-holing (explicitly the steady state for paths crossing China's GFW toward servers outside the mainland, HK included), short independent resumable requests, the sync feed stays contractually unary short-poll, stall-detection over total timeouts, bounded windows under adverse, LAN peering as the degraded-mode alternative, and in-region self-hosting named as the structural remedy — resilience, not circumvention. - Background execution (download-sync.md): the OS contracts auto-sync actually runs under (BGAppRefreshTask/BGProcessingTask budgets, WorkManager constraints + Doze, desktop self-throttling) with one uniform rule: every window is preemptible, losing at most one chunk, because resume-from-server-truth makes 'must finish' state structurally absent. Contract seams: original_held on the sync proto (S-B4-tagged), UploadPolicy/UploadTier in capsule-core, ConnectionClass/RetryClass in capsule-sdk. S-C1/S-C2 contract-text amendments ride the SLICES commit.
The killer sharing feature — accounts on different servers contributing to one album — previously had no shipping story: true multi-writer cross- server albums are deferred to v2 (open question #1) and v1 federation is read-direction only. The aggregated album closes the gap without touching either constraint: N ordinary container albums (one per contributor, each single-writer on its own home server) rendered client-side as one logical album. Group membership travels as an encrypted album-group assertion inside each contributor's OWN album metadata — no shared mutable object, so no cross-server consensus — and inclusion is injection-proof by construction (a constituent renders only if you were invited to it AND it asserts the group id). Zero new server surface; servers never learn a group exists. Ordering is computed (capture time, asset-id tiebreak), consistent with the container-vs-view split and the grouping-convergence requirement. Honest limitation stated: no group-level kick — you can only unshare your own photos; shared governance is exactly the v2 problem, and this design collapses cleanly into a v2 multi-writer album if it ships.
A reinstalled device re-enrolls with a new device_id by design (hardware-
bound keys), so one physical phone accumulates indistinguishable session-
ledger entries. The device cohort hash groups them: ONE primary identifier
per platform (iOS Keychain-persisted non-synchronized seed — IDFV rejected,
it dies exactly when cohorts matter; Android SSAID; macOS IOPlatformUUID;
Windows MachineGuid; Linux hashed machine-id), folded through domain-
separated canonical CBOR — never naive concatenation — as
SHA-256(CBOR([label, user_id, platform_tag, primary_id])). user_id is in
the preimage so the same device under two accounts is unlinkable.
Honest scope, stated in-doc: factory-reset stability is impossible on
iOS/Android by OS design (attestation returns verdicts, not identifiers),
so the promise is reinstall-stable everywhere, reset-stable only on macOS
— a scoped guarantee rather than a false one. The value is structurally
advisory: client-asserted, unverifiable, and no authorization decision may
read it; sent only in the session-creation body, never in signed
artifacts or to peers. UX asserts rather than litigates ('a device you've
used before'), with a one-tap support bundle of the hash + device-id map
as the dispute path. Pure function + label land in capsule_core::cohort
(tested); server storage and client emission are slices S-C13/S-D11.
Two hardening moves on the recovery story, no re-architecture — the audit confirmed the master key is already the sole escrowed root: - The Single-Root Invariant is now normative: every recovery path terminates at the one account master key, and the recovery secret (or its Shamir quorum) is the only user-held secret class. The wraps-not-roots audit is written into the doc (escrow, backup artifact, Shamir, OGK/Drop-Key escrows, the sponsoree exception) so no future feature quietly introduces a second secret class and nobody re-derives the chain by hand. - A recovery secret written on a napkin a year ago is one the user only BELIEVES they have, so the app now periodically verifies it: local-only (Argon2id unwrap of the cached escrow + HKDF derived-tag compare — no server round-trip, hence no guessing oracle and no lock-out potential), 7d -> 90d -> 180d cadence with re-arm on device-add/rotation/restore, a stale-cache refresh-before-fail rule so rotations don't manufacture false failures, and a guided re-wrap after repeated failure — a fresh secret re-wrapping the SAME master key: O(1) escrow replacement, no data re-encryption. Clients are forbidden from persisting the passphrase to auto-pass the check (duty line in clients.md). capsule_core::backup gains the tested verify_recovery_secret seam + the capsule-recovery-verify/v1 label; cadence/UX is slice S-D12.
The (scope -> album) override map existed as a named hook with no grammar, which made 'how do folders on my phone map to remote albums' per-client improvisation. The scope is now a canonical identity — closed platform + source-kind enums plus a per-platform canonical locator, hashed through domain-separated canonical CBOR into a deterministic scope_id — so two devices looking at the same source agree without coordination. Locators are chosen for reinstall-stability per platform (Android uses relative paths, never BUCKET_ID, which is a display-name hash that differs across devices; removable media key on volume UUID so a re-mounted card stays the same scope). The mapping rows live in the E2E-encrypted library-settings document as LWW registers; resolution order is explicit and the planner records which rule fired; unmapped sources ask once instead of silently inventing destinations. This completes the concrete half of the S-Z1 library-settings schema.
Two leftovers from before the REST/gRPC decision that read as if GraphQL were still a plan rather than a retiring legacy: an empty capsule-api/graphql/ directory (only a stray .DS_Store — the real legacy crate is capsule-api-library, correctly frozen behind slice S-G1), and a capsule-web comment sketching the future server gateway as 'likely GraphQL for library queries'. The comment now states the committed design: rich queries have no server surface at all — the gateway is the browser's sync-fed client-side read path (S-D6) plus ranged REST blob fetches. (Local biome/rspack web tooling is broken on this machine independent of this change — verified identical failure on a clean tree; the edit is comment-only.)
A fresh full read of all 43 docs + SLICES.md after the revision series, fixing what it surfaced (the set was otherwise consistent: all 187 cross-doc anchors resolve, the six known overlap clusters are link-not-restate factored, every slice reference exists): - Terminology: 'backup' stays reserved for the encrypted export artifact — three drifted uses reworded (embeddings 'server-side backup', envelope 'key-free backup' fallback, staleness-nag phrasing). - Pre-append-only leftovers: 'assembled blob' comments in the blob-store layout and storage-verification reworded; the flat '24-hour TTL' in the deployment profiles now names itself as the cap and points at the floor/discard owner. - The wrapped-key passage in keys.md restated ~4 sentences of encryption.md's normative KDF discipline; trimmed to the hierarchy fact + authorization neutrality with pointers (the one substantial restatement the redundancy audit found). - The deprecated X-Capsule-Upload-Protocol alias is removed outright rather than kept 'accepted but deprecated': nothing shipped speaks it, it was absent from the header census, and an unregistered alias kept alive is exactly the hidden behavior the strict protocol forbids. - The server index-rebuild rule gains the staged-uploads carve-out (a missing original on an awaiting-original asset is expected state, not a dangling reference).
The status frontmatter (draft/stable) was schema-validated but invisible on the rendered site. A Starlight PageTitle override now shows it as a badge beside the page title (draft = caution, stable = success). Biome only parses .astro frontmatter, so template-only usage of imports and consts false-positives noUnusedImports/noUnusedVariables; scoped override added per Biome's Astro guidance.
…ified state Verified upstream (2026-07): OpenMLS ships the SHA-256 X-Wing suite (0x004D) today via its libcrux provider — a formally verified Rust backend (the old note wrongly called it C), pre-1.0 and not fully audited — while the RustCrypto effort (openmls#1940) and the IETF pq-ciphersuites draft have converged on SHA-512 variants, making 0x004D a likely dead-end codepoint. Re-pin the planned suite to MLS_256_XWING_CHACHA20POLY1305_SHA512_Ed25519 (codepoint pending) and rewrite the status note as a deliberate hold: non-final draft, no shipping backend for the target suite, unaudited pre-1.0 backend for the superseded one. Architecture unchanged — the AlbumAuthority seam makes any final re-pin an inventory edit, not a redesign. Nothing implements MLS yet, so the 0x0001 bundle edit is pre-deployment-safe.
… variant Fact-checked against vendor/TCG documentation: Secure Enclave is P-256-only; StrongBox's mandatory set is RSA-2048 + P-256 (Curve25519 is TEE-only, explicitly not expected of StrongBox); TPM 2.0's PC Client profile mandates RSA-2048 + P-256. P-256 is therefore the unique curve in the intersection, which is the technical justification for the P-256 hybrid-DSK variant. TPM 1.2 (no ECC, SHA-1-only signing) is explicitly excluded from hardware binding rather than accommodated via RSA-2048.
The 'key-free facts' statements in the filesystem index restated what Server Filesystem — 'PostgreSQL: What the Server Knows' owns normatively. Link the claim to its owner anchor instead so the exact field list has a single home and the summary cannot drift.
The implementation has exactly one session-store code path (Valkey via bb8_redis, for upload and auth sessions alike); the optional-Valkey / Postgres-only deployment-profile story existed only in the docs. Replace Deployment Profiles with Required Services (blob store + PostgreSQL + Valkey, all required), delete the profile table, the store-agnostic claim, and the profile-parity validation bullet, and update every inbound reference (upload protocol, versioning, principles, filesystem index). We do not offer multiple code paths for this.
client_version and generated_by_client were free-form strings (the one
real producer writes capsule-core/{crate-version}), which cannot scope a
defective-build incident to the assets it touched. Define the normative
producer grammar {client_id}/{semver}+{commit}[.dirty] in Provenance:
client_id names the client product (and thereby the platform), commit is
the client's own source hash. Nothing non-identifying rides along — OS
version and device model don't identify the code, and created_by_device
already identifies the device. Audit-only as before: the server bounds
the size but never validates the grammar.
Surface the field in Server Filesystem's envelope and Postgres lists so
a defective build's writes are enumerable, and add slice S-D15 for the
build-time commit embedding + per-client client_id injection.
…y scrub The Postgres index is authoritative, but nothing proactively verified that a frozen index + blob-store pair is mutually consistent — GC and rebuild are reactive paths, and an implementation bug on either side would surface only when it bit. Maintenance now owns a read-only, operator-invoked integrity scrub with an explicit checklist: row-to-blob presence (awaiting-original carve-out), blob-to-row orphans, deep re-hash, envelope-chain-to-index agreement, mirrored-fact agreement, and debris/quarantine inventory. Findings are classified, never adjudicated (fault is not assumed to one side) and the scrub never repairs, so it cannot become the deletion bug it exists to catch. Slice S-C14 tracks the implementation.
Nothing was server-signed about custody: finalization Completed and the
/storage/verify verdict were both explicitly unsigned, so 'the server
lost my photo' and 'the client never uploaded it' were symmetric,
unfalsifiable claims. Storage Verification now owns two server-signed
objects and their composition:
- CustodyReceipt — signed inside the finalization transaction (receipt
and uploaded flip commit together) over the server-recomputed
ciphertext hash, chain-positioned via envelope_hash, and hash-chained
into a per-server append-only receipt log (receipt_seq +
prior_receipt_hash). Fetched via GET /upload/{id}/receipt and
GET /assets/{asset_id}/receipts; persisted client-side as evidentiary
{uuid}.receipts.cbor, included in the backup artifact.
- StorageAttestation — the verify verdict signed on request with a
client nonce echoed, so a stale durable=true cannot answer a fresh
challenge. Priced like deep.
- Proof of Loss — receipt (acceptance) + signed non-stored attestation
or failed content-addressed retrieval (non-holding) + no authorized
purge (the delete-manifest rebuttal). The negative direction is a
burden shift: a custody claim without a receipt is unfalsifiable
noise, so signature unforgeability is the server's negative proof.
Split-view of the receipt log is the documented v1 residual, with
gossiped log heads as the designated v2 extension.
Signing uses a new long-lived hybrid Ed25519+ML-DSA-65 attestation key,
distinct from the classical operational key — the primitives carve-out
is clarified to be bounded by signature lifetime, and federation's
well-known document gains an append-only key history so old receipts
verify after rotation. The verify-before-destroy gate now also requires
a verified receipt, so a receipt-withholding server never becomes the
sole holder of an only-copy.
Threat model: invariants 33-34, scenario rows 43-45, client-side
receipt bullets. New slice S-C15; S-D4 gains the receipt half of the
release gate. error.upload.receipt_not_available added to locales with
platform files regenerated.
The h1's default top margin causes asymmetric spacing when using flex alignment. Move the margin to the container and switch to center alignment for consistent visual centering.
42c6031 to
f1a4ee9
Compare
design/dependencies.md is the single owner of non-crypto library pins per platform (jiff over chrono, thiserror/eyre, tracing, rustls, UUIDv7-default, and the standing server/web/FFI choices), registered in the principles owner table and summarized in AGENTS.md. The log->tracing migration the logging pin requires is indexed as slice S-F6.
Failure Modes stays the transport-security owner: 1.3 required wherever both endpoints support it, 1.2 admitted only for legacy ingress compatibility, nothing below ever negotiated. The rustls implementation pin for the hops Capsule code terminates itself lives in the Dependencies owner doc. Resolves the design half of issue #5.
All capsule-core datetime work now goes through jiff: RFC 3339 production/parsing (jiff::Timestamp), EXIF local wall-clock parsing (jiff::civil::DateTime, the offset-less analogue of NaiveDateTime), fixed UTC offsets (jiff::tz::Offset), and date bucketing via Zoned UTC dates. Serialized bytes are untouched: every signed format carries RFC 3339 strings or i64 epochs, never a datetime-library type. The soft-delete retention window now adds retain_days * 24 h of absolute time — identical semantics in UTC, stated explicitly at the call site. Pin: design/dependencies.md (datetime row).
The entity crate gains entity::time — the sanctioned chrono <-> jiff seam (lossless second+nanosecond conversion, now_entity() for column writes). model DTOs, service mutations, auth (JWT epoch seconds, password-reset expiry), and upload (Redis session RFC 3339 fields, EXIF capture-time extraction) now speak jiff; chrono remains only as the sea-orm column type in the entity crate and in the frozen capsule-api-library. Upload-session OpenAPI schemas pin the timestamp fields as value_type String (RFC 3339), matching what jiff's serde emits. Stored RFC 3339 values parse identically under jiff (Z vs +00:00 both accepted). Pin: design/dependencies.md (datetime row).
capsule-cli/entity keeps chrono as its sea-orm column type per the entity-boundary rule in design/dependencies.md; the CLI binary itself is now chrono-free.
capsule-sdk declared chrono but no source consumed it; the SDK's future datetime needs are jiff per design/dependencies.md.
anyhow is retired per the errors row in design/dependencies.md: thiserror in libraries, eyre/color-eyre in binaries.
Resolves the design half of issue #325: the sidecar gps value gains a closed datum tag (wgs84 | gcj02, wire-absent when wgs84 so existing sidecars and vectors stay byte-identical). User-inputted GCJ-02 is stored losslessly as entered — the inverse conversion has no exact solution, so converting on input would destroy ground truth. BD-09 is never stored: the exact closed-form fold to GCJ-02 runs at the input edge. Display conversion stays client-side behind the geocoordinates-rs gate. Implementation is indexed as slice S-A7.
Validation invariants 16-18 (non-upload writes) had no owning server
surface: no api-surfaces transport row and no slice. Authorization now
owns POST /albums/{album_id}/ops — one generic signed-op endpoint for
delete/metadata-update/derivative-add-replace/trash-restore, enforcing
the envelope gate + invariants 16-18 + 25 before any write, content-hash
replay idempotency, and provenance + sync_seq in one transaction. A new
action is a manifest-schema change, never a new endpoint. Indexed as
slice S-C16 (owns E2E case 7's server half with S-C11).
Resolves the design half of issue #296: an importer is a source adapter on the scanner's contract — structure-aware metadata folding (embedded EXIF wins except for exporter-authoritative constructs) at extraction, pure planner untouched, idempotent re-runs. Google Takeout is the v1 adapter (S-B6); iCloud and Immich are post-v1 (S-B7/S-B8); user-facing migration guides live outside design/ and gate on importer stabilization (S-Z2).
Resolves the design half of issue #365: a connected camera is a source adapter on the pipeline's scanner contract, spoken through ptpip-rs — a new in-house portable Rust PTP/IP library gate (libgphoto2 is ruled out for portability and licensing). Deterministic handle enumeration keeps the planner pure; hash-on-receipt is the end-to-end integrity story (PTP/IP has none of its own); dedupe-on-reimport falls out of the planner's content-hash rule; camera storage is read-only. Post-v1, indexed as slice S-B9; rides E2E case 2 once live.
…on pipeline Resolves the design halves of issues #366 and #385: i18n.md now owns the committed twelve-language set (zh-Hans, zh-Hant, ja, ko, fr, de, es, pt-BR, it, ru, hi, ar — RTL in scope via Arabic; zh-Hant falls back to en, never zh-Hans) while locales/config.json stays the mechanical truth of what ships, and the structure-aware xtask translate-readme contract (glossary-pinned LLM translation, committed outputs, key-less structural drift check mirroring i18n --check). New Lane I indexes the work: S-I1 string migration, S-I2 locale rollout, S-I3 README pipeline.
Absorbs the still-valid substance of issues #9, #12, and #17 into clients.md: swift-testing is the sole unit/smoke framework on Apple platforms (XCTest only inside XCUITest bundles; core-swift's XCTest smoke migrates as slice S-F7), Instruments + MetricKit are the performance stack with no on-device third-party APM, and Kotlin/web pins ride the same one-framework-per-platform principle.
The SLICES-vs-docs audit findings, closed out: video derivative generation gets its own contracted slice (S-B5 — thumbnails.md names the seam; S-B1 is stills-only), the three unclaimed bounded E2E cases get owners (case 1 -> S-D5, case 6 -> S-C12, case 9 -> S-D1, case 7 annotated to S-C16+S-C11), S-X3 gains its missing Contract line (mls-resilience.md + versioning.md), and every slice ID referenced anywhere now resolves to an index row and block.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A full system-architect review of
capsule-docs/src/content/docs/design/(39 docs, ~4.2k lines) against the actual codebase, with every finding fixed in place, the remaining build-out decomposed into bound slices, and compiling skeleton contracts at every stable seam. Everything except the individual implementation slices is done here;SLICES.md(new, at repo root) supersedes and replacesDEFERRED.mdas the executable map of what remains.Each commit is independently reviewable and passes
mise run check-ruston its own (validated viagit rebase -x).Decisions taken (the three forks that shaped everything)
/sync; module-map said gRPC) and the code carried three stacks. Resolution: REST/OpenAPI for every request/response surface (it generates the SDK), gRPC (capsule.sync.v1.SyncService) for the feed, ranged blob fetch stays REST, and GraphQL retires — rich queries run client-side overlibrary.sqlite, which is what a key-free server forces anyway. New owner doc:design/api-surfaces.md.todo!()bodies, oapi-annotated route handlers, proto contract,#[ignore]d contract tests naming acceptance criteria) so subsequent slices bind to a compiler-checked surface. MLS internals stay spec-only (upstream-blocked).photolibrary.metadata.v1proto, plaintext entities) keep compiling, carry greppableLEGACY-PLAINTEXT (frozen): SLICES.md S-Gxmarkers, and have explicit retirement slices with stated preconditions. Nothing user-facing changes in this PR.Highest-severity findings fixed (architect review)
module-map.mdmarked the implemented crypto core (primitives/keys/encryption/provenance/verify_asset/backup) "(planned)" while a dozen docs described deferred networked/MLS surfaces as implemented. The implemented spine —AlbumAuthority/ReferenceAuthority/SignedEpochLedger/lifecycle::Workspace— was documented nowhere. All trued up; a status convention (Implemented / Planned / Blocked) now lives inprinciples.md, and the MLS upstream blocker (openmls#1940) has one shared note.provenance_chain_hashwas defined as the hash of a record that commits to the hash of the sidecar containing it. Redefined as the prior chain head with the create/seal/sign ordering pinned.uuid/file_id/asset_id/blob_id(one of which salts key derivation) declared as one canonical UUIDv7, with UUID versions per identifier.read-derivative-onlyfederation scope both become enforceable off it.DerivativeManifestunverifiable (noamk_versionto select the write-tier key) → fields added (doc + wire-optional struct fields).content_type,gps.source,StackType) → value sets declared in their owner docs.nullvs absent-key signature-visible; the rules are now normative (legacy Options stay present-null; fields added within v1 are absent-key, so old signatures re-verify byte-identically — regression-tested in code).iat, nonstandardaud, revocation-list pruning, invariant-25-on-pulls, ≥2-vantage perspective check), quota Grace-expired letting users delete their way back under quota, the MLS lost-commit vs local-ahead discriminator, hardware-key claims corrected to what secure elements actually ship (P-256, PQ half software-sealed),ai.md's pgvector idioms replaced with the actual sqlite-vec model, per-surface 404-vs-410 policy, the.well-known/capsule/*registry, theX-Capsule-*header census, idempotency rows for five previously-unlisted write surfaces, the orphanederror.*i18n contract wired into every rejection, and a terminology/SSoT normalization pass.Skeleton contracts added (all compile + lint under the full gate)
capsule-core: manifestkey_mode/wrapped_file_key/metadata_blob_hash(byte-identity preserved — every prior signature still verifies, regression-tested),asset-keywrap/v1label,crypto::keys::p256,capsule_core::{drop, sharing},library::{space, storage_verify}(the purestreaming_recommendedandrelease_is_safepredicates are implemented and tested; I/O and behavior staytodo!()).capsule-api:POST /storage/verify, the drop store (/u/{opaque-id}/drop,/drops, atomic adopt), device enrollment (devices/enroll[/redeem]), theEnvelopeGateseam consuming the already-implementedcapsule_core::validationinvariants, and thecapsule.sync.v1proto + service stub (manifests as opaque canonical CBOR, stated in the proto).capsule-sdkdeliberately untouched: it cannot compile without the gitignored, DB-generatedopenapi.jsonand is excluded from every CI gate — its seams live in SLICES.md prose.SLICES.md
~45 slices in nine lanes with stable IDs, contract anchors, hard dependency edges, command-checkable done criteria, validation tiers, and a mermaid DAG. Every skeleton,
#[ignore]d test, and freeze marker names its slice (rg S-C3finds a slice's full footprint).DEFERRED.mdis deleted; its baseline moved into SLICES.md and its 11 inbound references were re-pointed.Verification
git rebase -x 'mise run check-rust' master— every commit passes fmt, clippy (-D warnings), i18n-check, workspace + ffi builds, binding generation, and the signer smoke individually.cargo nextest run --workspace --exclude capsule-sdk: 426/426 pass (incl. the testcontainer auth suite) +--features ffilane 296/296; 9 skips are the new named#[ignore]d contract tests.capsule-docsAstro build withstarlight-links-validator: all internal links valid (every edited doc + the newapi-surfaces.md).markdownlint-cli2: 0 errors (incl.SLICES.md).Review guide
Commits 1–6 are the docs review (status truthing → API decision → data-plane contracts → networked contracts → governance tables → terminology sweep); 7–11 are the skeletons (core types → core modules → seams → REST routes → sync proto); 12–13 are cleanup + SLICES.md. The three decisions above are the ones to push back on if you disagree — everything downstream follows from them.