[FEAT] Migrate unsigned sidecars through the signed write path and delete the second reader (S-D24, S-D23 owed) - #455
Open
justin13888 wants to merge 15 commits into
Conversation
…-unsigned-migration-412
A catalog stamped by a newer build was refused correctly by the migrator,
but `DatabaseDriver::open` flattened `MigrationError::CatalogTooNew` into a
`SqliteFailure` message, `open_library` re-wrapped that as `LibraryError::Db`,
and `Workspace::open` stringified it into `LifecycleError::Io`, so a client
could only print it. This is the owed half of S-D23.
`open_library` now goes through a crate-private `DatabaseDriver::open_typed`
and returns `LibraryError::CatalogTooNew { found, supported }`, with the
catalog untouched and the lock released; `Workspace::open` surfaces every
library-open failure as the new `LifecycleError::Library(LibraryError)`.
The public `DatabaseDriver::open`, consumed by capsule-core-ffi, keeps
flattening, so its signature and behaviour are unchanged.
Deploying capsule with
|
| Latest commit: |
7805ba5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e83498a8.capsule-22k.pages.dev |
| Branch Preview URL: | https://feat-sidecar-unsigned-migrat.capsule-22k.pages.dev |
A library written before the signed path holds, beside each original, a
flat unsigned CBOR sidecar with no provenance chain, no sealed metadata
blob, and no album key. `Workspace::open` anchors on chains, so such an
asset was silently invisible: it could not be verified, exported, or
uploaded, while a keyless `rebuild_index` still indexed it (S-D24).
`Workspace::migrate_unsigned_sidecars(&UnsignedMigrationOptions)` is the
explicit verb that brings such a library forward. Per legacy record, in
asset-id order, it refuses without writing (bad name, id collision, missing
or hash-mismatched original, a conflicting quarantine twin), copies the
legacy bytes verbatim to `.library/quarantine/{uuid}.cbor` with a sibling
`.reason.json`, then admits the asset as a signed create through the one
create commit every import takes — keeping the legacy id, the media
bucket (nothing moves; the original is signed where it lies), the legacy
import time, and folding the whole legacy map into the signed sidecar's
`_unknown` under `legacy-unsigned-sidecar`, where the signature covers it.
Rating, tags, GPS and capture time land in their signed registers with
the import precedence (EXIF over the legacy record over the legacy import
time, never now); `is_deleted` becomes a signed `delete`; `stack_hint`
groups of two or more get a deterministic v8 stack id over the user id
and the group key, written at create. The run ends with `rebuild_index`.
It is idempotent and resumable: an interrupted run's chainless or torn
sidecar is redone from its quarantine copy, but only while it is still the
migration's own create; an owed `delete` is applied on the next run unless
the asset has since been restored by hand.
Supporting pieces: a crate-private `sidecar::shape::probe` tells the two
shapes apart by their two discriminating keys alone (integer key 0 vs the
text key `version`) and builds no model of either; `Workspace::open` now
records every sidecar no chain anchors and exposes it through
`unmigrated_sidecars()`, warning once per file and naming the verb, so the
library still opens; and the body of `import_asset_with` is extracted into
`commit_signed_create(&CreateRequest)` with no change to the public
signature or to an ordinary import's behaviour — the migration is the one
caller that pins an id, a bucket, an import time, and a fold.
The unsigned reader in `rebuild_index` is left in place for the commit
that deletes it; `DatabaseDriver::open`'s doc no longer links the
crate-private `open_typed` from a public item.
`capsule-core-ffi` exposed `serialize_sidecar` / `deserialize_sidecar` over the unsigned pre-signed-path `AssetSidecar` — a writer of the shape S-G4 retired and S-D24 migrates away, and a second encoder of sidecar bytes beside the signed `SidecarV1` the core alone authors. The two functions, `AssetSidecarRecord`, `StackHintRecord`, and the module behind them are deleted, ahead of the core type's deletion so this crate never stops compiling. `CatalogError::Sidecar` stays; its contract is unchanged. On the Swift side, `FFISidecarCoder` (the façade over those exports), its test file, and the `CatalogFFIBridge` extensions converting to and from the deleted records go with it; `CatalogSidecar`'s doc no longer calls itself a mirror of a Rust record. The shipping composition root injects `JSONSidecarCoder`, so no production path changes. BREAKING CHANGE: the `capsule_core_ffi` uniffi namespace loses `serializeSidecar`, `deserializeSidecar`, `AssetSidecarRecord` and `StackHintRecord`.
With `Workspace::migrate_unsigned_sidecars` in place there is no reason
left to read the unsigned pre-signed-path shape: a rebuild holds no keys
and could never admit such an asset, and indexing it showed an asset the
workspace cannot verify, export, or upload. `rebuild_index` now reads one
shape, the signed `SidecarV1`; a file that fails that decode is probed,
and an unsigned one is counted and reported with a `warn` naming the
migration verb rather than indexed. Its projection, stack reconstruction,
four string helpers, and the compatibility tests go with it.
`sidecar::{AssetSidecar, StackHint, read_sidecar}` and the `cfg(test)`
`write_sidecar` are deleted along with `asset_sidecar.rs` and
`stack_hint.rs`; the one remaining decoder of the legacy shape is the
migration's private `LegacyRecord`. The migration's docs now say the
reader is gone, its refusal test asserts the closing rebuild indexed
nothing for a refused file, and the keyless-rebuild test over an
un-migrated library moves in beside it.
BREAKING CHANGE: `capsule_core::sidecar::{AssetSidecar, StackHint,
read_sidecar}` are removed; a keyless `rebuild_index` no longer indexes
unsigned sidecars.
…repair row The import pipeline's status note no longer claims the unsigned sidecar survives as a read model; it names the migration verb, the quarantine copy, and the fold. The maintenance repair table gets a row for an unsigned pre-signed-path sidecar beside the malformed-sidecar one. SLICES marks S-D23's owed typed error closed and S-D24 done, records the admission decision in S-D24's block, and names the two follow-ups (the CLI verb, #456; retiring the migration once no unsigned library remains,
…gned hash `write_asset_files` wrote the plaintext unconditionally, so every metadata-bearing write through `append_lifecycle` — `soft_delete` among them — rewrote the original over itself, opening a crash window in which the only copy is truncated. The migration's create avoided that with an `in_place` branch, but its own `soft_delete` for a legacy `is_deleted` did not, and neither did any ordinary edit. The plaintext is now written only when the bytes already at the media path do not hash to the sidecar's `hash`; streaming the existing file through SHA-256 is cheaper than rewriting it. The create's special branch goes away (the guard covers it), `in_place` survives only to keep Move-mode release from deleting what is now the asset, and the trash carry-over test asserts the original's mtime is unchanged through both the create and the delete.
…ine copy first
The signed sidecar was written straight over `{uuid}.cbor` with a plain
`fs::write`. For the migration that file *is* the legacy record, so a
crash mid-write tore the only non-quarantined copy — and the quarantine
copy itself had not been fsynced, so a power loss could take both.
The sidecar is now staged to `{uuid}.cbor.tmp` and renamed into place —
the single-file atomic write the maintenance doc specifies — so the
previous sidecar survives any failure before the rename; the stale `.tmp`
is the startup scrub's. `quarantine_legacy` fsyncs the copy and its
directory before the signed write begins. A `cfg(test)` fault hook fails
the write between the `.tmp` and the rename; the test proves the legacy
sidecar is byte-for-byte intact, the quarantine copy present, and the
rerun completes. Bundle-wide atomicity (sidecar, chain, blob renamed
together) stays its own slice.
… file The migration tagged a legacy record's coordinates `GpsSource::Exif`, claiming they were read out of the file bytes. They were read out of the unsigned record, and the sidecar is signed, so `Manual` is the honest provenance — the same reasoning the Takeout enrichment records for a service-held fix. A file whose own EXIF carries a fix still wins at the write site and is tagged `Exif` there.
…d extension
The migration pins each asset's files where they are by deriving
`AssetState::capture_utc` from its directory's month. That only holds
when the directory is a `media/{YYYY}/{YYYY-MM}` bucket the lifecycle
maps back to, and when the original's extension is the lowercase
single segment every derived path uses; a sidecar under `media/loose/`
or an original named `.JPG` would have produced paths pointing at files
that do not exist, or silently relocated them, which decision 3 rejects.
Both cases are now refused before anything is written, each with its own
`MigrationSkip` variant (`OutsideMonthBucket`, `UnusualExtension`), and
the bucket pinning itself is finally tested where it matters: a fixture
under `media/2024/2024-07` proves the asset's bucket, every derived path,
and the original's mtime are exactly what was on disk.
…orting `reconcile_legacy_trash` applied the `delete` record an interrupted run owed by calling `soft_delete`, which fails with `AlbumReadOnly` when the asset's album has since been recovered from a backup without its write capability — and that error aborted the whole run before a single new candidate was looked at. The owed delete is now reported per asset as `MigrationSkip::AlbumReadOnly` and the run continues. The report's `trashed` doc now says what it holds: this run's `is_deleted` carry-overs plus any owed delete applied from an earlier run.
…idecarCoder mentions S-D24's SLICES block described the legacy stack id as UUIDv5; the code derives an RFC 9562 v8 custom UUID over SHA-256, the construction the default album id already uses (recorded as decision 14 in PR #455). Two Swift doc comments still named `FFISidecarCoder`, deleted with the FFI codec; they now say `JSONSidecarCoder` is the mock lane's codec and that a core-linking build has no CBOR sidecar codec.
…form has none `quarantine_legacy` fsynced the quarantine directory by opening it as a file and calling `sync_all`. That is a Unix primitive: on Windows a directory cannot be opened that way, so the verb would have failed on every candidate there. The fsync moves into a crate-private `utils::paths::sync_dir`, `#[cfg(unix)]` with a documented no-op elsewhere — the same shape as `capsule-server`'s blob store — beside the `tmp_path` helper whose write-then-rename it partners. The copy itself is now flushed through a write handle, which flushing needs on every platform.
…d disk read `write_asset_files` re-streamed the file at the media path through SHA-256 to decide whether to skip the plaintext write, although `append_lifecycle` had just read that same file into the buffer it passes — two reads of the original on every metadata edit of every asset. The guard now decides from the buffer: a `stat` of the media path, a length comparison, and `hash_bytes(plaintext)` against the sidecar's `hash`, with no second read. A metadata edit costs one read of the original and no write (before this PR: one read and one write). The caller rule is in the doc: the buffer is either the bytes read from the media path or the file about to become it; a same-length file whose bytes differ from a correct buffer is not detected, which no caller in the tree can produce and which `verify` would catch. Both branches are tested directly, the limit pinned. `write_signed_artifacts` is `pub(super)` again: it is the right call for a metadata edit, which would then read nothing at all. The `thread_local!` fault hook's `///` becomes `//` (an unused doc comment the test build warned about).
`append_lifecycle` read the whole original back into memory and handed it to `write_asset_files` on every metadata edit, although a lifecycle write never changes the original. It now calls `write_signed_artifacts` — the sidecar, the provenance chain, and the sealed metadata blob — so a caption edit on a multi-gigabyte video reads and writes the original zero times (before this PR: one full read and one full write per edit). A new test makes the original unreadable for the duration of caption, tag, soft-delete and restore edits and asserts they succeed with the original's mtime and bytes intact.
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
Retires the unsigned pre-signed-path sidecar shape (
AssetSidecar) by migrating every such sidecar into a signedSidecarV1+ manifest + provenance chain through the existing lifecycle write path, then deletes the unsigned reader solibrary::rebuild_indexhas one shape to read (S-D24). Closes S-D23's owed item: a catalog stamped by a newer build is a typed refusal at theopenboundary.Stacked on
feat/media-rawshift-still-decode-410(PR #436); the PR targets that branch.Summary
LibraryError::CatalogTooNew { found, supported }fromopen_libraryvia a crate-privateDatabaseDriver::open_typed(the publicopenkeeps flattening forcapsule-core-ffi);Workspace::opensurfaces every library-open failure as the newLifecycleError::Library(LibraryError)instead of anIostring. Catalog untouched, lock released, tested at both boundaries.sidecar::shape::probe(crate-private) classifies a{uuid}.cborby its two discriminating keys alone — integer key0(signed) vs textversion(legacy) — building no model of either shape.Workspace::openstill succeeds on a library holding unsigned sidecars; a second pass records every sidecar no.provenance.cboranchors and exposes it throughWorkspace::unmigrated_sidecars()(UnmigratedSidecar { path, asset_id, shape }), onewarnper file naming the verb.Workspace::migrate_unsigned_sidecars(&UnsignedMigrationOptions { fallback_album, trash_retain_days }) -> UnsignedMigrationReport. Per legacy record, in asset-id order: refuse without writing (MigrationSkip::{InvalidAssetId, UnknownShape, Undecodable, IdCollision, OriginalMissing, HashMismatch, QuarantineConflict, Stranded, OutsideMonthBucket, UnusualExtension};AlbumReadOnlyreports an owed delete whose album lost its write capability); copy the legacy bytes verbatim to.library/quarantine/{uuid}.cbor+{uuid}.reason.json; admit the asset as a signed create with its legacy id, in its legacy bucket, bytes signed in place, through the create commit extracted fromimport_asset_with(commit_signed_create(&CreateRequest), no public signature change); carryis_deletedas a signeddelete; derive deterministic RFC 9562 v8 (custom) stack ids over SHA-256 forstack_hintgroups of two or more and write them at create. Ends by callingrebuild_index. Idempotent; resumes an interrupted run from the quarantine copy._unknownunderlegacy-unsigned-sidecar, covered by the signature.sidecar::{AssetSidecar, StackHint, read_sidecar}(+cfg(test)write_sidecar),sidecar/{asset_sidecar.rs, stack_hint.rs}, the legacy branch, projection, stack rebuild, helpers, and tests oflibrary/rebuild.rs;capsule-core-ffi::{AssetSidecarRecord, StackHintRecord, serialize_sidecar, deserialize_sidecar}andcapsule-core-ffi/src/sidecar.rs; the SwiftFFISidecarCoder, its test file, and theCatalogFFIBridgeextensions over the deleted records.design/import/pipeline.mdstatus note, adesign/filesystem/maintenance.mdrepair-table row, SLICES rows and detail blocks for S-D23 and S-D24.SLICES counts delta (not applied — the counts paragraph is owned by another lane): status
done+1,blocked−1 (S-D24blocked→done); the "Seven rows readblocked" paragraph and theD21 --> D24mermaid edge atSLICES.md:419-421, :436also describe S-D24 as blocked and are left for the same owner.Validation
All commands run inside the worktree
Capsule.worktrees/Capsule-feat-sidecar-unsigned-migration-412(base merged at2c742d7d).Per-slice focused proofs (each recorded at the commit it proves):
ae01271b):cargo check -p capsule-core— pass.cargo nextest run -p capsule-core -E 'test(catalog_newer_than_this_build) | test(too_new_catalog) | test(test_open_)'— 7 passed.8fa17139, one work-group commit — decision 13):cargo check -p capsule-core— pass.cargo nextest run -p capsule-core -E 'test(migrate_unsigned) | test(sidecar::shape) | test(lifecycle::open) | test(lifecycle::import) | test(library::rebuild) | test(library::open)'— 72 passed.cargo clippy -p capsule-core -- $CLIPPY_FLAGS(the gate's flags) — pass.cargo fmt --check -p capsule-core— pass. A pre-commit adversarial read by a separate reader produced seven findings, all fixed with tests before the commit (decision 15).376c83e1):cargo check -p capsule-core-ffiat the slice-4 tree — pass.mise run gen-bindings— pass (capsule_core+capsule_sdknamespaces). The retired codec lived in thecapsule_core_ffinamespace, whichgen-bindingsdoes not emit, so its bindings were generated directly:cargo build -p capsule-core-ffi && cargo run -q -p capsule-core-ffi --bin uniffi-bindgen -- generate --library target/debug/libcapsule_core_ffi.a --language {swift,kotlin} --out-dir target/bindings-ffi/…— pass;grep -rlover the generated Swift and Kotlin:serializeSidecar0 files,deserializeSidecar0,AssetSidecarRecord0,StackHintRecord0; controlclass Catalogpresent in both.HEAD~1):cargo nextest run -p capsule-core -E 'test(library::rebuild) | test(sidecar::) | test(migrate_unsigned) | test(lifecycle::open) | test(library::open)'— 86 passed.cargo clippy -p capsule-core -- $CLIPPY_FLAGS— pass.cargo fmt --check -p capsule-core— pass. Test count 804 → 796: −4 (asset_sidecar.rs), −1 (stack_hint.rs), −3 (rebuild.rs: four legacy-shape tests removed, one reporting test added), −1 (io.rs), +1 (migrate_unsigned.rs, the keyless-rebuild test).lifecycle::migrate_unsigned::tests::the_legacy_map_is_folded_verbatim_and_covered_by_the_signature): assertscbor::value_to_canonical_vec(sidecar.unknown["legacy-unsigned-sidecar"]) == cbor::canonicalize(legacy_bytes)(fails if the fold drops or alters any key,future_fieldincluded), that the fold contains("future_field", "kept verbatim"), thatsidecar.verify(user_ik)holds, and that removing the fold — orfuture_fieldalone from inside it — makesverifyreturn false.Aggregate gates at head
77386320:mise run check-rust— killed by the session's 10-minute command cap (exit 143, a signal) insidebuild-rust, afterformat-check-rust,lint-check-rust,doc-check-rust,i18n-check,i18n-guard,openapi-check-kynos,architecture-check,license-check, andtranslate-readme-checkhad passed (the aggregate stops at the first failure and had reachedbuild-rust). Per the run rules its remaining sub-tasks were re-run individually:mise run build-rust— exit 0;mise run build-check-wasm— exit 0;mise run build-ffi— exit 0;mise run lint-check-ffi— exit 0;mise run verify-examples— exit 0;mise run gen-bindings— exit 0 (re-run at this head). That is all 15 sub-tasks ofcheck-rustgreen, 9 inside the aggregate and 6 individually.mise run cli-surface-check—unavailable: no such task is defined on this base (mise ERROR no task cli-surface-check found); it is not part ofcheck-rusthere.mise run test-rust— exit 0:cargo nextest run --workspace1776 passed;cargo nextest run -p capsule-core --features ffi801 passed;cargo nextest run -p capsule-sdk --features ffi160 passed.mise run check-docs-truth— exit 0.bun install --frozen-lockfile(incapsule-docs/) — exit 0.mise run check-docs— exit 0.mise run check-md— exit 0.cargo nextest list -p capsule-coreat head77386320listed 796 tests (default features; an earlier draft of this line said 788, which counted only the lib binary); the two pre-existing warnings the test build prints (library/receipts.rs:93unused imports,import/group.rs:211dead code) are in files this PR does not touch and arepre-existing.GitHub checks at
77386320: Commit lint, Docs, Docs truth, Markdown, Rust (fmt + clippy + build), Rust (tests), Rust cross (android, windows, linux-arm64, apple) — pass.Build Capsule.apk + :core JVM smoke— fail,pre-existing: Kotlin compile errors incapsule-android/src/androidMain/kotlin/…(Unresolved reference 'di','DetailViewModel','MuseumObject', …), files this PR does not touch; the same job fails identically on the base PR [FEAT] capsule-core::media on rawshift-image: still decode, the LQIP producer, and typed unsupported formats #436 (run 33611894405) and on the other open lanes' branches.Swift (…)andBuild & test Capsule.app against the Rust core— pending at the time of writing; they are the compile proof for the.swiftdeletions.Swift CI lane (
.github/workflows/ci.ymljobswift;build-ios.ymlwithTUIST_FFI=1) —unavailable locally(no Xcode on this host). It is the only compile proof for the.swiftdeletions; the Rust-side proof above shows the generated surface they compiled against no longer carries the deleted names.Review round 1 (head
803c83b6)Commits:
7a553d3a(F1, decision 17),7ce0c98a(F2, decision 18),8df7ab80(F3, decision 19),a48cac95(F4, decision 20),e09ea51c(F11 + F6),803c83b6(F5 decision 21, F8).cargo nextest run -p capsule-core -E 'test(migrate_unsigned) | …'): 55, 61, 19, 21, 22 passed respectively;cargo fmt -p capsule-corebefore each commit.cargo nextest run -p capsule-core— 800 passed, 0 failed.cargo nextest list -p capsule-core— 800 (796 + the 4 tests added this round).mise run check-docs-truth— exit 0.mise run check-md— exit 0.cargo nextest run -p capsule-core-ffi— 14 passed.mise run doc-check-rust— exit 0.mise run build-ffi— exit 0.mise run gen-bindings— exit 0.mise run check-rust's 15 sub-tasks, run individually (the aggregate exceeds the session's command cap):format-check-rust0,lint-check-rust0,doc-check-rust0,i18n-check0,i18n-guard0,openapi-check-kynos0,architecture-check0,license-check0,translate-readme-check0,build-rust0,build-check-wasm0,build-ffi0,lint-check-ffi0,gen-bindings0,verify-examples0.mise run test-rust— exit 0:cargo nextest run --workspace1780 passed;cargo nextest run -p capsule-core --features ffi805 passed;cargo nextest run -p capsule-sdk --features ffi160 passed.unavailable locally, as before.Confirming round (head
6657474c)Commits:
3dcad84f(F-new-1, decision 22),6657474c(F-new-2 decision 23, F-new-3, and theunused doc commenton the round-1 fault hook).cargo nextest run -p capsule-core -E 'test(migrate_unsigned) | test(utils::)'— 24 passed;cargo nextest run -p capsule-core -E 'test(lifecycle::)'— 106 passed;cargo fmt -p capsule-corebefore each commit.cargo nextest run -p capsule-core— 802 passed, 0 failed.cargo nextest list -p capsule-core— 802 (800 + the two guard tests).mise run doc-check-rust— exit 0.cargo check -p capsule-core --target x86_64-pc-windows-msvc --features ffi—unavailable: the target is not installed on this host (error[E0463]: can't find crate forcore``); CI's Windows leg (Rust cross (windows)) compiles it.mise run check-docs-truth— exit 0.mise run check-md— exit 0.mise run test-rust— exit 0: workspace 1782 passed;capsule-core --features ffi807 passed;capsule-sdk --features ffi160 passed.write_asset_files_overwrites_wrong_bytes_and_leaves_correct_bytes_alone(different-length wrong bytes rewritten; correct bytes untouched, mtime intact) andwrite_asset_files_trusts_a_same_length_buffer_over_the_disk(the guard's documented limit, pinned;verifyrejects the wrong original). A same-length wrong file cannot be detected without the second read decision 23 removes — see that decision's Rejected line.Decision 24 (head
7805ba5b)Commit:
7805ba5b perf(core): write only the signed artifacts on a metadata edit—lifecycle/provenance.rs:200callswrite_signed_artifacts; a metadata edit reads and writes the original zero times. New testlifecycle::import::tests::metadata_edits_neither_read_nor_rewrite_the_original(original made unreadable during caption/tag/soft-delete/restore; mtime and bytes intact after).cargo nextest run -p capsule-core—Summary [ 11.139s] 803 tests run: 803 passed, 0 skipped, exit 0.cargo nextest list -p capsule-core— 803.mise run doc-check-rust— exit 0.Risks and rollout
capsule-core-ffiloses two records and two functions from itsuniffinamespace; the shipping iOS composition root injectsJSONSidecarCoder, so no production path calls them.rebuild_indexno longer indexes unsigned assets (it reports them, decision 12); a library holding unsigned sidecars still opens and lists them; the migration is an explicit verb with no CLI driver yet (cli:capsule library migratedrivesWorkspace::migrate_unsigned_sidecars#456)..library/quarantine/and fsyncing the copy (the directory fsync is a Unix primitive and a documented no-op elsewhere, decision 22); refusals write nothing (the closing rebuild included); the signed sidecar replaces the legacy one by a single-file atomic rename; no write path — create,soft_delete,reconcile_legacy_trash, or a later edit throughappend_lifecycle— rewrites an original whose bytes already carry the signed hash (proved by mtime through create and delete); existing signed sidecars encode byte-identically (no schema bump — the fold is an additive_unknownkey).metadata.md,validation.md) declare no upper bound for_unknown, so none is enforced here; an oversized legacy record therefore yields a correspondingly larger signed sidecar. No documented cap is violated; nothing filed.feat/media-rawshift-still-decode-410(PR [FEAT] capsule-core::media on rawshift-image: still decode, the LQIP producer, and typed unsupported formats #436) and merges after it.Related Issues
Closes #412
Refs #436 (base), #456 (CLI verb follow-up), #457 (retire the migration once no unsigned library remains)
Decisions taken
No human approved this plan: this lane ran unattended; the record below is what it decided, for a human to read afterwards.
Unresolved review notes
append_lifecycle(lifecycle/provenance.rs) rewrites the original plaintext over itself on every metadata write viawrite_asset_files; the same crash window this PR avoids on the create path exists there for every edit. Belongs with the per-asset-bundle atomicity slice named indesign/filesystem/maintenance.md.FFISidecarCoder(SidecarCodec.swift:7,ImportService.swift:24) were corrected — a recorded two-line manifest widening.append_lifecyclenoted above no longer happens;write_asset_filesskips an original that already carries the signed hash.append_lifecyclere-read of the original is gone;lifecycle/provenance.rsis a recorded one-line manifest widening.