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
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## Context

Researched directly against this codebase before writing any code:
`change-timeline.ts`'s existing git primitives (`blameLineDates`,
`getFileCreatedDate`) capture timestamps only, never author identity;
no PDF library exists anywhere in this monorepo (`grep`-confirmed
across all 6 `package.json` files, not assumed); `rest.ts` is JSON-only
today, but `static.ts`'s `serveFile` already proves a raw-`Buffer`
response works cleanly in this server.

## Goals / Non-Goals

**Goals:**
- Reuse `getChangeTimeline`/`getChangeTimelines` unchanged for
per-change data — this change adds authorship and PDF rendering
only, not a second copy of the timeline data layer.
- A user-specified date range (the user explicitly asked to set sprint
start/end dates), unlike the multi-change comparison view's
auto-derived range.
- No heavy PDF pipeline (headless browser, HTML-to-PDF) — `pdfkit` is
pure JS with no browser/native dependency.

**Non-Goals:**
- Not the VS Code command (`add-sprint-report-vscode-command`, next) —
this change is standalone-only.
- Not visual polish (custom fonts, a logo, tables/charts) — a plain,
readable structured document is enough for v1, consistent with this
project's existing bias elsewhere (e.g. the multi-change timeline's
plain-CSS-position choice over a charting library).
- Not perfect markdown fidelity in the "Why" excerpt — a regex-based
strip of the most common syntax (code spans, bold, links), not a full
AST parse. `renderMarkdown` (webui) was deliberately not reused here:
it renders to React elements for a browser DOM, not plain text for a
PDF library running in Node.

## Decisions

### Primary author = most recent commit touching the change's directory

Not "who created it" (the earliest commit, which `getFileCreatedDate`
already answers) — chosen because this repository's own squash-merge
convention means there is often exactly one commit per change anyway,
and for any repository with a longer history, the most recent commit
is the most representative single answer to "who shipped this."
`contributors` (every distinct author by email, oldest to newest)
covers the fuller picture without forcing a choice for callers that
want it.

### Date range filters tasks/stats, not which changes appear

The user already chose which changes belong in the report via the
existing multi-select picker (`ChangeTimelineRequestEntry[]`, reused
unchanged). Excluding a selected change from its own requested report
just because it started a day before the range would be a surprising,
unwanted behavior — the range instead determines which of a change's
completed tasks count toward `tasksCompletedInRange`/the sprint totals,
and is shown as the report's header.

### `pdfkit`, piped through a buffering `Promise`

`pdfkit` has no built-in promise/buffer API — `PDFDocument` is a
Node `Readable` stream. `renderSprintReportPdf` collects `"data"`
chunks into an array and resolves `Buffer.concat(chunks)` on `"end"`,
the standard pattern for this library (confirmed via its own type
definitions and documented usage, not guessed).

### The REST endpoint returns a raw PDF, not JSON-wrapped base64

