feat(cli): deck edit — change a deck without deleting it - #224
Merged
Conversation
`loopky` could create a deck and edit every card in it, but nothing about the deck itself. The only route to a new cover or a fixed typo was `deck delete` + `deck create --from-file`, which mints a **new deck id** — so every `pubky://…/decks/<id>/` link breaks and every card goes back to new in the scheduler — and which is impossible at all for a deck built with `card add`, since there is no file to recreate it from (#222). One `updateMetadata` call: the manifest and the tag records, never the chunks, so it costs the same on a 20k-card deck as on an empty one. Four rules, each already the surface's: - An absent flag leaves its field alone (`card edit`'s rule), and an explicitly empty value clears it (`--description=`, which is `--back=`). `--clear-tags` and `--clear-cover` are switches because those two are not single values: `--tag ""` would have to mean both "no tags" and "one blank tag", and a cover is an emoji and an image that layer. - `--tag` replaces rather than appends. Appending makes "the tags are exactly these" unsayable without a delete, and makes a retry after the hourly session expiry (#165) grow the list — the same property `card add`'s dedupe exists for. - An edit whose fields all already hold that value writes nothing and reports `changed: false`. A manifest write bumps `updated_at`, which is what every follower's "the author published changes" badge reads. - The derived language labels are reconciled only when the pair actually moves, so retyping a deck drops the old language's label — and `--clear-tags` on a language deck stays empty, because those are ordinary author-removable tags rather than a reserved family (§7.7). The study opt-ins are in with the metadata rather than held back for later: review state is keyed by `card_id` alone and the modes decide only how a card is presented, so turning one off costs no progress. Reverse's pairing is session-local and persisted nowhere. `--cover-url` and `--cover-emoji` are *not* mutually exclusive, contrary to the issue: they are separate manifest fields and `DeckTile` draws the image over the emoji, which is its fallback. `--clear-cover` removes both. Verified against a real staging homeserver end to end — see the #222 section of journeys/RESULTS.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye
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.
Closes #222.
loopkycould create a deck and edit every card in it, but nothing about the deck. The only workaround wasdeck delete+deck create --from-file, which mints a new deck id — everypubky://…/decks/<id>/link breaks, every card goes back to new in the scheduler — and which is impossible for a deck built withcard add, since there is no file to recreate it from.One
updateMetadatacall — the manifest and the tag records, never the chunks — so it costs the same on a 20k-card deck as on an empty one.The rules, and why each one is the surface's own
card edit's rule; an explicitly empty value clears it (--description=, which is--back=).--clear-tags/--clear-coverare switches because those two are not single values:--tag ""would have to mean both "no tags" and "one blank tag", and a cover is an emoji and an image that layer.--tagreplaces rather than appends, as the issue proposed. Appending makes "the tags are exactly these" unsayable without a delete, and makes a retry after the hourly session expiry (Every homeserver write dies on /session with a raw transport error, and publish reports it as "check your connection" #165) grow its own list — the same propertycard add's dedupe exists for.updated_at, which is what every follower's "the author published changes" badge reads, so a re-run reportschanged: falseand leaves the record alone.spanish;--clear-tagson a language deck stays empty, because those are ordinary author-removable tags rather than a reserved family (Architecture §7.7).--jsongets{deck, changed, fields}—fieldsnames what actually moved, diffed against the stored deck rather than read off which flags were typed, so it is something a caller can verify against.Two answers to the issue's open questions
The study-mode toggles are in, not split out. Turning one off costs no progress: review state is keyed by
card_idalone and the modes decide only how a card is presented. Reverse's pairing is session-local and persisted nowhere, so even that one has nothing to lose.--cover-urland--cover-emojiare not mutually exclusive, and they are not exclusive indeck createeither. They are separate manifest fields, andDeckTiledraws the image over the emoji — the emoji is the fallback when the image fails to load. Refusing the pair would have broken that. Both are settable in one call;--clear-coverremoves both halves.One thing this deliberately does not fix:
deck create --front-lang es-ESstill writes nospanish/languagelabel, because it never did. That is a separate gap and changing an existing command's behaviour did not belong here.Verification
./gradlew detektAll :cli:testgreen; 15 new tests inDeckEditTestcovering the overlay, the two clear gestures, tag replacement, the retag, the no-op write and every refusal.Then driven against a real staging homeserver on the live session, on a throwaway deck created and deleted in the same run, with two cards added partway through. Full table in
journeys/RESULTS.md; the load-bearing rows:changed: false,updated_atunmoved — no second writecard_count: 2, chunk table unchanged,card listintactdeck showin a fresh process--back-lang fr-FRspanishdropped,frenchadded, the author's own tags kept--clear-tagson a language deck[]— the derived labels are not put back--cover-url http://…--clear-tags --tag x,--title=,--clear-cover --cover-emoji🤖 Generated with Claude Code
https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye