Skip to content

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

Open
justin13888 wants to merge 9 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 9 commits into
docs/reference-generation-415from
feat/cli-help-catalogs-show-repair-413

Conversation

@justin13888

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). 59 en entries are the derive text verbatim, gated by a unit test that walks the whole tree 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.
  • (further slices are appended as they land)

Validation

Run inside the worktree /var/mnt/scratch/golem/dev/Capsulsaurus/Capsule.worktrees/Capsule-feat-cli-help-catalogs-show-repair-413.

Command Outcome
cargo nextest run -p capsule-cli --lib pass (34 tests, incl. the 7 new cli::help tests)
mise run cli-surface-check pass (artifact unchanged by S-I8)
mise run i18n-check pass
mise run i18n-guard pass
cargo clippy -p capsule-cli -p xtask --all-targets -- $CLIPPY_FLAGS pass (only the pre-existing proc-macro-error2 future-incompat note from a dependency)
cargo fmt --check pass after cargo fmt -p capsule-cli

(the full gate set — mise run check-rust, mise run test-rust, mise run check-docs-truth, check-docs/check-md — is recorded here when the last slice lands)

Risks and rollout

  • run() now parses through Command::get_matches + FromArgMatches::from_arg_matches_mut instead of Parser::parse; the two are the same code path inside clap, and dispatch is untouched, so every library-driven test keeps exercising the same arms.
  • Under a locale with no cli.help.* entries (every non-en locale today) the rendered help is byte-identical to before. Translators fill cli.help.* through the documented locales/ pull-request flow; nothing here translates.
  • Backout: revert the S-I8 commit (help.rs, the run() rewiring, the cli.help.* keys, regenerated catalogs).

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.

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: e765feb
Status: ✅  Deploy successful!
Preview URL: https://f5012b53.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.
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