Skip to content

Design-docs architect review, seam skeletons, and SLICES.md#384

Merged
justin13888 merged 58 commits into
masterfrom
design/architect-review
Jul 10, 2026
Merged

Design-docs architect review, seam skeletons, and SLICES.md#384
justin13888 merged 58 commits into
masterfrom
design/architect-review

Conversation

@justin13888

Copy link
Copy Markdown
Collaborator

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 replaces DEFERRED.md as the executable map of what remains.

Each commit is independently reviewable and passes mise run check-rust on its own (validated via git rebase -x).

Decisions taken (the three forks that shaped everything)

  1. API surface: REST/OpenAPI + gRPC for the sync feed & federation pull. The docs contradicted each other (download-sync said HTTP /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 over library.sqlite, which is what a key-free server forces anyway. New owner doc: design/api-surfaces.md.
  2. Compiling skeletons as contracts. Every stable seam got a compiling stub (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).
  3. Legacy plaintext server code: freeze + carve out. The plaintext-era surfaces (GraphQL library, photolibrary.metadata.v1 proto, plaintext entities) keep compiling, carry greppable LEGACY-PLAINTEXT (frozen): SLICES.md S-Gx markers, and have explicit retirement slices with stated preconditions. Nothing user-facing changes in this PR.

Highest-severity findings fixed (architect review)

  • Status inversion: module-map.md marked 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 in principles.md, and the MLS upstream blocker (openmls#1940) has one shared note.
  • Self-referential hash: the sidecar's provenance_chain_hash was 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.
  • Identifier soup: uuid/file_id/asset_id/blob_id (one of which salts key derivation) declared as one canonical UUIDv7, with UUID versions per identifier.
  • 48-bit recovery code wrapping the master key → ≥128-bit floor (sub-floor only inside an attested rate-limiting enclave).
  • Share-link passphrase endpoint contradicted its own "server never receives the passphrase" contract → GET of wrapped material, client-side unwrap; the web-upload write gate got a real possession proof (stored Argon2id verifier).
  • Manifest physical location contradicted across three docs → resolved as a server-visible, content-addressed envelope object beside fully opaque ciphertext blobs; the keyless index rebuild and read-derivative-only federation scope both become enforceable off it.
  • DerivativeManifest unverifiable (no amk_version to select the write-tier key) → fields added (doc + wire-optional struct fields).
  • Undeclared closed enums (content_type, gps.source, StackType) → value sets declared in their owner docs.
  • Wire-presence semantics: canonical CBOR makes present-null vs 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).
  • Plus ~50 more: federation token fixes (iat, nonstandard aud, 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, the X-Capsule-* header census, idempotency rows for five previously-unlisted write surfaces, the orphaned error.* i18n contract wired into every rejection, and a terminology/SSoT normalization pass.

Skeleton contracts added (all compile + lint under the full gate)

  • capsule-core: manifest key_mode/wrapped_file_key/metadata_blob_hash (byte-identity preserved — every prior signature still verifies, regression-tested), asset-keywrap/v1 label, crypto::keys::p256, capsule_core::{drop, sharing}, library::{space, storage_verify} (the pure streaming_recommended and release_is_safe predicates are implemented and tested; I/O and behavior stay todo!()).
  • capsule-api: POST /storage/verify, the drop store (/u/{opaque-id}/drop, /drops, atomic adopt), device enrollment (devices/enroll[/redeem]), the EnvelopeGate seam consuming the already-implemented capsule_core::validation invariants, and the capsule.sync.v1 proto + service stub (manifests as opaque canonical CBOR, stated in the proto).
  • capsule-sdk deliberately untouched: it cannot compile without the gitignored, DB-generated openapi.json and 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-C3 finds a slice's full footprint). DEFERRED.md is deleted; its baseline moved into SLICES.md and its 11 inbound references were re-pointed.

Verification

  • git rebase -x 'mise run check-rust' masterevery 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 ffi lane 296/296; 9 skips are the new named #[ignore]d contract tests.
  • capsule-docs Astro build with starlight-links-validator: all internal links valid (every edited doc + the new api-surfaces.md).
  • markdownlint-cli2: 0 errors (incl. SLICES.md).
  • No behavior changes anywhere: skeletons are additive, existing tests unchanged, legacy surfaces frozen not touched.

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.

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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploying capsule with  Cloudflare Pages  Cloudflare Pages

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

View logs

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.
@justin13888 justin13888 force-pushed the design/architect-review branch from 42c6031 to f1a4ee9 Compare July 5, 2026 19:57
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.
@justin13888 justin13888 merged commit 96ef5c4 into master Jul 10, 2026
14 of 16 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