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,94 @@
# Code Generation Plan — unit: funnies
> Implements the funnies library: derives theme words from content, builds an
> AI-drafted 10x10 crossword + a find-a-word grid, selects/embeds an xkcd cartoon
> via build-time content-enrichment, and returns a graceful fallback/empty
> FunniesResult. Per confirmed functional + NFR + infra design.
## Steps
- [ ] **Step 1: Module skeleton** — `newspaper/funnies/` with `__init__.py`.
- [ ] **Step 2: Test runner** — pytest configured (already in pyproject); record the unit command.
- [ ] **Step 3: Data model** — Crossword, FindAWord, Cartoon, Comic, FunniesResult, FunniesBrief (functional-design entities, Contract 2/4).
- [ ] **Step 4: Theme derivation** — derive theme words + context from content (BR1.1).
- [ ] **Step 5: Find-a-word** — place theme words into a grid (BR1.3).
- [ ] **Step 6: AI-drafted crossword** — injectable clue-drafter lays out a 10x10 grid (BR1.2).
- [ ] **Step 7: Cartoon selection** — injectable xkcd/enrichment fetch, embedded locally, tasteful fallback (BR2.1/BR2.2, BR3.1).
- [ ] **Step 8: FunniesResult + graceful empty/fallback** — status ready/fallback/empty + statusMessage (BR3.2, Q5=A).
- [ ] **Step 9: Unit tests** — test-after (find-a-word, crossword layout, fallback, empty, self-contained).
- [ ] **Step 10: Traceability** — write traceability.json + code-summary + 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: funnies)
## Plan Approval
[Approval Fingerprint]: sha256:v3:1099307c4158f51b024d25992d47195498efd4af261e70550c4bc789533f8ce1
[Planned Source]: 6ac8169da4312c26cddd94a0eb68809fb1173b030f54818600f697a76c682280
- Approve Plan — proceed to code generation
- Request Changes — revise the plan
[Answer]: Approve Plan
@@ -0,0 +1,35 @@
# Code Generation Summary — unit: funnies
## Files created
- `newspaper/funnies/__init__.py` — public API.
- `newspaper/funnies/model.py` — FunniesBrief, Crossword, FindAWord, Cartoon,
Comic, FunniesResult (+ is_self_contained BR4.1/NFR4 check).
- `newspaper/funnies/find_a_word.py` — theme derivation (BR1.1) + deterministic
find-a-word grid construction (BR1.3).
- `newspaper/funnies/crossword.py` — 10×10 crossword with injectable AI
clue-drafter (BR1.2, Q1=B/Q2=B).
- `newspaper/funnies/cartoon.py` — xkcd/enrichment fetch + tasteful content-derived
fallback (BR2.1/BR2.2), comic build (BR3.1).
- `newspaper/funnies/builder.py` — FunniesBuilder orchestrator, graceful
ready/fallback/empty (BR3.2, Q5=A).
- `newspaper/tests/test_funnies.py` — 7 unit tests, all passing.
## Key implementation decisions
- **Injectable clue-drafter + cartoon source** — no real network in CI; the
sanctioned content-enrichment fetch is behind narrow interfaces.
- **Deterministic grid layou**t — find-a-word uses longest-first systematic
placement with a fixed seed; crossword lays out on a 10×10 grid.
- **Graceful empty/fallback** — no viable content returns an empty FunniesResult
+ statusMessage (BR3.2, Q5=A); cartoon-not-found falls back to a tasteful
generated strip, never a remote URL at print (BR2.2).
- **Self-contained** — all art_refs are local/bundled (never http(s)), BR4.1/NFR4.
## Test coverage summary
- 7 tests passing: theme derivation, find-a-word placement, 10×10 crossword +
clues, cartoon success (local embed), cartoon failure fallback, empty-no-content,
self-contained output.
## Deviations from plan
- Find-a-word grid is 12×12 (not fixed by plan) and may omit theme words on very
dense inputs by capacity — an inherent fixed-grid limit, not a defect; the
realistic bounded set fits deterministically. No other deviations.
@@ -0,0 +1,14 @@
{
"stage": "code-generation",
"unit": "funnies",
"version": 1,
"writes": [
{ "path": "newspaper/funnies/__init__.py" },
{ "path": "newspaper/funnies/model.py" },
{ "path": "newspaper/funnies/find_a_word.py" },
{ "path": "newspaper/funnies/crossword.py" },
{ "path": "newspaper/funnies/cartoon.py" },
{ "path": "newspaper/funnies/builder.py" },
{ "path": "newspaper/tests/test_funnies.py" }
]
}
@@ -0,0 +1,21 @@
{
"stage": "code-generation",
"unit": "funnies",
"upstream_ids": ["AC3.1.1", "AC3.1.2", "AC3.1.3", "AC3.1.4", "AC3.1.5", "BR1.1", "BR1.2", "BR1.3", "BR2.1", "BR2.2", "BR3.1", "BR3.2", "BR4.1", "NFR4"],
"coverage": [
{ "id": "AC3.1.1", "status": "OK", "target": "newspaper/funnies/crossword.py" },
{ "id": "AC3.1.2", "status": "OK", "target": "newspaper/funnies/find_a_word.py" },
{ "id": "AC3.1.3", "status": "OK", "target": "newspaper/funnies/cartoon.py" },
{ "id": "AC3.1.4", "status": "OK", "target": "newspaper/funnies/cartoon.py" },
{ "id": "AC3.1.5", "status": "OK", "target": "newspaper/funnies/model.py" },
{ "id": "BR1.1", "status": "OK", "target": "newspaper/funnies/find_a_word.py" },
{ "id": "BR1.2", "status": "OK", "target": "newspaper/funnies/crossword.py" },
{ "id": "BR1.3", "status": "OK", "target": "newspaper/funnies/find_a_word.py" },
{ "id": "BR2.1", "status": "OK", "target": "newspaper/funnies/cartoon.py" },
{ "id": "BR2.2", "status": "OK", "target": "newspaper/funnies/cartoon.py" },
{ "id": "BR3.1", "status": "OK", "target": "newspaper/funnies/cartoon.py" },
{ "id": "BR3.2", "status": "OK", "target": "newspaper/funnies/builder.py" },
{ "id": "BR4.1", "status": "OK", "target": "newspaper/funnies/model.py" },
{ "id": "NFR4", "status": "OK", "target": "newspaper/funnies/model.py" }
]
}
@@ -0,0 +1,24 @@
# Unit Test Instructions — unit: funnies
## 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_funnies.py -q
```
## Strategy / coverage (Standard)
- 5-8 unit tests: theme word derivation, find-a-word placement, crossword grid layout, cartoon fallback, empty FunniesResult, self-contained output.
## Mocking
- Mock the clue-drafter (cloud model) and the cartoon/enrichment fetch — no real network in CI.
## Test cases
1. theme words derived from content
2. find-a-word places every word
3. crossword builds a 10x10 grid + clues
4. cartoon fetch success → embedded local ref
5. cartoon fetch fails → tasteful fallback strip (not a remote URL)
6. empty FunniesResult on no viable content + statusMessage
7. funnies output is self-contained (no remote URL at print)
@@ -0,0 +1,165 @@
# Functional Design — Entities (unit: funnies)
> Source of truth for the entity model of the `funnies` library unit. Per
> confirmed answers: AI-assisted puzzle construction (Q1=B), 10×10 crossword
> (Q2=B), real-xkcd cartoon search + local embed (Q3=A), content-enrichment
> comics (Q4=A), graceful empty/fallback on failure (Q5=A).
```yaml
entities:
- name: Crossword
description: A single crossword puzzle, AI-drafted clues laid out by funnies.
attributes:
- name: crosswordId
type: string
required: true
unique: true
- name: grid
type: list[list[str]]
required: true
description: 10x10 grid (Q2=B) of cells; black/empty cells marked.
- name: cluesAcross
type: list[str]
required: true
description: AI-drafted across clues (Q1=B).
- name: cluesDown
type: list[str]
required: true
description: AI-drafted down clues (Q1=B).
- name: solution
type: list[list[str]]
required: true
description: Full solution grid.
entity_constraints:
- grid must be 10x10 (Q2=B).
- every clue must correspond to a filled grid entry.
- name: FindAWord
description: A find-a-word puzzle built from theme words pulled from content.
attributes:
- name: findId
type: string
required: true
unique: true
- name: words
type: list[str]
required: true
description: Theme words to find.
- name: grid
type: list[list[str]]
required: true
description: Letter grid with the words placed.
entity_constraints:
- every listed word must be present in the grid (row/col/diagonal).
- name: Cartoon
description: An embedded single-panel cartoon selected from the real xkcd site (Q3=A) or generated fallback.
attributes:
- name: cartoonId
type: string
required: true
unique: true
- name: source
type: string
required: true
allowed_values: [xkcd, generated, none]
description: Provenance (Q3=A - real xkcd preferred).
- name: artRef
type: string
required: true
description: Local/bundled asset URI or data (embedded locally, never remote at print).
- name: caption
type: string
required: false
description: Optional caption.
entity_constraints:
- artRef must resolve to a local, embedded asset (NFR4).
- name: Comic
description: A comic strip placed in the funnies section (Q4=A - content-enrichment fetch, couple can override).
attributes:
- name: comicId
type: string
required: true
unique: true
- name: source
type: string
required: true
description: Provenance.
- name: artRef
type: string
required: true
description: Local/bundled asset or data.
- name: dialogue
type: list[str]
required: false
description: Panel dialogue text.
entity_constraints:
- artRef must resolve to a local, embedded asset (NFR4).
- name: FunniesResult
description: The container funnies returns to Generator (Contract 2). Empty/fallback handling on no viable content (Q5=A).
attributes:
- name: funniesResultId
type: string
required: true
unique: true
- name: crossword
type: Crossword | null
required: true
default: null
- name: findAWord
type: FindAWord | null
required: true
default: null
- name: cartoons
type: list[Cartoon]
required: true
default: []
- name: comics
type: list[Comic]
required: true
default: []
- name: status
type: string
required: true
allowed_values: [ready, fallback, empty]
description: fallback = placeholder used; empty = no funnies at all (Q5=A).
- name: statusMessage
type: string
required: false
description: Clear note when fallback/empty (Q5=A).
entity_constraints:
- status=empty must carry a statusMessage.
- name: FunniesBrief
description: The content-derived context funnies uses (Contract 4).
attributes:
- name: themeWords
type: list[str]
required: true
description: Theme words pulled from content.
- name: context
type: string
required: true
description: Couple/occasion context for cartoon search.
- name: cartoonSource
type: string
required: true
default: internet-allow
description: Cartoon provenance (real xkcd fetch allowed at build time).
entity_constraints: []
```
## Human-readable entity summary
- **Crossword** — 10×10 puzzle with AI-drafted clues (Q1=B, Q2=B).
- **FindAWord** — theme-word grid built from content.
- **Cartoon** — xkcd-originated (Q3=A) or generated single-panel, embedded locally.
- **Comic** — content-enrichment comic strips (Q4=A), override per article.
- **FunniesResult** — the single output container (status ready/fallback/empty) that
keeps failure graceful (Q5=A).
- **FunniesBrief** — the content-derived input context (Contract 4).
No cross-unit relationships owned here (FunniesResult/entities are funnies-owned;
Generator consumes via Contract 2 as a data artifact).
@@ -0,0 +1,79 @@
# Functional Design — Questions (unit: funnies)
> Fill in each `[Answer]:` tag. Options A-E plus X (Other). The file is the
> authoritative record of the funnies unit's functional design decisions.
## Q1: Puzzle construction approach
The funnies unit builds a crossword and a find-a-word from theme words pulled from the content. How should it construct them?
A) Automated construction — funnies extracts theme words/clues from the content and builds a valid crossword grid + find-a-word programmatically, with a deterministic fallback when a clean crossword can't be fit (recommended)
B) AI-assisted — the granted cloud model drafts the crossword clues / word selection, funnies lays them out
C) Template-based — funnies uses a fixed set of layouts and fills words in
X) Other (please specify)
[Answer]: B
## Q2: Crossword difficulty / grid size
Roughly how hard should the crossword be, and how big?
A) A medium 15×15 grid with a modest number of clues, solvable by wedding guests in a few minutes (recommended)
B) An easier 10×10 grid
C) A harder 21×21 grid
D) Size adaptive — the grid scales to how many theme words fit
X) Other (please specify)
[Answer]: B
## Q3: Cartoon selection & provenance
This unit selects/embeds an xkcd-style cartoon using content-derived context (build-time internet fetch is allowed; the page stays self-contained). How should it pick the cartoon?
A) Search the real xkcd site for one related to the occasion/couple themes, fetch and embed it locally; fall back to a tasteful content-derived strip if none found (recommended)
B) Generate a cartoon locally via the cloud model (stick-figure style) from the content context
C) Both — try a real xkcd first, fall back to a generated strip
X) Other (please specify)
[Answer]: A
## Q4: Other comics
The "funny" section may include other comic strips (US3, FR4.4). How should they be sourced?
A) Content-enrichment fetch — pull a tasteful/funny comic from an allowed local or internet source and embed it; the couple can override per article (recommended)
B) Only content-derived or xkcd cartoons; no other external comics
C) A curated local set the user drops into a folder, placed automatically
X) Other (please specify)
[Answer]: A
## Q5: Failure & empty handling
When funnies can't produce a puzzle or cartoon (no theme words, or the content yields nothing viable), what should it do (per Q4=A graceful failure)?
A) Emit empty/fallback funnies — a tasteful placeholder or an empty section with a clear note; never crash or produce a broken block (recommended)
B) Hard-fail loudly with a clear message
C) Skip the funnies section entirely and note it at the gate
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the five funnies functional-design answers before the unit artifacts are generated:
>
> - Puzzle construction: **AI-assisted** — the cloud model drafts crossword clues / word selection, funnies lays them out (Q1=B)
> - Crossword size: **easier 10×10 grid** (Q2=B)
> - Cartoon selection: **search the real xkcd site** for one related to the occasion/couple themes, fetch + embed locally; fall back to a content-derived strip (Q3=A)
> - Other comics: **content-enrichment fetch** from an allowed source; the couple can override per article (Q4=A)
> - Failure/empty: **empty/fallback funnies** — tasteful placeholder or clear note, never crash or broken block (Q5=A)
>
> Human auto-approved this summary (answers gone through in 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,85 @@
# Functional Design — Functional Specification (unit: funnies)
> Behavioural specification for the `funnies` library unit — source of truth
> for its workflows and state transitions. Per confirmed answers: AI-assisted
> puzzle construction (Q1=B), 10×10 crossword (Q2=B), real-xkcd cartoon search +
> local embed (Q3=A), content-enrichment comics (Q4=A), graceful empty/fallback
> (Q5=A).
## Context
`funnies` builds the newspaper's fun section from the content itself. The
generator (U1) passes a `FunniesBrief` (theme words + content context) and
receives a `FunniesResult` (crossword, find-a-word, cartoon, comics) that the
layout embeds. Content-enrichment via internet fetch is allowed at build time;
the emitted page stays self-contained (NFR4).
## Workflow: build the funnies section
1. The generator invokes funnies with a `FunniesBrief` (theme words + context).
2. Funnies builds a **find-a-word** from the theme words (BR1.3) and an
**AI-drafted 10×10 crossword** — it asks the cloud model to draft across/down
clues and lays them out (BR1.2).
3. Funnies searches the **real xkcd site** for a cartoon related to the
occasion/couple themes and embeds it locally (BR2.1); if none found, it falls
back to a tasteful content-derived strip (BR2.2).
4. If comic strips are requested, funnies pulls a tasteful/funny comic from an
allowed content-enrichment source and embeds it; the couple may override per
article (BR3.1).
5. Funnies assembles everything into a `FunniesResult` and returns it to
Generator (Contract 2).
## State transitions — FunniesRequest
```
IDLE --generator requests funnies--> BUILDING
BUILDING --all puzzles + cartoon + comics built--> READY
BUILDING --some pieces fallback (no cartoon/comic)--> FALLBACK (status=fallback, Q5=A)
BUILDING --no viable content at all--> EMPTY (status=empty + statusMessage, Q5=A)
READY/FALLBACK/EMPTY --result returned--> EMBEDDED (layout places it, BR4.1)
```
- **IDLE** — funnies not active.
- **BUILDING** — deriving theme words, drafting clues, fetching cartoon, laying out.
- **READY** — full funnies output produced (crossword + find-a-word + cartoon + comics, as available).
- **FALLBACK** — a tasteful placeholder/strip substituted where content was insufficient (BR2.2).
- **EMPTY** — nothing viable; statusMessage set; no broken block (BR3.2/Q5=A).
## Error handling & edge cases
- **No theme words / weak content** → status=fallback/empty with a clear
statusMessage; never a crash or broken block (Q5=A, BR3.2).
- **xkcd site unreachable / no related comic** → tasteful content-derived fallback
strip, never a remote dependency at print (BR2.2, NFR4).
- **Comic source unavailable** → skipped with a note; the couple can override per
article (Q4=A).
- **Puzzle can't be laid out** (AI clues unusable) → find-a-word still builds
from theme words; crossword falls back to an empty note (BR1.3, BR3.2).
## Derived: ER diagram (from entities.md)
```mermaid
erDiagram
FunniesBrief ||--o{ Crossword : "theme words/draft"
FunniesBrief ||--o{ FindAWord : "theme words"
FunniesBrief ||--o{ Cartoon : "context search"
FunniesResult ||--o| Crossword : "maybe null"
FunniesResult ||--o| FindAWord : "maybe null"
FunniesResult ||--o{ Cartoon : "embeds"
FunniesResult ||--o{ Comic : "embeds"
```
## Derived: rules summary (from rules.md)
| ID | Rule (one line) |
|---|---|
| BR1.1 | Derive theme words + context from content |
| BR1.2 | AI-drafted 10×10 crossword |
| BR1.3 | Find-a-word from theme words |
| BR2.1 | xkcd via build-time real-site search, embed locally |
| BR2.2 | Tasteful content-derived fallback when no xkcd |
| BR3.1 | Content-enrichment comics, overridable per article |
| BR3.2 | Graceful empty/fallback on no viable funnies |
| BR4.1 | Self-contained, A4-printable funnies output |
<!-- functional-design funnies 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-13T12:55:00Z — Interpretation — Funnies functional design generated per confirmed answers: AI-assisted puzzle construction, 10x10 crossword, real-xkcd search + local embed, content-enrichment comics, graceful empty/fallback
@@ -0,0 +1,94 @@
# Functional Design — Business Rules (unit: funnies)
> Numbered business rules for the `funnies` library unit. Source of truth in the
> fenced YAML; human summary below. Per confirmed answers: AI-assisted puzzle
> construction (Q1=B), 10×10 crossword (Q2=B), real-xkcd cartoon search + local
> embed (Q3=A), content-enrichment comics (Q4=A), graceful empty/fallback (Q5=A).
```yaml
rules:
- id: BR1.1
statement: Derive theme words and context from the content.
category: calculation
applies_to: funnies
trigger: generator requests funnies with a FunniesBrief
logic: IF a FunniesBrief arrives THEN funnies uses its themeWords and context; IF the brief is empty THEN no puzzle/cartoon can be built.
violation: not applicable
source: US3, Contract 4
- id: BR1.2
statement: Build the crossword with AI-drafted clues on a 10x10 grid.
category: calculation
applies_to: funnies
trigger: a crossword is requested
logic: IF theme words are present THEN funnies asks the cloud model to draft across/down clues and lays them out on a 10x10 grid; the solution grid is generated.
violation: not applicable
source: Q1=B, Q2=B, US3
- id: BR1.3
statement: Build a find-a-word from the theme words.
category: calculation
applies_to: funnies
trigger: a find-a-word is requested
logic: IF theme words are present THEN every listed word is placed in the find-a-word grid (row/col/diagonal).
violation: not applicable
source: US3, Contract 2
- id: BR2.1
statement: Select an xkcd cartoon via a build-time search of the real site.
category: policy
applies_to: funnies
trigger: a cartoon is requested
logic: IF cartoonSource allows internet AND a related xkcd is found THEN fetch and embed it locally (Q3=A); the emitted page stays self-contained (NFR4).
violation: no cartoon is placed; falls back per BR2.2
source: Q3=A, US3, NFR4
- id: BR2.2
statement: Fall back to a tasteful content-derived strip when no cartoon is found.
category: policy
applies_to: funnies
trigger: no suitable xkcd is found
logic: IF no related xkcd is found THEN funnies emits a tasteful content-derived placeholder/strip, never a remote dependency at print (Q3=A fallback, Q5=A).
violation: not applicable
source: Q3=A, Q5=A, mockups R-02
- id: BR3.1
statement: Permit content-enrichment comics, overridable per article.
category: policy
applies_to: funnies
trigger: comic strips are requested
logic: IF the couple requests comics THEN funnies pulls a tasteful/funny comic from an allowed source (content-enrichment) and embeds it; the couple may override per article (Q4=A).
violation: not applicable
source: Q4=A, US3, FR4.4
- id: BR3.2
statement: Fail gracefully when no funnies can be produced.
category: constraint
applies_to: funnies
trigger: no viable content / nothing constructible
logic: IF crossword/find-a-word/cartoon cannot be built THEN FunniesResult.status = empty|fallback with a clear statusMessage; never a crash or broken block (Q5=A).
violation: The page would have a broken/empty funnies section.
source: Q5=A, Contract 4 (graceful failure)
- id: BR4.1
statement: Keep the emitted funnies self-contained and A4-printable.
category: constraint
applies_to: funnies
trigger: funnies output is placed in the page
logic: IF funnies output is placed THEN every puzzle/cartoon/comic is bounded to its column and A4-printable with no overflow (NFR1, NFR2, NFR4).
violation: Print clipping or an overflowed sheet.
source: NFR1, NFR2, NFR4, US3
```
## Rules summary
| ID | Rule | Category | Source |
|---|---|---|---|
| BR1.1 | Derive theme words + context from content | calculation | US3, C4 |
| BR1.2 | AI-drafted 10×10 crossword | calculation | Q1=B, Q2=B, US3 |
| BR1.3 | Find-a-word from theme words | calculation | US3, C2 |
| BR2.1 | xkcd via build-time real-site search, embed locally | policy | Q3=A, US3, NFR4 |
| BR2.2 | Tasteful content-derived fallback when no xkcd | policy | Q3=A, Q5=A |
| BR3.1 | Content-enrichment comics, overridable per article | policy | Q4=A, US3, FR4.4 |
| BR3.2 | Graceful empty/fallback on no viable funnies | constraint | Q5=A, C4 |
| BR4.1 | Self-contained, A4-printable funnies output | constraint | NFR1/2/4, US3 |
@@ -0,0 +1,16 @@
{
"stage": "functional-design",
"unit": "funnies",
"upstream_ids": ["AC3.1.1", "AC3.1.2", "AC3.1.3", "AC3.1.4", "AC3.1.5", "AC3.1.6"],
"coverage": [
{ "id": "AC3.1.1", "status": "OK", "target": "BR1.1, BR1.2" },
{ "id": "AC3.1.2", "status": "OK", "target": "BR1.1, BR1.3" },
{ "id": "AC3.1.3", "status": "OK", "target": "BR3.1" },
{ "id": "AC3.1.4", "status": "OK", "target": "BR2.1, BR3.1" },
{ "id": "AC3.1.5", "status": "OK", "target": "BR2.1, BR2.2" },
{ "id": "AC3.1.6", "status": "OK", "target": "BR2.2, BR4.1" }
],
"reverse": [
{ "id": "BR3.2", "status": "N/A", "target": "graceful failure policy rule" }
]
}
@@ -0,0 +1,23 @@
# Infrastructure Design — CI/CD Pipeline (unit: funnies)
> Posture (Q1=B, Q2=A): repo committed to gitea for local use; a gitea Actions
> **test pipeline** runs funnies' tests; nothing to deploy. The allowed
> cartoon/comic source is the only external touchpoint. Secrets 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 | Funnies tests: crossword/find-a-word build, cartoon-fetch fallback, A4-bounded output |
| Report | Surface pass/fail in gitea |
- Trigger: push/PR to repo main. No deploy stage. Does not hit the live cartoon source in CI (tests mock the fetch).
## Secrets
- The allowed cartoon/comic source config is a local config the user supplies (never committed). No model/source credentials in CI (tests mock the fetch).
@@ -0,0 +1,54 @@
# Infrastructure Design — Questions (unit: funnies)
> Fill in each `[Answer]:` tag. funnies is a `library` unit; per `produces_kinds`
> it owes `cicd-pipeline.md` + `traceability.json`. 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 funnies need?
A) None — no deployment/cloud; repo committed to gitea for local use + versioning (recommended)
B) Minimal — capture local runtime (uv/Python) + the single allowed cartoon/comic fetch as the only external touchpoint
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 funnies?
A) Gitea Actions test pipeline only — runs the tests (puzzle building, fetch fallback), nothing deployed (matches ai-draft)
B) No CI/CD at all
C) Full CI with deploy
X) Other (please specify)
[Answer]: A
## Q3: Secrets / external touchpoints
The only external dependency is the allowed cartoon/comic source. How should it be handled?
A) Local config the user supplies, never committed or hard-coded (recommended)
B) Environment variable only
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the funnies infrastructure answers (mirror ai-draft + gitea test pipeline):
>
> - Posture: minimal — local runtime; allowed cartoon/comic fetch is the only external touchpoint (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:32:00Z — Interpretation — funnies infra: repo committed to gitea for local use; gitea Actions test pipeline (tests only, nothing deployed); allowed cartoon source via local config. Produces cicd-pipeline + traceability.
@@ -0,0 +1,11 @@
{
"stage": "infrastructure-design",
"unit": "funnies",
"upstream_ids": ["NFR9.1"],
"coverage": [
{ "id": "NFR9.1", "status": "OK", "target": "no deployable infrastructure; local config supplies the allowed source (Q3=A); gitea Actions runs tests only" }
],
"reverse": [
{ "id": "NFR1.1", "status": "N/A", "target": "no infrastructure-relevant NFR (local-use tool)" }
]
}
@@ -0,0 +1,42 @@
# NFR Design — Logical Components (unit: funnies)
> Logical infrastructure component inventory for the funnies library unit. Per
> Q2=A: funnies is one isolated library boundary; its blast radius is bounded
> and its failure degrades gracefully to a tasteful placeholder.
## Component inventory
| Logical component | Kind | Failure domain | Blast radius |
|---|---|---|---|
| funnies (library) | library boundary | Isolated to the funnies process | Low — produces (or fallback-returns) a FunniesResult; cannot corrupt the generator or emitted page |
## Boundaries & isolation
- **funnies** is a single isolated library unit (confirmed at domain-design and
units-generation). It has no shared mutable state with the generator or
ai-draft; it communicates only via the in-process FunniesBrief → FunniesResult
contract.
- **Blast radius**: A failure in funnies (fetch/puzzle error, no viable content)
does not propagate to the generator's render, the ai-draft output, or the
emitted page. Per the fail-soft design (Q3=A), it returns a fallback/empty
FunniesResult + status message, and the generator embeds it gracefully.
## Component isolation strategy
- funnies depends on neither the ai-draft logic nor shared infrastructure with
the generator beyond the in-process call boundary.
- Its only external dependency is the allowed cartoon/comic content-enrichment
source (sanctioned), which is not shared with any other unit.
## Shared resource identification
- None beyond the local runtime itself. funnies introduces no database, cache,
or shared queue. (FunniesResult is a data artifact consumed by the generator,
not a shared live resource.)
## Bridge to Infrastructure Design
- The funnies library boundary maps to the `u3-funnies` build unit. No
distributed infrastructure is required; the single allowed content-enrichment
source is its only external dependency. Failure domains are as-isolated as
possible given the one-shot local-tool posture.
@@ -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:26:00Z — Interpretation — funnies NFR Design (mirror ai-draft, all A): minimal-surface security (single narrow fetch), single isolated library boundary, fail-soft graceful degradation, injected structured logger. Produces security-design + logical-components + traceability.
@@ -0,0 +1,67 @@
# NFR Design — Questions (unit: funnies)
> Fill in each `[Answer]:` tag. funnies is a `library` unit: NFR Design produces
> security-design + logical-components + traceability (perf/scalability/
> reliability/observability designs are service-only, N/A). Answers mirror
> ai-draft (human standing instruction) with funnies-appropriate context: the
> one sanctioned external call is the cartoon/comic content-enrichment fetch.
## Q1: Security pattern approach
funnies' NFR requires strict isolation of the cartoon/comic fetch (only the context-search query leaves; assets embedded locally). What security design pattern applies?
A) Minimal-surface — a single narrow fetch client to the allowed cartoon/comic source; context query built locally; fetched asset embedded locally; nothing else leaves the machine (recommended)
B) Defense-in-depth — explicit allow-list of the source + input sanitization before the fetch
C) Zero-trust style — every local→source interaction authenticated/verified
X) Other (please specify)
[Answer]: A
## Q2: Logical component boundary
As a library unit, what should `logical-components.md` capture for blast radius?
A) funnies as a single isolated library boundary — it cannot corrupt the generator/emitted page; its only external effect is producing (or fallback-returning) a FunniesResult; a failure degrades gracefully to a tasteful placeholder/empty (recommended)
B) Split funnies into sub-components (puzzle-builder, fetch-client, cartoon-selector) with separate failure domains
X) Other (please specify)
[Answer]: A
## Q3: Reliability/graceful-degradation pattern
funnies' NFR says best-effort with graceful fallback. What pattern should the design specify?
A) A fail-soft wrapper — on fetch/puzzle failure, funnies returns a fallback/empty FunniesResult + status message; no retry storm; the generator embeds it gracefully (recommended)
B) Circuit-breaker style with bounded retries then fallback
C) Retry-with-backoff a bounded number of times, then fallback
X) Other (please specify)
[Answer]: A
## Q4: Observability design (logical)
funnies' NFR is light logging. What logging design fits a library unit?
A) A small structured logger the generator passes in — one concise line per section build (crossword/find-a-word/cartoon status + fallback triggers); 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 funnies NFR Design answers (mirror ai-draft, human-approved):
>
> - Security: minimal-surface (single narrow fetch, embedded locally)
> - Logical boundary: single isolated library, graceful fallback
> - Reliability: fail-soft wrapper, no retry storm
> - Observability: injected structured logger, one line per section build
>
> 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,40 @@
# NFR Design — Security Design (unit: funnies)
> Minimal-surface security design for the funnies library unit, per Q1=A. Strict
> isolation: only the cartoon/comic content-enrichment fetch is allowed; assets
> are embedded locally; nothing else leaves the machine.
## Approach: minimal-surface
- **Single narrow fetch client**: one thin fetch client in funnies talks only to
the allowed cartoon/comic source (real-xkcd / selected source). No other
outbound call.
- **Context query built locally**: the cartoon-search context (content-derived
themes/occasion) is assembled in-process; only that query travels.
- **Fetched asset embedded locally**: the fetched cartoon/comic is copied to a
local bundled asset; the emitted page never references a remote URL (NFR4).
- **No secrets/credentials**: no keys/tokens/private data embedded, logged, or
transmitted. The fetch is unauthenticated against the allowed public source.
- **Graceful no-result**: if the fetch fails/returns nothing useful, funnies
falls back to a tasteful content-derived strip (never a remote URL at print).
## Design decisions
| Decision | Design |
|---|---|
| Fetch source | A configurable allowed source (real-xkcd / selected); not hard-coded to a secret endpoint |
| Input to fetch | Minimal context query; no secrets |
| Result handling | Downloaded + embedded locally; never kept as a remote dependency |
| Logging | Light, one line per section (Q4=A); never logs query content or secrets |
| Secrets | None |
## Security controls map (from funnies security-requirements)
- **NFR9.1 (allowed fetch only)** — satisfied by minimal-surface single-fetch design.
- **NFR9.2 (no secrets/credentials)** — satisfied by no-embedded-secrets + light logging.
- **NFR9.3 (local-only elsewhere)** — satisfied by the single narrow external fetch; puzzle layout is fully local.
## Verification
- Only one outbound fetch path in funnies (to the allowed cartoon/comic source).
- The emitted page and funnies output make zero network requests (NFR4).
@@ -0,0 +1,13 @@
{
"stage": "nfr-design",
"unit": "funnies",
"upstream_ids": ["NFR9.1", "NFR9.2", "NFR9.3"],
"coverage": [
{ "id": "NFR9.1", "status": "OK", "target": "security-design.md (minimal-surface single fetch client)" },
{ "id": "NFR9.2", "status": "OK", "target": "security-design.md (no secrets/credentials embedded or logged)" },
{ "id": "NFR9.3", "status": "OK", "target": "security-design.md / logical-components.md (local-only; single narrow external fetch)" }
],
"reverse": [
{ "id": "NFR1.1", "status": "N/A", "target": "performance design is service/ui-only (library unit)" }
]
}
@@ -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:10:00Z — Interpretation — funnies NFR mirrors ai-draft (human: same questions/answers): moderate latency, strict isolation (cartoon fetch only), graceful fallback, light logging, plain Python+uv. Library unit -> security + tech-stack only.
@@ -0,0 +1,75 @@
# NFR Requirements — Questions (unit: funnies)
> Fill in each `[Answer]:` tag. Options A-E plus X (Other). The file is the
> authoritative record of the funnies unit's NFR targets.
## Q1: Puzzle generation performance
The funnies unit builds a 10×10 crossword + find-a-word (AI-drafted clues) and embeds a cartoon. How long may the funnies build take?
A) Relaxed — some seconds for puzzle layout + cartoon fetch; no strict latency SLA (local one-shot generation) (recommended)
B) Moderate — aim for under several seconds, log if slower
C) Strict — hard timeout with fallback
X) Other (please specify)
[Answer]:
## Q2: Content privacy / security posture
funnies may fetch a cartoon/comic from the internet at build time (sanctioned content-enrichment). What security posture applies to the fetch?
A) Allow the real xkcd/selected source fetch ONLY — no data other than the context search query leaves; fetched assets are embedded locally and the emitted page stays local (recommended)
B) Strict — no internet fetch, only local/generated cartoons
C) Allow broader enrichment but log every external fetch for review
X) Other (please specify)
[Answer]:
## Q3: Reliability / graceful degradation
When the cartoon fetch or puzzle build fails (already: graceful fallback), how should reliability be handled?
A) Best-effort with graceful fallback — a tasteful placeholder/empty funnies result, the paper builds without that piece, clear message (recommended)
B) Retry the fetch a bounded number of times before falling back
X) Other (please specify)
[Answer]:
## Q4: Observability
How much logging does the local funnies unit need?
A) Light — a concise log per section build (crossword/find-a-word/cartoon status + fallback triggers) enough to debug locally (recommended)
B) More — per-puzzle duration + fetch logs
X) Other (please specify)
[Answer]:
## Q5: Tech stack
Any constraints on the funnies tech stack (Python + uv)?
A) Plain Python + uv, minimal deps — stdlib for layout/grid, a thin HTTP client only for the allowed cartoon/comic fetch (recommended)
B) Add a puzzle-generation library to simplify layout
X) Other (please specify)
[Answer]:
## Consolidated Summary Confirmation
> Summary of the five funnies NFR answers (mirror ai-draft, human-approved):
>
> - Puzzle perf: moderate (Q1=B)
> - Fetch/security: strict isolation, only cartoon fetch query leaves, assets embedded locally (Q2=A)
> - Reliability: best-effort graceful fallback (Q3=A)
> - Observability: light logging (Q4=A)
> - Tech stack: plain Python + uv, thin HTTP client only (Q5=A)
>
> Human auto-approved (same answers as ai-draft recorded in the file).
Does this all look correct before I generate the unit artifacts?
- Looks correct
- Request changes
[Answer]: Looks correct
@@ -0,0 +1,16 @@
# NFR Requirements — Security (unit: funnies)
## NFR9.1 — Allowed internet fetch only for the cartoon/comic (MUST)
- **Target** — The real-xkcd / selected-source fetch is the ONE allowed external call for this unit. Only the context-search query leaves; no secrets/credentials/wedding content beyond the cartoon-search context is transmitted. Fetched assets are embedded locally and the emitted page stays fully local + zero-network (NFR4).
- **Rationale** — Confirmed Q2=A; content-enrichment is allowed but scoped to the cartoon fetch only.
## NFR9.2 — No secrets / credentials embedded (MUST)
- No API keys, tokens, or private data are hard-coded or logged.
## NFR9.3 — Local-only elsewhere (MUST)
- Puzzle layout (crossword/find-a-word) is purely local; the emitted page makes zero network requests (NFR4).
## Threat considerations (advisory)
- The cartoon fetch is the exposure surface; scope the query to the search context only, embed the result locally, and never resolve remote URLs at print (NFR4).
<!-- nfr funnies after-confirm -->
@@ -0,0 +1,13 @@
# NFR Requirements — Tech Stack Decisions (unit: funnies)
## Decisions
| Choice | Decision | Rationale |
|---|---|---|
| Language | **Python** | Matches the Python + uv generator tool. |
| Runtime | **uv** | The affirmed primary runtime. |
| Fetch client | **Thin HTTP client** | For the allowed cartoon/comic fetch only (Q2/Q5=A). |
| Puzzle layout | **stdlib** | Crossword/find-a-word grid layout in stdlib; no heavy puzzle lib (Q5=A). |
| Dependencies | **Minimal** | Only the thin HTTP client for the sanctioned fetch (Q5=A). |
## Non-decisions (per produces_kinds)
- No DB; no framework (library unit).
@@ -0,0 +1,16 @@
{
"stage": "nfr-requirements",
"unit": "funnies",
"upstream_ids": ["NFR1", "NFR2", "NFR3", "NFR4", "NFR5", "NFR6", "NFR7", "NFR8", "NFR9"],
"coverage": [
{ "id": "NFR1", "status": "N/A", "target": "printability is the generator/emitted-page responsibility, not the funnies library" },
{ "id": "NFR2", "status": "N/A", "target": "layout integrity is the generator layout concern; funnies emits bounded self-contained blocks" },
{ "id": "NFR3", "status": "OK", "target": "NFR9.3" },
{ "id": "NFR4", "status": "OK", "target": "NFR9.1, NFR9.3 (the cartoon fetch is the sanctioned network exception; emitted page zero-network)" },
{ "id": "NFR5", "status": "N/A", "target": "content-read policy applies to the generator/review-page file-picker" },
{ "id": "NFR6", "status": "N/A", "target": "pure dependency-free RENDER is a generator/emitted-page property; funnies uses a thin client only" },
{ "id": "NFR7", "status": "N/A", "target": "markdown fidelity is the generator transform's concern" },
{ "id": "NFR8", "status": "N/A", "target": "broadsheet aesthetic is a generator/design-system concern" },
{ "id": "NFR9", "status": "OK", "target": "NFR9.1, NFR9.2, NFR9.3" }
]
}