Algorithms
wity-system provides trait-enriched wrappers around graph-headless's traversal and pathfinding primitives, plus domain-level cascade and impact analysis. When types are registered, algorithms auto-read behavioral traits. Explicit options always override.
What the structure is: a flow network, or a plain map
The analyses read the types' traits as the system's structure says (formalism/structure.js, declared as data like an aspect's formalism):
| structure | follows | begins at | a route costs |
|---|---|---|---|
'flow-network' (the default — every system before 0.19) | connection types with carriesFlow: true (all, when none is marked) | asset types with role: 'source' | defaultCost, or the connection's costField |
'plain' | every connection | nothing — cascade must be given sourceUids | one per step |
sys.setStructure('plain'); // an org chart, a service map: nothing flows
sys.getStructure(); // 'plain'
sys.entryAssets(); // [] — the sources of a flow network; none hereThe declarations are exported: FLOW_NETWORK, PLAIN, STRUCTURES (by id) and DEFAULT_STRUCTURE ('flow-network'); readStructure(structure, types) → { edgeTypes, sourceTypes, cost } is how the analyses read a system's types under one — edgeTypes null for all of them, cost null for one per step.
It is stored with the system only when it is not 'flow-network', so a flow network stores exactly as before. The type registry's getSourceTypes, getFlowTypes and buildCostFn are the flow network's reading of the traits.
Propagation
BFS traversal from a starting node, following edges in a given direction.
sys.propagate('V1');
// → [{ uid: 'H1', depth: 1, via: 'V1' }]
sys.propagate('H1', { direction: 'upstream' });
// → [{ uid: 'V1', depth: 1, via: 'H1' }, { uid: 'WTP', depth: 2, via: 'V1' }]Options:
| Option | Default | Description |
|---|---|---|
direction | 'downstream' | 'downstream', 'upstream', or 'both' |
edgeTypes | auto (flow types) | Edge types to traverse. Auto-filtered to carriesFlow: true types when registry is populated |
maxDepth | Infinity | Maximum traversal depth |
Cascade
Cascade analysis determines which nodes are cut off from source nodes when specific nodes are disabled.
sys.cascade(['V1']);
// → { reachable: ['WTP'], cutOff: ['H1'], disabled: ['V1'] }The algorithm:
- Source detection — auto-detects sources from asset types with
role: 'source', or uses explicitly providedsourceUids - Edge filtering — auto-filters to connection types with
carriesFlow: true, or uses providededgeTypes - BFS from sources — skipping disabled nodes, marking everything reachable
- Cut-off classification — everything not reachable and not disabled is cut off
// Explicit source/edge overrides
sys.cascade(['V1'], {
sourceUids: ['WTP', 'BACKUP'],
edgeTypes: ['feeds'],
direction: 'downstream',
});Options:
| Option | Default | Description |
|---|---|---|
sourceUids | auto (source role) | Override source node detection |
edgeTypes | auto (flow types) | Edge types to consider for reachability |
direction | 'downstream' | 'downstream', 'upstream', or 'both' |
Returns { reachable: string[], cutOff: string[], disabled: string[] }.
Impact
Full impact analysis — what is upstream and downstream of a node.
sys.impact('V1');
// → { downstream: [...], upstream: [...] }
sys.impact('V1', { maxDepth: 2 });
// → limited to 2 hops in each directionOptions:
| Option | Default | Description |
|---|---|---|
edgeTypes | auto (flow types) | Edge types to traverse |
maxDepth | Infinity | Maximum traversal depth in each direction |
Returns { downstream: Array, upstream: Array }, each array containing { uid, depth, via } entries.
Reachable
List all nodes reachable from a starting node.
sys.reachable('WTP');
// → ['V1', 'H1']
sys.reachable('H1', { direction: 'upstream' });
// → ['V1', 'WTP']Options:
| Option | Default | Description |
|---|---|---|
direction | 'downstream' | 'downstream', 'upstream', or 'both' |
edgeTypes | auto (flow types) | Edge types to traverse |
Shortest path
Dijkstra's shortest path with trait-driven cost functions.
sys.shortestPath('WTP', 'H1');
// → { path: ['WTP', 'V1', 'H1'], cost: 620 }Cost resolution (when types are registered):
- If the edge's connection type defines
costFieldandedge.data[costField]exists, use that value - Otherwise use
defaultCostfrom the connection type - No type registered for the edge type → cost is
1
// Override with a custom cost function
sys.shortestPath('WTP', 'H1', { cost: (edge) => edge.data.latency ?? 1 });Options:
| Option | Default | Description |
|---|---|---|
edgeTypes | auto (flow types) | Edge types to traverse |
cost | auto (costField/defaultCost) | Custom (edge) => number cost function |
Returns { path: string[], cost: number } or null if no path exists.
All paths
DFS-based enumeration of all paths between two nodes.
sys.allPaths('WTP', 'H1', { maxDepth: 10 });
// → [{ path: ['WTP', 'V1', 'H1'], cost: 620 }]Options:
| Option | Default | Description |
|---|---|---|
maxDepth | none | Maximum path length |
edgeTypes | auto (flow types) | Edge types to traverse |
cost | auto (costField/defaultCost) | Custom cost function |
Returns an array of { path: string[], cost: number }.
Pure function variants
cascade and impact are also exported as standalone pure functions that operate directly on a GraphAbstract, without trait enrichment:
import { cascade, impact } from 'wity-system';
const result = cascade(graph, ['V1'], {
sourceUids: ['WTP'],
edgeTypes: ['feeds'],
});
const analysis = impact(graph, 'V1', {
edgeTypes: ['feeds'],
maxDepth: 5,
});Use the pure function variants when you need to run algorithms against a raw graph without a SystemGraph wrapper.
