Skip to content

Stop offering a live spelling imports will refuse - #405

Merged
lamemustafa merged 2 commits into
masterfrom
fix/unimportable-live-spelling
Sep 15, 2026
Merged

lamemustafa merged 2 commits into
masterfrom
fix/unimportable-live-spelling

Conversation

@lamemustafa

Copy link
Copy Markdown
Owner

#400 let a catalogue carry a ledger name holding a control character — a ledger genuinely named across two lines — because a catalogue name is a fact about the book. The proposal side still refuses one, because a caller's name is input. Both rules are right. The gap between them is a spelling the tool can report but the caller cannot send back.

master_recovery_guidance told them to send it anyway:

For identifier-bound entries, copy exact_live_spelling from this fresh result.

Following that does not fail one row. source_entities collects into a single Result and refuses on the first bad name, so one such ledger fails the entire batch with master_name_unsafe — every voucher in the request, including those naming no unusual ledger at all.

The fix

The report now carries importable, and the guidance offers the copy only where copying can work. Where it cannot, it says so and names the one remedy that exists: have an operator rename the ledger in Tally.

importable is answered by calling SourceEntity::new rather than restating what it accepts, so the report and the import path cannot drift apart.

Severity

No corruption was possible — the failure was closed, not silent. What it cost was a caller following the tool's own instructions into a dead end it could not diagnose, with a batch-wide failure pointing at a name it had just been handed as correct.

Provenance

Found by an independent review of #294 that was asked to trace how catalogue names flow into agent_import.rs. It is a consequence of #400 meeting pre-existing code in agent_import.rs that neither #400 nor #294 touched, which is why neither PR's own tests could have caught it.

Verification

  • Both new tests confirmed failing before the fix and passing after.
  • The negative case is covered too: an ordinary identifier match stays importable: true and is still offered for copying.
  • Both workspaces green — src-tauri and tools.
  • reseal.sh --verify exit 0; pin set unchanged at 216 (agent_import.rs is pinned).

🤖 Generated with Claude Code

#400 let a catalogue carry a ledger name holding a control character -- a
ledger genuinely named across two lines -- because a catalogue name is a fact
about the book. The proposal side still refuses one, because a caller's name is
input. Both rules are right, and the gap between them is a spelling the tool can
report but the caller cannot send back.

`master_recovery_guidance` told them to send it anyway: "For identifier-bound
entries, copy exact_live_spelling from this fresh result." Following that does
not fail one row. `source_entities` collects into a single Result and refuses on
the first bad name, so one such ledger fails the **entire** batch with
`master_name_unsafe` -- every voucher in the request, including those naming no
unusual ledger at all.

The report now carries `importable`, and the guidance offers the copy only for
entries where copying can work. Where it cannot, it says so and names the one
remedy that exists: have an operator rename the ledger in Tally.

`importable` is answered by calling `SourceEntity::new` rather than restating
what it accepts, so the report and the import path cannot drift apart.

No corruption was possible -- the failure was closed, not silent. What it cost
was a caller following the tool's own instructions into a dead end it could not
diagnose.

Found by an independent review of #294 that was asked to trace this exact path.
Both tests confirmed failing before the fix and passing after. Both workspaces
green; reseal --verify exits 0 with the pin set unchanged at 216.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@lamemustafa
lamemustafa merged commit 925c2df into master Sep 15, 2026
13 checks passed
@lamemustafa
lamemustafa deleted the fix/unimportable-live-spelling branch September 15, 2026 19:20
lamemustafa pushed a commit that referenced this pull request Sep 15, 2026
#405 resealed pinned files without changing the pin set, so both sides moved
the two manifests and they conflicted. Pin lists compared by path: master's 216
is a strict subset of this branch's 217, the difference being this branch's own
addition, and #405 changed hashes only. Resolved by taking this branch's list
and running scripts/reseal.sh, which recomputes every hash from the merged tree
rather than hand-merging one. Exactly one hash moved, and no path is missing
versus either side.

