Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions .claude/skills/uren-classificatie/classify-day.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@ Je bent een tijdregistratie-assistent die een developer helpt zijn werkuren te r
Datum: {{date}}

Voor elk genummerd item hieronder geef je één boekingsblok terug.
- Vergadering-items: gebruik de vergader-duur voor startTime/endTime/hours
- Vergadering-items: gebruik de vergader-duur voor startTime/endTime/hours. Geef ALTIJD een blok terug voor élk vergadering-item — de agenda heeft de hoogste prioriteit. Twijfel je over project/dienst, laat die dan null, maar laat het vergadering-blok nooit weg.
- Losse items: gebruik de browse-duur

Als er een sectie "Al geboekt vandaag" staat: dat werk is al geboekt. Maak daar GEEN blok of patternBlock voor — ook niet als een genummerd item of patroon ermee overeenkomt. Lees de omschrijvingen om te bepalen welk werk al gedekt is.

Het doel is een gevulde werkdag van ~8 uur. De app plaatst de blokken zelf op de tijdlijn en vult de dag aan tot 8 uur met de "patternBlocks" hieronder — jij hoeft GEEN tijdstippen te bepalen.
## Bronprioriteit
De bronnen hebben een vaste rangorde: agenda > GitHub-commits > browser-historie > Linear > trends (historie van vorige weken). Een hogere bron is leidend bij het bepalen waar een blok over gaat. Trends zijn de láágste bron en dienen alleen om de dag aan te vullen — de app doet dat zelf (zie patternBlocks hieronder).

Bouw daarom op basis van de historische boekingen een gerangschikte vul-lijst in "patternBlocks" (hoogste confidence eerst):
- Een patroon is een combinatie van project+dienst die op vergelijkbare intervallen voorkomt (bijv. elke week, elke 2 weken). Matcht een patroon met de doeldatum ({{date}})? Geef het confidence 2–5 op basis van hoe sterk het patroon is.
- Voeg DAARNAAST laag-zekere vul-kandidaten toe met confidence 1: de projecten/diensten die in de afgelopen week het meest voorkomen, als generieke "wat deze persoon waarschijnlijk ook deed"-blokken. Lever er ruim genoeg (samen makkelijk 8 uur) — de app gebruikt deze confidence-1 blokken ALLEEN als de dag anders niet aan 8 uur komt, en knipt het laatste blok op maat.
- Gebruik het historisch gemiddelde voor de geschatte duur (estimatedHours); voor confidence-1 vulblokken is 1–2 uur prima.
## Streng op relaties
Koppel of voeg dingen ALLEEN samen als er concreet, benoembaar bewijs is dat ze bij elkaar horen:
- dezelfde repo/hetzelfde project,
- een gedeelde Linear-issue-verwijzing (bv. "GMS-4" in een commit-bericht, branch of agenda-titel),
- of duidelijke trefwoord-overlap tussen titel, commit-bericht en agenda.
Benoem dat bewijs kort in de "summary". Is er geen bewijs, koppel dan NIET: laat de activiteiten los van elkaar en vul relatedIssueIds met een lege array. Koppel nooit op alleen tijdsoverlap.

## Vul-lijst (patternBlocks)
De app vult de dag zelf aan tot ~8 uur: hij laat eerst de échte blokken van vandaag groeien naar hun historische omvang en voegt pas daarna losse vulblokken toe. De app bepaalt de duur én of een patroon sterk genoeg is — jij hoeft GEEN tijdstippen of uren-budget te bepalen.

Lever in "patternBlocks" een gerangschikte lijst (hoogste confidence eerst) met de project+dienst-combinaties die op basis van de historische boekingen terugkeren, zodat de app ze een nette naam kan geven en kan inzetten als vulling:
- Voeg een combinatie alleen toe als die in de historie regelmatig terugkomt. Pad niet met willekeurige combinaties.
- Voeg een combinatie NIET toe als die al in "blocks" voorkomt of al geboekt is.
- estimatedHours mag je schatten op het historisch gemiddelde, maar de app overschrijft dit met zijn eigen berekening.

