diff --git a/src/Mimir.Runtime/MimirRuntime.cs b/src/Mimir.Runtime/MimirRuntime.cs index 2a93cb0..d0e4ff3 100644 --- a/src/Mimir.Runtime/MimirRuntime.cs +++ b/src/Mimir.Runtime/MimirRuntime.cs @@ -37,6 +37,7 @@ public sealed class MimirRuntime : IAquariumRuntime private const int DefaultSpectrumSplineBandStride = 1; private const int DefaultSpectrumSplineSubdivisions = 2; private readonly MimirSynchronizationHub synchronization; + private readonly MimirFensalirFieldLowering fieldLowering; private readonly MimirAudioSpectrumAnalyzer spectrumAnalyzer; private readonly IReadOnlyList sourceFactories; private readonly AquariumUiDocument ui; @@ -105,6 +106,9 @@ public sealed class MimirRuntime : IAquariumRuntime { Options = options; synchronization = new MimirSynchronizationHub(settings); + fieldLowering = new MimirFensalirFieldLowering(new MimirFensalirLoweringOptions( + settings.BufferDuration.TotalSeconds, + settings.BufferDuration.TotalSeconds)); spectrumAnalyzer = new MimirAudioSpectrumAnalyzer(ParseSpectrumFftSize(), ParseSpectrumBandCount()); this.sourceFactories = sourceFactories.ToArray(); audioSyncSettings = settings.Audio; @@ -140,6 +144,7 @@ public sealed class MimirRuntime : IAquariumRuntime private AquariumFrame CreateFrame() { var channelCount = Math.Max(1, lastAudioSpectra.Count); + var fieldEvidenceFrame = BuildFieldEvidenceFrame(); var splineFrame = BuildSpectrumSplineFrame(); var windowCount = Math.Max(1, spectrumHistory.Count); var (splineMin, splineMax) = SpectrumSplineAabb(channelCount, windowCount); @@ -165,11 +170,56 @@ public sealed class MimirRuntime : IAquariumRuntime { TraceHeightFieldSurface = false, UseStarfieldBackground = false, + FieldEvidenceFrame = fieldEvidenceFrame, BufferFieldFrame = AquariumBufferFieldFrame.Empty, SplineFrame = splineFrame, }); } + private AquariumFieldEvidenceFrame BuildFieldEvidenceFrame() + { + var buffers = synchronization.Buffers.Buffers; + var windows = new List(buffers.Count); + var observations = new List(buffers.Count); + MimirFensalirBridgeMapper.MapWindows(buffers, windows); + MimirFensalirBridgeMapper.MapLatestObservations(buffers, observations); + + var constraints = new List(synchronization.AudioSynchronizationStates.Count); + MimirFensalirBridgeMapper.MapCalibrationConstraints(synchronization.AudioSynchronizationStates, constraints); + + var observationsByWindow = observations.ToDictionary(observation => observation.WindowId, StringComparer.Ordinal); + var intents = new List(observations.Count); + foreach (var window in windows) + { + if (!observationsByWindow.TryGetValue(window.WindowId, out var observation)) + { + continue; + } + + intents.Add(BuildSurfaceIntent(window, observation)); + } + + return fieldLowering.BuildFieldEvidenceFrame(windows, observations, constraints, intents); + } + + private static MimirSurfaceIntent BuildSurfaceIntent(MimirRollingStreamWindow window, MimirObservation observation) + { + if (window.SourceKind == MimirStreamKind.Audio) + { + return new MimirSurfaceIntent( + IntentKey: $"{window.WindowId}:spectrum", + SourceObservationKeys: [observation.ObservationKey], + Domain: MimirSurfaceDomain.AudioSpectrum, + Axes: new MimirSurfaceAxes("frequency", "amplitude", "time-age", "stream"), + SupportPolicy: new MimirSurfaceSupportPolicy("rolling-spectrum", 0.0, window.Duration.TotalSeconds), + MaterialGraph: new MimirSurfaceMaterialIntent("spectrum-evidence", "source-evidence", observation.Confidence), + UpdateBudget: new MimirSurfaceUpdateBudget(0.0, 1, Math.Max(0, window.Payload.ByteLength)), + Purpose: MimirSurfaceIntentPurpose.Debug); + } + + return MimirFensalirBridgeMapper.MapDefaultSurfaceIntent(window, observation); + } + private static Vector3 SpectrumCameraPosition( int spectrumCount, int windowCount, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index eff55d0..38c6563 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -137,3 +137,4 @@ {"ts": "2026-05-26T14:04:08+01:00", "type": "implementation", "status": "ok", "note": "Phase 1 Mimir bridge skeleton is live in src/Mimir.Runtime/Synchronization/MimirFensalirBridgeContracts.cs. It defines pure DTOs for rolling stream windows, observations, calibration constraints, and surface intent plus mappers from rolling buffers and audio sync state/report surfaces. No rendering, device reads, timers, JSON/base64 hot paths, or backend-specific visual choices were added. dotnet build Mimir.slnx passed with one pre-existing EveRelay CA2024 warning."} {"ts": "2026-05-26T14:39:17+01:00", "type": "correction", "status": "ok", "note": "Corrected the Phase 1 field evidence boundary: Mimir no longer emits AquariumFieldBackendPacket rows with Backend=Unknown for surface intents. Surface intent now lowers only to field domains, claims, and candidates; backend packet selection remains Fensalir authority. dotnet build Mimir.slnx --no-restore passed with 0 warnings and 0 errors after the cut."} {"ts": "2026-05-26T14:44:02+01:00", "type": "implementation", "status": "ok", "note": "Added BufferSmoke --fensalir-field-evidence-smoke as a hardware-free contract receipt. It builds a synthetic rolling audio window, observation, bioacoustic calibration constraint, and audio-spectrum surface intent, lowers them through MimirFensalirFieldLowering, validates with AquariumFieldEvidenceValidator, and verifies Fensalir receives three pending lowering requests and zero backend packets. Smoke passed: domains=4 claims=3 candidates=3 packets=0 requests=3 errors=False."} +{"ts": "2026-05-26T14:50:33+01:00", "type": "implementation", "status": "ok", "note": "MimirRuntime now publishes AquariumFieldEvidenceFrame on every AquariumSceneState from live rolling buffers, latest observations, audio sync calibration constraints, and per-source surface intent. Existing direct spectrum splines remain diagnostic; Fensalir receives backend-neutral domains/claims/candidates through FieldEvidenceFrame. dotnet build Mimir.slnx --no-restore passed with the pre-existing EveRelay CA2024 warning, and --fensalir-field-evidence-smoke still passes."}