Skip to content

Every letter belongs to the graph - #1

Merged
rvaccone merged 1 commit into
mainfrom
feat/every-letter-belongs-to-the-graph
Aug 19, 2026
Merged

Every letter belongs to the graph#1
rvaccone merged 1 commit into
mainfrom
feat/every-letter-belongs-to-the-graph

Conversation

@rvaccone

Copy link
Copy Markdown
Contributor

A verb sitting on a letter costs an address forever, and an address is the
scarcer thing. Three verbs were doing it. All three move off, and
Gestures.reservedLetters is now empty — the whole alphabet belongs to the
graph, and is meant to stay that way.

The key map

verb was now why
attention timeline lode X retired least-used verb in the instrumentation, by more than two orders of magnitude against the address it blocked
layout undo / redo lode Z / ⇧Z lode ← / lode → Layout.swift already calls it "one global timeline"; walking one is what arrows mean
flip orientation lode O lode \ already moved in 0.14.2 — the roster just never heard

Undo becomes a two-key pair like [ / ] rather than needing shift, and
costs no punctuation: `, -, and = all stay free. Since lode is
right-command it only shadows right-⌘←, which is not how anyone types
⌘←.

Retiring the timeline takes FocusHistory with it — it had no other caller,
and it was recording every focus change in the process.

One definition of the reserved set

It had been restated in six places, and each of the last three key moves
left copies behind:

before after
Gestures.reservedLetters [] (the only definition)
Gestures.graphLetters filtered o, x, z derives from the above
Gestures.roster flip-orientation keys: ["o"] keys: ["\\"]
Gestures.roster layout-undo keys: ["z"] keys: ["left", "right"]
Config.reservedTopLevel ["z", "x"] reads Gestures
Advisor.reservedLetters ["x", "z"] reads Gestures
cheat sheet GuideRow key: "X" (row removed)

Bugs this fixes

  1. The flip-orientation toggle was wired to the wrong key. 0.14.2 moved
    the verb from O to \ in the engine but not in the roster. Because the
    engine disables by key, flip-orientation: false disabled lode O
    breaking an unrelated graph address — while lode \ kept flipping. The
    verb could not be turned off, and turning it off broke something else.

  2. The cheat sheet taught a key the engine did not dispatch. The verb list
    in cheatSections() is hand-written, so it drifts silently.

  3. mnemonicLetters dropped every parenthesised word. It split on spaces
    and took each word's first character, so "(xonar)" contributed nothing.
    Lodestar generates these names itself (Graph.Target.label renders a
    profile as "Brave (Xonar)"), so the distinguishing half of every
    browser-profile name was invisible and no profile could ever be offered a
    shorter address. "1Password" was hidden behind its digit the same way.

  4. shorten could not clear its own probability gate. It priced
    uncertainty as perUseSavedSD = residualSD * saved — a fixed multiple of
    the saving — so the signal-to-noise ratio was always 1 / residualSD.
    With learningCost == 0 the Monte Carlo condition collapses to
    saved > 0, whose probability is Φ(1 / residualSD): a constant,
    independent of the size of the saving and of how often the chain is
    used.

    saving=0.05s/use -> p=0.8497
    saving= 0.2s/use -> p=0.8497
    saving= 2.0s/use -> p=0.8497   (identical)
    

    At residualSD = 0.966 that is 0.850 against a 0.9 gate, so no
    shorten could ever be offered, at any usage. The saving is a difference
    of two means, so it is now priced by how well those means are known: the
    chain being replaced tightens as √n, while the proposed chain keeps the
    full population spread because it has never been typed. bindCandidates
    already did this correctly via record.commit.se.

  5. The Monte Carlo was not replayable, despite saying it was.
    Advisor.Random is documented as "Deterministic LCG so the Monte Carlo is
    replayable in tests" but was seeded from UInt64(abs(key.hashValue)), and
    Swift randomises hashValue per process — three runs of "b x".hashValue
    give three different values, so every run drew a different sample and any
    candidate near p = 0.9 would appear and vanish between runs. Now seeded
    from a stable FNV-1a hash, which is also total where abs(Int.min) would
    have trapped.

  6. The test that should have caught bug 1 could not.
    testKeysAreDisjointAndCoverTheIdleDispatch claimed to check "every
    idle-state key the engine dispatches" but hardcoded the expected set, so it
    drifted in lockstep with the roster and actively asserted the stale mapping
    was correct. It now asks the engine — pressing every key in Keys.ansi
    and treating anything that is not .passThrough as claimed — and compares
    that against the roster.

Label is not target

Caught by review, and a regression this change introduced.
shortenCandidates built its edit from leaf.label — for a browser
profile, the rendered "Brave (Xonar)" rather than the bindable
"brave:xonar". It was unreachable while mnemonicLetters could only ever
yield b for a profile leaf; fixing the mnemonics is precisely what made
it reachable. Accepting the headline offer would have gone
AppIndex.entry(named: "Brave (Xonar)") → no exact match → Fuzzy.rank
plain Brave, profile dropped in silence.

  • GraphTarget.configValue sits beside label, so the two strings are
    named and documented rather than conflated.
  • Advisor.Leaf replaces the (chain:label:) tuple and carries both.
  • ConfigEdit.bindApp.bindTarget, because the edit holds what a config
    line writes.
  • Recommendation.display (optional) lets the chip show
    lode X → Brave (Xonar) while committing brave:xonar — a chip reading
    lode X → brave:xonar would quote the machinery at someone who only asked
    for their browser.
  • addTargetToGraph binds a profile reference as written and checks the
    profiles registry, rather than sending it through the app index.

shorten had no test at all before this. It does now, and it fails
against the old behaviour — verified by reintroducing the bug.

Result

The first actionable recommendation the coach has ever produced on real
data:

worth looking at
  lode B X fires 547×/week · it has earned lode X   (≈116s/week, p 1.00)
  lode V Z fires 44×/week · it has earned lode Z    (≈18s/week, p 1.00)

Both carry a ConfigEdit, so they are offerable rather than report-only, and
the first clears the coach's 30s/week debut floor by nearly 4×. Reproducible
across consecutive runs.

Testing

swift test — 518 tests, 0 failures. Six FocusHistory tests retired with
the feature; three added (the profile-shorten regression, the engine-derived
dispatch check, and the whole-alphabet check). Updated every case that
encoded the old reserved set — GesturesTests, StarterGraphTests,
EngineTests, AnalysisTests, ConfigBuildTests, ModifierTapTests.

Config.parse still refuses a graph binding on a reserved letter. With an
empty set it has nothing to refuse, which is deliberate: it stays as a guard
in case a verb ever moves back onto a letter, and its message no longer names
particular letters.

Pre-0.17 configs carry a gestures.back-forward: switch for a verb that no
longer exists; ConfigDefaults.normalized absorbs it the same way it
absorbed the searcherlauncher rename, so old files do not start
reporting an unknown key.

Three verbs sat on letters. A verb on a letter costs an address forever,
and an address is the scarcer thing — so all three move off, and
Gestures.reservedLetters is now empty and meant to stay that way.

  - The attention timeline is retired. Over the same window of real use it
    was the least-used verb in the instrumentation by more than two orders
    of magnitude against the address it was blocking, and it is largely
    redundant once the graph is fluent: lode G is as fast as a back-jump and
    says where it lands. FocusHistory goes with it.
  - Layout undo/redo moves from lode Z to lode ← / lode →. Layout.swift
    already calls it "one global timeline", and walking a timeline is what
    the arrows mean. It becomes a two-key pair like [ and ] rather than
    needing shift, and costs no punctuation: `, -, and = all stay free.
  - O rejoined the graph back in 0.14.2 when orientation moved to \.

