Skip to content

feat: add logging session CSV export to Daqifi.Core#167

Merged
tylerkron merged 5 commits intomainfrom
claude/infallible-nobel-6fff72
Apr 25, 2026
Merged

feat: add logging session CSV export to Daqifi.Core#167
tylerkron merged 5 commits intomainfrom
claude/infallible-nobel-6fff72

Conversation

@tylerkron
Copy link
Copy Markdown
Contributor

Summary

Closes #166.

Moves the streaming CSV export pipeline out of daqifi-desktop and into Daqifi.Core so any consumer (desktop, CLI, future UIs) can reuse it without pulling in EF Core or WPF.

  • ILoggingSessionSource — async-enumerable abstraction over session data; hides storage (EF/SQLite) from the exporter
  • ChannelDescriptor — identifies a channel by device name, serial number, channel name, and type
  • SampleRow — flat record: (TimestampTicks, ChannelKey, Value)
  • CsvExportOptionsDelimiter, UseRelativeTime, AverageWindow
  • LoggingSessionCsvExporter.ExportAsync — pure logic ported from OptimizedLoggingSessionExporter:
    • Timestamp-bucketing (multiple channels at same tick → one row)
    • Absolute (DateTime.ToString("O")) vs. relative (F3 seconds) timestamps
    • Invalid-tick fallback (INVALID({ticks}))
    • All-samples and N-sample rolling-average modes
    • IProgress<int> (0–100) and CancellationToken support

Test plan

  • 29 xUnit tests in Daqifi.Core.Tests/Logging/Export/ on net8.0 and net9.0
  • Covers: absolute/relative time, averaged vs. all-samples, mixed-channel sessions with gaps, invalid ticks, custom delimiter, duplicate channel at same timestamp (last-value-wins), progress reporting, cancellation
  • No Microsoft.EntityFrameworkCore.*, System.Windows.*, or Microsoft.Win32.* references in new code
  • Full suite: 847 passed, 0 failed, 0 regressions

🤖 Generated with Claude Code

Adds Daqifi.Core.Logging.Export namespace with a storage-agnostic CSV
export pipeline: ILoggingSessionSource abstracts the data source (hiding
EF/SQLite), LoggingSessionCsvExporter contains the pure formatting logic
ported from daqifi-desktop's OptimizedLoggingSessionExporter.

Supports absolute/relative timestamps, configurable delimiter, all-samples
and rolling-average modes, IProgress<int> reporting, and cancellation.
29 xUnit tests on net8.0 and net9.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tylerkron tylerkron requested a review from a team as a code owner April 18, 2026 21:02
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add streaming CSV exporter for logging sessions to Daqifi.Core

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Adds storage-agnostic CSV export pipeline to Daqifi.Core
  - ILoggingSessionSource abstracts data source (EF/SQLite/in-memory)
  - LoggingSessionCsvExporter contains pure formatting logic
• Supports absolute/relative timestamps, custom delimiters, rolling-average modes
• Includes 29 comprehensive xUnit tests covering all export scenarios
• No EF Core, WPF, or Windows dependencies in new code
Diagram
flowchart LR
  Source["ILoggingSessionSource<br/>(Data abstraction)"] -- "StreamSamples" --> Exporter["LoggingSessionCsvExporter<br/>(Pure formatting logic)"]
  Options["CsvExportOptions<br/>(Delimiter, Time mode, Averaging)"] --> Exporter
  Exporter -- "Writes CSV" --> Writer["TextWriter<br/>(CSV output)"]
  Exporter -- "Reports progress" --> Progress["IProgress&lt;int&gt;<br/>(0-100)"]
Loading

Grey Divider

File Changes

1. src/Daqifi.Core/Logging/Export/ILoggingSessionSource.cs ✨ Enhancement +30/-0

Storage-agnostic session data source interface

• Defines async-enumerable abstraction over session data
• Hides storage implementation (EF Core, SQLite, in-memory)
• Provides channel list, sample count, and sample stream

src/Daqifi.Core/Logging/Export/ILoggingSessionSource.cs


