This commit is contained in:
+132
@@ -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).
|
||||
+78
@@ -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
|
||||
+89
@@ -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 -->
|
||||
+16
@@ -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
|
||||
+94
@@ -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 |
|
||||
+19
@@ -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" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user