docs: add ubiquitous language, QFD design, and ADRs
This commit is contained in:
105
CONTEXT.md
Normal file
105
CONTEXT.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# meadows
|
||||||
|
|
||||||
|
The ubiquitous language for **meadows** — a web app for building systems-thinking
|
||||||
|
models (stock-and-flow diagrams with feedback loops) in the spirit of Donella
|
||||||
|
Meadows' *Thinking in Systems*. The diagram is the product now; numeric simulation
|
||||||
|
is a planned later phase, so the language is chosen to be simulation-ready.
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
**System**:
|
||||||
|
The real-world thing being studied — an interconnected set of elements producing
|
||||||
|
a behaviour over time. It is the *referent*, never the artifact in the app.
|
||||||
|
_Avoid_: model, diagram, map (those are the artifact, not the real thing).
|
||||||
|
|
||||||
|
**Model**:
|
||||||
|
One saved document — a simplified representation of a System, made of Stocks,
|
||||||
|
Flows, Converters, and Information Links. The unit of save / export / reopen.
|
||||||
|
_Avoid_: system, diagram, map, canvas, sheet, project.
|
||||||
|
|
||||||
|
**Stock**:
|
||||||
|
An accumulation that holds a quantity over time — the system's memory (e.g. water
|
||||||
|
in a bathtub, money in an account, a population).
|
||||||
|
_Avoid_: Box, level, container, node.
|
||||||
|
|
||||||
|
**Flow**:
|
||||||
|
A rate that moves quantity into (**inflow**) or out of (**outflow**) a Stock —
|
||||||
|
the only element that can change a Stock's value; drawn as a pipe with a valve.
|
||||||
|
Inflow/outflow is one Flow's direction relative to a given Stock, not a separate
|
||||||
|
element type (a stock→stock Flow is an outflow for one and an inflow for the
|
||||||
|
other). _Avoid_: faucet, drain, pipe, rate (that's the Flow's value), arrow.
|
||||||
|
|
||||||
|
**Converter**:
|
||||||
|
A stateless helper value, recomputed each instant from its inputs (or a fixed
|
||||||
|
constant), that feeds Flows or other Converters via Information Links — e.g.
|
||||||
|
`birth rate = fertility × 0.01`. A constant is a Converter with no inputs.
|
||||||
|
_Avoid_: variable (Stocks and Flows are variables too), function, auxiliary,
|
||||||
|
parameter, node.
|
||||||
|
|
||||||
|
**Information Link**:
|
||||||
|
A connector showing that one element's value influences a Flow's or Converter's
|
||||||
|
value; carries no quantity, never changes a Stock. _Avoid_: arrow, dependency,
|
||||||
|
edge, connector.
|
||||||
|
|
||||||
|
**Source** / **Sink**:
|
||||||
|
The model boundary on an open Flow end — a **Source** is where a Flow originates
|
||||||
|
outside the model (unlimited supply), a **Sink** is where it disappears. Drawn as
|
||||||
|
a cloud, auto-rendered when a Flow has no Stock at one end. _Avoid_: cloud (that's
|
||||||
|
the symbol, not the term), external, environment.
|
||||||
|
|
||||||
|
**Polarity**:
|
||||||
|
The sign on an Information Link (and inherent to a Flow): `+` when more cause
|
||||||
|
means more effect, `−` when more cause means less effect. The input that lets
|
||||||
|
loops be classified. _Avoid_: sign, direction (direction is inflow/outflow),
|
||||||
|
weight.
|
||||||
|
|
||||||
|
**Feedback Loop**:
|
||||||
|
A closed cycle in the wiring (Stock → … → Flow → same Stock); **detected** from
|
||||||
|
structure, never stored or hand-drawn. Classified by the product of polarities
|
||||||
|
around the cycle. _Avoid_: cycle (internal term), circle, circuit.
|
||||||
|
|
||||||
|
**Reinforcing**:
|
||||||
|
A Feedback Loop with an even number of `−` polarities — it amplifies change (the
|
||||||
|
snowball); labelled `R`. _Avoid_: positive, self-reinforcing, vicious/virtuous.
|
||||||
|
|
||||||
|
**Balancing**:
|
||||||
|
A Feedback Loop with an odd number of `−` polarities — it counteracts change and
|
||||||
|
seeks a goal (the thermostat); labelled `B`. _Avoid_: negative, stabilizing,
|
||||||
|
goal-seeking.
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
- A **Stock** is the only stateful element — it accumulates (∫ flows) and has
|
||||||
|
memory; it changes **only** through **Flows**.
|
||||||
|
- A **Flow** and a **Converter** are stateless — recomputed each instant from
|
||||||
|
their inputs; neither has memory.
|
||||||
|
- An **Information Link** feeds a **Stock**, **Flow**, or **Converter** value
|
||||||
|
into a **Flow** or **Converter** (never into a Stock — Stocks change only via
|
||||||
|
Flows), and carries a **Polarity**.
|
||||||
|
- A **Feedback Loop** is a detected cycle in the wiring; it is **Reinforcing**
|
||||||
|
when it has an even number of `−` polarities, **Balancing** when odd.
|
||||||
|
|
||||||
|
## Example dialogue
|
||||||
|
|
||||||
|
> **Dev:** "For a population model, **Population** is a **Stock**, and **births**
|
||||||
|
> is a **Flow** into it — an inflow. Where do the births come from?"
|
||||||
|
> **Modeller:** "From outside the model — so the Flow's other end is a **Source**
|
||||||
|
> cloud. Same with **deaths**: an outflow from Population to a **Sink**."
|
||||||
|
> **Dev:** "And births depend on how many people there are?"
|
||||||
|
> **Modeller:** "Right — an **Information Link** from Population to the births
|
||||||
|
> Flow, with `+` **Polarity**: more people, more births. That closes a cycle, so
|
||||||
|
> the app detects a **Feedback Loop**. Even number of `−` links — zero here — so
|
||||||
|
> it's **Reinforcing**."
|
||||||
|
> **Dev:** "And `birth rate = fertility × 0.01`?"
|
||||||
|
> **Modeller:** "That's a **Converter** — stateless, recomputed each step. The
|
||||||
|
> Population Stock is the only thing that remembers."
|
||||||
|
|
||||||
|
## Flagged ambiguities
|
||||||
|
|
||||||
|
- "arrow" was used for two distinct things — resolved into **Flow** (moves
|
||||||
|
material, changes a Stock) and **Information Link** (carries influence only).
|
||||||
|
- "function" was the user's word for the stateless helper — resolved to
|
||||||
|
**Converter** (avoids collision with programming "function" and with the
|
||||||
|
stateful Stock).
|
||||||
|
- "positive / negative feedback" — resolved to **Reinforcing** / **Balancing**
|
||||||
|
(Meadows' preferred terms; `+/−` is reserved for link **Polarity**).
|
||||||
172
DESIGN.md
Normal file
172
DESIGN.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
Strength weights used in matrices: **9** strong, **3** medium, **1** weak, blank none.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
|
||||||
|
## 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 | — |
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 4. House — Goals × Functions
|
||||||
|
|
||||||
|
Cells: strength 9/3/1. Importance Σ = Σ(weight × strength).
|
||||||
|
|
||||||
|
| | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 |
|
||||||
|
|----------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||||
|
| G1 (9) | 9 | 9 | 9 | 3 | 1 | 3 | 1 | | 3 |
|
||||||
|
| G2 (8) | | 3 | 1 | 3 | 9 | 9 | | | 1 |
|
||||||
|
| G3 (7) | | | | 1 | | | 9 | 9 | 9 |
|
||||||
|
| **Σ** | 81 | 105| 89 | 58 | 81 | 99 | 72 | 63 | 98 |
|
||||||
|
|
||||||
|
**Top engineering priorities (from importance):** the matrix puts **F2 (connect, 105)**, **F6 (polarity, 99)** and **F9 (undo, 98)** above the headline detector **F5 (81)**. The lesson: loop insight is only as good as its *inputs* and *ergonomics* — getting wiring (F2) and polarity capture (F6) effortless matters as much as the detection itself, and a reliable safety net (F9) underwrites fearless building. F4 (58) is real but lowest-leverage — 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.
|
||||||
|
|
||||||
|
## 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) | — |
|
||||||
|
|
||||||
|
| | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 |
|
||||||
|
|-----|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:---:|:---:|:---:|
|
||||||
|
| 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 | | | | | | | |
|
||||||
|
|
||||||
|
## 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) | — |
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
26
docs/adr/0001-detected-feedback-loops.md
Normal file
26
docs/adr/0001-detected-feedback-loops.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Feedback loops are detected from a polarity-annotated graph, not stored
|
||||||
|
|
||||||
|
A **Feedback Loop** in a **Model** is a cycle in the wiring (Stock → Information
|
||||||
|
Link(s)/Flow → same Stock). We chose to **derive** loops by detecting cycles and
|
||||||
|
classifying each as **Reinforcing** (even number of `−` polarities) or
|
||||||
|
**Balancing** (odd), rather than storing them as first-class objects the user
|
||||||
|
hand-labels. This makes loops *discovered insight* that can never disagree with
|
||||||
|
the actual structure — the point of a systems-thinking tool.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
- **Hand-labeled annotation** — user drops a loop badge and tags it R/B. Simplest,
|
||||||
|
but the label carries no structural truth and can contradict the wiring.
|
||||||
|
- **Hybrid** — app detects the cycle exists; user assigns R/B. Removes the
|
||||||
|
inconsistency risk only partially and makes the user do reasoning the app can do.
|
||||||
|
- **Detected from structure (chosen)** — app finds cycles and classifies them.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Every **Information Link** must carry a **Polarity** (`+`/`−`); inflows/outflows
|
||||||
|
carry inherent polarity. This is a required field from the diagram phase on.
|
||||||
|
- There is **no `Loop` entity** in the data model — loops are computed, not
|
||||||
|
persisted. A future reader seeing polarity on every link and no stored loop
|
||||||
|
should look here.
|
||||||
|
- Requires cycle-detection + sign-product logic. The same polarity data is
|
||||||
|
reused when numeric simulation is added later.
|
||||||
22
docs/adr/0002-vue-flow-substrate.md
Normal file
22
docs/adr/0002-vue-flow-substrate.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Vue Flow is the editor substrate; the domain Model is the source of truth
|
||||||
|
|
||||||
|
The editor is built on **Vue Flow** (`@vue-flow/core`) for pan/zoom, node
|
||||||
|
dragging, handles, edge-drawing, and selection, with Stocks/Flows/Converters/
|
||||||
|
Clouds as custom Vue node components. We rejected hand-rolling an SVG editor (too
|
||||||
|
slow to the friction goals F1–F3) and heavier kits (AntV X6, mxGraph — not
|
||||||
|
Vue-3-native).
|
||||||
|
|
||||||
|
The constraint that makes this safe: the **domain `Model` is the single source of
|
||||||
|
truth; the Vue Flow graph is a derived projection of it, never the reverse.** All
|
||||||
|
writes go through store actions that mutate the `Model`; the Vue Flow view is
|
||||||
|
re-derived. The future simulator reads the `Model` and has no knowledge of Vue
|
||||||
|
Flow.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A **projection layer** (Model → Vue Flow nodes/edges, and Vue Flow events →
|
||||||
|
store actions) must be built and kept thin. This is the price of the dependency.
|
||||||
|
- Swapping the rendering substrate later means rewriting only the projection +
|
||||||
|
node/edge components, not the domain model or the simulator.
|
||||||
|
- A reader wondering "why is there a sync/projection layer instead of editing Vue
|
||||||
|
Flow state directly?" should look here: it protects the simulate-later constraint.
|
||||||
20
docs/adr/0003-flow-as-node-materialised-clouds.md
Normal file
20
docs/adr/0003-flow-as-node-materialised-clouds.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# A Flow is a node; Source/Sink clouds are materialised nodes
|
||||||
|
|
||||||
|
In the domain `Model`, a **Flow** is a *node* carrying `source` and `target`
|
||||||
|
references (each to a Stock or Cloud), not an edge. The pipe you see is *rendered*
|
||||||
|
from those references. The **Source/Sink** boundary is a **materialised Cloud
|
||||||
|
node**, so every Flow connects node→node uniformly — no `null` ends.
|
||||||
|
|
||||||
|
We rejected modelling a Flow as an edge (it breaks "an Information Link targets a
|
||||||
|
Flow," since edges can't be edge endpoints, and it muddies the simulator) and
|
||||||
|
rejected `null`-ended flows (special-casing in every graph walk and in rendering).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The only stored edge type is the **Information Link**. Flows and Clouds are
|
||||||
|
nodes. A reader seeing a "node that renders as a pipe" and "phantom cloud nodes"
|
||||||
|
should look here.
|
||||||
|
- **Cloud lifecycle** must be auto-managed: spawn a Cloud when a Flow is left
|
||||||
|
open-ended; remove it when the Flow attaches to a Stock; never leave orphans.
|
||||||
|
- Loop detection and the future simulator both walk a uniform node graph
|
||||||
|
(info-links + flow→stock edges), with no boundary special cases.
|
||||||
Reference in New Issue
Block a user