227 lines
11 KiB
Markdown
227 lines
11 KiB
Markdown
# Weekly Report — Design
|
||
|
||
**Date:** 2026-06-03
|
||
**Status:** Approved (pending spec review)
|
||
|
||
## Goal
|
||
|
||
Generate a short, standup-focused report of what the user did over the past week,
|
||
for the Wednesday standup. The report is built from the user's Claude Code session
|
||
history across all repos, distilled and summarized by Claude. Personal repos under a
|
||
configurable excluded path (default `C:\Private`) are left out. The user can author
|
||
per-day bullet notes inside ClaudeDo (via the My Day list) that are folded into the
|
||
report.
|
||
|
||
## Decisions (from brainstorming)
|
||
|
||
- **Data source:** all Claude Code history in `~/.claude/projects/*/*.jsonl`, both manual
|
||
sessions and ClaudeDo-run tasks, grouped by repo.
|
||
- **Exclusion:** a configurable list of path prefixes (default `["C:\\Private"]`). Any
|
||
session whose `cwd` starts with an excluded prefix is dropped.
|
||
- **Summarization:** Claude CLI summarizes. The Worker distills the logs, then runs a
|
||
single one-shot `claude -p` call via the existing `ClaudeProcess` and returns the
|
||
result markdown. No worktree, no task row, no queue.
|
||
- **Period:** default "since last Wednesday → today", computed from a configurable
|
||
standup weekday. The range is adjustable in the modal.
|
||
- **Signal fed to Claude:** user prompts (intent), assistant closing summaries, and the
|
||
user's daily notes. No git-commit scanning.
|
||
- **Report shape:** German, grouped by day, first-person past-tense bullets, ~3-5
|
||
bullets/day with trivia merged/dropped, notes blended into one deduplicated list per
|
||
day. See the Report Prompt section.
|
||
- **Placement:** a "Weekly Report" overlay modal opened from the toolbar, rendering via
|
||
the existing `MarkdownView`.
|
||
- **Output:** view-only in-app (no export).
|
||
- **Notes UI:** authored in the My Day list via a pinned non-task "Notes" pseudo-row that
|
||
repurposes the Details island into a bullet-notes editor. Per-day bullets with a day
|
||
navigator (prev/next arrows + date picker + Today).
|
||
- **Report persistence:** generated reports are stored, keyed by exact date range, and
|
||
reused. Generation is button-driven (never automatic); a Regenerate button overwrites.
|
||
|
||
## Architecture Overview
|
||
|
||
```
|
||
UI (WeeklyReportModal, Details-island notes mode)
|
||
│ SignalR
|
||
▼
|
||
WorkerHub ── GetWeekReport / GenerateWeekReport / daily-notes CRUD
|
||
│
|
||
├── WeekReportService ──► ClaudeHistoryReader (scan ~/.claude/projects)
|
||
│ │ (distilled activity)
|
||
│ ├── DailyNoteRepository (notes in window)
|
||
│ ├── ClaudeProcess (one-shot summarize)
|
||
│ └── WeekReportRepository (store/reuse)
|
||
└── DailyNoteRepository (CRUD)
|
||
|
||
Data: DailyNoteEntity, WeekReportEntity + repositories + EF migration
|
||
AppSettingsEntity: ReportExcludedPaths, StandupWeekday
|
||
```
|
||
|
||
## Components
|
||
|
||
### 1. Data layer (`ClaudeDo.Data`)
|
||
|
||
**`DailyNoteEntity`** (table `daily_notes`)
|
||
- `Id` (GUID string, init-only PK)
|
||
- `Date` (date-only; the day the bullet belongs to)
|
||
- `Text` (string, the bullet content)
|
||
- `SortOrder` (int; ordering within a day)
|
||
- `CreatedAt` (DateTime)
|
||
|
||
**`DailyNoteRepository`** (async, CancellationToken, follows existing repo pattern)
|
||
- `ListByDayAsync(DateOnly day)` — bullets for one day, ordered by `SortOrder`.
|
||
- `ListBetweenAsync(DateOnly start, DateOnly end)` — bullets in a window (used by the report).
|
||
- `AddAsync(DateOnly day, string text)` — appends a bullet (assigns next `SortOrder`).
|
||
- `UpdateAsync(string id, string text)`
|
||
- `DeleteAsync(string id)`
|
||
|
||
**`WeekReportEntity`** (table `week_reports`)
|
||
- `Id` (GUID string, init-only PK)
|
||
- `StartDate`, `EndDate` (date-only; the report window — unique together)
|
||
- `Markdown` (string; the generated report)
|
||
- `GeneratedAt` (DateTime)
|
||
|
||
**`WeekReportRepository`**
|
||
- `GetByRangeAsync(DateOnly start, DateOnly end)` — stored report for an exact range, or null.
|
||
- `UpsertAsync(DateOnly start, DateOnly end, string markdown)` — insert or overwrite by range.
|
||
|
||
**`AppSettingsEntity`** — two new columns:
|
||
- `ReportExcludedPaths` (string, JSON array of path prefixes; default `["C:\\Private"]`)
|
||
- `StandupWeekday` (int, `DayOfWeek`; default `Wednesday` = 3)
|
||
|
||
**Migration** — one EF migration adds `daily_notes`, `week_reports`, and the two
|
||
`app_settings` columns. Entity configs in `Configuration/` (date-only and enum/JSON
|
||
conversion via `ValueConverter`, per existing convention).
|
||
|
||
### 2. Worker (`ClaudeDo.Worker`) — new `Report/` folder
|
||
|
||
**`ClaudeHistoryReader`** (raw → distilled)
|
||
- Input: date window + excluded path prefixes.
|
||
- Enumerates `~/.claude/projects/*/*.jsonl`.
|
||
- Parses each line as JSON; tolerant of malformed lines (skip, never throw).
|
||
- Drops a session entirely if its `cwd` starts with any excluded prefix
|
||
(case-insensitive, normalized separators).
|
||
- Keeps messages whose `timestamp` falls in `[start, end]`.
|
||
- Extracts, per repo (`cwd`) → per day:
|
||
- **user prompts**: `type == "user"` text content (string or `content[].text`).
|
||
Skip tool-result-only user turns and queue/attachment/hook noise.
|
||
- **assistant closing summaries**: the final assistant text block of each turn/session.
|
||
- Output: a structured model, e.g.
|
||
`IReadOnlyList<RepoActivity>` where `RepoActivity { RepoPath, Days: List<DayActivity{ Date, Prompts[], Summaries[] }> }`.
|
||
|
||
**`WeekReportService`** (distilled → stored summary)
|
||
- `GenerateAsync(start, end, ct)`:
|
||
1. Read settings (excluded paths, standup weekday).
|
||
2. `ClaudeHistoryReader` → distilled activity.
|
||
3. `DailyNoteRepository.ListBetweenAsync` → notes grouped by day.
|
||
4. Pivot the distilled activity (repo→day from the reader) into **day-major**
|
||
(day→repo) to match the day-grouped report, and build the prompt from the
|
||
template in the Report Prompt section. Empty window → produce a "no activity"
|
||
report without calling Claude.
|
||
5. Run `ClaudeProcess` once (`claude -p`, no worktree/agents; working dir = a neutral
|
||
dir). Read `RunResult.ResultMarkdown`.
|
||
6. `WeekReportRepository.UpsertAsync(start, end, markdown)`; return markdown.
|
||
7. On Claude failure, surface `RunResult.ErrorMarkdown` to the caller (do not store).
|
||
- `GetStoredAsync(start, end)` → `WeekReportRepository.GetByRangeAsync`.
|
||
|
||
Interfaces live in `Report/Interfaces/` per the area convention.
|
||
|
||
#### Report Prompt
|
||
|
||
`WeekReportService` assembles this prompt. Instructions are in English (more reliable
|
||
steering); the output is forced to German. `{...}` are filled at build time.
|
||
|
||
```
|
||
You are generating a concise weekly standup report for a software developer.
|
||
Summarize what they accomplished between {start:dd.MM.yyyy} and {end:dd.MM.yyyy}.
|
||
|
||
Rules:
|
||
- Write the ENTIRE report in German.
|
||
- Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has
|
||
activity (German weekday names). Omit days with no activity entirely.
|
||
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt",
|
||
"- Y behoben"). Merge related small work into one bullet.
|
||
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
|
||
- Blend the developer's own notes and the derived activity into ONE deduplicated
|
||
bullet list per day. The developer's notes are authoritative — never omit or
|
||
contradict their substance.
|
||
- Name the project/repo when it adds clarity.
|
||
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
|
||
|
||
== Activity (from session history) ==
|
||
{day-major: for each day → for each repo → its prompts + closing summaries}
|
||
|
||
== Developer notes ==
|
||
{day-major: for each day → the bullets}
|
||
```
|
||
|
||
### 3. IPC (Hub + WorkerClient)
|
||
|
||
**`WorkerHub`** new methods:
|
||
- `GetWeekReport(string startIso, string endIso)` → stored markdown or null.
|
||
- `GenerateWeekReport(string startIso, string endIso)` → generates, stores, returns markdown.
|
||
- `GetDailyNotes(string dayIso)` → bullets for a day.
|
||
- `AddDailyNote(string dayIso, string text)` → created bullet.
|
||
- `UpdateDailyNote(string id, string text)`.
|
||
- `DeleteDailyNote(string id)`.
|
||
|
||
**`WorkerClient`** (UI) mirrors these, following the existing
|
||
`WorkerPrimeScheduleApi`/AppSettings method pattern.
|
||
|
||
### 4. UI (`ClaudeDo.Ui`)
|
||
|
||
**Weekly Report modal** (`WeeklyReportModalView` + `WeeklyReportModalViewModel`)
|
||
- Overlay modal in the `Modals/` pattern (like `WorktreesOverviewModalView`),
|
||
registered in `IslandsShellViewModel`, opened from a new toolbar button.
|
||
- Date range: two `ThemedDatePicker`s, default "since last Wednesday → today" computed
|
||
from `StandupWeekday`.
|
||
- On open and on range change: call `GetWeekReport`.
|
||
- Stored report exists → render markdown via `MarkdownView`, show `GeneratedAt`, show
|
||
a **Regenerate** button.
|
||
- None → empty state ("Not generated yet") + a **Generate** button.
|
||
- **Generate**/**Regenerate**: call `GenerateWeekReport` with a busy/spinner state;
|
||
render the returned markdown. Generation only ever runs from these buttons.
|
||
- View-only; no export.
|
||
|
||
**Notes in My Day**
|
||
- The My Day smart list (`smart:my-day`) pins a fixed, non-task "Notes" pseudo-row at
|
||
the top, recognized by the list/selection code (not a `TaskEntity`).
|
||
- Selecting it puts the **Details island** into **notes mode** (task fields hidden,
|
||
notes editor shown). The island hosts a dedicated `NotesEditorViewModel` + small view
|
||
rather than swelling `DetailsIslandViewModel` (already ~978 lines); the bullet logic
|
||
stays isolated and testable.
|
||
- **Day navigator** in the editor header: `<` / `>` arrows to step days, a
|
||
`ThemedDatePicker` to jump to any date, and a "Today" button. Defaults to today; the
|
||
pinned row's default day rolls over at midnight (no data lost — past days remain
|
||
reachable via the navigator).
|
||
- **Bullet editing** for the selected day: list of bullets with add / inline-edit /
|
||
delete / reorder (`SortOrder`). Each operation goes through the daily-notes hub CRUD.
|
||
|
||
### 5. Settings
|
||
|
||
- Add the excluded-path list and the standup weekday to the existing Settings modal,
|
||
persisted via the new `app_settings` columns and the existing
|
||
`GetAppSettings`/`UpdateAppSettings` path.
|
||
|
||
## Error Handling
|
||
|
||
- Malformed/unreadable JSONL lines are skipped, never fatal.
|
||
- Empty window → a "no activity" report, no Claude call.
|
||
- Claude call failure → error surfaced in the modal; nothing stored.
|
||
- Date ranges normalized to date-only; the stored report key is the exact (start, end).
|
||
|
||
## Testing
|
||
|
||
- **`ClaudeHistoryReader`** (Worker tests, fixture `.jsonl`): date-window filtering,
|
||
excluded-prefix dropping (case/separator normalization), prompt/summary extraction,
|
||
malformed-line tolerance, repo/day grouping.
|
||
- **`WeekReportService`**: prompt-building from distilled activity + notes; empty-window
|
||
short-circuit; storage upsert; with a faked `ClaudeProcess`.
|
||
- **`DailyNoteRepository`** and **`WeekReportRepository`**: CRUD / upsert / range lookup
|
||
against real SQLite (matches existing test style).
|
||
|
||
## Out of Scope
|
||
|
||
- Report export (clipboard/file) — view-only for now.
|
||
- Git-commit scanning.
|
||
- Editing or summarizing full transcripts; only prompts + closing summaries are used.
|