Matches `static.ts`'s existing `res.writeHead(200, {"content-type":
...}); res.end(buffer)` shape rather than inventing a
JSON-with-base64-payload convention — a real `content-type:
application/pdf` response lets `response.blob()` work directly on the
client side, and is what any HTTP client (including a future CLI use)
would expect from a "download a PDF" endpoint.

## Risks / Trade-offs

- **[Risk]** `pdfkit` is a new supply-chain dependency (plus its
`@types/pdfkit` companion). → **Mitigation**: it is a long-established,
widely-used, pure-JS library with no native bindings or browser
dependency — the lowest-risk category of new dependency for exactly
this need, and `npm audit --omit=dev --audit-level=high` (this
repository's actual CI gate) reports zero vulnerabilities with it
installed.
- **[Risk]** The plain-text "Why" excerpt is a best-effort regex strip,
not a real markdown parser — unusual markdown (nested formatting,
tables inside the Why section) could render awkwardly. →
**Mitigation**: accepted for v1; the excerpt is a summary aid, not
the full proposal, and every proposal in this repository's own
history uses simple prose in its Why section.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## Why

Following up on the 2026-08-26/27 product-direction discussion: the
user asked for a downloadable "sprint summary" PDF report — given a
start/end date, who did what (from git), when, task completion, plus
final aggregate statistics. This is the first (standalone-only) half;
`add-sprint-report-vscode-command` (next) adds the VS Code command.

## What Changes

- Add `getChangeAuthorship(cwd, changeDirPath)` to
`packages/core/src/change-timeline.ts`: a new git primitive (`git log
--format=%an\x1f%ae\x1f%aI`) returning the primary author (most
recent commit touching the change's directory — this repository's own
squash-merge convention makes this the most representative single
answer to "who did this") and every distinct contributor. No existing
code in this repository extracted author name/email before this —
only commit timestamps (`author-time`/`%aI`).
- Add `packages/core/src/sprint-report.ts`
(`buildSprintReport(workspaceRoot, entries, rangeStart, rangeEnd)`):
aggregates `getChangeTimeline`/`getChangeAuthorship` per selected
change into a `SprintReport` (per-change author/dates/task counts/a
plain-text "Why" excerpt, plus totals and a per-author breakdown).
The date range filters which *tasks* count toward the sprint's stats,
not which *changes* appear — the user already chose those explicitly
via the existing change picker.
- Add `packages/core/src/sprint-report-pdf.ts`
(`renderSprintReportPdf(report)`, via the new `pdfkit` dependency): a
plain, structured PDF — no tables/graphics/custom fonts, matching
this project's existing "plain and functional over polished" bias.
- Add `POST /api/sprint-report` in `packages/server`, returning
`application/pdf` with `content-disposition: attachment` — the first
non-JSON response this server's REST layer has ever sent (mirrors
`static.ts`'s existing raw-Buffer response pattern, not `sendJson`).
- Add a third "Sprint report" mode to the standalone Timeline tab
(alongside "Single change"/"Compare changes"), reusing the same
date-range + multi-select UI pattern already built for "Compare
changes." A "Download PDF" button fetches the PDF as a `Blob` and
triggers a browser download (`URL.createObjectURL` + a temporary
`<a download>` — no existing precedent in this codebase, a standard
browser pattern).

## Capabilities

### Modified Capabilities

- `execution-core`: adds a Requirement for sprint report generation
(authorship, aggregation, PDF rendering).
- `standalone-app`: adds a Requirement for the Sprint report mode.

## Impact

- `packages/core/src/change-timeline.ts` (new `getChangeAuthorship`)
- `packages/core/src/sprint-report.ts` (new)
- `packages/core/src/sprint-report-pdf.ts` (new)
- `packages/core/package.json` (new dependency: `pdfkit`,
`@types/pdfkit`)
- `packages/server/src/rest.ts`, `server.ts`
- `packages/webui/src/sprint-report-client.ts` (new)
- `packages/webui/src/standalone-entry.tsx`
- `.changeset/*.md` (new changeset file)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## ADDED Requirements

### Requirement: Sprint report generation

The system SHALL generate, for a given set of changes and a date
range, a sprint summary containing each change's best-effort git
authorship, dates, task completion, and a plain-text summary, plus
aggregate statistics (total changes, tasks completed within the range,
and a per-author change count), rendered as a PDF document.

#### Scenario: Authorship for a change with a single commit

- **WHEN** authorship is determined for a change whose directory has
exactly one commit touching it
- **THEN** that commit's author is reported as both the primary author
and the sole contributor

#### Scenario: Authorship for a change with multiple commits by different authors

- **WHEN** authorship is determined for a change touched by commits
from more than one author
- **THEN** the most recent commit's author is reported as the primary
author, and every distinct author is listed among the contributors

#### Scenario: A task completed within the requested range

- **WHEN** a selected change's task was completed (per its best-effort
date) within the requested date range
- **THEN** it counts toward that change's and the report's total
tasks-completed-in-range figure

#### Scenario: A selected change started before the requested range

- **WHEN** a user explicitly selects a change for the report whose
created date falls before the requested range
- **THEN** the change still appears in the report; only its
task-completion counts are filtered by the range

#### Scenario: Authorship is undeterminable

- **WHEN** git history for a change's directory is unavailable or
yields no commits
- **THEN** the report includes the change with no primary author or
contributors, rather than failing to generate
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## ADDED Requirements

### Requirement: The Timeline tab offers a downloadable sprint report

The system SHALL offer, within the Timeline tab, a mode where the user
picks a date range and multiple active and/or archived changes, then
downloads a generated PDF sprint report as a browser file download.

#### Scenario: User generates a sprint report

- **WHEN** the user selects a date range and one or more changes in the
Sprint report mode and starts the download
- **THEN** a PDF file download begins, named after the selected range

#### Scenario: User has not selected a range or any changes

- **WHEN** the user attempts to generate a report without a complete
date range or without selecting any change
- **THEN** the system reports what is missing rather than attempting
to generate an empty or partial report
69 changes: 69 additions & 0 deletions openspec/changes/archive/2026-08-27-add-sprint-report-pdf/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
## 1. Core: authorship, aggregation, PDF rendering

- [x] 1.1 Add `getChangeAuthorship(cwd, changeDirPath)` to
`change-timeline.ts` (`CommitAuthor`, `ChangeAuthorship` types) — a
new `git log --format=%an\x1f%ae\x1f%aI` primitive, graceful
degradation on failure (matching every other function in the file).
- [x] 1.2 Add tests against a real temp git repo fixture: primary
author is the most recent commit's author, `contributors` lists
every distinct author (by email) oldest to newest, empty authorship
when git fails or the directory has no history.
- [x] 1.3 Add `pdfkit` + `@types/pdfkit` to `packages/core/package.json`.
- [x] 1.4 Add `packages/core/src/sprint-report.ts`
(`buildSprintReport`) — reuses `getChangeTimeline`/
`getChangeAuthorship` unchanged; date-range filters tasks/stats, not
which changes appear (see design.md).
- [x] 1.5 Add tests: authorship/task-count aggregation, date-range
task filtering, a change kept even when it started before the range,
`changesByAuthor` ranked by count descending, empty report for no
entries.
- [x] 1.6 Add `packages/core/src/sprint-report-pdf.ts`
(`renderSprintReportPdf`) using `pdfkit`, buffered via the
collect-chunks-on-`"end"` pattern.
- [x] 1.7 Add a test confirming a real PDF buffer (`%PDF-` magic bytes,
`%%EOF` trailer) comes back, including for an empty report.
- [x] 1.8 Export both new modules from `index.ts` (Node-only — not
`browser.ts`, since both use git/Node streams).

## 2. Server: REST endpoint

- [x] 2.1 Add `sendPdf(res, buffer, filename)` to `rest.ts`, mirroring
`sendJson`'s shape (adds `content-disposition: attachment`).
- [x] 2.2 Add `handleSprintReportRequest` (body:
`{cwd, entries, rangeStart, rangeEnd}`) calling `buildSprintReport`
then `renderSprintReportPdf`.
- [x] 2.3 Wire `POST /api/sprint-report` in `server.ts`.
- [x] 2.4 Add tests: a real PDF comes back with the correct
`content-type`/`content-disposition`; a request missing the date
range is rejected with 400.

## 3. Webui: Sprint report mode

- [x] 3.1 Add `packages/webui/src/sprint-report-client.ts`
(`fetchSprintReportPdf`, returns a `Blob`) and its test.
- [x] 3.2 Add a third "Sprint report" mode to the Timeline tab in
`standalone-entry.tsx`, reusing the existing date-range/multi-select
state from "Compare changes"; a "Download PDF" button triggers the
browser download (`URL.createObjectURL` + a temporary `<a
download>`).

## 4. Verification

- [x] 4.1 `npm run typecheck` and `npm run lint` (including
`lint:english`) pass workspace-wide.
- [x] 4.2 `npm run test` passes workspace-wide, including all new test
files.
- [x] 4.3 `npm audit --omit=dev --audit-level=high` (this repository's
actual CI gate) still reports zero vulnerabilities with `pdfkit`
installed.
- [x] 4.4 Manual smoke test: started the standalone server against
this repository itself, requested a real sprint report for five of
today's/yesterday's own real archived changes, confirmed a valid PDF
(`file` reports "PDF document, version 1.3, 2 page(s)") with correct
real data — actual proposal excerpts, dates, git author, and correct
totals/per-author statistics.
- [x] 4.5 Propose a changeset (`npx changeset`) for `@openspec-ui/core`,
`@openspec-ui/server`, and `@openspec-ui/webui` (all minor: new
capability, no breaking change) instead of hand-editing `version`/
`CHANGELOG.md`; apply it via `npx changeset version`.
- [x] 4.6 Run `openspec change validate --strict add-sprint-report-pdf`.
43 changes: 43 additions & 0 deletions openspec/specs/execution-core/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,46 @@ never be flagged regardless of age.
(e.g. blame unavailable)
- **THEN** the system does not report it as stale

### Requirement: Sprint report generation

The system SHALL generate, for a given set of changes and a date
range, a sprint summary containing each change's best-effort git
authorship, dates, task completion, and a plain-text summary, plus
aggregate statistics (total changes, tasks completed within the range,
and a per-author change count), rendered as a PDF document.

#### Scenario: Authorship for a change with a single commit

- **WHEN** authorship is determined for a change whose directory has
exactly one commit touching it
- **THEN** that commit's author is reported as both the primary author
and the sole contributor

#### Scenario: Authorship for a change with multiple commits by different authors

- **WHEN** authorship is determined for a change touched by commits
from more than one author
- **THEN** the most recent commit's author is reported as the primary
author, and every distinct author is listed among the contributors

#### Scenario: A task completed within the requested range

- **WHEN** a selected change's task was completed (per its best-effort
date) within the requested date range
- **THEN** it counts toward that change's and the report's total
tasks-completed-in-range figure

#### Scenario: A selected change started before the requested range

- **WHEN** a user explicitly selects a change for the report whose
created date falls before the requested range
- **THEN** the change still appears in the report; only its
task-completion counts are filtered by the range

#### Scenario: Authorship is undeterminable

- **WHEN** git history for a change's directory is unavailable or
yields no commits
- **THEN** the report includes the change with no primary author or
contributors, rather than failing to generate

19 changes: 19 additions & 0 deletions openspec/specs/standalone-app/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,22 @@ it when rendering a change's timeline.
reloads) a change's timeline
- **THEN** pending tasks are flagged stale according to the new value

### Requirement: The Timeline tab offers a downloadable sprint report

The system SHALL offer, within the Timeline tab, a mode where the user
picks a date range and multiple active and/or archived changes, then
downloads a generated PDF sprint report as a browser file download.

#### Scenario: User generates a sprint report

- **WHEN** the user selects a date range and one or more changes in the
Sprint report mode and starts the download
- **THEN** a PDF file download begins, named after the selected range

#### Scenario: User has not selected a range or any changes

- **WHEN** the user attempts to generate a report without a complete
date range or without selecting any change
- **THEN** the system reports what is missing rather than attempting
to generate an empty or partial report

Loading
Loading