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,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" }
]
}