This guide explains how to work on spytial-core, with a focus on the pipeline-driven flow that powers demos and UI integrations. Use this as a map for where to plug in new functionality and how to reason about data moving through the system.
Everything in this repo revolves around a pipelined flow from input data → evaluation → layout → rendering. You can see the full flow in the HTML demos inside webcola-demo/, which stitch together the core TypeScript APIs and visualize the output.
-
Data instance ingestion
- Parse or normalize input data (Alloy XML, JSON, Forge, etc.) into a
DataInstance. - This is the canonical format that the rest of the system consumes.
- Parse or normalize input data (Alloy XML, JSON, Forge, etc.) into a
-
Evaluator initialization
- Pick a query evaluator (e.g., Forge-based, SGraph-based) that can interpret selectors.
- Initialize the evaluator with the
DataInstanceso selectors and constraints can be resolved.
-
Layout specification (CnD)
- Parse a CnD spec (constraints + directives) into a
LayoutSpec. - This spec defines what the layout engine must satisfy (alignment, ordering, spacing, color, etc.).
- Note: Projections are not part of the layout spec. They are applied as a pre-layout data transform via
applyProjectionTransform().
- Parse a CnD spec (constraints + directives) into a
-
Layout instance generation
- Build a
LayoutInstancefrom theLayoutSpec+ evaluator. - Generate a layout using the
DataInstance(after any projection transform has been applied).
- Build a
-
Rendering / visualization
- Use the generated layout with WebCola, SVG, Canvas, or a React-based UI.
- Most demos render through WebCola and provide controls for re-running the pipeline.
-
Sequence continuity (optional)
- For ordered sequences of instances (e.g., Alloy traces), pass a
policy,prevInstance, andcurrInstancetorenderLayout(). - A
SequencePolicy(e.g.,stability,changeEmphasis,randomPositioning) is applied pairwise insiderenderLayoutto resolve prior positions for the solver. - See docs/SEQUENCE_LAYOUT_API.md for the full API reference.
- For ordered sequences of instances (e.g., Alloy traces), pass a
The demos are the best references for how to wire everything up end-to-end:
webcola-demo/alloy-demo.html- Demonstrates the Alloy XML → AlloyDataInstance → ForgeEvaluator → Layout → WebCola pipeline.
webcola-demo/json-demo.html- Demonstrates the JSON → JSONDataInstance → SGraphQueryEvaluator → Layout → WebCola pipeline.
webcola-demo/dot-demo.html- Demonstrates the DOT → DotDataInstance → Evaluator → Layout → WebCola pipeline, including the type hierarchy system.
webcola-demo/selector-synthesis-demo.html- Shows the selector synthesis pipeline, including initialization and status reporting.
webcola-demo/structured-input-demo.html- Full pipeline with structured input and interactive layout.
webcola-demo/integrated-demo-components.tsx- Example of how the pipeline can integrate into component-driven UIs.
When adding new features, start by deciding which stage of the pipeline your change belongs to, then check the corresponding demo or component for patterns you can copy.
A quick map of the most relevant folders:
src/- Core library code: parsers, evaluators, layout logic, and components.
webcola-demo/- Interactive HTML/TSX demos showing complete pipeline usage.
docs/- Deeper explanations of features, algorithms, and optimization work.
tests/- Vitest-based unit/integration tests.
-
Install dependencies
npm install
-
Run a local file server for demos
npm run serve
Then open
http://localhost:8080/webcola-demo/and choose a demo HTML file. -
Build or watch
npm run build # or npm run dev -
Run tests
npm run test:run
When extending the system, use this checklist:
- Decide the stage
- Does it change input parsing? Evaluator behavior? Layout spec parsing? Rendering?
- Add or update a demo
- Most behavior is easiest to validate visually. Add a demo or extend an existing one.
- Document the behavior
- Add a short note in
docs/so the next developer knows where your change lives.
- Add a short note in
- Add tests
- For pure logic changes, add unit tests under
tests/.
- For pure logic changes, add unit tests under
- CnD: “Constraint & Directive” language for layout rules.
- DataInstance: Normalized, canonical graph data structure.
- Evaluator: Resolves selectors (queries) against a data instance.
- LayoutSpec: Parsed CnD specification.
- LayoutInstance: The runtime pipeline state (spec + evaluator) used to generate layouts.
Projections are a pre-layout data transformation, not a layout directive. They operate on an IDataInstance and produce a new IDataInstance with projected types/atoms removed and relation arities collapsed. This is a form of metaprogramming: projections rewrite the datum itself before any layout logic runs.
Directives (color, size, alignment, etc.) describe how to display a given data instance. Projections change what data is displayed — they are a semantic transformation on the model. By decoupling projections from the layout engine:
- The layout engine has a simpler API:
generateLayout(instance)takes one argument - Projected instances can be reused for export, analysis, or rendering without going through the layout engine
- The projection logic is testable in isolation
IDataInstance ──► applyProjectionTransform() ──► projected IDataInstance ──► LayoutInstance.generateLayout()
▲
Projection[]
selections: Record<string, string>
import { applyProjectionTransform, Projection } from 'spytial-core';
const projections: Projection[] = [
{ sig: 'State', orderBy: 'next' }
];
const selections: Record<string, string> = {}; // type → chosen atom
const result = applyProjectionTransform(
dataInstance,
projections,
selections,
{
// Optional: evaluator for orderBy sorting
evaluateOrderBy: (sel) => evaluator.evaluate(sel).selectedTwoples(),
// Optional: error handler
onOrderByError: (sel, err) => console.warn(`orderBy error for ${sel}:`, err),
}
);
// result.instance — the projected IDataInstance (pass to layout)
// result.choices — ProjectionChoice[] for populating UI controls| Type | Description |
|---|---|
Projection |
{ sig: string; orderBy?: string } — which type to project over |
ProjectionChoice |
{ type, projectedAtom, atoms } — UI dropdown metadata |
ProjectionTransformOptions |
{ evaluateOrderBy?, onOrderByError? } — optional callbacks |
ProjectionTransformResult |
{ instance, choices } — transform output |
- Without
orderBy: atoms are sorted lexicographically by ID - With
orderBy+evaluateOrderBycallback: atoms are topologically sorted based on the binary relation - Cycles in the relation are broken by choosing the lexicographically smallest atom
- Atoms not in the relation are interleaved when their in-degree reaches 0
The evaluateOrderBy callback inside applyProjectionTransform is invoked before instance.applyProjections(). This means it evaluates against the original, un-projected data instance. This is intentional:
- The ordering relation (e.g.,
next: Time → Time) involves atoms of the projected type. After projection removes all but one atom, those relation tuples are gone. - Layout selectors that run later (inside
generateLayout) still work: atom IDs returned by the evaluator that don't exist in the projected graph are silently filtered out during node matching.
Consequence for callers:
evaluator.initialize({ sourceData: originalInstance }) // ① evaluator sees full data
applyProjectionTransform(originalInstance, ...) // ② orderBy evaluated here
layoutInstance.generateLayout(projectedInstance) // ③ layout on projected data
Do not re-initialise the evaluator with the projected instance between steps ② and ③ — the evaluator's role at step ③ is to resolve layout selectors, and IDs that were projected away are harmlessly ignored.
In demos and applications, projections are specified separately from the CnD layout spec:
// 1. Initialise evaluator with the original (un-projected) instance
evaluator.initialize({ sourceData: originalInstance });
// 2. Define projections
const projections = [{ sig: 'Time', orderBy: 'next' }];
const selections = {};
// 3. Apply projection (pre-layout step) — orderBy evaluated here
const projResult = applyProjectionTransform(originalInstance, projections, selections, {
evaluateOrderBy: (sel) => evaluator.evaluate(sel).selectedTwoples(),
});
// 4. Generate layout on the projected instance
const layoutResult = layoutInstance.generateLayout(projResult.instance);
// 5. Populate projection controls with choices
updateProjectionControls(projResult.choices);