2. src/Daqifi.Core/Logging/Export/ChannelDescriptor.cs ✨ Enhancement +19/-0

Channel identification record with composite key

• Record type identifying channels by device name, serial number, channel name, and type
• Provides composite Key property in format {DeviceName}:{DeviceSerialNo}:{ChannelName}

src/Daqifi.Core/Logging/Export/ChannelDescriptor.cs


3. src/Daqifi.Core/Logging/Export/SampleRow.cs ✨ Enhancement +15/-0

Single sample data point record

• Record representing single data point with timestamp ticks, channel key, and numeric value
• Enforces ascending timestamp ordering requirement

src/Daqifi.Core/Logging/Export/SampleRow.cs


View more (4)
4. src/Daqifi.Core/Logging/Export/CsvExportOptions.cs ✨ Enhancement +26/-0

CSV export formatting options configuration

• Configuration class for CSV export formatting
• Supports custom delimiter, relative/absolute time modes, rolling-average window size

src/Daqifi.Core/Logging/Export/CsvExportOptions.cs


5. src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs ✨ Enhancement +207/-0

Core CSV export logic with timestamp bucketing

• Main exporter class with pure formatting logic ported from daqifi-desktop
• Implements timestamp bucketing (multiple channels at same tick → one row)
• Supports absolute ISO 8601 and relative time formatting with invalid-tick fallback
• Implements all-samples and N-sample rolling-average modes
• Provides IProgress<int> (0–100) and CancellationToken support

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs


6. src/Daqifi.Core.Tests/Logging/Export/InMemoryLoggingSessionSource.cs 🧪 Tests +37/-0

In-memory test implementation of session source

• Test helper implementing ILoggingSessionSource with in-memory data
• Stores channels and samples, supports cancellation in stream
• Automatically orders samples by timestamp

src/Daqifi.Core.Tests/Logging/Export/InMemoryLoggingSessionSource.cs


7. src/Daqifi.Core.Tests/Logging/Export/LoggingSessionCsvExporterTests.cs 🧪 Tests +464/-0

Comprehensive test suite for CSV exporter

• 29 xUnit tests covering all export scenarios
• Tests header formatting, absolute/relative timestamps, invalid ticks, timestamp bucketing
• Tests mixed-channel sessions with gaps, custom delimiters, averaging modes
• Tests progress reporting, cancellation, and channel key formatting
• Verifies no EF Core or Windows dependencies

src/Daqifi.Core.Tests/Logging/Export/LoggingSessionCsvExporterTests.cs


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 18, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (3)

Grey Divider


Action required

1. Locale-dependent number formatting🐞 Bug ≡ Correctness
Description
LoggingSessionCsvExporter uses current-culture ToString("F3")/ToString("G"), so in locales with
comma decimal separators it can emit extra commas inside numeric fields and corrupt comma-delimited
CSV column structure.
Code

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[R189-196]

