Skip to content

Diagram Fidelity

Diagram fidelity measures whether the analyzer can represent a program without unresolved or ambiguous nodes. A score helps you track coverage. The exact field gives CI a strict pass or fail result.

import {
analysis,
computeDiagramFidelity,
formatDiagramFidelity,
} from "effect-analyzer"
import { Effect } from "effect"
const ir = await Effect.runPromise(
analysis.file("./src/transfer.ts").single,
)
const report = computeDiagramFidelity(ir)
console.log(formatDiagramFidelity(report))

DiagramFidelityReport contains:

Field Meaning
exact true when the report contains no issues
score Percentage of IR nodes without a fidelity penalty
totalNodes Number of nodes in the deep IR tree
exactRuntimeNodes Span-backed nodes with a unique runtime identity
staticOnlyNodes Nodes that have no runtime span identity
ambiguousRuntimeNodes Nodes that share a span path
unresolvedNodes Unknown, opaque, or dynamic-span nodes
issues Located findings with a suggested source change
Issue Cause Source change
unknown-node The analyzer cannot resolve an expression Use a supported Effect v4 operation or extract a named Effect.fn
opaque-node The analyzer treats a boundary as opaque Extract diagram-relevant work or add an explicit span
dynamic-span-name Code computes the Effect.withSpan name Use a string literal and store dynamic values in span attributes
duplicate-span-path Two nodes produce the same nested span path Give sibling spans distinct names
Terminal window
npx effect-analyze ./src --assert-diagram-fidelity

The command checks one file or each analyzed program in a directory. It prints the findings and exits with code 1 when any report has exact: false.

The analyzer joins runtime spans to static nodes by their nested path. These two spans form the path transfer > debit:

const program = Effect.gen(function* () {
yield* debit
}).pipe(
Effect.withSpan("debit"),
Effect.withSpan("transfer"),
)

Keep names literal and give sibling spans different names. Put request IDs, account IDs, and other runtime values in span attributes.

A captured trace usually starts above the analyzed program, in an HTTP handler or a job runner the analyzed file never mentions. The analyzer matches the full path first, then the longest path suffix that identifies a single node, so those outer spans still resolve everything nested beneath them.

import {
renderMermaidWithRuntimeTrace,
traceFromEffectSpans,
} from "effect-analyzer"
const trace = traceFromEffectSpans(effectSpans)
const overlay = renderMermaidWithRuntimeTrace(ir, trace)
import {
renderMermaidWithRuntimeTrace,
traceFromOpenTelemetry,
} from "effect-analyzer"
const trace = traceFromOpenTelemetry(readableSpans)
const overlay = renderMermaidWithRuntimeTrace(ir, trace)

Tracing tools that export a trace as a nested tree of spans go through traceFromSpanTree(). Each node carries a spanId, a name, a status of ok, error or unset, an optional durationMs, an optional running flag, and its children:

import {
renderMermaidWithRuntimeTrace,
traceFromSpanTree,
} from "effect-analyzer"
const trace = traceFromSpanTree(JSON.parse(exportedTrace))
const overlay = renderMermaidWithRuntimeTrace(ir, trace)

The CLI reads that same shape from a file:

Terminal window
npx effect-analyze ./src/transfer.ts --format mermaid --runtime-trace ./trace.json

The diagram is followed by a comment line counting how the spans matched, so a reviewer or an agent can weigh the colors:

%% runtime overlay: 5 matched, 1 matched by suffix, 0 unmatched, 0 ambiguous

RuntimeOverlayResult returns the Mermaid source and four span-ID lists:

Field Meaning
matchedSpanIds The full trace path identifies one static node
suffixMatchedSpanIds A path suffix identifies one static node, after dropping outer spans
unmatchedSpanIds The trace path identifies no static node
ambiguousSpanIds The trace path identifies more than one static node

The renderer colors successful spans green, failed spans red, and running spans amber.