Class RecordingCorrelationAnalyzer

Namespace
Kuestenlogik.Bowire.Recordings.Correlation
Assembly
Kuestenlogik.Bowire.Recordings.dll

Turns a recording into a correlated timeline (#539) — the single canonical implementation behind both the workbench's Timeline tab (via POST /api/recordings/correlate) and bowire recording correlate.

A recording has no trace id: nothing in the .bwr format carries one, and no capture path writes one. So the analyzer resolves a signal in three tiers — a correlation header on step.metadata, otherwise a shared id-shaped JSON leaf inferred from the payloads, otherwise nothing, in which case the whole recording is treated as one transaction and the result is a plain protocol-lane time chart.

One key is not always enough (#545): a transaction that renames its identifier on the way across the landscape lights only the lanes that speak the chosen key. So a step the key missed gets a second and last edge — it joins the transaction when it shares a distinctive id-shaped value with a step the key did match, and the resulting RecordingCorrelationLink always names that value. See Kuestenlogik.Bowire.Recordings.Correlation.RecordingCorrelationAnalyzer.JoinThroughBridges(System.Collections.Generic.List{Kuestenlogik.Bowire.Mocking.BowireRecordingStep},System.Int64[],System.Collections.Generic.List{Kuestenlogik.Bowire.Recordings.Correlation.RecordingCorrelationEvent},Kuestenlogik.Bowire.Recordings.Correlation.RecordingCorrelationKey,System.Collections.Generic.List{System.String}) for the admissibility rule and what it turns down.

Pure and stateless by contract: no cache, no ring buffer, no injected dependency. Two calls with the same inputs produce the same output, which is what lets the CLI and the endpoint agree.

public static class RecordingCorrelationAnalyzer
Inheritance
RecordingCorrelationAnalyzer
Inherited Members

Fields

HeaderScoreBase

Score floor for a header candidate. Any correlation header beats every inferred field candidate, because a header is an explicit statement by the producer while a shared id is an inference. Field scores are protocols * 1000 + steps, which cannot realistically approach this, so the ordering is total without clamping at int.MaxValue (which would make several header candidates indistinguishable from each other).

public const int HeaderScoreBase = 1000000

Field Value

int

LaneFrameWarningThreshold

Frames per lane past which the timeline stops being readable and the renderer thins its ticks. Reported as a warning rather than silently truncating the model — the CLI still prints everything.

public const int LaneFrameWarningThreshold = 200

Field Value

int

MaxBridgeCarrierShare

The largest share of a recording's steps a value may appear on and still be treated as identifying ONE transaction.

public const double MaxBridgeCarrierShare = 0.6

Field Value

double

Remarks

Length alone is the wrong measure of "distinctive", and dangerously so: a session, tenant, customer or node id is long, high-entropy and sits on the same field name at both ends — the exact profile the strength score rewards most. A GUID session id would therefore be the single best bridge this analyzer can find, and it would fuse every request made in that session into one "transaction". A password change would become part of an order.

What separates a transaction key from a context key is not how it looks but how far it spreads: the transaction id appears on the steps belonging to that transaction, the session id appears on nearly everything. A value carried by most of the recording is describing the capture, not a transaction inside it.

Deliberately generous — this rejects the pathological case without second-guessing short recordings, where a legitimate id can easily touch half the steps.

MaxJoinDepth

Edges the join may walk, counting the seed match as the first. Fixed, not configurable: an unbounded walk over an id-rich recording relates everything to everything, which is worse than no join at all. Concretely — a step the key matched directly may bridge one hop further, and a step reached through a bridge never bridges onward.

public const int MaxJoinDepth = 2

Field Value

int

MinBridgeValueLength

How improbable a value has to be before it may bridge two steps (#545). A bridge is an inference, not the operator's choice, and unlike the seed key it gets no corroboration from a matching field name — so the value has to carry its own weight. Anything shorter than this has too few possible values for a collision to be surprising: 1, 42, true and OK are not evidence, and a harbour capture where portCallId, craneId and a dock number are all 1 is precisely the case that must not join.

public const int MinBridgeValueLength = 6

Field Value

int

Methods

Analyze(BowireRecording, RecordingCorrelationKey?)

Place every step (and every streamed frame) of the recording on a shared time axis and verdict each one against the resolved key.

public static RecordingCorrelationTimeline Analyze(BowireRecording recording, RecordingCorrelationKey? key = null)

Parameters

recording BowireRecording

The recording to analyse.

key RecordingCorrelationKey

An explicit key from the operator or from CI. When null, the recording's persisted Correlation wins, then the top-scoring suggestion, then nothing.

Returns

RecordingCorrelationTimeline

ResolveSource(string?)

Decide where a key came from when the caller did not say. A name that reads as one of the recognised correlation headers is a header key; anything else is a payload leaf.

public static string ResolveSource(string? name)

Parameters

name string

Returns

string

Suggest(BowireRecording)

Every key this recording would accept, best first. Empty when no value is shared by at least two steps.

public static IReadOnlyList<RecordingCorrelationCandidate> Suggest(BowireRecording recording)

Parameters

recording BowireRecording

Returns

IReadOnlyList<RecordingCorrelationCandidate>