Skip to content

Source weighting, grow-first fill & project consolidation#3

Merged
guuse merged 6 commits into
mainfrom
feat/source-weighting-grow-first-fill
Jun 2, 2026
Merged

Source weighting, grow-first fill & project consolidation#3
guuse merged 6 commits into
mainfrom
feat/source-weighting-grow-first-fill

Conversation

@guuse
Copy link
Copy Markdown
Owner

@guuse guuse commented Jun 1, 2026

Why

Time-tracking days were being built mostly from trends (historical bookings): real meeting/commit/browser blocks got lost or mislabelled, unrelated things were coupled, and the calendar — which should be authoritative — barely showed up. This PR reworks classification, packing, and the timeline UI so that observed activity owns the day and trends only top it up, the calendar is the highest-priority source, and nothing the classifier finds is silently dropped.

Designed across several grilling sessions; decisions live in CONTEXT.md (glossary) and docs/adr/0004-* (amends ADR-0002). Two design explorations are recorded in docs/design/.

Core model

  • Source priority calendar > GitHub commits > browser > Linear > trends. Trends are strictly subordinate: fill only, never relabel or evict observed work.
  • Deterministic trend engine (computeTrendPatterns) — weeks present, cadence, avg duration, historical share computed in TS; the LLM only labels candidates, never invents them (hybrid; see ADR-0004).
  • Grow-first fill (PackDayUseCase) — the 8h gap is filled by growing observed project blocks proportional to historical share (capped at their historical average), then by fill blocks only for strong recurring patterns (≥3 of 4 weeks + cadence) for projects with no activity that day.
  • Project consolidation (consolidateByProjectService) — same project+service folds into one block (PRs preserved in the note); different services stay separate; meetings excluded.
  • Linear as a first-class source (groupLinearIssuesIntoBlocks) — completed issues not referenced by a commit become their own classifiable blocks.
  • Stable repo→project cache key (mappingCacheKey) — strip the @HH:mm session suffix so commit classification stops re-guessing daily.

