Compare commits

..

2 Commits

Author SHA1 Message Date
Julien Calixte
fe544ccdd3 refactor(docs): merge quality-house into qfd §3
quality-house.md mirrored qfd.md's §1/§2 catalogues in a separate file,
which silently drifted (H3 cadence read 20 there vs 64 in qfd). Fold the
House diagram, perception scores, and regen notes into qfd.md §3 with no
section renumbering, so inbound #3/#6/#7 anchors still resolve. Delete
quality-house.md; re-point quality-house-empty.md and the docs index.
2026-07-11 17:17:18 +02:00
Julien Calixte
a700412773 docs: record :sync latency budget from hardware measurement
New docs/notes/sync-latency.md breaks down the measured ~16 s cold :sync
(Wi-Fi + SNTP + one TLS push), explains the optimistic-retry handshake saving,
the reconcile/last-writer-wins semantics, and why the rest is near the protocol
floor. Linked from the notes index, the docs index, and the Git row of the root
README.
2026-07-11 17:04:02 +02:00
7 changed files with 780 additions and 704 deletions

View File

@@ -71,7 +71,7 @@ surface (mostly `usb_kbd.rs`) is in [`MEMORY_AUDIT.md`](MEMORY_AUDIT.md).
| UI layer | Custom thin widget layer | Ratatui's API _shape_ without its char-grid terminal model ([ADR-002](docs/adr.md#adr-002-ui-strategy--custom-widgets-on-embedded-graphics-not-ratatui)). |
| Editor core | Custom, in-tree (`src/editor.rs`) | Modal (Normal / Insert / View / Command), motions, operators + text objects. Plain-ASCII buffer until the v0.2 UTF-8 work. |
| USB host | `esp-idf` TinyUSB bindings | Boot-protocol HID; verified on hardware (Spike 4). |
| Git | **libgit2 via `git2`**, built as an esp-idf component with mbedTLS (`firmware/components/libgit2/`) | `gix` was the original pick but can't push over HTTPS — the [ADR-004](docs/adr.md#adr-004-git-implementation--gitoxide-gix) kill-switch fired ([postmortem](docs/postmortems/2026-07-05-spike7-gix-https-push.md)). On-device add → commit → push verified. |
| Git | **libgit2 via `git2`**, built as an esp-idf component with mbedTLS (`firmware/components/libgit2/`) | `gix` was the original pick but can't push over HTTPS — the [ADR-004](docs/adr.md#adr-004-git-implementation--gitoxide-gix) kill-switch fired ([postmortem](docs/postmortems/2026-07-05-spike7-gix-https-push.md)). On-device add → commit → push verified; ~16 s cold-`:sync` [latency breakdown](docs/notes/sync-latency.md). |
| TLS | `mbedtls` via `esp-idf` | GitHub HTTPS with the chain checked against embedded roots; ≈35 KB heap measured during handshake (Spike 6). |
| Auth | HTTPS + GitHub PAT | v0.1 bakes credentials in at build time via `TW_*` env vars; provisioning + at-rest protection is [ADR-011](docs/adr.md#adr-011-credential-provisioning--how-the-pat-reaches-the-device-and-is-protected-at-rest) (open), on-device settings land in v0.9. |
| Filesystem | FAT on SD (`esp_vfs_fat`) | Working copy lives here. Internal LittleFS holds config. |

View File

@@ -21,9 +21,8 @@
| Doc | What's in it |
| --- | --- |
| [`qfd.md`](qfd.md) | Quality Function Deployment — turns user-facing requirements into technical HOWs. |
| [`quality-house.md`](quality-house.md) | The 14 WHATs × 14 HOWs House of Quality, filled in. |
| [`quality-house-empty.md`](quality-house-empty.md) | The same chassis, blank — for re-scoring from scratch. |
| [`qfd.md`](qfd.md) | Quality Function Deployment — turns user-facing requirements into technical HOWs; §3 is the filled 14 WHATs × 14 HOWs House of Quality. |
| [`quality-house-empty.md`](quality-house-empty.md) | The House chassis, blank — for re-scoring from scratch. |
## Bench work
@@ -31,5 +30,5 @@
| --- | --- |
| [`spikes.md`](spikes.md) | Rendering & UX spikes — display/UX risks proved outside the hardware stack. |
| [`postmortems/`](postmortems/README.md) | Bring-up debugging write-ups: what broke, the root cause, and the decisions that came out of it. |
| [`notes/`](notes/README.md) | Longer-form essays on the thinking behind specific choices. |
| [`notes/`](notes/README.md) | Longer-form essays on the thinking behind specific choices — e.g. where the ~16 s cold [`:sync`](notes/sync-latency.md) goes. |
| [`tradeoff-curves/`](tradeoff-curves/README.md) | Cost-vs-knob curves behind chosen defaults — energy, latency, memory. |

View File

@@ -11,3 +11,4 @@
| [`ctrl-g-perceived-latency.md`](ctrl-g-perceived-latency.md) | Durability before delivery — surfacing "commit landed" at ~0.2 s makes the 510 s `Ctrl-G` push feel instant. |
| [`git-sync-images-and-repo-size.md`](git-sync-images-and-repo-size.md) | Why we don't shrink the notes repo — its 153 MB of media is remanso's image CDN, so rewriting history to slim the on-device clone breaks the web app. |
| [`boot-time-budget.md`](boot-time-budget.md) | Where the ~4.3 s to cursor goes — and why the ≤ 3 s v1.0 target is hard: one ~1.9 s full refresh is unavoidable at cold boot, so the splash is nearly free. |
| [`sync-latency.md`](sync-latency.md) | Where the ~16 s cold `:sync` goes — Wi-Fi + SNTP + one TLS push; why optimistic-retry cut a whole handshake, and why the rest is close to the protocol floor. |

View File

@@ -0,0 +1,88 @@
# Sync latency — where the ~16 s cold `:sync` goes
> **Measured 2026-07-11** on hardware, via the `:sync timing —` log line in
> [`firmware::git_sync`](../../firmware/src/git_sync.rs) (`publish_cycle`). A
> **cold** `:sync` (first of a power cycle) is **~16.0 s** power-on of Wi-Fi →
> `push done`; a **warm** one skips the one-time setup and is just the ~10 s
> publish. This note breaks the number down and records why most of it is a
> floor, not a bug.
>
> Notes index: [`README.md`](README.md). Docs index:
> [`../README.md`](../README.md). Why the raw number matters less than it looks:
> [`ctrl-g-perceived-latency.md`](ctrl-g-perceived-latency.md). Energy/keep-Wi-Fi-up
> tradeoff: [`../tradeoff-curves/wifi-auto-sync.md`](../tradeoff-curves/wifi-auto-sync.md).
> Sibling timing note: [`boot-time-budget.md`](boot-time-budget.md).
## The waterfall (cold sync)
From the serial log, first `:sync` after a cold boot
(`… wifi 3654ms, clock 2108ms, tls 304ms, publish(commit+push) 9944ms, total 16012ms`):
| Phase | ~ms | One-time? | Lever |
| --- | ---: | --- | --- |
| Wi-Fi assoc + DHCP | ~3650 | yes (per power cycle) | radio off until first `:sync`; association floor |
| SNTP first sync | ~2100 | yes | varies with NTP RTT (4.2 s the prior run); needed before TLS + commit time |
| TLS trust store install | ~300 | yes | write ~6 KB CA bundle to SD + set libgit2 option |
| **publish** = stage+commit + push | **~9900** | **every sync** | see below |
| **Total** | **~16000** | | |
The three one-time phases (~6.1 s) only pay on the *first* sync of a power cycle —
Wi-Fi, the clock, and the trust store are set up once and reused, so a **warm sync
is just the ~10 s publish**. Publish splits as:
| Sub-phase | ~ms | Note |
| --- | ---: | --- |
| stage + commit | ~3150 | `add_all(["*"])` walking the SD/FAT working tree, then commit to FAT |
| push: TLS handshake | ~2400 | one mbedTLS handshake to github.com |
| push: pack negotiate + upload | ~4400 | tiny delta — cost is negotiation/round-trips, not payload |
## The win: one TLS handshake, not two
The first hardware run (2026-07-11) measured **23.7 s** because it did a
**pre-commit fetch** — a second full TLS handshake plus a ref exchange — on every
sync, to absorb a foreign push before committing. That's ~3 s wasted on a normal
sync (remote unchanged), and it did ~6 s of real work the one time it absorbed a
maintenance commit.
The optimistic-retry rewrite (commit `3386969`) drops it: **push onto the current
tip first**; only if the remote *rejects* the push non-fast-forward do we fetch,
reconcile, and retry. The happy path — what runs ~99 % of the time — is now a
**single** handshake. That took the true normal-cold baseline from ~19 s to
**16.0 s** (and the inflated 23.7 s figure will never recur, since it was the
one-time reconcile).
## Foreign pushes: reconcile-and-replay, last-writer-wins
On a rejected push, `reconcile_onto_origin` fetches origin and does a **mixed**
reset onto it — moving the branch ref + index but leaving the working tree, so the
just-saved note survives — then `stage_and_commit` replays the note on the new tip
and retries. For this **single-writer appliance** that resolves last-writer-wins:
a concurrent remote *edit* to the same note loses to ours, and a remote-only
*added* file the card doesn't have would be dropped by the replay's `add --all`.
Both need a real merge (increment B) and don't arise from the device's own use.
This path is **hardware-verified for the happy case** (`9b635c42` fast-forwarded
clean); the reconcile branch itself is compile-verified but not yet exercised on
device.
## Can cold sync go lower?
The big rocks are physics or protocol, not slack:
- **Wi-Fi assoc ~3.6 s** and **SNTP ~24 s** are one-time per power cycle and
mostly out of our hands (association floor, NTP RTT). Keeping Wi-Fi up between
syncs trades battery for latency — see
[`../tradeoff-curves/wifi-auto-sync.md`](../tradeoff-curves/wifi-auto-sync.md).
- **TLS handshake ~2.4 s** and **push negotiate/upload ~4.4 s** are inherent to
libgit2-over-mbedTLS on this part; the payload is tiny, so there's little to
shave.
- **stage + commit ~3.1 s** is the one soft spot: staging `notes.md` directly
instead of `add_all(["*"])` would skip the SD/FAT tree walk (likely →
sub-second), at the cost of the file-agnostic design that a future multi-file
publish wants. Deferred, on purpose.
**Conclusion:** ~16 s cold / ~10 s warm is close to the floor for "commit to FAT +
one TLS push over Wi-Fi with a fresh clock." It reads as slow only if you wait on
it — and by design you don't: `:sync` is a deliberate action with a snackbar, and
[`ctrl-g-perceived-latency.md`](ctrl-g-perceived-latency.md) argues the perceived
cost is set by *when durability is surfaced*, not by wall-clock. Recorded here so
the number is scoped against the protocol, not treated as a regression.

View File

@@ -15,11 +15,12 @@ with the v0.2v1.0 trajectory ([README](../README.md),
glossary at [`../CONTEXT.md`](../CONTEXT.md).
Format inspired by the classic House of Quality, kept compact. Strength
weights: **9** strong, **3** medium, **1** weak, blank none. The §3
matrix and §4 roof — plus a guessed competitor perception zone — live
in [`quality-house.md`](quality-house.md); this file owns the WHAT/HOW
catalogues (§1, §2), the narrative reading of the numbers (§3, §4), and
the downstream sections (§5§8).
weights: **9** strong, **3** medium, **1** weak, blank none. This one file
owns everything: the WHAT/HOW catalogues (§1, §2); the House diagram itself —
matrix, roof, basement Σ, and the guessed competitor perception zone — in §3;
the narrative reading of the numbers (§3, §4); and the downstream sections
(§5§8). (The House was a separate `quality-house.md` until 2026-07-11, merged
into §3 to end the mirror-drift between the two files.)
---
@@ -104,11 +105,580 @@ Breakdown + levers: [`notes/boot-time-budget.md`](notes/boot-time-budget.md).
## 3. House of Quality — WHATs × HOWs
The matrix (row × column = how strongly characteristic H advances requirement
W) and its Σ row — the weighted vote `Σ(weight × strength)` on which
characteristics deserve the most engineering attention — live in
[`quality-house.md`](quality-house.md). The Σ totals quoted below are
from its basement row.
This section is the House itself: §1's WHATs (rows) × §2's HOWs (columns), each
cell scoring how strongly a characteristic advances a requirement (9 / 3 / 1 /
blank). The roof carries the §4 HOW-vs-HOW correlations; the basement carries the
v0.1 targets (from §2), the weighted-vote sums `Σ = Σ(W weight × cell strength)`,
and rounded relative weights. The right-hand zone scores five products against
the WHATs (05, **guessed, not measured** — see
[Perception scores](#perception-scores-guessed)). The Σ totals quoted in the
priority list below come from the basement.
> **Single source of truth.** The `\foreach` blocks in the diagram restate §1's
> weights and §2's targets — TikZ can't read the tables, so keep them in sync when
> either changes, and **recompute the basement Σ / Rel % here** (see
> [Regenerating](#regenerating)) rather than transcribing them from elsewhere.
> This mirror used to live in a separate `quality-house.md`; it was merged into §3
> on 2026-07-11 so the two can no longer silently drift.
For a blank version of the same chassis (WHATs, HOWs, importance, and v0.1
targets kept; relations + roof + Σ basement left empty for practice), see
[`quality-house-empty.md`](quality-house-empty.md).
```tikz
% =====================================================================
% QFD "House of Quality" preamble
% =====================================================================
\usetikzlibrary{arrows.meta, positioning, shapes.geometric, shapes.misc, calc, fit, backgrounds}
\newif\ifqfdshowroof \qfdshowrooftrue
\newif\ifqfdshowbasement \qfdshowbasementtrue
\newif\ifqfdshowcompetitive \qfdshowcompetitivetrue
\newif\ifqfdshowlegend \qfdshowlegendtrue
\newif\ifqfdshowimportance \qfdshowimportancetrue
\newif\ifqfdshowcorrlegend \qfdshowcorrlegendtrue
\newif\ifqfdshowevallegend \qfdshowevallegendtrue
\def\qfdNW{5}
\def\qfdNH{5}
\def\qfdWhatW{4.0}
\def\qfdImpW{0.9}
\def\qfdCmpW{3}
\def\qfdHdrH{2.6}
\def\qfdBasementN{4}
\def\qfdWhatsTitle{Customer needs}
\def\qfdImpTitle{Imp.\ \%}
\def\qfdPerceptionTitle{Comparative evaluation}
\def\qfdPoorLabel{poor}
\def\qfdExcellentLabel{excellent}
\def\qfdAltOneLabel{Typoena}
\def\qfdAltTwoLabel{Competitor A}
\def\qfdAltThreeLabel{Competitor B}
\def\qfdRelTitle{Relation}
\def\qfdCorrTitle{Correlation}
\def\qfdEvalTitle{Evaluation}
\tikzset{
qfdthin/.style ={line width=0.35pt},
qfdmed/.style ={line width=0.7pt},
qfdstrong/.style={circle, draw, fill=black,
minimum size=7pt, inner sep=0pt},
qfdmod/.style ={circle, draw,
minimum size=7pt, inner sep=0pt, line width=0.8pt},
qfdweak/.style ={regular polygon, regular polygon sides=3, draw,
minimum size=8.5pt, inner sep=0pt, line width=0.7pt},
qfdrel/.is choice,
qfdrel/S/.style={qfdstrong},
qfdrel/M/.style={qfdmod},
qfdrel/W/.style={qfdweak},
qfdalt1mk/.style={circle, draw, fill=black,
minimum size=6pt, inner sep=0pt, line width=1pt},
qfdalt1ln/.style={line width=1.2pt},
qfdalt2mk/.style={regular polygon, regular polygon sides=3, draw,
fill=black, minimum size=6pt, inner sep=0pt,
line width=0.7pt},
qfdalt2ln/.style={line width=0.7pt, dashed},
qfdalt3mk/.style={rectangle, draw, fill=black,
minimum size=5pt, inner sep=0pt, line width=0.7pt},
qfdalt3ln/.style={line width=0.7pt, dotted},
}
\newcommand{\qfdDrawGrid}{%
\foreach \c in {1,...,\qfdNHm} \draw[qfdthin] (\c, 0) -- (\c, -\qfdNW);
\foreach \r in {1,...,\qfdNWm} \draw[qfdthin] (0, -\r) -- (\qfdNH, -\r);
\foreach \r in {1,...,\qfdNWm}
\draw[qfdthin] (\qfdLeftEdge, -\r) -- (0, -\r);
\ifqfdshowroof
\foreach \c in {1,...,\qfdNHm}
\draw[qfdthin] (\c, 0) -- (\c, \qfdHdrH);
\fi
\ifqfdshowcompetitive
\foreach \r in {1,...,\qfdNWm}
\draw[qfdthin] (\qfdNH, -\r) -- (\qfdNH+\qfdCmpW, -\r);
\fi
\ifqfdshowbasement
\foreach \r in {1,...,\qfdBasementN}
\draw[qfdthin] (0, -\qfdNW-\r) -- (\qfdNH, -\qfdNW-\r);
\foreach \c in {1,...,\qfdNHm}
\draw[qfdthin] (\c, -\qfdNW) -- (\c, -\qfdNW-\qfdBasementN);
\fi
}
\newcommand{\qfdDrawRoof}{%
\ifqfdshowroof
\foreach \k in {1,...,\qfdNHm} {%
\pgfmathsetmacro{\rx}{(\k+\qfdNH)/2}
\pgfmathsetmacro{\ry}{\qfdHdrH + (\qfdNH-\k)/2}
\pgfmathsetmacro{\lx}{\k/2}
\pgfmathsetmacro{\ly}{\qfdHdrH + \k/2}
\draw[qfdthin] (\k, \qfdHdrH) -- (\rx, \ry);
\draw[qfdthin] (\k, \qfdHdrH) -- (\lx, \ly);
}%
\draw[qfdmed] (0, \qfdHdrH)
-- (\qfdNH/2, \qfdApexY) -- (\qfdNH, \qfdHdrH);
\foreach \i in {1,...,\qfdNH}
\foreach \k in {1,...,\qfdNH} {%
\pgfmathtruncatemacro{\jj}{\i+\k}
\ifnum\jj>\qfdNH\relax\else
\pgfmathsetmacro{\xx}{\i + \k/2 - 0.5}
\pgfmathsetmacro{\yy}{\qfdHdrH + \k/2}
\coordinate (C-\i-\jj) at (\xx, \yy);
\fi
}%
\fi
}
\newcommand{\qfdDrawScale}{%
\ifqfdshowcompetitive
\foreach \tk in {0,1,2,3,4,5} {%
\pgfmathsetmacro{\tx}{\qfdNH + (\tk+0.5)*\qfdCmpW/6}
\node[anchor=south, font=\scriptsize] at (\tx, 0.02) {\tk};
}%
\node[anchor=south, font=\scriptsize\bfseries, align=center,
text width=\qfdCmpW cm]
at ({\qfdNH + \qfdCmpW/2}, 0.7) {\qfdPerceptionTitle};
\node[anchor=north, font=\scriptsize\itshape]
at ({\qfdNH + 0.45}, -\qfdNW) {\qfdPoorLabel};
\node[anchor=north, font=\scriptsize\itshape]
at ({\qfdNH + \qfdCmpW - 0.45}, -\qfdNW) {\qfdExcellentLabel};
\fi
}
\newcommand{\qfdDrawZoneTitles}{%
\ifqfdshowimportance
\node[rotate=90, anchor=west, font=\footnotesize\bfseries]
at ({-\qfdImpW/2}, 0.12) {\qfdImpTitle};
\fi
\node[font=\scriptsize\bfseries, align=center, text width=\qfdWhatW cm]
at ({\qfdLeftEdge + \qfdWhatW/2},
{\ifqfdshowroof \qfdHdrH/2 \else 0.6 \fi}) {\qfdWhatsTitle};
}
\newcommand{\qfdDrawFrames}{%
\begin{scope}[qfdmed]
\draw (\qfdLeftEdge, 0) rectangle (\qfdNH, -\qfdNW);
\ifqfdshowimportance \draw (-\qfdImpW, 0) -- (-\qfdImpW, -\qfdNW); \fi
\draw (0, 0) -- (0, -\qfdNW);
\ifqfdshowroof
\draw (0, 0) rectangle (\qfdNH, \qfdHdrH); \fi
\ifqfdshowbasement
\draw (0, -\qfdNW) rectangle (\qfdNH, -\qfdNW-\qfdBasementN); \fi
\ifqfdshowcompetitive
\draw (\qfdNH, 0) rectangle (\qfdNH+\qfdCmpW, -\qfdNW); \fi
\end{scope}
}
\newcommand{\qfdDrawLegend}{%
\ifqfdshowlegend
\pgfmathsetmacro{\qfdLegX}{%
\qfdNH + \ifqfdshowcompetitive \qfdCmpW + 0.7 \else 0.7 \fi}
\pgfmathsetmacro{\qfdLegBottom}{%
-2.05
\ifqfdshowroof \ifqfdshowcorrlegend - 2.55 \fi \fi
\ifqfdshowcompetitive \ifqfdshowevallegend - 2.20 \fi \fi}
\pgfmathsetmacro{\qfdLegY}{\qfdHdrH - 0.4}
\begin{scope}[shift={(\qfdLegX, \qfdLegY)}]
\draw[qfdmed, rounded corners=2pt]
(-0.15, 0.4) rectangle (4.5, \qfdLegBottom);
\node[anchor=west, font=\footnotesize\bfseries] at (0, 0.1)
{\qfdRelTitle};
\draw[qfdthin] (0, -0.15) -- (4.35, -0.15);
\node[qfdstrong] at (0.22, -0.5) {};
\node[anchor=west] at (0.5, -0.5) {Strong (9)};
\node[qfdmod] at (0.22, -0.95) {};
\node[anchor=west] at (0.5, -0.95) {Medium (3)};
\node[qfdweak] at (0.22, -1.4) {};
\node[anchor=west] at (0.5, -1.4) {Weak (1)};
\ifqfdshowroof \ifqfdshowcorrlegend
\node[anchor=west, font=\footnotesize\bfseries] at (0, -2.10)
{\qfdCorrTitle};
\draw[qfdthin] (0, -2.35) -- (4.35, -2.35);
\node[anchor=west] at (0, -2.70) {{$+\!+$}\quad very positive};
\node[anchor=west] at (0, -3.05) {{$+$\phantom{$+$}}\quad positive};
\node[anchor=west] at (0, -3.40) {{$-$\phantom{$-$}}\quad negative};
\node[anchor=west] at (0, -3.75) {{$-\!-$}\quad very negative};
\fi \fi
\ifqfdshowcompetitive \ifqfdshowevallegend
\pgfmathsetmacro{\qfdEvalTop}{%
-2.10 \ifqfdshowroof\ifqfdshowcorrlegend - 2.55 \fi\fi}
\node[anchor=west, font=\footnotesize\bfseries]
at (0, \qfdEvalTop) {\qfdEvalTitle};
\pgfmathsetmacro{\qfdEvalSep}{\qfdEvalTop - 0.25}
\draw[qfdthin] (0, \qfdEvalSep) -- (4.35, \qfdEvalSep);
\pgfmathsetmacro{\qfdLegA}{\qfdEvalTop - 0.55}
\draw[qfdalt1ln] (0.05, \qfdLegA) -- (0.45, \qfdLegA);
\node[qfdalt1mk] at (0.25, \qfdLegA) {};
\node[anchor=west, font=\bfseries] at (0.55, \qfdLegA)
{\qfdAltOneLabel};
\pgfmathsetmacro{\qfdLegB}{\qfdEvalTop - 0.95}
\draw[qfdalt2ln] (0.05, \qfdLegB) -- (0.45, \qfdLegB);
\node[qfdalt2mk] at (0.25, \qfdLegB) {};
\node[anchor=west] at (0.55, \qfdLegB) {\qfdAltTwoLabel};
\pgfmathsetmacro{\qfdLegC}{\qfdEvalTop - 1.35}
\draw[qfdalt3ln] (0.05, \qfdLegC) -- (0.45, \qfdLegC);
\node[qfdalt3mk] at (0.25, \qfdLegC) {};
\node[anchor=west] at (0.55, \qfdLegC) {\qfdAltThreeLabel};
\fi \fi
\end{scope}
\fi
}
\newenvironment{qfdhouse}{%
\begin{tikzpicture}[x=1cm, y=1cm, font=\scriptsize,
line cap=round, line join=round]
\ifqfdshowimportance
\pgfmathsetmacro{\qfdLeftEdge}{-\qfdWhatW-\qfdImpW}
\else
\pgfmathsetmacro{\qfdLeftEdge}{-\qfdWhatW}
\fi
\pgfmathsetmacro{\qfdApexY}{\qfdHdrH + \qfdNH/2}
\pgfmathtruncatemacro{\qfdNHm}{\qfdNH - 1}
\pgfmathtruncatemacro{\qfdNWm}{\qfdNW - 1}
\qfdDrawGrid
\qfdDrawRoof
\qfdDrawScale
\qfdDrawZoneTitles
}{%
\qfdDrawFrames
\qfdDrawLegend
\end{tikzpicture}%
}
% --- Dimensions tuned for the typewriter QFD (14 W x 15 H) ---
\def\qfdNW{14}
\def\qfdNH{14}
\def\qfdWhatW{4.6}
\def\qfdImpW{0.7}
\def\qfdHdrH{5.0}
\def\qfdBasementN{3}
\def\qfdCmpW{3.4}
\qfdshowlegendfalse % we draw a 4-alternative legend manually
\def\qfdWhatsTitle{User-facing requirements (W)}
\def\qfdImpTitle{Weight}
\def\qfdPerceptionTitle{Competitive perception\\(05, guessed)}
\def\qfdPoorLabel{poor}
\def\qfdExcellentLabel{excellent}
% Perception-zone markers: shape + colour-blind-safe colour per product.
% Palette is Okabe-Ito (blue, vermillion, bluish green, reddish purple).
% Light fills + saturated outlines keep stacked markers legible.
\definecolor{qfdcTypoena}{RGB}{0,114,178}
\definecolor{qfdcRem}{RGB}{213,94,0}
\definecolor{qfdcFrw}{RGB}{0,158,115}
\definecolor{qfdcPom}{RGB}{204,121,167}
\definecolor{qfdcFrwS}{RGB}{86,180,233}
\tikzset{
qfdalt1mk/.style={circle, draw=qfdcTypoena, fill=qfdcTypoena!55!white,
minimum size=6.5pt, inner sep=0pt, line width=1.1pt},
qfdalt1ln/.style={line width=1.2pt, qfdcTypoena},
qfdalt2mk/.style={regular polygon, regular polygon sides=3,
draw=qfdcRem, fill=qfdcRem!55!white,
minimum size=7pt, inner sep=0pt, line width=0.9pt},
qfdalt2ln/.style={line width=0.8pt, dashed, qfdcRem},
qfdalt3mk/.style={rectangle, draw=qfdcFrw, fill=qfdcFrw!55!white,
minimum size=5.5pt, inner sep=0pt, line width=0.9pt},
qfdalt3ln/.style={line width=0.8pt, dotted, qfdcFrw},
qfdalt4mk/.style={diamond, aspect=1, draw=qfdcPom,
fill=qfdcPom!50!white,
minimum size=7pt, inner sep=0pt, line width=1.0pt},
qfdalt4ln/.style={line width=0.8pt, dash dot, qfdcPom},
qfdalt5mk/.style={regular polygon, regular polygon sides=5,
draw=qfdcFrwS, fill=qfdcFrwS!40!white,
minimum size=5.5pt, inner sep=0pt, line width=0.8pt},
qfdalt5ln/.style={line width=0.7pt, dash dot dot, qfdcFrwS},
}
\begin{document}
\begin{qfdhouse}
% ---------- WHATs (left column) ----------
% Box width is 0.2 cm narrower than the column so labels have 0.1 cm
% clearance on each side and don't bleed into the Weight column.
\pgfmathsetmacro{\qfdWhatTextW}{\qfdWhatW - 0.2}
\foreach \r/\t in {%
1/{W1 Sub-second visible response to typing},
2/{W2 Publishing is one deliberate action away},
3/{W3 Pulling power never corrupts the file},
4/{W4 Provisioning never interrupts a writing session},
5/{W5 Quick boot to a writing cursor},
6/{W6 Long sessions without crash, lag, drift},
7/{W7 Nothing on the device competes with prose},
8/{W8 The UI never moves except when I move it},
9/{W9 Codebase absorbs the planned roadmap},
10/{W10 I can repair or fork it with hobbyist tools},
11/{W11 Multi-day battery life (v0.8 onward)},
12/{W12 Local-only files coexist with git scope (v0.5+)},
13/{W13 Typography sets a writing-tool tone},
14/{W14 I can carry the device and write away from a desk}%
}
\node[anchor=west, font=\scriptsize,
text width=\qfdWhatTextW cm, align=left]
at ({\qfdLeftEdge + 0.1}, {-\r + 0.5}) {\t};
% ---------- Importance (raw 1-10 weight) ----------
\foreach \r/\w in {1/10, 2/9, 3/10, 4/7, 5/6, 6/9, 7/8, 8/7,
9/8, 10/5, 11/4, 12/5, 13/7, 14/8}
\node[font=\scriptsize] at ({-\qfdImpW/2}, {-\r + 0.5}) {\w};
% ---------- HOWs (rotated column titles) ----------
\foreach \c/\t in {%
1/{H1 Type latency},
2/{H2 Refresh area per keystroke},
3/{H3 Full-refresh cadence},
4/{H4 Boot latency (cold)},
5/{H5 Continuous-typing endurance},
6/{H6 Publish reliability},
7/{H7 Publish latency},
8/{H8 Save durability},
9/{H9 PSRAM heap headroom},
10/{H10 Firmware binary size},
11/{H11 Total stack budget},
12/{H12 Network reconnect time},
13/{H13 Idle / typing / push current},
14/{H15 Clean release build time}%
}
\node[rotate=90, anchor=west, font=\scriptsize]
at ({\c - 0.5}, 0.15) {\t};
% ---------- Relation matrix (S=9, M=3, W=1) ----------
% W1 row 1: H1S H2S H3M H5M H9W H11W
\node[qfdrel/S] at ({1 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/S] at ({2 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/M] at ({3 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/M] at ({5 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/W] at ({9 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/W] at ({11 - 0.5}, {-1 + 0.5}) {};
% W2 row 2: H6S H7M H9S H12S
\node[qfdrel/S] at ({6 - 0.5}, {-2 + 0.5}) {};
\node[qfdrel/M] at ({7 - 0.5}, {-2 + 0.5}) {};
\node[qfdrel/S] at ({9 - 0.5}, {-2 + 0.5}) {};
\node[qfdrel/S] at ({12 - 0.5}, {-2 + 0.5}) {};
% W3 row 3: H8S
\node[qfdrel/S] at ({8 - 0.5}, {-3 + 0.5}) {};
% W4 row 4: H6M H12M
\node[qfdrel/M] at ({6 - 0.5}, {-4 + 0.5}) {};
\node[qfdrel/M] at ({12 - 0.5}, {-4 + 0.5}) {};
% W5 row 5: H4S H10M
\node[qfdrel/S] at ({4 - 0.5}, {-5 + 0.5}) {};
\node[qfdrel/M] at ({10 - 0.5}, {-5 + 0.5}) {};
% W6 row 6: H1M H3M H5S H6M H8M H9S H11M H12M
\node[qfdrel/M] at ({1 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({3 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/S] at ({5 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({6 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({8 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/S] at ({9 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({11 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({12 - 0.5}, {-6 + 0.5}) {};
% W7 row 7: H1M H2M H3M H13M
\node[qfdrel/M] at ({1 - 0.5}, {-7 + 0.5}) {};
\node[qfdrel/M] at ({2 - 0.5}, {-7 + 0.5}) {};
\node[qfdrel/M] at ({3 - 0.5}, {-7 + 0.5}) {};
\node[qfdrel/M] at ({13 - 0.5}, {-7 + 0.5}) {};
% W8 row 8: H1W H2S H3S
\node[qfdrel/W] at ({1 - 0.5}, {-8 + 0.5}) {};
\node[qfdrel/S] at ({2 - 0.5}, {-8 + 0.5}) {};
\node[qfdrel/S] at ({3 - 0.5}, {-8 + 0.5}) {};
% W9 row 9: H10W H11W H15M
\node[qfdrel/W] at ({10 - 0.5}, {-9 + 0.5}) {};
\node[qfdrel/W] at ({11 - 0.5}, {-9 + 0.5}) {};
\node[qfdrel/M] at ({14 - 0.5}, {-9 + 0.5}) {};
% W10 row 10: H10M H13W H15W
\node[qfdrel/M] at ({10 - 0.5}, {-10 + 0.5}) {};
\node[qfdrel/W] at ({13 - 0.5}, {-10 + 0.5}) {};
\node[qfdrel/W] at ({14 - 0.5}, {-10 + 0.5}) {};
% W11 row 11: H13S
\node[qfdrel/S] at ({13 - 0.5}, {-11 + 0.5}) {};
% W12 row 12: H6W H8M
\node[qfdrel/W] at ({6 - 0.5}, {-12 + 0.5}) {};
\node[qfdrel/M] at ({8 - 0.5}, {-12 + 0.5}) {};
% W13 row 13: H9M
\node[qfdrel/M] at ({9 - 0.5}, {-13 + 0.5}) {};
% W14 row 14: H4W H8M H12M H13S
\node[qfdrel/W] at ({4 - 0.5}, {-14 + 0.5}) {};
\node[qfdrel/M] at ({8 - 0.5}, {-14 + 0.5}) {};
\node[qfdrel/M] at ({12 - 0.5}, {-14 + 0.5}) {};
\node[qfdrel/S] at ({13 - 0.5}, {-14 + 0.5}) {};
% ---------- Roof correlations ----------
\node[font=\scriptsize] at (C-1-2) {$+\!+$}; % H1-H2 strong reinforce
\node[font=\scriptsize] at (C-1-3) {$-$}; % H1-H3 mild conflict
\node[font=\scriptsize] at (C-1-5) {$+$}; % H1-H5 mild reinforce
\node[font=\scriptsize] at (C-1-13) {$-$}; % H1-H13 mild conflict
\node[font=\scriptsize] at (C-2-3) {$+\!+$}; % H2-H3 strong reinforce
\node[font=\scriptsize] at (C-2-13) {$+$}; % H2-H13
\node[font=\scriptsize] at (C-3-13) {$+$}; % H3-H13
\node[font=\scriptsize] at (C-4-10) {$-$}; % H4-H10 boot vs binary
\node[font=\scriptsize] at (C-5-6) {$+$}; % H5-H6
\node[font=\scriptsize] at (C-5-8) {$+$}; % H5-H8
\node[font=\scriptsize] at (C-5-9) {$-\!-$}; % H5-H9 soak vs heap
\node[font=\scriptsize] at (C-6-7) {$+$}; % H6-H7
\node[font=\scriptsize] at (C-6-9) {$-\!-$}; % H6-H9 push vs heap
\node[font=\scriptsize] at (C-6-12) {$+\!+$}; % H6-H12
\node[font=\scriptsize] at (C-7-9) {$-$}; % H7-H9
\node[font=\scriptsize] at (C-7-12) {$+\!+$}; % H7-H12
\node[font=\scriptsize] at (C-9-10) {$-\!-$}; % H9-H10 heap vs binary
\node[font=\scriptsize] at (C-10-14) {$-\!-$}; % H10-H15 binary vs build
\node[font=\scriptsize] at (C-11-13) {$-$}; % H11-H13
% ---------- Basement: target / abs weight / rel weight % ----------
\foreach \c/\tgt/\abs/\rel in {%
1/{$\leq$200\,ms}/148/10,
2/{$\leq$1 line}/177/11,
3/{1 : 64}/144/9,
4/{$\leq$5\,s}/62/4,
5/{$\geq$1\,h}/111/7,
6/{$\geq$95\,\%}/134/9,
7/{$\leq$30\,s}/27/2,
8/{100\,\%}/156/10,
9/{$\geq$1\,MB}/193/12,
10/{$\leq$2\,MB}/41/3,
11/{$\leq$80\,KB}/45/3,
12/{$\leq$30\,s}/153/10,
13/{obs.}/137/9,
14/{$\leq$7\,min}/29/2%
} {
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 0.5}) {\tgt};
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 1.5}) {\abs};
\node[font=\scriptsize\bfseries]
at ({\c - 0.5}, {-\qfdNW - 2.5}) {\rel};
}
% ---------- Basement row labels (in the margin below WHATs) ----------
\foreach \k/\lbl in {1/{Target (v0.1)}, 2/{$\Sigma$ abs}, 3/{Rel.\ \%}}
\node[anchor=east, font=\scriptsize\itshape]
at ({-0.1}, {-\qfdNW - \k + 0.5}) {\lbl};
% ---------- Perception zone: 5 products x 14 WHATs (0-5 scores) ----------
% Columns: \so=Typoena target, \st=reMarkable 2 + Type Folio,
% \sf=Freewrite Traveler, \sg=Pomera DM250,
% \sh=Freewrite Smart Typewriter.
% Pass 1: stash each score as a named coordinate so the profile lines
% below can reuse it without recomputing.
\foreach \r/\so/\st/\sf/\sg/\sh in {%
1/4/1/4/5/3,
2/5/4/4/2/4,
3/4/4/2/2/2,
4/5/2/2/5/2,
5/4/3/4/5/4,
6/3/3/4/5/4,
7/5/2/5/5/5,
8/4/3/4/5/4,
9/4/3/2/1/2,
10/5/4/2/1/2,
11/1/5/5/4/5,
12/3/1/2/3/2,
13/3/5/2/2/2,
14/2/4/5/2/5%
} {
\pgfmathsetmacro{\xo}{\qfdNH + (\so + 0.5)*\qfdCmpW/6}
\pgfmathsetmacro{\xt}{\qfdNH + (\st + 0.5)*\qfdCmpW/6}
\pgfmathsetmacro{\xf}{\qfdNH + (\sf + 0.5)*\qfdCmpW/6}
\pgfmathsetmacro{\xg}{\qfdNH + (\sg + 0.5)*\qfdCmpW/6}
\pgfmathsetmacro{\xs}{\qfdNH + (\sh + 0.5)*\qfdCmpW/6}
\coordinate (po-\r) at (\xo, {-\r + 0.5});
\coordinate (pr-\r) at (\xt, {-\r + 0.5});
\coordinate (pf-\r) at (\xf, {-\r + 0.5});
\coordinate (pp-\r) at (\xg, {-\r + 0.5});
\coordinate (ps-\r) at (\xs, {-\r + 0.5});
}
% Pass 2: profile lines per alternative. Drawn before markers so the
% dots sit on top of (not under) the line endpoints.
\draw[qfdalt1ln] (po-1) \foreach \r in {2,...,\qfdNW} { -- (po-\r) };
\draw[qfdalt2ln] (pr-1) \foreach \r in {2,...,\qfdNW} { -- (pr-\r) };
\draw[qfdalt3ln] (pf-1) \foreach \r in {2,...,\qfdNW} { -- (pf-\r) };
\draw[qfdalt4ln] (pp-1) \foreach \r in {2,...,\qfdNW} { -- (pp-\r) };
\draw[qfdalt5ln] (ps-1) \foreach \r in {2,...,\qfdNW} { -- (ps-\r) };
% Pass 3: markers on top of the lines.
\foreach \r in {1,...,\qfdNW} {
\node[qfdalt1mk] at (po-\r) {};
\node[qfdalt2mk] at (pr-\r) {};
\node[qfdalt3mk] at (pf-\r) {};
\node[qfdalt4mk] at (pp-\r) {};
\node[qfdalt5mk] at (ps-\r) {};
}
% ---------- Manual legend (5 alternatives, placed right of zones) ----------
\pgfmathsetmacro{\qfdLegX}{\qfdNH + \qfdCmpW + 0.7}
\begin{scope}[shift={(\qfdLegX, \qfdHdrH - 0.4)}]
\draw[qfdmed, rounded corners=2pt]
(-0.15, 0.4) rectangle (5.1, -7.50);
% Relations
\node[anchor=west, font=\footnotesize\bfseries] at (0, 0.1)
{Relation};
\draw[qfdthin] (0, -0.15) -- (4.95, -0.15);
\node[qfdstrong] at (0.22, -0.5) {};
\node[anchor=west] at (0.5, -0.5) {Strong (9)};
\node[qfdmod] at (0.22, -0.95) {};
\node[anchor=west] at (0.5, -0.95) {Medium (3)};
\node[qfdweak] at (0.22, -1.4) {};
\node[anchor=west] at (0.5, -1.4) {Weak (1)};
% Correlation
\node[anchor=west, font=\footnotesize\bfseries] at (0, -2.10)
{Correlation};
\draw[qfdthin] (0, -2.35) -- (4.95, -2.35);
\node[anchor=west] at (0, -2.70) {{$+\!+$}\quad very positive};
\node[anchor=west] at (0, -3.05) {{$+$\phantom{$+$}}\quad positive};
\node[anchor=west] at (0, -3.40) {{$-$\phantom{$-$}}\quad negative};
\node[anchor=west] at (0, -3.75) {{$-\!-$}\quad very negative};
% Perception
\node[anchor=west, font=\footnotesize\bfseries] at (0, -4.20)
{Perception};
\draw[qfdthin] (0, -4.45) -- (4.95, -4.45);
\draw[qfdalt1ln] (0.05, -4.80) -- (0.45, -4.80);
\node[qfdalt1mk] at (0.25, -4.80) {};
\node[anchor=west, font=\bfseries] at (0.55, -4.80)
{Typoena (v0.1 target)};
\draw[qfdalt2ln] (0.05, -5.25) -- (0.45, -5.25);
\node[qfdalt2mk] at (0.25, -5.25) {};
\node[anchor=west] at (0.55, -5.25) {reMarkable 2 + Type Folio};
\draw[qfdalt3ln] (0.05, -5.70) -- (0.45, -5.70);
\node[qfdalt3mk] at (0.25, -5.70) {};
\node[anchor=west] at (0.55, -5.70) {Freewrite Traveler};
\draw[qfdalt5ln] (0.05, -6.15) -- (0.45, -6.15);
\node[qfdalt5mk] at (0.25, -6.15) {};
\node[anchor=west] at (0.55, -6.15) {Freewrite Smart Typewriter};
\draw[qfdalt4ln] (0.05, -6.60) -- (0.45, -6.60);
\node[qfdalt4mk] at (0.25, -6.60) {};
\node[anchor=west] at (0.55, -6.60) {Pomera DM250};
\node[anchor=west, font=\scriptsize\itshape] at (0, -7.15)
{0 = poor, 5 = excellent};
\end{scope}
\end{qfdhouse}
\end{document}
```
### Reading the house
- **Importance (left column)** is the raw 110 weight from §1, not a normalised
%, so adding stays cheap when a WHAT shifts. Sum of weights is 103; treat each
unit as ~0.97 % if you want a percentage view.
- **Roof** carries the §4 symbols translated into classical QFD glyphs:
`++` strong reinforcement (`◎`), `+` mild reinforcement (`○`), `` mild
conflict (`×`), `` strong conflict (`⊗`).
- **Basement rows** are: v0.1 target → column sum (`Σ` of `weight × strength`) →
relative weight as integer % of total (1557). Relative weights round to 100.
- **H7, H10, H15** (Publish latency, binary size, build time) sit at the bottom
of the basement — knowingly-paid costs per §7, not signals to optimise harder.
### Top engineering priorities (from importance)
@@ -160,6 +730,98 @@ user preference for faster iteration, not matrix-derived priority; if it
pushes back against [ADR-001]'s "+510 min" pricing, the target moves
before the runtime decision does.
### Perception scores (guessed)
Five products on the 05 scale, scored against each WHAT. Reference
configurations: **reMarkable 2 + Type Folio**, **Freewrite Traveler**,
**Freewrite Smart Typewriter**, **Pomera DM250** (DM250 has a reflective
monochrome LCD, not e-ink — flagged in W1 / W8). "Typoena" is the v0.1
target from §2, not measured yet.
Freewrite Traveler scores assume the
[Sailfish firmware](https://getfreewrite.com/blogs/writing-success/freewrite-sailfish-firmware)
(released 2025-11-19), which rewrote the OS in Rust, cut keystroke latency
40100 %, and trimmed power draw 30 % typing / 50 % idle on both
Traveler and Smart Typewriter Gen 3. Three rows rescored upward as a
result: W1 Traveler 3→4 / Smart 2→3 (Smart's larger panel still trails
Traveler by one notch), W5 both 3→4 (boot accelerated, no published
number), W9 both 1→2 (Rust rewrite explicitly unblocked features that
JS could not carry; still closed so neither reaches reMarkable's
hackable-Linux 3).
| ID | WHAT (truncated) | Typoena | reM. | Frw.T | Frw.S | Pom. | Rationale (shortest defensible) |
| --- | ------------------------------------------------- | :-----: | :--: | :---: | :---: | :--: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| W1 | Sub-second response to typing | 4 | 1 | 4 | 3 | 5 | Typoena targets ≤200 ms; reMarkable e-ink visibly laggy on a typing-focused device — tested less responsive than Smart Typewriter, and latency is so load-bearing for W1 that it earns a 1 not a 2; both Freewrites post-Sailfish trimmed latency 40100 % (Frw.T plausibly inside 200 ms; Frw.S still trails by one notch on larger panel); Pomera LCD ~zero. |
| W2 | Publishing is one deliberate action away | 5 | 4 | 4 | 4 | 2 | Ctrl-G atomic; reMarkable + Freewrite cloud-sync is one-tap but not git; Pomera = USB/SD copy or QR transfer. |
| W3 | Pulling power never corrupts the file | 4 | 4 | 2 | 2 | 2 | Typoena: atomic-rename + fsync. reMarkable journals. Freewrite + Pomera: forum reports of corruption on yank. |
| W4 | Provisioning never interrupts writing | 5 | 2 | 2 | 2 | 5 | Typoena v0.1: build-time config (dev-only). reM/Frw need Wi-Fi + account. Pomera: literally none. |
| W5 | Quick boot to a writing cursor | 4 | 3 | 4 | 4 | 5 | Typoena target ≤5 s. reMarkable cold-boots ~20 s (great from sleep). Both Freewrites accelerated post-Sailfish (no published number; were ~1015 s e-ink wake). Pomera ~3 s. |
| W6 | Long sessions without crash / lag / drift | 3 | 3 | 4 | 4 | 5 | Typoena unproven (1 h target). Freewrite famously stable (both variants). Pomera firmware is decades-mature. |
| W7 | Nothing on the device competes with prose | 5 | 2 | 5 | 5 | 5 | reMarkable has apps, menus, drawing, PDFs. Freewrite + Pomera are single-purpose; Typoena by design. |
| W8 | The UI never moves except when I move it | 4 | 3 | 4 | 4 | 5 | reMarkable animates more; Typoena uses dirty-rects; Freewrites minimal motion; Pomera near-static LCD. |
| W9 | Codebase absorbs the planned roadmap | 4 | 3 | 2 | 2 | 1 | Modular Rust Typoena; reMarkable is hackable Linux; both Freewrites carry Sailfish (Rust rewrite explicitly unblocked features JS could not carry) but closed; Pomera closed firmware. |
| W10 | I can repair or fork it with hobbyist tools | 5 | 4 | 2 | 2 | 1 | Typoena: open BOM + ESP32. reMarkable: rooted Linux + community ROMs. Freewrite + Pomera: closed. |
| W11 | Multi-day battery life (v0.8 onward) | 1 | 5 | 5 | 5 | 4 | Typoena v0.1 = wall-powered (battery deferred). reMarkable + both Freewrites legendary (~4 weeks; Sailfish trimmed 30 % typing / 50 % idle). Pomera ~24 h. |
| W12 | Local-only files coexist with git scope | 3 | 1 | 2 | 2 | 3 | Typoena v0.5+ design. reMarkable cloud-only. Freewrites have local + Postbox but no VCS. Pomera = pure local. |
| W13 | Typography sets a writing-tool tone | 3 | 5 | 2 | 2 | 2 | Typoena v0.1: single mono (serif option in v1.0). reMarkable: rich type rendering. Freewrite + Pomera: utilitarian. |
| W14 | I can carry the device and write away from a desk | 2 | 4 | 5 | 1 | 5 | Typoena v0.1 wall-powered (ADR-008), no enclosure spec yet — desk-bound by design. reMarkable + Type Folio bag-friendly with bulk. Freewrite Traveler is the form-factor reference (~1.6 lb, folds). Smart Typewriter ~5 lb, desk-bound. Pomera DM250 pocketable foldable. |
**Totals** (sum across 14 WHATs, no weighting): Typoena 52, Pomera 50,
Freewrite Traveler 47, reMarkable 44, Freewrite Smart Typewriter 42
(Traveler pre-Sailfish 44; Smart pre-Sailfish 39; reMarkable W1 dropped
3→2→1 across two rounds of author testing — first to 2 after firsthand
typing, then to 1 once latency was recognised as the dominant W1
signal). Pomera closing to within 2 of
Typoena is W14 doing what W14 should — surfacing the dimension on
which v0.1's tethered MVP loses ground that v0.8 is expected to
recover. The "Pomera + Wi-Fi + git + hackable BOM" framing from
`README.md` still holds and reads stronger.
Weighted totals (Σ score × W weight) tell the same story with more
contrast — left as exercise; the unweighted view is enough to read the
picture.
#### Caveats
- **Single-rater bias.** All thirteen rows are scored from the project
author's POV. A reMarkable buyer would weight W11 (battery) at 10 and
W12 (git) at 1, flipping the totals.
- **Configuration matters.** Freewrite Smart Typewriter and Traveler are
both tracked; they diverge most on W1 / W5 because of display tech
(Smart's larger panel is slower to refresh). Traveler is still the
more direct competitor on form factor.
- **W3 / W6 Freewrite scores are anecdotal.** Forum reports, not bench
data. Treat the 2 / 4 as "we'd need to test this" rather than fact.
- **No price column.** Typoena-as-BOM is materially cheaper than the
competitors but cost is not a WHAT in §1, so it's absent here.
Worth a row if a v0.x WHAT ever calls it out.
### Regenerating
The matrix cells (`\node[qfdrel/{S,M,W}]`), roof symbols (`C-i-j` slots),
and basement Σ + Rel% are native to this file — re-score directly in the
TikZ source, then update the §3 priority list and §4 conflict list in
the narrative above to match the new picture.
When §1 or §2 changes:
1. Importance column → §1 weight column.
2. HOW titles + v0.1 targets → §2 target column.
3. Recompute basement Σ for any HOW whose column changed: per-cell
contribution = `(W weight) × (cell strength: 9 / 3 / 1 / 0)`.
4. Recompute relative weight: each Σ ÷ total Σ × 100, rounded to integer
percent.
Perception scores are **not** derived from §1/§2 — they live only in
this file. Update them when (a) a competitor ships a relevant change,
(b) measurement replaces a guess, or (c) a WHAT is added/removed in §1.
Each score keeps its one-line rationale in the table above.
If a renderer rejects the `tikz` fence, the file is still readable as
source — the placement comments name each WHAT, HOW, and cell. The
perception-scores table above is the human-readable fallback for the
right-hand zone of the diagram.
---
## 4. Roof — HOW-vs-HOW tradeoffs
@@ -167,9 +829,9 @@ before the runtime decision does.
The roof shows where pushing one characteristic pushes another the wrong way.
ASCII glyphs (with classical QFD equivalents): **`++`** strong
reinforcement (`◎`), **`+`** mild reinforcement (`○`), **``** mild
conflict (`×`), **``** strong conflict (`⊗`). The 14×14 roof matrix
lives in [`quality-house.md`](quality-house.md); the cells that
actually shape the design are called out below.
conflict (`×`), **``** strong conflict (`⊗`). The 14×14 roof matrix is
in the §3 diagram; the cells that actually shape the design are called out
below.
### Conflicts that actually shape the design
@@ -437,8 +1099,8 @@ These are the live tensions we are watching, not deciding harder:
honest reading that "codebase absorbs the planned roadmap" is
delivered by ADRs, not by a measurable characteristic. ID "H14" left
as a gap (cross-doc HOW references survive without renumbering H15).
Total basement Σ drops 1674 → 1557, so rel% recomputed in
[`quality-house.md`](quality-house.md).
Total basement Σ drops 1674 → 1557, so rel% recomputed in the §3
basement.
- **HOWs renamed "characteristics," not "functions."** A function is a
transformation (input → output); HOWs like H6 "success rate" and
H10 "binary size" are *measures* of functions or properties of
@@ -516,8 +1178,8 @@ README, the product/technical docs, and [ADR-003] are all updated to ~13 lines
reality drifts from estimates.
- The WHATs (§1) change rarely; the HOWs (§2) change with each release.
When either changes, re-score the matrix and recompute the basement Σ
in [`quality-house.md`](quality-house.md); then check the §3 priority
list and §4 conflict list here still match the new picture.
in the §3 diagram; then check the §3 priority list and §4 conflict list
here still match the new picture.
[ADR-001]: adr.md#adr-001-language-and-runtime--rust-on-esp-idf-rs-std
[ADR-002]: adr.md#adr-002-ui-strategy--custom-widgets-on-embedded-graphics-not-ratatui

View File

@@ -1,9 +1,9 @@
# Quality House — empty (training)
Same 14 WHATs × 14 HOWs chassis as [`quality-house.md`](quality-house.md),
with the **relations**, **roof correlations**, and basement Σ / Rel %
**deliberately blank**. Fill them in yourself, then compare against the
populated house to check your reading.
Same 14 WHATs × 14 HOWs chassis as the filled House in
[`qfd.md` §3](qfd.md#3-house-of-quality--whats--hows), with the **relations**,
**roof correlations**, and basement Σ / Rel % **deliberately blank**. Fill them
in yourself, then compare against the populated house to check your reading.
Kept (definitional inputs from `qfd.md`):
@@ -368,9 +368,10 @@ Blank (training surface):
4. **Basement Rel %.** Each Σ ÷ total Σ × 100, rounded to integer percent
(should sum to ~100 across HOWs).
Once filled, diff your numbers against [`quality-house.md`](quality-house.md)
basement and roof to see where your reading agrees or differs — divergences
are usually the most interesting part.
Once filled, diff your numbers against the filled House in
[`qfd.md` §3](qfd.md#3-house-of-quality--whats--hows) (basement and roof) to see
where your reading agrees or differs — divergences are usually the most
interesting part.
## Gotchas

View File

@@ -1,675 +0,0 @@
# Quality House
The 14 WHATs × 14 HOWs House of Quality. The roof carries function-vs-
function correlations; the basement carries v0.1 targets (mirrored from
[`qfd.md`](qfd.md) §2) plus the weighted-vote sums
(`Σ = Σ(W weight × cell strength)`) and rounded relative weights. The
narrative reading of these numbers lives in `qfd.md` §3 (priority list)
and §4 (conflict list).
This file is the authoritative source for the matrix cells, the roof
correlations, and the basement Σ + relative weights. WHATs (§1) and HOWs
(§2) are mirrored here from `qfd.md`; if those diverge, trust `qfd.md`.
Out of scope here: §5 component mapping (would need a second house),
§6 critical performance budget (already a curated rank), §7 tradeoffs
(narrative), §8 inconsistencies (history).
For a blank version of the same chassis (WHATs, HOWs, importance, and
v0.1 targets kept; relations + roof + Σ basement left empty for practice),
see [`quality-house-empty.md`](quality-house-empty.md).
The perception zone scores five products against the WHATs on a 05 scale.
**These are educated guesses, not measurements** — see the
[scoring rationale](#perception-scores-guessed) below for what each cell
is based on. Useful for self-positioning ("where do we land vs the
market"), not as a fair head-to-head buyer's guide.
```tikz
% =====================================================================
% QFD "House of Quality" preamble
% =====================================================================
\usetikzlibrary{arrows.meta, positioning, shapes.geometric, shapes.misc, calc, fit, backgrounds}
\newif\ifqfdshowroof \qfdshowrooftrue
\newif\ifqfdshowbasement \qfdshowbasementtrue
\newif\ifqfdshowcompetitive \qfdshowcompetitivetrue
\newif\ifqfdshowlegend \qfdshowlegendtrue
\newif\ifqfdshowimportance \qfdshowimportancetrue
\newif\ifqfdshowcorrlegend \qfdshowcorrlegendtrue
\newif\ifqfdshowevallegend \qfdshowevallegendtrue
\def\qfdNW{5}
\def\qfdNH{5}
\def\qfdWhatW{4.0}
\def\qfdImpW{0.9}
\def\qfdCmpW{3}
\def\qfdHdrH{2.6}
\def\qfdBasementN{4}
\def\qfdWhatsTitle{Customer needs}
\def\qfdImpTitle{Imp.\ \%}
\def\qfdPerceptionTitle{Comparative evaluation}
\def\qfdPoorLabel{poor}
\def\qfdExcellentLabel{excellent}
\def\qfdAltOneLabel{Typoena}
\def\qfdAltTwoLabel{Competitor A}
\def\qfdAltThreeLabel{Competitor B}
\def\qfdRelTitle{Relation}
\def\qfdCorrTitle{Correlation}
\def\qfdEvalTitle{Evaluation}
\tikzset{
qfdthin/.style ={line width=0.35pt},
qfdmed/.style ={line width=0.7pt},
qfdstrong/.style={circle, draw, fill=black,
minimum size=7pt, inner sep=0pt},
qfdmod/.style ={circle, draw,
minimum size=7pt, inner sep=0pt, line width=0.8pt},
qfdweak/.style ={regular polygon, regular polygon sides=3, draw,
minimum size=8.5pt, inner sep=0pt, line width=0.7pt},
qfdrel/.is choice,
qfdrel/S/.style={qfdstrong},
qfdrel/M/.style={qfdmod},
qfdrel/W/.style={qfdweak},
qfdalt1mk/.style={circle, draw, fill=black,
minimum size=6pt, inner sep=0pt, line width=1pt},
qfdalt1ln/.style={line width=1.2pt},
qfdalt2mk/.style={regular polygon, regular polygon sides=3, draw,
fill=black, minimum size=6pt, inner sep=0pt,
line width=0.7pt},
qfdalt2ln/.style={line width=0.7pt, dashed},
qfdalt3mk/.style={rectangle, draw, fill=black,
minimum size=5pt, inner sep=0pt, line width=0.7pt},
qfdalt3ln/.style={line width=0.7pt, dotted},
}
\newcommand{\qfdDrawGrid}{%
\foreach \c in {1,...,\qfdNHm} \draw[qfdthin] (\c, 0) -- (\c, -\qfdNW);
\foreach \r in {1,...,\qfdNWm} \draw[qfdthin] (0, -\r) -- (\qfdNH, -\r);
\foreach \r in {1,...,\qfdNWm}
\draw[qfdthin] (\qfdLeftEdge, -\r) -- (0, -\r);
\ifqfdshowroof
\foreach \c in {1,...,\qfdNHm}
\draw[qfdthin] (\c, 0) -- (\c, \qfdHdrH);
\fi
\ifqfdshowcompetitive
\foreach \r in {1,...,\qfdNWm}
\draw[qfdthin] (\qfdNH, -\r) -- (\qfdNH+\qfdCmpW, -\r);
\fi
\ifqfdshowbasement
\foreach \r in {1,...,\qfdBasementN}
\draw[qfdthin] (0, -\qfdNW-\r) -- (\qfdNH, -\qfdNW-\r);
\foreach \c in {1,...,\qfdNHm}
\draw[qfdthin] (\c, -\qfdNW) -- (\c, -\qfdNW-\qfdBasementN);
\fi
}
\newcommand{\qfdDrawRoof}{%
\ifqfdshowroof
\foreach \k in {1,...,\qfdNHm} {%
\pgfmathsetmacro{\rx}{(\k+\qfdNH)/2}
\pgfmathsetmacro{\ry}{\qfdHdrH + (\qfdNH-\k)/2}
\pgfmathsetmacro{\lx}{\k/2}
\pgfmathsetmacro{\ly}{\qfdHdrH + \k/2}
\draw[qfdthin] (\k, \qfdHdrH) -- (\rx, \ry);
\draw[qfdthin] (\k, \qfdHdrH) -- (\lx, \ly);
}%
\draw[qfdmed] (0, \qfdHdrH)
-- (\qfdNH/2, \qfdApexY) -- (\qfdNH, \qfdHdrH);
\foreach \i in {1,...,\qfdNH}
\foreach \k in {1,...,\qfdNH} {%
\pgfmathtruncatemacro{\jj}{\i+\k}
\ifnum\jj>\qfdNH\relax\else
\pgfmathsetmacro{\xx}{\i + \k/2 - 0.5}
\pgfmathsetmacro{\yy}{\qfdHdrH + \k/2}
\coordinate (C-\i-\jj) at (\xx, \yy);
\fi
}%
\fi
}
\newcommand{\qfdDrawScale}{%
\ifqfdshowcompetitive
\foreach \tk in {0,1,2,3,4,5} {%
\pgfmathsetmacro{\tx}{\qfdNH + (\tk+0.5)*\qfdCmpW/6}
\node[anchor=south, font=\scriptsize] at (\tx, 0.02) {\tk};
}%
\node[anchor=south, font=\scriptsize\bfseries, align=center,
text width=\qfdCmpW cm]
at ({\qfdNH + \qfdCmpW/2}, 0.7) {\qfdPerceptionTitle};
\node[anchor=north, font=\scriptsize\itshape]
at ({\qfdNH + 0.45}, -\qfdNW) {\qfdPoorLabel};
\node[anchor=north, font=\scriptsize\itshape]
at ({\qfdNH + \qfdCmpW - 0.45}, -\qfdNW) {\qfdExcellentLabel};
\fi
}
\newcommand{\qfdDrawZoneTitles}{%
\ifqfdshowimportance
\node[rotate=90, anchor=west, font=\footnotesize\bfseries]
at ({-\qfdImpW/2}, 0.12) {\qfdImpTitle};
\fi
\node[font=\scriptsize\bfseries, align=center, text width=\qfdWhatW cm]
at ({\qfdLeftEdge + \qfdWhatW/2},
{\ifqfdshowroof \qfdHdrH/2 \else 0.6 \fi}) {\qfdWhatsTitle};
}
\newcommand{\qfdDrawFrames}{%
\begin{scope}[qfdmed]
\draw (\qfdLeftEdge, 0) rectangle (\qfdNH, -\qfdNW);
\ifqfdshowimportance \draw (-\qfdImpW, 0) -- (-\qfdImpW, -\qfdNW); \fi
\draw (0, 0) -- (0, -\qfdNW);
\ifqfdshowroof
\draw (0, 0) rectangle (\qfdNH, \qfdHdrH); \fi
\ifqfdshowbasement
\draw (0, -\qfdNW) rectangle (\qfdNH, -\qfdNW-\qfdBasementN); \fi
\ifqfdshowcompetitive
\draw (\qfdNH, 0) rectangle (\qfdNH+\qfdCmpW, -\qfdNW); \fi
\end{scope}
}
\newcommand{\qfdDrawLegend}{%
\ifqfdshowlegend
\pgfmathsetmacro{\qfdLegX}{%
\qfdNH + \ifqfdshowcompetitive \qfdCmpW + 0.7 \else 0.7 \fi}
\pgfmathsetmacro{\qfdLegBottom}{%
-2.05
\ifqfdshowroof \ifqfdshowcorrlegend - 2.55 \fi \fi
\ifqfdshowcompetitive \ifqfdshowevallegend - 2.20 \fi \fi}
\pgfmathsetmacro{\qfdLegY}{\qfdHdrH - 0.4}
\begin{scope}[shift={(\qfdLegX, \qfdLegY)}]
\draw[qfdmed, rounded corners=2pt]
(-0.15, 0.4) rectangle (4.5, \qfdLegBottom);
\node[anchor=west, font=\footnotesize\bfseries] at (0, 0.1)
{\qfdRelTitle};
\draw[qfdthin] (0, -0.15) -- (4.35, -0.15);
\node[qfdstrong] at (0.22, -0.5) {};
\node[anchor=west] at (0.5, -0.5) {Strong (9)};
\node[qfdmod] at (0.22, -0.95) {};
\node[anchor=west] at (0.5, -0.95) {Medium (3)};
\node[qfdweak] at (0.22, -1.4) {};
\node[anchor=west] at (0.5, -1.4) {Weak (1)};
\ifqfdshowroof \ifqfdshowcorrlegend
\node[anchor=west, font=\footnotesize\bfseries] at (0, -2.10)
{\qfdCorrTitle};
\draw[qfdthin] (0, -2.35) -- (4.35, -2.35);
\node[anchor=west] at (0, -2.70) {{$+\!+$}\quad very positive};
\node[anchor=west] at (0, -3.05) {{$+$\phantom{$+$}}\quad positive};
\node[anchor=west] at (0, -3.40) {{$-$\phantom{$-$}}\quad negative};
\node[anchor=west] at (0, -3.75) {{$-\!-$}\quad very negative};
\fi \fi
\ifqfdshowcompetitive \ifqfdshowevallegend
\pgfmathsetmacro{\qfdEvalTop}{%
-2.10 \ifqfdshowroof\ifqfdshowcorrlegend - 2.55 \fi\fi}
\node[anchor=west, font=\footnotesize\bfseries]
at (0, \qfdEvalTop) {\qfdEvalTitle};
\pgfmathsetmacro{\qfdEvalSep}{\qfdEvalTop - 0.25}
\draw[qfdthin] (0, \qfdEvalSep) -- (4.35, \qfdEvalSep);
\pgfmathsetmacro{\qfdLegA}{\qfdEvalTop - 0.55}
\draw[qfdalt1ln] (0.05, \qfdLegA) -- (0.45, \qfdLegA);
\node[qfdalt1mk] at (0.25, \qfdLegA) {};
\node[anchor=west, font=\bfseries] at (0.55, \qfdLegA)
{\qfdAltOneLabel};
\pgfmathsetmacro{\qfdLegB}{\qfdEvalTop - 0.95}
\draw[qfdalt2ln] (0.05, \qfdLegB) -- (0.45, \qfdLegB);
\node[qfdalt2mk] at (0.25, \qfdLegB) {};
\node[anchor=west] at (0.55, \qfdLegB) {\qfdAltTwoLabel};
\pgfmathsetmacro{\qfdLegC}{\qfdEvalTop - 1.35}
\draw[qfdalt3ln] (0.05, \qfdLegC) -- (0.45, \qfdLegC);
\node[qfdalt3mk] at (0.25, \qfdLegC) {};
\node[anchor=west] at (0.55, \qfdLegC) {\qfdAltThreeLabel};
\fi \fi
\end{scope}
\fi
}
\newenvironment{qfdhouse}{%
\begin{tikzpicture}[x=1cm, y=1cm, font=\scriptsize,
line cap=round, line join=round]
\ifqfdshowimportance
\pgfmathsetmacro{\qfdLeftEdge}{-\qfdWhatW-\qfdImpW}
\else
\pgfmathsetmacro{\qfdLeftEdge}{-\qfdWhatW}
\fi
\pgfmathsetmacro{\qfdApexY}{\qfdHdrH + \qfdNH/2}
\pgfmathtruncatemacro{\qfdNHm}{\qfdNH - 1}
\pgfmathtruncatemacro{\qfdNWm}{\qfdNW - 1}
\qfdDrawGrid
\qfdDrawRoof
\qfdDrawScale
\qfdDrawZoneTitles
}{%
\qfdDrawFrames
\qfdDrawLegend
\end{tikzpicture}%
}
% --- Dimensions tuned for the typewriter QFD (14 W x 15 H) ---
\def\qfdNW{14}
\def\qfdNH{14}
\def\qfdWhatW{4.6}
\def\qfdImpW{0.7}
\def\qfdHdrH{5.0}
\def\qfdBasementN{3}
\def\qfdCmpW{3.4}
\qfdshowlegendfalse % we draw a 4-alternative legend manually
\def\qfdWhatsTitle{User-facing requirements (W)}
\def\qfdImpTitle{Weight}
\def\qfdPerceptionTitle{Competitive perception\\(05, guessed)}
\def\qfdPoorLabel{poor}
\def\qfdExcellentLabel{excellent}
% Perception-zone markers: shape + colour-blind-safe colour per product.
% Palette is Okabe-Ito (blue, vermillion, bluish green, reddish purple).
% Light fills + saturated outlines keep stacked markers legible.
\definecolor{qfdcTypoena}{RGB}{0,114,178}
\definecolor{qfdcRem}{RGB}{213,94,0}
\definecolor{qfdcFrw}{RGB}{0,158,115}
\definecolor{qfdcPom}{RGB}{204,121,167}
\definecolor{qfdcFrwS}{RGB}{86,180,233}
\tikzset{
qfdalt1mk/.style={circle, draw=qfdcTypoena, fill=qfdcTypoena!55!white,
minimum size=6.5pt, inner sep=0pt, line width=1.1pt},
qfdalt1ln/.style={line width=1.2pt, qfdcTypoena},
qfdalt2mk/.style={regular polygon, regular polygon sides=3,
draw=qfdcRem, fill=qfdcRem!55!white,
minimum size=7pt, inner sep=0pt, line width=0.9pt},
qfdalt2ln/.style={line width=0.8pt, dashed, qfdcRem},
qfdalt3mk/.style={rectangle, draw=qfdcFrw, fill=qfdcFrw!55!white,
minimum size=5.5pt, inner sep=0pt, line width=0.9pt},
qfdalt3ln/.style={line width=0.8pt, dotted, qfdcFrw},
qfdalt4mk/.style={diamond, aspect=1, draw=qfdcPom,
fill=qfdcPom!50!white,
minimum size=7pt, inner sep=0pt, line width=1.0pt},
qfdalt4ln/.style={line width=0.8pt, dash dot, qfdcPom},
qfdalt5mk/.style={regular polygon, regular polygon sides=5,
draw=qfdcFrwS, fill=qfdcFrwS!40!white,
minimum size=5.5pt, inner sep=0pt, line width=0.8pt},
qfdalt5ln/.style={line width=0.7pt, dash dot dot, qfdcFrwS},
}
\begin{document}
\begin{qfdhouse}
% ---------- WHATs (left column) ----------
% Box width is 0.2 cm narrower than the column so labels have 0.1 cm
% clearance on each side and don't bleed into the Weight column.
\pgfmathsetmacro{\qfdWhatTextW}{\qfdWhatW - 0.2}
\foreach \r/\t in {%
1/{W1 Sub-second visible response to typing},
2/{W2 Publishing is one deliberate action away},
3/{W3 Pulling power never corrupts the file},
4/{W4 Provisioning never interrupts a writing session},
5/{W5 Quick boot to a writing cursor},
6/{W6 Long sessions without crash, lag, drift},
7/{W7 Nothing on the device competes with prose},
8/{W8 The UI never moves except when I move it},
9/{W9 Codebase absorbs the planned roadmap},
10/{W10 I can repair or fork it with hobbyist tools},
11/{W11 Multi-day battery life (v0.8 onward)},
12/{W12 Local-only files coexist with git scope (v0.5+)},
13/{W13 Typography sets a writing-tool tone},
14/{W14 I can carry the device and write away from a desk}%
}
\node[anchor=west, font=\scriptsize,
text width=\qfdWhatTextW cm, align=left]
at ({\qfdLeftEdge + 0.1}, {-\r + 0.5}) {\t};
% ---------- Importance (raw 1-10 weight) ----------
\foreach \r/\w in {1/10, 2/9, 3/10, 4/7, 5/6, 6/9, 7/8, 8/7,
9/8, 10/5, 11/4, 12/5, 13/7, 14/8}
\node[font=\scriptsize] at ({-\qfdImpW/2}, {-\r + 0.5}) {\w};
% ---------- HOWs (rotated column titles) ----------
\foreach \c/\t in {%
1/{H1 Type latency},
2/{H2 Refresh area per keystroke},
3/{H3 Full-refresh cadence},
4/{H4 Boot latency (cold)},
5/{H5 Continuous-typing endurance},
6/{H6 Publish reliability},
7/{H7 Publish latency},
8/{H8 Save durability},
9/{H9 PSRAM heap headroom},
10/{H10 Firmware binary size},
11/{H11 Total stack budget},
12/{H12 Network reconnect time},
13/{H13 Idle / typing / push current},
14/{H15 Clean release build time}%
}
\node[rotate=90, anchor=west, font=\scriptsize]
at ({\c - 0.5}, 0.15) {\t};
% ---------- Relation matrix (S=9, M=3, W=1) ----------
% W1 row 1: H1S H2S H3M H5M H9W H11W
\node[qfdrel/S] at ({1 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/S] at ({2 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/M] at ({3 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/M] at ({5 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/W] at ({9 - 0.5}, {-1 + 0.5}) {};
\node[qfdrel/W] at ({11 - 0.5}, {-1 + 0.5}) {};
% W2 row 2: H6S H7M H9S H12S
\node[qfdrel/S] at ({6 - 0.5}, {-2 + 0.5}) {};
\node[qfdrel/M] at ({7 - 0.5}, {-2 + 0.5}) {};
\node[qfdrel/S] at ({9 - 0.5}, {-2 + 0.5}) {};
\node[qfdrel/S] at ({12 - 0.5}, {-2 + 0.5}) {};
% W3 row 3: H8S
\node[qfdrel/S] at ({8 - 0.5}, {-3 + 0.5}) {};
% W4 row 4: H6M H12M
\node[qfdrel/M] at ({6 - 0.5}, {-4 + 0.5}) {};
\node[qfdrel/M] at ({12 - 0.5}, {-4 + 0.5}) {};
% W5 row 5: H4S H10M
\node[qfdrel/S] at ({4 - 0.5}, {-5 + 0.5}) {};
\node[qfdrel/M] at ({10 - 0.5}, {-5 + 0.5}) {};
% W6 row 6: H1M H3M H5S H6M H8M H9S H11M H12M
\node[qfdrel/M] at ({1 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({3 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/S] at ({5 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({6 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({8 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/S] at ({9 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({11 - 0.5}, {-6 + 0.5}) {};
\node[qfdrel/M] at ({12 - 0.5}, {-6 + 0.5}) {};
% W7 row 7: H1M H2M H3M H13M
\node[qfdrel/M] at ({1 - 0.5}, {-7 + 0.5}) {};
\node[qfdrel/M] at ({2 - 0.5}, {-7 + 0.5}) {};
\node[qfdrel/M] at ({3 - 0.5}, {-7 + 0.5}) {};
\node[qfdrel/M] at ({13 - 0.5}, {-7 + 0.5}) {};
% W8 row 8: H1W H2S H3S
\node[qfdrel/W] at ({1 - 0.5}, {-8 + 0.5}) {};
\node[qfdrel/S] at ({2 - 0.5}, {-8 + 0.5}) {};
\node[qfdrel/S] at ({3 - 0.5}, {-8 + 0.5}) {};
% W9 row 9: H10W H11W H15M
\node[qfdrel/W] at ({10 - 0.5}, {-9 + 0.5}) {};
\node[qfdrel/W] at ({11 - 0.5}, {-9 + 0.5}) {};
\node[qfdrel/M] at ({14 - 0.5}, {-9 + 0.5}) {};
% W10 row 10: H10M H13W H15W
\node[qfdrel/M] at ({10 - 0.5}, {-10 + 0.5}) {};
\node[qfdrel/W] at ({13 - 0.5}, {-10 + 0.5}) {};
\node[qfdrel/W] at ({14 - 0.5}, {-10 + 0.5}) {};
% W11 row 11: H13S
\node[qfdrel/S] at ({13 - 0.5}, {-11 + 0.5}) {};
% W12 row 12: H6W H8M
\node[qfdrel/W] at ({6 - 0.5}, {-12 + 0.5}) {};
\node[qfdrel/M] at ({8 - 0.5}, {-12 + 0.5}) {};
% W13 row 13: H9M
\node[qfdrel/M] at ({9 - 0.5}, {-13 + 0.5}) {};
% W14 row 14: H4W H8M H12M H13S
\node[qfdrel/W] at ({4 - 0.5}, {-14 + 0.5}) {};
\node[qfdrel/M] at ({8 - 0.5}, {-14 + 0.5}) {};
\node[qfdrel/M] at ({12 - 0.5}, {-14 + 0.5}) {};
\node[qfdrel/S] at ({13 - 0.5}, {-14 + 0.5}) {};
% ---------- Roof correlations ----------
\node[font=\scriptsize] at (C-1-2) {$+\!+$}; % H1-H2 strong reinforce
\node[font=\scriptsize] at (C-1-3) {$-$}; % H1-H3 mild conflict
\node[font=\scriptsize] at (C-1-5) {$+$}; % H1-H5 mild reinforce
\node[font=\scriptsize] at (C-1-13) {$-$}; % H1-H13 mild conflict
\node[font=\scriptsize] at (C-2-3) {$+\!+$}; % H2-H3 strong reinforce
\node[font=\scriptsize] at (C-2-13) {$+$}; % H2-H13
\node[font=\scriptsize] at (C-3-13) {$+$}; % H3-H13
\node[font=\scriptsize] at (C-4-10) {$-$}; % H4-H10 boot vs binary
\node[font=\scriptsize] at (C-5-6) {$+$}; % H5-H6
\node[font=\scriptsize] at (C-5-8) {$+$}; % H5-H8
\node[font=\scriptsize] at (C-5-9) {$-\!-$}; % H5-H9 soak vs heap
\node[font=\scriptsize] at (C-6-7) {$+$}; % H6-H7
\node[font=\scriptsize] at (C-6-9) {$-\!-$}; % H6-H9 push vs heap
\node[font=\scriptsize] at (C-6-12) {$+\!+$}; % H6-H12
\node[font=\scriptsize] at (C-7-9) {$-$}; % H7-H9
\node[font=\scriptsize] at (C-7-12) {$+\!+$}; % H7-H12
\node[font=\scriptsize] at (C-9-10) {$-\!-$}; % H9-H10 heap vs binary
\node[font=\scriptsize] at (C-10-14) {$-\!-$}; % H10-H15 binary vs build
\node[font=\scriptsize] at (C-11-13) {$-$}; % H11-H13
% ---------- Basement: target / abs weight / rel weight % ----------
\foreach \c/\tgt/\abs/\rel in {%
1/{$\leq$200\,ms}/148/10,
2/{$\leq$1 line}/177/11,
3/{1 : 64}/144/9,
4/{$\leq$5\,s}/62/4,
5/{$\geq$1\,h}/111/7,
6/{$\geq$95\,\%}/134/9,
7/{$\leq$30\,s}/27/2,
8/{100\,\%}/156/10,
9/{$\geq$1\,MB}/193/12,
10/{$\leq$2\,MB}/41/3,
11/{$\leq$80\,KB}/45/3,
12/{$\leq$30\,s}/153/10,
13/{obs.}/137/9,
14/{$\leq$7\,min}/29/2%
} {
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 0.5}) {\tgt};
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 1.5}) {\abs};
\node[font=\scriptsize\bfseries]
at ({\c - 0.5}, {-\qfdNW - 2.5}) {\rel};
}
% ---------- Basement row labels (in the margin below WHATs) ----------
\foreach \k/\lbl in {1/{Target (v0.1)}, 2/{$\Sigma$ abs}, 3/{Rel.\ \%}}
\node[anchor=east, font=\scriptsize\itshape]
at ({-0.1}, {-\qfdNW - \k + 0.5}) {\lbl};
% ---------- Perception zone: 5 products x 14 WHATs (0-5 scores) ----------
% Columns: \so=Typoena target, \st=reMarkable 2 + Type Folio,
% \sf=Freewrite Traveler, \sg=Pomera DM250,
% \sh=Freewrite Smart Typewriter.
% Pass 1: stash each score as a named coordinate so the profile lines
% below can reuse it without recomputing.
\foreach \r/\so/\st/\sf/\sg/\sh in {%
1/4/1/4/5/3,
2/5/4/4/2/4,
3/4/4/2/2/2,
4/5/2/2/5/2,
5/4/3/4/5/4,
6/3/3/4/5/4,
7/5/2/5/5/5,
8/4/3/4/5/4,
9/4/3/2/1/2,
10/5/4/2/1/2,
11/1/5/5/4/5,
12/3/1/2/3/2,
13/3/5/2/2/2,
14/2/4/5/2/5%
} {
\pgfmathsetmacro{\xo}{\qfdNH + (\so + 0.5)*\qfdCmpW/6}
\pgfmathsetmacro{\xt}{\qfdNH + (\st + 0.5)*\qfdCmpW/6}
\pgfmathsetmacro{\xf}{\qfdNH + (\sf + 0.5)*\qfdCmpW/6}
\pgfmathsetmacro{\xg}{\qfdNH + (\sg + 0.5)*\qfdCmpW/6}
\pgfmathsetmacro{\xs}{\qfdNH + (\sh + 0.5)*\qfdCmpW/6}
\coordinate (po-\r) at (\xo, {-\r + 0.5});
\coordinate (pr-\r) at (\xt, {-\r + 0.5});
\coordinate (pf-\r) at (\xf, {-\r + 0.5});
\coordinate (pp-\r) at (\xg, {-\r + 0.5});
\coordinate (ps-\r) at (\xs, {-\r + 0.5});
}
% Pass 2: profile lines per alternative. Drawn before markers so the
% dots sit on top of (not under) the line endpoints.
\draw[qfdalt1ln] (po-1) \foreach \r in {2,...,\qfdNW} { -- (po-\r) };
\draw[qfdalt2ln] (pr-1) \foreach \r in {2,...,\qfdNW} { -- (pr-\r) };
\draw[qfdalt3ln] (pf-1) \foreach \r in {2,...,\qfdNW} { -- (pf-\r) };
\draw[qfdalt4ln] (pp-1) \foreach \r in {2,...,\qfdNW} { -- (pp-\r) };
\draw[qfdalt5ln] (ps-1) \foreach \r in {2,...,\qfdNW} { -- (ps-\r) };
% Pass 3: markers on top of the lines.
\foreach \r in {1,...,\qfdNW} {
\node[qfdalt1mk] at (po-\r) {};
\node[qfdalt2mk] at (pr-\r) {};
\node[qfdalt3mk] at (pf-\r) {};
\node[qfdalt4mk] at (pp-\r) {};
\node[qfdalt5mk] at (ps-\r) {};
}
% ---------- Manual legend (5 alternatives, placed right of zones) ----------
\pgfmathsetmacro{\qfdLegX}{\qfdNH + \qfdCmpW + 0.7}
\begin{scope}[shift={(\qfdLegX, \qfdHdrH - 0.4)}]
\draw[qfdmed, rounded corners=2pt]
(-0.15, 0.4) rectangle (5.1, -7.50);
% Relations
\node[anchor=west, font=\footnotesize\bfseries] at (0, 0.1)
{Relation};
\draw[qfdthin] (0, -0.15) -- (4.95, -0.15);
\node[qfdstrong] at (0.22, -0.5) {};
\node[anchor=west] at (0.5, -0.5) {Strong (9)};
\node[qfdmod] at (0.22, -0.95) {};
\node[anchor=west] at (0.5, -0.95) {Medium (3)};
\node[qfdweak] at (0.22, -1.4) {};
\node[anchor=west] at (0.5, -1.4) {Weak (1)};
% Correlation
\node[anchor=west, font=\footnotesize\bfseries] at (0, -2.10)
{Correlation};
\draw[qfdthin] (0, -2.35) -- (4.95, -2.35);
\node[anchor=west] at (0, -2.70) {{$+\!+$}\quad very positive};
\node[anchor=west] at (0, -3.05) {{$+$\phantom{$+$}}\quad positive};
\node[anchor=west] at (0, -3.40) {{$-$\phantom{$-$}}\quad negative};
\node[anchor=west] at (0, -3.75) {{$-\!-$}\quad very negative};
% Perception
\node[anchor=west, font=\footnotesize\bfseries] at (0, -4.20)
{Perception};
\draw[qfdthin] (0, -4.45) -- (4.95, -4.45);
\draw[qfdalt1ln] (0.05, -4.80) -- (0.45, -4.80);
\node[qfdalt1mk] at (0.25, -4.80) {};
\node[anchor=west, font=\bfseries] at (0.55, -4.80)
{Typoena (v0.1 target)};
\draw[qfdalt2ln] (0.05, -5.25) -- (0.45, -5.25);
\node[qfdalt2mk] at (0.25, -5.25) {};
\node[anchor=west] at (0.55, -5.25) {reMarkable 2 + Type Folio};
\draw[qfdalt3ln] (0.05, -5.70) -- (0.45, -5.70);
\node[qfdalt3mk] at (0.25, -5.70) {};
\node[anchor=west] at (0.55, -5.70) {Freewrite Traveler};
\draw[qfdalt5ln] (0.05, -6.15) -- (0.45, -6.15);
\node[qfdalt5mk] at (0.25, -6.15) {};
\node[anchor=west] at (0.55, -6.15) {Freewrite Smart Typewriter};
\draw[qfdalt4ln] (0.05, -6.60) -- (0.45, -6.60);
\node[qfdalt4mk] at (0.25, -6.60) {};
\node[anchor=west] at (0.55, -6.60) {Pomera DM250};
\node[anchor=west, font=\scriptsize\itshape] at (0, -7.15)
{0 = poor, 5 = excellent};
\end{scope}
\end{qfdhouse}
\end{document}
```
## Perception scores (guessed)
Five products on the 05 scale, scored against each WHAT. Reference
configurations: **reMarkable 2 + Type Folio**, **Freewrite Traveler**,
**Freewrite Smart Typewriter**, **Pomera DM250** (DM250 has a reflective
monochrome LCD, not e-ink — flagged in W1 / W8). "Typoena" is the v0.1
target from `qfd.md` §2, not measured yet.
Freewrite Traveler scores assume the
[Sailfish firmware](https://getfreewrite.com/blogs/writing-success/freewrite-sailfish-firmware)
(released 2025-11-19), which rewrote the OS in Rust, cut keystroke latency
40100 %, and trimmed power draw 30 % typing / 50 % idle on both
Traveler and Smart Typewriter Gen 3. Three rows rescored upward as a
result: W1 Traveler 3→4 / Smart 2→3 (Smart's larger panel still trails
Traveler by one notch), W5 both 3→4 (boot accelerated, no published
number), W9 both 1→2 (Rust rewrite explicitly unblocked features that
JS could not carry; still closed so neither reaches reMarkable's
hackable-Linux 3).
| ID | WHAT (truncated) | Typoena | reM. | Frw.T | Frw.S | Pom. | Rationale (shortest defensible) |
| --- | ------------------------------------------------- | :-----: | :--: | :---: | :---: | :--: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| W1 | Sub-second response to typing | 4 | 1 | 4 | 3 | 5 | Typoena targets ≤200 ms; reMarkable e-ink visibly laggy on a typing-focused device — tested less responsive than Smart Typewriter, and latency is so load-bearing for W1 that it earns a 1 not a 2; both Freewrites post-Sailfish trimmed latency 40100 % (Frw.T plausibly inside 200 ms; Frw.S still trails by one notch on larger panel); Pomera LCD ~zero. |
| W2 | Publishing is one deliberate action away | 5 | 4 | 4 | 4 | 2 | Ctrl-G atomic; reMarkable + Freewrite cloud-sync is one-tap but not git; Pomera = USB/SD copy or QR transfer. |
| W3 | Pulling power never corrupts the file | 4 | 4 | 2 | 2 | 2 | Typoena: atomic-rename + fsync. reMarkable journals. Freewrite + Pomera: forum reports of corruption on yank. |
| W4 | Provisioning never interrupts writing | 5 | 2 | 2 | 2 | 5 | Typoena v0.1: build-time config (dev-only). reM/Frw need Wi-Fi + account. Pomera: literally none. |
| W5 | Quick boot to a writing cursor | 4 | 3 | 4 | 4 | 5 | Typoena target ≤5 s. reMarkable cold-boots ~20 s (great from sleep). Both Freewrites accelerated post-Sailfish (no published number; were ~1015 s e-ink wake). Pomera ~3 s. |
| W6 | Long sessions without crash / lag / drift | 3 | 3 | 4 | 4 | 5 | Typoena unproven (1 h target). Freewrite famously stable (both variants). Pomera firmware is decades-mature. |
| W7 | Nothing on the device competes with prose | 5 | 2 | 5 | 5 | 5 | reMarkable has apps, menus, drawing, PDFs. Freewrite + Pomera are single-purpose; Typoena by design. |
| W8 | The UI never moves except when I move it | 4 | 3 | 4 | 4 | 5 | reMarkable animates more; Typoena uses dirty-rects; Freewrites minimal motion; Pomera near-static LCD. |
| W9 | Codebase absorbs the planned roadmap | 4 | 3 | 2 | 2 | 1 | Modular Rust Typoena; reMarkable is hackable Linux; both Freewrites carry Sailfish (Rust rewrite explicitly unblocked features JS could not carry) but closed; Pomera closed firmware. |
| W10 | I can repair or fork it with hobbyist tools | 5 | 4 | 2 | 2 | 1 | Typoena: open BOM + ESP32. reMarkable: rooted Linux + community ROMs. Freewrite + Pomera: closed. |
| W11 | Multi-day battery life (v0.8 onward) | 1 | 5 | 5 | 5 | 4 | Typoena v0.1 = wall-powered (battery deferred). reMarkable + both Freewrites legendary (~4 weeks; Sailfish trimmed 30 % typing / 50 % idle). Pomera ~24 h. |
| W12 | Local-only files coexist with git scope | 3 | 1 | 2 | 2 | 3 | Typoena v0.5+ design. reMarkable cloud-only. Freewrites have local + Postbox but no VCS. Pomera = pure local. |
| W13 | Typography sets a writing-tool tone | 3 | 5 | 2 | 2 | 2 | Typoena v0.1: single mono (serif option in v1.0). reMarkable: rich type rendering. Freewrite + Pomera: utilitarian. |
| W14 | I can carry the device and write away from a desk | 2 | 4 | 5 | 1 | 5 | Typoena v0.1 wall-powered (ADR-008), no enclosure spec yet — desk-bound by design. reMarkable + Type Folio bag-friendly with bulk. Freewrite Traveler is the form-factor reference (~1.6 lb, folds). Smart Typewriter ~5 lb, desk-bound. Pomera DM250 pocketable foldable. |
**Totals** (sum across 14 WHATs, no weighting): Typoena 52, Pomera 50,
Freewrite Traveler 47, reMarkable 44, Freewrite Smart Typewriter 42
(Traveler pre-Sailfish 44; Smart pre-Sailfish 39; reMarkable W1 dropped
3→2→1 across two rounds of author testing — first to 2 after firsthand
typing, then to 1 once latency was recognised as the dominant W1
signal). Pomera closing to within 2 of
Typoena is W14 doing what W14 should — surfacing the dimension on
which v0.1's tethered MVP loses ground that v0.8 is expected to
recover. The "Pomera + Wi-Fi + git + hackable BOM" framing from
`README.md` still holds and reads stronger.
Weighted totals (Σ score × W weight) tell the same story with more
contrast — left as exercise; the unweighted view is enough to read the
picture.
### Caveats
- **Single-rater bias.** All thirteen rows are scored from the project
author's POV. A reMarkable buyer would weight W11 (battery) at 10 and
W12 (git) at 1, flipping the totals.
- **Configuration matters.** Freewrite Smart Typewriter and Traveler are
both tracked; they diverge most on W1 / W5 because of display tech
(Smart's larger panel is slower to refresh). Traveler is still the
more direct competitor on form factor.
- **W3 / W6 Freewrite scores are anecdotal.** Forum reports, not bench
data. Treat the 2 / 4 as "we'd need to test this" rather than fact.
- **No price column.** Typoena-as-BOM is materially cheaper than the
competitors but cost is not a WHAT in `qfd.md` §1, so it's absent here.
Worth a row if a v0.x WHAT ever calls it out.
## Reading the house
- **Importance (left column)** is the raw 110 weight from `qfd.md` §1, not
a normalised %, so adding stays cheap when a WHAT shifts. Sum of weights
is 103; treat each unit as ~0.97 % if you want a percentage view.
- **Roof** carries the §4 symbols translated into classical QFD glyphs:
`++` strong reinforcement (`◎`), `+` mild reinforcement (`○`), `` mild
conflict (`×`), `` strong conflict (`⊗`).
- **Basement rows** are: v0.1 target → §3 column sum (`Σ` of
`weight × strength`) → relative weight as integer % of total (1557).
Relative weights round to 100.
- **H7, H10, H15** (Publish latency, binary size, build time) sit at the bottom
of the basement — knowingly-paid costs per `qfd.md` §7, not signals to
optimise harder.
## Regenerating
The matrix cells (`\node[qfdrel/{S,M,W}]`), roof symbols (`C-i-j` slots),
and basement Σ + Rel% are native to this file — re-score directly in the
TikZ source, then update the §3 priority list and §4 conflict list in
`qfd.md` to match the new picture.
When `qfd.md` §1 or §2 changes:
1. Importance column → §1 weight column.
2. HOW titles + v0.1 targets → §2 target column.
3. Recompute basement Σ for any HOW whose column changed: per-cell
contribution = `(W weight) × (cell strength: 9 / 3 / 1 / 0)`.
4. Recompute relative weight: each Σ ÷ total Σ × 100, rounded to integer
percent.
Perception scores are **not** derived from `qfd.md` — they live only in
this file. Update them when (a) a competitor ships a relevant change,
(b) measurement replaces a guess, or (c) a WHAT is added/removed in §1.
Each score keeps its one-line rationale in the table above.
If a renderer rejects the `tikz` fence, the file is still readable as
source — the placement comments name each WHAT, HOW, and cell. The
perception-scores table above is the human-readable fallback for the
right-hand zone of the diagram.