uttrflow / uttrflow/uttrflow-swift

Design: two application tables that disagree, and a destination formatter the model path never reads

Open
#231 0 comments 0 reactions 0 assignees View on GitHub
area:dictation enhancement P1
Dominant language
Swift
Stars
4
Forks
17
Avg merge
3h 32m
Merged PRs (30d)
277

Description

**P1 — design. There are two tables of applications and they disagree, and the formatter they select is bypassed on the path that does the formatting.**

`Docs/cleanup-design.md` §1–2 describes one table: `DestinationRules.standard` maps a bundle
identifier prefix or window title to a `Destination`, and `DestinationFormatter.registry` holds
one policy value per destination. The promise is that adding an application is a row, and adding
a decision is a case on an enum. #203, #201, #209 and #204 are what that promise costs today.

### Three separate failures of the same table

**The table is partial, and a second table knows more than it does.** `AppKind` already
enumerates applications that `DestinationRules` has no row for — Signal, Sublime Text, Notion,
Spark, Warp, and the Electron build of WhatsApp among them (#203). They fall to `.plain`, which
carries `terminalStop: .always`, `grammar: .repair` and a layout with no `.lists`. So "on my way"
into Signal gets a full stop that the same words in Slack correctly do not; a line of code
dictated into Sublime Text gets a full stop and a grammar repair; a spoken list in Notion is
never laid out. Two tables that answer the same question and disagree is the defect, not the
missing rows.

**The formatter is selected and then not used.** `GenerativeTextTransformer` builds its
pre-model pipeline from a stored constant made for `.plain`, so the destination's `NumberPolicy`
never reaches the model path (#201). A spreadsheet gets `12000` from the model where the passes
wrote `12,000` — which `Docs/cleanup.md` already records as a corpus failure. The registry is
consulted for the rules path and ignored for the model path, so the same dictation is formatted
by two different policies depending on which engine answered.

**A decision that should be a policy is inferred from text.** Digit grouping is read off the
preceding context word rather than off the destination, so a SQL or code editor is given
`12,000` (#209); `InsertionPoint.sentenceState` reads one character, so a caret after a bullet,
a bracket, a quote or a heading marker is called mid-sentence (#204). Both are the destination's
question being answered by a local heuristic.

### The target shape

**One table, and let the compiler prove it is total.** Make `Destination` derivable from
`AppKind` — one exhaustive `switch` with no `default`, so adding an application to `AppKind`
fails to build until its destination is stated. The bundle-identifier prefix and window-title
matching stays where it is; what goes away is the possibility of an application known to one
table and absent from the other. `Docs/cleanup-design.md` §9 already says a new application
should be a row; this is what makes that true.

**One formatter, reached by every path.** The transformer must take the `DestinationFormatter`
for the situation rather than holding a constant. Build the pre-model pipeline from the same
registry value the rules path uses, so there is exactly one answer to "what does this place
want" and both engines read it. That closes #201 and removes the class: a policy added to the
registry later cannot silently apply to one path and not the other.

**Policies, not inferences.** Digit grouping becomes a `GroupingPolicy` on the formatter
alongside `NumberPolicy` (#209). `sentenceState` takes the caret's surrounding text rather than
one character, with the list, quote and heading markers named (#204). Each is a case on an enum
and a value per destination, which is the shape the design already chose.

### Principles

DRY, in its literal sense: one authoritative table of what an application is, instead of two
that drift. Open/closed: a new application is a row and a new decision is a case, and neither
edits behaviour anywhere else. Dependency inversion: the transformer should depend on the
formatter abstraction it is given, not on a constant it constructs. KISS: an exhaustive switch
is simpler than a lookup that can miss, and it fails at build time rather than in someone's chat
window. Single responsibility: inferring a destination's preference from nearby text puts two
jobs in one pass.

### Sequencing

Totality first — derive `Destination` from `AppKind` and fill the rows, which is mechanical and
closes #203 on its own. Then the formatter injection into `GenerativeTextTransformer` (#201),
which needs corpus cases per destination on the model path before it can be measured. Then the
two policy moves (#209, #204). `make bakeoff` per destination before and after, as
`Docs/cleanup-design.md` §10 phase C requires.

---
Design issue for the cluster #203, #201, #209, #204. Traced in the source, not reproduced at
runtime.

Contributor guide

Open the contributing guide

Research direction

Start with Docs/cleanup-design.md §§1–2 and 9–10, then locate AppKind, DestinationRules, DestinationFormatter, GenerativeTextTransformer, and InsertionPoint.sentenceState. Trace both formatting paths and the referenced issues before sequencing the changes. Done means one exhaustive application-to-destination table, one formatter policy used by both engines, the two policy decisions moved out of text heuristics, and a make bakeoff per destination.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
desktop
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.