+    private static string FormatTimestamp(long ticks, long firstTicks, bool useRelativeTime)
+    {
+        if (useRelativeTime)
+            return ((ticks - firstTicks) / (double)TimeSpan.TicksPerSecond).ToString("F3");
+
+        return (ticks > 0 && ticks <= DateTime.MaxValue.Ticks)
+            ? new DateTime(ticks).ToString("O")
+            : $"INVALID({ticks})";
Evidence
Relative timestamps and sample values are formatted without CultureInfo, which is culture-sensitive;
the repo elsewhere consistently uses CultureInfo.InvariantCulture for numeric text
parsing/formatting, indicating exports should be culture-stable.

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[189-196]
src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[113-118]
src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[161-166]
src/Daqifi.Core/Device/SdCard/SdCardCsvFileParser.cs[352-363]
src/Daqifi.Core.Tests/Device/SdCard/SdCardTestTextFileBuilders.cs[23-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`LoggingSessionCsvExporter` formats numbers (relative seconds and values) using the current thread culture. In locales that use `,` as a decimal separator, comma-delimited CSV becomes structurally invalid because numeric cells can contain commas.

### Issue Context
This affects:
- Relative timestamp formatting (`F3`)
- Sample value formatting in both all-samples and averaged modes (`G`)

The rest of the codebase commonly uses `CultureInfo.InvariantCulture` for numeric text.

### Fix Focus Areas
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[189-196]
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[113-118]
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[161-166]

### Suggested change
- Use `ToString("F3", CultureInfo.InvariantCulture)` for relative seconds.
- Use `ToString("G", CultureInfo.InvariantCulture)` for values/averages.
- Optionally use `new DateTime(ticks, DateTimeKind.Utc).ToString("O", CultureInfo.InvariantCulture)` (or at least pass invariant culture) to keep formatting consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Averaging drops trailing samples 🐞 Bug ≡ Correctness
Description
ExportAveragedAsync never flushes a final partial window, so any trailing samples when sampleCount %
AverageWindow != 0 are silently omitted from the CSV.
Code

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[R142-183]

+        await foreach (var sample in source.StreamSamples(cancellationToken))
+        {
+            firstTicks ??= sample.TimestampTicks;
+            lastTick = sample.TimestampTicks;
+
+            if (totals.ContainsKey(sample.ChannelKey))
+            {
+                totals[sample.ChannelKey] += sample.Value;
+                counts[sample.ChannelKey]++;
+            }
+
+            windowCount++;
+            processed++;
+
+            if (windowCount >= window)
+            {
+                sb.Clear();
+                sb.Append(FormatTimestamp(lastTick, firstTicks.Value, options.UseRelativeTime));
+
+                foreach (var key in channelKeys)
+                {
+                    sb.Append(options.Delimiter);
+                    if (counts[key] > 0)
+                        sb.Append((totals[key] / counts[key]).ToString("G"));
+                }
+
+                sb.AppendLine();
+                await writer.WriteAsync(sb.ToString());
+
+                foreach (var key in channelKeys)
+                {
+                    totals[key] = 0.0;
+                    counts[key] = 0;
+                }
+                windowCount = 0;
+
+                ReportProgress(progress, processed, totalSamples);
+            }
+        }
+
+        progress?.Report(100);
+    }
Evidence
Averaged rows are only written inside the if (windowCount >= window) block; after the stream ends
there is no post-loop write when windowCount > 0, so remaining samples are dropped.

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[142-183]
src/Daqifi.Core/Logging/Export/CsvExportOptions.cs[20-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ExportAveragedAsync` only writes output when it has accumulated a full `AverageWindow` samples, and it does not write the last partial window at end-of-stream. This causes silent data loss for trailing samples.

### Issue Context
If there are 5 samples and `AverageWindow=2`, only 4 samples are represented (2 rows) and the last sample is dropped.

### Fix Focus Areas
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[142-183]

### Suggested fix
After the `await foreach` loop:
- If `windowCount > 0`, write one final averaged row using the accumulated `totals`/`counts` and `lastTick`.
- Optionally `ReportProgress(...)` after flushing so progress reflects the last samples.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Relative time ignores SessionStart 📎 Requirement gap ≡ Correctness
Description
When UseRelativeTime is enabled, the exporter computes offsets from the first sample tick instead
of ILoggingSessionSource.SessionStart, so exports can be incorrect when session start differs from
the first sample timestamp.
Code

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[R68-76]

+        long? firstTicks = null;
+        long? currentTick = null;
+        var bucket = new List<SampleRow>();
+        var processed = 0;
+
+        await foreach (var sample in source.StreamSamples(cancellationToken))
+        {
+            firstTicks ??= sample.TimestampTicks;
+
Evidence
Compliance requires relative timestamps to be seconds from session start. The interface exposes
SessionStart, but the exporter derives firstTicks from the first streamed sample and uses it for
relative-time formatting.

Support export options parity with desktop (timestamps, delimiter, averaging)
src/Daqifi.Core/Logging/Export/ILoggingSessionSource.cs[9-13]
src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[73-76]
src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[189-193]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UseRelativeTime` offsets are computed from the first sample tick instead of the session start (`ILoggingSessionSource.SessionStart`). This can produce incorrect relative timestamps when there is a gap between session start and the first sample.

## Issue Context
The compliance requirement for relative timestamps is "seconds elapsed since session start".

## Fix Focus Areas
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[68-76]
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[137-160]
- src/Daqifi.Core/Logging/Export/ILoggingSessionSource.cs[9-13]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. No golden-file CSV tests 📎 Requirement gap ☼ Reliability
Description
The added tests assert individual behaviors but do not provide byte-identical golden-file coverage
across the required options matrix, so exact output compatibility with the desktop exporter is not
proven.
Code

src/Daqifi.Core.Tests/Logging/Export/LoggingSessionCsvExporterTests.cs[R16-30]

+    private static async Task<(string[] lines, string header)> ExportToLinesAsync(
+        ILoggingSessionSource source,
+        CsvExportOptions options,
+        IProgress<int>? progress = null,
+        CancellationToken cancellationToken = default)
+    {
+        var sw = new StringWriter();
+        var exporter = new LoggingSessionCsvExporter();
+        await exporter.ExportAsync(source, sw, options, progress, cancellationToken);
+        var content = sw.ToString();
+        var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries)
+                           .Select(l => l.TrimEnd('\r'))
+                           .ToArray();
+        return (lines, lines.Length > 0 ? lines[0] : string.Empty);
+    }
Evidence
The compliance checklist requires golden-file (byte-identical) tests across key scenarios. The test
suite added here builds CSV strings and checks selected properties/values, but does not compare full
output bytes against golden baselines.

Golden-file tests proving byte-identical CSV output to desktop exporter across the options matrix
src/Daqifi.Core.Tests/Logging/Export/LoggingSessionCsvExporterTests.cs[16-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Required golden-file tests (byte-identical CSV output across the options matrix) are missing; current tests validate pieces of output rather than full-file byte equality.

## Issue Context
The goal is to prove backwards-compatible CSV output matching the desktop exporter for the same inputs (absolute vs relative time, averaged vs all-samples, mixed-channel sessions, timestamp gaps).

## Fix Focus Areas
- src/Daqifi.Core.Tests/Logging/Export/LoggingSessionCsvExporterTests.cs[16-464]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Culture-dependent value assertion🐞 Bug ☼ Reliability
Description
Export_AllSamples_UsesGeneralFormat compares the exported numeric string against
double.ToString("G") using the current culture, but the exporter formats numbers with
InvariantCulture. The test can fail on machines with non-'.' decimal separators (e.g., de-DE).
Code

src/Daqifi.Core.Tests/Logging/Export/CsvExporterTests.cs[R270-281]

+    [Fact]
+    public async Task Export_AllSamples_UsesGeneralFormat()
+    {
+        var source = new InMemorySampleSource(
+            [Ch1],
+            [new SampleRow(T0, Ch1.Key, 1.23456789)]);
+
+        var (lines, _) = await ExportToLinesAsync(source, new CsvExportOptions { UseRelativeTime = true });
+
+        var parts = lines[1].Split(',');
+        Assert.Equal(1.23456789.ToString("G"), parts[1]);
+    }
Evidence
The exporter writes values using CultureInfo.InvariantCulture, but the test’s expected string uses
current-culture formatting; on non-invariant cultures these strings differ (e.g., 1,23 vs 1.23).

src/Daqifi.Core.Tests/Logging/Export/CsvExporterTests.cs[270-281]
src/Daqifi.Core/Logging/Export/CsvExporter.cs[121-126]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The test computes the expected value string using the current culture, but the exporter outputs numeric values using `CultureInfo.InvariantCulture`. This makes the test locale-dependent.

### Issue Context
`CsvExporter` uses `value.ToString("G", CultureInfo.InvariantCulture)`. The test should use the same culture to avoid failures on non-English locales.

### Fix Focus Areas
- src/Daqifi.Core.Tests/Logging/Export/CsvExporterTests.cs[270-281]
- src/Daqifi.Core/Logging/Export/CsvExporter.cs[121-126]

### Suggested fix
Update the assertion to:
`Assert.Equal(1.23456789.ToString("G", CultureInfo.InvariantCulture), parts[1]);`
(and add `using System.Globalization;` if needed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. AverageWindow lacks validation🐞 Bug ≡ Correctness
Description
CsvExportOptions documents AverageWindow as a positive integer, but ExportAveragedAsync does not
enforce this; zero/negative values change behavior (the window condition is immediately true) and
produce unexpected output.
Code

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[R133-157]

+        var window = options.AverageWindow!.Value;
+        var totals = channelKeys.ToDictionary(k => k, _ => 0.0);
+        var counts = channelKeys.ToDictionary(k => k, _ => 0);
+        var sb = new StringBuilder(4 * 1024);
+        long? firstTicks = null;
+        long lastTick = 0;
+        var windowCount = 0;
+        var processed = 0;
+
+        await foreach (var sample in source.StreamSamples(cancellationToken))
+        {
+            firstTicks ??= sample.TimestampTicks;
+            lastTick = sample.TimestampTicks;
+
+            if (totals.ContainsKey(sample.ChannelKey))
+            {
+                totals[sample.ChannelKey] += sample.Value;
+                counts[sample.ChannelKey]++;
+            }
+
+            windowCount++;
+            processed++;
+
+            if (windowCount >= window)
+            {
Evidence
The code reads AverageWindow!.Value and compares windowCount >= window without checking that
window > 0, despite the options documentation requiring a positive integer.

src/Daqifi.Core/Logging/Export/CsvExportOptions.cs[20-25]
src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[133-157]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AverageWindow` is documented as a positive integer but is not validated. `0` or negative values will cause the averaging loop to behave incorrectly.

### Issue Context
With `window <= 0`, `windowCount >= window` becomes true immediately, changing semantics and making outputs surprising.

### Fix Focus Areas
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[23-44]
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[133-157]

### Suggested fix
Add a guard before selecting averaged mode (or at the start of `ExportAveragedAsync`):
- If `options.AverageWindow` is not null and `<= 0`, throw `ArgumentOutOfRangeException`.
- Keep the existing `null` meaning (all-samples mode) unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. INVALID() skipped in relative 📎 Requirement gap ≡ Correctness
Description
In relative-time mode, FormatTimestamp does not apply the INVALID({ticks}) fallback for
out-of-range ticks, which can change CSV output compared to the required invalid-tick behavior.
Code

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[R189-197]

+    private static string FormatTimestamp(long ticks, long firstTicks, bool useRelativeTime)
+    {
+        if (useRelativeTime)
+            return ((ticks - firstTicks) / (double)TimeSpan.TicksPerSecond).ToString("F3");
+
+        return (ticks > 0 && ticks <= DateTime.MaxValue.Ticks)
+            ? new DateTime(ticks).ToString("O")
+            : $"INVALID({ticks})";
+    }
Evidence
The checklist requires the invalid-tick fallback format INVALID({ticks}). The current
implementation returns an F3 seconds value immediately when useRelativeTime is true, bypassing
the invalid-tick check entirely.

Preserve desktop CSV formatting edge cases (invalid ticks and relative time format)
src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[189-197]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FormatTimestamp` bypasses invalid-tick handling when `useRelativeTime` is enabled, so out-of-range tick values may serialize as numeric offsets rather than `INVALID({ticks})`.

## Issue Context
Compliance requires preserving the desktop exporter edge-case formatting for invalid ticks.

## Fix Focus Areas
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[189-197]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
8. Cancellation after header write 🐞 Bug ☼ Reliability
Description
ExportAsync writes the CSV header before any explicit cancellation check, so a pre-cancelled token
can still leave header-only output in the destination before OperationCanceledException is observed
later.
Code

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[R30-38]

+        var channels = source.GetChannels();
+        if (channels.Count == 0)
+            return;
+
+        var channelKeys = channels.Select(c => c.Key).ToList();
+
+        await WriteHeaderAsync(writer, channelKeys, options);
+
+        var totalSamples = await source.GetSampleCountAsync(cancellationToken);
Evidence
The header is written unconditionally before cancellation is guaranteed to be observed (it depends
on source.GetSampleCountAsync/StreamSamples honoring the token), which can leave partial results in
a file/stream.

src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[30-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ExportAsync` writes output (header) before an explicit cancellation check, which can result in header-only output when a token is already cancelled.

### Issue Context
Even though `GetSampleCountAsync` receives the token, cancellation observation is source-dependent; an explicit early check makes behavior deterministic.

### Fix Focus Areas
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[23-44]

### Suggested fix
At the start of `ExportAsync` (before `WriteHeaderAsync`), add:
- `cancellationToken.ThrowIfCancellationRequested();`

Optionally also check again immediately before starting the main export loop.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

9. Wrong exception param name🐞 Bug ⚙ Maintainability
Description
ExportAsync throws ArgumentOutOfRangeException with paramName set to "options" even though
the invalid argument is CsvExportOptions.AverageWindow. This reduces the usefulness of exception
diagnostics and conventionally should point at the offending option name.
Code

src/Daqifi.Core/Logging/Export/CsvExporter.cs[R34-36]

+        if (options.AverageWindow.HasValue && options.AverageWindow.Value <= 0)
+            throw new ArgumentOutOfRangeException(nameof(options), options.AverageWindow.Value,
+                $"{nameof(CsvExportOptions.AverageWindow)} must be greater than zero.");
Evidence
The thrown exception uses nameof(options) as the parameter name while validating
options.AverageWindow, so consumers see ParamName == "options" rather than "AverageWindow".

src/Daqifi.Core/Logging/Export/CsvExporter.cs[34-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ArgumentOutOfRangeException` is thrown with `paramName` set to `options`, but the invalid value is specifically `CsvExportOptions.AverageWindow`.

### Issue Context
This is a diagnostics/usability issue: exception handlers and logs typically rely on `ParamName` to identify the invalid input.

### Fix Focus Areas
- src/Daqifi.Core/Logging/Export/CsvExporter.cs[34-37]

### Suggested fix
Change the throw to use `nameof(CsvExportOptions.AverageWindow)` (or the string `"AverageWindow"`) as the `paramName` argument.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +68 to +76
long? firstTicks = null;
long? currentTick = null;
var bucket = new List<SampleRow>();
var processed = 0;

await foreach (var sample in source.StreamSamples(cancellationToken))
{
firstTicks ??= sample.TimestampTicks;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Relative time ignores sessionstart 📎 Requirement gap ≡ Correctness

When UseRelativeTime is enabled, the exporter computes offsets from the first sample tick instead
of ILoggingSessionSource.SessionStart, so exports can be incorrect when session start differs from
the first sample timestamp.
Agent Prompt
## Issue description
`UseRelativeTime` offsets are computed from the first sample tick instead of the session start (`ILoggingSessionSource.SessionStart`). This can produce incorrect relative timestamps when there is a gap between session start and the first sample.

## Issue Context
The compliance requirement for relative timestamps is "seconds elapsed since session start".

## Fix Focus Areas
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[68-76]
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[137-160]
- src/Daqifi.Core/Logging/Export/ILoggingSessionSource.cs[9-13]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree, with action — SessionStart has been removed from the interface in commit 6b9e333. The exporter intentionally derives relative time from the first observed tick in the stream because:

  1. SessionStart was never consulted by the export logic in the first place.
  2. For storage-agnostic consumers (SD-card files, capture buffers, future CLI tools) there's often no meaningful "session start" distinct from the first sample, so requiring sources to populate it would have been ceremony.

The rename commit message captures the rationale: "the exporter uses the first observed tick from the stream for relative-time calculations and never consulted SessionStart."

If a future consumer ever needs offsets from a different anchor, they can normalize ticks before yielding them through ISampleSource.

Comment on lines +16 to +30
private static async Task<(string[] lines, string header)> ExportToLinesAsync(
ILoggingSessionSource source,
CsvExportOptions options,
IProgress<int>? progress = null,
CancellationToken cancellationToken = default)
{
var sw = new StringWriter();
var exporter = new LoggingSessionCsvExporter();
await exporter.ExportAsync(source, sw, options, progress, cancellationToken);
var content = sw.ToString();
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(l => l.TrimEnd('\r'))
.ToArray();
return (lines, lines.Length > 0 ? lines[0] : string.Empty);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. No golden-file csv tests 📎 Requirement gap ☼ Reliability

The added tests assert individual behaviors but do not provide byte-identical golden-file coverage
across the required options matrix, so exact output compatibility with the desktop exporter is not
proven.
Agent Prompt
## Issue description
Required golden-file tests (byte-identical CSV output across the options matrix) are missing; current tests validate pieces of output rather than full-file byte equality.

## Issue Context
The goal is to prove backwards-compatible CSV output matching the desktop exporter for the same inputs (absolute vs relative time, averaged vs all-samples, mixed-channel sessions, timestamp gaps).

## Fix Focus Areas
- src/Daqifi.Core.Tests/Logging/Export/LoggingSessionCsvExporterTests.cs[16-464]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defer / partially disagree — the test plan committed to in this PR is "29 xUnit tests covering the options matrix" (now 32 after my recent fix-pass), not byte-identical golden-file comparison. The unit tests cover the matrix Qodo cites: absolute vs relative time, averaged vs all-samples, mixed-channel sessions with gaps, invalid ticks (now in both time modes), custom delimiter, duplicate channel at same tick, progress, cancellation.

Adding desktop-byte-equivalence tests would also cement coupling to a specific desktop snapshot, which works against the design intent of this PR — making the exporter consumable by SD-card tooling, headless CLIs, and other contexts where bit-for-bit desktop parity isn't a goal. If a desktop-side regression test is wanted, the natural place is desktop's own test suite, asserting a frozen golden against CsvExporter output.

Happy to revisit if there's a concrete scenario where desktop's CSV output and core's must agree to the byte, but I don't think speculating on that scenario justifies the test infra now.

Comment thread src/Daqifi.Core/Logging/Export/CsvExporter.cs Outdated
Comment on lines +142 to +183
await foreach (var sample in source.StreamSamples(cancellationToken))
{
firstTicks ??= sample.TimestampTicks;
lastTick = sample.TimestampTicks;

if (totals.ContainsKey(sample.ChannelKey))
{
totals[sample.ChannelKey] += sample.Value;
counts[sample.ChannelKey]++;
}

windowCount++;
processed++;

if (windowCount >= window)
{
sb.Clear();
sb.Append(FormatTimestamp(lastTick, firstTicks.Value, options.UseRelativeTime));

foreach (var key in channelKeys)
{
sb.Append(options.Delimiter);
if (counts[key] > 0)
sb.Append((totals[key] / counts[key]).ToString("G"));
}

sb.AppendLine();
await writer.WriteAsync(sb.ToString());

foreach (var key in channelKeys)
{
totals[key] = 0.0;
counts[key] = 0;
}
windowCount = 0;

ReportProgress(progress, processed, totalSamples);
}
}

progress?.Report(100);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Averaging drops trailing samples 🐞 Bug ≡ Correctness

ExportAveragedAsync never flushes a final partial window, so any trailing samples when sampleCount %
AverageWindow != 0 are silently omitted from the CSV.
Agent Prompt
### Issue description
`ExportAveragedAsync` only writes output when it has accumulated a full `AverageWindow` samples, and it does not write the last partial window at end-of-stream. This causes silent data loss for trailing samples.

### Issue Context
If there are 5 samples and `AverageWindow=2`, only 4 samples are represented (2 rows) and the last sample is dropped.

### Fix Focus Areas
- src/Daqifi.Core/Logging/Export/LoggingSessionCsvExporter.cs[142-183]

### Suggested fix
After the `await foreach` loop:
- If `windowCount > 0`, write one final averaged row using the accumulated `totals`/`counts` and `lastTick`.
- Optionally `ReportProgress(...)` after flushing so progress reflects the last samples.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d4cb91aExportAveragedAsync now flushes a final partial window after the await foreach exits when windowCount > 0, so trailing samples that don't fill AverageWindow are no longer dropped. New test Export_AverageWindow_FlushesPartialFinalWindow covers the 5-samples-with-window-2 case Qodo described (writes 3 rows: two full windows + one partial holding only the final sample).

tylerkron and others added 3 commits April 18, 2026 22:04
- Make WriteTimestampRow async (consistent with averaged path, avoids
  blocking thread pool on file I/O when buffer fills)
- Gate GetSampleCountAsync behind progress != null (avoids unnecessary
  DB round-trip when caller doesn't need progress reporting)
- Guard against AverageWindow <= 0 with ArgumentOutOfRangeException
- Use CultureInfo.InvariantCulture for all number formatting to prevent
  broken CSV output in non-US locales
- Switch progress test collections to ConcurrentBag (thread-safe)
- Add Theory test covering AverageWindow 0, -1, -100

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop the desktop-flavored "Session" naming from the export types. The CSV
exporter is consumed by anything streaming timestamped channel samples
(SD-card log files, live capture, EF-backed sessions, etc.), not just
desktop's session model.

Renames:
  ILoggingSessionSource     -> ISampleSource
  LoggingSessionCsvExporter -> CsvExporter
  InMemoryLoggingSessionSource (test fake) -> InMemorySampleSource

Also drops the unused SessionStart property from the interface; the
exporter uses the first observed tick from the stream for relative-time
calculations and never consulted SessionStart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tylerkron
Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 25, 2026

Persistent review updated to latest commit 6b9e333

- Averaging now flushes a final partial window when the sample count
  isn't a multiple of AverageWindow; previously trailing samples were
  silently dropped (Qodo #2).
- INVALID({ticks}) fallback now applies in both absolute and relative
  time modes; previously relative mode skipped the invalid-tick check
  entirely and emitted a numeric offset for out-of-range ticks (#7).
- ExportAsync now calls cancellationToken.ThrowIfCancellationRequested()
  before writing the CSV header, preventing header-only output for a
  pre-cancelled token (#8).
- ArgumentOutOfRangeException for invalid AverageWindow now reports
  paramName as "options.AverageWindow" instead of "options" (#9).
- Test for "G" formatting now compares against InvariantCulture, matching
  the exporter's invariant output and avoiding failures on de-DE etc. (#5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tylerkron
Copy link
Copy Markdown
Contributor Author

Qodo review response (commit d4cb91a)

For the items in Qodo's persistent review that don't have inline comment threads:

# Item Status
1 Locale-dependent number formatting ☑ Already resolved in 4b2147f (struck through by Qodo)
2 Averaging drops trailing samples ✅ Fixed in d4cb91a — final partial window now flushed; new test covers it
3 Relative time ignores SessionStart 🚫 Disagree (responded inline) — SessionStart was removed from the interface in 6b9e333
4 No golden-file CSV tests 🚫 Defer (responded inline) — out of scope; the 32 unit tests cover the options matrix
5 Culture-dependent value assertion ✅ Fixed in d4cb91a — test now uses InvariantCulture to match the exporter
6 AverageWindow lacks validation ☑ Already resolved in 4b2147f (struck through by Qodo)
7 INVALID() skipped in relative time ✅ Fixed in d4cb91a — invalid-tick check now precedes the relative-time branch; new test covers it
8 Cancellation after header write ✅ Fixed in d4cb91aThrowIfCancellationRequested() now runs before WriteHeaderAsync; new test asserts no header is written for a pre-cancelled token
9 Wrong exception param name ✅ Fixed in d4cb91aparamName is now "options.AverageWindow"

Test suite: 856 passing on net9.0 + net10.0 (was 853 before; 3 new tests added).

@tylerkron tylerkron merged commit b7b0735 into main Apr 25, 2026
1 check passed
@tylerkron tylerkron deleted the claude/infallible-nobel-6fff72 branch April 25, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add logging session export (CSV) to daqifi-core

1 participant