Every letter belongs to the graph - #1
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.reservedLettersis now empty — the whole alphabet belongs to thegraph, and is meant to stay that way.
The key map
lode Xlode Z/⇧Zlode ←/lode →Layout.swiftalready calls it "one global timeline"; walking one is what arrows meanlode Olode \Undo becomes a two-key pair like
[/]rather than needing shift, andcosts no punctuation:
`,-, and=all stay free. Sincelodeisright-command it only shadows right-⌘←, which is not how anyone types
⌘←.
Retiring the timeline takes
FocusHistorywith 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:
Gestures.reservedLetters[](the only definition)Gestures.graphLetterso, x, zGestures.rosterflip-orientationkeys: ["o"]keys: ["\\"]Gestures.rosterlayout-undokeys: ["z"]keys: ["left", "right"]Config.reservedTopLevel["z", "x"]GesturesAdvisor.reservedLetters["x", "z"]GesturesGuideRowkey: "X"Bugs this fixes
The
flip-orientationtoggle was wired to the wrong key. 0.14.2 movedthe verb from
Oto\in the engine but not in the roster. Because theengine disables by key,
flip-orientation: falsedisabledlode O—breaking an unrelated graph address — while
lode \kept flipping. Theverb could not be turned off, and turning it off broke something else.
The cheat sheet taught a key the engine did not dispatch. The verb list
in
cheatSections()is hand-written, so it drifts silently.mnemonicLettersdropped every parenthesised word. It split on spacesand took each word's first character, so
"(xonar)"contributed nothing.Lodestar generates these names itself (
Graph.Target.labelrenders aprofile as
"Brave (Xonar)"), so the distinguishing half of everybrowser-profile name was invisible and no profile could ever be offered a
shorter address.
"1Password"was hidden behind its digit the same way.shortencould not clear its own probability gate. It priceduncertainty as
perUseSavedSD = residualSD * saved— a fixed multiple ofthe saving — so the signal-to-noise ratio was always
1 / residualSD.With
learningCost == 0the Monte Carlo condition collapses tosaved > 0, whose probability isΦ(1 / residualSD): a constant,independent of the size of the saving and of how often the chain is
used.
At
residualSD = 0.966that is0.850against a0.9gate, so noshortencould ever be offered, at any usage. The saving is a differenceof 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 thefull population spread because it has never been typed.
bindCandidatesalready did this correctly via
record.commit.se.The Monte Carlo was not replayable, despite saying it was.
Advisor.Randomis documented as "Deterministic LCG so the Monte Carlo isreplayable in tests" but was seeded from
UInt64(abs(key.hashValue)), andSwift randomises
hashValueper process — three runs of"b x".hashValuegive three different values, so every run drew a different sample and any
candidate near
p = 0.9would appear and vanish between runs. Now seededfrom a stable FNV-1a hash, which is also total where
abs(Int.min)wouldhave trapped.
The test that should have caught bug 1 could not.
testKeysAreDisjointAndCoverTheIdleDispatchclaimed to check "everyidle-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.ansiand treating anything that is not
.passThroughas claimed — and comparesthat against the roster.
Label is not target
Caught by review, and a regression this change introduced.
shortenCandidatesbuilt its edit fromleaf.label— for a browserprofile, the rendered
"Brave (Xonar)"rather than the bindable"brave:xonar". It was unreachable whilemnemonicLetterscould only everyield
bfor a profile leaf; fixing the mnemonics is precisely what madeit 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.configValuesits besidelabel, so the two strings arenamed and documented rather than conflated.
Advisor.Leafreplaces the(chain:label:)tuple and carries both.ConfigEdit.bindApp→.bindTarget, because the edit holds what a configline writes.
Recommendation.display(optional) lets the chip showlode X → Brave (Xonar)while committingbrave:xonar— a chip readinglode X → brave:xonarwould quote the machinery at someone who only askedfor their browser.
addTargetToGraphbinds a profile reference as written and checks theprofiles registry, rather than sending it through the app index.
shortenhad no test at all before this. It does now, and it failsagainst the old behaviour — verified by reintroducing the bug.
Result
The first actionable recommendation the coach has ever produced on real
data:
Both carry a
ConfigEdit, so they are offerable rather than report-only, andthe first clears the coach's 30s/week debut floor by nearly 4×. Reproducible
across consecutive runs.
Testing
swift test— 518 tests, 0 failures. SixFocusHistorytests retired withthe 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.parsestill refuses a graph binding on a reserved letter. With anempty 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 nolonger exists;
ConfigDefaults.normalizedabsorbs it the same way itabsorbed the
searcher→launcherrename, so old files do not startreporting an unknown key.