system-core (wity-system)
Headless spatial/topological network computation library. Zero DOM, zero framework, isomorphic.
Peer dependency: @wity/graph-headless
Install
npm install wity-systemImport
import { SystemGraph, TypeRegistry, cascade, impact, serialize, parse, setGraphProvider } from 'wity-system';SystemGraph
Top-level domain object. Wraps a GraphAbstract internally.
Constructor
new SystemGraph({ GraphAbstract, spatial?, types?, extent?, formalisms? })| Parameter | Type | Required | Description |
|---|---|---|---|
GraphAbstract | Function | yes | Constructor from @wity/graph-headless |
spatial | object | no | { project, unproject, coordFields } |
spatial.project | (a, b) => { x, y } | — | Domain → pixel projection |
spatial.unproject | (x, y) => { a, b } | — | Pixel → domain unprojection |
spatial.coordFields | string[] | — | Default coordinate field names, e.g. ['lat', 'lon'] |
types | TypeRegistry | no | Pre-built type registry |
extent | object | no | { sw: {a, b}, ne: {a, b} } initial bounds |
formalisms | object[] | no | The formalisms aspects of this system may be made in — declarations (wity-system/formalisms has them) |
Type registration
registerAssetType(name, definition) → this
registerConnectionType(name, definition) → this
getAssetType(name) → object | null
getConnectionType(name) → object | null
getAssetTypes() → Map
getConnectionTypes() → Map
types → TypeRegistryAsset CRUD
addAsset(uid, type, data?) → this
updateAsset(uid, data) → this
getAsset(uid) → object | null
removeAsset(uid) → this
getAssets() → object[]
getAssetsByType(type) → object[]
assetCount → numberConnection CRUD
addConnection(srcUid, targetUid, type?, data?) → Edge
updateConnection(srcUid, targetUid, type?, data?) → Edge
getConnection(srcUid, targetUid, type?) → Edge | null
removeConnection(srcUid, targetUid, type?) → this
getConnections() → Edge[]
getConnectionsByType(type) → Edge[]
getOutgoing(uid, type?) → Edge[]
getIncoming(uid, type?) → Edge[]
getConnectionsAmong(assetUids) → Edge[]
connectionCount → numberConnection UIDs are auto-generated as ${type}:${srcUid}→${targetUid}.
Propagation & analysis
propagate(fromUid, options?) → Array<{ uid, depth, via }>
cascade(disabledUids, options?) → { reachable: string[], cutOff: string[], disabled: string[] }
impact(uid, options?) → { downstream: Array, upstream: Array }Propagation options: { direction?, edgeTypes?, maxDepth? }. Cascade options: { sourceUids?, edgeTypes?, direction? }. Impact options: { edgeTypes?, maxDepth? }.
All auto-read traits from the type registry when populated.
Pathfinding
reachable(fromUid, options?) → string[]
shortestPath(fromUid, toUid, options?) → { path: string[], cost: number } | null
allPaths(fromUid, toUid, options?) → Array<{ path: string[], cost: number }>Pathfinding options: { edgeTypes?, cost? }. cost is (edge) => number. Auto-built from costField/defaultCost traits when omitted.
The same analyses over an aspect
Every analysis above takes in: <aspect id> and then answers for that aspect instead of the structure: nodes are its members, named by member id, and its lines are followed.
impact(memberId, { in, line?, maxDepth? }) → { downstream, upstream } // each [{ uid: memberId, depth, via }]
propagate(memberId, { in, line?, direction?, maxDepth? }) → [{ uid, depth, via }]
reachable(memberId, { in, line? }) → memberId[]
shortestPath(fromId, toId, { in, line?, costField? }) → { path, cost } | null
allPaths(fromId, toId, { in, line?, maxDepth? })
cascade(memberIds, { in, direction? }) → { reachable, cutOff, disabled } // from the formalism's entry kind; 'upstream': back from its exit kindline keeps to one kind of line. costField — a number field the nodes carry — works for the structure too: the way on which that field adds up to the least.
Viewport
setCenter(a, b) → this
getCenter() → { a, b } | null
setZoom(zoom) → this
getZoom() → number | null
setExtent(sw, ne) → this
getExtent() → { sw: {a,b}, ne: {a,b} } | null
setViewport({ center?, zoom?, bounds? }) → this
getViewport() → { center, zoom, bounds }
computeExtent() → { sw: {a,b}, ne: {a,b} } | null
getExtentOfAssets(uids) → { sw: {a,b}, ne: {a,b} } | nullSpatial
isSpatial → boolean
project(a, b) → { x, y }
unproject(x, y) → { a, b }
reproject() → void
setSpatial({ project, unproject, coordFields }) → void
clearSpatial() → void
getCoordFields() → string[] | null
setCoordFields(fields | null) → thisTwo different things, kept apart:
- Position fields (
getCoordFields/setCoordFields) say which fields of an asset's data are its position — e.g.['lat', 'lon']for a system placed on the real world,nullfor one laid out on the canvas (x/y). They are part of the model: saved with it (snapshot().spatial) and read the same wherever the system is opened. An asset type may name its own (coordFieldson the type). Regions and extents use them. - A projection (
setSpatial/project/unproject/reproject) turns those coordinates into a drawing surface's pixels. It belongs to whatever is drawing the system and is not saved.isSpatialsays whether one is attached.setSpatialalso sets the position fields;clearSpatialclears both.
A document saved before position fields were recorded with it (or by something that dropped them) is read by two facts in the document (earlierCoordFields(graphData, viewport)): it is on the real world (GEOGRAPHIC_FIELDS — lat, lon) if it has a saved map centre, or if its assets carry lat / lon and none carries a canvas position; otherwise it is on the canvas. A saved scenario records its position fields too.
Regions
addRegion(uid, { label?, polygon, meta? }) → this
updateRegion(uid, { label?, polygon?, meta? }) → this
moveRegion(uid, da, db) → this // shift every corner by (da, db), in the polygon's own fields
getRegion(uid) → { label, polygon, meta } | null
removeRegion(uid) → boolean
getRegions() → Array<{ uid, label, polygon, meta }>
regionCount → number
getAssetsInRegion(regionUid) → string[]
getRegionsOfAsset(assetUid) → string[]polygon is an array of [a, b] coordinate pairs (minimum 3), in the system's position fields (see Spatial) — e.g. lat, lon — or the canvas x / y when it is laid out on the canvas. An asset inside the polygon belongs to the region.
Identity and saved canvas view
getMeta() → object // { title?, subtitle?, description?, … } (a copy)
setMeta(patch) → this // merge; a field given as '' / null is removed
getCanvasView() → { x, y, zoom } | null // where a canvas-laid system was last left
setCanvasView({ x, y, zoom } | null) → thisBoth are part of snapshot() (meta, canvas) only when set, so a system with neither serializes exactly as before.
How it is run and shown
getRun() → { autoplay, behaviour, streams, speed } // the defaults (off, the first behaviour, every input stream, 1×) where nothing was set
setRun({ autoplay?, behaviour?, streams?, speed? }) → this // streams: input stream ids, null for all of them, [] for none; speed: above 0, at most 100
getView() → { open, focus, legend } // open: null (its first view) | 'map' | an aspect's id; focus: null | { region } | { assets }; legend: one of VIEW_LEGEND ('open' | 'collapsed')
setView({ open?, focus?, legend? }) → this // open: refused unless 'map' or one of its aspects; it falls back to null when that aspect is removed
getViewAssets() → Node[] // the assets the view is focused onIn snapshot() as run / view, only what differs from the defaults.
Inputs and input streams
getInputs(typeName) → { inputName: { label?, payload? } } | null // an asset type's inputs (its input ports)
getInputStreams() → stream[] · getInputStream(id) → stream | null
setInputStream(stream) → this · removeInputStream(id) → booleanAn asset type's inputs and the system's input streams — what comes in from outside, and when. See Inputs.
Formalisms and aspects
A further model of the same system, in the same graph, beside its assets and connections — see Aspects and formalisms for what they are and how they are stored.
registerFormalism(declaration) → this
getFormalism(id) → object | null
getFormalisms() → object[]
getContents() → { assets, connections, types, aspects: [{ formalism, count, one, many }] } // what it holds, counted
registerMemberType(name, { formalism, kind?, label?, description?, features?, meta? }) → this
getMemberType(name) → object | null
getMemberTypes(formalism?) → Map
setAspect({ id, formalism, label?, members, lines? }) → this // throws when not an aspect in shape
getAspect(id) → aspect | null
getAspects(formalism?) → aspect[]
removeAspect(id) → boolean
setAspectMember(id, member, { before? }?) → this // add, or replace by id
removeAspectMember(id, memberId) → boolean // with its lines
setAspectLine(id, line, { at? }?) → this // add, or replace the same kind between the same two
removeAspectLine(id, { from, to, kind? }) → boolean
checkAspect(id) → [{ rule, member?, line?, message }] // [] when sound
memberKind(member) → string | null
membersLinkedTo(uid) → [{ aspect, member, link }]
aspectGraph(id) → view // + uidOf(memberId), idOf(uid)Standalone, from the same package: validateAspect(declaration, aspect, { memberType? }) → string[], checkAspect(declaration, { members, lines }, { graph, uidOf, links? }), RULES, memberKind, lineKindOf, wordsOf, aspectUid, memberUid, ASPECT_TYPE, MAX_MEMBERS.
Dynamics (the behaviour layer)
getDynamics() → object[] // the behaviours, in order (copies)
getDynamicsSpec(id) → object | null
setDynamics(layers | spec | null) → this // replace the whole layer; null / [] clears it
setDynamicsSpec(spec) → this // add, or replace the one with the same id (keeps its place); needs an id
removeDynamicsSpec(id) → booleanHow the system's signals evolve is authored as data: a list of named Dynamics specs ({ id, label?, generators, influence, modes, events }) — one system can bear many (normal / peak / failure). The system holds them and includes them in snapshot() (only when there is at least one, so a system without behaviours serializes exactly as before). What a spec means, and whether it is valid, belongs to system-sim.
Playback
setPlayback(steps) → this
getPlayback() → Array<{ type, ...fields }>
resolveSelector(selector) → string[]
resolvePlaybackStep(step, prevAssets?) → { assets, connections, regions, viewport }Selector types: region, assetType, path, uids, all, union.
Scenarios
saveScenario(name) → this
loadScenario(name) → this
deleteScenario(name) → boolean
getScenarioNames() → string[]
diffScenarios(nameA, nameB) → { assets, connections, regions }Each category in diff returns { added, removed, modified }.
Serialization
snapshot() → object
restore(data, { spatial? }?) → void
graph → GraphAbstractTypeRegistry
Standalone type schema registry. Can be used independently or via SystemGraph.
Constructor
new TypeRegistry()Asset types
registerAssetType(name, definition?) → this
getAssetType(name) → object | null
getAssetTypes() → Map
hasAssetType(name) → boolean
removeAssetType(name) → booleanConnection types
registerConnectionType(name, definition?) → this
getConnectionType(name) → object | null
getConnectionTypes() → Map
hasConnectionType(name) → boolean
removeConnectionType(name) → booleanMember types
registerMemberType(name, { formalism, kind, label?, description?, features?, meta? }) → this
getMemberType(name) → object | null
getMemberTypes() → Map
hasMemberType(name) → boolean
removeMemberType(name) → boolean
memberTypeCount → numberA kind of member a system declares for its aspects, in one formalism. That its kind is one of the formalism's is checked by SystemGraph.registerMemberType, which knows the formalisms.
Trait queries
getSourceTypes() → string[]
getSinkTypes() → string[]
getFlowTypes() → string[]
getFailableTypes() → string[]
buildCostFn() → (edge) => numberCounts
assetTypeCount → number
connectionTypeCount → number
isEmpty → booleanSerialization
serialise() → { assetTypes, connectionTypes, memberTypes? } // memberTypes only when there are some
hydrate(data) → thiscascade(graph, disabledUids, options?)
Pure function. Cascade analysis over a raw GraphAbstract.
cascade(graph, disabledUids, options?) → { reachable: string[], cutOff: string[], disabled: string[] }| Parameter | Type | Description |
|---|---|---|
graph | GraphAbstract | The graph to analyze |
disabledUids | string[] | UIDs of disabled nodes |
options.sourceUids | string[] | Override source detection |
options.edgeTypes | string[] | Edge types to traverse |
options.direction | string | 'downstream' (default), 'upstream', 'both' |
impact(graph, uid, options?)
Pure function. Upstream and downstream analysis over a raw GraphAbstract.
impact(graph, uid, options?) → { downstream: Array, upstream: Array }| Parameter | Type | Description |
|---|---|---|
graph | GraphAbstract | The graph to analyze |
uid | string | Node to analyze |
options.edgeTypes | string[] | Edge types to traverse |
options.maxDepth | number | Maximum traversal depth |
serialize(snapshot)
Serialize a SystemGraph snapshot to a <wity-knowledge> XML document. Requires setGraphProvider() to have been called.
serialize(snapshot) → stringComposing structure + behaviour in one document
A snapshot may carry an optional dynamics field — one or more behaviour specs (see Declarative dynamics) — which serialize as a <dynamics> layer alongside the system, so structure and behaviour travel as a single artifact while staying cleanly separated:
sys.setDynamics([
{ id: 'normal', label: 'Normal ops', generators: { /* … */ }, influence: {}, events: {} },
{ id: 'peak', label: 'Peak load', generators: { /* … */ }, influence: {}, events: {} },
]);
const doc = serialize(sys.snapshot());The dynamics payload is opaque to the serializer (its shape is owned by system-sim), and one system can bear many dynamics — the array above is parse()d back onto snapshot.dynamics, and restore() reads it back into the system (getDynamics()). A snapshot without dynamics serializes exactly as before (no <dynamics> layer, byte-identical).
parse(xml)
Parse a <wity-knowledge> XML document into a SystemGraph-compatible snapshot. Requires setGraphProvider().
parse(xml) → objectsetGraphProvider(provider)
Inject the wity-graph substrate for serialize() and parse(). Call once at startup.
import * as graph from '@wity/graph-headless';
import { setGraphProvider } from 'wity-system';
setGraphProvider(graph);Accepts either the full @wity/graph-headless module (extracts GraphAbstract, serialize, parse) or a GraphAbstract constructor function directly.
