# wity-system -- API Quick Reference Pure API surface. No prose. Signatures, types, constants. Version: 0.15.0 (monorepo: system-core + system-formalisms + system-sim + system-ui-compute + system-ui) --- ## IMPORT ```js // system-core import { SystemGraph, TypeRegistry, cascade, impact, serialize, parse, setGraphProvider, // formalisms and aspects — the mechanism validateAspect, checkAspect, RULES, memberKind, lineKindOf, wordsOf, aspectUid, memberUid, ASPECT_TYPE, MAX_MEMBERS, FLOW_NETWORK, PLAIN, STRUCTURES, DEFAULT_STRUCTURE, readStructure, VIEW_LEGEND, inputsProblems, PAYLOAD_KINDS, // an asset type's inputs (its input ports) } from 'wity-system'; // system-formalisms — the formalisms themselves, declared as data import { formalisms, process, stateMachine, petriNet, behaviourTree, syntaxTree, STEP_KINDS, lanes, describe, describeInWords } from 'wity-system/formalisms'; // system-sim import { SimulationRuntime, FeatureStore, SimClock, InfluenceEngine, HistoryBuffer, // what comes in from outside sourceArchetypes, validateInputStream, inputEventProblems, payloadProblems, streamsToRun, } from 'wity-system/sim'; ``` --- ## SystemGraph ### Constructor ``` new SystemGraph({ GraphAbstract, spatial?, types?, extent?, formalisms? }) GraphAbstract: Function — required, from @wity/graph-headless spatial: { project, unproject, coordFields } — optional types: TypeRegistry — optional, pre-built registry extent: { sw: {a,b}, ne: {a,b} } — optional, initial bounds formalisms: object[] — optional, the formalisms aspects may be made in (declarations) ``` ### Type registration ``` registerAssetType(name: string, definition: object) → this registerConnectionType(name: string, definition: object) → this getAssetType(name: string) → object | null getConnectionType(name: string) → object | null getAssetTypes() → Map getConnectionTypes() → Map getFeatureSchema(assetTypeName: string) → object | null — the signals an asset type measures types → TypeRegistry ``` ### Asset CRUD ``` addAsset(uid: string, type: string, data?: object) → this updateAsset(uid: string, data: object) → this getAsset(uid: string) → { uid, type, data } | null removeAsset(uid: string) → this getAssets() → Array<{ uid, type, data }> getAssetsByType(type: string) → Array<{ uid, type, data }> assetCount → number ``` ### Connection CRUD ``` addConnection(srcUid: string, targetUid: string, type?: string, data?: object) → Edge updateConnection(srcUid: string, targetUid: string, type?: string, data?: object) → Edge getConnection(srcUid: string, targetUid: string, type?: string) → Edge | null removeConnection(srcUid: string, targetUid: string, type?: string) → this getConnections() → Edge[] getConnectionsByType(type: string) → Edge[] getOutgoing(uid: string, type?: string) → Edge[] getIncoming(uid: string, type?: string) → Edge[] getConnectionsAmong(assetUids: string[] | Set) → Edge[] connectionCount → number ``` Edge UID format: `${type}:${srcUid}→${targetUid}`. Default type: `'default'`. ### Propagation & analysis ``` propagate(fromUid: string, options?: { direction?: 'downstream' | 'upstream' | 'both', edgeTypes?: string[], maxDepth?: number, }) → Array<{ uid: string, depth: number, via: string }> cascade(disabledUids: string[], options?: { sourceUids?: string[], edgeTypes?: string[], direction?: 'downstream' | 'upstream' | 'both', }) → { reachable: string[], cutOff: string[], disabled: string[] } impact(uid: string, options?: { edgeTypes?: string[], maxDepth?: number, }) → { downstream: Array, upstream: Array } ``` ### Pathfinding ``` reachable(fromUid: string, options?: { direction?: 'downstream' | 'upstream' | 'both', edgeTypes?: string[], }) → string[] shortestPath(fromUid: string, toUid: string, options?: { edgeTypes?: string[], cost?: (edge: Edge) => number, }) → { path: string[], cost: number } | null allPaths(fromUid: string, toUid: string, options?: { maxDepth?: number, edgeTypes?: string[], cost?: (edge: Edge) => number, }) → Array<{ path: string[], cost: number }> ``` ### Viewport ``` setCenter(a: number, b: number) → this getCenter() → { a: number, b: number } | null setZoom(zoom: number) → this getZoom() → number | null setExtent(sw: {a,b}, ne: {a,b}) → this getExtent() → { sw: {a,b}, ne: {a,b} } | null setViewport({ center?: {a,b}, zoom?: number, bounds?: {sw,ne} }) → this getViewport() → { center, zoom, bounds } computeExtent() → { sw: {a,b}, ne: {a,b} } | null getExtentOfAssets(uids: string[]) → { sw: {a,b}, ne: {a,b} } | null ``` ### Spatial ``` isSpatial → boolean project(a: number, b: number) → { x: number, y: number } unproject(x: number, y: number) → { a: number, b: number } reproject() → void setSpatial({ project, unproject, coordFields: string[] }) → void — attach a drawing surface's projection (not saved); also sets the position fields clearSpatial() → void — detach it; clears the position fields getCoordFields() → string[] | null — which fields of an asset's data are its position; null = the canvas (x, y) setCoordFields(fields: [string, string] | null) → this — model state: saved (snapshot().spatial), needs no projection GEOGRAPHIC_FIELDS = ['lat', 'lon'] earlierCoordFields(graphData, viewport) → string[] | null — how a document saved without position fields is read: real-world if it has a saved centre, or lat / lon assets with no canvas position ``` ### Regions ``` addRegion(uid: string, { label?: string, polygon: number[][], meta?: object }) → this updateRegion(uid: string, { label?: string, polygon?: number[][], meta?: object }) → this getRegion(uid: string) → { label: string, polygon: number[][], meta: object } | null removeRegion(uid: string) → boolean moveRegion(uid: string, da: number, db: number) → this — every point of its outline by (da, db) getRegions() → Array<{ uid, label, polygon, meta }> regionCount → number getAssetsInRegion(regionUid: string) → string[] getRegionsOfAsset(assetUid: string) → string[] ``` polygon coordinates: the system's position fields (a type's own coordFields first); on a canvas-laid system, x / y. ### Identity and saved canvas view (in snapshot() as `meta` / `canvas`, only when set) ``` getMeta() → object · setMeta(patch: object) → this — '' / null removes a field getCanvasView() → { x, y, zoom } | null · setCanvasView(view | null) → this ``` ### How it is run and shown (in snapshot() as `run` / `view`, only what differs from the defaults) ``` getRun() → { autoplay: boolean, behaviour: string | null, streams: string[] | null, speed: number } — streams null: every input stream setRun(patch: { autoplay?, behaviour?: string | null, streams?: string[] | null, speed?: number }) → this — streams: ids ([] none, null all); speed: above 0, at most 100 getView() → { open: null | 'map' | aspectId, focus: null | { region: uid } | { assets: uid[] }, legend: 'open' | 'collapsed' } — open: the view it opens on; null = its first (the map, or its first aspect when it holds only aspects) setView(patch: { open?, focus?, legend? }) → this — open is refused unless 'map' or one of its aspects; removing that aspect sets it back to null getViewAssets() → Node[] — the assets the view is focused on (all when it is not) VIEW_LEGEND = ['open', 'collapsed'] ``` ### What the structure is (in snapshot() as `structure`, only when it is not 'flow-network') getStructure() → 'flow-network' | 'plain' — 'flow-network' (the default): something flows along the connections; 'plain': parts and how they relate (an org chart, a service map) setStructure(kind: 'flow-network' | 'plain') → this entryAssets() → string[] — the assets things begin at: a flow network's sources; none in a plain map The analyses read the types' traits as the structure says (readStructure): a flow network follows connection types with carriesFlow (all, when none is), begins at role 'source', costs defaultCost / costField; a plain map follows every connection, begins nowhere (cascade needs sourceUids — it throws otherwise), costs one per step. ### Formalisms and aspects An ASPECT is one more model of the same system, in the same graph, beside its assets and connections (its structure). A FORMALISM is the kind of model an aspect is, declared as data. system-core names no formalism: they are handed in (wity-system/formalisms has them). ``` registerFormalism(declaration: { id, label?, words?, nodeKinds, lineKinds, rules, links?, roles?, … }) → this getFormalism(id: string) → object | null getFormalisms() → object[] getContents() → { assets, connections, types, aspects: [{ formalism, count, one, many }] } — what it holds, counted; an aspect's words (one, many) are its formalism's (words.self, words.selves) registerMemberType(name: string, { formalism: string, kind?: string, label?, description?, features?, meta? }) → this getMemberType(name: string) → { formalism, kind, label, description, meta, features? } | null getMemberTypes(formalism?: string) → Map setAspect(aspect: { id, formalism, label?, members: Member[], lines?: Line[] }) → this Member { id, kind | type, label?, x?, y?, ?, …fields } Line { from, to, kind?, label?, …fields } throws when it is not an aspect of its formalism in shape; one that breaks a rule is kept (see checkAspect) getAspect(id: string) → aspect | null — each line with its kind getAspects(formalism?: string) → aspect[] removeAspect(id: string) → boolean setAspectMember(id: string, member: Member, { before?: memberId }) → this — add, or replace by id; the others are left as they are removeAspectMember(id: string, memberId: string) → boolean — with its lines setAspectLine(id: string, line: Line, { at?: number }) → this — add, or replace the line of the same kind between the same two removeAspectLine(id: string, { from, to, kind? }) → boolean checkAspect(id: string) → Array<{ rule: string, member?: string, line?: { from, to }, message: string }> — [] when sound memberKind(member: Member) → string | null — its own kind, or its type's — analyses: the ones above (propagate, cascade, impact, reachable, shortestPath, allPaths) with `in: ` — they then run over that aspect: nodes are its members (named by member id), its lines are followed impact(memberId, { in, line?, maxDepth? }) → { downstream: [{ uid: memberId, depth, via }], upstream: [...] } shortestPath(fromId, toId, { in, line?, costField? }) → { path: memberId[], cost: number } | null cascade(memberIds, { in, direction? }) → { reachable, cutOff, disabled } — from the formalism's roles.entry kind; 'upstream': back from roles.exit line: keep to one kind of line · costField: a number field the nodes carry (also for the structure) membersLinkedTo(uid: string) → Array<{ aspect, member, link }> — the members that name this asset aspectGraph(id: string) → view — the aspect as a graph (an L1 view) + uidOf(memberId), idOf(uid) ``` The structure is unaffected by aspects: getAssets, getConnections, getOutgoing, getIncoming, the analyses, regions, scenarios and the simulator see assets and connections only. ### Dynamics (the behaviour layer — specs are system-sim's; the system holds and serializes them) ``` getDynamics() → object[] getDynamicsSpec(id: string) → object | null setDynamics(layers: object[] | object | null) → this setDynamicsSpec(spec: { id: string, … }) → this — add, or replace by id removeDynamicsSpec(id: string) → boolean ``` ### Inputs (what comes in from outside — streams are system-sim's; the system holds and serializes them) ``` getInputs(assetTypeName: string) → { inputName: { label?, description?, payload?: { key: 'text'|'number'|'boolean' } } } | null getInputStreams() → object[] getInputStream(id: string) → object | null setInputStream(stream: { id: string, kind, … }) → this — add, or replace by id removeInputStream(id: string) → boolean — also dropped from run.streams ``` In snapshot() as `inputStreams`, only when there are some. An asset type declares `inputs` when registered (inputsProblems(inputs) → string[] is its check, PAYLOAD_KINDS the payload kinds). ### Playback ``` setPlayback(steps: Array<{ type: string, [key: string]: any }>) → this getPlayback() → Array<{ type: string, [key: string]: any }> resolveSelector(selector: Selector) → string[] Selector types: { type: 'region', uid: string } { type: 'assetType', name: string } { type: 'path', from: string, to: string } { type: 'uids', uids: string[] } { type: 'all' } { type: 'union', selectors: Selector[] } resolvePlaybackStep(step: { label?: string, select?: Selector | Selector[], connections?: 'related' | 'all' | 'none', regions?: string[], viewport?: 'fit' | { center: {a,b}, zoom: number }, cumulative?: boolean, }, prevAssets?: string[]) → { assets: string[], connections: string[], regions: string[], viewport: { bounds } | { center, zoom } | null, } ``` ### Scenarios ``` saveScenario(name: string) → this loadScenario(name: string) → this deleteScenario(name: string) → boolean getScenarioNames() → string[] diffScenarios(nameA: string, nameB: string) → { assets: { added: object[], removed: object[], modified: Array<{ uid, before, after }> }, connections: { added: object[], removed: object[], modified: Array<{ uid, before, after }> }, regions: { added: object[], removed: object[], modified: Array<{ uid, before, after }> }, } ``` ### Serialization ``` snapshot() → object restore(data: object, options?: { spatial?: object }) → void graph → GraphAbstract ``` --- ## TypeRegistry ### Constructor ``` new TypeRegistry() ``` ### Asset types ``` registerAssetType(name: string, definition?: object) → this getAssetType(name: string) → object | null getAssetTypes() → Map hasAssetType(name: string) → boolean removeAssetType(name: string) → boolean ``` ### Connection types ``` registerConnectionType(name: string, definition?: object) → this getConnectionType(name: string) → object | null getConnectionTypes() → Map hasConnectionType(name: string) → boolean removeConnectionType(name: string) → boolean ``` ### Member types (kinds of member a system declares for its aspects, each in one formalism) ``` registerMemberType(name: string, { formalism: string, kind: string, label?, description?, features?, meta? }) → this getMemberType(name: string) → object | null getMemberTypes() → Map hasMemberType(name: string) → boolean removeMemberType(name: string) → boolean memberTypeCount → number getFeatureSchema(assetTypeName: string) → object | null getInputs(assetTypeName: string) → object | null — its inputs, or null when it declares none ``` The registry does not know the formalisms: that a type's kind is one of its formalism's is checked by SystemGraph.registerMemberType. ### Trait queries ``` getSourceTypes() → string[] getSinkTypes() → string[] getFlowTypes() → string[] getFailableTypes() → string[] buildCostFn() → (edge: Edge) => number ``` ### Counts ``` assetTypeCount → number connectionTypeCount → number isEmpty → boolean ``` ### Serialization ``` serialise() → { assetTypes: object, connectionTypes: object, memberTypes?: object } — memberTypes only when there are some hydrate(data: { assetTypes?, connectionTypes?, memberTypes? }) → this — `stepTypes` (0.14) is read as member types of 'process' ``` --- ## STANDALONE FUNCTIONS ### cascade(graph, disabledUids, options?) ``` cascade( graph: GraphAbstract, disabledUids: string[], options?: { sourceUids?: string[], edgeTypes?: string[], direction?: string }, ) → { reachable: string[], cutOff: string[], disabled: string[] } ``` ### impact(graph, uid, options?) ``` impact( graph: GraphAbstract, uid: string, options?: { edgeTypes?: string[], maxDepth?: number }, ) → { downstream: Array, upstream: Array } ``` ### serialize(snapshot) ``` serialize(snapshot: object) → string ``` Requires `setGraphProvider()`. Returns `` XML. ### parse(xml) ``` parse(xml: string) → object ``` Requires `setGraphProvider()`. Returns SystemGraph-compatible snapshot. ### setGraphProvider(provider) ``` setGraphProvider(provider: object | Function) → void ``` Inject `@wity/graph-headless` module or `GraphAbstract` constructor. Required for `serialize()`/`parse()`. --- ## FORMALISMS AND ASPECTS — standalone (system-core) FLOW_NETWORK, PLAIN — the two structures, as data: { id, label, guide: { when, not? }, follows(connectionType), followsAll?, begins(assetType), cost(connectionType, edge) | null } STRUCTURES — { 'flow-network': FLOW_NETWORK, plain: PLAIN } DEFAULT_STRUCTURE — 'flow-network' readStructure(structure, typeRegistry) → { edgeTypes: string[] | null, sourceTypes: string[], cost: ((edge) => number) | null } ``` validateAspect(declaration | null, aspect, { memberType?: (name) => object | null }) → string[] — what makes it not an aspect in shape ([] = it is one) checkAspect(declaration, { members, lines }, { graph, uidOf, links? }) → Array<{ rule, member?, line?, message }> — the one rule checker RULES → string[] — the rules a declaration may list, in the order they are checked memberKind(member, memberType?) → string | null lineKindOf(line, declaration) → string | null — its own kind, or its formalism's only one wordsOf(declaration) → { member, line, self } — the words a formalism uses for its parts aspectUid(id) → '$sys/aspect/' · memberUid(id, memberId) → '$sys/aspect//' ASPECT_TYPE = 'system-aspect' · MAX_MEMBERS = 500 ``` The rules: - `count` — a kind of member occurs exactly / at least / at most n times — `{ rule: 'count', kind, exactly | min | max }` - `degree` — how many lines come into and go out of each member of a kind — `{ rule: 'degree', kind, in: [min, max], out: [min, max], line? }` (`null` max: any number) - `labelled-out` — every line out of a kind of member carries a label, and no two the same — `{ rule: 'labelled-out', kind, line? }` - `link` — what a member names in a link field is there — `{ rule: 'link', field }` (checked against the system: a task's performer is one of its assets) - `joins` — every line joins kinds of member its kind of line allows — `{ rule: 'joins' }`, with `pairs` on the line kinds - `reachable-from` — every member can be reached from the one member of a kind — `{ rule: 'reachable-from', kind, line? }` - `can-reach` — from every member, a member of a kind can be reached — `{ rule: 'can-reach', kind, line? }` - `acyclic` — no line of a kind leads back to where it started — `{ rule: 'acyclic', line }` - `tree` — each member has at most one line of a kind coming in, and none leads back to itself — `{ rule: 'tree', line }` - `connected` — no member is cut off from the rest — `{ rule: 'connected' }` ## system-formalisms (published as @wity-ai/system-formalisms; in the monorepo: wity-system/formalisms) ``` formalisms → object[] — every formalism, to hand a system: new SystemGraph({ GraphAbstract, formalisms }) process, stateMachine, petriNet, behaviourTree, syntaxTree → object — each formalism's declaration STEP_KINDS → { [kind]: { label, says, in: [min, max], out: [min, max] } } lanes(aspect) → { lanes: [{ performedBy: string | null, steps: string[] }], handovers: [{ from, to, fromPerformer, toPerformer, label? }] } describe(declaration) → { id, label, when?, not?, member, line, kinds, anyKind, memberTypeKind?, lineKinds, links, rules: string[], analyses } describeInWords(declaration) → string ``` The formalisms (generated from their declarations): ``` Process (formalism 'process') use it for: Things that happen in an order — a procedure, a workflow, a flowchart, a checklist with decisions in it. not for: Parts that exist and connect (that is the map's assets and connections), or how a number changes over time (a behaviour). members: steps — start (where the process begins; flows in 0, out 1) · end (where it finishes; flows in 1 or more, out 0) · task (a piece of work; flows in 1 or more, out 1) · branch (one way out is taken — each flow out says its case in its label; flows in 1 or more, out 2 or more) · split (every way out is taken, side by side; flows in 1 or more, out 2 or more) · join (waits for all the ways in, then goes on; flows in 2 or more, out 1) lines: flows — then (`from` happens, then `to`) links: performedBy (the uid of the asset that does the step — the steps one asset does are its lane) a member type is a task unless it says another kind checked: A process begins at one start; A process finishes at an end; Each way out of a branch says the case it is taken in, and no two say the same; A step is performed by an asset of the system; Every step can be reached from the start; From every step, an end can be reached analyses: lanes (who performs which steps, and where the work changes hands) State machine (formalism 'state-machine') use it for: The conditions one thing can be in and the events that move it between them — an order (placed, paid, shipped), a connection, a door, a document under review. not for: Work done in an order by several parts (that is a process), or a choice someone must make (a decision). members: states — initial (where it starts — a marker, with one transition to the first state; transitions in 0, out 1) · state (a condition it stays in until an event takes it elsewhere; transitions in any, out any) · final (where it ends — nothing leaves it; transitions in any, out 0) lines: transitions — transition (on the event in `label`, `from` gives way to `to` (`guard`: a condition that must also hold)) a member type is a state unless it says another kind checked: A state machine starts at one initial marker; Each transition out of a state says the event it is taken on, and no two out of one state say the same; Every state can be reached from the initial marker Petri net (formalism 'petri-net') use it for: Things that are used up and made, or shared and waited for — stock and orders, machines and jobs, a protocol where two sides must both be ready. not for: A plain order of steps with no counting of what is available (that is a process). members: nodes — place (holds tokens — a stock, a free machine, a waiting order (`tokens`: how many it starts with); arcs in any, out any) · transition (something that happens when every place leading into it has enough tokens; arcs in any, out any) lines: arcs — arc (from a place to a transition (it takes tokens) or from a transition to a place (it gives them); `weight`: how many) a member type is a place unless it says another kind checked: A Petri net has at least one place; A Petri net has at least one transition; Each arc joins the kinds it may: arc place → transition or transition → place; No node is cut off from the rest Behaviour tree (formalism 'behaviour-tree') use it for: How an agent — a robot, a game character, an automated responder — picks what to do next from what is true right now: priorities, fallbacks, checks before acting. not for: A fixed order of steps (that is a process), or the conditions a thing can be in (a state machine). members: nodes — sequence (does its children in order; fails at the first that fails; links in 0 to 1, out 1 or more) · selector (tries its children in order; succeeds at the first that succeeds; links in 0 to 1, out 1 or more) · parallel (does all its children; succeeds when all succeed; links in 0 to 1, out 1 or more) · inverter (one child; succeeds when it fails and fails when it succeeds; links in 0 to 1, out 1) · repeat (one child, done again (`times`: how many; forever when not said); links in 0 to 1, out 1) · action (a leaf: something done; links in 0 to 1, out 0) · condition (a leaf: something checked — true or false; links in 0 to 1, out 0) lines: links — child (`to` is a child of `from`; a node's children are tried in the order of its lines) a member type is a action unless it says another kind checked: It is a tree: each node has at most one parent, and none leads back to itself; It is one tree: no node is cut off from the rest Syntax tree (formalism 'syntax-tree') use it for: The structure of something written — a formula, a line of code, a query, a sentence — shown as parts within parts. not for: Choosing what to do (a behaviour tree), or steps in an order (a process). members: nodes — of any kind you name lines: links — child (`to` is a part of `from`; parts are in the order of the lines, and `label` may say what part it is) checked: It is a tree: each node has at most one parent, and none leads back to itself; It is one tree: no node is cut off from the rest ``` --- ## ASSET TYPE TRAIT DEFAULTS ```js role: 'relay' canFail: true cascadeOnFailure: true coordFields: null ``` ## CONNECTION TYPE TRAIT DEFAULTS ```js directionality: 'directed' carriesFlow: true defaultCost: 1 costField: null ``` ## SNAPSHOT FORMAT (v1) ```js { version: 1, graph: { nodes: Array<{ uid, type, data }>, edges: Array<{ uid, srcUid, targetUid, type, data }> }, scenarios: { [name]: { graph, viewport, regions } }, spatial: { coordFields: string[] } | null, types: { assetTypes: object, connectionTypes: object, memberTypes?: object } | null, viewport: { center: {a,b} | null, zoom: number | null, bounds: {sw:{a,b}, ne:{a,b}} | null }, regions: { [uid]: { label: string, polygon: number[][], meta: object } } | null, playback: Array<{ type: string, [key]: any }> | null, // each of these only when the system has it: canvas, meta, run, view, dynamics, aspects: { nodes: Array<{ uid, type, label, data }>, edges: Array<{ uid, srcUid, targetUid, type, data }> }, } // read, never written: `occurants` (0.14's processes), `activities` (0.13's), `types.stepTypes` ``` ## GRAPH MAPPING (for serialization) ``` $sys → type: 'system-document' — version, viewport, spatial, top-level fields $sys/types → type: 'system-types' — serialized TypeRegistry $sys/scenario/ → type: 'system-scenario' — saved scenario data $sys/aspect/ → type: 'system-aspect' — an aspect: { id, formalism, label?, … } $sys/aspect// → type: the member's type, else its kind — a member of that aspect → typed nodes — directly from system graph → typed edges — directly from system graph 'contains' edges → $sys → $sys/types, $sys → $sys/scenario/*, $sys → $sys/aspect/ { order }, an aspect → its members { order } an aspect's lines → edges between its members, typed by the line's kind, { order, label?, … } ``` --- ## SimulationRuntime ### Constructor ``` new SimulationRuntime(system: SystemGraph, opts?: { speed?: number, — default 1 tickRate?: number, — default 100 (ms) historyDepth?: number, — default 360 eventNeighbours?: boolean, — default true: an event carries `neighbours` (the assets one connection away, their signals then) inputLogDepth?: number, — default 1000: input events kept for getInputEvents }) ``` ### Inputs — what comes in from outside ``` defineInputEffect(typeName, input, effects: [{ feature, set? | add? | scale?: number | { payload: key } }]) → this playInputStream(stream, sources?) → this — lenient (throws only on an unknown kind); its events arrive as time reaches them, each checked as it arrives sendInput({ input, asset, payload? }) → void — arrives on the next tick; checked getInputEvents({ from?, to? }?) → [{ input, uid, simTime, payload?, stream? }] — what came in, oldest first ``` An input event arrives after the generators, before influence; emitted on 'event' (source 'input') before the tick's events. ### Schema ``` defineFeatures(typeName: string, schemas: FeatureSchema[]) → this defineFeaturesFromSystem() → this — pull feature schemas from the system's types defineMode(name: string, predicate) → this setGenerator(typeName: string, featureId: string, fn: GeneratorFn) → this defineInfluence(connectionType: string, rules: InfluenceRule[]) → this defineEvent(eventId: string, { condition: ConditionFn, cooldown?: number } | { mode: string, cooldown?: number }) → this FeatureSchema = { id: string, — required, unique within type frequency: number, — required, ms between emissions type?: 'number' | 'boolean', — default 'number' defaultValue?: number, — default 0 range?: [number, number], unit?: string, } GeneratorFn = (currentValue: number, simTime: number, dt: number, asset: object) => number InfluenceRule = { source: string, target: string, fn: (srcVal, tgtVal, conn) => number } ConditionFn = (assetState: object, asset: object) => boolean ``` ### Binding ``` bind() → this bindAsset(uid: string, typeName: string) → void unbindAsset(uid: string) → void ``` ### Control ``` start(opts?: { speed?: number }) → void pause() → void resume(speed?: number) → void stop() → void step(dt?: number) → void setSpeed(speed: number) → void simTime → number running → boolean speed → number ``` ### State ``` getValue(uid: string, featureId: string) → number | undefined getAssetState(uid: string) → { [featureId]: number } | null getSnapshot() → { simTime: number, values: object } getNaturalValue(uid: string, featureId: string) → number | undefined — without the interventions on it inject(uid: string, featureId: string, value: number) → void — under an intervention: sets the natural value injectEvent(eventId: string, data?: object) → void — emits { id, simTime, source: 'external', ...data } ``` ### Interventions — an outside actor forcing a signal for a while (an operator, a decision carried out, an incident) ``` applyIntervention({ uid, feature, set?, scale?, add?, durationMs?, source? }) → id — at least one of set / scale / add endIntervention(id: string) → boolean — whether it was active getInterventions() → [{ id, uid, feature, set?, scale?, add?, startedAt, endsAt: number | null, source? }] — active, oldest first The behaviour keeps producing the natural value; the signal is (set ?? natural) × scale + add, in the feature's range, each active intervention in the order made. Not part of getSnapshot() / restore(). ``` ### History ``` trackHistory(uid: string, featureId: string) → void trackAssetHistory(uid: string) → void getHistory(uid: string, featureId: string, opts?: { from?, to? }) → { times: number[], values: number[] } | null getNaturalHistory(uid: string, featureId: string, opts?) → { times, values } | null — a tracked signal's natural values, once intervened on ``` ### Events ``` on(event: string, fn: Function) → unsubscribe: () => void Events: 'tick', 'event', 'intervention', 'started', 'paused', 'stopped' Tick: { simTime, dt, changedCount } Event: { id, uid, simTime, state, source: 'condition' | 'external' | 'input', neighbours?: [{ uid, via, direction: 'to' | 'from', type, state }] } — an input event: id = its input's name, and input, payload?, stream? Intervention: { op: 'start' | 'end', intervention } ``` ### Serialization ``` serialize() → { simTime: number, values: object } restore(data: { values: object }) → void destroy() → void ``` --- ## FeatureStore ``` defineFeatures(typeName: string, schemas: FeatureSchema[]) → void addAsset(uid: string, typeName: string) → void removeAsset(uid: string) → void getValue(uid: string, featureId: string) → number | undefined setValue(uid: string, featureId: string, value: number) → void getAssetState(uid: string) → object | null getFeatureArray(typeName: string, featureId: string) → Float64Array | null getDueFeatures(typeName: string, simTime: number) → string[] markEmitted(typeName: string, featureId: string, simTime: number) → void totalFeatureCount → number getTypeNames() → string[] getAssetUids(typeName: string) → string[] getSchemas(typeName: string) → Map | null getLocation(uid: string) → { typeName, ordinal } | null snapshot() → object restore(data: object) → void ``` ## SimClock ``` new SimClock(onTick: (simTime, dt) => void, opts?: { tickRate?, speed? }) simTime → number speed → number running → boolean tickRate → number start() / pause() / resume(speed?) / stop() / step(dt?) / reset() setSpeed(speed: number) → void ``` ## InfluenceEngine ``` define(connectionType: string, rules: InfluenceRule[]) → void propagate(system: SystemGraph, store: FeatureStore, updatedFeatures: Set) → void hasRules(connectionType: string) → boolean getRules(connectionType: string) → InfluenceRule[] clear() → void ``` ## HistoryBuffer ``` new HistoryBuffer(depth?: number) track(uid: string, featureId: string) → void trackAsset(uid: string, featureIds: Iterable) → void untrack(uid: string, featureId: string) → void record(uid: string, featureId: string, time: number, value: number) → void get(uid: string, featureId: string, opts?: { from?, to? }) → { times: number[], values: number[] } | null isTracked(uid: string, featureId: string) → boolean clear() → void ``` --- ## DECLARATIVE DYNAMICS (system-sim) ``` buildRuntime(system, spec, opts?: { …runtime opts, streams?: string[] }, registries?) → SimulationRuntime — features from the system's types, spec compiled, bound, the input streams it is run with playing (opts.streams: these; [] none); not started compileDynamics(runtime, spec, registries?) → runtime validateDynamics(spec, ctx?: { featureExists?(type, feature), connectionTypeExists?(type), featureKnown?(feature), inputOf?(type, input), inputNames?() }, registries?) → [{ path, message }] — [] = valid; never throws; nested generators and event fields are checked simulate(system, spec, { durationMs? = 120000, stepMs? = 100, maxOccurrences? = 200, streams? }?, registries?) → { durationMs, steps, events: [{ id, count, firstAt, lastAt, assets: { uid: count } }], inputs?: [{ input, count, firstAt, lastAt, assets }] — what came in (only when something did), apart from events occurrences: [{ t, id, uid }], occurrencesTruncated, signals: [{ uid, type, feature, min, max, last, mean, threshold?: { event, op, value }, overMs? }] } — headless run; the system is not changed evaluatePredicate(pred, ctx) / validatePredicate(pred, ctx?) generatorArchetypes / influenceArchetypes / eventArchetypes / defaultRegistries ({ generators, influence, events, sources }) / ArchetypeRegistry new ArchetypeRegistry(family) · register(kind, { params?, compile }) → this · has(kind) · list() → [{ kind, params }] · compile(spec) → fn spec = { generators: { "type:feature": { kind, ...params } }, influence: { connType: [ { source, target, kind, ...params } ] }, modes: { name: predicate }, events: { eventId: { kind, ...params, cooldown? } | { kind: 'whenMode', mode, cooldown? } }, inputs: { "type:input": [ { feature, set? | add? | scale? } ] }, — a number, or { payload: key } } input streams (sourceArchetypes — a kind compiles to (from, to) → input events): { id, label?, kind: 'list', events: [{ at, input, asset, payload? }], every? } { id, label?, kind: 'sampler', input, assets: [uid], every, arrivals?: 'poisson'|'regular', draw?: [{ payload, weight? }], start?, until?, seed? } validateInputStream(stream, system, sources?) → string[] · inputEventProblems(system, { input, asset, payload? }) → string[] payloadProblems(payload, declared) → string[] · streamsToRun(system, ids?) → streams (not checked: playing is lenient; throws on an id not on the system) ``` Generator kinds (times in ms of simulation time): ``` constant value sine offset, amplitude, period, phase randomWalk step, bias noise base, amplitude ramp from, to, duration pulse base, peak, start, rise, hold, fall, every — base → peak from `start` over `rise`, holds `hold`, falls over `fall`; repeats each `every` if > 0 sum terms: generator[] — each term keeps its own previous value per asset byAsset default?: generator, assets: { uid: generator } — per-asset override; no entry → default; no default → holds its value ``` Influence kinds: `proportional` (gain) · `additive` (amount) · `decay` (rate 0..1) · `max` · `replace` Event kinds: `threshold` (feature, op, value) · `range` (feature, min, max) · `whenMode` (mode) Predicate: ``` { feature, op: '>'|'>='|'<'|'<='|'=='|'!=', value } | { feature, in: [min, max] } { all: [pred] } | { any: [pred] } | { not: pred } | { mode: name } { anyNeighbor: pred } | { allNeighbors: pred } | { anySource: pred } | { allSources: pred } { count: { of: 'neighbors'|'sources', where: pred, op, value } } ``` --- ## system-ui-compute (published as @wity-ai/system-ui-compute; in the monorepo: wity-system/ui-compute) Zero-DOM. ``` computeScene(system, opts?: { getAssetType?, getConnectionType?, positionOf?, hidden? }) → { bounds, nodes, edges, regions, legend, view } withHidden(scene, hidden: { assetTypes?, connectionTypes?, regions? }) → scene — what the reader hid left out; the legend lists all, each with `hidden`; bounds unchanged toggleHidden(hidden, kind: 'assetTypes' | 'connectionTypes' | 'regions', key) → hidden — hide one legend entry, or show it again computeAspectScene(system, aspectId) → { aspect: { id, label, formalism }, words: { member, line, … } (its formalism's), bounds, nodes, edges, regions, legend, view, findings } | null the same scene shape, for one aspect — null when there is none, or its formalism declares no layout. Drawn from the declaration: each kind's `look` (shape, size, colour), `layout` 'rows' (from where it begins) or 'tree' (parents over children, in line order), a link marked `lane` framing the members that name one asset; a node named by a finding carries `problems` aspectLayout(aspect, declaration, kindOf?) → Map · memberLook(member, system, declaration) → { kind, shape, w, h, fill, label } | null isDrawn(declaration) → boolean · ASPECT_LAYOUTS = ['rows', 'tree'] · MEMBER_LOOK · FLOW_STROKE · LANE_TONES node { uid, type, x, y, shape: one of NODE_SHAPES, fill, fillLight, fillLighter, icon, label, radius? (circle) | dims?: { w, h } (block, boxed), items?, contentColor? (block), outline?, labelLines? (boxed), annotations?: [{ text, status, dx, dy, chipW, chipH }] } edge { uid, type, srcUid, targetUid, from, to, points, stroke, strokeHover, dashed, carriesFlow, arrow, arrowStart, label?: { text, x, y, w, h } } blockDims(asset, typeDef) → { w, h } ASSET_RADIUS = 16 · LABEL_OFFSET = 28 · ARROW_SIZE = 6 · BLOCK = { width: 200, headerH: 34, itemH: 24, pad: 14, r: 6, minBodyH: 40 } lightenHex(hex, amount) · isLightColor(hex) · strongOf(hex) · typeTints(fill) → { fillLight, fillLighter } · strokeHoverOf(stroke) circleColors(asset, typeDef) · iconGlyph(icon) · regionLabelAnchor(points) · REGION_FILL · REGION_STROKE · REGION_STROKE_WIDTH the drawing vocabulary — how a type's `meta` says it is drawn: NODE_SHAPES = ['circle', 'block', 'rect', 'rounded', 'pill', 'diamond'] · BOXED_SHAPES = ['rect', 'rounded', 'pill', 'diamond'] LINE_ROUTES = ['straight', 'elbow'] · LINE_ARROWS = ['end', 'none', 'both'] · BOX (sizes of the boxed shapes) · LINE_LABEL validateTypeStyle(kind: 'asset' | 'connection', meta) → string[] — what in a type's style is not in the vocabulary shapeOf(typeDef) · routeOf(typeDef) · arrowsOf(typeDef) · boxDims(shape, typeDef) → { w, h } boxOutline(shape, { w, h }) → SVG path · boxEdgeOffset(shape, { w, h }, nx, ny) · boxLabelLines(shape, dims, text) → string[] elbowRoute(src, tgt) → points | null · trimRoute(points, by, end?) · routePath(points) → SVG path routeLabelPoint(points) · lineLabel(text, points) → { text, x, y, w, h } | null contentsText(contents) → string — what it holds in one line ('2 assets · 1 connection · 1 type · 2 processes'); only what it has is named; '' when nothing summaryView({ assets, featuresOf, events, stateOf, history, now?, maxCharts?, maxSeries? }) → what a reader is shown of a running system thresholdText({ op, value }) → string wavesView({ assets, featuresOf, events, history, marks?, now, windowMs? = 120000, pinned? = [], max? = 4 }) → { t0, t1, rows: [{ uid, label, featureId, range, last, threshold: { y, value, op } | null, points: [x, y][], over: [x0, x1][], marks }] } heatView({ assets, featuresOf, events, history, marks?, now, windowMs? = 120000, buckets? = 60 }) → { t0, t1, buckets, rows: [{ uid, label, featureId, cells: (number|null)[], peak, marks }] } — hottest first stripAssets({ assets, marks?, pinned?, max? = 4 }) → uid[] assetsOverAt({ assets, featuresOf, events, history, t }) → uid[] thresholdsByFeature(events) → { [featureId]: [{ name, op, value }] } — threshold events only stress(value, { op, value }, range?) → number — 1 = at the threshold, > 1 = past it assets [{ uid, label, type }] featuresOf (type) → { [featureId]: { range?: [lo, hi] } } events { [name]: { kind: 'threshold', feature, op, value } | … } history (uid, featureId) → { times, values } | null marks [{ t, uid, kind: 'event' | 'decision', name, status? }] ``` ## system-ui (published as @wity-ai/system-ui) — Muffin components; the host page provides the Muffin global ``` paintScene(svgGroup, scene, { arrowId, renderIcon?(name, size) → svg markup, projectRegionPoint?, emphasis? }) — the one painter (app and viewer) AspectView new AspectView(container, system, aspectId) · destroy() — one aspect, fitted to its box, from its scene; a reader explores it: click a member → it and what leads to it / follows it stay bright (impact), click a second → the way between them (shortestPath), click again / the background / Escape → clear; a line under it (.se-aspect-hint) says what is shown SignalStrip () SignalStrip.compose() configure({ assets(), featuresOf(type), events(), history(uid, featureId), now(), onSelect?(uid), simulated?, windowMs? }) mark({ t, uid, kind, name, status? }) pin(uid) · unpin(uid) · pinned focus(uid, t) · focus(null) setView('waves' | 'heat') refresh() · clear() ```