Compare commits
2 Commits
92068f1cce
...
fe544ccdd3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe544ccdd3 | ||
|
|
a700412773 |
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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 5–10 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. |
|
||||
|
||||
88
docs/notes/sync-latency.md
Normal file
88
docs/notes/sync-latency.md
Normal 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 ~2–4 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.
|
||||
696
docs/qfd.md
696
docs/qfd.md
@@ -15,11 +15,12 @@ with the v0.2–v1.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 (0–5, **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\\(0–5, 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 1–10 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 "+5–10 min" pricing, the target moves
|
||||
before the runtime decision does.
|
||||
|
||||
### Perception scores (guessed)
|
||||
|
||||
Five products on the 0–5 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
|
||||
40–100 %, 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 40–100 % (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 ~10–15 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 0–5 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\\(0–5, 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 0–5 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
|
||||
40–100 %, 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 40–100 % (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 ~10–15 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 1–10 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.
|
||||
Reference in New Issue
Block a user