Digital twin simulation engine. Rolls trace spans into a point-in-time snapshot of which nodes are active, groups spans into user flows, synthesizes "what-if" trace spans for planned runs, and overlays active-node state back onto the graph for visualization.
- Snapshot computation.
computeDigitalTwinSnapshotderives active nodes and user flows from a graph + trace spans over a rolling time window. - User-flow grouping.
buildUserFlowsbuckets spans bytraceId, sorts each bucket chronologically, and computes worst-case status. - Span synthesis.
buildSimulationSpansproduces a singleUserFlow-shaped sequence of synthetic trace spans for an ordered list of node ids. - Active-node overlay.
overlayActiveNodesmarks a graph's active nodes withtraceState.status = "success"(without downgrading an existingerror). - Store-backed API helpers.
getDigitalTwin(read) andsimulateAction(write) wrap the engine withcodeflow-storereads and observability merges. - CLI.
codeflow-dtwinwith four subcommands.
| Subpath | Module |
|---|---|
. |
buildUserFlows, computeDigitalTwinSnapshot, buildSimulationSpans, overlayActiveNodes, idleTraceState, and the type set. |
./simulate |
The simulateAction handler plus the SimulateRequest / SimulateResponse types. |
The CLI binary codeflow-dtwin ships under bin/.
import {
buildUserFlows,
computeDigitalTwinSnapshot,
buildSimulationSpans,
overlayActiveNodes,
idleTraceState,
} from '@abhinav2203/codeflow-dtwin';
import type {
BlueprintGraph,
TraceSpan,
UserFlow,
DigitalTwinSnapshot,
NodeTraceState,
SimulationResult,
} from '@abhinav2203/codeflow-dtwin';★ Insight ─────────────────────────────────────
The engine is a pure compute layer. The four core functions take a graph and spans in, return data out, and never touch the filesystem. I/O lives in api/route.ts (store reads + observability merges) and the CLI (file reads). The split keeps the math testable in isolation.
─────────────────────────────────────────────────
type OutputProvenance =
| "deterministic" | "ai" | "heuristic" | "simulated" | "observed";
interface NodeTraceState {
status: "idle" | "success" | "warning" | "error";
count: number;
errors: number;
totalDurationMs: number;
lastSpanIds: string[];
}
interface DigitalTwinSnapshot {
projectName: string;
computedAt: string; // ISO timestamp
maturity: "production" | "preview" | "experimental" | "scaffold";
activeNodeIds: string[];
flows: UserFlow[];
observedSpanCount: number;
simulatedSpanCount: number;
observedFlowCount: number;
simulatedFlowCount: number;
activeWindowSecs: number;
}
interface SimulationConfig {
iterations: number;
activeWindowSecs?: number;
}
interface SimulationResult {
snapshot: DigitalTwinSnapshot;
spans: TraceSpan[];
flows: UserFlow[];
}UserFlow and TraceSpan come from codeflow-core. UserFlow carries a worst-case status, summed totalDurationMs, and a provenance resolved by majority vote over its member spans.
Walks every span and keeps its resolved blueprint node id if the span's timestamp falls within activeWindowSecs * 1000 ms of Date.now(). Spans without a timestamp count as always active (an intentional fallback for stored snapshots and tests). The active set deduplicates in first-seen order.
The returned snapshot also tallies observed vs simulated flow counts and the activeWindowSecs used for the computation, so the consumer can reconstruct what window produced these numbers.
Buckets spans by traceId. Within each bucket it sorts timestamped spans first (insertion order for the rest), walks the sequence, dedupes nodeIds in traversal order, and picks a worst-case status from the priority success=1, warning=2, error=3. provenance resolves by majority vote in the order observed > simulated > deterministic > heuristic > ai. The final list sorts most-recent first by startedAt.
Synthesizes a single trace for a planned run. Every span gets a shared traceId = "sim-" + Date.now(), status: "success", durationMs: 1, provenance: "simulated", and a timestamp that steps +10 ms per node. Unknown node ids drop out silently.
Returns a shallow-cloned graph whose matching nodes get traceState = { ...(existing or default), status: "success" }. An existing error or warning status survives; the overlay never downgrades a real signal. Nodes outside the active set stay untouched.
★ Insight ─────────────────────────────────────
The "never downgrade" rule on overlayActiveNodes is the load-bearing detail. The digital twin reflects what the system says is active, but it does not paper over failure. A node that errored in the last window stays red even if it's in the active set.
─────────────────────────────────────────────────
Factory for an empty NodeTraceState. Use it when initializing a node that has not yet been touched by a span.
Parallel-loads loadObservabilitySnapshot(projectName) and loadLatestSession(projectName) from codeflow-store, then returns:
interface DigitalTwinResponse {
snapshot: DigitalTwinSnapshot | null;
graph: BlueprintGraph | null; // return type of overlayActiveNodes
activeWindowSecs: number;
}If no session exists, snapshot and graph come back null. Mount this in a Next.js app/api/dtwin/route.ts as a GET handler that reads ?projectName=...&window=... from the query string.
Loads the latest session, generates synthetic spans, persists them via mergeObservabilitySnapshot({ projectName, spans, logs: [], graph }), and returns:
interface SimulateResponse extends SimulationResult {
latestSpans: TraceSpan[]; // last 100 merged spans
latestLogs: unknown[];
}Throws "No session found for project: <name>" if the project has no session on disk.
codeflow-dtwin simulate <blueprint.json> [trace-data.json]
codeflow-dtwin snapshot <blueprint.json> [--trace-latest]
codeflow-dtwin active-nodes <blueprint.json> [trace-data.json]
codeflow-dtwin build-flows <blueprint.json> <trace-data.json>
The CLI is built on node:util's parseArgs. Global flags: --help, --trace-latest, --iterations <n>, --json.
simulate reads a blueprint, picks the first 5 nodes, and calls buildSimulationSpans + computeDigitalTwinSnapshot with the default 60-second window. snapshot runs the snapshot with empty spans. active-nodes prints the comma-separated active node ids. build-flows prints one line per flow with span count and status.
| Constant | Value | Why |
|---|---|---|
Default activeWindowSecs |
60 | Rolling window for the "active" calculation. |
worstStatus priority |
success=1, warning=2, error=3 |
A flow's status is the worst of its spans. |
| Simulation span stride | 10 ms | Spans within a synthesized flow step +10 ms each. |
Simulation durationMs |
1 | Synthetic spans run "instantly". |
| Provenance priority | observed > simulated > deterministic > heuristic > ai |
Order in which buildUserFlows picks the majority. |
Like codeflow-execution, the build script uses tsc --build and scripts/wrap-cli.mjs re-injects the shebang into dist/bin/cli.js (TSC strips it from source). The published output mirrors src/ exactly so subpath exports resolve to the right files.
codeflow-dtwin/
├── package.json
├── tsconfig.json, tsconfig.build.json
├── vitest.config.ts
├── scripts/wrap-cli.mjs
└── src/
├── index.ts barrel
├── types.ts local types + idleTraceState / emptyContract factories
├── digital-twin.ts engine: buildUserFlows, computeDigitalTwinSnapshot, buildSimulationSpans, overlayActiveNodes
├── digital-twin.test.ts vitest unit tests
├── api/
│ ├── route.ts getDigitalTwin() + DigitalTwinResponse
│ └── simulate/
│ └── route.ts simulateAction() + SimulateRequest/Response
└── bin/
└── cli.ts simulate | snapshot | active-nodes | build-flows
- The CLI's
simulatecommand always picks the first 5 nodes. For targeted runs usesimulateActionfrom./simulate. - The default 60-second window is hard-coded at the call site, not derived from project config. Pass a custom value at the API boundary.
- The
--iterationsglobal flag is parsed but not used by any current subcommand. It exists as a forward-looking knob. codeflow-executionis declared as a dependency but not yet imported anywhere insrc/. The seam is reserved for future expansion.