reseal.sh --verify exit 0, read directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lamemustafa added a commit that referenced this pull request Sep 15, 2026
* Refuse a proposed name that reads as a different master

`char::is_control` is Unicode category Cc. Bidi overrides and zero-width
characters are Cf, so every one of them walked straight through the proposal
side's control-character check: `U+202E`, `U+200B`, `U+061C` and `U+FEFF` all
answer `false`.

The catalog side refuses them and this side did not, which is the wrong way
round. A name read from a book is a fact about the book. A name a caller
supplies is the one that can be *chosen* so that what a reviewer sees is not
the master it binds.

#400 added the catalog-side refusal and its doc comment claimed these
characters "stay refused on both sides". That was false when I wrote it -- I
asserted a property of a function I had not changed. This makes the claim true
rather than deleting it.

The test asserts the premise before the conclusion: for each character it first
checks `char::is_control` is false, so the test fails loudly if a future Rust
release reclassifies one and the coverage silently becomes vacuous. It also
pins what must keep working -- an ordinary name, and the two-line catalog name
#400 deliberately admits.

Found by an independent review of #405, raised as suspected and unverified;
confirmed by running `char::is_control` over the set.

Gate: both workspaces green, clippy silent on both, reseal --verify exits 0
with the pin set unchanged at 216.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Stop treating Indic orthography as deception

`U+200B..=U+200F` is not a set of deceptive characters. It contains ZWNJ
(U+200C) and ZWJ (U+200D), which Devanagari and other Indic scripts use to
prevent or force a conjunct. They are ordinary spelling, and this repository's
own fixtures are full of Indic ledger names.

Refusing them failed the **whole** catalog on a legitimately spelled Hindi or
Marathi ledger -- the exact failure the newline fix existed to remove, on a
book type Indian CA practice is full of.

**This was a regression I introduced.** Before #400, `MasterCatalog::new`
checked only `char::is_control`, which is category Cc and admits ZWNJ. #400
added the deceptive-set check to the catalog side and took the whole
`U+200B..=U+200F` span with it, so a Devanagari name that worked before stopped
working. The protocol crate refused it before and after; only the core is a
regression.

The set is narrowed rather than abandoned: `U+200B`, `U+200E` and `U+200F`
carry no orthographic role and stay refused, alongside the override, isolate
and BOM ranges. The test pins both directions -- ZWNJ and ZWJ admissible on
both sides, their neighbours in the same span still refused -- so this cannot
be widened back by accident.

Prompted by asking an independent reviewer to check exactly this before
merging, rather than after.

Gate: both workspaces green, clippy silent on both, reseal --verify exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Reseal after merging master

Took master's pin list, which is a superset of this branch's at 217 to 216 --
it adds the scheduled audit workflow and this branch adds nothing. Sets
compared as paths against both parents; none dropped from either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Name the trade-off, and cover the crate that actually reads books

Two gaps an independent review found, both fair.

The protocol crate carries its own copy of the deceptive set and it is the one
on the real read path -- `observed_standard_ledger_name` runs on every name a
book returns -- yet the narrowing landed there with no test at all. It has one
now, driven to failure by restoring the wide range in that crate alone.

And the predicates now say what admitting ZWNJ costs. A codepoint filter cannot
tell a joiner between two Devanagari consonants from one injected into ASCII,
so `Alpha<ZWJ> Traders` is byte-distinct from `Alpha Traders` and renders the
same. The guarantee is bounded rather than closed: the fold and the token index
keep the joiner verbatim, so such a name tends to fail exact and token matching
rather than quietly aliasing a real master. Worse than refusal, far better than
breaking every Indic book.

One claim withdrawn: the `char::is_control` premise assertion does not guard
against the refusal going vacuous -- `unwrap_err()` already panics on an
unexpected `Ok`. It guards against Unicode reclassifying a codepoint out from
under the comment, which is narrower than I said.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Reseal after merging master

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: t <dev@example.invalid>
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