512 lines
31 KiB
Markdown
512 lines
31 KiB
Markdown
# 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](./CONTEXT.md) for vocabulary and
|
||
[ADR-0001](./docs/adr/0001-detected-feedback-loops.md) (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](./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.
|
||
|
||
```tikz
|
||
% =====================================================================
|
||
% 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 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 `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.
|