[FEAT] CLI: help from the catalogs, capsule show, and the capture-time repair (#413) - #450
Open
justin13888 wants to merge 14 commits into
Open
Conversation
…ogs-show-repair-413
Help text was the one user-facing surface the i18n contract could not reach: clap renders doc comments itself, and i18n-guard records the gap as a blind spot. This adds the seam (S-I8). - `cli::help::localize` walks a built `clap::Command` tree and replaces every about/long_about/help/long_help with the catalog message under a key derived from the command path and argument id (`cli.help.<path>.about`, `cli.help.<path>.arg.<id>`, ...). A missing key leaves the derive text in place, so a partial translation never prints a raw key. - `run()` builds the parser through the rewriter under the bundle negotiated from the process locale; `command_tree()` resolves through an explicitly pinned `en` bundle so `cli-surface.json` stays locale-proof and unchanged. - The 59 `en` entries are the derive text verbatim, and a unit test asserts that for every string in the tree plus byte-identical rendered help under `en`. That test is the gate i18n-guard cannot be for this surface; its blind-spot comment now says so. - The i18n design doc records the decision (help is localized) and the residual: ValueEnum variant help, which clap 4 cannot re-word without discarding the typed parser. Catalogs regenerated with `mise run i18n`.
Deploying capsule with
|
| Latest commit: |
b219db3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://59029b91.capsule-22k.pages.dev |
| Branch Preview URL: | https://feat-cli-help-catalogs-show.capsule-22k.pages.dev |
`asset_row_from_state` indexed `capture_timestamp`/`capture_utc` from the
in-memory `AssetState::capture_utc` shard, while `rebuild_index` projects
them from the signed sidecar — and the rebuild's own comment says the two
must agree. They were equal at import, so nothing observed the difference;
a capture-time correction (S-B17) is exactly the write that separates them,
because it re-signs the sidecar and deliberately leaves the
`media/{YYYY}/{YYYY-MM}` shard where the files are. Without this, a correct
repair would be invisible to the timeline until the next rebuild.
Behaviour-neutral for every asset written today; an unparseable sidecar
timestamp indexes as the epoch, as the rebuild already does.
Nothing in the CLI printed what the importer wrote into an asset's signed sidecar, so a user could not verify the Takeout enrichment (S-B10) that `--provider takeout` exists to deliver, and the migration guide had to say so instead of instructing the check (S-B18). - `capsule show <ASSET> --library <PATH>` resolves an asset id or a hex prefix (>= 8 chars) of the content hash — the SHA-256 a user already has from the guide's spot-hash step — and prints the sidecar projection: album, content type, hash, dimensions, capture and import instants, caption, rating, user and AI tags, the fix with its datum and source, cull flag, hidden, stack placement, LQIP presence, and the provenance record count. Every absent value is spelled out as unset. - An ambiguous prefix is refused with the match count; a 32-hex-digit prefix that parses as a bare UUID still reaches the prefix arm. - Every line is a `cli.show.*` catalog key (40 keys), including the list separator and the datum names, so a GCJ-02 fix stored verbatim is never read as WGS-84. - Smoke tests spawn the binary over a synthesized EXIF JPEG and over the Takeout fixture; the guide's metadata-sampling step is now executable and is asserted as written. `cli-surface.json` gains the verb; catalogs regenerated with `mise run i18n`.
Every asset imported before S-B16 carries its import time as its capture
time inside the signed sidecar; the correct value is recoverable from the
original's EXIF, but the wrong one is under signature, so correcting it is
a `metadata-update` issued by a key-holding client, not an edit (S-B17).
- `Workspace::set_capture_timestamp(asset_id, Timestamp)` appends one
signed `metadata-update` through `append_lifecycle`: sidecar re-signed,
blob re-sealed under a fresh nonce, binding self-checked, artifacts
rewritten, index row re-projected. It takes a `jiff::Timestamp` so an
out-of-range instant is unrepresentable at the call site.
- The media bundle is deliberately not relocated: the sidecar is
authoritative for the date and the `media/{YYYY}/{YYYY-MM}` directory is
only the shard fixed at import; the design records bucket-vs-timestamp
drift after a capture correction as expected, and `Workspace::open`
already reconciles it by keeping the directory.
- `Workspace::original_path(asset_id)` exposes the original's on-disk path
so the repair pass can re-read EXIF without decrypting anything.
The test imports, corrects, reopens from disk, and rebuilds the index,
asserting a two-record chain that verifies, an unmoved bundle, and the same
corrected instant from the live row and the rebuilt one.
…ogs-show-repair-413
Every asset imported before S-B16 carries its import time as its capture time inside the signed sidecar. This is the pass that goes back to the original and asks (S-B17). - `capsule repair capture-time --library <PATH> [--apply] [--limit N]` re-reads each original's EXIF under the importer's own resolution (`resolve_timezone` over `extract_exif`). An instant that disagrees with the sidecar is affected; a floating time or no EXIF resolves to nothing and is skipped rather than guessed as UTC, exactly as the importer skips it, which makes the pass a no-op on a post-S-B16 library by construction and leaves Takeout-folded captures alone. It never compares capture to import time. An unreadable original is reported as such, never as "no EXIF". - Dry run is the default: `push`/`sync` default to writing because they write to a re-drivable server; this appends an irreversible signed record per asset. `--apply` corrects each affected asset as its own `metadata-update` through `Workspace::set_capture_timestamp`, so an interrupted run leaves completed assets correct and a re-run skips them. `--limit` bounds one run's corrections; the report always covers the whole library. - Every printed line is a `cli.repair.*` key (12 keys). Unit tests cover the verdicts over synthesized EXIF, detect/apply/idempotence over a workspace, and the limit; the smoke tests spawn the binary through dry-run, apply, `show`, `library rebuild` and a second run. `cli-surface.json` gains the verb; catalogs regenerated with `mise run i18n`.
…s remainder S-I8: help is localized via the cli.help.* keys; the ValueEnum residual stays English. S-B18: `capsule show`, selected by asset id or content-hash prefix; the guide's sampling step is executable. S-B17: `capsule repair capture-time`, dry run by default, over the importer's own resolution rule, with the index-projection precondition and the expected post-repair drift recorded. S-B11 stays done* and links #452, which carries the real-archive run this machine cannot perform.
…ogs-show-repair-413
`capsule show` and `capsule repair capture-time` open the sealed workspace and accept `--passphrase-stdin`, so the overview's count of three was stale the moment they landed. The five named here are exactly the commands whose `passphrase_stdin` argument appears in cli-surface.json.
…apture time `Workspace::is_trashed(asset_id)` is the provenance-chain replay the workspace already applies, made public so the CLI's read and repair verbs report or skip a trashed asset without re-deriving the rule (review round 1 of #450, decision 12). Unknown ids read as not trashed. `asset_row_from_state` now logs a warning when a sidecar's capture timestamp does not parse, exactly as `rebuild_index` does at the same projection, instead of silently indexing the epoch.
…rojection The warn-on-unparseable branch is a `map_or_else`, which is what `lint-check-rust` accepts; behaviour unchanged.
…ten --limit Review round 1 of #450. - The trash fact is exposed once (`Workspace::is_trashed`) and used twice: `capsule repair capture-time` skips an asset in trash before reading its original and reports the count as its own category, because appending an irreversible signed record to an asset the user discarded is not a repair; `capsule show` regains its "In trash" row through the same accessor, so the catalog contexts that described it describe something that exists again. - `--limit` is a positive count (`value_parser!(u64).range(1..)`), and `--limit` without `--apply` is refused with a line naming both flags before the library is opened, instead of silently doing nothing. - A derive/matches mismatch in `run()` is formatted through the localized command before it exits, the way clap reports its own errors. - `ShowError`'s `Display` strings are developer-facing (selector and count only); the user always sees `describe_error`, which goes through the catalog. - The help invariant test now also asserts the converse: every `cli.help.*` key in the canonical catalog is one the command walk produces, so a key orphaned by a rename fails the build. - Tests: a trashed asset is neither corrected nor counted as affected and is picked up again after a restore; `apply` writes exactly `Affected::recovered` (tripwire) and the detect → apply path lands the fixture's EXIF instant itself; a swept asset shows `In trash: yes` in process and through the binary; `--limit 0` and `--limit` without `--apply` are refused through the binary. Catalogs and cli-surface.json regenerated (`mise run i18n`, `mise run cli-surface`); the `--limit` help text's catalog entry follows its doc comment.
The sentence added for `show` and `repair capture-time` left one line past the page's wrap width.
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
Issue #413, stacked on #443 (
docs/reference-generation-415):--helprendered from the catalogs (S-I8), acapsule showread surface for what the importer wrote (S-B18), andcapsule repair capture-timefor sidecars stamped with import time before S-B16 (S-B17), plus the migration guide's sampling step made executable (S-B11's doc half).Summary
capsule_cli::cli::help::localizerewrites everyabout/long_about/help/long_helpof a builtclap::Commandtree fromcli.help.<path>.about/cli.help.<path>.arg.<id>catalog keys.run()applies it under the bundle negotiated fromLC_ALL/LC_MESSAGES/LANG;command_tree()applies it under an explicitly pinnedenbundle socli-surface.jsonis locale-proof and byte-unchanged. A missing key leaves the derive text (never a raw key). 69cli.help.*entries are the derive text verbatim, gated by a unit test that walks the whole tree in both directions (every string has its key; every key is produced by the walk) and by a byte-identical rendered-help comparison. The i18n design doc records the decision and theValueEnumresidual;i18n-guard's blind-spot comment now points at that test.capsule show.capsule show <ASSET> --library <PATH>resolves an asset id or a hex prefix (>= 8 chars) of the content hash — the SHA-256 the migration guide already has the user compute — and prints the signed sidecar's projection (album, content type, hash, dimensions, capture/import instants, caption, rating, user/AI tags, the fix with its datum and source, cull flag, hidden, in-trash (viaWorkspace::is_trashed), stack placement, LQIP presence, provenance record count); absent values print as(unset); an ambiguous prefix is refused with the count. 39cli.show.*keys. The guide's metadata-sampling step is rewritten as an executable loop and asserted as written intests/takeout_import.rs.asset_row_from_statenow projectscapture_timestamp/capture_utcfrom the signed sidecar, asrebuild_indexalready did; equal at import, they part ways at exactly a capture correction, which would otherwise have been invisible to the timeline until a rebuild.Workspace::set_capture_timestamp(asset_id, jiff::Timestamp)(plusWorkspace::is_trashed, review round 1) appends one signedmetadata-update(sidecar re-signed, blob re-sealed, binding self-checked, artifacts rewritten, row re-projected) and does not relocate the bundle;Workspace::original_path(asset_id)exposes the original's path for the repair pass. All three are additive public items on the freezingcapsule-coresurface (decision 7).capsule repair capture-time --library <PATH> [--apply] [--limit N]. Re-reads each original's EXIF under the importer's own resolution (resolve_timezoneoverextract_exif); an instant that disagrees with the sidecar is affected, a floating time or no EXIF is skipped (never guessed as UTC), an unreadable original is reported as such. Assets in trash are skipped before their original is read and counted as their own category. Dry run by default;--applycorrects each affected asset as its own signed write, so an interrupted run is safe and a re-run skips completed assets;--limit N(N >= 1, requires--apply) bounds one run. A no-op on a post-S-B16 library by construction. 14cli.repair.*keys. Smoke tests reproduce the pre-S-B16 shape, dry-run, apply,show,library rebuild, and re-run across process boundaries.SLICES.mdrows S-I8/S-B18/S-B17 →done, S-B11 staysdone*linking S-B11: run the Google Takeout import against a real archive (the done* remainder) #452.Validation
Run inside the worktree
/var/mnt/scratch/golem/dev/Capsulsaurus/Capsule.worktrees/Capsule-feat-cli-help-catalogs-show-repair-413, in the foreground, on headb219db35(review round 1 applied). Every failure is classified.cargo nextest run -p capsule-cli --no-fail-fastcli::help, 12show, 11repair, 6show_and_repairsmoke, 1 added totakeout_import)cargo nextest run -p capsule-core lifecycle::metadata lifecycle::importis_trashedreplay)mise run test-rustcapsule-core --features ffi733/733,capsule-sdk --features ffi160/160mise run check-rust(aggregate)e765feb6was killed by the 10-minute tool cap (exit 143) inside its last sub-task — classifiedunavailableas an aggregate; every sub-task run individually belowmise run format-check-rust,lint-check-rust,i18n-check,i18n-guard,openapi-check-kynos,cli-surface-check,architecture-check,license-check,translate-readme-check,doc-check-rust,build-rust,build-check-wasm,build-ffi,lint-check-ffi,gen-bindings,verify-examplesexit=0, run individually in the foreground onb219db35;gen-bindingsleft the tree cleanmise run check-docs-truthmise run check-mdSummary: 0 issues in 0 files(one caused MD024 duplicate heading from a SLICES.md edit was found and fixed before it was committed)mise run check-docsmise run check-webe765feb6— 59 tests, build green (the regeneratedcapsule-web/src/i18n/messages/en.json; the later commits change that file only by adding keys)cargo clippy -p capsule-cli --all-targets -- $CLIPPY_FLAGScargo clippy -p capsule-core --all-targets -- $CLIPPY_FLAGSunreadable_literalincapsule-core/src/cbor/mod.rs:114-116,capsule-core/src/db/driver.rs:647-648and oncapsule-core/src/lifecycle/import.rs:690-691(test code) — pre-existing (all blame to commits before this branch;--all-targetsis stricter than the gate'scargo clippy --workspace, which passes above)LC_ALL=fr_FR.UTF-8/tr_TR/ja_JPcommand_tree()equality (existing test) and the new byte-identical rendered-help testcli-surface.jsonunchanged by S-I8Catalog additions, measured (
git diff origin/docs/reference-generation-415..HEAD -- locales/en.json, added keys only, none removed): 122 keys —cli.help.*69,cli.show.*39,cli.repair.*14.Gate unavailable: S-B11's real-Takeout-archive run — no such archive exists on this machine (filed as #452).
Risks and rollout
run()now goesCommandFactory::command()→help::localize→get_matches()→FromArgMatches::from_arg_matches_mutinstead ofParser::parse; the same clap code path, anddispatchis untouched. Under every locale withoutcli.help.*entries (all twelve today) the rendered help is byte-identical to before.asset_row_from_state) is behaviour-neutral for every asset written today; it only matters after a capture correction. Reverting it afterrepair --applyhas run on a real library makes repaired rows stale untilcapsule library rebuild.repair --applyis irreversible by design (a signed record per asset); the default is dry run,--limitallows a checked first batch, and each write is independent, so an interrupted run is safe and idempotent.Workspace::openlogs its reconciliation warning per corrected asset on every open. Documented as expected inmaintenance.mdand in the S-B17 block; the opportunistic rename bundle is not built.showcommit, therepaircommit, the core API commit, the projection fix — each is a pure addition except the projection fix (see above).Related Issues
Refs #413. Stacked on #443 (targets its branch).
Decisions taken
Unresolved review notes
(append-only; empty)