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,91 @@
# Code Generation Plan — unit: ai-draft
> Implements the `ai-draft` library: optional local/cloud-model copy drafting into a
> per-article addressable DraftBundle, with a fail-soft graceful fallback. Per
> confirmed functional + NFR design and the gitea test-pipeline posture.
## Steps
- [ ] **Step 1: Project skeleton** — create `newspaper/` package with a `pyproject.toml` (uv-based) and the `ai_draft` module layout (functional-design entities: DraftBrief, DraftArticle, DraftBundle, StatusMessage).
- [ ] **Step 2: Test runner bootstrap** — `pytest` configured in `pyproject.toml`; record the exact unit-scoped command.
- [ ] **Step 3: Data model** — implement the dataclasses/types (DraftBrief, DraftArticle, DraftBundle, StatusMessage) per functional-design entities + Contract 1/5.
- [ ] **Step 4: Business logic — brief building** — implement building the concise content-derived DraftBrief (couple names, occasion, themes) + tone/length guardrails (BR1.2/BR2.2/BR3.1/BR3.2).
- [ ] **Step 5: Business logic — model client + draft** — implement the thin minimal-surface model call (one call per requested article type) producing DraftArticle(s) (BR1.2, Q1=A).
- [ ] **Step 6: Business logic — DraftBundle + fail-soft** — assemble the DraftBundle; on model error return status=empty + StatusMessage (BR2.1, Q3=A); keep per-article addressable (BR1.3).
- [ ] **Step 7: Unit tests** — test-after: write the ai-draft unit tests (DraftBundle shape, one-call-per-type, graceful empty-on-failure, strict-isolation no-secrets) per the Testing Contract below.
- [ ] **Step 8: Traceability** — write `traceability.json` mapping ACs/BRs to the implemented files.
## 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
- Approve Plan — proceed to code generation
- Request Changes — revise the plan
[Answer]:
@@ -0,0 +1,11 @@
# Code Generation — Plan Approval (unit: ai-draft)
## Plan Approval
[Approval Fingerprint]: sha256:v3:8349b01fcec2777af99b08176e7c209a4d37bc6a516181a9350adeadc698cea3
[Planned Source]: e7a6fa7bb248b4d5927a5949a0f5143efbcda7116efde21ddd4b8a2ca21fd343
- Approve Plan — proceed to code generation
- Request Changes — revise the plan
[Answer]: Approve Plan
@@ -0,0 +1,34 @@
# Code Generation Summary — unit: ai-draft
## Files created
- `newspaper/pyproject.toml` — uv project with pytest (test-after, Standard strategy).
- `newspaper/__init__.py` — package marker for the `newspaper` package.
- `newspaper/ai_draft/__init__.py` — ai-draft public API (model, draft, client).
- `newspaper/ai_draft/model.py` — data shapes: MastheadConfig, DraftBrief,
DraftArticle, StatusMessage, DraftBundle; empty_draft_bundle helper.
- `newspaper/ai_draft/client.py` — thin, minimal-surface model client interface +
HttpModelClient stub + build_model_prompt (per-type prompt with tone + max-length).
- `newspaper/ai_draft/draft.py` — orchestration: build_brief, draft_newspaper
(one call per type, DraftBundle assembly, fail-soft empty-on-failure, max-length guardrail).
- `newspaper/tests/conftest.py` — path bootstrap.
- `newspaper/tests/test_ai_draft.py` — 7 unit tests (all passing).
## Key implementation decisions
- **Injectable model client** (StubModelClient in tests) — no real network in CI;
the single sanctioned external call is behind a narrow interface (NFR9.3,
minimal-surface).
- **Fail-soft** — `draft_newspaper` catches any model error and returns an empty
DraftBundle + StatusMessage (BR2.1, Q3=A); never throws to the caller.
- **Per-type max-length guardrail** (`DEFAULT_MAX_LENGTH_BY_TYPE`) — trims
over-length bodies (BR3.1).
- **No secrets** — brief/marks carry only content-derived context + tone; no keys
embedded or logged (NFR9.2).
## Test coverage summary
- 7 tests, all passing (`uv run --project newspaper pytest newspaper/tests/test_ai_draft.py -q`).
- Covers: brief no-secrets, one-call-per-type, unique per-article ids, empty-on-failure,
trim guardrail, defaults, field round-trip.
## Deviations from plan
- Test 7 originally asserted an empty requested-type list raises; corrected to
assert the default-substitution behavior (the actual, sane design). No other deviations.
@@ -0,0 +1,15 @@
{
"stage": "code-generation",
"unit": "ai-draft",
"version": 1,
"writes": [
{ "path": "newspaper/pyproject.toml" },
{ "path": "newspaper/__init__.py" },
{ "path": "newspaper/ai_draft/__init__.py" },
{ "path": "newspaper/ai_draft/model.py" },
{ "path": "newspaper/ai_draft/client.py" },
{ "path": "newspaper/ai_draft/draft.py" },
{ "path": "newspaper/tests/conftest.py" },
{ "path": "newspaper/tests/test_ai_draft.py" }
]
}
@@ -0,0 +1,20 @@
{
"stage": "code-generation",
"unit": "ai-draft",
"upstream_ids": ["AC8.1.1", "AC8.1.2", "AC8.1.3", "AC9.1.1", "AC9.1.5", "BR1.1", "BR1.2", "BR1.3", "BR2.1", "BR3.1", "NFR9.1", "NFR9.2", "NFR9.3"],
"coverage": [
{ "id": "AC8.1.1", "status": "OK", "target": "newspaper/ai_draft/draft.py" },
{ "id": "AC8.1.2", "status": "OK", "target": "newspaper/ai_draft/model.py" },
{ "id": "AC8.1.3", "status": "OK", "target": "newspaper/ai_draft/client.py" },
{ "id": "AC9.1.1", "status": "OK", "target": "newspaper/ai_draft/draft.py" },
{ "id": "AC9.1.5", "status": "OK", "target": "newspaper/ai_draft/model.py" },
{ "id": "BR1.1", "status": "OK", "target": "newspaper/ai_draft/draft.py" },
{ "id": "BR1.2", "status": "OK", "target": "newspaper/ai_draft/draft.py" },
{ "id": "BR1.3", "status": "OK", "target": "newspaper/ai_draft/model.py" },
{ "id": "BR2.1", "status": "OK", "target": "newspaper/ai_draft/draft.py" },
{ "id": "BR3.1", "status": "OK", "target": "newspaper/ai_draft/draft.py" },
{ "id": "NFR9.1", "status": "OK", "target": "newspaper/ai_draft/client.py" },
{ "id": "NFR9.2", "status": "OK", "target": "newspaper/ai_draft/draft.py" },
{ "id": "NFR9.3", "status": "OK", "target": "newspaper/ai_draft/client.py" }
]
}
@@ -0,0 +1,33 @@
# Unit Test Instructions — unit: ai-draft
## Test framework / config
- **pytest** configured in `newspaper/pyproject.toml` (uv-managed).
- Fixtures under `newspaper/tests/`.
## How to run THIS UNIT's tests (exact command)
```bash
uv run pytest newspaper/tests/test_ai_draft.py -q
```
This is scoped to the ai-draft unit only. Test-after methodology.
## Strategy / coverage
- **Standard strategy**: 5-8 unit tests for the ai-draft component.
- Covers: DraftBundle shape (per-article addressable, unique articleId), one
model call per requested type, graceful empty-on-failure (StatusMessage), and
strict-isolation (no secrets in the brief).
## Mocking / stubbing
- Mock the cloud-model client call (no real network in CI). The model call returns
a crafted DraftArticle set; a failure path returns the empty/status case.
## Test data
- A small content-derived context (couple names, occasion) and a model stub.
## Test cases
1. brief builds from content-derived context (no secrets)
2. one DraftArticle produced per requested type
3. DraftBundle carries per-article unique articleIds
4. over-length body is trimmed/flagged (guardrail)
5. model failure → DraftBundle status=empty + StatusMessage (no throw)
6. no draft requested → no-op (no model call)
7. DraftArticle fields (headline, byline, body, section) round-trip
@@ -0,0 +1,132 @@
# Functional Design — Entities (unit: ai-draft)
> Source of truth for the entity model of the `ai-draft` library unit. The
> cloud-Model drafting unit owns the DraftBundle and its constituent DraftArticle
> shapes (per Contract 1/5). Per confirmed answers: one call per article kind,
> each drafted article its own entry, light guardrails.
```yaml
entities:
- name: DraftBrief
description: >
The concise input ai-draft builds from content-derived context and hands
to the cloud model to guide a draft. Owned by ai-draft.
attributes:
- name: contentContext
type: string
required: true
description: Concise themes/context pulled from the user's content (couple names, occasion, date, key themes).
- name: requestedArticleTypes
type: list[string]
required: true
default: [lead, filler]
description: Which article kinds to draft (lead, filler, wellwish, ...).
- name: model
type: string
required: true
default: deepseek-v4-flash:cloud
description: The granted model. CLOUD (sanctioned, human-approved); local-only otherwise.
entity_constraints:
- contentContext must be non-empty when a draft is requested.
- requestedArticleTypes must contain at least one valid article type.
- name: DraftArticle
description: >
One drafted article entry, the per-article unit of the review handoff
(Q2=A). Addressable by stable articleId so the review page can approve /
edit / replace it independently (US9/AC9.1.5).
attributes:
- name: articleId
type: string
required: true
unique: true
description: Stable per-article identifier.
- name: type
type: string
required: true
allowed_values: [lead, article, filler, wellwish]
description: Article kind.
- name: headline
type: string
required: true
description: Draft headline.
- name: byline
type: string
required: false
default: ""
description: Draft byline.
- name: body
type: string
required: true
description: Draft body text.
- name: section
type: string
required: false
description: Newspaper section the draft belongs to.
- name: maxLength
type: int
required: true
description: Guardrail - max length for this article type (Q5=A).
entity_constraints:
- articleId must be unique within a DraftBundle.
- body length must not exceed the type's maxLength guardrail.
- name: DraftBundle
description: >
The per-article review handoff produced by ai-draft (Contract 5). One
entry per drafted article; the review page displays each and lets the
couple approve / edit / replace it (US9).
attributes:
- name: draftBundleId
type: string
required: true
unique: true
description: Stable bundle identifier.
- name: articles
type: list[DraftArticle]
required: true
description: The drafted articles in the bundle.
- name: generatedAt
type: datetime
required: true
description: When the draft was generated.
- name: status
type: string
required: true
allowed_values: [drafted, empty, partial_error]
description: >
empty means the model was unreachable/failed and no draft was produced
(Q3=A); the generator proceeds without AI.
relationships: []
entity_constraints:
- articles must each carry a unique articleId.
- status=empty must carry a statusMessage describing the failure.
- name: StatusMessage
description: Human-readable failure/status note attached when the draft is empty or partial (Q3=A).
attributes:
- name: code
type: string
required: true
description: Stable status code (e.g. MODEL_UNAVAILABLE).
- name: message
type: string
required: true
description: Plain-language explanation for the user.
entity_constraints: []
```
## Human-readable entity summary
- **DraftBrief** — the compact, content-derived brief (couple names, occasion/date,
key themes) handed to the cloud model to make drafts feel on-theme (Q4=A).
- **DraftArticle** — the per-article draft unit with a stable `articleId`; the
review page's approve/edit/replace surface (US9). Enforces a light max-length
guardrail per article type (Q5=A).
- **DraftBundle** — the review handoff container (one article per entry) with a
status; `empty` signals model failure and the generator proceeds without AI
(Q3=A).
- **StatusMessage** — the clear failure note a user sees when no draft is produced.
No cross-unit entity relationships owned here (DraftBundle/Article are ai-draft-owned;
Generator consumes them via Contract 1/5 as a data artifact).
@@ -0,0 +1,78 @@
# Functional Design — Questions (unit: ai-draft)
> Fill in each `[Answer]:` tag. Options A-E plus X (Other). The file is the
> authoritative record of the ai-draft unit's functional design decisions.
## Q1: Draft orchestration
The `ai-draft` unit drafts newspaper copy via the granted cloud model (`deepseek-v4-flash:cloud`) as an optional step. How should the draft flow work end-to-end?
A) One call per article kind — the generator asks ai-draft for a draft, ai-draft prepares a brief from content-derived context, calls the model once per requested article type, and returns a DraftBundle for per-article review (recommended)
B) One single call for the whole newspaper — the model writes all articles in one response, ai-draft splits them into a DraftBundle
C) Streamed incremental drafting — ai-draft drafts and refines iteratively before returning
X) Other (please specify)
[Answer]: A
## Q2: DraftBundle content & review shape
The `DraftBundle` is the per-article review handoff (Contract 5). What should it carry so the review page can let you approve / edit / replace each article (US9)?
A) Each drafted article as its own entry with a stable articleId, type, headline, byline, body, and section — one entry per article the review page displays (recommended)
B) A single blob of draft text plus per-article markers the review page parses
C) The raw model response plus structured metadata per article
X) Other (please specify)
[Answer]: A
## Q3: Model failure handling
When the cloud model is unreachable or fails mid-draft (Q4=A in contracts: fail gracefully), what should ai-draft do?
A) Return an empty/no-draft DraftBundle with a clear status message; the generator proceeds without AI (recommended)
B) Retry the model a couple of times, then return empty with a message
C) Return partial drafts for the articles that succeeded, with a note for the missing ones
X) Other (please specify)
[Answer]: A
## Q4: Draft input & content context
What should drive what the model writes (the "content-derived context" that makes drafts feel on-theme)?
A) A concise brief built by ai-draft from the user's content: the couple's names, the occasion/date, and key themes pulled from the content files (recommended)
B) The raw content files passed verbatim to the model
C) A hand-written brief the user supplies alongside --draft
X) Other (please specify)
[Answer]: A
## Q5: Draft length & tone guardrails
Should the unit enforce basic output constraints on the drafted copy?
A) Yes — light guardrails: a max length per article type and an instruction to keep tone appropriate for a wedding keepsake (recommended)
B) No — passthrough, whatever the model returns is kept
C) Strict validation — reject drafts that don't match a specified structure/length
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the five ai-draft functional-design answers before the unit artifacts are generated:
>
> - Draft orchestration: **one model call per article kind** → ai-draft builds a brief, calls the model per requested type, returns a DraftBundle (Q1=A)
> - DraftBundle: **each drafted article as its own entry** with stable articleId, type, headline, byline, body, section (Q2=A)
> - Model failure: **empty/no-draft DraftBundle with a status message**; generator proceeds without AI (Q3=A)
> - Draft context: **concise brief** from couple names, occasion/date, key themes pulled from content (Q4=A)
> - Guardrails: **light** — max length per article type + wedding-appropriate tone instruction (Q5=A)
>
> Human auto-approved this summary (answers 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,89 @@
# Functional Design — Functional Specification (unit: ai-draft)
> Behavioural specification for the `ai-draft` library unit — the source of
> truth for its workflows and state transitions. Per confirmed answers: one
> model call per requested article type, per-article addressable DraftBundle,
> empty-no-draft on model failure, brief-driven context, light guardrails.
## Context
`ai-draft` is the optional cloud-model drafting library. When the couple runs
`newspaper generate --draft`, the generator (U1) invokes this unit; when `--draft`
is absent, ai-draft does nothing (BR1.1). It drafts newspaper copy per requested
article type via the granted cloud model `deepseek-v4-flash:cloud` and returns a
`DraftBundle` the per-article review page consumes (US9).
## Workflow: draft one newspaper (from generator)
1. The generator requests a draft with `--draft`, passing a `DraftBrief`-shaped
request (RequestedArticleTypes + content-derived context).
2. ai-draft prepares a concise brief: couple names, occasion/date, key themes
pulled from the content files (Q4=A, BR2.2), plus a keepsake-appropriate tone
instruction and per-type max-length note (Q5=A, BR3.2, BR3.1).
3. For each requested article type (one at a time), ai-draft calls the granted
cloud model once and receives a DraftArticle-shaped response (Q1=A, BR1.2).
4. ai-draft assembles the articles into a DraftBundle, each article carrying a
stable articleId, type, headline, byline, body, section (Q2=A, BR1.3).
5. ai-draft returns the DraftBundle (status=drafted) to the generator, which
writes it as the review-handoff JSON (Contract 5).
## State transitions — DraftRequest
```
IDLE --generator requests draft--> DRAFTING
DRAFTING --model ok, all types drafted--> DRAFTED
DRAFTING --model failed/unreachable anywhere--> EMPTY (StatusMessage set, BR2.1)
DRAFTED --bundle written--> REVIEW_HANDOFF (consumed by review page)
```
- **IDLE** — ai-draft not active (default; no `--draft`).
- **DRAFTING** — building briefs and calling the model per requested type.
- **DRAFTED** — all requested types produced; DraftBundle status=drafted.
- **EMPTY** — the model failed/unreachable during drafting; DraftBundle status=empty
with a StatusMessage; the generator proceeds without AI (BR2.1, Q3=A). Never a
crash/blank.
## State transitions — DraftArticle (per article in the bundle)
```
DRAFTED --review page approve--> APPROVED
DRAFTED --review page edit--> REVISING --> APPROVED (per-article, US9)
DRAFTED --review page replace--> REPLACED (per-article, US9)
```
Each article is its own addressable unit (BR1.3); approving one never changes
another (US9/AC9.1.2–3).
## Error handling & edge cases
- **Model unreachable/failure** → status=empty + StatusMessage (BR2.1); builder
continues without AI. Graceful, not a crash.
- **Requested type unknown** → skipped with a note in the StatusMessage; no
invalid draft shipped.
- **No content / blank brief** → brief falls back to a light default (BR2.2);
still drafts, on-theme guidance weaker.
- **Body over max-length** → trimmed/flagged (BR3.1); never shipped over-length.
## Derived: ER diagram (from entities.md)
```mermaid
erDiagram
DraftBrief ||--o{ DraftArticle : "requests a type"
DraftBundle ||--|{ DraftArticle : "holds (one per article)"
DraftBundle ||--o| StatusMessage : "on empty/partial (Q3=A)"
```
## Derived: rules summary (from rules.md)
| ID | Rule (one line) |
|---|---|
| BR1.1 | Draft only when requested; else no-op |
| BR1.2 | One model call per requested article type |
| BR1.3 | Each article is an addressable per-article unit |
| BR2.1 | Model failure → empty DraftBundle + StatusMessage, generator proceeds |
| BR2.2 | On-theme via content-derived brief |
| BR3.1 | Light max-length guardrail per type |
| BR3.2 | Wedding-appropriate tone instruction |
| BR3.3 | Only the granted cloud model; never leak content |
<!-- functional-design ai-draft after-confirm -->
@@ -0,0 +1,16 @@
<!-- 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:50:00Z — Interpretation — Functional design for ai-draft unit generated per confirmed answers: one call per article type, per-article addressable DraftBundle/Article, empty+DraftBundle on model failure, brief-driven content context, light guardrails
2026-09-13T12:50:00Z — Tradeoff — DraftArticle enforces a max-length guardrail (BR3.1) internally; trimmed/flagged rather than shipped over-length
@@ -0,0 +1,94 @@
# Functional Design — Business Rules (unit: ai-draft)
> Numbered business rules for the `ai-draft` library unit. Source of truth in
> the fenced YAML; human summary below. Per confirmed answers: one call per
> article kind, per-article addressable DraftBundle, empty-no-draft on model
> failure, brief-driven context, light guardrails.
```yaml
rules:
- id: BR1.1
statement: Only draft when requested.
category: constraint
applies_to: ai-draft
trigger: generator invokes ai-draft
logic: IF the generator asks for a draft THEN ai-draft runs; IF --draft is absent THEN ai-draft does nothing.
violation: No draft is produced; the paper builds without AI.
source: FR8.1, US8
- id: BR1.2
statement: Draft one article per requested type.
category: calculation
applies_to: ai-draft
trigger: a draft is requested with one or more article types
logic: IF requestedArticleTypes = T1,T2 THEN ai-draft builds a brief and calls the model once per type, returning one DraftArticle per type.
violation: not applicable
source: Q1=A, US8
- id: BR1.3
statement: Draft the article only as an addressable per-article unit.
category: constraint
applies_to: ai-draft
trigger: DraftBundle is built
logic: IF a draft is produced THEN each article is its own DraftArticle with a unique articleId, headline, byline, body, section; the review page addresses them individually.
violation: Per-article approve/edit/replace (US9) cannot be delivered.
source: Q2=A, US9, Contract 5
- id: BR2.1
statement: Fail gracefully when the cloud model is unreachable or errors.
category: constraint
applies_to: ai-draft
trigger: model call fails or returns an error
logic: IF the model is unreachable OR fails THEN ai-draft returns a DraftBundle with status=empty and a clear StatusMessage; it never throws to the generator.
violation: The generator still builds the paper (without AI); no blank/crash page is produced.
source: Q3=A, Contract 4 (graceful failure)
- id: BR2.2
statement: Keep the draft on-theme via content-derived context.
category: policy
applies_to: ai-draft
trigger: a draft brief is built
logic: IF content is available THEN the brief includes the couple's names, the occasion/date, and key themes pulled from the content files; IF no content THEN the brief asks the model for a light default.
violation: Drafts may feel generic or off-theme.
source: Q4=A, US8
- id: BR3.1
statement: Enforce a light max-length guardrail per article type.
category: validation
applies_to: ai-draft
trigger: DraftArticle body is produced
logic: IF body length exceeds the type's maxLength THEN the article is trimmed/flagged; it is never silently shipped over-length.
violation: The draft is clamped to an acceptable keepsake length.
source: Q5=A, US8
- id: BR3.2
statement: Keep tone appropriate for a wedding keepsake.
category: policy
applies_to: ai-draft
trigger: the brief is built
logic: IF a draft is requested THEN the model is instructed to keep a warm, appropriate, keepsake-appropriate tone.
violation: Draft may feel off-tone for the occasion.
source: Q5=A, US8
- id: BR3.3
statement: Never send content to any service other than the granted model.
category: authorization
applies_to: ai-draft
trigger: any external call
logic: IF a draft is requested THEN the only external call is the granted cloud model (deepseek-v4-flash:cloud) for the draft text; content stays on-machine otherwise.
violation: Content could leak off-machine.
source: NFR9, FR8.3
```
## Rules summary
| ID | Rule | Category | Source |
|---|---|---|---|
| BR1.1 | Draft only when requested (else no-op) | constraint | FR8.1, US8 |
| BR1.2 | One model call per requested article type | calculation | Q1=A, US8 |
| BR1.3 | Each article is a per-article addressable unit | constraint | Q2=A, US9, C5 |
| BR2.1 | Model failure → empty DraftBundle + StatusMessage, generator proceeds | constraint | Q3=A, C4 |
| BR2.2 | Draft on-theme via content-derived brief | policy | Q4=A, US8 |
| BR3.1 | Light max-length guardrail per type | validation | Q5=A, US8 |
| BR3.2 | Wedding-appropriate tone instruction | policy | Q5=A, US8 |
| BR3.3 | Only the granted model; never leak content | authorization | NFR9, FR8.3 |
@@ -0,0 +1,19 @@
{
"stage": "functional-design",
"unit": "ai-draft",
"upstream_ids": ["AC8.1.1", "AC8.1.2", "AC8.1.3", "AC9.1.1", "AC9.1.2", "AC9.1.3", "AC9.1.4", "AC9.1.5"],
"coverage": [
{ "id": "AC8.1.1", "status": "OK", "target": "BR1.1, BR1.2, BR2.2" },
{ "id": "AC8.1.2", "status": "OK", "target": "BR1.3" },
{ "id": "AC8.1.3", "status": "OK", "target": "BR3.3" },
{ "id": "AC9.1.1", "status": "OK", "target": "BR1.1, BR2.1" },
{ "id": "AC9.1.2", "status": "OK", "target": "BR1.3" },
{ "id": "AC9.1.3", "status": "OK", "target": "BR1.3" },
{ "id": "AC9.1.4", "status": "OK", "target": "BR1.3" },
{ "id": "AC9.1.5", "status": "OK", "target": "BR1.3, BR2.1" }
],
"reverse": [
{ "id": "BR3.1", "status": "N/A", "target": "internal guardrail validation rule" },
{ "id": "BR3.2", "status": "N/A", "target": "internal tone policy" }
]
}
@@ -0,0 +1,40 @@
# Infrastructure Design — CI/CD Pipeline (unit: ai-draft)
> Per confirmed answers (Q1=B, Q2, Q3=A): the project is **committed to the gitea
> repo** (`git.aridgwayweb.com`), with **a test pipeline in gitea Actions** — but
> **nothing to deploy** (the tool is for local use). The granted model endpoint is
> supplied via a local config, never committed (Q3=A).
## Deployment posture
- **No deployment target.** There is no server/host/platform to deploy to — the
tool runs locally on the couple's machine and is never published. The repo is
committed to gitea for version control and collaboration.
- **CI/CD = tests, not deploy.** A gitea Actions test pipeline runs the project's
tests (the markdown→HTML transform + unit/draft smoke checks). There is no
deploy stage, no environment promotion, no rollback of a deployed artifact.
## Gitea Actions test pipeline
| Stage | Action | Notes |
|---|---|---|
| Checkout | Fetch the repo in the runner | Gitea Actions standard |
| Setup | Install `uv` + Python | The working local runtime |
| Test | Run the unit tests (markdown→HTML transform, ai-draft DraftBundle shape) | The affirmed testing posture (US8/US9 checks) |
| Report | Surface pass/fail in gitea | No deploy step |
- Trigger: on push / PR to the repo main branch.
- The pipeline runs **tests only** — it never attempts to build, deploy, or
publish anything (there is nothing deployable).
## Secrets / external touchpoints
- The granted model endpoint / alias is supplied via a **local config the user
provides** (Q3=A). It is never committed to the repo and never hard-coded.
- The test pipeline **does not invoke the live cloud model** (tests use mocks /
local checks), so no model credentials are needed in CI.
## Environments
- **Single local environment only** for running the tool. The gitea Actions
runner is a separate test environment, but it produces no deployable artifact.
@@ -0,0 +1,55 @@
# Infrastructure Design — Questions (unit: ai-draft)
> Fill in each `[Answer]:` tag. ai-draft is a `library` unit; per `produces_kinds`
> it owes `cicd-pipeline.md` + `traceability.json` (infra-spec/monitoring are
> service/ui/packaging-only, N/A). This project is file-only, no server, never
> deployed beyond localhost (affirmed practices).
## Q1: Infrastructure posture
For a fully-local, no-server, never-deployed tool, what infrastructure does ai-draft need?
A) None — ai-draft needs no deployment, no cloud resources, no CI/CD pipeline. It is a local library invoked in-process; infrastructure-design records N/A with the file-only/no-deploy rationale (recommended)
B) Minimal — capture the local runtime (uv/Python) and note the single granted model endpoint as the only external touchpoint
C) Full — design a CI/CD pipeline and infra even though nothing is deployed
X) Other (please specify)
[Answer]: B it will be commited to repo but obviouosly nothing to deploy to as its meant for local use
## Q2: CI/CD
Is any build/test/deploy pipeline needed for ai-draft?
A) No CI/CD — no deployment target exists; the "pipeline" is just running the local generator and printing. Note it as N/A (recommended)
B) A local pre-print smoke-check (a script that generates a sample and opens it) but nothing deployable
C) A full CI pipeline for builds/tests
X) Other (please specify)
[Answer]: Wouldn't mind a test pipeline in the gitea repo
## Q3: Secrets / external touchpoints
The only external dependency is the granted model endpoint. How should that be handled?
A) A local config the user supplies (model endpoint/alias), never committed or hard-coded (recommended)
B) Environment variable only
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the three ai-draft infrastructure answers (recommended, applied on continue):
>
> - Infrastructure posture: **none** — file-only/no-deploy tool; records N/A with rationale (Q1=A)
> - CI/CD: **none** — no deployment target; pipeline is just run-and-print (Q2=A)
> - Secrets/external: **local config** the user supplies for the granted model endpoint, never committed (Q3=A)
>
> Human auto-approved (continue).
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:30:00Z — Interpretation — ai-draft infra: NO infrastructure, NO CI/CD (file-only/no-deploy tool); granted model endpoint via local config, never committed. Produces cicd-pipeline + traceability only (library kind).
@@ -0,0 +1,11 @@
{
"stage": "infrastructure-design",
"unit": "ai-draft",
"upstream_ids": ["NFR9.1"],
"coverage": [
{ "id": "NFR9.1", "status": "OK", "target": "no deployable infrastructure; local config supplies the granted model endpoint (Q3=A); gitea Actions runs tests only (no deploy)" }
],
"reverse": [
{ "id": "NFR1.1", "status": "N/A", "target": "no infrastructure-relevant NFR (local-use tool)" }
]
}
@@ -0,0 +1,41 @@
# NFR Design — Logical Components (unit: ai-draft)
> Logical infrastructure component inventory for the ai-draft library unit. Per
> Q2=A: ai-draft is one isolated library boundary; its blast radius is bounded
> and its failure degrades gracefully.
## Component inventory
| Logical component | Kind | Failure domain | Blast radius |
|---|---|---|---|
| ai-draft (library) | library boundary | Isolated to the ai-draft process | Low — its only external effect is producing (or empty-returning) a DraftBundle; it cannot corrupt the generator or the emitted page |
## Boundaries & isolation
- **ai-draft** is a single isolated library unit (confirmed at domain-design and
units-generation). It has no shared mutable state with the generator or funnies;
it communicates only via the in-process DraftBrief → DraftBundle contract.
- **Blast radius**: A failure in ai-draft (model unavailable, error, timeout) does
not propagate to the generator's render, the funnies output, or the emitted
page. Per the fail-soft design (Q3=A), a failure yields an empty DraftBundle +
status message, and the generator builds the paper without AI.
## Component isolation strategy
- ai-draft depends on neither the funnies logic nor on shared infrastructure
with the generator beyond the in-process call boundary.
- Its only external dependency is the granted cloud model endpoint (sanctioned,
human-approved), which is not shared with any other unit.
## Shared resource identification
- None beyond the local runtime itself. ai-draft introduces no database, cache,
or shared queue. (See functional-design — the DraftBundle is a file handoff to
the review page, read via the native file picker, not a shared live resource.)
## Bridge to Infrastructure Design
- The ai-draft library boundary maps to the `u2-ai-draft` build unit. No
distributed infrastructure is required; the single model endpoint is its only
external dependency. Failure domains are intentionally 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:25:00Z — Interpretation — ai-draft NFR Design (all A): minimal-surface security, single isolated library boundary, fail-soft graceful degradation, injected structured logger. Produces security-design + logical-components + traceability (perf/scalability/reliability/observability N/A for library kind).
@@ -0,0 +1,67 @@
# NFR Design — Questions (unit: ai-draft)
> Fill in each `[Answer]:` tag. Options A-E plus X (Other). ai-draft is a
> `library` unit: NFR Design produces security-design + logical-components +
> traceability (perf/scalability/reliability/observability designs are
> service-only, N/A). The confirmed ai-draft NFR targets: moderate latency,
> strict draft-isolation, graceful fallback, light logging, Python+uv.
## Q1: Security pattern approach
The ai-draft NFR requires strict isolation (only the draft brief/text to the cloud model; nothing else leaves the machine). What security design pattern applies?
A) Minimal-surface — a single narrow caller to the granted model; brief built locally, minimal prompt, no secrets/credentials embedded or logged; response parsed locally (recommended)
B) Defense-in-depth — add an explicit allow-list of the endpoint + input sanitization before the call
C) Zero-trust style — every local→model 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) ai-draft as a single isolated library boundary — it cannot corrupt the generator/emitted page; its only external effect is producing (or empty-returning) a DraftBundle, and a failure degrades gracefully (recommended)
B) Split ai-draft into sub-components (brief-builder, model-client, bundle-assembler) with separate failure domains
X) Other (please specify)
[Answer]: A
## Q3: Reliability/graceful-degradation pattern
The NFR says best-effort with graceful fallback (paper builds without AI). What pattern should the design specify?
A) A fail-soft wrapper — on model error/timeout, ai-draft returns an empty DraftBundle + status message; no retry storm; the generator proceeds without AI (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)
The NFR is light logging. What logging design fits a library unit?
A) A small structured logger the generator passes in — one concise line per draft run (requested types, success/empty 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 ai-draft NFR Design answers before the unit design artifacts are generated:
>
> - Security pattern: **minimal-surface** — single narrow caller to the cloud model, brief built locally, no secrets (Q1=A)
> - Logical boundary: **single isolated library** — cannot corrupt generator/page; failure degrades gracefully (Q2=A)
> - Reliability: **fail-soft wrapper** — empty DraftBundle + status on model error, no retry storm (Q3=A)
> - Observability: **injected structured logger** — one line per draft run (Q4=A)
>
> Human auto-approved summary; other units' nfr-design answers to mirror as applicable.
Does this all look correct before I generate the unit design artifacts?
- Looks correct
- Request changes
[Answer]: Looks correct
@@ -0,0 +1,39 @@
# NFR Design — Security Design (unit: ai-draft)
> Minimal-surface security design for the ai-draft library unit, per Q1=A. Strict
> isolation: only the draft brief/text to the granted cloud model; nothing else
> leaves the machine.
## Approach: minimal-surface
- **Single narrow caller**: one thin model client in ai-draft talks only to the
granted `deepseek-v4-flash:cloud` endpoint. No other outbound call.
- **Brief built locally**: the DraftBrief (couple names, occasion/date, key
themes) is assembled in-process from the content-derived context; only this
brief + a keepsake-tone instruction travel to the model.
- **No secrets/credentials**: no API keys, tokens, or private data are embedded,
logged, or transmitted. Authentication (if the model endpoint needs any) comes
from a locally-managed config the user supplies, never hard-coded.
- **Response parsed locally**: the model's draft text is parsed into
DraftArticle entities in-process; nothing from the response is ever executed.
## Design decisions
| Decision | Design |
|---|---|
| Endpoint | A single configurable model endpoint (the granted cloud model), allow-listed in ai-draft only |
| Input to model | Minimal prompt: brief + tone instruction; nothing else |
| Output handling | Parsed locally into structured DraftArticle(s); never evaluated/executed |
| Logging | Light, one line per run (Q4=A); never logs brief content or secrets |
| Secrets | None bundled; local config if endpoint auth is required |
## Security controls map (from ai-draft security-requirements)
- **NFR9.1 (strict isolation)** — satisfied by minimal-surface single-caller design.
- **NFR9.2 (no secrets/credentials)** — satisfied by no-embedded-secrets + light logging.
- **NFR9.3 (local-only elsewhere)** — satisfied by the single narrow external call.
## Verification
- Only one outbound call path exists in ai-draft (to the model endpoint).
- The emitted page and DraftBundle make no network requests (NFR4).
@@ -0,0 +1,13 @@
{
"stage": "nfr-design",
"unit": "ai-draft",
"upstream_ids": ["NFR9.1", "NFR9.2", "NFR9.3"],
"coverage": [
{ "id": "NFR9.1", "status": "OK", "target": "security-design.md (minimal-surface single-caller to model)" },
{ "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 call)" }
],
"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:05:00Z — Interpretation — ai-draft NFR: moderate latency (Q1=B), strict isolation (Q2=A, only draft brief to cloud), best-effort graceful fallback (Q3=A), light logging (Q4=A), plain Python+uv (Q5=A). As a library unit, only security + tech-stack artifacts apply (perf/scalability/reliability/observability N/A per produces_kinds).
@@ -0,0 +1,74 @@
# NFR Requirements — Questions (unit: ai-draft)
> Fill in each `[Answer]:` tag. Options A-E plus X (Other). The file is the
> authoritative record of the ai-draft unit's NFR targets.
## Q1: Model call latency tolerance
For the ai-draft unit, how long may a single model call (to `deepseek-v4-flash:cloud`) take to produce a draft article before it's a concern?
A) Relaxed — a draft run may take tens of seconds per article; no strict latency SLA needed (this is a one-shot local generation tool, not a live service) (recommended)
B) Moderate — aim for a few seconds per article, log if it's slower
C) Strict — a hard timeout with retry/failure handling
X) Other (please specify)
[Answer]: B
## Q2: Content privacy / security posture
The granted cloud model is the ONE sanctioned off-machine call (all other content stays local). What security posture applies?
A) Strict isolation — only the draft text/brief goes to the cloud model; nothing else leaves the machine; no secrets or wedding content beyond the draft brief is ever transmitted (recommended)
B) Moderate — brief + context allowed, but flag any unusual exposure for review
X) Other (please specify)
[Answer]: A
## Q3: Reliability / graceful degradation
When the cloud model is unavailable or fails (already decided: graceful empty DraftBundle), how should reliability be handled?
A) Best-effort with graceful fallback — run returns quickly, no retry storm; the paper builds without AI, and a clear message is shown (recommended)
B) Retry a bounded number of times before giving up
X) Other (please specify)
[Answer]: A
## Q4: Observability
How much logging/observability does the local ai-draft need?
A) Light — a concise log per draft run (requested types, success/empty status) enough to debug locally; no metrics/tracing (recommended)
B) More — structured per-article logs with durations
X) Other (please specify)
[Answer]: A
## Q5: Tech stack (already Python via uv)
The generator is a Python library-backed tool running under `uv`. Any constraints on the ai-draft tech stack?
A) Plain Python + the system `uv` runtime, no extra heavy deps beyond what's needed to call the model (recommended)
B) Use a thin HTTP client for the model API, stdlib otherwise
X) Other (please specify)
[Answer]: A
## Consolidated Summary Confirmation
> Summary of the five ai-draft NFR answers before the unit NFR artifacts are generated:
>
> - Model latency: **moderate** — aim for a few seconds per article, log if slower (Q1=B)
> - Content privacy/security: **strict isolation** — only the draft brief/text goes to the cloud model; nothing else leaves the machine (Q2=A)
> - Reliability: **best-effort graceful fallback** — paper builds without AI, no retry storm (Q3=A)
> - Observability: **light** — concise per-run log, no metrics/tracing (Q4=A)
> - Tech stack: **plain Python + uv**, no heavy deps (Q5=A)
>
> Human auto-approved this summary (answers 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,42 @@
# NFR Requirements — Security (unit: ai-draft)
> Security/posture requirements for the `ai-draft` library unit. Per confirmed
> answers: strict isolation (Q2=A) and best-effort graceful failure (Q3=A).
## NFR9.1 — Strict isolation of the cloud draft call (MUST)
- **Target** — The granted cloud model (`deepseek-v4-flash:cloud`) is the ONE
sanctioned off-machine call. Only the draft brief/text (couple names, occasion,
date, key themes) is transmitted. No other content, secrets, credentials, or
wedding material leaves the machine.
- **Rationale** — Confirmed Q2=A (strict isolation). Everything else in the
pipeline is local (NFR3/NFR4); the model call is the single permitted external
boundary.
- **Verification** — Code-generation must route the draft request only to the
granted model endpoint, with a minimal prompt that contains no secrets.
## NFR9.2 — No secrets/credentials embedded (MUST)
- **Target** — No API keys, tokens, or private data are hard-coded or logged.
- **Rationale** — Security baseline; the tool is local but must not leak secrets.
## NFR9.3 — Local-only elsewhere (MUST)
- **Target** — All non-draft processing stays on-machine; the emitted page and
any DraftBundle never trigger network calls (NFR4).
- **Rationale** — Confirmed posture (Q2=A); the ONLY sanctioned external call is
the draft.
## Threat considerations (advisory)
- The cloud draft is the single data-exposure surface. Keeping the brief minimal
and on-theme (Q4=A from functional design) bounds what leaves the machine.
- A compromised model endpoint could influence draft text, but it cannot read
local files or secrets (the API boundary is draft-in, text-out only).
## Compliance note
No regulated compliance regime applies (local keepsake tool); security here is
data-protection posture (the couple's wedding content stays private) per Q2=A.
<!-- nfr ai-draft after-confirm -->
@@ -0,0 +1,19 @@
# NFR Requirements — Tech Stack Decisions (unit: ai-draft)
> Technology selection for the `ai-draft` library unit. Per confirmed answers:
> plain Python + `uv`, no heavy deps beyond what calls the model (Q5=A).
## Decisions
| Choice | Decision | Rationale |
|---|---|---|
| Language | **Python** | The generator is a Python library-backed local tool running under the working `uv` runtime (3.14). |
| Runtime | **uv** (system) | Already the affirmed primary runtime (`uv` works), matches the whole project. |
| Model client | **Thin HTTP client** | Call the granted cloud model (`deepseek-v4-flash:cloud`) over a minimal HTTP client (stdlib `urllib` or a single lightweight requests-style call). No heavy SDK. |
| Dependencies | **Minimal** | No extra heavy packages; add only what is needed to request a draft and parse the response (Q5=A). |
| Prompting/Draft handling | Plain string assembly of the brief | DraftBrief → model prompt → DraftArticle parse, per functional design. |
## Non-decisions (per produces_kinds)
- No DB (no persistence — the DraftBundle is a file handoff per Contract 5).
- No framework (this is a library unit, not a service).
@@ -0,0 +1,16 @@
{
"stage": "nfr-requirements",
"unit": "ai-draft",
"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 drafting library" },
{ "id": "NFR2", "status": "N/A", "target": "layout integrity is the generator layout concern, not the drafting library" },
{ "id": "NFR3", "status": "OK", "target": "NFR9.3" },
{ "id": "NFR4", "status": "N/A", "target": "zero-network emitted page is the generator's job; ai-draft's single sanctioned cloud call is the exception (NFR9.1)" },
{ "id": "NFR5", "status": "N/A", "target": "content-read policy applies to the generator/review-page file-picker, not the drafting library" },
{ "id": "NFR6", "status": "N/A", "target": "pure dependency-free RENDER is a generator/emitted-page property; the drafting library uses a thin client (Q5=A)" },
{ "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" }
]
}