Skip to content

system-core (wity-system) ​

Headless spatial/topological network computation library. Zero DOM, zero framework, isomorphic.

Peer dependency: @wity/graph-headless

Install ​

bash
npm install wity-system

Import ​

js
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? })
ParameterTypeRequiredDescription
GraphAbstractFunctionyesConstructor from @wity/graph-headless
spatialobjectno{ project, unproject, coordFields }
spatial.project(a, b) => { x, y }—Domain → pixel projection
spatial.unproject(x, y) => { a, b }—Pixel → domain unprojection
spatial.coordFieldsstring[]—Default coordinate field names, e.g. ['lat', 'lon']
typesTypeRegistrynoPre-built type registry
extentobjectno{ sw: {a, b}, ne: {a, b} } initial bounds
formalismsobject[]noThe 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 → TypeRegistry

Asset CRUD ​

addAsset(uid, type, data?) → this
updateAsset(uid, data) → this
getAsset(uid) → object | null
removeAsset(uid) → this
getAssets() → object[]
getAssetsByType(type) → object[]
assetCount → number

Connection 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 → number

Connection 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 kind

line 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} } | null

Spatial ​

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) → this

Two 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, null for 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 (coordFields on 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. isSpatial says whether one is attached. setSpatial also sets the position fields; clearSpatial clears 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) → this

Both 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 on

In 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) → boolean

An 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) → boolean

How 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 → GraphAbstract

TypeRegistry ​

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) → boolean

Connection types ​

registerConnectionType(name, definition?) → this
getConnectionType(name) → object | null
getConnectionTypes() → Map
hasConnectionType(name) → boolean
removeConnectionType(name) → boolean

Member types ​

registerMemberType(name, { formalism, kind, label?, description?, features?, meta? }) → this
getMemberType(name) → object | null
getMemberTypes() → Map
hasMemberType(name) → boolean
removeMemberType(name) → boolean
memberTypeCount → number

A 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) => number

Counts ​

assetTypeCount → number
connectionTypeCount → number
isEmpty → boolean

Serialization ​

serialise() → { assetTypes, connectionTypes, memberTypes? }      // memberTypes only when there are some
hydrate(data) → this

cascade(graph, disabledUids, options?) ​

Pure function. Cascade analysis over a raw GraphAbstract.

cascade(graph, disabledUids, options?) → { reachable: string[], cutOff: string[], disabled: string[] }
ParameterTypeDescription
graphGraphAbstractThe graph to analyze
disabledUidsstring[]UIDs of disabled nodes
options.sourceUidsstring[]Override source detection
options.edgeTypesstring[]Edge types to traverse
options.directionstring'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 }
ParameterTypeDescription
graphGraphAbstractThe graph to analyze
uidstringNode to analyze
options.edgeTypesstring[]Edge types to traverse
options.maxDepthnumberMaximum traversal depth

serialize(snapshot) ​

Serialize a SystemGraph snapshot to a <wity-knowledge> XML document. Requires setGraphProvider() to have been called.

serialize(snapshot) → string

Composing 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:

js
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) → object

setGraphProvider(provider) ​

Inject the wity-graph substrate for serialize() and parse(). Call once at startup.

js
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.