Files
meadows/DESIGN.md
2026-06-20 10:13:21 +02:00

31 KiB
Raw Permalink Blame History

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
    • 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
    • F3 Render & manipulate smoothly ↑ 60 fps @ ≤150
      • How: Vue Flow viewport (GPU transforms); Model→VueFlow projection memoised
        • Component: C1 Vue Flow canvas · C6 projection
    • F9 Undo/redo ↑ ≥50 steps
      • How: ring buffer of deep-cloned Model snapshots in the store
        • Component: C5 model store (history)
  • 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
    • 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
  • 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)
    • F8 Round-trip Models as versioned JSON → lossless
      • How: serialise Model (version field) → download; import → validate → load
      • How: PouchDB/cloud sync — deferred (needs a backend), see Tensions
        • Component: C10 import/export · C4 domain types
    • F9 (also serves G3 as a safety net) → C5 model store
  • 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)
    • 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

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 F1F3, 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 PouchdbModelRepository or 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 a ModelRepository interface 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.