Skip to content

[FEAT] CLI: help from the catalogs, capsule show, and the capture-time repair (#413) - #450

Open
justin13888 wants to merge 14 commits into
docs/reference-generation-415from
feat/cli-help-catalogs-show-repair-413
Open

[FEAT] CLI: help from the catalogs, capsule show, and the capture-time repair (#413)#450
justin13888 wants to merge 14 commits into
docs/reference-generation-415from
feat/cli-help-catalogs-show-repair-413

Conversation

@justin13888

@justin13888 justin13888 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Description

Issue #413, stacked on #443 (docs/reference-generation-415): --help rendered from the catalogs (S-I8), a capsule show read surface for what the importer wrote (S-B18), and capsule repair capture-time for 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

  • S-I8 — localized help. capsule_cli::cli::help::localize rewrites every about/long_about/help/long_help of a built clap::Command tree from cli.help.<path>.about / cli.help.<path>.arg.<id> catalog keys. run() applies it under the bundle negotiated from LC_ALL/LC_MESSAGES/LANG; command_tree() applies it under an explicitly pinned en bundle so cli-surface.json is locale-proof and byte-unchanged. A missing key leaves the derive text (never a raw key). 69 cli.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 the ValueEnum residual; i18n-guard's blind-spot comment now points at that test.
  • S-B18 — 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 (via Workspace::is_trashed), stack placement, LQIP presence, provenance record count); absent values print as (unset); an ambiguous prefix is refused with the count. 39 cli.show.* keys. The guide's metadata-sampling step is rewritten as an executable loop and asserted as written in tests/takeout_import.rs.
  • S-B17 precondition — index projection. asset_row_from_state now projects capture_timestamp/capture_utc from the signed sidecar, as rebuild_index already did; equal at import, they part ways at exactly a capture correction, which would otherwise have been invisible to the timeline until a rebuild.
  • S-B17 core — Workspace::set_capture_timestamp(asset_id, jiff::Timestamp) (plus Workspace::is_trashed, review round 1) appends one signed metadata-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 freezing capsule-core surface (decision 7).
  • S-B17 CLI — 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 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; --apply corrects 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. 14 cli.repair.* keys. Smoke tests reproduce the pre-S-B16 shape, dry-run, apply, show, library rebuild, and re-run across process boundaries.
  • S-B11 doc half + bookkeeping. The guide's sampling step is executable; the real-archive remainder is filed as S-B11: run the Google Takeout import against a real archive (the done* remainder) #452; SLICES.md rows S-I8/S-B18/S-B17 → done, S-B11 stays done* 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 head b219db35 (review round 1 applied). Every failure is classified.

Command Outcome
cargo nextest run -p capsule-cli --no-fail-fast pass — 73 tests, the crate total; 37 are new to this PR (7 cli::help, 12 show, 11 repair, 6 show_and_repair smoke, 1 added to takeout_import)
cargo nextest run -p capsule-core lifecycle::metadata lifecycle::import pass — 21 (correction test: two-record chain that verifies, unmoved bundle, reopen, rebuild agreeing with the live row; is_trashed replay)
mise run test-rust pass — workspace 1755/1755, capsule-core --features ffi 733/733, capsule-sdk --features ffi 160/160
mise run check-rust (aggregate) not run as an aggregate on this head: an earlier aggregate on e765feb6 was killed by the 10-minute tool cap (exit 143) inside its last sub-task — classified unavailable as an aggregate; every sub-task run individually below
mise 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-examples pass — each exit=0, run individually in the foreground on b219db35; gen-bindings left the tree clean
mise run check-docs-truth pass — 88 citations and 118 module paths resolve
mise run check-md pass — Summary: 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-docs pass — exit 0, 73 pages built
mise run check-web pass on e765feb6 — 59 tests, build green (the regenerated capsule-web/src/i18n/messages/en.json; the later commits change that file only by adding keys)
cargo clippy -p capsule-cli --all-targets -- $CLIPPY_FLAGS pass
cargo clippy -p capsule-core --all-targets -- $CLIPPY_FLAGS fails on unreadable_literal in capsule-core/src/cbor/mod.rs:114-116, capsule-core/src/db/driver.rs:647-648 and on capsule-core/src/lifecycle/import.rs:690-691 (test code) — pre-existing (all blame to commits before this branch; --all-targets is stricter than the gate's cargo clippy --workspace, which passes above)
LC_ALL=fr_FR.UTF-8 / tr_TR / ja_JP command_tree() equality (existing test) and the new byte-identical rendered-help test pass — cli-surface.json unchanged by S-I8

Catalog 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

  • Help parsing path. run() now goes CommandFactory::command()help::localizeget_matches()FromArgMatches::from_arg_matches_mut instead of Parser::parse; the same clap code path, and dispatch is untouched. Under every locale without cli.help.* entries (all twelve today) the rendered help is byte-identical to before.
  • Index projection change (asset_row_from_state) is behaviour-neutral for every asset written today; it only matters after a capture correction. Reverting it after repair --apply has run on a real library makes repaired rows stale until capsule library rebuild.
  • repair --apply is irreversible by design (a signed record per asset); the default is dry run, --limit allows a checked first batch, and each write is independent, so an interrupted run is safe and idempotent.
  • Post-repair drift: a corrected asset's sidecar no longer names its month directory, so Workspace::open logs its reconciliation warning per corrected asset on every open. Documented as expected in maintenance.md and in the S-B17 block; the opportunistic rename bundle is not built.
  • capsule-core public surface grows by two methods (decision 7) on a branch whose base is freezing that API.
  • Backout per slice: revert the S-I8 commit (help), the show commit, the repair commit, 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

Issue 413 - cli: help text from the catalogs, an enrichment read surface, and the capture-timestamp repair (S-I8, S-B18, S-B17)
Plan:     v1 (planned against f433d918; executed on the head of lane #415's branch, which is on #399's)
Branch:   feat/cli-help-catalogs-show-repair-413
Base:     docs/reference-generation-415 (head of the W-DOCSREF PR), stacked — it needs command_tree() and cli-surface.json; the PR targets that branch
Worktree: /var/mnt/scratch/golem/dev/Capsulsaurus/Capsule.worktrees/Capsule-feat-cli-help-catalogs-show-repair-413
Cause:    -
Touches:  locales/en.json (+~93 keys: cli.help.*, cli.show.*, cli.repair.*) and the four generated catalogs via `mise run i18n`, capsule-cli/src/cli/{help.rs (new), mod.rs (pub mod help), commands.rs (Show, Repair/RepairCommands)}, capsule-cli/src/{lib.rs (run() rewiring, dispatch arms), i18n.rs (key constants), show.rs (new), repair.rs (new)}, capsule-core/src/lifecycle/{metadata.rs (set_capture_timestamp), mod.rs (original_path accessor), import.rs (asset_row_from_state projects capture_timestamp from the sidecar, :163-164)}, capsule-cli/tests/{show_and_repair.rs (new), takeout_import.rs (extend)}, the cli-surface artifact as #443 placed it (regenerated for the two new verbs; UNCHANGED by S-I8), xtask/src/i18n_guard.rs (:66 blind-spot comment only), capsule-docs/src/content/docs/design/i18n.md (S-I8 decision + ValueEnum residual), capsule-docs/src/content/docs/guides/google-photos-migration.md (:266-275 executable sampling step), SLICES.md (rows/blocks S-I8, S-B18, S-B17, S-B11 ONLY — the S-I5 correction belongs to #398)
Will not: translate help into the twelve non-source locales; localize ValueEnum variant help; relocate media bundles after a capture correction; run S-B11's real-archive round trip; add --json to show; touch capsule-server/**
Lane:     serialised behind #415 (capsule-cli/src/cli/mod.rs, cli-surface.json) and transitively #399. Forecast collisions: capsule-core/src/lifecycle/{import.rs,mod.rs} with #410/#412 (distinct hunks; #412's LibraryCommands::Migrate and this lane's Show/Repair both extend commands.rs and lib.rs dispatch), locales/en.json with #412/#407 (distinct keys).
Settled:  Bucket-vs-timestamp drift after a capture correction is expected and the bundle is not relocated (filesystem/maintenance.md:26). Barrels (#399). command_tree() reads the pinned `en` bundle (#415).

Decisions taken.

1. Deliverable boundary - all three of S-I8, S-B18 and S-B17, plus S-B11's doc half.
   Taken:    All three: append_lifecycle (lifecycle/provenance.rs:89) already implements correction-as-a-new-signed-revision, so S-B17 applies settled rules. Includes the index-projection fix (asset_row_from_state reads the sidecar, matching rebuild.rs:305) without which a correct repair is invisible until a rebuild.
   Rejected: S-I8 + S-B18 now, S-B17 filed - no unsettled provenance rule exists to justify it, and SLICES.md:1083-1085 records that the repair is cheap now and becomes a user-data migration later. Rejected: including S-B11's real-archive run - SLICES.md:867-872: no real Takeout archive exists on this machine.
   Reverses: drop delivery slices 3-5; restore the S-B17 row to ready.
   Filed:    the lane files the S-B11 real-archive round-trip remainder and links it; S-B11 stays done*.

2. Where localized help is applied, given #415's committed cli-surface.json.
   Taken:    A localize(Command, &Bundle) seam applied in run(); #415's command_tree() pinned to Bundle::for_locale("en"); an invariant test that every cli.help.* English message equals the derive text it replaces (so cli-surface.json is unchanged by S-I8), and a test that a fr bundle changes localize output while command_tree() is unchanged. Missing key ⇒ derive English, never a raw key.
   Rejected: localizing only inside the binary with Cli::command() untouched - a second source of truth with no gate. Rejected: #[command(about = fn())] with a global bundle - clap derive needs 'static, forcing a process-global OnceLock that command_tree() cannot pin.
   Reverses: delete help.rs, restore <Cli as Parser>::parse(), drop the cli.help.* keys, rerun mise run i18n.

3. How `capsule show` names an asset.
   Taken:    One positional accepting a UUID or a sha256 hex prefix (>= 8 chars) resolved over Workspace::asset_ids() against sidecar.hash.to_hex(); an ambiguous prefix is refused with the match count.
   Rejected: UUID only - nothing the CLI prints during import emits asset ids, so the migration guide's sampling step would stay unexecutable. Rejected: source filename - SidecarV1 stores none.
   Reverses: delete the hash arm in show.rs::resolve.

4. Default mode of `capsule repair capture-time`.
   Taken:    Dry run by default, --apply to write.
   Rejected: --dry-run opt-in, matching push/sync - those write to a re-drivable server; this appends an irreversible signed metadata-update per asset.
   Reverses: rename the flag to dry_run and invert the branch.

5. The "fr bundle changes localize output" test in decision 2, as executed.
   Taken:    The localization-changes-output proof is a stub look-up (`localize_with`, answering "XX" for one key and nothing else) rather than a `fr` bundle, plus the pre-existing `command_tree()` test that renders under `en_US`, `tr_TR` and `ja_JP` and asserts equality. Adds `localize_with(Command, &dyn Fn(&str) -> Option<String>)` as the testable seam `localize` is a one-line wrapper over.
   Rejected: a `fr` bundle - `locales/fr.json` carries no `cli.help.*` entry and this lane does not translate (`Will not`), so under `fr` the rewriter is provably the identity (that case is asserted too, under an unknown locale) and could not demonstrate a change without a translation this lane refuses to author.
   Reverses: none needed; the stub test is strictly stronger than the one it replaces.

6. Lockstep of `long_about`/`long_help` with the short form.
   Taken:    When the derive sets the long form equal to the short one (its behaviour for a one-paragraph doc comment), a translated short form is copied into the long form too; a distinct long form is replaced only by its own `.long_about` / `.long_help` key. The invariant test demands the long key exists exactly when the derive gives a distinct long text.
   Rejected: translating the short form alone - `-h` would speak the user's language and `--help` English for the same argument.
   Reverses: drop the two `derive_long == derive_*` branches in help.rs.
7. Additive public items on the freezing capsule-core surface (#399/#426).
   Taken:    Three new public methods on `Workspace`, all in `capsule-core/src/lifecycle`: `set_capture_timestamp(&mut self, &Uuid, jiff::Timestamp) -> Result<()>` (metadata.rs), `original_path(&self, &Uuid) -> Option<PathBuf>` (mod.rs), and — from review round 1, decision 12 — `is_trashed(&self, &Uuid) -> bool` (mod.rs), which makes the private `asset_is_deleted` chain replay reachable through one public path. No new types, no new error variants, no signature changed. `set_capture_timestamp` takes a `jiff::Timestamp` rather than `i64` seconds so an out-of-range instant is unrepresentable at the call site instead of needing a new `LifecycleError` variant on a frozen enum.
   Rejected: an `i64` parameter clamped to the epoch (silent wrong data) or a new `LifecycleError::InvalidTimestamp` variant (widens the frozen enum).
   Reverses: delete the two methods; the CLI repair verb goes with them.

8. `show` prints the datum with every fix; the trash row is back (amended by decision 12).
   Taken:    The GPS row is `{lat}, {lon} ({datum}, {source})` with `WGS-84`/`GCJ-02` as catalog keys, because `SidecarV1::gps.datum` is stored verbatim and a GCJ-02 coordinate would otherwise read as WGS-84. The "In trash" row was first dropped (deriving it would have duplicated the lifecycle's private chain replay in the CLI) and is restored in review round 1 through `Workspace::is_trashed`, the one public path for that fact — the `cli.show.value.yes/no` contexts already described the row, and the repair needs the same fact.
   Rejected: replaying the chain in `show.rs`; leaving the catalog describing a row that does not exist.
   Reverses: drop the row and the accessor together.

9. `capsule repair capture-time --limit N` (amended by decision 13).
   Taken:    `--limit` bounds how many affected assets one `--apply` run corrects; detection and the report always cover the whole library, and the run says it stopped and that a re-run continues. Since review round 1 it is a positive count (`value_parser!(u64).range(1..)`) and is refused without `--apply` (decision 13) rather than silently ignored.
   Rejected: no limit at all — a first `--apply` over a real library should be able to correct a handful and be checked with `capsule show` before the rest.
   Reverses: delete the flag and the `take(budget)` in `repair::apply`.

10. The S-B11 real-archive remainder is filed as #452 (per decision 1). S-B11 stays `done*` and links it directly; S-Z2 (the migration guide) stays `done*` for the same reason and reaches #452 transitively through its existing "real-archive round trip → S-B11" note.
11. Manifest widened by `capsule-docs/src/content/docs/reference/cli.md` (orchestrator decision).
   Taken:    One sentence: the overview now names the five commands that unseal a library and accept `--passphrase-stdin` (`import`, `push`, `cull`, `show`, `repair capture-time`), confirmed against `cli-surface.json`, which lists exactly those five `passphrase_stdin` arguments. Stale because of this PR; #443's lane is terminal.
   Rejected: leaving a reference page wrong until a later lane.
   Reverses: restore the three-command sentence.
12. The trash fact is exposed once and used twice (review round 1, Q1 → a, Q3 → c, F1 + F2).
   Taken:    One additive public accessor, `Workspace::is_trashed(&self, &Uuid) -> bool`, the existing chain replay made public and named in decision 7. `capsule repair capture-time` skips trashed assets before reading their originals and reports them as their own category (`cli.repair.capture_time.trashed`, plus a `{trashed}` count in the summary); `capsule show` restores the "In trash" row. Tests: a trashed asset is neither corrected nor counted as affected, and is affected again after a restore; `show` prints the row over a real reject sweep, in process and through the binary.
   Rejected: marking trashed rows but still writing irreversible records to them; leaving the catalog contexts describing a row that does not exist.
   Reverses: delete the accessor, the skip, the row, and the two keys.

13. `--limit` is a positive count and needs `--apply` (review round 1, Q2 → a, F3).
   Taken:    `value_parser!(u64).range(1..)` on the argument, so `--limit 0` is a parser error; `--limit` without `--apply` is refused in the dispatch arm with `cli.repair.capture_time.limit_requires_apply`, which names both flags, before the library is opened. Both are asserted through the binary.
   Rejected: clap's `requires = "apply"` — its message is English and not the catalog's.
   Reverses: drop the range and the guard.

14. Low-severity repairs from review round 1, as executed.
   Taken:    F4 — the warn on an unparseable capture timestamp lives at the projection site in `asset_row_from_state`, mirroring `rebuild.rs` line for line, not hoisted into `rfc3339_to_secs` (which also parses import and deletion timestamps, where the epoch fallback is not a data defect worth a warning). F6 — `ShowError`'s `#[error]` strings are developer-facing (selector and count only); the user always sees `describe_error`. F7 — the `from_arg_matches_mut` error is formatted through a clone of the localized command before `exit()`. F9 — the help invariant test asserts the converse by reading `locales/en.json` from `CARGO_MANIFEST_DIR`, since `Bundle` exposes no key iterator.
   Rejected: pinning `ShowError`'s `Display` to the catalog with a test — a second rendering of the same message with no reader.
   Reverses: each is a one-hunk revert.

Unresolved review notes

(append-only; empty)

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

cloudflare-workers-and-pages Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploying capsule with  Cloudflare Pages  Cloudflare Pages

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

View logs

`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.
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.
`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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant