Skip to content

fix(cli): label a deck with the language it declares - #226

Merged
jvsena42 merged 2 commits into
mainfrom
feat/cli-language-tags
Sep 4, 2026
Merged

fix(cli): label a deck with the language it declares#226
jvsena42 merged 2 commits into
mainfrom
feat/cli-language-tags

Conversation

@jvsena42

@jvsena42 jvsena42 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Closes #225.

What was wrong

deck create and import accepted --front-lang/--back-lang, wrote them into the manifest, and stopped there. The ordinary tags a declared pair is supposed to contribute — "spanish", plus the "language" umbrella (LanguageTags, Architecture §7.7 point 4) — were never derived.

$ loopky deck create --env staging --title "lang tag probe" \
    --front-lang en-US --back-lang es-ES --listen --speak --json
tags: []
pair: en-US -> es-ES

The labels are the entire reason the pair contributes a tag at all: a tag record is the only thing Loopky publishes that a network-wide index can answer questions about. So a deck imported with loopky import deck.apkg --front-lang ja-JP --back-lang en-US was invisible to tag browse, to tag trending and to anyone on Nexus looking for Japanese decks — while the byte-identical deck published from a phone was not, because PublishDeckViewModel and DeckEditorViewModel both route a language pick through LanguageTags.retag. Only the headless path skipped it.

Nothing reported it. The deck published, --json said ok, deck show printed the pair, and the only symptom was a search that came back empty somewhere else entirely. Bulk .apkg import is the job the CLI was built for (#46), which made this the path where the most decks arrived and the least were discoverable.

What changed

Every path that sets a pair now goes through LanguageTags where the apps go through it on the pick:

  • Decks.ktdeckCreate
  • Import.ktnewDeck
  • Import.ktoverlaidWith, the --resume overlay — which also has to drop the previous pair's labels, since a resumed run can move the pair

deck edit already did it, but only when the pair actually moved. It now reconciles whenever the invocation names a pair. That is the answer to the first thing the issue left open:

Existing decks. Anything the CLI has already published with a pair is missing its labels and will stay missing until someone edits it.

Restating the pair a deck already has is the repair, so the workaround the issue described — retype the deck to a different region of the same language and back — is gone rather than documented. Reconciling nothing is still not a write, because changedFieldsFrom diffs.

And the second:

The interaction with deck edit --tag. --tag replaces the set, deliberately (#222).

It still does, and the two commands now agree out loud: naming a pair reconciles the labels, naming none leaves them alone. So --tag verbs on a language deck drops them, --clear-tags really does empty the set, and passing the pair alongside either one puts them back. They are ordinary author-removable tags, not a reserved family, so those gestures have to be believed.

Not all decks are language decks, and that is the invariant with the most tests on it: LanguageTags.forPair returns nothing for a deck that declares no pair, umbrella included, so "language" never lands on a deck of capital cities.

Verification

11 new tests in DeckLanguageTagsTest across all four paths, plus a run against live staging with a real session, recorded in journeys/RESULTS.md:

Step Result
The issue's own probe, re-run [language, english, spanish] — was []
deck show in a fresh process on the homeserver, not just in the cache
deck create --tag geografia, no pair [geografia] — no umbrella
deck edit --clear-tags on a language deck []
deck edit restating the pair it already had labels return
import --tag core --front-lang ja-JP --back-lang en-US [core, language, japanese, english]
import --resume --back-lang pt-BR english dropped, portuguese added
import --resume with no flags unchanged, no metadata write
tag trending --env staging language/english/portuguese/spanish rank on Nexus

Three throwaway decks, created and deleted in the same run. ./gradlew :cli:test :shared:jvmTest detektAll green. No UI touched, so no journeys/*.xml needed re-running.

🤖 Generated with Claude Code

https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye

jvsena42 and others added 2 commits September 4, 2026 08:12
…hat sets one

`deck create` and `import` wrote `--front-lang`/`--back-lang` into the manifest and
stopped there, so the ordinary tags a declared pair is supposed to contribute —
`"spanish"`, plus the `"language"` umbrella — were never derived. A deck published
with `--front-lang ja-JP` was invisible to tag browse, to `tag trending` and to
anyone on Nexus looking for Japanese decks, while the byte-identical deck published
from a phone was not: both ViewModels route a language pick through
`LanguageTags.retag`, and only the headless path skipped it.

Nothing reported it. The deck published, `--json` said ok, and the only symptom was
a search that came back empty somewhere else entirely. Bulk `.apkg` import is the
job the CLI was built for (#46), which made this the path where the most decks
arrived and the least were discoverable.

All three write paths now go through `LanguageTags` where the apps go through it on
the pick — `deckCreate`, `import`'s `newDeck`, and the `--resume` overlay, which
also has to *drop* the previous pair's labels since a resumed run can move the pair.

`deck edit` already did it, but only when the pair actually moved, which left no way
to repair a deck already published without labels short of retyping it to a
different *region* of the same language and back. It now reconciles whenever the
invocation **names** a pair, so restating the one a deck already has is the repair —
and reconciling nothing is still not a write, because `changedFieldsFrom` diffs.
Naming no pair leaves the labels entirely alone, so `--clear-tags` on a language
deck still empties it: these are ordinary author-removable tags, not a reserved
family, and that gesture has to be believed.

A deck that declares no pair gets nothing, umbrella included — most decks are not
language decks, and `"language"` on a deck of capital cities would be a lie the
index repeats.

Closes #225

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye
…ng run

A "Language decks" section in the CLI README with the four rules — a deck with no
pair gets nothing, the labels are ordinary removable tags, retyping swaps them, and
naming the pair reconciles them (which is the repair for a deck published before
this existed). Architecture §7.7 point 4 gains the headless paths beside the
ViewModels, and §13.7's `deck edit` bullet is corrected from "only when the pair
moves" to "whenever the invocation names a pair".

`journeys/RESULTS.md` records the run against live staging: the issue's own probe
re-run, the read-back in a fresh process, the non-language deck, `--clear-tags`
still emptying the set, and the resume overlay both moving and not moving the pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye
@jvsena42
jvsena42 merged commit 807a0b5 into main Sep 4, 2026
4 checks passed
@jvsena42
jvsena42 deleted the feat/cli-language-tags branch September 4, 2026 11:14
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.

A CLI-created language deck carries no language tags, so nothing can find it

1 participant