first pass at the newspaper builder
Test / test (push) Has been cancelled

This commit is contained in:
2026-09-14 11:57:22 +10:00
commit bec1eaac87
497 changed files with 178953 additions and 0 deletions
@@ -0,0 +1,97 @@
# Code Generation Plan — unit: generator
> Implements the generator core: CLI entry, config + content ingestion, a light
> markdown→article parser, a column-flow multi-page A4 layout, funnies + photo
> placement, and a self-contained zero-network newspaper.html emit with a
> friendly empty-state. Per confirmed functional + NFR + infra design.
## Steps
- [ ] **Step 1: Module skeleton** — `newspaper/generator/` with `__init__.py`, `cli.py` (the `newspaper generate` entry).
- [ ] **Step 2: Test runner** — pytest (already configured); record the exact command.
- [ ] **Step 3: Content + config ingestion** — read `config.json` (MastheadConfig) + `content/` folder (BR1.1/BR1.2, US5/US7).
- [ ] **Step 4: Markdown→article parser** — parse `.md`/`.txt` into Article entities with fidelity (NFR7, US5); tolerant of empty/odd sections.
- [ ] **Step 5: Layout + pagination** — column-flow across A4 pages with page breaks at boundaries, page numbers + running headers (BR2.1, US1, NFR2).
- [ ] **Step 6: Funnies + photo placement** — embed funnies output + auto-balanced photos; text-only when photos absent (BR3.1, US3/US4).
- [ ] **Step 7: Draft handoff** — write DraftBundle JSON for the review page (loaded via native file picker, BR4.1/US9).
- [ ] **Step 8: Emit + empty-state** — write self-contained zero-network `newspaper.html`, file:// printable with A4 margins (BR5.1/BR5.3); friendly guided empty-state when no content (BR5.2/US10).
- [ ] **Step 9: Unit tests** — test-after (parse fidelity, layout/pagination, emit self-contained, empty-state).
- [ ] **Step 10: Traceability** — code-summary + traceability.json + source-manifest.
## Testing Contract
```json
{
"version": 1,
"methodology": "test-after",
"source": "team",
"ordering": "implement the transform and layout, then write and run tests that",
"scope": "classic",
"test_strategy": "standard",
"project_type": "greenfield",
"applicable_notes": [
{
"layer": "org",
"text": "We treat tests as a first-class deliverable in every Bolt. The specific\nmethodology (TDD, BDD, ATDD, or classic test-after) is affirmed at\npractices-discovery and recorded in `team.md` under this heading with explicit\n`Methodology` and `Ordering` fields; Code Generation resolves those fields\nindependently from coverage, tooling, and scope notes.\n\nWhen no posture has been affirmed, our default per scope is:\n- **Methodology**: test-after\n- **Ordering**: implement each applicable testable layer, then write and run\n that layer's tests.\n- `mvp`, `enterprise`, `feature`, `infra`, `classic` add an 80% line-coverage\n floor and CI execution before merge.\n- `bugfix`, `security-patch` add a targeted regression for the specific\n bug/vulnerability and require the existing suite to remain green.\n- `express` uses the Minimal strategy: requirement-driven unit tests (one per\n requirement, with a happy-path floor per component); existing tests remain\n green.\n- `poc`, `refactor`, `workshop` add no extra new-test floor and require the\n existing suite to remain green.\n\nThe active `Test Strategy` still applies in every scope and determines test\nvolume/types. Scope floors are additive; they never reduce or replace the\nselected strategy.\n\nBuild and Test verifies defined coverage floors and affirmed quality targets;\nthey may not be weakened to make a step pass.\n\nAffirm a stricter posture in `team.md` if the team commits to one."
},
{
"layer": "team",
"text": "Unit tests for the markdown→HTML transform. The primary testable surface is the\nrenderer: sample article markdown must always map to complete, correctly\nstructured newspaper HTML with no dropped sections or mangled escaping, and the\nprint stylesheet must keep content inside an A4 sheet. We write focused tests for\nthe transform logic rather than an exhaustive suite; the print result remains the\nreal-world acceptance check.\n\nMethodology: test-after\nOrdering: implement the transform and layout, then write and run tests that\nverify markdown→HTML fidelity and A4 print containment."
}
],
"obligations": {
"strategy": "standard",
"strategy_volume": [
"Five to eight tests per component.",
"Unit tests plus integration tests for key boundaries.",
"Add E2E, performance, or security tests when requirements demand them."
],
"scope_floor": [
"Keep the existing test suite green.",
"This scope adds no extra new-test floor beyond the selected test strategy."
],
"combination_rule": "Apply every selected-strategy obligation and every scope-floor obligation; neither replaces the other, and a targeted scope regression may add the narrowest necessary test type beyond the strategy default."
},
"plan_profile": {
"methodology": "test-after",
"runner_step": "Bootstrap the minimal test runner/configuration and record the exact unit-scoped command.",
"runner_ready_before_first_test": true,
"testable_layers": [
"Data model / database behavior",
"Repository / data access",
"Business logic",
"API / endpoint",
"Frontend behavior"
],
"steps": [
"Project structure and production configuration skeleton.",
"Bootstrap the minimal test runner/configuration and record the exact unit-scoped command.",
"Data model / database behavior - implement.",
"Data model / database behavior - write and run its tests after implementation.",
"Repository / data access - implement.",
"Repository / data access - write and run its tests after implementation.",
"Business logic - implement.",
"Business logic - write and run its tests after implementation.",
"API / endpoint - implement.",
"API / endpoint - write and run its tests after implementation.",
"Frontend behavior - implement.",
"Frontend behavior - write and run its tests after implementation.",
"Environment/build configuration.",
"Documentation and traceability."
]
},
"input_sha256": "sha256:7df1c2c0484b987ee3ac61ed4a435eddbecc0e5cf219e85e0eec73e54809a607",
"contract_sha256": "sha256:69c65f5ed93c67be42f6b31cb7ec9cb0e0c18861358949313d9d44b8f9335f99"
}
```
## Plan Approval
[Approval Fingerprint]: _to_fill_
[Planned Source]: _to_fill_
- Approve Plan — proceed to code generation
- Request Changes — revise the plan
[Answer]:
@@ -0,0 +1,11 @@
# Code Generation — Plan Approval (unit: generator)
## Plan Approval
[Approval Fingerprint]: sha256:v3:2dd76de7c20ec49a352c6865ba4dd5a67904842041213b6160c2b5a5d68af784
[Planned Source]: c87645d0501acc4574751f253afb4eed92e351ab2954a76f542bbaa813328480
- Approve Plan — proceed to code generation
- Request Changes — revise the plan
[Answer]: Approve Plan
@@ -0,0 +1,38 @@
# Code Generation Summary — unit: generator
## Files created (workspace root `newspaper/`)
- `newspaper/generator/__init__.py` — generator public API.
- `newspaper/generator/cli.py` — `main` + `run_generate` (`--project`, `--draft`).
- `newspaper/generator/model.py` — MastheadConfig, Article, Photo, Column,
ColumnItem, Page, Issue.
- `newspaper/generator/content.py` — config.json load + content/ parse (light
front-matter, markdown/txt → Article, NFR7 fidelity).
- `newspaper/generator/layout.py` — column-based multi-page A4 flow with running
headers (BR2.1/BR5.3).
- `newspaper/generator/emitter.py` — self-contained zero-network `newspaper.html`
with A4 print CSS + friendly empty-state (BR5.1/BR5.2/BR5.3).
- `newspaper/tests/test_generator.py` — 8 unit tests, all passing.
## Key decisions
- **Light dependency-free parser + layout** — no external markdown lib; front-matter
+ markdown fidelity via a tolerant hand parser (NFR7).
- **Graceful empty-state** — no content emits a friendly guided page (US10/BR5.2),
never blank.
- **AI draft optional** — `--draft` folds ai-draft in; on model unavailability the
paper builds with the human copy (BR4.1 fail-soft).
- **Zero-network self-contained emit** — `@page A4`, all assets inline, `file://`
printable (NFR4/NFR1/NFR2).
- **No fabricated photos** — no `<img>` when none supplied (FR7.4/BR3.1).
## Test coverage
- 8 generator tests + the 7 ai-draft + 7 funnies = 22 tests pass.
- Covers: config load, parse fidelity, empty-state, layout pagination, self-contained
emit, no-fake-photos, no-AI fallback, funnies embed.
## Verified end-to-end
- `python -m newspaper.generator.cli --project <sample>` writes a valid
self-contained `newspaper.html` (1198 bytes sample) with A4-printable CSS.
## Deviations
- None material. A cosmetic `python -m newspaper.generator.cli` import warning
noted (not encountered via the intended `newspaper` console entry / tests).
@@ -0,0 +1,34 @@
{
"stage": "code-generation",
"unit": "generator",
"version": 1,
"writes": [
{
"path": "newspaper/generator/__init__.py"
},
{
"path": "newspaper/generator/cli.py"
},
{
"path": "newspaper/generator/model.py"
},
{
"path": "newspaper/generator/content.py"
},
{
"path": "newspaper/generator/layout.py"
},
{
"path": "newspaper/generator/emitter.py"
},
{
"path": "newspaper/tests/test_generator.py"
},
{
"path": "newspaper/uv.lock"
},
{
"path": ".gitignore"
}
]
}
@@ -0,0 +1,40 @@
{
"stage": "code-generation",
"unit": "generator",
"upstream_ids": [
"AC1.1.1", "AC1.1.2", "AC1.1.3",
"AC4.1.3", "AC5.1.1", "AC5.1.2", "AC6.1.1", "AC6.1.2", "AC6.1.3", "AC7.1.1",
"AC10.1.1", "AC10.1.2",
"BR1.1", "BR1.2", "BR2.1", "BR2.2", "BR3.1", "BR4.1", "BR5.1", "BR5.2", "BR5.3", "BR6.1",
"NFR1", "NFR2", "NFR4", "NFR5", "NFR7"
],
"coverage": [
{ "id": "AC1.1.1", "status": "OK", "target": "newspaper/generator/layout.py" },
{ "id": "AC1.1.2", "status": "OK", "target": "newspaper/generator/layout.py" },
{ "id": "AC1.1.3", "status": "OK", "target": "newspaper/generator/layout.py" },
{ "id": "AC4.1.3", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "AC5.1.1", "status": "OK", "target": "newspaper/generator/cli.py" },
{ "id": "AC5.1.2", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "AC6.1.1", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "AC6.1.2", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "AC6.1.3", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "AC7.1.1", "status": "OK", "target": "newspaper/generator/content.py" },
{ "id": "AC10.1.1", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "AC10.1.2", "status": "OK", "target": "newspaper/generator/cli.py" },
{ "id": "BR1.1", "status": "OK", "target": "newspaper/generator/content.py" },
{ "id": "BR1.2", "status": "OK", "target": "newspaper/generator/content.py" },
{ "id": "BR2.1", "status": "OK", "target": "newspaper/generator/layout.py" },
{ "id": "BR2.2", "status": "OK", "target": "newspaper/generator/cli.py" },
{ "id": "BR3.1", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "BR4.1", "status": "OK", "target": "newspaper/generator/cli.py" },
{ "id": "BR5.1", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "BR5.2", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "BR5.3", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "BR6.1", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "NFR1", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "NFR2", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "NFR4", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "NFR5", "status": "OK", "target": "newspaper/generator/emitter.py" },
{ "id": "NFR7", "status": "OK", "target": "newspaper/generator/content.py" }
]
}
@@ -0,0 +1,25 @@
# Unit Test Instructions — unit: generator
## Test framework / config
- pytest, under `newspaper/tests/`.
## How to run THIS UNIT's tests (exact command)
```bash
uv run --project newspaper pytest newspaper/tests/test_generator.py -q
```
## Strategy / coverage (Standard)
- 6-8 unit tests: config+content ingestion, markdown→article fidelity, layout/pagination (page breaks), self-contained emit, empty-state, no-fabricated-photos.
## Mocking
- Mock ai-draft + funnies return values (empty DraftBundle / a FunniesResult) — no real model/fetch in the generator tests.
## Test cases
1. reads config.json → MastheadConfig (dynamic, US7)
2. parses a markdown article into an Article without mangling escaping (NFR7)
3. empty content → guided empty-state emitted (US10/BR5.2)
4. layout flows content into multiple pages with running headers (US1/BR2.1)
5. emitted newspaper.html is self-contained (zero http(s) refs, NFR4)
6. no photos supplied → text-only, no fabricated images (FR7.4/BR3.1)
7. AI draft unavailable (empty DraftBundle) → paper builds without AI (BR4.1)
8. funnies result embedded into the output (US3)
@@ -0,0 +1,182 @@
# Functional Design — Entities (unit: generator)
> Source of truth for the entity model of the `generator` library unit (the CLI
> orchestrator). Per confirmed answers: content/ folder + config.json (Q1=A),
> column-based flow + pagination (Q2=A), dynamic masthead from config (Q3=A),
> auto-balanced photos (Q4=A), DraftBundle via file-picker handoff (Q5=A),
> self-contained emit + guided empty-state (Q6=A), multi-page fidelity (Q7=A),
> and AI may be used to fit content into the layout (human note).
```yaml
entities:
- name: MastheadConfig
description: The dynamic masthead/metadata input read from config.json (US7, Q3=A).
attributes:
- name: title
type: string
required: true
description: Newspaper / couple name.
- name: coupleNames
type: string
required: true
description: The couple (for the masthead/subtitle).
- name: dateLine
type: string
required: true
description: Issue date.
- name: issueNumber
type: string
required: true
description: Issue number.
- name: volume
type: string
required: true
description: "Volume X".
entity_constraints:
- all fields must be non-empty for a valid masthead.
- name: Issue
description: The resolved newspaper issue the generator builds (US1, US2, US7).
attributes:
- name: issueId
type: string
required: true
unique: true
- name: masthead
type: MastheadConfig
required: true
description: Resolved from config.json (Q3=A).
- name: articles
type: list[Article]
required: true
description: Parsed content articles.
- name: pages
type: list[Page]
required: true
description: Flowed multi-page layout.
entity_constraints: []
- name: Article
description: A parsed content article placed in the newspaper (US5). Content is the store (filesystem).
attributes:
- name: articleId
type: string
required: true
unique: true
- name: type
type: string
required: true
allowed_values: [lead, article, filler, wellwish]
description: Article kind.
- name: headline
type: string
required: true
- name: byline
type: string
required: false
default: ""
- name: body
type: string
required: true
description: Markdown/txt body (may be AI-drafted or hand-written).
- name: section
type: string
required: false
description: Newspaper section.
entity_constraints:
- articleId must be unique within an Issue.
- name: Page
description: One flowed A4 page of the newspaper (US1, Q2/Q7=A).
attributes:
- name: pageNumber
type: int
required: true
unique: true
description: 1-based page number.
- name: columns
type: list[Column]
required: true
description: The page's column boxes.
- name: runningHeader
type: string
required: true
description: Running header (Q7=A).
entity_constraints:
- pageNumber must be unique within an Issue; page stays within A4 margins.
- name: Column
description: A filled column box within a page (Q2=A).
attributes:
- name: columnIndex
type: int
required: true
- name: items
type: list[ColumnItem]
required: true
description: Articles, photos, quotes, funnies blocks placed in the column.
entity_constraints: []
- name: ColumnItem
description: A placed item in a column (article, photo, pull quote, funnies output, etc.).
attributes:
- name: itemId
type: string
required: true
unique: true
- name: kind
type: string
required: true
allowed_values: [article, photo, pullquote, factbox, funnies, schedule, wellwish]
- name: sourceRef
type: string
required: true
description: Reference to the source Article / photo / funnies result.
entity_constraints: []
- name: Photo
description: An embedded photo, auto-balanced into columns (US4, Q4=A).
attributes:
- name: photoId
type: string
required: true
unique: true
- name: src
type: data
required: true
description: Locally embedded image (data/asset), never remote at print (NFR4).
- name: caption
type: string
required: false
- name: credit
type: string
required: false
entity_constraints:
- when no photos are supplied, none are fabricated or placed (FR7.4).
- name: DraftBundleRef
description: Reference to the AI-draft DraftBundle the generator writes for the review-page handoff (US9/Contract 5, Q5=A).
attributes:
- name: bundleRef
type: string
required: true
description: JSON file the review page loads via the native file picker (NFR5).
entity_constraints:
- the review page reads the DraftBundle through the user-granted file picker, never fetch of a sibling file.
```
## Human-readable entity summary
- **MastheadConfig** — the dynamic masthead metadata (title, names, date, issue,
volume) read from config.json each generation (US7/Q3=A).
- **Issue** — the top-level resolved newspaper with masthead + articles + flowed
pages.
- **Article** — a parsed content article (the filesystem contents folder is the
store).
- **Page / Column / ColumnItem** — the column-based multi-paged layout model
(Q2/Q7=A): pages flow with columns, page numbers, running headers, A4 margins.
- **Photo** — auto-balanced embedded photo with caption/credit, clean when
absent (US4/Q4=A).
- **DraftBundleRef** — the review-page handoff JSON (read via file picker).
No cross-unit relationships owned here.
@@ -0,0 +1,102 @@
# Functional Design — Questions (unit: generator)
> Fill in each `[Answer]:` tag. Options A-E plus X (Other). The file is the
> authoritative record of the generator unit's functional design decisions.
## Q1: Content ingestion shape
The generator reads content files and dynamic masthead metadata. How should content be authored/ingested (Q2=A in mockups: content/ folder + config)?
A) A `content/` folder of markdown/txt files (one per article) plus a small `config.json` holding masthead metadata (title, couple names, date, issue, volume) — recommended
B) One markdown "issue file" with front-matter holding everything
C) Paste text into the browser page + a metadata form
X) Other (please specify)
[Answer]: A
## Q2: Layout & pagination model
The generator places articles into a classic broadsheet layout that flows across A4 pages (US1, US6). How should the layout/pagination work?
A) A column-based flow model — articles/boxes fill columns row by row, breaking to a new page at defined page boundaries/section breaks, with page numbers and running headers (recommended)
B) Fixed per-page templates the generator fills, then overflow to extra pages
C) A single stream laid out by the browser's print CSS (no generator-side pagination)
X) Other (please specify)
[Answer]: A
## Q3: Masthead & dynamic metadata
How should the masthead identity (US2, US7) be resolved from config?
A) Generator reads config.json and renders the masthead (title, date line, issue, volume) dynamically each generation (recommended)
B) Masthead is hard-coded in a template; user edits it there
X) Other (please specify)
[Answer]: A
## Q4: Photo embedding
The generator embeds photos when supplied (US4), auto-balancing into columns, and renders cleanly without them when absent (FR7.4). How should photos be placed?
A) Auto-balance — embedded photos flow into the column grid sized to fit, with captions/credits; when no photos are supplied the layout renders clean, text-only (recommended)
B) Photos placed at fixed positions the user specifies
C) Photos inline in the article markdown at authoring time
X) Other (please specify)
[Answer]: A
## Q5: Review-handoff & review page data path
The generator writes the AI-draft DraftBundle and hands the review page to the couple (US9, Contract 5). Under the no-server/zero-network constraint, how should the review page receive the draft?
A) The CLI writes a DraftBundle JSON file the review page loads via the native file picker (user-granted, NFR5) — recommended
B) A short-lived localhost server serves the draft during review only (departs from no-server)
C) Merge review into the CLI prompt (departs from the confirmed HTML review page)
X) Other (please specify)
[Answer]: A
## Q6: Emit & empty-state
The generator emits the self-contained newspaper.html and handles the first-run empty state (US10). How should that behave?
A) A `newspaper generate` command emits a single self-contained, zero-network HTML; with no content it emits a friendly guided empty-state page with a sample-issue pointer (recommended)
B) Emit always, but no empty-state handling
C) A stub page only (no guidance)
X) Other (please specify)
[Answer]: A
## Q7: Multi-page print fidelity
Multi-page A4 output with clean page breaks and running headers is a must-have (US1/FR1/NFR2). What print-fidelity behaviour should the generator guarantee?
A) Page breaks at section/article boundaries where possible (never split an article awkwardly), page numbers + running headers on every page, margins within A4 (recommended)
B) A single continuous page that the browser splits (accept weaker control)
C) Fixed 2-page layout regardless of content (departs from fluid Q7)
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the seven generator functional-design answers before the unit artifacts are generated:
>
> - Content ingestion: **content/ folder of markdown/txt + config.json** holding masthead metadata (Q1=A)
> - Layout & pagination: **column-based flow model** with page breaks at boundaries/section breaks + page numbers/running headers (Q2=A)
> - Masthead metadata: **read config.json, render dynamically** each generation (Q3=A)
> - Photo embedding: **auto-balance into columns** with captions; clean text-only when none supplied (Q4=A)
> - Review-handoff: **CLI writes DraftBundle JSON, review page loads via native file picker** (NFR5) (Q5=A)
> - Emit & empty-state: **newspaper generate emits self-contained zero-network HTML** + friendly guided empty-state (Q6=A)
> - Multi-page fidelity: **page breaks at boundaries, page numbers + running headers, A4 margins** (Q7=A)
> - Additional constraint (human): **the generator may use AI to fit content into the layout**; non-deterministic is acceptable provided it gives the appearance of a newspaper.
>
> Human auto-approved this summary (answers + clarification read from the file; explicit permission granted).
Does this all look correct before I generate the unit artifacts?
- Looks correct
- Request changes
[Answer]: Looks correct
@@ -0,0 +1,99 @@
# Functional Design — Functional Specification (unit: generator)
> Behavioural specification for the `generator` library unit — source of truth
> for its workflows and state transitions. Per confirmed answers: content/
> + config ingestion, column-flow pagination, dynamic masthead, auto-balanced
> photos, file-picker DraftBundle handoff, self-contained emit + empty-state,
> multi-page fidelity, and AI may fit content into the layout.
## Context
`generator` is the core orchestrator and CLI entry point. It reads config.json +
the content/ folder, parses markdown/txt into articles, flows them into a classic
broadsheet multi-page A4 layout (columns, page numbers, running headers),
optionally triggers ai-draft and funnies, renders a self-contained zero-network
`newspaper.html`, and (when AI drafted) writes a DraftBundle for the per-article
review page handoff (US9).
## Workflow: generate a newspaper
1. `newspaper generate` reads config.json + content/ (BR1.1).
2. Generator resolves masthead metadata from config for the front page (BR1.2).
3. It parses content into Article entities, placing them into the classic
broadsheet layout (columns, page breaks at boundaries, page numbers + running
headers) across as many A4 pages as needed (BR2.1).
4. If `--draft` was given, generator invokes `ai-draft` (optional) → DraftBundle
→ per-article review page handoff (BR4.1) via the file picker.
5. It requests the funnies section from the `funnies` unit (crossword, find-a-word,
cartoon, comics) and embeds it (US3/BR2 from funnies).
6. It embeds photos if supplied, auto-balancing into columns; else renders clean
text-only (BR3.1).
7. It writes the self-contained `newspaper.html` — zero-network, file:// printable
to A4 (BR5.1), with A4 margins and clean print (BR5.3).
8. If no content, it emits a friendly guided empty-state page (BR5.2).
## State transitions — GeneratorRun
```
IDLE --newspaper generate--> LOADING
LOADING --config + content read--> PARSING
PARSING --articles parsed--> LAYOUT (column flow, pagination)
LAYOUT --funnies + photos placed--> RENDERING
RENDERING --optional AI draft handoff done--> REVIEW_HANDOFF (if --draft)
REVIEW_HANDOFF/RENDERING --final HTML written--> EMITTED
LOADING --no content--> EMPTY_STATE (friendly guided, BR5.2)
```
- **IDLE** — no generation.
- **LOADING** — reading config.json + content/.
- **PARSING** — converting content to articles.
- **LAYOUT** — column-flow + pagination + funnies + photos placement.
- **RENDERING** — building the self-contained HTML + print CSS.
- **REVIEW_HANDOFF** — writing the DraftBundle for the review page (US9, BR4.1).
- **EMITTED** — `newspaper.html` complete.
- **EMPTY_STATE** — no content; guided empty-state page (BR5.2/US10).
## Error handling & edge cases
- **No content** → guided empty-state (US10, BR5.2); not a blank/broken page.
- **AI draft unavailable** → ai-draft returns empty DraftBundle; generator builds
the paper without AI (graceful, BR4.1).
- **Funnies unavailable/empty** → funnies returns a fallback/empty FunniesResult;
generator embeds it gracefully (never a broken block).
- **No photos** → text-only, no fabricated images (FR7.4).
- **Oversize / overflow concern** → the generator *may use AI to fit content* so it
gives the appearance of a newspaper (human note; BR2.2), plus page breaks at
boundaries preserve fidelity (BR2.1/BR5.3).
- **Review page without DraftBundle** → clear pick-prompt, never a broken page
(BR6.1).
## Derived: ER diagram (from entities.md)
```mermaid
erDiagram
MastheadConfig ||--|| Issue : "resolves masthead"
Issue ||--|{ Article : "contains parsed content"
Issue ||--|{ Page : "flows across A4"
Page ||--|{ Column : "holds columns"
Column ||--|{ ColumnItem : "places items"
ColumnItem }o--|| Photo : "embeds (auto-balanced)"
ColumnItem }o--|| Article : "renders"
Issue ||--o| DraftBundleRef : "AI review handoff"
```
## Derived: rules summary (from rules.md)
| ID | Rule (one line) |
|---|---|
| BR1.1 | Read content/config (filesystem store) |
| BR1.2 | Masthead dynamic from config |
| BR2.1 | Column-flow multi-page, breaks at boundaries |
| BR2.2 | AI may fit content to the layout |
| BR3.1 | Auto-embed photos / clean text-only when absent |
| BR4.1 | AI-draft → DraftBundle via native file picker |
| BR5.1 | Self-contained zero-network HTML |
| BR5.2 | Friendly guided empty-state |
| BR5.3 | A4 print-faithful with margins |
| BR6.1 | Graceful review-handoff error state |
<!-- functional-design generator after-confirm -->
@@ -0,0 +1,15 @@
<!-- INVARIANT: examples are single-line HTML comments so a fresh template parses to total=0 (MEMORY_EMPTY). Do NOT un-comment or split across lines. t100 guards this. -->
> This file is kept up to date automatically while the stage runs. Add observations at the review step, not by editing here directly.
## Interpretations
<!-- example: 2026-05-29T10:14:32Z — chose REST over GraphQL; the consuming team only needs CRUD, revisit if subscriptions land -->
## Deviations
<!-- example: 2026-05-29T10:14:32Z — skipped the optional caching layer the stage prose suggested; the dataset is small enough that it adds risk -->
## Tradeoffs
<!-- example: 2026-05-29T10:14:32Z — picked TDD over BDD this run; the team is unit-first and the domain is well-understood -->
## Open questions
<!-- example: 2026-05-29T10:14:32Z — confirm the retention window with compliance before the next stage hardens the schema -->
2026-09-13T13:00:00Z — Interpretation — Generator functional design generated per confirmed answers: content/config ingestion, column-flow pagination, dynamic masthead, auto-balanced photos, file-picker DraftBundle handoff, self-contained emit + empty-state, multi-page fidelity. Human note: AI may fit content into the layout (non-deterministic OK, appearance of a newspaper).
@@ -0,0 +1,116 @@
# Functional Design — Business Rules (unit: generator)
> Numbered business rules for the `generator` library unit. Source of truth in
> the fenced YAML; human summary below. Per confirmed answers: content/config
> ingestion (Q1=A), column-flow pagination (Q2=A), dynamic masthead (Q3=A),
> auto-balanced photos (Q4=A), file-picker DraftBundle handoff (Q5=A),
> self-contained emit + empty-state (Q6=A), multi-page fidelity (Q7=A), and AI
> may fit content into the layout (human note).
```yaml
rules:
- id: BR1.1
statement: Read content from the content/ folder and config.json.
category: constraint
applies_to: generator
trigger: generator runs
logic: IF newspaper generate runs THEN the generator reads config.json (masthead metadata) and parses the content/ folder of markdown/txt into Article entities; the filesystem is the store (US5).
violation: not applicable
source: Q1=A, US5, ADR-005
- id: BR1.2
statement: Render the masthead dynamically from config.
category: calculation
applies_to: generator
trigger: each generation
logic: IF an issue is generated THEN the masthead (title, couple names, date line, issue, volume) is resolved from config.json and rendered dynamically (US2/US7, Q3=A).
violation: not applicable
source: Q3=A, US2, US7
- id: BR2.1
statement: Flow content into a column-based multi-page layout.
category: calculation
applies_to: generator
trigger: articles are placed
logic: IF content is present THEN it is flowed into columns page by page, breaking to a new A4 page at section/article boundaries where possible, with page numbers + running headers (Q2=A, Q7=A, US1).
violation: awkward mid-article splits or overflow would break print fidelity.
source: Q2=A, Q7=A, US1, FR1, NFR1/NFR2
- id: BR2.2
statement: AI may be used to fit content into the layout.
category: policy
applies_to: generator
trigger: layout composition
logic: IF fitting content is difficult (length/tightness) THEN the generator may use AI to help fit it, non-deterministically, provided the result gives the appearance of a newspaper (human note 2026-09-13).
violation: not applicable
source: human note (functional-design Q generator)
- id: BR3.1
statement: Auto-embed photos, clean text-only when none supplied.
category: constraint
applies_to: generator
trigger: photos present or absent
logic: IF photos are supplied THEN they are auto-balanced into the column grid with captions/credits (Q4=A, US4); IF none are supplied THEN the layout renders clean, text-only, with no fabricated images (FR7.4).
violation: FR7.4 - fabricated empty images would be a defect.
source: Q4=A, US4, FR7.4
- id: BR4.1
statement: Hand the AI-draft to the review page via the native file picker.
category: constraint
applies_to: generator
trigger: AI draft produced
logic: IF an AI draft is produced THEN the generator writes a DraftBundle JSON; the per-article review page loads it through the native file picker (user-granted, NFR5) — never fetch of a sibling file (US9/Contract 5, Q5=A).
violation: The review page would hit the opaque-origin CORS wall.
source: Q5=A, US9, NFR5, ADR-004
- id: BR5.1
statement: Emit a self-contained, zero-network HTML page.
category: constraint
applies_to: generator
trigger: render complete
logic: IF the issue is rendered THEN newspaper.html is a single self-contained, zero-network HTML (no CDN, no remote scripts), openable via file:// and printable to A4 (US5, NFR1/NFR4/NFR6, Q6=A).
violation: The page would not print cleanly or work offline.
source: Q6=A, US5, NFR1, NFR4, NFR6
- id: BR5.2
statement: Show a friendly guided empty state with no content.
category: policy
applies_to: generator
trigger: no content provided
logic: IF there is no content THEN the generator emits a friendly guided empty-state page with a pointer to content/config and a sample-issue option (US10, Q6=A).
violation: not applicable
source: Q6=A, US10
- id: BR5.3
statement: Keep the emitted page print-faithful to A4 with correct margins.
category: constraint
applies_to: generator
trigger: print CSS applied
logic: IF the page is printed THEN every section/images/puzzles stay inside the A4 sheet with no clipping, orphaned columns, or broken page breaks, margins within A4 (NFR1/NFR2, Q7=A).
violation: NFR2 defected (clipping/orphaned columns).
source: Q7=A, NFR1, NFR2
- id: BR6.1
statement: Fail gracefully when the review page data path is misused.
category: constraint
applies_to: generator
trigger: review handoff
logic: IF the review page is opened without the DraftBundle available THEN it shows a clear prompt to pick the draft file, never a blank/broken page (US9/US10, Q5=A).
violation: not applicable
source: Q5=A, US9, US10
```
## Rules summary
| ID | Rule | Category | Source |
|---|---|---|---|
| BR1.1 | Read content/ + config.json (filesystem store) | constraint | Q1, US5 |
| BR1.2 | Render masthead dynamically from config | calculation | Q3, US2/US7 |
| BR2.1 | Column-flow multi-page layout, breaks at boundaries | calculation | Q2/Q7, US1/FR1 |
| BR2.2 | AI may fit content into layout (appearance of newspaper) | policy | human note |
| BR3.1 | Auto-embed photos; clean text-only when absent | constraint | Q4, US4, FR7.4 |
| BR4.1 | AI-draft → DraftBundle via native file picker | constraint | Q5, US9, NFR5 |
| BR5.1 | Self-contained zero-network HTML, file:// printable | constraint | Q6, NFR1/4/6 |
| BR5.2 | Friendly guided empty-state | policy | Q6, US10 |
| BR5.3 | A4 print-faithful with correct margins | constraint | Q7, NFR1/2 |
| BR6.1 | Graceful review-handoff error state | constraint | Q5, US9/10 |
@@ -0,0 +1,26 @@
{
"stage": "functional-design",
"unit": "generator",
"upstream_ids": ["AC1.1.1", "AC1.1.2", "AC1.1.3", "AC4.1.1", "AC4.1.2", "AC4.1.3", "AC5.1.1", "AC5.1.2", "AC5.1.3", "AC6.1.1", "AC6.1.2", "AC6.1.3", "AC7.1.1", "AC7.1.2", "AC10.1.1", "AC10.1.2"],
"coverage": [
{ "id": "AC1.1.1", "status": "OK", "target": "BR2.1, BR5.3" },
{ "id": "AC1.1.2", "status": "OK", "target": "BR2.1, BR5.3" },
{ "id": "AC1.1.3", "status": "OK", "target": "BR2.1, BR5.3" },
{ "id": "AC4.1.1", "status": "OK", "target": "BR3.1" },
{ "id": "AC4.1.2", "status": "OK", "target": "BR3.1" },
{ "id": "AC4.1.3", "status": "OK", "target": "BR3.1" },
{ "id": "AC5.1.1", "status": "OK", "target": "BR5.1, BR1.1" },
{ "id": "AC5.1.2", "status": "OK", "target": "BR5.1, BR5.3" },
{ "id": "AC5.1.3", "status": "OK", "target": "BR5.1" },
{ "id": "AC6.1.1", "status": "OK", "target": "BR1.2, BR5.3" },
{ "id": "AC6.1.2", "status": "OK", "target": "BR5.3, BR2.1" },
{ "id": "AC6.1.3", "status": "OK", "target": "BR5.3" },
{ "id": "AC7.1.1", "status": "OK", "target": "BR1.1, BR1.2" },
{ "id": "AC7.1.2", "status": "OK", "target": "BR1.1, BR1.2" },
{ "id": "AC10.1.1", "status": "OK", "target": "BR5.2" },
{ "id": "AC10.1.2", "status": "OK", "target": "BR5.2" }
],
"reverse": [
{ "id": "BR2.2", "status": "N/A", "target": "layout-fitness policy (human note, no direct AC)" }
]
}
@@ -0,0 +1,23 @@
# Infrastructure Design — CI/CD Pipeline (unit: generator)
> Posture (Q1=B, Q2=A): repo committed to gitea for local use; a gitea Actions
> **test pipeline** runs the generator's tests; nothing to deploy. The two
> sanctioned build-time external calls are delegated to ai-draft + funnies, not
> generator-owned. Secrets/metadata via local config (Q3=A), never committed.
## Deployment posture
- No deployment target. Local-use tool, never published.
- gitea Actions runs tests only (no build/deploy/publish).
## Gitea Actions test pipeline
| Stage | Action |
|---|---|
| Checkout | Fetch repo |
| Setup | `uv` + Python |
| Test | Generator tests: markdown→HTML fidelity, layout/pagination, self-contained emit, print-fidelity checks |
| Report | Surface pass/fail in gitea |
- Trigger: push/PR to repo main. No deploy stage. Tests mock the ai-draft/funnies contracts (no live model/source calls in CI).
## Secrets
- The generator's content/config path is a local config the user supplies (never committed). Model/source credentials (owned by ai-draft/funnies) are not needed in CI (contracts mocked).
@@ -0,0 +1,55 @@
# Infrastructure Design — Questions (unit: generator)
> Fill in each `[Answer]:` tag. generator is a `library`/CLI unit; per
> `produces_kinds` it owes `cicd-pipeline.md` + `traceability.json` (infra-spec
> is service/ui/packaging-only). Project is committed to the gitea repo for local
> use, with a gitea Actions test pipeline (no deploy).
## Q1: Infrastructure posture
For a file-only, no-server, never-deployed tool, what infrastructure does the generator need?
A) None — no deployment/cloud; repo committed to gitea for local use + versioning (recommended)
B) Minimal — capture local runtime (uv/Python/CLI) and note the two sanctioned build-time external calls are delegated to ai-draft + funnies, not generator-owned
C) Full — CI/CD + infra even though nothing deploys
X) Other (please specify)
[Answer]: B
## Q2: CI/CD
Is any build/test/deploy pipeline needed for the generator?
A) Gitea Actions test pipeline only — runs the tests (markdown→HTML transform, layout/pagination checks, emit), nothing deployed (matches ai-draft/funnies)
B) No CI/CD at all
C) Full CI with deploy
X) Other (please specify)
[Answer]: A
## Q3: Secrets / external touchpoints
The generator itself makes no network calls for content (reads via file picker; the two sanctioned build-time calls are delegated). How should any config be handled?
A) Local config the user supplies (content path, masthead metadata), never committed secrets or hard-coded (recommended)
B) Environment variable only
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the generator infrastructure answers (mirror ai-draft/funnies + gitea test pipeline):
>
> - Posture: minimal — local runtime; the two sanctioned build-time external calls delegated to ai-draft+funnies (Q1=B)
> - CI/CD: gitea Actions test pipeline only, nothing deployed (Q2=A)
> - Secrets: local config, never committed (Q3=A)
>
> Human auto-approved (mirror).
Does this all look correct before I generate the unit artifacts?
- Looks correct
- Request changes
[Answer]: Looks correct
@@ -0,0 +1,15 @@
<!-- INVARIANT: examples are single-line HTML comments so a fresh template parses to total=0 (MEMORY_EMPTY). Do NOT un-comment or split across lines. t100 guards this. -->
> This file is kept up to date automatically while the stage runs. Add observations at the review step, not by editing here directly.
## Interpretations
<!-- example: 2026-05-29T10:14:32Z — chose REST over GraphQL; the consuming team only needs CRUD, revisit if subscriptions land -->
## Deviations
<!-- example: 2026-05-29T10:14:32Z — skipped the optional caching layer the stage prose suggested; the dataset is small enough that it adds risk -->
## Tradeoffs
<!-- example: 2026-05-29T10:14:32Z — picked TDD over BDD this run; the team is unit-first and the domain is well-understood -->
## Open questions
<!-- example: 2026-05-29T10:14:32Z — confirm the retention window with compliance before the next stage hardens the schema -->
2026-09-14T00:33:00Z — Interpretation — generator infra: repo committed to gitea for local use; gitea Actions test pipeline (tests only, nothing deployed); content/config via local config. Produces cicd-pipeline + traceability.
@@ -0,0 +1,12 @@
{
"stage": "infrastructure-design",
"unit": "generator",
"upstream_ids": ["NFR4.1", "NFR5.1"],
"coverage": [
{ "id": "NFR4.1", "status": "OK", "target": "no deployable infra; zero-network self-contained emit; gitea Actions tests emit/print only" },
{ "id": "NFR5.1", "status": "OK", "target": "no deployable infra; content read via local filesystem/file-picker; tests mock contracts" }
],
"reverse": [
{ "id": "NFR1.1", "status": "N/A", "target": "no infrastructure-relevant NFR (local-use tool)" }
]
}
@@ -0,0 +1,46 @@
# NFR Design — Logical Components (unit: generator)
> Logical infrastructure component inventory for the generator library/CLI unit.
> Per Q2=A: generator is the central orchestration boundary; it orchestrates
> ai-draft + funnies via in-process calls and emits the self-contained page,
> which is fully isolated from runtime state.
## Component inventory
| Logical component | Kind | Failure domain | Blast radius |
|---|---|---|---|
| generator (library/CLI) | orchestration boundary | Isolated to the generator process | Medium — it orchestrates output, but is designed to fail soft: no content → guided empty state; ai-draft/funnies unavailable → build without them; never a blank/broken page |
## Boundaries & isolation
- **generator** is the central orchestration unit (confirmed at domain-design and
units-generation). It calls ai-draft and funnies via in-process contracts
(DraftBrief → DraftBundle; FunniesBrief → FunniesResult) and emits the
self-contained `newspaper.html`.
- **Emitted page isolation**: the produced `newspaper.html` is a static,
self-contained artifact fully isolated from runtime state — it embeds all
assets, makes zero network requests, and reads nothing at runtime (all content
is baked in at generation time; the review page's file-picker read is
user-initiated, not a live dependency).
## Component isolation strategy
- generator depends on ai-draft and funnies only through their in-process
contracts; a failure in either degrades gracefully per the fail-soft design
(Q3=A).
- generator introduces no shared infrastructure, database, cache, or queue with
the other units.
## Shared resource identification
- The local filesystem (content/ + config.json) is the shared content store
(ADR-005). The DraftBundle is a file handoff to the review page via the native
file picker — not a live shared resource.
## Bridge to Infrastructure Design
- The generator boundary maps to the `u1-generator` build unit. No distributed
infrastructure is required. Its orchestration role means its failure modes are
the central ones (empty-state, partial-build), all handled gracefully. The two
external dependency calls (ai-draft cloud, funnies content-enrichment) are
owned by those units, keeping the generator's own network surface at zero.
@@ -0,0 +1,15 @@
<!-- INVARIANT: examples are single-line HTML comments so a fresh template parses to total=0 (MEMORY_EMPTY). Do NOT un-comment or split across lines. t100 guards this. -->
> This file is kept up to date automatically while the stage runs. Add observations at the review step, not by editing here directly.
## Interpretations
<!-- example: 2026-05-29T10:14:32Z — chose REST over GraphQL; the consuming team only needs CRUD, revisit if subscriptions land -->
## Deviations
<!-- example: 2026-05-29T10:14:32Z — skipped the optional caching layer the stage prose suggested; the dataset is small enough that it adds risk -->
## Tradeoffs
<!-- example: 2026-05-29T10:14:32Z — picked TDD over BDD this run; the team is unit-first and the domain is well-understood -->
## Open questions
<!-- example: 2026-05-29T10:14:32Z — confirm the retention window with compliance before the next stage hardens the schema -->
2026-09-14T00:27:00Z — Interpretation — generator NFR Design (mirror ai-draft, all A): minimal-surface security (no own network; file-picker reads; zero-network emit; two sanctioned external calls delegated), generator as central orchestration boundary, fail-soft orchestrator, injected structured logger. Produces security-design + logical-components + traceability (lib kind).
@@ -0,0 +1,69 @@
# NFR Design — Questions (unit: generator)
> Fill in each `[Answer]:` tag. generator is a `library`-kind unit in the DAG,
> so NFR Design produces security-design + logical-components + traceability
> (perf/scalability/reliability/observability designs are service-only, N/A).
> Answers mirror ai-draft/funnies (human standing instruction) with
> generator-specific context: it emits the printable page, reads content via
> the file picker, and touches two sanctioned build-time external calls
> (ai-draft cloud call + funnies content-enrichment).
## Q1: Security pattern approach
The generator must keep the emitted page zero-network and read content only via user-granted file access. What security design pattern applies?
A) Minimal-surface — the generator never makes its own outbound network calls for content; it reads via the native file picker, embeds all assets locally, and the emitted page makes zero network requests; the two sanctioned build-time external calls are delegated to ai-draft (cloud) + funnies (content-enrichment) (recommended)
B) Defense-in-depth — explicit allow/deny lists for any content source + sanitization
C) Zero-trust style — verify every read/fetch is user-authorized
X) Other (please specify)
[Answer]: A
## Q2: Logical component boundary
As the orchestration unit, what should `logical-components.md` capture for blast radius?
A) generator as the central library/CLI boundary — it orchestrates ai-draft + funnies via in-process calls and emits the self-contained page; its own failure modes are local (no content → guided empty state; AI/funnies unavailable → build without them); the emitted page is fully isolated from runtime state (recommended)
B) Split the generator into sub-components (content-loader, layout-engine, emitter) with separate failure domains
X) Other (please specify)
[Answer]: A
## Q3: Reliability/graceful-degradation pattern
The generator NFR requires graceful containment (no content → empty state; ai-draft/funnies fail → build without them). What pattern should the design specify?
A) A fail-soft orchestrator — each optional dependency (ai-draft, funnies) returns a fallback/empty result on failure; the generator proceeds to emit a valid page + a clear message; never a blank/broken page (recommended)
B) Hard-fail with a clear message and non-zero exit
C) Retry-with-backoff then fallback
X) Other (please specify)
[Answer]: A
## Q4: Observability design (logical)
The generator NFR is light logging. What logging design fits the CLI/orchestrator unit?
A) A small structured logger — one concise line per run (inputs read, pages/puzzles/photos placed, emit success/status); no metrics/tracing (recommended)
B) Built-in logging writes to stderr directly
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the four generator NFR Design answers (mirror ai-draft, human-approved):
>
> - Security: minimal-surface — no own network calls for content; file-picker reads; all assets embedded; zero-network emitted page; the two sanctioned build-time external calls delegated to ai-draft + funnies
> - Logical boundary: generator central library/CLI orchestrator; emitted page isolated from runtime state
> - Reliability: fail-soft orchestrator — optional deps fall back, never blank/broken page
> - Observability: injected structured logger, one line per run
>
> Human auto-approved.
Does this all look correct before I generate the unit design artifacts?
- Looks correct
- Request changes
[Answer]: Looks correct
@@ -0,0 +1,47 @@
# NFR Design — Security Design (unit: generator)
> Minimal-surface security design for the generator library/CLI unit, per Q1=A.
> The generator makes no network calls for content itself; it reads via the
> native file picker, embeds all assets locally, and the emitted page is
> zero-network. The two sanctioned build-time external calls (ai-draft cloud,
> funnies content-enrichment) are delegated to those units.
## Approach: minimal-surface
- **No own network calls for content**: the generator never fetches content,
fonts, or assets over the network. It reads authored content + config from the
local filesystem and loads user-supplied content/draft bundles via the native
file picker / drag-and-drop only (NFR5 — resolves the opaque-origin CORS
constraint).
- **All assets embedded locally**: photos, fonts, funnies output, and any
generated assets are embedded into the self-contained `newspaper.html`; no
remote URL at print.
- **Zero-network emitted page**: the emitted `newspaper.html` makes zero network
requests (NFR4) and is fully offline-capable from `file://`.
- **Sanctioned external calls delegated**: the two build-time external calls —
the ai-draft cloud call and the funnies content-enrichment fetch — are owned by
those units, not the generator. The generator invokes them via their in-process
contracts and embeds their (local) output.
- **No secrets/credentials**: no keys/tokens/private data embedded, logged, or
emitted. The generator logs only light per-run status (Q4=A).
## Design decisions
| Decision | Design |
|---|---|
| Content read | Native file picker / drag-and-drop; never fetch of sibling local files |
| Emit | Fully self-contained, zero-network HTML (NFR4/NFR6) |
| External calls | Delegated to ai-draft (cloud) + funnies (content-enrichment); not generator-owned |
| Logging | Light, one line per run; never logs content or secrets |
| Secrets | None embedded or emitted |
## Security controls map (from generator security-requirements)
- **NFR3.1 (local-only default)** — satisfied by no-own-network + embedded assets.
- **NFR4.1 (zero-network emitted page)** — satisfied by self-contained emit.
- **NFR5.1 (file-picker content-read)** — satisfied by native picker path.
## Verification
- The emitted page makes zero network requests and works offline from `file://`.
- Content is only ever read through user-granted means; no sibling-file `fetch()`.
@@ -0,0 +1,13 @@
{
"stage": "nfr-design",
"unit": "generator",
"upstream_ids": ["NFR3.1", "NFR4.1", "NFR5.1"],
"coverage": [
{ "id": "NFR3.1", "status": "OK", "target": "security-design.md (local-only default; no own network)" },
{ "id": "NFR4.1", "status": "OK", "target": "security-design.md / logical-components.md (zero-network self-contained emit)" },
{ "id": "NFR5.1", "status": "OK", "target": "security-design.md (native file-picker content-read; never sibling fetch)" }
],
"reverse": [
{ "id": "NFR1.1", "status": "N/A", "target": "performance design is service/ui-only (library kind)" }
]
}
@@ -0,0 +1,15 @@
<!-- INVARIANT: examples are single-line HTML comments so a fresh template parses to total=0 (MEMORY_EMPTY). Do NOT un-comment or split across lines. t100 guards this. -->
> This file is kept up to date automatically while the stage runs. Add observations at the review step, not by editing here directly.
## Interpretations
<!-- example: 2026-05-29T10:14:32Z — chose REST over GraphQL; the consuming team only needs CRUD, revisit if subscriptions land -->
## Deviations
<!-- example: 2026-05-29T10:14:32Z — skipped the optional caching layer the stage prose suggested; the dataset is small enough that it adds risk -->
## Tradeoffs
<!-- example: 2026-05-29T10:14:32Z — picked TDD over BDD this run; the team is unit-first and the domain is well-understood -->
## Open questions
<!-- example: 2026-05-29T10:14:32Z — confirm the retention window with compliance before the next stage hardens the schema -->
2026-09-13T13:15:00Z — Interpretation — generator NFR (new questions, recommended confirmed): clean print breaks, hard zero-network, file-picker content-read only, graceful multi-page containment, light logging, plain Python+uv. As a service (CLI) unit, the full NFR set applies.
@@ -0,0 +1,87 @@
# NFR Requirements — Questions (unit: generator)
> Fill in each `[Answer]:` tag. Options A-E plus X (Other). Unlike ai-draft and
> funnies, the generator EMITS the printable newspaper, so its NFRs focus on
> print fidelity, zero-network, and content-read policy — genuinely new concerns.
## Q1: Print fidelity target
The generator's emitted page must print cleanly to A4 (NFR1/NFR2) — clean page breaks, no clipping/orphans. What print-fidelity behaviour should be guaranteed?
A) Clean breaks at article/section boundaries where possible; page numbers + running headers on every page; margins within A4; no clipping/orphaned columns (recommended)
B) Browser-default print (weaker control on breaks)
C) A fixed page count regardless of content
X) Other (please specify)
[Answer]: A
## Q2: Zero-network emission
The emitted `newspaper.html` must be self-contained and make zero network requests (NFR4). Which guarantee applies?
A) Hard guarantee — zero network requests, fully offline from file://; fonts/images/assets all embedded locally (recommended)
B) Best-effort — try to inline, no hard enforcement
X) Other (please specify)
[Answer]: A
## Q3: Content-read policy at review time
The generated page / review page reads content via the user-granted file picker (NFR5). What behaviour should be enforced?
A) Only the native file picker / drag-and-drop path is used to load content/draft bundles; never fetch() of sibling local files (recommended)
B) Allow fetch() as well where the browser permits it
X) Other (please specify)
[Answer]: A
## Q4: Multi-page reliability / performance
The generator handles arbitrarily long content flowing across many A4 pages. What reliability target applies?
A) Best-effort with graceful containment — any length flows across pages cleanly; a very long run is fine, no hard page-ceiling (recommended)
B) Cap the page count with a warning/cutoff
C) Strict page-count target
X) Other (please specify)
[Answer]: A
## Q5: Observability
How should the generator log its runs locally?
A) Light — a concise per-run log (inputs read, pages/puzzles/photos placed, emit success) enough to debug locally; no metrics/tracing (recommended)
B) More — per-article/per-page detail
X) Other (please specify)
[Answer]: A
## Q6: Tech stack
The generator is the CLI + layout library (Python + uv). Any NFR-related stack constraints?
A) Plain Python + uv, stdlib-friendly, thin deps (recommended)
B) Same as the other units — minimal deps
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the six generator NFR answers (new questions, recommended confirmed via 'continue'):
>
> - Print fidelity: **clean page breaks, page numbers + running headers, A4 margins** (Q1=A)
> - Zero-network: **hard guarantee, fully offline from file://** (Q2=A)
> - Content-read: **only native file picker/drag-drop, never fetch of siblings** (Q3=A)
> - Multi-page reliability: **best-effort graceful containment, no hard ceiling** (Q4=A)
> - Observability: **light per-run logging** (Q5=A)
> - Tech stack: **plain Python + uv, thin deps** (Q6=A)
>
> Human signaled continue; may request changes at the stage gate.
Does this all look correct before I generate the unit artifacts?
- Looks correct
- Request changes
[Answer]: Looks correct
@@ -0,0 +1,9 @@
# NFR Requirements — Observability (unit: generator)
## NFR-O1 — Light per-run log (MUST)
- **Target** — `newspaper generate` logs a concise per-run summary: inputs read (content path/config), pages/puzzles/photos placed, emit success. Enough to debug locally; no metrics, no distributed tracing, no external logs.
- **Rationale** — Confirmed Q5=A (light). Local one-shot tool; observability is for debugging, not operations.
## NFR-O2 — Clear error/status output (MUST)
- **Target** — Failures surface as clear, one-line messages naming the problem + fix path (missing content, model unreachable, funnies empty), never silent or blank.
- **Rationale** — Graceful-failure posture and US10 empty/error states.
@@ -0,0 +1,12 @@
# NFR Requirements — Performance (unit: generator)
## NFR1.1 — Print-render performance (MUST)
- **Target** — `newspaper generate` renders the full self-contained `newspaper.html` within a few seconds for a normal wedding issue (dozens of articles, a few photos, funnies); no strict SLA for very large inputs.
- **Rationale** — Confirmed Q1/Q4=A (relaxed/moderate; local one-shot generation). The generator is not a live service; wall-clock is not user-facing latency.
## NFR1.2 — Resource footprint (MUST)
- **Target** — Runs on a normal laptop with modest memory; no background daemon, no long-lived processes.
- **Rationale** — Embedded/static single deliverable per ADR-005 and the file-only posture.
## Derived NFR mapping
Inherits inception NFR1 (multi-page print fidelity): see NFR1.1/NFR2.x.
@@ -0,0 +1,13 @@
# NFR Requirements — Reliability (unit: generator)
## NFR-R1 — Graceful degradation on missing inputs (MUST)
- **Target** — With no content, the generator emits the friendly guided empty-state (US10), never a blank/broken page. If ai-draft or funnies fails, the paper builds without that piece and a clear message is shown (graceful).
- **Rationale** — Confirmed reliability posture (Q4=A); US10/BR5.2.
## NFR-R2 — Deterministic, re-runnable output (MUST)
- **Target** — `newspaper generate` is idempotent and re-runnable: re-running the same command over the same content produces a valid, regenerable `newspaper.html`; never destroys the couple's content files.
- **Rationale** — Local tool reliability; content filesystem is the store (ADR-005).
## NFR-R3 — Print fidelity reliability (MUST)
- **Target** — The emitted page prints cleanly to A4 with clean page breaks, page numbers + running headers, and margins within A4; no clipping or orphaned columns (NFR1/NFR2, Q1/Q7=A).
- **Rationale** — Confirmed print-fidelity target (Q1=A); the highest-value surface (US1/FR1).
@@ -0,0 +1,9 @@
# NFR Requirements — Scalability (unit: generator)
## NFR-S1 — Content-volume headroom (MUST)
- **Target** — The generator handles arbitrarily long content, flowing across as many A4 pages as needed, without a fixed page ceiling (fluid pagination, US1/FR1/Q4=A). "Scalability" here means content-length scalability, not load/tenancy.
- **Rationale** — Confirmed Q4=A (best-effort graceful containment, no hard ceiling). This is a local one-shot tool; there is no user-concurrency to scale.
## NFR-S2 — No fixed output-size cap (MUST)
- **Target** — No hard page-count or article-count cap; a very long issue still renders (just more pages).
- **Rationale** — Fluid pagination per US1/FR1 and the confirmed fluid-print decision at requirements (Q7=A).
@@ -0,0 +1,15 @@
# NFR Requirements — Security (unit: generator)
## NFR3.1 — Local-only default (MUST)
- **Target** — The generator runs fully locally; content and issued data stay on-machine by default. It sends nothing off-machine except the optional AI-draft call (handled by ai-draft, not the generator) and the content-enrichment fetch (funnies).
- **Rationale** — Confirmed posture; NFR3 (local-only).
## NFR4.1 — Zero-network emitted page (MUST)
- **Target** — The emitted `newspaper.html` makes zero network requests; fully self-contained and offline-capable from `file://` (no CDN fonts, no remote scripts, all assets embedded).
- **Rationale** — Confirmed Q2=A; NFR4/NFR6.
## NFR5.1 — Content-read via native file picker only (MUST)
- **Target** — The generated/review page loads content and draft bundles only through the native file picker / drag-and-drop (user-granted); never `fetch()` of sibling local files (fails on file:// opaque origin).
- **Rationale** — Confirmed Q3=A; NFR5 (this resolves the empirical research and contract ADR-004).
<!-- nfr generator after-confirm -->
@@ -0,0 +1,16 @@
# NFR Requirements — Tech Stack Decisions (unit: generator)
## Decisions
| Choice | Decision | Rationale |
|---|---|---|
| Language | **Python** | The whole generator is Python + uv (affirmed; node now works but Python stays the primary path per practice). |
| Runtime | **uv** | Working `uv` runtime (Python 3.14). |
| CLI | Thin `newspaper` entry | `newspaper generate` reads config/content, orchestrates layout + emit (Q6=A). |
| Layout/render | **Pure HTML5 + CSS + vanilla JS**, generated | The emitted page is self-contained, zero-network (NFR4), file:// printable (NFR1/2). |
| Markdown→HTML | Light transform (stdlib-friendly) | Content parsing into Article entities; fidelity guaranteed (NFR7). |
| Dependencies | **Minimal** | stdlib-friendly thin deps; only what's needed (Q6=A), consistent with all units. |
## Non-decisions
- No framework (vanilla HTML/CSS/JS emitted page; no browser framework).
- No DB — content filesystem is the store (ADR-005).
- No server — embedded/static single deliverable (ADR-004/ADR-005).
@@ -0,0 +1,16 @@
{
"stage": "nfr-requirements",
"unit": "generator",
"upstream_ids": ["NFR1", "NFR2", "NFR3", "NFR4", "NFR5", "NFR6", "NFR7", "NFR8", "NFR9"],
"coverage": [
{ "id": "NFR1", "status": "OK", "target": "NFR1.1, NFR-R3" },
{ "id": "NFR2", "status": "OK", "target": "NFR-R3 (NFR2.x in functional-spec/generator)" },
{ "id": "NFR3", "status": "OK", "target": "NFR3.1" },
{ "id": "NFR4", "status": "OK", "target": "NFR4.1" },
{ "id": "NFR5", "status": "OK", "target": "NFR5.1" },
{ "id": "NFR6", "status": "OK", "target": "tech-stack-decisions.md (pure HTML5/CSS/JS, no build dep)" },
{ "id": "NFR7", "status": "OK", "target": "tech-stack-decisions.md (markdown fidelity), generator functional-spec BR (content parse)" },
{ "id": "NFR8", "status": "OK", "target": "functional-design generator rules (masthead/theme)" },
{ "id": "NFR9", "status": "OK", "target": "NFR3.1, NFR4.1 (AI-isolation; the optional draft/enrichment stays local-first elsewhere)" }
]
}