Skip to content

fix: the emergency 'Default' alphabet was selectable and dead (#88) - #88

Merged
willwade merged 6 commits into
mainfrom
fix/default-alphabet-dead
Sep 11, 2026
Merged

willwade merged 6 commits into
mainfrom
fix/default-alphabet-dead

Conversation

@willwade

@willwade willwade commented Sep 10, 2026 •

Copy link
Copy Markdown

Live-reported bug

"as I drive it I dont see any text go into the output text writing area" — Dasher-GTK, tonight

Root cause, isolated by an engine-level repro on the reporter's actual profile: their settings held AlphabetID="Default" — the bare a–z "last ditch" fallback alphabet that CAlphIO's constructor registered unconditionally. That alphabet is structurally dead: with it selected, the engine renders, reports no error, and commits no output however long you drive (no space, no newline, no groups). Because it was registered like any real alphabet, it appeared in pickers' permitted values and round-tripped through settings.

The editor-contract work was exonerated by the repro: with a fresh profile the same binary drives text into the pane normally.

Fix

  1. CAlphIO no longer eagerly registers "Default" — it disappears from pickers/permitted values. GetInfo falls back to the preferred real alphabet; the emergency alphabet is fabricated (once, lazily) only when nothing at all loaded — the case it was written for.
  2. Realize() heals SP_ALPHABET_ID == "Default" to the loaded default, after the lazy alphabet-load block — pickers and the next settings save hold the healed id. Old settings self-heal on first launch after upgrade.
  3. HandleParameterChange(BP_CONTROL_MODE): null guard. Setting control mode pre-realize was a latent null-deref (no NCManager yet); exposed by the create-flow shift, fixed properly — the NCManager ctor reads the flag, so pre-realize sets still apply at realize.

Two collateral traps dodged (documented in the investigation)

  • Healing inside ChangeAlphabet broke the on-demand alphabet loader and set_string_parameter's stored-value contract (the "English, lower case" comma-normalisation wrinkle: name index vs parsed-name mismatch — worth its own issue).
  • The guard-less control-mode case segfaulted only because the retired registration shifted when the NCManager first exists.

Tests

New regression case in dasher_alphabet_xml_tests: settings naming Default heal to 'English with limited punctuation' and the canonical drive produces output. Full matrix 43/43; clang-format clean.

Frontend follow-ups

  • Dasher-GTK carries the live repro as an engine-contract regression test + submodule bump (PR follows).
  • Reporter's immediate workaround (pre-merge): switch the alphabet picker off "Default" — e.g. "English with limited punctuation".

DCO signed.

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable new defect or outstanding blocking finding remains.

Summary

  • Lazily creates the emergency alphabet only when no real alphabet can be loaded.
  • Resolves indexed alphabet files by their selected exact path.
  • Prevents multiple C API contexts from reading each other’s data directories.
  • Restores accumulated user training at startup without double-loading single-directory configurations.
  • Guards control-mode changes made before realization.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Realize context] --> B[Scan this context's alphabet index]
    B --> C[Load configured alphabet by exact indexed path]
    C --> D{Stored ID is Default?}
    D -- No --> G[Create model and node manager]
    D -- Yes --> E{Preferred alphabet loaded?}
    E -- Yes --> F[Persist preferred real ID]
    E -- No --> H[Load real alphabets from this context's data directory]
    H --> I{Any real alphabet loaded?}
    I -- Yes --> J[Persist first loaded real ID]
    I -- No --> K[Retain last-ditch emergency behavior]
    F --> G
    J --> G
    K --> G
    G --> L[Load bundled training corpus]
    L --> M{User and data directories differ?}
    M -- Yes --> N[Load accumulated user training]
    M -- No --> O[Skip duplicate scan]
Loading

Reviews (5) · Last reviewed commit: "fix: on-demand load honours the indexed ..."

Adaptive appends land in <user_dir>/training_<alphabet>.txt, but the
startup scan walked the data dir only — every split-dir frontend
(Android, GTK, Apple) silently discarded all learning between sessions;
Windows survived only because it passes one dir for both.

- FileUtils::ScanDirectory(parser, pattern, dir): the walk extracted
  from ScanFiles, pointed at an explicit directory instead of the
  process-global data dir; ScanFiles unchanged in behaviour
- FileUtils::IsSameDirectory: equivalent()-based with weakly_canonical
  fallback for not-yet-created dirs
- CDasherInterfaceBase: per-context data dir beside the existing
  per-context user dir (last-create-wins globals owned the training
  scan before; NCManager now reads interface-owned dirs)
- CNodeCreationManager: train from this context's data dir, then its
  user dir when distinct (single-dir setups scan once — never double)
- dasher_capi_version() = 1 so frontends can gate stopgap post-create
  re-imports and avoid double-counting on newer cores

Tests: user-dir training shifts the model; per-context isolation (a
context created after another with training must match the clean
baseline despite the globals pointing elsewhere); single-dir smoke.
43/43 green.

Signed-off-by: will wade <willwade@gmail.com>
Reported live: 'as I drive it I dont see any text go into the output
text writing area'. Root cause chain, isolated by a GTK-side engine
repro: the user's settings held AlphabetID='Default' — the bare a-z
'last ditch' fallback CAlphIO registered unconditionally — and that
alphabet is structurally dead: the engine renders, reports no error,
and commits no output however long you drive.

- CAlphIO no longer eagerly registers 'Default' (it vanished from
  pickers and permitted values). GetInfo falls back to the preferred
  real alphabet; the emergency alphabet is fabricated only when
  nothing at all loaded — the case it existed for
- Realize heals SP_ALPHABET_ID=='Default' to the loaded default AFTER
  the lazy alphabet load (pickers and the next settings save hold the
  healed id; the parameter event rides the normal alphabet-switch
  path). Old settings self-heal on first launch
- HandleParameterChange(BP_CONTROL_MODE): null guard — setting control
  mode pre-realize was a latent null-deref, exposed by the create-flow
  shift (NCManager ctor reads the flag, so pre-realize sets still apply
  at realize)

Regression test: settings naming Default heal to 'English with limited
punctuation' and the canonical drive produces output. 43/43.

Signed-off-by: will wade <willwade@gmail.com>
Comment thread src/DasherCore/DasherInterfaceBase.cpp Outdated
With a custom data dir that has alphabets but NOT the preferred
'English with limited punctuation', GetDefault() returns the literal
'Default' and the heal assignment no-oped back into the dead id. The
heal now: preferred if loaded; else bulk-load the data dir's alphabet
files (ScanFiles basename-glob — the lazy name index is empty at that
point, so listing candidates cannot work) and heal to the first real
id that parsed; else leave SP alone (no loadable alphabets = original
last-ditch behaviour).

New test: custom dir with ONLY alphabet.english.without.punctuation +
settings Default -> heals to 'English without punctuation' and the
canonical drive produces output. 43/43.

Signed-off-by: will wade <willwade@gmail.com>
Comment thread src/DasherCore/DasherInterfaceBase.cpp Outdated
…#88)

The multi-context test exposed more than the flagged bulk-load: the lazy
load block's LoadAlphabetFile and ScanNameIndex ALSO route through
FileUtils' process-global data directory (last-create-wins), so
realizing context A after creating context B loaded B's bundle into A —
A could heal to an alphabet its own data dir does not ship.

- CAlphIO::ScanNameIndex(dir): scans an explicit directory per-context,
  global fallback preserved for non-CAPI hosts
- Realize: index + loadById + the heal's bulk scan all use this
  context's m_dataDir via FileUtils::ScanDirectory (#86); loadById
  basename-normalises index entries (dir-relative paths never match
  ScanFiles' basename patterns)
- New test pins the interleaving: create A(custom)+B(full), realize A
  after B — A heals to its OWN 'English without punctuation', not B's
  preferred

ChangeAlphabet's post-realize on-demand load still routes globally —
single-context-sequential only; same pre-existing class, follow-up.

43/43.

Signed-off-by: will wade <willwade@gmail.com>
Comment thread src/DasherCore/DasherInterfaceBase.cpp Outdated
…low-up on #88)

Same class as the realize-path fix: LoadAlphabetFile scans FileUtils'
process-global data directory (last-create-wins) — under multiple live
contexts an on-demand switch could load the OTHER context's file under
this id. Basename-normalised + ScanDirectory(m_dataDir) with the global
fallback for hosts that never set a data dir.

43/43.

Signed-off-by: will wade <willwade@gmail.com>
#88)

The basename normalisation discarded the index's tier-resolved choice:
with the same filename in multiple directories (user-dir override vs
bundle, v6 vs legacy copy) the basename glob re-parsed every match in
traversal order and the filesystem decided which definition won.
LoadAlphabetById now: indexed path (weakly-canonicalised when relative)
parsed EXACTLY via ScanDirectory's absolute-pattern branch; basename
scan only for the synthesized fallback name; both per-context. Shared
by the realize lazy loader and ChangeAlphabet's on-demand block.

43/43.

Signed-off-by: will wade <willwade@gmail.com>
@willwade
willwade merged commit a0f5da6 into main Sep 11, 2026
15 checks passed
@willwade
willwade deleted the fix/default-alphabet-dead branch September 11, 2026 08:53
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