Skip to content

fix(state): never drop manual flags — non-destructive sync, scoped accept-edits - #4

Merged
mhenrixon merged 2 commits into
mainfrom
fix/state-churn-preserve-manual
Jul 16, 2026
Merged

fix(state): never drop manual flags — non-destructive sync, scoped accept-edits#4
mhenrixon merged 2 commits into
mainfrom
fix/state-churn-preserve-manual

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Problem

In consuming-app PRs (e.g. getzazu/app#2918), locallingo churned state unrelated to the PR: manual: true removed from hand-curated keys, added to unrelated namespaces, inconsistently across PRs.

This was not a version regression — the core files were unchanged since the bin/translate extraction (b007b34). Three extraction-era flaws interacted:

  1. lingo sync destroyed state. It replaced every target entry with {source_hash} only, wiping target_hash + manual: true app-wide. The pre-extraction bin/translate sync preserved both (entry["manual"] = true if locale_state.dig(key, "manual")); the extraction dropped that line. zazu auto-runs bin/lingo sync from a Claude Code Stop hook whenever locales change, so the wipe fired silently in unrelated PRs.
  2. translate --force-key dropped manual when rewriting the entry — and the outdated validator's suggestion text told agents to run exactly that command.
  3. accept-edits blanket-stamped manual: true on every key of every namespace, and the manual-edits validator suggested that blanket command for a single drifted key.

Which way flags flipped depended on which command ran last on a branch — hence the "random" churn.

Fix

  • sync_state! merges: refreshes source_hash, preserves target_hash/manual. Hand-edit drift stays visible until explicitly accepted (deliberate divergence from the old script, which silently absorbed edits unprotected).
  • update_locale_state carries manual: true through retranslation. No command removes the flag anymore — unprotecting requires editing the state JSON by hand.
  • accept-edits scoped: unscoped accepts exactly the keys the manual_edits validator flags; --key KEY (repeatable) for surgical accepts; --all for the old blanket (initial adoption).
  • StateStore#save skips byte-identical files — unrelated namespace files are never rewritten.
  • Validator suggestions are key-scoped and safe to follow verbatim; outdated on a manual key now says hand-update + re-accept instead of pushing machine translation over curated text.
  • Docs (commands, drift-state, validators), README, CHANGELOG updated. No version bump — rake release[x.y.z] owns it.

Verification

  • 98 examples, 0 failures (13 new specs covering sync preservation, force-key flag survival, the accept-edits matrix, suggestion texts, and byte-identical unrelated files); RuboCop clean on changed files.
  • E2E against real zazu state (Gemfile path override, restored after):
    • bin/lingo sync on a clean tree: zero fr/af churn (previously stripped thousands of manual/target_hash lines); only legitimate additions of previously untracked en hashes.
    • Hand-edit one fr value → validate --strict flags it with the scoped suggestion → running that suggestion verbatim changed exactly one entry in one file, everything else byte-identical.

Follow-up in getzazu/app (after release)

Bump the gem, update agent docs referencing blanket accept-edits to the --key form, and optionally reconsider the Stop hook (sync, even fixed, masks outdated detection for en-only edits made without translating in the same turn).

…s, accept-edits scoped

Three extraction-era flaws made .i18n-state churn unrelated keys across
consuming-app PRs:

- sync_state! replaced every target entry with {source_hash} only, wiping
  target_hash and manual: true app-wide (the pre-extraction bin/translate
  preserved both). Sync now merges: it refreshes source_hash and leaves
  hand-edit protection untouched.
- translate --force-key rewrote the state entry without the manual flag —
  and the outdated validator's suggestion pushed agents to run exactly that.
  The flag now survives retranslation; no command removes manual: true.
- accept-edits stamped manual: true on every key of a locale. Unscoped it
  now accepts only the keys the manual_edits validator flags; --key KEY
  (repeatable) accepts specific keys; --all keeps the blanket for adoption.

StateStore#save also skips byte-identical files so unrelated namespace
files are never rewritten, and validator suggestions are key-scoped so
they are safe to follow verbatim.
@mhenrixon
mhenrixon merged commit 00c7c20 into main Jul 16, 2026
3 checks passed
@mhenrixon
mhenrixon deleted the fix/state-churn-preserve-manual branch July 16, 2026 16:26
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