Fix LSP timesheet diagnostics: doubled prefix, duplicate publish, cross-file dates #137

Merged
kfickel merged 4 commits from 136_lsp-timesheet-diagnostics-fixes into main 2026-08-19 21:50:01 +02:00
Owner

Closes #136.

Summary

  • Doubled "Timesheet error:" prefix: StreamdError::TimesheetError's Display already includes the prefix; lsp.rs was adding it a second time when building the diagnostic message.
  • Duplicate diagnostic delivery: the server always pushed diagnostics via publish_diagnostics on didOpen/didChange/didSave and answered pull requests (textDocument/diagnostic), so a client that declared textDocument.diagnostic support got every diagnostic twice, in non-deterministic order relative to the push. Now the server checks the client's declared capabilities at initialize and skips the unsolicited push for pull-capable clients. diagnosticProvider.interFileDependencies is also flipped to true to reflect the cross-file dependency introduced below.
  • File-scoped timesheet validation: R18 diagnostics only looked at the single open file's shard, but timesheets are a repository-wide concept — a day's entries can be split across multiple files (as the CLI's streamd timesheet command already accounts for via load_markdown_shards). This caused false positives (a real closing @Break living in a sibling file) and false negatives (overlaps only visible once sibling files are merged in). compute_diagnostics now merges the open file with every other repository file that shares at least one timesheet date with it before calling extract_timesheets, and only reports diagnostics for dates the open file itself contributes to — so an unrelated file's own violation (including one that aborts the whole computation) can never surface on this file.

Also included

While testing the timesheet state machine for the above, found and fixed a separate bug: an unclosed @Timesheet card logged for later today (before that time actually arrives — e.g. planning ahead) produced an inverted timecard (e.g. from 12:00 to 11:00), because the synthetic "close at now" fallback in aggregate_timecard_day didn't check that now was actually after the card's start time. Now clamped to a zero-duration timecard instead.

Test plan

  • cargo test — 211 tests pass, including new regression tests for all four fixes (doubled-message, cross-file merge, cross-file date attribution, inverted-timecard clamp)
  • cargo clippy --all-targets -- -D warnings
  • cargo fmt
  • nix flake check
Closes #136. ## Summary - **Doubled `"Timesheet error:"` prefix**: `StreamdError::TimesheetError`'s `Display` already includes the prefix; `lsp.rs` was adding it a second time when building the diagnostic message. - **Duplicate diagnostic delivery**: the server always pushed diagnostics via `publish_diagnostics` on `didOpen`/`didChange`/`didSave` *and* answered pull requests (`textDocument/diagnostic`), so a client that declared `textDocument.diagnostic` support got every diagnostic twice, in non-deterministic order relative to the push. Now the server checks the client's declared capabilities at `initialize` and skips the unsolicited push for pull-capable clients. `diagnosticProvider.interFileDependencies` is also flipped to `true` to reflect the cross-file dependency introduced below. - **File-scoped timesheet validation**: R18 diagnostics only looked at the single open file's shard, but timesheets are a repository-wide concept — a day's entries can be split across multiple files (as the CLI's `streamd timesheet` command already accounts for via `load_markdown_shards`). This caused false positives (a real closing `@Break` living in a sibling file) and false negatives (overlaps only visible once sibling files are merged in). `compute_diagnostics` now merges the open file with every other repository file that shares at least one timesheet date with it before calling `extract_timesheets`, and only reports diagnostics for dates the open file itself contributes to — so an unrelated file's own violation (including one that aborts the whole computation) can never surface on this file. ## Also included While testing the timesheet state machine for the above, found and fixed a separate bug: an unclosed `@Timesheet` card logged for later today (before that time actually arrives — e.g. planning ahead) produced an **inverted timecard** (e.g. `from 12:00 to 11:00`), because the synthetic "close at now" fallback in `aggregate_timecard_day` didn't check that `now` was actually after the card's start time. Now clamped to a zero-duration timecard instead. ## Test plan - [x] `cargo test` — 211 tests pass, including new regression tests for all four fixes (doubled-message, cross-file merge, cross-file date attribution, inverted-timecard clamp) - [x] `cargo clippy --all-targets -- -D warnings` - [x] `cargo fmt` - [x] `nix flake check`
An unclosed @Timesheet card logged for later today (before that time
actually arrives) produced an inverted Timecard, e.g. from 12:00 to
11:00, because the synthetic "close at now" fallback didn't check
that now was actually after the card's start time.
did_open/did_change/did_save always pushed diagnostics via
publish_diagnostics, while textDocument/diagnostic also let clients
pull them on demand. A client that declared textDocument.diagnostic
support got every diagnostic twice, in a non-deterministic order
relative to the push notification.

Detect pull support from the client's declared capabilities at
initialize time and skip the unsolicited push for such clients.
fix(lsp): validate timesheets across files sharing a date, fix doubled error prefix
All checks were successful
Continuous Integration / Lint, Check & Test (push) Successful in 4m34s
Continuous Integration / Build Package (push) Successful in 5m40s
47cbe0a1f0
R18 diagnostics only looked at the currently open file's shard, but
timesheets are a repository-wide concept (a day's entries can span
multiple files). This caused false positives when a closing @Break
lived in a different file, and false negatives when an overlap only
became visible once sibling files were included.

compute_diagnostics now merges the open file with every other
repository file that shares at least one timesheet date with it
before running extract_timesheets, and only reports diagnostics for
dates the open file itself contributes to — so an unrelated file's
own violation (including one that aborts the whole computation) can
never surface on this file.

Also fixes StreamdError::TimesheetError's Display already including
a "Timesheet error: " prefix that lsp.rs was adding a second time.

Update REQUIREMENTS.md (R18, R25b) to document both the cross-file
validation and the today-only synthetic-close clamping behavior.
chore(build): bump version to 0.4.0
All checks were successful
Continuous Integration / Build Package (push) Successful in 6s
Continuous Integration / Lint, Check & Test (push) Successful in 24s
Release / Build and Release (push) Successful in 6m32s
ea28256192
kfickel deleted branch 136_lsp-timesheet-diagnostics-fixes 2026-08-19 21:50:01 +02:00
Sign in to join this conversation.
No reviewers
No labels
planned
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
kfickel/streamd!137
No description provided.