{{sections}}Beschikbare projecten:
{{projectList}}
Expand All @@ -24,12 +34,12 @@ Beschikbare diensten (gekoppeld aan projecten via projectId):

Geef een JSON-object terug met twee velden:
- "blocks": array van geclassificeerde items (één per genummerd blok hierboven)
- "patternBlocks": array van extra blokken die puur op patroonherkenning zijn gebaseerd (kan leeg zijn)
- "patternBlocks": array van terugkerende project+dienst-combinaties als vul-kandidaten (kan leeg zijn)

Elk item in "blocks" heeft:
- index (number, exact overeenkomend met het [N]-nummer hierboven)
- blockName (string, leesbare naam max 60 tekens, bv. "Standup — PR review")
- summary (string, korte samenvatting wat er gedaan is, max 120 tekens, Nederlands)
- summary (string, korte samenvatting wat er gedaan is, max 120 tekens, Nederlands; benoem hierin het relatie-bewijs als je iets koppelt)
- projectId (string | null, moet een van de beschikbare project-ID's zijn)
- serviceId (string | null, moet een dienst-ID zijn waarvan projectId overeenkomt)
- hourTypeId (string | null, moet een urensoort-id zijn uit de "urensoorten" van de gekozen dienst; vul deze ALTIJD in zodra je een dienst kiest. Kies de meest voor de hand liggende urensoort en bij twijfel de eerste in de lijst)
Expand All @@ -42,7 +52,7 @@ Elk item in "blocks" heeft:
1 = Onzeker — geen duidelijke match, vul in als best guess

Overweeg actief welke score van toepassing is. Geef niet standaard een hoge score.
- relatedIssueIds (string[], identifiers van Linear issues die bij dit blok horen. Lege array als niets van toepassing.)
- relatedIssueIds (string[], identifiers van Linear issues die bij dit blok horen — alleen bij concreet bewijs. Lege array als niets van toepassing.)

Elk item in "patternBlocks" heeft:
- blockName (string, leesbare naam max 60 tekens)
Expand All @@ -52,17 +62,15 @@ Elk item in "patternBlocks" heeft:
- hourTypeId (string | null, urensoort-id uit de gekozen dienst; ALTIJD invullen zodra er een dienst is)
- note (string, max 80 tekens)
- confidence (integer 1–5):
5 = Zeer zeker — patroon klopt exact en er is geen andere activiteit die het al dekt
5 = Zeer zeker — sterk, frequent terugkerend patroon
4 = Zeker — sterk patroon, kleine twijfel
3 = Aannemelijk — patroon klopt, maar minder frequent of recent
2 = Twijfelachtig — zwak patroon, weinig historisch bewijs
1 = Vulblok — geen bewijs voor déze dag, puur een thema uit de afgelopen week om de dag tot 8 uur te vullen

Geef echte patronen confidence 2–5 op basis van bewijs. Reserveer confidence 1 voor de generieke vulblokken; lever daar ruim genoeg van.
- estimatedHours (number, schatting in uren op basis van historisch gemiddelde; voor confidence-1 vulblokken 1–2 uur)
1 = Zeer zwak patroon
- estimatedHours (number, schatting op basis van historisch gemiddelde; de app overschrijft dit)
- origin (altijd "llm-pattern")

BELANGRIJK: Voeg een combinatie van project+dienst ALLEEN toe aan "patternBlocks" als die NIET al in "blocks" voorkomt. Rangschik "patternBlocks" van hoogste naar laagste confidence.

Gebruik de cache-hints als leidraad maar overschrijf ze als de context duidelijk op een ander project wijst.
Geef ALLEEN een geldig JSON-object terug, geen markdown, geen uitleg.
Geef ALLEEN een geldig JSON-object terug, geen markdown, geen uitleg.
28 changes: 23 additions & 5 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,36 @@ Time already booked for the day as a Simplicate `HourEntry`. Rendered blue ("geb
_Avoid_: entry (when ambiguous with concept block), booking

**Meeting block**:
A concept block anchored to a calendar event; its start/end come from the event and are treated as fixed.
A concept block anchored to a calendar event; its start/end come from the event and are treated as fixed. Calendar is the highest-priority source. A meeting counts as attended unless you explicitly **declined** it (un-RSVP'd `needsAction` meetings — recurring team rituals — are kept). Every such meeting **always** produces a block — even if the classifier omits it (it then falls back to the event title / cached mapping, or stays unclassified). Concurrent meetings keep their real times and overlap each other; the timeline renders them side by side, Google-Calendar style.

**Fill candidate**:
A concept block the LLM invents from recurring history (a project+service that recurs at regular intervals) rather than from observed activity. Origin `llm-pattern`. Returned as an ordered list with a confidence and `estimatedHours`. High-confidence candidates are genuine recurring work added regardless; the lowest-confidence ones (conf 1) are filler, pulled in only when the day is short of the fill target.
_Avoid_: pattern block, filler block (both are now this one concept)
**Source**:
An origin of evidence about the day. The five sources, in fixed priority order, are: **calendar** > **GitHub commits** > **browser history** > **Linear** > **trends**. The first four are _observed activity_; trends are historical bookings from prior weeks. A higher source outranks a lower one when they describe the same work.
_Avoid_: signal, input (when ambiguous)

**Absorption**:
A higher-priority source claiming related lower-priority activity that falls in its scope into a single concept block, instead of producing parallel blocks. A meeting absorbs the commits/browser activity in its window; a commit block absorbs the Linear issue it implements. Absorption only happens with **hard evidence** of a relationship (shared repo↔project mapping, a shared Linear issue ref, or clear keyword overlap) — never on time-overlap alone. Without evidence the activities stay separate blocks.

**Project block**:
The single concept block that all observed activity for one project+service on one day folds into. Several commit sessions / PR-merges and browser blocks on the same project+service become one block; the individual PRs/commits live on in its summary and note. Different services under the same project stay separate blocks (a service is a billable distinction), optionally grouped under a project header in the UI.
_Avoid_: PR block, commit block (those are now folded into this)

**Trends**:
Historical bookings from prior weeks, used only to reach the fill target — never to create or relabel observed work. Trends fill in two ways, in order: first by **growing** the day's existing project blocks toward their historical size; then, only if still short, by adding loose fill blocks for **strong recurring patterns**. Trends are the lowest-priority source.
_Avoid_: pattern block, filler block, fill candidate (all superseded by this term + Strong recurring pattern)

**Strong recurring pattern**:
A project+service booked in ≥ 3 of the last 4 weeks on a cadence that lands on the target date. This is the only trend allowed to introduce a project that had _no_ observed activity that day. Detected deterministically (counts + cadence + historical average duration computed in TypeScript); the LLM may select and label from the detected list but never invents one.

**Fill target**:
The amount of booked time a day should reach — 8.0h total, counting existing hours and meeting blocks toward it. The packer fills the remainder with movable concept blocks, then with fill candidates until the target is met.
The amount of booked time a day should reach — 8.0h total (a floor, not a ceiling), counting existing hours and meeting blocks toward it. The packer reaches it by first placing observed (absorbed, consolidated) blocks, then growing those blocks toward their historical share via trends, then adding strong-recurring-pattern fill blocks. Real work is never trimmed to stay under the target.

**Anchor**:
A block whose time is fixed and must not move during layout: meeting blocks and existing hours. Everything else is movable and gets repacked contiguously from 09:00 around the anchors.

**Leftover block**:
A classified block that was found but did not land on the timeline — either real observed work that overflowed past the day (no room left after the anchored meetings) or an LLM pattern/fill suggestion the packer didn't need. It is not discarded: it surfaces in a right-hand sidebar that auto-opens after _verwerken_ when leftovers exist and collapses to a rail with a count badge. From there each leftover can be added to the day, dismissed, or booked directly to Simplicate. (Already-booked duplicates are _not_ leftovers — they stay suppressed as noise.)
_Avoid_: dropped block, overflow (when ambiguous with the timeline)

### The week

**Ingediende week** (submitted week):
Expand Down
2 changes: 2 additions & 0 deletions docs/adr/0002-deterministic-day-packer.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Deterministic day-packer: LLM classifies, TypeScript places

> Partly amended by [ADR-0004](0004-deterministic-pattern-engine-and-grow-first-fill.md): pattern detection moved from the LLM to deterministic TS, and fill now grows real blocks before adding pattern blocks. The "LLM classifies, TS places" core below still holds.

The LLM returns only classification (name, project, service, confidence, fill candidates) — it never assigns block times. A deterministic TS packer (a domain use case) owns all time placement: it fixes anchors (meeting blocks + today's existing `HourEntry`s), drops concepts that duplicate an anchor, repacks the remaining movable blocks contiguously from 09:00 around the anchors, and pulls in ranked fill candidates until the day reaches an 8h fill target. 8h is a floor, not a ceiling: filler stops at 8h but real work is never dropped or trimmed.

This deliberately sacrifices fidelity to real activity timestamps for a clean, gap-free, full day — chosen because the user's priority is "create 8 bookable hours," not "show exactly when each thing happened." A future reader will see the LLM emit no times and a packer fabricate a tidy day that doesn't mirror the raw browser/calendar timeline; this is intentional.
Expand Down
28 changes: 28 additions & 0 deletions docs/adr/0004-deterministic-pattern-engine-and-grow-first-fill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
status: accepted (amends ADR-0002)
---

# Deterministic pattern engine and grow-first fill

[ADR-0002](0002-deterministic-day-packer.md) deliberately kept trend/pattern judgment in the LLM (it rejected a pure deterministic packer because it "can't fit something based on last week's themes when unsure"). In practice the LLM over-detected patterns — it eyeballed weekly intervals from a text list of historical bookings and emitted a large `patternBlocks` fill list, so days filled up mostly from trends and genuine observed work got drowned out. We are moving pattern detection to deterministic TypeScript (a **hybrid** engine) and changing how the day is filled to the target.

The same reasoning ADR-0002 used to keep _placement_ deterministic ("LLMs are unreliable at no-overlap arithmetic and hitting exactly 8h") applies to pattern detection: "booked in ≥3 of the last 4 weeks on a matching cadence" and "historical average duration" are arithmetic, not judgment.

## Decision

- **Hybrid pattern engine.** TypeScript computes, per project+service over the last 4 weeks: occurrence count, whether its cadence lands on the target date, average duration, and historical daily share. The LLM may only _select and label_ from this computed list — it no longer invents `patternBlocks` or estimates durations.
- **A trend is the lowest-priority source.** It never creates or relabels observed work. It only reaches the 8h fill target, in this order:
1. **Grow first** — distribute the gap to 8h across the day's existing observed project blocks, proportional to each project+service's historical share (blocks with no history get an equal share).
2. **Then strong-pattern fill** — only if still short, add loose fill blocks for **strong recurring patterns** (booked ≥3 of last 4 weeks on matching cadence). This is the only path by which a project with _no_ observed activity that day may appear.
- The 8h target remains a floor, not a ceiling (unchanged from ADR-0002): observed work is never trimmed to stay under it.

## Considered options

- **Keep the LLM detecting patterns, just with stricter prompt rules** (count weeks, require cadence). Rejected: still arithmetic done by an LLM at temperature 0.1 over a truncated text list — the exact unreliability ADR-0002 cites for placement.
- **Fully deterministic patterns, no LLM involvement.** Rejected: contextual fit ("does this recurring booking actually make sense today?") is judgment worth keeping, so the LLM selects/labels from the computed candidates.

## Consequences

- The `classify-day` prompt no longer asks for a ranked `patternBlocks` fill list with `estimatedHours`/confidence; that contract is replaced by a TS-computed candidate list the LLM picks from. The prompt skill (`uren-classificatie`) and `GeminiRepository.classifyDay` change accordingly.
- `PackDayUseCase` gains the grow-first fill logic and the historical-share inputs; the old "ranked fill candidates until target" loop (and the ADR-0002 note that confidence ≥2 candidates are "added regardless") is superseded.
- "Fill candidate" disappears from the domain language, replaced by **Trends** + **Strong recurring pattern** (see CONTEXT.md).
Loading
Loading