Calendar is highest priority

  • Every accepted meeting always yields a block, independent of whether the LLM echoes its index (falls back to event title / cached mapping / unclassified). Never silently dropped.
  • Meetings anchored at their real times and never repacked or deduped away. The day may exceed 8h (floor, not ceiling); you manually deselect before booking.
  • RSVP filter relaxed — keep everything except explicitly declined. Recurring team meetings left on needsAction (attended but un-RSVP'd) now flow through; previously they vanished before becoming data.
  • Google-Calendar-style overlap layoutassignBlockColumns groups blocks into transitive overlap clusters and splits width only within a cluster, so a local overlap no longer narrows the whole day. 4px gutter + 3px vertical inset for breathing room.
  • Readable short meetings — blocks under 42px render a compact single line (title + time inline) instead of clipping.

Leftover-blocks sidebar

Blocks the classifier found but the packer couldn't place no longer disappear:

  • PackDayUseCase.executeWithLeftovers returns { placed, leftovers } — overflowed real work ('overflow') + unused LLM suggestions ('suggestion', deduped by project+service). Meetings are never leftovers.
  • A new compact-chips right sidebar (LeftoverSidebar) auto-opens when leftovers exist, collapses to a rail with a count badge, and offers per-chip Toevoegen / Boek / Negeer plus a bulk Alles +. WeekPage routes placed → timeline, unplaced → sidebar.

Tests

1654 passing, typecheck + lint clean. New unit/component coverage for the trend engine, consolidation, grow-first + strong-pattern fill, leftover split, the cache key, Linear blocks, the RSVP filter, the overlap layout, and the sidebar.

Known caveats / easy toggles

  1. Meeting↔browser absorption is still time-only (no hard-evidence gate); low severity since a meeting block uses the meeting's duration.
  2. Suggestion volume: every unplaced candidate surfaces (deduped); can be floored at confidence ≥ 2 if noisy.
  3. "Add to day" appends after the last placed block rather than re-running the packer.
  4. Prompt-text changes take effect on the next build and aren't unit-testable here.

🤖 Generated with Claude Code

Rework day classification so observed activity owns the day and trends only
top it up — fixing the symptom where almost everything came from trends and
real blocks were lost.

- Source priority calendar > commits > browser > Linear > trends, with trends
  strictly subordinate (fill only, never relabel/evict observed work).
- Deterministic trend engine (computeTrendPatterns): weeks present, cadence,
  avg duration and historical share computed in TS; the LLM only labels, never
  invents (hybrid — see ADR-0004, amends ADR-0002).
- Grow-first fill in PackDayUseCase: distribute the 8h gap proportionally across
  observed project blocks (capped at historical average), then fill only with
  strong recurring patterns (>=3 of 4 weeks + cadence) for no-activity projects.
- Project consolidation (consolidateByProjectService): fold same project+service
  blocks into one Project block, preserving individual PRs/commits in the note;
  different services stay separate (billing); meetings/fill excluded.
- Linear becomes a first-class source: completed issues not referenced by a
  commit become their own classifiable blocks; covered ones stay absorbed.
- Stable repo->project cache key (mappingCacheKey): strip the @hh:mm session
  suffix so commit classification stops re-guessing the repo's project daily.
- Prompt rewrite: source priority, hard-evidence relatedness gate (name the
  evidence or don't couple), and the app now owns sizing/fill.

Glossary updated (CONTEXT.md): Source, Absorption, Project block, Trends,
Strong recurring pattern, Fill target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 98.69% (🎯 95%) 2656 / 2691
🔵 Statements 98.02% (🎯 95%) 3033 / 3094
🔵 Functions 97.73% (🎯 95%) 778 / 796
🔵 Branches 95.9% (🎯 95%) 1709 / 1782
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/domain/entities/ClassifiedBlock.ts 100% 100% 100% 100%
src/domain/usecases/GroupAndClassifyDayUseCase.ts 100% 98.26% 100% 100%
src/domain/usecases/PackDayUseCase.ts 95.18% 90.12% 97.72% 97.77% 92, 102, 265-267, 271, 275, 293
src/domain/usecases/ProcessDayUseCase.ts 100% 100% 100% 100%
src/domain/usecases/computeTrendPatterns.ts 97.56% 81.25% 90% 97.22% 125
src/domain/usecases/consolidateByProjectService.ts 94% 85.71% 100% 97.36% 69, 93, 94
src/domain/usecases/groupLinearIssuesIntoBlocks.ts 100% 100% 100% 100%
src/domain/usecases/mappingCacheKey.ts 100% 100% 100% 100%
src/infrastructure/googlecalendar/GoogleCalendarRepository.ts 97.75% 96.22% 100% 98.68% 175, 182
src/ui/components/DayTimeline.helpers.ts 100% 97.22% 100% 100%
src/ui/components/DayTimeline.tsx 99.21% 92.06% 100% 100% 120
src/ui/components/LeftoverSidebar.tsx 86.04% 79.62% 88.88% 88.23% 32-33, 38, 119
src/ui/hooks/useImport.ts 100% 100% 100% 100%
src/ui/pages/WeekPage.tsx 88.1% 89.5% 83.9% 90.87% 205-248, 313, 363, 402, 469, 478, 507, 601-604
Generated in workflow #12 for commit c4a3232 by the Vitest Coverage Report Action

guuse and others added 5 commits June 2, 2026 09:12
…t real times

Two defects surfaced in review: morning meetings vanished while trend-fill
re-invented meeting-named blocks, and surviving meetings were repacked to the
wrong times.

- Every accepted meeting now ALWAYS yields a block (GroupAndClassifyDayUseCase),
  independent of whether the LLM echoes its index. Falls back to the event title
  and cached project/service mapping, else stays unclassified — never silently
  dropped. Standalone browser items stay LLM-driven (the model may skip noise).
- Meetings are anchored at their real calendar times again (PackDayUseCase),
  matching CONTEXT.md's Anchor definition. Concurrent meetings keep their times
  and overlap each other; the existing assignBlockColumns renders them side by
  side (Google-Calendar style). Movable work/fill flows around the union of
  existing entries + meeting spans; meetings count toward the 8h target.
- Prompt: instruct the model to always return a block for every meeting item
  (project/service may be null) since calendar has highest priority.

CONTEXT.md Meeting block entry updated with the guarantee + overlap rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real reason morning meetings vanished: the calendar fetch's isAttending
kept only 'accepted'/'tentative', so recurring team meetings left on
'needsAction' (attended but never RSVP'd) were filtered out before they ever
became events — no downstream guarantee could recover them.

Relax isAttending to keep everything except explicitly 'declined' meetings,
matching the highest-priority status of the calendar source. Added a test for
needsAction kept / declined dropped; CONTEXT.md wording updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
assignBlockColumns now groups blocks into transitive overlap CLUSTERS and splits
width only within a cluster, so a single overlapping meeting no longer narrows
the whole day (the old global numCols flung overlappers to a far column). Each
block carries `cols` (its cluster's column count); DayTimeline renders width =
100/cols with a 4px gutter between concurrent columns and a 3px vertical inset
for Google-Calendar breathing room.

Chosen from four mockups in docs/design/timeline-overlap-options.html
(Option 1, split-within-band). Added a test asserting a localized overlap keeps
non-overlapping blocks at full width.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Short blocks (15-min meetings) rendered a two-line layout that clipped at the
  min height. Render a single compact line (title + time inline, badge hidden)
  when a block is under 42px, and raise the content-height floor to 24px so one
  line always fits. Applies to concept and entry blocks.
- The packer no longer deduplicates meeting blocks against booked entries — a
  meeting is never removed (calendar is highest priority). The day may exceed 8h;
  the user manually deselects blocks before booking. Non-meeting concepts are
  still deduped as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocks classification found but the packer couldn't place were silently lost.
They now surface in a right sidebar (compact-chips design) instead of vanishing.

Domain:
- PackDayUseCase.executeWithLeftovers returns { placed, leftovers }. Leftovers =
  movable work that overflows past dayEnd (reason 'overflow') + LLM candidates the
  day didn't need (reason 'suggestion', deduped by project+service). Meetings are
  never leftovers. execute() stays a placed-only delegator for existing callers.
- ClassifiedBlock gains unplaced + leftoverReason; ProcessDayUseCase persists
  placed + leftovers together (UI routes by the flag).

UI:
- New LeftoverSidebar (Design 2): collapsible rail with count badge, auto-opens
  when leftovers exist, dense chips with hover actions Toevoegen / Boek / Negeer
  and an "Alles +" bulk add. Confidence colors + reason labels match the app.
- WeekPage routes placed→DayTimeline, unplaced→sidebar, and wires the actions:
  add appends to the day, book routes through the booking modal, dismiss removes.

Glossary: CONTEXT.md "Leftover block". Design options kept in
docs/design/leftover-sidebar-options.html (Option 2 chosen). 1654 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@guuse guuse merged commit 1cc9ce0 into main Jun 2, 2026
1 check passed
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.

1 participant