From 1cb05f468914854904ab01116ad1d6e7730106f8 Mon Sep 17 00:00:00 2001 From: Dave Date: Sat, 19 Sep 2026 08:10:55 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=20Phase=207=20=E2=80=94=20logged=20?= =?UTF-8?q?work=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design for letting the owner record off-repo work as a commits row tagged source=manual, reusing every existing commit render path (recent work, card history, activity/monthly aggregation) instead of a parallel system, with a declared-hours figure added to the total rather than session-clustered, and a small "Logged" tag distinguishing it from a real commit everywhere it appears. Co-Authored-By: Claude Sonnet 5 --- ...9-19-phase-7-logged-work-entries-design.md | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-19-phase-7-logged-work-entries-design.md diff --git a/docs/superpowers/specs/2026-09-19-phase-7-logged-work-entries-design.md b/docs/superpowers/specs/2026-09-19-phase-7-logged-work-entries-design.md new file mode 100644 index 0000000..3b485ff --- /dev/null +++ b/docs/superpowers/specs/2026-09-19-phase-7-logged-work-entries-design.md @@ -0,0 +1,162 @@ +# Phase 7: Logged Work Entries — Design + +## Problem + +The dashboard's hours and activity figures are entirely derived from git commit +timestamps. Real work that never touches the repo — third-party configuration, +meetings that produced a decision, non-code work done for a client — is +invisible to it, even though it happened. + +## Goals + +- Let the owner record a block of off-repo work with a message, a date, and a + declared number of hours. +- Have it appear alongside real commits everywhere commits already appear — + the dashboard's recent-work feed, the activity heatmap, monthly totals, and + (optionally) a card's commit history — by reusing that machinery rather than + building a parallel one. +- Keep the totals honestly explainable. A logged entry's hours are exactly + what the owner typed, added to the total, and the methodology panel says so. +- Keep it visually distinguishable from a real commit. A small "Logged" tag + replaces where a commit hash would be — there is no diff behind these + entries, and the dashboard does not pretend there is. + +## Non-goals + +- No new collection. Logged entries are rows in the existing `commits` + collection, distinguished by one new field, so every existing render path + (recent work, card commit history, activity/monthly/type aggregation) works + on them for free. +- No session-clustering for logged entries. A single declared block of time + cannot be inferred from timestamp gaps the way real commit sessions are — + the owner's declared hours are used directly instead. +- No batch AI summarization queue for these. The owner polishes or writes the + message once, at creation time; it never enters the same pending-commits + queue real ingested commits use, and is never rewritten later. +- No editing or deleting a logged entry after creation in this phase. If one + is wrong, deleting it directly in the database is an acceptable stopgap — + an edit/delete UI can be a follow-up if it turns out to be needed often. + +## Data model + +Two additions to the `commits` collection: + +- `source`: select, `"git" | "manual"`, default `"git"`. Backfilled to + `"git"` for every existing row, so nothing already ingested is + misclassified. +- `manual_minutes`: number, only set (and only meaningful) when + `source = "manual"`. + +A logged entry otherwise reuses existing `commits` fields: `subject`/`summary` +carry the message (identical to how a real commit's one-liner is displayed), +`authored_at` is the date the owner picks, `card` is the existing optional +relation to a card (reused as-is — a logged entry can be linked to a card +exactly like a real commit can), `type`/`scope` are optional and left blank +unless the owner wants the entry counted in the type/feature-area breakdowns. + +`sha` still has to be non-empty and unique per project (existing unique index +`(project, sha)`), so a logged entry is given a synthetic value like +`manual-` — guaranteed not to collide with a real 40-hex +git sha. `is_merge` is always `false`. `link_source` is set to `"manual"` +(joining the existing `CommitLinkSource` union) so a card's linked-commit +query does not need to special-case anything to include it. + +## Metrics: where the hours go + +`computeProjectMetrics` (`src/lib/metrics/compute.ts`) currently takes +`MetricCommit[]` — `{ type, scope, authoredAt, isMerge }` — and is pure (no +database, no clock). It gains one more optional field: + +```ts +export interface MetricCommit { + type: string; + scope: string; + authoredAt: string; + isMerge: boolean; + /** Present only for a manually logged entry. */ + manualMinutes?: number; +} +``` + +Every `MetricCommit` — git-derived or manual — still feeds the `byType`, +`byScope`, `activity` (heatmap), and `monthly` aggregations exactly as today, +since those only ever read `type`/`scope`/`authoredAt`/`isMerge`. Only the +**session-clustering** call is filtered to git-derived entries: + +```ts +const clusterable = commits.filter((c) => c.manualMinutes === undefined); +const sessions = clusterSessions(clusterable.map((c) => ({ authoredAt: c.authoredAt })), constants); +const manualMinutes = commits.reduce((sum, c) => sum + (c.manualMinutes ?? 0), 0); +``` + +`estimatedMinutes` becomes the clustered total plus `manualMinutes`. Nothing +about the clustering math changes for real commits — a project with zero +logged entries computes byte-for-byte the same numbers it does today. + +`src/lib/metrics/store.ts` (`recomputeMetrics`) is the only caller; it already +queries `commits` with a `fields` projection, which gains `source` and +`manual_minutes`. Both `manual_minutes` (stored) and `manualMinutes` (the +`MetricCommit` field) are plain minutes throughout — the same unit +`estimatedMinutes` already uses — so no conversion happens anywhere in this +path; the value the owner typed in hours is only ever multiplied by 60, once, +in the form's submit handler before it reaches the server action. + +`src/components/dashboard/methodology.tsx` gains one line when +`manualMinutes > 0`: stating how many hours were logged directly rather than +estimated from commit timing. This is the "keep it explainable" requirement — +the panel already exists specifically to state how the numbers were produced. + +## Rendering + +**`src/components/dashboard/recent-activity.tsx`** and +**`src/components/card/commit-history.tsx`** both currently type their prop as +`CommitRecord[]` and read `commit.summary || commit.subject`, which already +works unmodified for a manual row. Each gains one conditional: when +`commit.source === "manual"`, render a small tag reading "Logged" (its own +distinct color, not reused from an existing status color, so it reads as its +own category at a glance) in the position a hash or icon would otherwise go. +In `CommitHistory`, the "Show technical detail" toggle (which displays +`short_sha` and links via `commit.url`) is hidden entirely for a manual row — +there is no real diff or hash behind it, and showing a fabricated one would +reintroduce the exact problem this design avoids. + +## Creating an entry + +A new **"Log work"** button on the dashboard TopBar, beside the existing +Refresh/Recompute buttons, following the same pattern +(`src/components/dashboard/recompute-button.tsx`): owner-gated, a client +component with `useTransition`, calling a new server action. + +The action, `logWorkAction` in `src/app/actions/log-work.ts`: requires an +owner session (same `requireOwner()` guard used throughout +`src/app/actions/*.ts`), validates the date and hours, generates the synthetic +sha, and creates the `commits` row directly. + +The form itself: a date field (defaults to today), an hours field (a plain +number — "3.5" — per the earlier decision to keep this a direct declared +figure rather than inferring it from a start/end time), a message textarea, +and an optional "Polish with AI" button. That button calls a new, small, +single-shot helper — `polishNote(text: string): Promise` in +`src/lib/ai/polish.ts` — built from the same provider/system-prompt/ +`stripThinking` pieces `summarise.ts` already uses, but as one direct +`provider.complete()` call rather than the batch/DB-coupled pipeline +`summariseProject` is built around (that pipeline reads pending rows from the +database and writes results back in batches; a manual note is a single piece +of text being composed interactively, so it needs the simpler shape). Per the +`max_tokens` lesson from Phase 3 (`docs/superpowers/plans/...` — MiniMax's +reasoning is billed against the cap), this call uses `maxTokens: 16384`, not a +smaller number that looks sufficient for one sentence. + +An optional card-link dropdown lets the owner attach the entry to a specific +card, reusing the same relation real commits use — entirely optional, and +omitted it just means the entry only shows up in the project-level feed, not +on any card. + +## Permissions + +Unchanged from the rest of the app. Creating a logged entry is owner-only, +enforced server-side in `logWorkAction`, the same way every other +owner-restricted write is. A client manager's read access to `commits` is +already scoped by the existing Phase 1 row rules; a logged entry is visible to +them under the exact same rule a real commit is, with no new rule and no new +exception.