The reserved set had been restated in six places, and each of the last
three key moves left copies behind:

  - Gestures.roster still bound flip-orientation to "o" after the verb
    moved to "\". Because the engine disables by key, setting
    flip-orientation: false disabled lode O — breaking it as an address —
    while lode \ went on flipping. The toggle was wired to the wrong key,
    and turning it off broke something unrelated.
  - Gestures.graphLetters still filtered "o" out of the chain starters.
  - The cheat sheet still drew a row reading "X  back · ⇧X forward".

There is now one definition, which Config.reservedTopLevel and
Advisor.reservedLetters read instead of restating.

Three advisor fixes come with it:

  - mnemonicLetters split names on spaces and took each word's first
    *character*, so a word starting with a non-letter contributed nothing.
    Lodestar writes those names itself — Graph.Target.label renders a
    browser profile as "Brave (Xonar)" — so the distinguishing half of
    every profile name was invisible, and no browser profile could ever be
    offered a shorter address. "1Password" was hidden behind its digit the
    same way.
  - shortenCandidates priced uncertainty as residualSD * saved, a fixed
    multiple of the saving, which made P(saves time) equal Φ(1/residualSD)
    for every candidate alike — a constant, unmoved by the size of the
    saving or by the evidence behind it. The saving is a difference of two
    means, so it is now priced by how well those means are known: the
    replaced chain tightens as √n, the proposed chain keeps the population
    spread because it has never been typed.
  - Advisor.Random promised a "deterministic LCG so the Monte Carlo is
    replayable in tests" but was seeded from hashValue, which Swift
    randomises per process — every run drew a different sample. Seeded from
    a stable FNV-1a hash instead, which is also total where abs(Int.min)
    would have trapped.

A code review of the above caught one more, and it was a regression this
change introduced: shortenCandidates built its edit from leaf.label, which
for a browser profile is the rendered string "Brave (Xonar)" rather than
anything bindable. That was unreachable while mnemonicLetters could only
ever produce "b" for a profile leaf — fixing the mnemonics is what made it
reachable. Accepting the offer would have run "Brave (Xonar)" through
AppIndex.entry(named:), missed the exact match, fuzzy-ranked its way to
plain Brave, and bound the wrong thing without a word about it.

GraphTarget now exposes configValue beside label, Advisor.Leaf carries
both, and ConfigEdit.bindApp becomes bindTarget so the edit holds what a
config line writes. Recommendation gains an optional display so the chip
can still say "lode X → Brave (Xonar)" while committing "brave:xonar".

Together these produce the first actionable recommendation the coach has
ever had on real data: lode B X (547×/week) has earned lode X, ≈116s/week
at p 1.00.
@rvaccone
rvaccone merged commit 5dd5a63 into main Aug 19, 2026
1 check passed
@rvaccone
rvaccone deleted the feat/every-letter-belongs-to-the-graph branch August 19, 2026 01:56
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