Files
meadows/src/model/types.ts
Julien Calixte 4a0498ee55 feat(editor): give each node and link a description
A free-text "why this element is here" note on Stocks, Flows,
Converters, and Information Links — editable in the Inspector next to
the unit, shown read-only in the gloss card below the generic kind
definition on selection. The Inspector now also opens for Information
Links, which previously had no editing surface.
2026-06-21 15:19:31 +02:00

154 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The domain Model — the single source of truth for a meadows document.
*
* The Vue Flow graph is a derived projection of this (ADR-0002); the future
* simulator reads this and knows nothing of the view layer. Everything here is
* plain, serialisable data (no class instances, no functions) so it round-trips
* as JSON (F8) and deep-clones cheaply for undo snapshots (F9).
*
* Vocabulary follows CONTEXT.md. Structural rules follow the ADRs:
* - A Flow is a *node* with `source`/`target` references, not an edge (ADR-0003).
* - Source/Sink boundaries are materialised Cloud nodes, so no end is ever null.
* - The only stored edge type is the Information Link; loops are detected, never
* stored (ADR-0001).
*/
/** Schema version stamped on every Model for round-trip migrations (F8). */
export const MODEL_VERSION = 1
/** The four node kinds. A `kind` field discriminates the `ModelNode` union. */
export type NodeKind = "stock" | "flow" | "converter" | "cloud"
/**
* 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 Reinforcing/Balancing.
*/
export type Polarity = "+" | "-"
export interface Position {
x: number
y: number
}
/**
* How a Flow's rate or a Converter's value is computed — a small fixed
* vocabulary, *not* a free-form formula, so a Model stays valid by construction
* and teaches structure → behaviour (ADR-0004). Operands are read from the
* element's inbound Information Links; Polarity selects each operand's role.
*
* - `constant` — a fixed number; reads nothing. (→ linear Stock change)
* - `proportional` — `factor ×` the product of its `+`-polarity inputs.
* (→ exponential growth/decay)
* - `gap` — `factor × (level target)`, where the `+` input is the
* level and the `` input the target. (→ goal-seeking)
*/
export type Rule =
| { kind: "constant"; value: number }
| { kind: "proportional"; factor: number }
| { kind: "gap"; factor: number }
/**
* The run parameters for a simulation: integrate from `start` to `stop` in steps
* of `dt` (forward Euler). Optional on a Model; absent means the diagram phase or
* a not-yet-simulated Model — the engine falls back to `DEFAULT_SIM_SPEC`.
*/
export interface SimSpec {
start: number
stop: number
dt: number
}
/** Sensible default run when a Model carries no `sim` of its own. */
export const DEFAULT_SIM_SPEC: SimSpec = { start: 0, stop: 100, dt: 1 }
interface BaseNode {
id: string
position: Position
}
/**
* An accumulation that holds a quantity over time — the system's memory and the
* only stateful element. Changes only through Flows.
*/
export interface StockNode extends BaseNode {
kind: "stock"
name: string
/** Initial accumulated quantity. Optional in the diagram phase; the simulator reads it. */
initialValue?: number
/** Unit of the quantity, e.g. "°C", "people", "$" — shown beside the value (display only). */
unit?: string
/** Why this Stock exists in *this* model — a free-text note surfaced on selection (display only). */
description?: string
}
/**
* A rate that moves quantity between two ends. A Flow is a node (ADR-0003): the
* pipe-with-valve you see is rendered from `source`/`target`. Inflow/outflow is
* relative to a given Stock, not a separate type — a stock→stock Flow is an
* outflow for `source` and an inflow for `target`.
*/
export interface FlowNode extends BaseNode {
kind: "flow"
name: string
/** Node id of the Stock or Cloud the Flow draws from. */
source: string
/** Node id of the Stock or Cloud the Flow feeds into. */
target: string
/** How its rate is computed each instant (ADR-0004). Optional in the diagram phase. */
rule?: Rule
/** Why this Flow exists in *this* model — a free-text note surfaced on selection (display only). */
description?: string
}
/**
* A stateless helper value, recomputed each instant from its inputs (or a fixed
* constant). Feeds Flows or other Converters via Information Links.
*/
export interface ConverterNode extends BaseNode {
kind: "converter"
name: string
/** How its value is computed each instant (ADR-0004). Optional in the diagram phase. */
rule?: Rule
/** Why this Converter exists in *this* model — a free-text note surfaced on selection (display only). */
description?: string
}
/**
* A model boundary (Source/Sink) on an open Flow end — drawn as a cloud.
* Auto-managed: spawned when a Flow is left open-ended, removed when the Flow
* attaches to a Stock (ADR-0003). Carries no name and no value.
*/
export interface CloudNode extends BaseNode {
kind: "cloud"
}
export type ModelNode = StockNode | FlowNode | ConverterNode | CloudNode
/**
* Shows that one element's value influences a Flow's or Converter's value.
* Carries a Polarity; carries no quantity and never targets a Stock (a Stock
* changes only via Flows).
*/
export interface InformationLink {
id: string
/** Source node id — a Stock, Flow, or Converter (the value being read). */
source: string
/** Target node id — a Flow or Converter, never a Stock (ADR-0001). */
target: string
polarity: Polarity
/** Why this link exists in *this* model — a free-text note surfaced on selection (display only). */
description?: string
}
/** One saved document: the unit of save / export / reopen. */
export interface Model {
version: number
id: string
name: string
nodes: ModelNode[]
infoLinks: InformationLink[]
/** Run parameters for simulation (ADR-0004). Optional; absent → not yet simulated. */
sim?: SimSpec
}