[CHORE] Land the v1 head that #396 left behind - #418
Open
justin13888 wants to merge 77 commits into
Open
Conversation
The pinned swiftformat install on the dev host was a corrupt app-bundle extraction whose Info.plist no longer matched its signature, so the hardened runtime SIGKILLed it (exit 137) on every invocation. Reinstalling it fixed that — and revealed that `mise run format-swift` had therefore never actually run against this tree, neither locally (the hook silently died) nor in CI (`build-ios.yml` runs no linters and `ci.yml` has no Swift job at all). Running it surfaced a config contradiction the two gates could never both satisfy: swiftformat's `wrapMultilineStatementBraces` moves an opening brace onto its own line whenever the condition wraps, while swiftlint's `opening_brace` rule rejects exactly that. Any file with a wrapped `if let` failed one gate or the other no matter how it was written. Disable the swiftformat rule — swiftlint wins because its style matches the Swift API Design Guidelines and the rest of this codebase — and apply the formatter to the tree it had never touched. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
The app could not build without Rust. `CapsuleCatalog` compiled the generated uniffi glue and linked `.ffi/CapsuleCoreFFI.xcframework`, so `tuist generate && xcodebuild` required a five-target Rust cross-compile first — the dominant cost of any local verification, and a hard prerequisite for work that never touches the core at all. Split the module along the seam that was already there: - `CapsuleCatalog` keeps the contract, the Swift-native models, a native `CatalogError` (a parity mirror of the uniffi enum), and `InMemoryAssetCatalog` — the faithful reference implementation promoted out of `CapsuleTestSupport`, because the mock-lane app and SwiftUI previews need it too, not just tests. No Rust. - `CapsuleCatalogFFI` takes `FFIAssetCatalog`, the record conversions, and the new error mapping. It is gated behind `TUIST_FFI=1` and is absent from the default graph; nothing above it can name a generated type. Two hard-wired dependencies became injected seams so `ManagedStore` compiles either way: `CatalogOpening` (how the catalog is opened) and `SidecarCoding` (the sidecar wire format — canonical CBOR through the core, JSON otherwise). `FFIAssetCatalog` now maps every uniffi error onto the native `CatalogError` at its boundary rather than letting generated types escape. `tuist generate && xcodebuild test` now runs from a clean checkout with no Rust toolchain: 93 tests across 28 suites pass on the iOS simulator. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
The app was iPhone/iPad only, on an iOS 18 floor, with UIKit reached for directly from twenty files across nine modules. This makes macOS a native destination — not Catalyst — and raises the floor to iOS/iPadOS 26 and macOS 26. The floor is the enabling decision: at 26 the Liquid Glass APIs, `.tabBarMinimizeBehavior`, and `.navigationTransition(.zoom)` are available unconditionally, so `CapsuleGlass` sheds every `#available` fence and its pre-26 material fallbacks, and there is one visual language to design and audit rather than two. Platform differences are confined, named, and enforced: - `CapsuleFoundation/Sources/Platform/` holds the type shim — `PlatformImage`, `PlatformColor`, `Image(platformImage:)`, plus `PlatformEnvironment` and `PlatformLifecycle`, which expose *capabilities* (`hasMenuBar`, `libraryIsSandboxPrivate`, a memory-warning notification that is `nil` where the platform has none) rather than an OS name to switch on. - `CapsuleUI/Sources/PlatformCollection/` is the collection-view island every grid is built on: one SwiftUI-facing wrapper over `UICollectionView` and `NSCollectionView`, sharing the compositional-layout maths and the diffable snapshot logic, with cells hosting ordinary SwiftUI. The three `UICollectionViewCell` subclasses became SwiftUI views written once, and `ZoomableImageView` became pure SwiftUI gestures. - A `no_platform_ui_import` SwiftLint rule fails any `import UIKit`/`AppKit` outside a `Platform/` directory. A view body that needs a platform difference gets a named modifier, not an inline `#if`. Some differences are real and are handled rather than hidden: the Mac viewer pages with arrow keys and chevrons instead of a swipe pager, import uses an open panel (which yields real filenames), the search field sits in the window toolbar, and `UIActivityViewController` is replaced by `ShareLink` on both platforms — which also made sharing decode lazily, so selecting 200 photos no longer loads 200 images before the sheet opens. Also here: the app target is now one shared `App/` tree; the Mac gets a real menu bar and a `Settings` scene behind ⌘,; and an XCUITest bundle runs Apple's `performAccessibilityAudit` across six mock scenarios — the automatable half of "Apple design compliant". One test was reaching the real Photos library from a unit-test bundle, which is a TCC violation that hard-fails the process on macOS. It now uses a mock, which is what it should have done anyway — the claim under test never needed a system photo library. Verified: build and unit tests pass on macOS, iOS Simulator (iPhone 17 Pro), and iPadOS Simulator (iPad Pro 13-inch M5). BREAKING CHANGE: the Apple client's deployment floor is now iOS/iPadOS 26 and macOS 26, raised from iOS 18. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
macOS needs strings iOS never did: the viewer has no swipe pager there, so it grows two chevron buttons that need accessibility labels, and the menu bar needs names for the commands it exposes. The diagnostics share sheet also needed a title distinct from the button that opens it — the button ends in an ellipsis, which reads wrong as a heading. Six keys, English only. Translations may lag: every locale falls back to the source catalog per `locales/config.json`, so a missing key renders in English rather than as a raw key. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Swift was the only toolchain with no `check-<toolchain>` task and no CI job. `ci.yml` had no Swift filter and no Swift job at all, so its aggregated `required` check — the one branch protection gates on — could pass with the Apple client broken. `build-ios.yml` built one iPhone simulator and ran no linters, and Swift format/lint were enforced only by local hooks, which is to say not at all for anyone who skips them or is not on macOS. - `mise run check-swift` joins the per-toolchain convention: format-check + lint-check + `test-swift`, which runs the unit suites on macOS, an iPhone simulator, and an iPad simulator. - A `swift` job in `ci.yml` runs it on `macos-26`, gated by a new path filter, and is now in `required`'s needs. An iOS or macOS break can no longer merge. - `build-swift` and `setup-swift` default to the mock lane, so neither needs a Rust toolchain; `setup-swift-ffi` is the Rust-backed path. - `build-ios.yml` becomes the Rust-backed lane explicitly: it generates with `TUIST_FFI=1` and additionally builds for macOS, because the macOS staticlib slice is the one most likely to rot unnoticed. - `build-rust-ffi.sh` gained `aarch64-apple-darwin` and `x86_64-apple-darwin` slices — without them the xcframework has nothing for the Mac to link. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
`SLICES.md` recorded two blockers that no longer hold, and one of them was never quite right. Both are now resolved on the dev host, so lane P does not have to ride CI. swiftformat was recorded as "broken on dev host — binary SIGKILLed (invalid signature)". It was a corrupt app-bundle extraction whose `Info.plist` no longer matched its signature; reinstalling gives a working binary. That it had never run is why the swiftformat/swiftlint brace conflict went unnoticed for so long. Xcode was recorded as failing `-create-xcframework` with exit 70 after `CoreSimulator.framework` was removed, needing `sudo xcodebuild -runFirstLaunch`. `CoreSimulator.framework` is present and `-create-xcframework` exits 0. The actual residual failure was different: Xcode 26.6 ships the iOS 26.5 SDK but the host carried only 26.0/26.3 runtimes, so `xcodebuild` enumerated no iOS simulator destinations at all — `-showdestinations` listed only ineligible device entries. `xcodebuild -downloadPlatform iOS` fixed it, with no sudo. The README is rewritten around what the app now is: three platforms, two build lanes, the platform rule, and how to run it on the Mac. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
A photo grid that materializes its sections cannot scroll a real library. The docs position Capsule at "terabytes of visual assets", the catalog is paged (`offset`/`limit`) by construction, and a `LazyVGrid` over a `ForEach` needs the whole identity array up front — so its diffing cost grows with the library, and it cannot know total content height without loading everything. A scrollbar that lies and a scroll position that jumps are the visible symptoms. `TimelineLayout` computes the geometry from section *counts* alone. Its input is one `(dayKey, count)` row per day — a few thousand rows for a decade, which the local index answers with a `GROUP BY` rather than a scan — and from that it precomputes prefix sums of item counts and vertical offsets. Three things then become cheap that are otherwise impossible: - total content height is exact before a single asset loads; - "which items intersect the viewport?" is a binary search over sections, not a walk over items; - seeking to a date is O(log n), so a fast-scroll scrubber can jump to October 2019 in a 250 000-asset library instantly. It is a pure value type — no view, no store, no platform dependency — so the maths is directly testable, and the same engine can back every grid in the app by swapping the aggregate query. Tested at both scales: exact height, tiling order, section lookup, scrubber keys, and nearest-item resolution (which is what keeps your place across a zoom-level change); plus a suite over 3 650 sections and 250 000 items asserting that construction and a thousand viewport queries each stay in the low milliseconds, and that a viewport resolves to tens of items rather than the whole library. Also fixes two main-actor isolation warnings in `AnyCollectionLink` that would become errors under a stricter language mode, and wires up `CapsuleUI`'s unit test target, which the module factory had never been asked to create — so `CapsuleUI`'s tests, including the grid metrics and zoom maths, were inert. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Both failures predate this branch and neither is caused by the Apple client work, but both sit in `required`, so nothing can go green until they are fixed. **protoc.** `capsule-sdk`'s build script compiles the `capsule.sync.v1` proto with prost-build, which shells out to `protoc`. The `rust`, `rust-test`, android, and release jobs already install it via `arduino/setup-protoc`; the `rust-cross` matrix and `build-ios` never did, so both died in the SDK build script before reaching anything they were meant to test. This just applies the convention already used everywhere else in the workflow. **ktlint.** Three violations in `capsule-core-kotlin`'s `StrongBoxSigner.kt` failed `:core:ktlintMainSourceSetCheck`: a multiline expression that had to start on its own line, and a KDoc block written with `///`, which is not Kotlin doc syntax — ktlint reads the third slash as a comment body with no leading space. Rewritten as a `/** */` block, which is also what actually renders. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
The UI is being built against mocks because `capsule-core` and `capsule-sdk` are
mid-rebuild. These two modules are the seam that makes that honest rather than
throwaway: `CapsuleDomain` holds the value types, `CapsulePorts` the async
protocols, and feature modules import only these for data. Swapping the mock
adapters for the real uniffi ones becomes a change in the composition root with
no view, view-model, or navigation code touched.
Every type is shaped as a structural mirror of the Rust type it will be
generated from, with the doc comment naming its owner design doc: the full
`SidecarV1` field set with its CRDT registers, the provenance manifests and the
13 `verify_asset` reject reasons, the representation/degrade ladder, the closed
predicate grammar with its depth-8 / 64-term validator, the five quota states,
the eight quarantine surfaces from the threat model's own table, staged upload
tiers, device cohorts, share links and drops, federation and peering.
Six FFI parity rules are encoded and tested, not just documented. The load-
bearing one is `ClosedWireEnum`: closed enums cross the boundary as strings, so
each carries an `unknown(String)` case — reading an unrecognised value is legal
and renders the "created with a newer version" indicator, while writing one is a
structural rejection via `requireWritable()`. Swift forbids a raw-value enum
from carrying an associated value, so the conformance is hand-rolled.
Three defects the tests caught before any UI depended on them:
- **`Hashable` and `Equatable` disagreed on every wire enum.** The stdlib gives
`RawRepresentable` an `==` that compares raw values, but `hash(into:)` was
synthesised structurally — so `.unknown("pick")` equalled `.pick` with a
different hash. That silently breaks every `Set` and every dictionary keyed on
a wire enum, including the query-grammar lookup tables.
- **`Lww.merging` was not idempotent**, duplicating superseded entries on every
self-merge, which violates the grouping-convergence requirement.
- **The timeline query modelled trash and hidden as include-toggles**, so the
Trash view would have returned the whole library with trash mixed in. Replaced
with a closed `VisibilitySlice`, orthogonal to stack-hidden.
Two naming deviations from the design docs, both forced: `Predicate` collides
with `Foundation.Predicate` and `Operator` is a Swift keyword, so they are
`SmartAlbumPredicate` and `PredicateOperator`. Wire casing is mirrored per type
rather than globally snake_case, because the Rust `serde` attributes disagree
with each other and one wrong character is a failed signature.
280 tests pass on macOS, iOS, and iPadOS.
Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
`values-zh-rHANS` is not a valid Android resource directory. `aapt2` rejects it with "Invalid resource directory name" and fails the *entire* Android build, not just that locale — which is why `:android:packageDebugResources` was failing. The mapping treated every subtag after the language as a region and forced it into the legacy `-rREGION` qualifier. But `Hans` is a *script*, and Android has no legacy spelling for one: a script can only be named in the BCP-47 form, `values-b+zh+Hans`. Regions are two letters or three digits and scripts are four letters, so the two are separable by length alone. `pt-BR` keeps its correct `values-pt-rBR`; only the two Chinese locales move. This predates the branch — it arrived with the twelve-locale rollout and lay dormant because `build-android.yml` is path-filtered and nothing had touched `capsule-android/**` since. Regenerating the catalogs for the Apple client touched `values/strings.xml`, which triggered the workflow and surfaced it. Also adds `wasm32-unknown-unknown` to the Rust job's toolchain step. `check-rust` includes `build-check-wasm`, which type-checks the wasm32 sealing surface; without the target, cargo cannot find `core` and the gate dies on the first dependency it compiles. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
The app has around eighty screens across three shells — a tab bar on iPhone, a three-column split view on iPad and Mac, plus detached windows and a menu bar on Mac. Without a shared vocabulary each shell grows its own navigation, and deep links, menu commands, and notifications each need three implementations. `CapsuleNavigation` is that vocabulary: a 39-case `Route`, a `SidebarItem` catalog of 19 sections, and an `@Observable` `Router` holding one `NavigationStack` path *per section* — so switching sections and coming back preserves each section's own history, which is the behaviour a tab bar implies and the thing most often got wrong. It contains no SwiftUI views, which is precisely what lets three different shells bind it. Deep-link parsing is total: an unknown or malformed URL returns `nil` rather than guessing. Share and upload links (`/s/<id>#<secret>`, `/u/<id>#<key>`) carry a `LinkSecret` that redacts itself in `description` *and* `debugDescription`, is not `Codable`, and is returned from `open(_:)` rather than stored — so the fragment never reaches observable or persisted state, and nothing logs it. Five deliberate deviations from the brief, each forced by the domain: - `Route.linkRedemption` is separate from `shareDetail(ShareID)`. A `/s/` link's opaque id and a `ShareID` are different id spaces — the latter is the owner's revocation handle — and conflating them would have crossed them. - `Route.albums` was added: every other index/detail pair had an index, so the Albums section otherwise had no landing screen. - `search` carries the query text, because `capsule://search?q=` has nowhere else to put it. - Zoom is `zoom(TimelineFocus)` rather than an integer, so ⌘1–⌘4 navigate instead of broadcasting a level. - `select(_:)` takes only a `Route`; an overload on `SidebarItem` made `select(.imports)` ambiguous. The module originally carried a `DomainCodableBridge` with `@retroactive Codable` conformances for `MapRegion`, `SearchScope`, and `TimelineQuery`. That is deleted: the conformances now live on the types themselves in `CapsuleDomain`, so the compiler synthesises them. The stopgap's real cost was silent — a new `TimelineQuery` facet would have compiled but quietly failed to round-trip through state restoration, because the coding keys were hand-maintained. 59 catalog keys for the sidebar, settings sections, onboarding steps, and menu commands. 338 tests pass on macOS, iOS, and iPadOS. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
The app had a four-tab shell and a comment promising a split view "in phase 7". This is that phase: one `Router` now drives a `TabView` on iPhone and a `NavigationSplitView` on iPad and Mac, with each section owning its own navigation stack so switching away and back restores where you were. The shell is chosen from the live horizontal size class, not from the platform. An iPad in Slide Over is genuinely compact and a Mac window dragged narrow should behave the same way; `PlatformEnvironment` says only what a platform is *capable* of. On macOS the size class is `nil`, and a Mac always wants the split view, so the default is the correct answer rather than a fallback. `RouteDestination` is a single exhaustive switch from `Route` to view. Adding a route without a destination is then a compile error rather than a dead tap found by a user. Destinations that are not built yet resolve to `RouteScaffold`, which keeps the section's own name and symbol and says plainly that it has no interface yet — deliberately plain, because a convincing mock would hide the gap and the point is to make it visible. That is what lets the sidebar, deep links, menu commands, and the UI-test sweep run end to end while screens are filled in. The router's SwiftUI adapter (`binding(for:)`) lives in the app target rather than on `Router`, because `CapsuleNavigation` imports no SwiftUI — which is exactly what lets the router be driven and asserted on in tests with no view hierarchy. `List(selection:)` takes an optional binding on iOS (the non-optional overload is macOS-only), so the sidebar maps through one, ignoring the `nil` SwiftUI sends on deselection rather than forcing a "no section" state. Verified on all three destinations: builds, 338 tests pass, and the app launches on an iPhone 17 Pro simulator, an iPad Pro simulator, and as a Mac app whose menu bar carries Settings…, Import Photos…, Culling Review, and Capsule Help — rendered as real text, which is the first proof the string catalog resolves on macOS. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
This is not a stub. Until the Rust core is rebuilt it is the only thing behind every port, so its behaviour *is* the app's behaviour — a reviewer has to be able to walk every screen, including the failure states, and believe it. The load-bearing design decision is that the library is a **pure function of `(seed, index)`**: `MockLibrary` stores exactly one array — day boundaries, sized by *days* rather than assets, about 29 KB for a decade — and derives every field from a keyed splitmix64 hash. Library size is therefore a parameter, not a memory cost. `.hugeLibrary` is 250 000 assets and constructs in well under a second; `dayCounts` over it is O(days) and a page at offset 180 000 costs the same order as offset 0. That matters because the app's timeline is virtualized over a paged port and `TimelineLayout` is tested against 3 650 sections and 250 000 items. A mock that materialized an array would make the single most important property of this UI impossible to demonstrate. Everything derived is plausible rather than uniform: capture times shaped by weekday and season, GPS clustered into twelve trips plus a home cluster, stacks (RAW+JPEG, bursts, Live Photos), AI tags carrying model-slot provenance with some deliberately stale, and per-asset representation ladders and sync states. Thumbnails are drawn at request time with `CGContext` behind a 48 MB cache, so no image bytes enter the repository. `MockScenario` selects a whole coherent world, not a flag: `.offline` sets the connection class, stalls uploads, degrades remote-only representations down the ladder, and leaves every local read working — which is the offline-first contract. `.quarantine` populates several distinct surfaces from the threat model's table. `.newerVersionState` produces assets carrying `unknown(...)` closed-enum values so the "created with a newer version" indicator and the disabled-editing path are actually reachable. The raw values are the contract with the XCUITest bundle, which cannot import this module. Mutations genuinely mutate and emit on `changes()` — album creation bumps an MLS epoch, the default album refuses deletion, tag removal is add-id gated, caption edits supersede, and verify-before-destroy really refuses to release. Ports that cannot be honest are documented as thin rather than faked: import plans and streams progress but cannot grow a derived index, uploads move session state without moving bytes, and search scans to a documented ceiling because no inverted index exists. 70 tests pass; two real bugs surfaced while writing them — a stack's group cull state derived from pre-edit rather than resolved assets, and cross-module actor isolation on the ports handed out. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
XML forbids `--` anywhere inside a comment, and the generated banner read ``<!-- GENERATED by `cargo run -p xtask -- i18n` ... -->`` — the argument separator lands right in the middle of one. aapt2 enforces the rule and fails the *entire* resource merge, so every locale's `strings.xml` was rejected, not just one. The banner now names the task (`mise run i18n`) rather than the raw cargo invocation, which is the command a contributor should be running anyway. Like the qualifier bug this sits behind, it predates the branch and stayed dormant because `build-android.yml` is path-filtered and nothing had touched `capsule-android/**`. Fixing the invalid directory name was what let aapt2 get far enough to parse the files and find this. Guarded by a test that renders a document and asserts no comment body contains `--`, so the banner cannot regress into invalid XML again. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
…ency
Launching the app showed a Photos permission prompt and an empty library. That
is right for the Rust-backed lane and wrong for this one: Local Gallery is
explicit that a native Capsule app is "a complete local gallery first and a
synced client second" (FR1), that never-signed-in is a valid mode (FR2), and
that gallery reads perform zero network I/O — "not tolerate failure, but do not
attempt" (NFR1). A mocked app that cannot show a library until the user grants
access to a different library is not that.
The composition root now builds from `MockEnvironment` and names no PhotoKit
type. `MockBridge` adapts the port world to the older provider protocols the
existing screens still consume, so the timeline, albums, and search render mock
data without being rewritten.
The interesting part is `PagedLibrarySnapshot`. `AssetSnapshot` is synchronous
and random-access; `LibraryPort` is paged and async. Rather than materializing
250 000 assets to bridge that, an unloaded index returns an explicitly
provisional `Asset` and schedules its window, so the next read is real. One
field is exact rather than provisional — `captureDate`, resolved by a prefix-sum
binary search over the day histogram `loadTimeline()` already fetched. A wrong
date would section the row into the wrong month and make the grid jump when the
real row arrived.
`isFavorite` is derived from a reserved user tag rather than a rating threshold.
The domain keeps rating orthogonal on purpose, and "rating ≥ 1 means favourite"
would silently zero a five-star photo's rating when it was un-favourited.
Three formatter rules are now disabled, each because it produces code that does
not compile inside a swift-testing macro:
- `hoistTry` moves `try` to the front of the expression, turning
`#expect(!(try await port.sessions()).isEmpty)` into a form where `try` no
longer covers the throwing call.
- `preferKeyPath` rewrites `allSatisfy { $0.flag }` as `allSatisfy(\.flag)`;
`allSatisfy` is `rethrows`, and inside an `#expect` expansion the compiler
stops inferring that the closure does not throw.
- `wrapMultilineStatementBraces` was already off for conflicting with SwiftLint.
Since every test here is a macro invocation, these are not stylistic
preferences — they are correctness constraints. A long ternary the formatter
had also mangled is rewritten as a guard, which reads better anyway.
One flaky test fixed properly rather than retried: it spun on `Task.yield()`
waiting for a detached fetch that hops onto the port's actor, which reschedules
on the same executor and can spin without the fetch ever resuming. It now polls
a real clock with a deadline.
Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Two of the four screen modules built in this pass are now in the graph, giving
the app its onboarding and identity surfaces and the full eighteen-section
settings tree — roughly 75 source files and 10 000 lines of screens, each with
a view model that takes its ports by constructor and is testable without a view.
`FeatureAuth` covers the fork that matters most: "Use without an account" is a
first-class choice rather than a skip link, because Local Gallery FR2 makes
never-signed-in a supported mode rather than a degraded one. It also carries the
six-step first-device enrollment ceremony with its type-back gate, the recovery
cadence prompt (7 → 90 → 180 days, snooze capped at three, never blocking), and
the device ledger whose copy asserts rather than litigates.
`FeatureSettings` renders one tree two ways — a grouped list on iOS, a tabbed
Settings window on macOS — and tells the truth per platform about the at-rest
posture, which is a sandbox guarantee on iOS and full-disk encryption on a Mac.
Three lint decisions worth recording, because each is a rule being wrong rather
than the code being wrong:
- `inclusive_language` flagged `masterKey` and `requiresMasterKeyProof`. This is
not the master/slave usage the rule exists to catch: the master key is the root
of the key chain (`capsule-core::crypto::keys::MasterKey`) and master-key proof
is the specific authentication the docs require for `revoke_all_sessions`.
Renaming it in Swift alone would put the client out of step with the design
docs, the Rust core, and the wire contract. The term is allowed, and only it.
- `cyclomatic_complexity` flagged an eighteen-branch switch mapping settings
sections to symbols. That switch is exhaustive over a closed enum on purpose —
adding a section without a symbol should be a compile error. A lookup table
would trade that guarantee for a better score.
- Thirteen `SettingsScreen(...) { ... }` call sites became explicit
`content:` arguments, which the rule is right about.
`FeatureTransfer` and `FeatureSharing` are committed but **deliberately not in
the graph**. Both typecheck clean for iOS and macOS, and both crash the Swift
6.3 compiler in IRGen — `report_at_maximum_capacity` inside
`SyncCallEmission::setArgs`, reached from `IRGenSILFunction::emitSILFunction`
while compiling `ModerationView.swift` and `UploadDetailView.swift`. It is not a
type error and it survives stripping their `#Preview` blocks, so it needs
isolating rather than patching around. They are excluded from lint and format
too: findings on code that cannot be compiled are findings nobody can verify.
Also fixes a macOS-only name collision: `RGBColor` is ambiguous there because
ApplicationServices still exposes QuickDraw's C `RGBColor`, so the domain type
is now module-qualified at its use sites.
427 tests pass on macOS, iOS, and iPadOS.
Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Nineteen slices covering the whole anticipated Apple surface, plus the reason it is a lane of its own: lane P sits entirely behind `S-P1`, so waiting on the SDK would leave the client unbuilt at the moment the SDK lands — the worst possible sequencing for the piece with the most design surface and the slowest review loop. Lane U removes the dependency instead. `S-U19` is the only slice in it that depends on anything outside it. Also refreshes `capsule-swift/README.md`, whose feature list still described the prototype: documents the timeline engine's aggregate-plus-prefix-sum design and why `LazyVGrid` cannot answer it, the mock-scenario launch argument that is the only way to reach roughly thirty of the screens, and what is honestly not wired. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
`makeUniformGrid` handed a single `fractionalWidth(1)` item to `repeatingSubitem:count:`, which is the documented pattern for an evenly divided row and does not divide it. Every grid in the app rendered one full-width tile per row — a five-column library came out as a stack of 400×80 bands, on iPhone, iPad and Mac alike. It compiled, ran, scrolled, prefetched, and was wrong. The item now carries the `1 / columns` fraction itself and the group is given an explicit array of that many items, so the division does not depend on either API's semantics. That also retires the UIKit/AppKit fork in `horizontalGroup`, which existed only because `repeatingSubitem:` was never added to AppKit — `subitems:` is spelled the same on both. `UniformGridLayoutTests` measures resolved frames from a real `UICollectionViewCompositionalLayout` rather than asserting the code constructs what it meant to: column count per row, square tiles, and a row that spans the full width. Nothing short of resolving the layout would have caught this. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Day, month, and year bucketing all grouped *consecutive runs* of the same
period, on the documented assumption that assets arrive newest-first. When
that assumption did not hold, a period appearing in two non-adjacent runs
produced two sections sharing an id — and a diffable data source treats
duplicate section identifiers as a programmer error and raises, so the
process died before drawing anything.
That is not hypothetical: the 250 000-asset scenario terminated on launch
with `Duplicate identifiers: {("2026-07-12", "2026-07-11")}`. The scenario
that exists to prove the timeline scales could not open.
All three now coalesce by key, keeping each period's first appearance for
ordering and its first asset as the representative, so a correctly sorted
input is unaffected and an incorrectly sorted one renders slightly oddly
instead of terminating. One dictionary makes the failure unrepresentable,
which is worth more than the assumption was.
Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
`Project.swift` is one file and these changes are inseparable in it, so they land together rather than as commits that generate a broken project. **Stop the mock lane asking for Photos access.** A fully mocked build greeted the user with a permission prompt over the home screen, before it had drawn a pixel. There is no call behind it: `AppEnvironment` constructs no PhotoKit type and nothing in the tree constructs `PhotoKitProvider` at all. iOS presents the limited-library alert automatically at launch for any app declaring `NSPhotoLibraryUsageDescription` while holding a `.limited` grant. Declaring it here was wrong on its own terms — nothing in this lane can use it, and *Local Gallery* FR1/FR2/NFR1 say the gallery works with no system access. The key now ships only under `TUIST_FFI=1`, which makes the invariant self-enforcing: a mock-lane path that reaches PhotoKit is terminated by iOS naming the call site, rather than quietly prompting. `PHPicker` and `PhotosPicker` are out-of-process and need no entry either way. **Un-park `FeatureTransfer` and `FeatureSharing`.** The recorded diagnosis was wrong on its central claim — the modules did not typecheck clean; five files were missing a closing paren and never parsed, which masked the real IRGen crash. That crash is fixed at its two call sites and both modules are back in the graph, lint, and format. **Add the remaining feature modules** — `FeatureAuth`, `FeatureImport`, `FeatureSettings`, `FeatureSharing`, `FeatureTransfer`, and `FeatureAlbums` (which had been arriving transitively through `FeatureCollections`; the app imports it directly, so it says so). **`CapsuleUI` now depends on `CapsuleDomain`**, so the design system can render domain states — a seal, a cull flag, a sync tier. Deliberately not on `CapsulePorts`: it must be unable to *fetch* one, which is why `AssetWindowStore` is generic over a fetch closure. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Minimised to nine lines:
let setter: @mainactor @sendable (Bool) -> Void
Toggle("x", isOn: Binding(get: { true }, set: setter))
Forwarding a *stored* `@MainActor @Sendable` closure property straight
into `Binding.init(set:)` makes SILGen emit the reabstraction thunk
`@$sSbScA_pSgIeAghyg_SbIeAghn_TR`, and lowering it asks the IRGen
`Explosion` for capacity 4294967297 — a `0 − 1` underflow, which is the
`report_at_maximum_capacity` abort inside `SyncCallEmission::setArgs`.
Wrapping the call (`set: { setter($0) }`) emits an ordinary closure and no
thunk. Two sites, identical rendered UI, a one-line reason at each.
It had nothing to do with `#Preview`, view-body size, or modifier chains,
which is what the parked note guessed. That note also claimed both modules
typechecked clean; they did not — five files were missing a closing paren
and never parsed, and that is what hid the real crash for as long as it
did.
Also here, surfaced only once the modules could compile: a
`LocalizedStringKey` built from a `String?` in the storage-reclamation
consumer row, and a `void_function_in_ternary` misfire rewritten as an
`if`/`else` expression rather than suppressed.
Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
`TimelineLayout` already answered *where* item 148 302 sits from a `(dayKey, count)` aggregate. Nothing answered *what* it is. `AssetWindowStore` is that half: a sliding LRU window of fixed pages over a fetch closure, which cancels fetches that scroll out of margin rather than awaiting them, discards results by generation when the library moves underneath, and never evicts a page the viewport still needs. It is generic over a closure rather than over a port on purpose — `CapsuleUI` has no business knowing what a `LibraryPort` is, and a closure makes the paging testable against a counter instead of a library. Two things `@Observable` made non-obvious and both are commented at the site: every stored property is tracked by default, so the LRU bookkeeping that `element(at:)` does as a side effect had to be `@ObservationIgnored` or a view body would mutate tracked state and invalidate itself forever; and a `@MainActor` type's `deinit` is not actor-isolated, so cancellation on teardown is an explicit call instead. `TimelineAttributeSolver` and the collection-view layouts bind the engine to the grid, answering `layoutAttributesForElements(in:)` from one binary search plus one frame lookup per *visible* item — no cached attribute dictionary, no per-item pass. That claim is tested by counting queries through a wrapping geometry, not by reading the output. Also fixes a real bug in `rowAlignedItemOffset`, found by building on it: it clamped to `rows × columns`, which differs from the item count when the last row is ragged, so a viewport edge landing in an inter-section gap skipped the first items of the next day. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Five semantic roles rather than a palette — settled, in-flight, waiting, degraded, attention — so contrast is proven once per role instead of once per site, and every Capsule state maps onto one of them. Two judgements are encoded rather than left to each caller. Over a photograph, every non-actionable role collapses to white-on-scrim and only `attention` keeps a hue: photo content is arbitrary, so a colour that carries meaning against a settings background carries none against a sunset. And `writtenByNewerVersion` maps to `attention` rather than `degraded` — the data is intact and preserved verbatim, but this build will not write it back, and a user who edits it elsewhere and loses the round trip was never told. The copy that goes with it says "created with a newer version", never "damaged". `SyncStateBadge` renders all eight sync states; `durable` renders nothing, because it is what almost every asset is almost always and a badge on every cell is not information. `AssetCellOverlay` fixes the four corners by meaning so the eye learns them once, and takes a stack's member count as a parameter — `StackMembership` carries only this asset's role and ordering, since the size of a stack is a fact about the stack. Culling flags deliberately keep their own colours instead of borrowing the sync roles: a rejected photo is a decision, not a fault, and drawing it in the alarm colour would make a review pass look like a failure report. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Sixteen views over the view models that already existed: the enrollment ceremony, the recovery passphrase reveal and type-back gate, the re-verification cadence, devices and sessions, cross-device add, restore, passkey and TOTP — with 137 tests. Every file is small and every `body` is a handful of elements; the IRGen crash made expression shape load-bearing, and the habit is worth keeping regardless. Five bugs, each found by a test author who declined to assert the buggy behaviour and flagged it instead: - **`SecretPasteboard` never wrote its concealed marker on macOS.** `setString(_:forType:)` writes nothing and returns `false` for a type the pasteboard has not been told to expect, and a custom type is never implied — so the entire macOS mitigation was silently absent while the recovery passphrase itself was still written, leaving it to clipboard managers. Now declared before it is set. - **`canReconstructFromShares` counted invalidated Shamir shares**, so one live share plus one dead one enabled reconstruction that could not possibly work, failing at the port mid-recovery. Dead shares stay selectable — they are shown so a holder learns they are dead — but they no longer count. - **The mock's recovery generator emitted six words**: 66 bits against a 128-bit floor, so every preview of the passphrase screen sat in its "this is a defect in this build" state — the one state that screen exists to prove is not happening. - **`SettingsConnectivity.isOffline()` used `!isUsable`**, which is also false for an unknown connection class from a newer writer. That made `phase(for:)` return `.offline` and discard the real error code, telling a user on a working connection to check their network. - **`commitRestore` returned `true` for a restore that threw**, because the error was swallowed into `phase`. On that screen it means dismissing the flow with the user believing their library is back. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
**Import** — source picker, scan, plan-and-confirm, execution, history, with ~60 tests. The plan screen is the one that carries the product's argument: three stat tiles over a free-space meter that goes amber on `streaming_recommended` and red on the hard case, and a destination row that states its album *and the resolution rule that fired*. The docs require the fired rule be recorded, so the UI explains itself rather than showing a bare destination the user cannot account for. Free-space verdicts are derived client-side rather than baked into the plan, because free space changes while the screen is open. New ports are one per screen need, not a grab-bag: `resolveScope` exists because a scope id is computed in `capsule-core` and never in Swift, so a user-picked folder has to round-trip; `scanStream` has no cancel call because a scan writes nothing and tearing down the consumer is a complete stop; `replan` returns a *plan*, so a re-run passes back through confirmation rather than silently repeating. **Settings** — 147 tests over the eighteen sections, and the mock world is deliberately not the injection point for most of them: it is coherent and healthy by construction, so it cannot produce the empty and failed states each screen has to render. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
`RouteDestination` resolved 9 of 38 routes and swallowed the other 29 in a single `default:`. It now resolves 27, dispatching to small per-area properties, and the remaining 11 are enumerated by name rather than caught by a fallback — so adding a `Route` case is still a compile error and not a dead tap found by a user. Routes that carry an id no screen accepts go through one generic resolver that owns the resolve-state, so `RouteDestination` owns none. Neither `QuarantinePort` nor `DropPort` has a read-by-id, so it walks the first page; an item that is gone renders "no longer here" rather than an error, because it isn't one. The settings index is built from `SettingsRootCatalog` — groups, order, symbols, completeness — instead of a second hand-maintained list that could drift from the first. Its rows push a `SettingsSection` rather than a `Route`, since pushing `Route.settings` would resolve back to the index. Two navigation bugs found and recorded rather than quietly patched: - **Nothing in the app pushes a `Route`.** Every detail route is wired and unreachable until a screen adds a `NavigationLink(value:)`; `quota`, `maintenance` and `onboarding` have no sidebar row, deep link, or menu command at all. - **The iPhone shell drops 15 of 19 sections.** `CompactShell` renders only `SidebarItem.tabs` and there is no surface for the overflow, despite a doc comment claiming they are reached "through the Collections section" — there is no such tab. Transfers, Imports, Shares, Drops, Quarantine, Devices, Peers, Federation, Storage, Memories, Duplicates, Trash, Hidden, People and Places are unreachable on a phone. Verification is shell-agnostic on purpose: a swift-testing suite asserts each route is a scaffold *iff* it is on the declared list, by reflecting the rendered tree so the `switch` stays the single source of truth, and a UI sweep walks sidebar rows on iPad and tab-bar buttons on iPhone. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
1,453 new keys, taking `locales/en.json` from 547 to 2,000 entries, every one with a `context` field, regenerated into the Rust bundle, web JSON, Android `strings.xml` and the iOS `.xcstrings`. That is far more than the guard could see. `i18n-guard` only understood stock SwiftUI call shapes, so keys passed as `titleKey:`/`labelKey:` parameters and keys built by interpolation were outside it entirely — several hundred strings that compiled, rendered as their own raw text, and failed nothing. Every Swift file was read directly rather than trusting the worklist. Copy follows the domain rather than the key names, which is why each call site was read first. Cohort text asserts instead of hedging. Quarantine is *held*, never damaged, and offers Inspect / Repair / Discard with an explicit note that there is no default. `durable` is the only state that offers release. `awaiting_original` and `newer_version` read as informational, not as faults. Degraded federation says the shortfall is reach, not loss. A drop's filename is labelled as given by the sender *before* the name is shown. And the below-floor entropy string is worded as a fault in this build, because that is the only thing that can produce it. Eight strings use printf placeholders rather than ICU, because their call sites are `String(format: String(localized:))`; their contexts say what each value holds. The twelve non-English catalogs are untouched — absent keys fall back to the source locale, and machine-translating 1,453 strings would defeat the human review gate. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Two holes in `i18n-guard`, both of which let a string fail silently at runtime rather than loudly at build time. **Swift's `ErrorCode` is now checked against the catalog.** The Rust codes are generated from `locales/en.json` and cannot drift; the web reads the catalog directly. Swift's is a hand-written enum whose raw values *are* catalog keys, so it is the one surface where the contract can rot — and it rots into the worst symptom, a lookup miss that renders a blank message on exactly the screens a user reaches when something has already gone wrong. The two directions are deliberately asymmetric. A code in Swift the catalog lacks is fatal: there is no message to show. A code in the catalog that no Swift source mentions is only reported, because the enum's `unknown(String)` case is load-bearing by design — a newer server may send a code this build predates, and the raw value *is* the key, so it still localizes. Making that fatal would force a Swift change for every server-side error addition, which is the coupling `unknown` exists to avoid. It is measured against every Swift source rather than the enum alone, so client-local strings reached through `unknown(_)` count as handled; a gate that reports noise is a gate people learn to skim. **`…Key:` parameters are now scanned.** The detector knew only stock SwiftUI call shapes, so `titleKey:`, `labelKey:`, `emptyDescriptionKey:` and a dozen more were invisible — a typo'd or never-added key there compiled and rendered as its own raw text. The leading character class excludes `.` as well as identifier characters, because an argument label is never preceded by a dot but an enum case in a switch always is: `case .masterKey: "key.fill"` returns an SF Symbol, not a catalog key, and was the first false positive the widened detector produced. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Corrections to claims made a few commits ago that ran ahead of the code: `S-U5` was marked done while the collection view still used the prototype grid, and `capsule-swift/README.md` said the engine "backs every grid", which it did not yet. Both now say what is true, and the binding is owed to `S-U7`. Also adds a status note to the clients design doc explaining why the Apple client is built against ports rather than the SDK, and that doing so relaxes no client duty — `verify_asset` quarantine states, forward-version refusal, and the unreadable-on-this-device surface are all reachable and tested in the mocked client. A client that cannot *show* a quarantine is not one that can be trusted to enforce it. `capsule-swift/build/` is now ignored: `xcodebuild -project -target`, which is how a single module is built without the workspace scheme, writes its products there rather than into DerivedData. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
Resolves three SLICES.md conflicts, all of them the two branches recording the same Apple-toolchain repair from different halves of it. Merged rather than picked: upstream found the root cause (a stale DVTDownloads.framework, and the documented fix for it removing CoreSimulator.framework), this branch found the fault behind it (Xcode 26.6 ships the iOS 26.5 SDK, the host had only 26.0/26.3 runtimes, so xcodebuild enumerated no simulator destinations at all). Both lanes build locally now, so both rows say so. Row counts recomputed from the table rather than taken from either side: 148 rows, 63 ACTIVE / 49 RETIRED / 36 MIXED, 49 done / 21 done* / 70 ready / 3 blocked / 4 post-v1 / 1 part-done. S-U19 flips blocked -> ready. Upstream's own 4b950a1 unblocked lane P when S-P1 landed but could not touch S-U19, which does not exist on that branch; leaving it blocked would have misreported the merge. Claude-Session: https://claude.ai/code/session_016vJa976u11Y7XvpVq3dcG6
`S-U5`'s note said the collection-view binding was owed to `S-U7` and the grid on screen was "still the prototype's". Both are now wrong in an instructive way, so the note says what was measured rather than being quietly deleted. The binding was assumed to be the prerequisite for a continuous headerless grid. It is not: one 250 000-item uniform section resolves in a compositional layout in 8.7 ms with viewport queries in tens of microseconds, while the 3 650 day sections it replaced cost 426 ms. The boundaries were the expense, not the tiles — so removing the headers was a ~50× win on the *existing* grid and the virtualized engine was not needed to get it. What the engine is still owed for is recorded too, because "unused" and "unnecessary" are different claims: it renders a library the app has not materialised, and `TimelineViewModel` still builds an `[Asset]`. Clearing that is entangled with how hiding is *stored* — the timeline filters `HiddenStore` client-side out of that array and no client-side filter survives virtualization — which is a data-layer change and does not belong in a UI slice. Writing that down is what stops the next person rediscovering it halfway through. S-U7 and S-U8 move to `done*` with their remainders named: culling review for one, provenance and superseded captions for the other. Claude-Session: https://claude.ai/code/session_012pkMhSnJe2199A9rNixnxL
The accessibility audit caught a regression the previous commit introduced, and the audit is only able to say so because the failure was classified against the pre-series tree rather than waved off as "the audit was already red". `viewer.adjust` measured **45 × 19.3 pt**. A bar glyph with `maxWidth: .infinity` is as tall as the glyph; the padding that makes it *look* like a comfortable target belongs to the glass capsule around it, not to the button, so the tappable region was 19 pt tall. That looks right in a screenshot and is wrong under a thumb — in a viewer where the neighbouring control deletes the photo. The minimum is stated once, on the shared label and on the two circular buttons, rather than at each call site. Apple's 44 pt, which is also what the audit checks. With this the viewer's audit returns to the failure the base tree already had — `dynamic-type` on the timestamp in `viewer.title` — which is pre-existing and not this series' to close. The huge-library audit's timeout is likewise identical at base. Both were verified by running the audit in a worktree at `13a799d`, not assumed. Claude-Session: https://claude.ai/code/session_012pkMhSnJe2199A9rNixnxL
Both branches localized the Apple client at the same time, from opposite ends. `feat/ios-app` renamed 1,635 keys from `ios.*` to `app.*` because a shared skeleton should not be named after whichever client was written first; `feat/v1-code-push` kept adding to `ios.*` while it landed `S-I4`, the SR1 trash gate, and an Apple String Catalog generator that compiles ICU instead of copying it. The two halves are complementary, and nothing here drops either one. **The catalogs merged by key, not by line.** Every incoming key was renamed through the same map the refactor used (`ios.menu.*` to `apple.menu.*`, everything else to `app.*`), then merged three-way against the base. Eleven of theirs described concepts this branch already carries under other names — `ios.media_type.*` is `app.media.*`, `ios.search.date_range.*` is `app.search.filter.date.*` — and are dropped rather than duplicated. Seven more were the only strings of screens this branch deleted (`CollectionsRootView`'s parallel category vocabulary), and their own commit's rule applies: no dead strings through thirteen translations. `ios.auth.reason.*` becomes `app.<screen>.auth.reason` to sit beside `app.hidden.auth.reason`, which was already there. **The SR1 gate needed a home in a split module.** `da2d9108` put `GatedView`, `LocalAuthGate` and `LocalAuthError` on the FFI-free `AssetCatalog` protocol, which `419bf1ea` had already made unable to name a generated type. They are now Swift-native mirrors in `CapsuleCatalog` with a `GateFFIBridge` mapping them at the boundary — the discipline `CatalogError` already established, extended to `CatalogError.viewLocked` and to `FFIAssetCatalog`'s four gate methods. `InMemoryAssetCatalog` gains the grants and the hidden-row exclusions that commit added to the old `MockCatalog`. **The mock lane had no core to hold the grant.** `ManagedProvider` takes it in Rust; `PortBackedTrashProvider` has only ports behind it, so it holds the grant itself over the `LocalAuthenticator` seam this branch built, with the same five-minute window. `TrashProvider` therefore answers `unlockTrash()` the same way in both lanes, and `RecentlyDeletedView` — which had no gate at all here — needs no idea which one it is talking to. **Three findings the merge produced rather than inherited.** The interpolated- literal detector from `S-I4` and the computed-property detector from `13a799d9` only exist together on this branch, and together they caught two hardcoded English strings this branch had shipped (`AlbumCoverCard`'s photo count, the Library's delete confirmation) — so `app.albums.photo_count` is restored rather than dropped. `FannedAssetStack` was asking for `app.places.preview.accessibility %lld`, because SwiftUI folds an interpolation into the key itself; VoiceOver was reading the key aloud. And that key's message was an ICU plural embedded in a sentence, which the new generator refuses — correctly, since a language that inflects "at this place" with the count cannot be served by a fixed suffix. **`Project.swift` keeps both plist positions.** `NSFaceIDUsageDescription` is declared unconditionally, as `da2d9108` had it: it prompts for nothing and a build that reaches `evaluatePolicy` without it is terminated. `NSPhotoLibraryUsageDescription` stays behind the FFI lane, as `6ac90504` had it: declaring it in the mocked lane put a Photos prompt over the home screen for access that lane cannot use. Verified rather than assumed: `mise run check-swift` green (format, lint, unit tests on macOS / iPhone 17 Pro / iPad Pro 13-inch, and the iPhone UI sweep), `mise run check-web` green, `mise run check-docs` green, `cargo nextest run --workspace` 1,702 passed, and clippy, `i18n-check`, `i18n-guard`, `architecture-check` and `translate-readme-check` all clean. Kotlin is unverified — Gradle does not run on this machine — but no `R.string.*` reference in the Android tree names a key the merge moved.
The only conflict is the SLICES.md count paragraph, which both sides rewrote: this branch added the 19 rows of lane U, and the base added the notification lane's two (`S-Z11`, `S-D29`). Resolved by recounting the merged table rather than picking a side — 201 rows, 83 ACTIVE / 80 RETIRED / 38 MIXED, and 94 done / 54 done* / 39 ready / 7 blocked / 4 post-v1 / 3 part. The prose keeps this branch's reading of the blocked set (only `S-N2` and `S-P4` wait on code; the other five wait on a decision) and the base's note that the table spells the three partial rows `part` and `part 1 done`.
`check-web` stopped being pure bun when S-E1 and S-D3 put `build-wasm`, `share-kat` and `drop-kat` under `test-web`/`build-web`. The `web` job's step list predates that and was never updated, so the gate has not passed once since: the last green run is master `cb230755`, where `test-web` had no `depends` at all. Three things were missing, and each alone is fatal: - No toolchain install. Rust is rustup-on-PATH here, resolved lazily from `rust-toolchain.toml`, so all three tasks raced that bootstrap into one `~/.rustup/downloads` and clobbered each other's `.partial` — "could not rename 'downloaded' file", on `cargo` one run and `clippy` the next. - No `wasm32-unknown-unknown`. It is not in `rust-toolchain.toml`, which pins the iOS targets; the `rust` job adds it explicitly for the same reason. - No `wasm-bindgen`. It is a `[tools]` pin, but this workflow sets `MISE_AUTO_INSTALL=false`, so it has to be named. It is installed after the rustup step, never before — cargo-built mise tools race the toolchain bootstrap, as build-ios.yml already records. Also orders the three cargo tasks behind one another rather than leaving them parallel siblings. Warming the toolchain in CI removes the download race, but they still contend on the single `target/` lock, which is the stated reason `check` and `check-rust` are sequential — and ordering fixes a cold local `mise run check-web` too, which a CI-only change would not.
The module has never built. `TUIST_FFI=1` puts uniffi's generated Swift in the *same* module as the hand-written bridges, so four generated names — `CatalogError`, `GatedView`, `LocalAuthError`, `LocalAuthGate` — shadow the `CapsuleCatalog` types they are bridges *to*. A same-module declaration outranks an imported one silently, with no ambiguity diagnostic. `GateFFIBridge` and `CatalogErrorFFIBridge` both diagnose this and define `Native*`/`Generated*` aliases telling the reader to use them rather than the bare names. Three places did not: - `FFIAssetCatalog`'s three gate methods took the *generated* `GatedView` and `LocalAuthGate`, so they witnessed none of the three gated-view requirements and the type failed to conform — the error naming only the protocol, never which requirement went missing. Module-qualified rather than aliased, because the aliases are internal and cannot appear in a public signature. - `FFIAssetCatalogTests` imports both modules, where the bare name is an ambiguity error rather than a silent bind. - `FFISidecarCoderTests` never imported `CapsuleCatalogFFI` at all, so `FFISidecarCoder` was out of scope — and adding the import makes its two `CatalogError` uses ambiguous, so both halves land together. Verified by building the `TUIST_FFI=1` graph, which now succeeds.
…each `receive` records a fetch failure and deliberately does not mark the page `exhausted`, on the stated grounds that "a failure is worth retrying when the page is next required". It is not. `setVisibleRange` returns early on an unchanged range, before the refill loop, and `startFetch` has only two other callers — so a viewport sitting still over a failed page held its placeholder until the user scrolled far enough to change the required page range and came back. The only recovery was `invalidate()`, which drops every resident page to repair one. Refilling from `setVisibleRange` regardless would be worse: it is called every frame, so a page that fails fast would re-issue a failing fetch every frame for as long as the user looked at it. So the retry is explicit — `retryFailedPages()`, for whatever surfaces `lastError` — and the comment now says what the code does. Tests cover both halves: that twenty identical `setVisibleRange` calls after a failure issue no further fetches, and that `retryFailedPages()` recovers without the viewport moving.
`ManagedProvider.purge` removed the catalog row and left the bytes on
disk — "the on-disk file cleanup is a follow-up", per its own comment. So
the operation a user reaches for when they want a photograph *gone*
reported success while the file was still there. For a product whose
posture is local custody, that is the wrong direction to fail in.
Adds `ManagedLibrary.removeAssetFiles(uuid:captureDate:)`, which sweeps
`{uuid}.*` from the capture-date partition. Addressed by stem because the
catalog records no file extension — the import derives one from the source
filename — and matching the stem takes the `.cbor` sidecar with it. A
missing file is not an error, so a second purge converges.
Bytes are removed *before* the row, and the ordering is the contract: if
the file cannot be deleted the call throws and the asset stays in Recently
Deleted, where it can be tried again. The other order's failure is a row
over vanished bytes — a broken thumbnail. This order's failure is a
vanished row over bytes still on disk, and the two are not comparable.
`AssetViewerView` is fully built and is presented as a full-screen cover from the timeline, album detail, search and places. The `.viewer` *route* resolved to `RouteScaffold`, so the one entry point that goes through the router — `capsule://asset/<uuid>`, which the deep-link parser accepts and turns into `.viewer(id, context: .library)` — parsed correctly and landed on a placeholder. Resolves the sequence through the existing `ResolvedDestination`, which already owns the resolving / resolved / no-longer-there states and their catalog keys, so this adds no new strings and no new loading machinery. Only `.timeline` sequences resolve, because only they can be answered from `loadTimeline()`. That is the honest boundary rather than a shortcut: the deep link is currently the sole producer of `.viewer` and always emits `.library`. An album, person, place or search sequence needs its own collection query, so those keep the scaffold rather than get a viewer that cannot page past the asset it opened on — and the contract list in `RouteDestinationTests` now says exactly that, with a census row for the non-timeline case so the claim is asserted rather than asserted-by-absence.
…o control Security & Privacy has a per-device "resolve place names" toggle that persists to `UserDefaults`. Nothing read it. `SystemPlaceNameResolver` — the only reader of `PlaceNamePreference.isEnabled` — was constructed exactly once in the whole tree, in a test: every viewer defaulted to `NoPlaceNameResolver`, and none of the four `AssetViewerView` call sites passed one. The preference was write-only, so the toggle did nothing in either position. It failed safe, which is why it went unnoticed: no geocoding happened. But a control that reports a setting it does not have is worse than an absent one, so it is now wired rather than removed. `AppEnvironment` composes the real resolver and hands it to every screen that presents the viewer, including the newly routed `.viewer` destination. The default at the view stays `NoPlaceNameResolver` — that is deliberate, so a viewer built without an explicit resolver still cannot make a network call by accident, and the composition root is the one place that opts in. Off-by-default behaviour is unchanged: the per-device consent check lives inside the resolver, not at its call sites.
Six rows read `ready` while their screens had partly landed, one read `done` while three of its eighteen sections were scaffolds, and five real gaps were owned by no slice in any lane. - `S-U9`–`S-U14` become `part`, each with an owed note naming what landed and what did not. Not `done*`: their deliverables are much larger than the screens that shipped — `S-U9` alone still owes the predicate builder, member avatars, role badges and epoch chips. - `S-U15` becomes `done*`. Federation, advanced and about are scaffolds, and the Advanced mock-scenario switcher its own slice note relies on as the way to reach the otherwise-unreachable screens does not exist (#392). - `S-U8` records that the `.viewer` route landed. - Five orphaned gaps get rows: `S-U20` memories, `S-U21` duplicate review, `S-U22` inbound link redemption, `S-U23` the two onboarding steps. `memories` returned zero hits in this file while `SidebarItem.memories` was a live sidebar row; `duplicates` returned only server-side blob dedupe, which is a different concern. Row counts recomputed from the table rather than edited by hand: 205 rows, 87 ACTIVE / 80 RETIRED / 38 MIXED, 93 done / 55 done* / 37 ready / 9 part / 7 blocked / 4 post-v1.
feat(swift)!: build the Apple client for iPhone, iPad, and Mac
`starlightLinksValidator()` proves every link inside the documentation site on each `build-docs`, and that is why the site has none broken. It cannot see a link that leaves the site, and three kinds do: `SLICES.md` and `AGENTS.md` reference design docs by repo-relative path with an anchor, the thirteen root READMEs reference the published site by absolute URL, and a leading-slash path reads as repo-relative to a human while resolving to nothing on GitHub. Nothing checked any of them. That matters now rather than later: the design-doc restructure this branch is opening moves and renames files, and 241 `SLICES.md` links plus 39 README URLs would have broken with no signal. The gate lands before the first move so it guards that work instead of auditing it afterwards. It found fifteen, which reduce to three defects. `CONTRIBUTING.md` pointed at the development docs with a leading slash. `SLICES.md` cited `design/drops.md`, which has never existed — the drop surface is owned by `web-upload.md`, as `authentication.md` and `api-surfaces.md` both already say. And `capsule-android/README.md` was missing while all thirteen READMEs linked it, so one absent file was thirteen findings. Writing that README meant reading the module, and it does not compile: `ListScreen`, `DetailScreen` and `CapsuleApp` reference `data.MuseumObject`, `di.initKoin`, `ListViewModel` and `DetailViewModel`, none of which exist anywhere in the repository. It is the Kotlin Multiplatform starter template with its data, DI and view-model layers removed, and no screen touches `capsule-core-kotlin`. The shared library below it does not build either — its two smoke tests call `FfiWorkspace.create` and `createWithHardwareSigner` without the `client: FfiClientBuild` argument both constructors have required since `capsule-core::ffi` gained client build identity. The README says so rather than describing the app the module is named after; repairing the Kotlin lane is code, and is not this change. **Where it runs.** Its own job, not `check-rust` and not `check-docs`. `check-rust`'s paths filter excludes `capsule-docs/**`, so a docs-only pull request would skip it there, and widening that filter makes every `docs(design):` commit pay for a full cargo build. `check-docs`'s expensive step is the Astro build, which this needs no part of. The scripts import only `node:` builtins, so the job installs nothing — the absent `bun install` step is deliberate and says so. The gate proves that a name resolves. It does not prove that the prose around it is true, and the entrypoint states that boundary so it is not oversold and then disbelieved the first time it misses something. **The slugger is GitHub's, and three details of it are load-bearing.** Whitespace runs are preserved: `## Damage Scenario → Invariant Map` is `damage-scenario--invariant-map`, and collapsing the run rejects five links `SLICES.md` gets right. Underscores survive, or the four headings carrying an identifier are silently renamed. And the character class is Unicode-aware, because `\w` is ASCII-only and drops `## 機能` to the empty string — 142 headings across the translated READMEs. Also wires `test-docs`. `capsule-docs` has declared a vitest script and carried a `rehype-notranslate` suite for some time, and no mise task or CI job has ever run it; the eight tests in it pass. The thirty-five new ones needed a runner, and shipping a gate whose own tests nothing executes would have repeated the defect this commit is about.
`principles.md` says every doc carries a `status` tracking human review, and the site schema declared the field `.optional()` — so eight pages carried none: all three of `development/`, all three of `guides/`, `features/smart-albums.md`, and the splash page. Nothing flagged them, because an optional field is satisfied by omission. Dropping `.optional()` makes `astro build` reject a page without one, which means `mise run build-docs` and the `docs` CI job already enforce it — no new check, no new wiring. Verified by removing the field from a page and watching the build fail with `status: Required`. The eight are marked `draft`, which is what they are: not one of them has been through the review the field records. That is also the honest starting state for the audit this branch is doing — `draft` is the claim that no human has signed off on the page as written, and for these eight it has always been true and merely unstated. `principles.md` gains the scope it now has: the field is every page's, not only a design doc's. A guide that tells someone how to self-host is exactly as capable of going stale as a doc that specifies a wire format, and the review queue should be able to see both.
`architecture-check` has forbidden Salvo, gRPC and Progenitor by name since the teardown, and `salvo`, `tonic`, `tonic-prost`, `prost` and `prost-types` have been sitting in `[workspace.dependencies]` the whole time. The check reads `cargo metadata`, which reports member packages and their dependencies; a workspace dependency no member inherits is not a member dependency, so it was never looked at. Cargo does not lock one either, which is why `Cargo.lock` did not give it away — and why this commit removes fourteen declarations without touching the lockfile. The second rule matters more than the first. A declaration nothing consumes is a claim nothing can falsify, and the tree had a load-bearing example: `testcontainers` and `testcontainers-modules` are cited by nine design docs as the basis of a smoke tier, no test in the workspace starts a container, and `local-development.md` says so in as many words. Two documents disagreeing about whether a thing exists is a documentation problem; a manifest that supports neither of them is a tooling one. So an unused workspace dependency is now a violation unless `PLANNED_WORKSPACE_DEPENDENCIES` carries it with a reason. Five entries qualify, all sanctioned by decisions already written down: `redis`, `bb8` and `bb8-redis` are the Valkey adapters `AGENTS.md` requires for the two typed state ports, and the two testcontainers crates are the smoke tier the design docs specify. Naming them keeps the pin *and* the fact that nothing uses it yet in the same place, instead of leaving the manifest to imply the first and nothing to record the second. Removed: `salvo`, `tonic`, `tonic-health`, `tonic-types`, `tonic-prost`, `tonic-prost-build`, `prost`, `prost-types` — retired transports, and `AGENTS.md` forbids reintroducing them — plus `data-encoding`, `derive_more`, `file-format`, `futures-util`, `num-rational` and `tower-http`, which no member has referenced since the Salvo tree left. `file-format` in particular is media detection, which Rawshift owns. Three tests guard the exemption list rather than the code: a name cannot be both retired and planned, every entry states a reason, and the set is asserted whole so growing it is a deliberate edit and not a drive-by.
The design docs carry roughly ninety passages that are not contract: "is gone, not deprecated", "the earlier gate is amended to 0.7.1", "Considered and rejected: ThumbHash on its merits", a whole section of `api-surfaces.md` documenting a GraphQL surface that was deleted. They are the project's memory, and they are worth keeping — but a reader implementing the upload protocol is reading a specification, and each one makes the specification longer without making it clearer. This adds the place they go, and nothing else: no design doc is edited here, so the format can be judged on its own before ninety passages are moved into it. The rule that decides is written down rather than left to taste. A passage is rationale if deleting it leaves a reader unable to derive the contract's shape from the contract alone; it is history if deleting it only leaves them unable to say what the project used to think. Rationale is the best thing about these docs and it stays inline. `adr/` is a plain directory, not a site section, for the same reason `SLICES.md` is: the site is the specification an implementer reads, and the project's memory belongs beside it rather than inside it. One field departs from the usual ADR template. **`Contract:`** links the doc that now states the decision normatively, which is what makes the split checkable — an ADR with no live contract is unfalsifiable prose, and nobody can tell whether it still describes the system. Writing it as a link rather than a bare path means `check-docs-truth` already resolves it, so an ADR pointing at a renamed or deleted doc fails the build. That reuses the gate landed two commits ago instead of adding a second one. Three records to start, all decisions already taken and already narrated across several docs each: the single Kynos REST surface, Spargen over Progenitor with the generated/hand-written line redrawn, and Chromahash 0.7.1 with ThumbHash retired. Each collapses narration from three or four docs into one record — one ADR per decision, not per passage, because per-passage records would reproduce the scattering this directory exists to end.
…es them
`capsule-server/openapi.json` is committed, regenerated by
`mise run openapi-kynos` and drift-gated inside `check-rust`. That makes
it the one machine-readable statement of what the server serves, and
therefore an oracle for every endpoint a design doc names. Nothing was
using it that way, and 64 of 83 citations did not resolve.
Almost all of it was one systematic drift. The Salvo surface was
unversioned and the Kynos one is not: 42 of the contract's 51 paths sit
under `/v1`, and the docs had never been moved. So `POST /upload`,
`GET /blob/{hash}`, `GET /sync`, `GET /quota`, `POST /storage/verify`
and the rest name paths that do not exist, in the documents an
implementer would copy them from.
The rest is parameter drift, which is why matching is **exact on the
path template** rather than normalizing `{id}` to a wildcard. Normalizing
would pass `POST /v1/albums/{id}/upgrade` against the real
`/v1/albums/{album_id}/upgrade`, and that class — `{opaque-id}` for
`{opaque_id}`, `{id}` for `{album_id}` and `{drop_id}` — is precisely
what defeats reading a doc and grepping for what it names.
Four were not mechanical:
- `api-surfaces.md` mapped the upload surface as `POST/HEAD/PATCH
/upload` on one path. `POST` opens a session at `/v1/upload`; `HEAD`
and `PATCH` act on `/v1/upload/{id}`. The surface map, of all places,
had the shape wrong.
- The same table gave guest drops as `/u/{opaque-id}/drop`, which is not
an API path at all. `web-upload.md` already says `/u/{opaque_id}` is
the *page* a guest opens — `capsule-web`'s route — and the endpoint is
`POST /d/{opaque_id}`, which is what the contract serves.
- `POST /drop` appears nowhere in the contract; the drop session is
created at `POST /d/{opaque_id}`. That one was a heading in
`validation.md`, so its anchor moved and the link in `web-upload.md`
moved with it.
- `/u/` is now excluded from the paths the check recognises, because it
is a page namespace rather than an API one. A doc naming it is right.
Two citations stay unresolved on purpose and are allowlisted with their
reasons: `POST /v1/auth/validate`, which `authentication.md` names to
record that it was deliberately not ported, and `/v1/library`, the
deleted GraphQL schema named by the section recording its deletion. The
allowlist is keyed on `(file, citation)` rather than a line number so
reflowing a paragraph does not churn it, and an entry that no longer
matches any citation is itself a finding — an exemption nobody notices
has stopped being true is how a checker quietly stops checking.
The `docs-truth` paths filter grows to cover `openapi.json`, because
regenerating the server's surface can invalidate a citation without the
citing doc changing, and neither the `docs` nor the `rust` filter would
have caught that.
A design doc earns its keep by letting a reader grep for what it names. Seventeen citations named modules that are not there, so seventeen times the doc was a story rather than a map. Six were simply wrong, and the tree says what they should be. Lifecycle writes and device surfaces live under the server's `routes/` modules, not under `upload::` and `auth::`; album lifecycle is `capsule-server::album` and there has never been an `organization` module; and the MLS wrapper is `crypto::authority::openmls_authority`, which `mls.md` and `module-map.md` already said while `mls-resilience.md` and `versioning.md` said `crypto::mls`. That last one is the terminology drift a single-owner rule is supposed to prevent, surviving in the two docs nobody cross-read. `api-surfaces.md` also mapped storage verification onto `capsule-server::blob` when the module is `verify`. The check cannot catch that — `blob` exists — which is worth stating plainly: this gate proves a name resolves, never that the claim around it is true. Four are designed and unbuilt, and those go in `planned-modules.txt` with the reason and the slice rather than being quietly deleted or quietly tolerated. It is a short list and it is the honest answer to "what has the design committed to that nobody has built?" at the module layer: `capsule-core::media`, `capsule-core::notify`, `capsule-core::import::camera` and `capsule-server::federation`. An entry whose module has since been built, or that no doc names any more, is itself a finding — the failure mode of every allowlist is outliving its reason. `AGENTS.md` called `capsule-core::media` "the retiring stack". It is the opposite: it is the module that will consume Rawshift once Rawshift stabilizes, and neither exists yet — Rawshift is a pinned submodule that is not a workspace dependency, and nothing in Capsule decodes media today. The line now says that, because "retiring" and "not yet written" point a reader in opposite directions. Resolution is strict on the way down and lenient at the leaf, which is what takes the false-positive rate to zero here: a non-final segment must be a real directory or file, but a final segment may be an item or a `pub use` re-export found in the module's subtree. `capsule-core::library::available_bytes` is a re-export, not a file, and a resolver that demands a file reports a correct citation as broken. `SLICES.md` is deliberately out of scope. It is a historical ledger: it records that `capsule_core::media` *was* retired and that `capsule-api` *used to* hold the upload module, and forcing it to name only live modules would corrupt the record it exists to keep.
`module-map.md` opened with a table headed **Status** whose Server row read `Quarantined — rebuild with Kynos`. `capsule-server` is 125 files and 56,990 lines with 576 tests and a committed 59-operation contract, and it is the largest crate in the workspace. The Rust SDK row said `Regenerate with Spargen`; the SDK is 17,226 lines and its client is generated at build time by the `build.rs` that row describes as future work. The E2E section declared every case with a server or SDK leg "suspended". None of that has been true for weeks. The fix is not a more accurate status table. A map that keeps score goes stale at the rate the code moves, and this repository took 221 commits in August. The table is gone, and with it the Quarantined/Active vocabulary: this doc now answers one question — which design doc owns a module's contract, and what tier validates it — and points at `SLICES.md` for whether a thing exists yet, so there is one place to look and one place to update. What replaces it is a crate roster that is complete for the first time. `capsule-server`, `capsule-sdk` and `capsule-wire` were absent altogether; `capsule-wire` is 528 lines that both the server and the SDK depend on and no design doc had ever named it. The web, Swift, Kotlin, Android and vision crates are listed too. The server-module table was a plan and is now a description. It listed seven modules, two of which (`shares`, `federation`) do not exist under those names, and omitted thirteen that do — `attestation`, `directory`, `discovery`, `enrollment`, `escrow`, `gc`, `limits`, `problem`, `routes`, `scrub`, `serve`, `store`, `verify`. Each row now names the doc that owns it, which is what makes the table checkable at all. Three more corrections where the tree disagreed with the list: `library::trash` is not a module (`trash_path` is a re-export in `paths`), the import module list was missing seven of its members, and `filesystem/server.md` routed storage verification to `capsule-server::blob` when the module is `verify`. Chromahash was still gated on "after its v1 release" in four places — `module-map.md`, `metadata.md`, `import/pipeline.md` and `development/architecture.md` — while `dependencies.md` and `thumbnails.md` correctly pinned **0.7.1** and `Cargo.lock` has held 0.7.1 for some time. That is the Single Source of Truth rule failing in the exact way it is meant to prevent: six docs stating one choice, two of them current. All six now say 0.7.1. And `capsule-core::media` is no longer described as "the retiring stack". It is the module that will consume Rawshift once Rawshift stabilizes — the opposite of retiring — and calling it both leaves a reader unable to tell whether to write it or delete it.
Small diff, and the one in this branch a security reviewer should actually read. Each of these cites a numbered validation invariant for a property that invariant does not state. **`provenance.md` cited invariant 17 for append-only enforcement.** Invariant 17 is the chain-advance check: `prior_provenance_hash` equals the last accepted manifest's content hash. It says nothing about overwriting or deleting a provenance entry. Append-only is real, but it is *derived* — 17 refuses a manifest that does not advance the chain, and 16 closes the action enum, which contains no overwrite or delete action for a provenance entry, so there is no surface to request one through. Asserting it against a rule that does not say it means a reviewer checking the claim finds the wrong text and either mistrusts the doc or, worse, concludes the property is unprotected. **`upload-protocol.md` called invariant 26 "the platform-wide indistinguishable-404 rule".** It is not platform-wide: `federation.md` scopes 26–32 to the drop path, and 26 is specifically upload-*link* liveness. The platform-wide rule already had an owner — `api-surfaces.md`'s rejection mapping — so the citation now points there, and names 26 as the drop-path instance rather than the source. **`validation.md` filed invariants 16–18 under "non-upload writes", which made a correct doc look wrong.** `upload-protocol.md` applies 17 and 18 to a `replace` arriving over the upload protocol, and had to spend a clause insisting it was doing what lifecycle writes do — because the registry said those checks were for non-upload writes. The registry was wrong: the three are grouped by *where they are enforced*, the index's critical section, and that section is entered by a lifecycle write and by a `replace` alike. The heading now says so. Enforcing them at the gate instead would let two concurrent replaces both pass and double-apply, reintroducing the stale revival 17 exists to catch, in the code enforcing it. **`federation.md` cited "all items 1–18" and then restated seven of them.** An ordinal range goes stale the moment a check is added, and the restatement is a second copy of a list that already has an owner. It now cites the three sections by name. The exclusion is stated as a reason rather than a range — the drop-path checks cannot arise on a pull because a pull carries a manifest and a drop carries none — which stays true however the list grows. The underlying problem is that the numbering is one flat namespace grouped by write phase, and the phase grouping is what produced all three mis-scopings. Replacing it with owner-scoped identifiers is a larger change and is not this commit; these four are wrong today and are fixed today.
The documentation homepage advertised a demo at `demo.capsule.app` — a
domain that appears nowhere else in this repository and does not
resolve — from both the hero and a call-to-action band. It promised
"AI-powered object recognition", "face recognition", "automatic
synchronization", "easy self-hosting setup" and "no data limits: store
unlimited photos". There is no published client binary, no deployable
server, no inference runner, and `quota.md` is the design doc that owns
storage limits. It also carried a `{/* TODO: Finish this */}` and the
Astro starter template's mascot.
The screenshot was the sharpest version of the problem: `AppScreenshot`
rendered `/capsule-screenshot.webp` inside a browser-chrome frame, and
that file is thirty-one bytes reading `TODO: REPLACE THIS PLACEHOLDER`.
The homepage was showing a broken image in a window frame to anyone who
loaded it.
`design/principles.md` already forbids exactly this — "a doc describing
a planned surface writes the contract in normative present tense, but
must not claim the code exists" — and the splash page escaped it by
being the one page nobody thought of as a doc. It is the first page
anyone sees, so it is the page where the claim costs most.
The rewrite says what is true and what is not: a caution block states
plainly that nothing is usable yet and points at the design docs and at
local development, and the feature cards describe properties the design
actually commits to — the key-free server, self-hosting, sidecar-based
recoverability, originals kept as originals — each of which a reader can
follow into a doc that specifies it. The "why" section names the costs
rather than only the benefits, and links the three docs that state them.
`features/smart-albums.md` was six ML album types in the present tense —
people, trips, pets, food, scenes, best-of-year — for models that do not
exist. It now describes what a smart album *is*, which is a predicate
rather than a membership list, defers the grammar to `organization.md`
and the models to `ai.md`, and says the ML-dependent kinds depend on
them. That also fixes a gap the audit turned up: this page and the
predicate grammar had no link between them in either direction.
`AppScreenshot.astro`, `TryNowSection.astro` and the placeholder image
are removed rather than left unused. The second hardcoded the demo URL,
and a dead component carrying a false claim is a claim waiting to be
re-imported.
docs: audit the design docs against the code, and gate what they name
PR #396 merged a stale local copy of feat/v1-code-push. The 75 commits that had already landed on its origin head — the Apple client (#388), the docs-truth gates and ADRs (#395), the CI fixes for the wasm target, and the i18n-guard computed-property detector — never reached master. This merges origin/feat/v1-code-push. The one conflict, in the root Cargo.toml, resolves to the v1-head side: it removes a strict superset of the unused workspace dependencies master removed, and nothing outside legacy-review references any of them. Refs #397
Deploying capsule with
|
| Latest commit: |
99dd4bc
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9180d74d.capsule-22k.pages.dev |
| Branch Preview URL: | https://chore-merge-v1-head-397.capsule-22k.pages.dev |
5 tasks
The v1 head removed `@formatjs/cli` and its two `intl:*` scripts from capsule-web/package.json without regenerating the lockfile, so `bun install --frozen-lockfile` in the Web CI job fails with "lockfile had changes, but lockfile is frozen" — on master (run 33572237092) and on this branch (run 33574327583) alike. Regenerated with `bun install`; no dependency versions move, the stale entry leaves. Refs #397
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.
Description
PR #396 merged a stale local copy of
feat/v1-code-push(9cc89ac2). The 75 commits that had already landed onorigin/feat/v1-code-push— the Apple client (#388), the docs-truth gates and ADRs (#395), thewasm32-unknown-unknownCI fix, and thei18n-guardcomputed-property detector that #394 describes — never reachedmaster, which is whymaster's CI is red and #390–#394 cite code and slicesmasterdoes not have.This is one merge commit of
origin/feat/v1-code-pushintomaster. No rebase, no history rewritten.The single conflict, in the root
Cargo.toml, resolves to the v1-head side: it removes a strict superset of the unused[workspace.dependencies]thatmasterremoved (data-encoding,derive_more,futures-util,file-format,num-rational,tower-http); no manifest outsidelegacy-review/references any of them, andCargo.lockis unchanged by the resolution.Related Issues
Closes #397
Decisions taken
Conflict resolution in
Cargo.toml.Taken: the v1-head side — six unused workspace dependencies removed;
Cargo.lockunchanged.Rejected: keeping master's side — it retains entries no member consumes, and the v1 head's
xtaskworkspace-dependency check (21f875de) is what removed them.Reverses: re-add the six lines under
[workspace.dependencies].Base for every follow-on pull request in this programme.
Taken: this branch's head, until it merges (user decision, taken before the run started).
Rejected: targeting
feat/v1-code-pushdirectly; building onmasteras-is (would conflict with the 707 files here).Reverses: retarget each follow-on pull request to
masterafter this one merges.Validation
Run on the merged tree at
f433d918, on the development host (16 cores; other worktrees compiling concurrently):mise run check-rust— passed (format, clippy, i18n-check, i18n-guard, openapi-check-kynos, architecture-check, license-check, translate-readme-check, build-rust, build-check-wasm, build-ffi, lint-check-ffi, gen-bindings, verify-examples).mise run test-rust— passed:cargo nextest run --workspace1703/1703;-p capsule-core --features ffi734/734;-p capsule-sdk --features ffi160/160; 0 skipped.mise run check-docs-truth— passed: cross-links 473 resolve, endpoint-census 84 resolve, module-paths 119 resolve.check-web,check-docs,check-kotlin,check-swift(unchanged toolchains relative to the v1 head; CI runs them).Post-open repair
bun install --frozen-lockfilefailed in the Web CI job (run 33574327583) and onmaster(run 33572237092):capsule-web/package.jsonin the merged tree carries@formatjs/cliand the twointl:*scripts — they come frommaster's side (the [FEAT] Land the v1 implementation: Kynos server, Spargen SDK, wasm and wire #396 merge resolution re-added them) whilebun.locknever carried matching entries. The v1 head does not touch those lines. So the drift is pre-existing onmaster, inherited by this merge, and the commit message of99dd4bc8states the cause inverted (it blames the v1 head); that message is published history and is not rewritten — this paragraph is the corrected record, found by the post-open review. Classified pre-existing; repaired in99dd4bc8by regenerating the lockfile withbun install(no version moves). Re-run 33576515009: Web, Rust (fmt + clippy + build), Rust (tests), Rust cross ×4, Docs, Docs truth, Kotlin, Markdown, Vision all pass; Swift (format + lint + unit tests on macOS/iOS/iPadOS + iPhone UI sweep) and Build & test Capsule.app pass; the only red check isBuild Capsule.apk + :core JVM smoke, red onmasterindependently (issue Android build is red: capsule-android references a DI layer that is not in the tree #389; its harness half is repaired in PR [CI] Close eight merge-gate holes: required loop, path filters, pre-push parity, protoc, release tests, Kotlin smoke #419) and outsiderequired.Contributor Checklist
Unresolved review notes
99dd4bc8commit message · the stated cause of the lockfile drift is inverted (blames the v1 head; the drift is master-side). Not repaired in code: correcting it needs a history rewrite of a pushed commit, which this run does not perform; the corrected cause is recorded above and in the run report.