31 KiB
meadows — Design (QFD)
Goal-driven design for the meadows editor, phase 1 (diagram; numeric simulation is a later phase). Scope: data model, canvas/rendering, interaction model, and persistence for building stock-and-flow Models. It relies on CONTEXT.md for vocabulary and ADR-0001 (loops detected from a polarity-annotated graph, not stored). "Simulation-ready" is a constraint here, not a goal: nothing in the data model should need a rewrite when simulation lands. See README.md for the project overview and how to run it.
Strength weights used in matrices: 9 strong, 3 medium, 1 weak, blank none.
House of Quality (visual)
One picture of §1 (Goals, left), §2 (Functions, top), §4 (relation matrix, body),
§5 (roof, correlations) and §7/§4 (basement: targets + importance Σ + relative %).
The tables below stay authoritative — regenerate this when they change. Relation
symbols: ● strong (9) · ◐ medium (3) · △ weak (1). Roof: ++/+ reinforce,
−/−− conflict.
% =====================================================================
% QFD "House of Quality" preamble
% =====================================================================
\usetikzlibrary{arrows.meta, positioning, shapes.geometric, shapes.misc, calc, fit, backgrounds}
% Toggles — flip before \begin{qfdhouse} to hide sections.
\newif\ifqfdshowroof \qfdshowrooftrue
\newif\ifqfdshowbasement \qfdshowbasementtrue
\newif\ifqfdshowcompetitive \qfdshowcompetitivetrue
\newif\ifqfdshowlegend \qfdshowlegendtrue
\newif\ifqfdshowimportance \qfdshowimportancetrue
\newif\ifqfdshowcorrlegend \qfdshowcorrlegendtrue
\newif\ifqfdshowevallegend \qfdshowevallegendtrue
% Dimensions — override before \begin{qfdhouse} to resize.
\def\qfdNW{5} % number of WHATs (rows)
\def\qfdNH{5} % number of HOWs (columns)
\def\qfdWhatW{4.0} % width of WHATs column
\def\qfdImpW{0.9} % width of importance column
\def\qfdCmpW{3} % width of perception zone
\def\qfdHdrH{2.6} % height of column-titles band
\def\qfdBasementN{4} % number of basement rows
% Titles & labels — override before \begin{qfdhouse}.
\def\qfdWhatsTitle{Customer needs}
\def\qfdImpTitle{Imp.\ \%}
\def\qfdPerceptionTitle{Comparative evaluation}
\def\qfdPoorLabel{poor}
\def\qfdExcellentLabel{excellent}
\def\qfdAltOneLabel{Our product} % highlighted in legend
\def\qfdAltTwoLabel{Competitor A}
\def\qfdAltThreeLabel{Competitor B}
\def\qfdRelTitle{Relation}
\def\qfdCorrTitle{Correlation}
\def\qfdEvalTitle{Evaluation}
% Styles.
\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},
% Three perception-zone alternatives. Index 1 is emphasised.
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},
}
% --- Grid lines for every zone. ---
\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
}
% --- Roof: diagonal grid + named coordinates (C-i-j) for correlations. ---
\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
}
% --- Perception scale 0..5 + poor/excellent endpoints + zone title. ---
\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]
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
}
% --- Importance title (left) and WHATs title (header band). ---
\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};
}
% --- Outer frames around each zone. ---
\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}
}
% --- Legend on the right (Relations / Correlations / Evaluation). ---
\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);
% Relations
\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)};
% Correlations (roof)
\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
% Evaluation (3 alternatives)
\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
}
% --- The environment users wrap their content in. ---
\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}%
}
\begin{document}
% --- meadows: 4 goals (WHATs) x 11 functions (HOWs); no competitor zone;
% basement = Target / Importance Sigma / Relative %. ---
\def\qfdNW{4}
\def\qfdNH{11}
\def\qfdWhatW{4.2}
\def\qfdBasementN{3}
\def\qfdWhatsTitle{Goals (WHATs)}
\def\qfdImpTitle{Weight}
\qfdshowcompetitivefalse
\begin{qfdhouse}
% --- WHATs (goals) + importance weight ---
\pgfmathsetmacro{\qfdWhatTextW}{\qfdWhatW - 0.2}
\foreach \r/\t in {1/{G1 Build with near-zero friction},
2/{G2 Feedback surfaces as live insight},
3/{G3 Models safe \& portable},
4/{G4 Learn the language by example}}
\node[anchor=west, font=\scriptsize, text width=\qfdWhatTextW cm, align=left]
at ({\qfdLeftEdge + 0.1}, {-\r + 0.5}) {\t};
\foreach \r/\imp in {1/9, 2/8, 3/7, 4/4}
\node[font=\scriptsize] at ({-\qfdImpW/2}, {-\r + 0.5}) {\imp};
% --- HOWs (functions, rotated) ---
\foreach \c/\t in {1/{F1 place node}, 2/{F2 connect},
3/{F3 smooth canvas}, 4/{F4 valid inline},
5/{F5 loops live}, 6/{F6 polarity},
7/{F7 autosave}, 8/{F8 round-trip}, 9/{F9 undo/redo},
10/{F10 samples}, 11/{F11 glossary}}
\node[rotate=90, anchor=west, font=\scriptsize] at ({\c - 0.5}, 0.15) {\t};
% --- Relation matrix (col c = function, row r = goal); strength S/M/W ---
\foreach \c/\r/\s in {1/1/S, 2/1/S, 3/1/S, 4/1/M, 5/1/W, 6/1/M, 7/1/W, 9/1/M, 10/1/W, 11/1/W,
2/2/M, 3/2/W, 4/2/M, 5/2/S, 6/2/S, 9/2/W,
4/3/W, 7/3/S, 8/3/S, 9/3/S,
5/4/M, 6/4/W, 10/4/S, 11/4/S}
\node[qfdrel/\s] at ({\c - 0.5}, {-\r + 0.5}) {};
% --- Roof correlations (C-i-j, i<j); from DESIGN.md §5 ---
\node[font=\scriptsize] at (C-2-4) {$-$}; % F2 x F4 mild conflict
\node[font=\scriptsize] at (C-2-6) {$+\!+$}; % F2 o F6 strong reinforce
\node[font=\scriptsize] at (C-3-5) {$-\!-$}; % F3 x F5 strong conflict
\node[font=\scriptsize] at (C-3-9) {$-$}; % F3 x F9 mild conflict
\node[font=\scriptsize] at (C-4-5) {$+$}; % F4 o F5 mild reinforce
\node[font=\scriptsize] at (C-5-6) {$+\!+$}; % F5 o F6 strong reinforce
\node[font=\scriptsize] at (C-7-10) {$-$}; % F7 x F10 conflict (load clears history)
\node[font=\scriptsize] at (C-10-11) {$+\!+$}; % F10 o F11 reinforce (the two G4 aids)
% --- Basement: Target / Importance Sigma / Relative %, + row labels ---
\foreach \rr/\lbl in {0/{Target}, 1/{Imp.\ $\Sigma$}, 2/{Rel.\ \%}}
\node[anchor=east, font=\scriptsize\itshape]
at (-0.1, {-\qfdNW - 0.5 - \rr}) {\lbl};
\foreach \c/\tgt/\sig/\rel in
{1/{1 gest.}/81/10, 2/{1 drag}/105/12, 3/{60 fps}/89/10,
4/{0 inv.}/58/7, 5/{$\le$16ms}/93/11, 6/{dflt $\pm$}/103/12,
7/{$\le$500ms}/72/9, 8/{id.}/63/7, 9/{$\ge$50}/98/12, 10/{1 load}/45/5,
11/{gloss}/45/5} {
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 0.5}) {\tgt};
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 1.5}) {\sig};
\node[font=\scriptsize\bfseries] at ({\c - 0.5}, {-\qfdNW - 2.5}) {\rel};
}
\end{qfdhouse}
\end{document}
1. Goals — the WHATs
| ID | Goal | Weight | Source |
|---|---|---|---|
| G1 | Build a model with near-zero friction | 9 | original request; CONTEXT.md |
| G2 | Feedback structure surfaces as insight (live) | 8 | original request; ADR-0001 |
| G3 | Models are safe and portable | 7 | original request |
| G4 | Learn the language by example | 4 | README (Meadows spirit) |
2. Functions — the HOWs
| ID | Function | Dir | Target (now) | Target (future) |
|---|---|---|---|---|
| F1 | Place a node in one gesture (palette → canvas, auto-named) | ↓ | 1 click/drag, 0 dialogs | — |
| F2 | Connect by direct manipulation (auto source/sink cloud on open end) | ↓ | 1 drag, 0 menus | — |
| F3 | Render & manipulate the canvas smoothly | ↑ | 60 fps pan/zoom/drag @ ≤150 elements | ≥300 elements |
| F4 | Enforce valid structure inline | → | 0 invalid models persistable | — |
| F5 | Detect & classify loops live (R/B) on every structural change | ↓ | ≤16 ms recompute @ ≤150 elements | ≤16 ms @ ≥300 |
| F6 | Capture link polarity frictionlessly | ↓ | default on create, 1-click toggle | — |
| F7 | Autosave locally, continuously | → | debounced ≤500 ms; survive reload | — |
| F8 | Round-trip Models as versioned JSON | → | export→import identity; version |
schema migrations |
| F9 | Undo/redo every model change | ↑ | ≥50 steps, all structural ops | — |
| F10 | Load a curated sample on demand (covers the whole language) | → | 1 click; set spans whole language | more samples; shareable |
| F11 | Surface the language at point-of-use (palette tooltips + gloss-on-selection) | → | gloss on hover/selection; 0 persistent canvas marks | polarity / R/B glosses |
3. Cascade — Goals → Functions → How → Components
- G1 Build a model with near-zero friction W:9
- F1 Place a node in one gesture ↓ 1 click/drag
- How: Vue Flow drag-from-palette → store action creates the node
- Component: C1 Vue Flow canvas · C2 custom node components · C6 projection
- How: Vue Flow drag-from-palette → store action creates the node
- F2 Connect by direct manipulation (auto cloud on open end) ↓ 1 drag
- How: drag from a node handle; open end auto-spawns a Cloud
- Component: C2 node handles · C3 edge components · C5 store actions · C7 structure guard
- How: drag from a node handle; open end auto-spawns a Cloud
- F3 Render & manipulate smoothly ↑ 60 fps @ ≤150
- How: Vue Flow viewport (GPU transforms); Model→VueFlow projection memoised
- Component: C1 Vue Flow canvas · C6 projection
- How: Vue Flow viewport (GPU transforms); Model→VueFlow projection memoised
- F9 Undo/redo ↑ ≥50 steps
- How: ring buffer of deep-cloned Model snapshots in the store
- Component: C5 model store (history)
- How: ring buffer of deep-cloned Model snapshots in the store
- F1 Place a node in one gesture ↓ 1 click/drag
- G2 Feedback structure surfaces as insight (live) W:8
- F5 Detect & classify loops live ↓ ≤16 ms @ ≤150
- How: derive directed graph → Tarjan SCC → capped Johnson → polarity product (R/B)
- How: SCC-only — rejected (loses per-loop insight), see T3
- Component: C8 loop engine · C11 loop overlay
- F6 Capture link polarity frictionlessly ↓ default + toggle
- How: InfoLink defaults to
+on create; 1-click+/−toggle on the edge badge- Component: C3 edge components · C5 store actions
- How: InfoLink defaults to
- F4 Enforce valid structure inline → 0 invalid persistable
- How: guard guides (disallow invalid handle targets) rather than erroring after
- Component: C7 structure guard · C4 domain types
- How: guard guides (disallow invalid handle targets) rather than erroring after
- F5 Detect & classify loops live ↓ ≤16 ms @ ≤150
- G3 Models are safe and portable W:7
- F7 Autosave locally, continuously → ≤500 ms debounce
- How: store subscribes to Model changes → debounced
repository.save()- Component: C5 store · C9 ModelRepository (IndexedDB/idb)
- How: store subscribes to Model changes → debounced
- F8 Round-trip Models as versioned JSON → lossless
- How: serialise Model (
versionfield) → download; import → validate → load - How: PouchDB/cloud sync — deferred (needs a backend), see Tensions
- Component: C10 import/export · C4 domain types
- How: serialise Model (
- F9 (also serves G3 as a safety net) → C5 model store
- F7 Autosave locally, continuously → ≤500 ms debounce
- G4 Learn the language by example W:4
- F10 Load a curated sample on demand → 1 click, set spans the language
- How: ship example Models as pure data (the same factory constructors the
store uses); a header menu loads one via
store.setModel+fitView - How: interactive tutorial / guided tour — rejected (a tutorial
framework to maintain; a readable, editable gallery teaches by example and
doubles as instant content), see T6
- Component: C13 sample gallery · C5 store (
setModel) · C1 canvas (fitView)
- Component: C13 sample gallery · C5 store (
- How: ship example Models as pure data (the same factory constructors the
store uses); a header menu loads one via
- F11 Surface the language at point-of-use → gloss on hover/selection
- How: palette chips carry a tooltip; a corner panel glosses the single selected element — off-canvas, pointer-through, gone on deselect
- How: passive per-node hover tooltips — rejected (hover-noise on dense
models, and the bubble scales with the canvas zoom), see T7
- Component: C14 glossary + gloss panel · C12 palette
- F5 (seeing loops classified R/B is the lesson) → C8 loop engine · C11 overlay
- F6 (visible polarity reinforces the lesson) → C3 edge components
- F10 Load a curated sample on demand → 1 click, set spans the language
4. House — Goals × Functions
Cells: strength 9/3/1. Importance Σ = Σ(weight × strength).
| F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| G1 (9) | 9 | 9 | 9 | 3 | 1 | 3 | 1 | 3 | 1 | 1 | |
| G2 (8) | 3 | 1 | 3 | 9 | 9 | 1 | |||||
| G3 (7) | 1 | 9 | 9 | 9 | |||||||
| G4 (4) | 3 | 1 | 9 | 9 | |||||||
| Σ | 81 | 105 | 89 | 58 | 93 | 103 | 72 | 63 | 98 | 45 | 45 |
Top engineering priorities (from importance): the matrix still puts F2 (connect, 105), F6 (polarity, 103) and F9 (undo, 98) at the top, with the headline detector F5 climbing to 93 — G4 reinforces it, because seeing a loop classified R/B is itself the lesson. The lesson holds: loop insight is only as good as its inputs and ergonomics — effortless wiring (F2) and polarity capture (F6) matter as much as detection, and a reliable safety net (F9) underwrites fearless building. F10 (samples) and F11 (glossary), 45 each, are the lowest-leverage functions — exactly right for the two G4 supporting aids: each earns its place by being nearly free (static Models reusing the pipeline; one-line glosses surfaced on hover/selection), not by priority. F4 (58) stays low: keep it lightweight (guide, don't nag).
5. Roof — Function × Function tradeoffs
◎ strong reinforce · ○ mild reinforce · × mild conflict · ⊗ strong conflict.
| F2 | F3 | F4 | F5 | F6 | F9 | |
|---|---|---|---|---|---|---|
| F2 | — | × | ◎ | |||
| F3 | — | ⊗ | × | |||
| F4 | — | ○ | ||||
| F5 | — | ◎ | ||||
| F6 | — | |||||
| F9 | — |
Conflicts that actually shape the design:
- F5 ⊗ F3 — live loop enumeration on every change vs the 60 fps frame budget. The design-shaping conflict. Mitigated by the cap (≤200 cycles) + running detection off the render frame (debounce / idle callback / worker). Owned by ADR-0001 + the §7 budget.
- F2 × F4 — guardrails can add friction to connecting. Mitigated by guiding the gesture (invalid handles refuse the drop) instead of erroring afterwards.
- F3 × F9 — snapshot cloning on every change costs memory/time. Bounded by small models + a fixed-size ring buffer; revisit with structural sharing only if profiling demands.
- F2 ◎ F6, F5 ◎ F6 — polarity is captured in the connect gesture and is the detector's input; these reinforce, so build them together.
- F10 × F7 — loading a sample calls
setModel, which replaces the whole document and clears undo history. Against autosave (F7), one click could wipe real work. Mitigated by confirming the replace only when the canvas is non-empty (frictionless on the empty start). Owned by T6. (F7 and F10 sit outside the core roof table above; this is the new tension this release.) - F10 ◎ F11 — the two G4 aids reinforce: samples give worked examples, the glossary gives the words for the pieces they're made of. Build either and the other teaches better. (Both sit outside the core roof table.)
6. Components & Function → Component map
| ID | Component | ADR |
|---|---|---|
| C1 | Vue Flow canvas (@vue-flow/core + addons) |
ADR-0002 |
| C2 | Custom node components (Stock/Flow/Converter/Cloud) | ADR-0002 |
| C3 | Custom edge components (InfoLink badge, Flow pipe) | ADR-0002 |
| C4 | Domain Model + types (model/types.ts) |
ADR-0003 |
| C5 | Model store (Pinia) + snapshot history | — |
| C6 | Model ↔ Vue Flow projection (projection.ts) |
ADR-0002 |
| C7 | Structure guard (validation.ts) |
ADR-0003 |
| C8 | Loop engine (loops.ts: SCC + capped Johnson) |
ADR-0001 |
| C9 | ModelRepository (IndexedDB via idb) |
— |
| C10 | Import/Export (versioned JSON io.ts) |
— |
| C11 | Loop overlay UI (R/B badges, hover highlight) | ADR-0001 |
| C12 | Palette + chrome (DaisyUI toolbar, model list) | — |
| C13 | Sample gallery (model/samples.ts + header menu) |
— |
| C14 | Glossary + gloss affordances (glossary.ts, GlossPanel.vue, palette tooltips) |
— |
| C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 | C13 | C14 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| F1 | 9 | 9 | 3 | 9 | 9 | |||||||||
| F2 | 9 | 9 | 9 | 9 | 3 | 9 | ||||||||
| F3 | 9 | 3 | 3 | 9 | ||||||||||
| F4 | 1 | 1 | 3 | 3 | 9 | |||||||||
| F5 | 3 | 1 | 9 | 9 | ||||||||||
| F6 | 9 | 3 | 3 | 1 | ||||||||||
| F7 | 9 | 9 | ||||||||||||
| F8 | 3 | 1 | 9 | |||||||||||
| F9 | 9 | |||||||||||||
| F10 | 1 | 3 | 9 | |||||||||||
| F11 | 3 | 9 |
7. Critical performance budget
| Rank | Function | Target | Watched on | If we miss it |
|---|---|---|---|---|
| 1 | F5 | ≤16 ms recompute @ ≤150 | bench on synthetic dense graphs | enforce cycle cap; move detection off-frame (idle/worker); debounce |
| 2 | F3 | 60 fps pan/zoom/drag @ ≤150 | frame profiling in DevTools | memoise projection; simplify node DOM; virtualise off-screen nodes |
| 3 | F2 | 1 drag, 0 menus | manual UX walkthrough | simplify handle model; reduce connect steps |
| 4 | F7 | ≤500 ms debounce, 0 loss | reload-after-edit integration test | flush on visibilitychange/beforeunload |
| 5 | F8 | export→import identity | property test (round-trip) | pin schema; add migration on version bump |
8. Tradeoffs — Got / Paid / ADR
| ID | Tradeoff | Got | Paid | ADR |
|---|---|---|---|---|
| T1 | Vue Flow over hand-rolled SVG | fast F1–F3, batteries-included editor mechanics | a dependency owning the view layer + a Model↔VueFlow projection to maintain | ADR-0002 |
| T2 | Flow-as-node + materialised Clouds | uniform graph for loops/render; info-links can target flows; sim-clean | Cloud lifecycle management; a node that renders as a pipe | ADR-0003 |
| T3 | Enumerate loops with a cap | per-loop R/B insight (G2) within the frame budget | capped completeness on pathological graphs ("+N more") | ADR-0001 |
| T4 | IndexedDB(idb) behind ModelRepository |
size headroom + swappable engine (sync later) | async wrapper + a small dependency; no sync today | — |
| T5 | Snapshot undo over command pattern | simplicity and correctness | memory per snapshot (bounded by ring buffer + small models) | — |
| T6 | Samples load by replacing the whole Model | a sample is just a Model — no special load path; reuses the whole pipeline; one click | the load is destructive (setModel clears undo history); guarded by a confirm on a non-empty canvas |
— |
| T7 | Gloss on selection, not passive node hover | a clean, quiet canvas on dense models — no per-node hover popups, no zoom-scaled bubbles | you must select an element to read its definition (one click) | — |
Tensions being watched (unresolved by design)
- Cross-device sync / sharing. Not built; G3 is met by local persistence + JSON export. Trigger to revisit: a real multi-device or collaboration need → add a
PouchdbModelRepositoryor API-backed repository behind the existing interface. - Auto-layout. Manual placement only in phase 1. Trigger to revisit: users report large models becoming unreadable → add an optional layout pass (e.g. ELK/dagre) over the projection.
9. Inconsistencies spotted and fixed
- Brief vocabulary vs glossary. The original request said "functions," "arrows," "faucets"; DESIGN uses the resolved CONTEXT.md terms verbatim — Converter, Flow/Information Link, faucet dropped.
- G3 ambiguity. "Safe and portable" initially read as possible cloud sync; clarified — G3 is satisfied by local persistence + JSON export, not sync (PouchDB deferred, see Tensions).
- Storage default drift. Initial recommendation was localStorage; resolved to IndexedDB via
idb, behind aModelRepositoryinterface so the engine choice is no longer hard-to-reverse. - Teaching intent unrecorded. The project is framed as Meadows-inspired (README) and the whole vocabulary is pedagogical (CONTEXT.md), yet no Goal captured "teach the language" until now — the intent lived only in the vision. Surfaced and recorded as G4 (weight 4), answered by the sample gallery (F10 / C13) and inline glossary affordances (F11 / C14).
How to keep this honest
- When a new ADR lands → add its components to §6 and re-score affected rows.
- When a spike / measurement returns numbers → update §7
Target/Watched on. - WHATs change rarely; HOWs change with each release; matrices are recomputed when either side changes.
- If a section becomes empty after edits, delete it — empty sections lie.