Developer Handoff Notes
SECTION 32 — DEVELOPER HANDOFF NOTES & DESIGN ASSETS
32.1 Design Assets
Rule: Where Figma is provided — implement pixel-perfect. Where it is not — ShadCN + minimalist principles + WCAG 2.1 AA. No other UI frameworks.
32.2 Mandatory Component Choices
32.3 Key Implementation Dependencies (Developer Must Research)
Before implementation, the developer should read the documentation for the tech-stack.md32.4 JSON5 Comment Handling
All document JSON schemas in this PRD use JSON5-compatible/* */ comments for annotation. Strip these before runtime JSON parsing using:
- NPM package:
strip-json-comments - Or a JSON5 parser library
- These are documentation annotations — never feed unstripped JSON5 to
JSON.parse()
32.6 Build Order — Layered Roadmap (v5)
This roadmap is layered by dependency, not by feature wishlist. Each phase produces a working, demoable system; later phases compose on earlier substrate without rework. Skipping a phase or reordering across phase boundaries will force rebuilds. Within a phase, ordering of sub-tasks can be parallelised.Reading guide: every phase declares (a) what to build, (b) why it must come now (its dependency on prior phases or its role as a substrate for later ones), (c) the PRD sections it implements, and (d) the tests written before any implementation in that phase (TDD). The build is structured so the Newsletter atomic document path reaches production usability by end of Phase 5 — that is the smallest end-to-end vertical slice the product can ship and bill against. Everything after Phase 5 is incremental extension on a working core.
Testing Methodology — Test-Driven Development (TDD)
Every phase below follows a strict TDD loop. The agent or developer writes tests first, watches them fail, then writes the minimum implementation to turn them green. No production code is written without a failing test that justifies it. The TDD loop applied uniformly across phases:- Write tests first based on expected input/output pairs derived from the PRD section being implemented. Do not create mock implementations of functionality that does not yet exist.
- Run the tests and confirm they fail. Do not write implementation code at this stage.
- Commit the tests once you are satisfied with them — they are the executable contract.
- Write implementation that passes the tests without modifying them. Iterate until green.
- Commit the implementation as a separate commit from the tests.
CI gates (active from Phase 0):
- Unit + integration + contract + snapshot + a11y suites run on every PR. Red = block merge.
- e2e suite runs on every PR against an ephemeral staging stack.
- Coverage threshold per package: 80% line, 70% branch (raised over time, never lowered).
- Mutation testing (Stryker for TS, mutmut for Python) runs nightly on critical packages — billing, SLA state machine, atomic UID, HMAC signing.
- Writing tests after implementation (“test-last”) — defeats the purpose. The test must fail before any production code exists.
- Mocking the unit under test (“mocking yourself”) — only mock at external system boundaries.
- Writing implementation that only satisfies the test fixture and not the PRD invariant — use property tests to catch this.
- Deleting or weakening a test to make CI green — flag the conflict, do not silence the alarm.
PHASE 0 — Project Foundations
Why now: nothing can be built before the repo, deploy target, and secrets boundary exist. Establishing these once prevents per-feature improvisation.- Monorepo scaffold (Next.js App Router + FastAPI service + shared types). Use the existing starter as base.
- Environment + secrets management (Google Secret Manager). Define
.env.examplecovering every secret the PRD references (OpenRouter, Razorpay, Better-Auth, Drive, Cloudinary, Translate, archive HMAC seed). - CI pipeline (lint, typecheck, unit tests, build). Block merge on red.
- Containerisation for Cloud Run (frontend and backend images).
- Staging and production project separation in GCP from day one.
- Cross-cutting policy install: codify §6 UI Design Principles as ESLint/Stylelint rules where possible (no inline styles, ShadCN-only component import paths, tailwind utility lint), and §31.3 a11y as a Playwright + axe-core baseline that every later phase inherits.
- CI smoke test that fails if any required
.envkey is missing on container boot. - Build pipeline test:
lint && typecheck && test && buildexits 0 on a hello-world commit. - Container health endpoint contract test:
/healthzreturns 200 with\{ status: "ok", version: <git sha> \}. - Secret access test: a deliberate attempt to read a Secret Manager key from a service account without binding must be denied (negative test).
- Staging↔production isolation test: a write to staging DB must not appear in production DB (executed as part of CI on every infra-touching PR).
PHASE 1 — Data Layer & Identity Foundations
Why now: schema and identity are the substrate every later phase reads from. Building features before the schema settles forces destructive migrations.- Full Prisma schema implementing every table in §29, including all v5 additions (
qa_items,revision_screenshots,openrouter_config,openrouter_call_log,ay_archives) and all v5 column additions toorganisations,images,sub_members,documents. - Migration framework + seed scripts. Seed the Super Admin record + a default
openrouter_configrow. - Tenant isolation pattern: every repository query must take
org_idas the primary scoping key. Codify this as a Prisma middleware or repo-base-class so it cannot be forgotten. - Atomic UID service (§11A): backend generator, collision check against
atomic_uid_log, immutability enforcement. Build this now — every document, image, and Q&A item created later depends on it. - Better-Auth integration: Google OAuth, Email + Password, Magic Link. Provision flow for the three login role types (Super Admin / Editor / Teacher / Sub-member).
- RBAC middleware — server-side enforcement of the 4 roles + publication scoping. Client-side gating is decorative; the server is the gatekeeper.
- Atomic UID format regex test per entity type (Cat / News / thumb / ref) — covers every example in §11A.2 plus property-based fuzz on
headline[:10]sanitisation. - Atomic UID collision test: forced collision against
atomic_uid_logmust trigger regeneration, never return a duplicate. - Atomic UID immutability test: any attempt to update an existing UID row must be rejected at the repo layer.
- Tenant isolation test: every repository method call without an
org_idargument must throwMissingTenantContext(negative test asserts this for every public repo method). - RBAC matrix tests: a tabular test (driven by the §4.9 permission matrix) hits every endpoint with every role and asserts allow/deny matches the matrix exactly.
- Schema migration round-trip test: apply all migrations to an empty Neon branch, dump schema, diff against checked-in
schema.prisma— zero drift. - Better-Auth callback contract tests (Google OAuth flow, magic-link flow) using stubbed identity provider.
PHASE 2 — Tenant Lifecycle
Why now: every document, billing event, and notification is scoped to an authorised tenant. Without the lifecycle in place, there is nowhere for documents to live.- Super Admin dashboard skeleton: Editor management panel, Organisation approval queue, system-wide audit log table, platform metrics card shells.
- Editor onboarding (§5.4).
- Teacher self-registration → Organisation creation → Super Admin approval → Editor mapping → Editor authorisation gate (§5.5).
- Sub-member invite + permission configuration (§21), including the new
permissions.qaboolean. - Publication streams (§9A): default
CURRENT-AFFAIRSauto-created on Org registration, custom publication CRUD with the Teacher↔Super-Admin (and Editor-requested) approval chain. - Duplicate document prevention constraint (§9A.7) on the
documentstable. - Super Admin dashboard (§8.0): Editor management panel, Organisation management panel, system-wide activity & audit log, platform metrics. (Billing dashboard panels wait until Phase 13.)
- State-machine test for Org approval:
pending_approval → activerequires both Super Admin approval AND Editor authorisation; any other path rejected. - State-machine test for Teacher status:
pending → activerequires Editor authorisation after Org approval; orphan transitions blocked. - Sub-member permission JSON shape test: defaults to
\{ create: false, edit: false, submit: false, export: false, qa: false \}; only Teacher can mutate. - Publication uniqueness test: insert two
doc_type_namerows with same(org_id, doc_type_name)across different publications → constraint violation. - Publication approval-chain test: Editor-requested publication requires Teacher approval THEN Super Admin approval; either skip rejected.
- Duplicate-document e2e: attempt to create two Newsletters for same
(org_id, publication_id, doc_type_name, document_date)→ second create blocked at API boundary AND DB constraint (defence in depth). - Sub-member
permissions.qatest: withqa = false, Q&A wizard endpoint returns 403 even when route is hit directly. - Playwright e2e: full Teacher signup → SA approves Org → SA maps Editor → Editor authorises → Teacher can land in dashboard.
PHASE 3 — Storage Substrate
Why now: the editor in Phase 4 binds directly to Yjs and Cloudinary. Building these first means the editor work has nothing to mock.- Cloudinary integration: image upload pipeline, WebP at upload (§26.6),
atomic_uidinjection into Cloudinary MIME metadata (§11A.5),imagestable persistence withcrop,origin,storage_format. - Yjs server (
y-websocketsidecar, Node, on Cloud Run): authentication, document-room provisioning perdocuments.id, write-permission check that respectsis-lockedand reviewer overrides. - Lexical ↔ Yjs binding library on the client.
y-indexeddbprovider on the client (replaces v4 keystroke browser cache; doubles as offline recovery — §24.2).- 240s Neon snapshot worker (§24.3): periodically reads canonical Yjs doc state and writes to
documents.content_json. Also flushes immediately on lock / named-version / close events. - Named version system (§24.4).
- Google Drive OAuth (Editor and Teacher accounts) + folder bootstrap on Org approval (§25.2).
- Cloudinary upload contract test: uploaded asset URL contains
/f_webp/; response includespublic_id; MIME metadata read-back contains the suppliedatomic_uid. - Image record persistence test: every Cloudinary upload writes an
imagesrow withstorage_format = "webp",origin, andcrop = nullby default. - Yjs server authorisation test: write op from a Teacher when
is-locked = trueis rejected; Editor write duringreviewis accepted. - Yjs op apply → snapshot test: deterministic doc state after N ops is byte-equal across two replicas.
- 240s snapshot worker test: with fake timers, advance 239s → no Neon write; advance to 240s → exactly one write occurs with the canonical Yjs state.
- IndexedDB offline test (Playwright with
context.setOffline(true)): edits made offline survive page reload and merge correctly onsetOffline(false). - Named version test: finalising bumps
1.0.0 → 1.1.0(or1.x.0 → 1.(x+1).0); the prior version’scontent_jsonis immutable (UPDATE blocked). - Drive folder bootstrap test: after Org approval, calling the Drive client mock returns the expected folder tree per §25.2.
PHASE 4 — Newsletter Atomic Document Editor
Why now: Newsletter is the fundamental atomic unit that every aggregation in Phase 9 derives from. Build it first; build it complete.- Lexical canvas (§10.3): category and news-item blocks, plus-button additions, clear-field + delete dialogs with “don’t ask again”, drag-and-drop reordering, sidebar minimap.
- Thumbnail upload slot with non-destructive crop UI (§10.8): crop dialog, reset icon, frozen-on-lock behaviour, persistence of the
croptransform. - Brief = Lexical JSON tree (§10.9): inline image paste flow, async background Cloudinary upload, pending/complete/failed
upload_staterendering, broken-placeholder block onSend to Editorif any image is failed. - Derived
reference-imagesarray rebuilt server-side on every save by walking each Brief tree. - Newsletter document JSON v5 persistence end-to-end (matches §11).
- Metadata sheet (§10.2), YouTube link attachment via custom URL parse (§27).
- Teacher dashboard (§8.1): entry-point cards (grouped by publication when multiple exist), Kanban board, logs table.
- Property test on Brief Lexical tree: any sequence of paste / delete / reorder operations yields a tree the derived-
reference-imagesextractor can walk without error. - Derived
reference-imagesextractor: given a fixture Brief tree with 3 inline image nodes in known positions, returns exactly 3 entries in the correct order with matchingatomic_uidvalues. - Crop transform validator:
\{x, y, w, h\}bounds —x + w <= original_width,y + h <= original_height; invalid crops rejected. - Crop frozen-on-lock invariant: after
is-locked = true, any attempt to write a newcropvalue on the same image row is rejected. - Snapshot test: §11 JSON produced from a canonical fixture (3 categories, 5 news items, 2 inline images, 1 crop) matches the committed snapshot byte-for-byte (modulo timestamps).
- Inline paste broken-placeholder test: with Cloudinary upload mocked to fail, the “Send to Editor” action is blocked; error surfaced is the §10.9 wording.
- YouTube URL parse contract test: known URL shapes (watch, share, shortlink) all extract the correct channel/video ID without invoking YouTube API.
- Duplicate-prevention real-time UI test: typing a date in the date picker that has an existing doc surfaces the red indicator within 200ms.
- a11y test on the canvas: axe-core passes on the Lexical canvas, the metadata sheet, the crop dialog, and the duplicate-prevention warning.
PHASE 5 — Editor Pipeline & First Billable Output
Why now: this phase closes the smallest end-to-end loop — Teacher creates a Newsletter, Editor approves, PDF is delivered and billed. After this phase, the product is demoable, sellable, and revenue-capable for the defaultCURRENT-AFFAIRS Newsletter.
- Editor review queue + Kanban (queued / generated / delivered / flagged) (§8.2).
- SLA state machine for Stages 0–3 (§13.3): timers, auto-continuation,
sla-paused, pipeline event log. - WeasyPrint PDF generation pipeline (§17.6): branded templates, header/footer injection, page count recording, billable-page-count.
- Stage 1 live co-visibility for the Teacher via Yjs read-only (§13.4).
- Stage 3 delivery: in-app + email notification (always); billing meter increment (always); Reveal.js auto-persistence of slide-deck PDF to backend (always, server-side headless print, compressed per §26.5); Drive sync to
YYYY/MMMYY/DAILY/FINAL/only iforganisations.drive_sync_enabled = true(§25.0). - Non-payment pipeline block scaffolding (§13.5) — full Razorpay integration arrives in Phase 13; here only the block-and-tooltip logic plus a manual
billing_statusflag. - Editor dashboard (§8.2): entry-point cards (aggregate across publications and assigned Orgs), Editor Kanban board, queue management, priority notification panel, document metrics dashboard.
- SLA timer arithmetic test:
stage1-sla-deadline = submitted-to-editor-at + 2h; withsla-paused = true, the deadline does not advance regardless of wall time. - Full state-machine transition table test: every transition in §13.3 Stages 0–3 exercised with valid and invalid preconditions; invalid transitions rejected with structured errors.
- Auto-continuation test (fake timers): no Editor action for 2h on Stage 1 → system auto-triggers PDF generation; pipeline_event row written with
triggered_by = "system". - WeasyPrint contract test: given a fixture HTML for a known Newsletter JSON, output is a valid PDF whose page count matches the snapshot. Compare rendered PDF text content via
pdftotextagainst the snapshot text. - Billing meter test: PDF delivery increments
billing_cycles.total_pagesby exactlypdf_outputs.page_count. - Drive sync toggle test (two states): with
organisations.drive_sync_enabled = trueand on Stage 3, FINAL PDF appears at the expectedYYYY/MMMYY/DAILY/FINAL/path in both Teacher and (mapped) Editor Drives. Withdrive_sync_enabled = false, zero Drive SDK calls are made and the document remains fully retrievable via the in-app dashboard and signed backend URL. - Reveal.js auto-persistence test: at
event_type = "delivered", the backend produces a slide-deck PDF in the backend store; size after §26.5 compression is smaller than the raw print output. Teacher’s local print-to-PDF action (if invoked) does NOT overwrite the backend artefact and is NOT compressed. - Yjs co-visibility test (Playwright, two browser contexts): Editor types in Stage 1; Teacher’s read-only canvas reflects the change within 250ms; Teacher write attempt is rejected with the
is-locked = trueerror. - Non-payment block test: with
billing_status = "overdue"past grace,POST /documents/:id/submitreturns 402 with the §13.5 tooltip copy; document creation and PPTX export endpoints still return 200. - e2e: Teacher submits → Editor approves → PDF delivered → Teacher sees Delivered card → billing cycle
total_pagesincrement matches the WeasyPrint page count.
PHASE 6 — Revision Flow & Screenshot Wizard
Why now: revision is the natural follow-up to delivery in the SLA pipeline. Building it directly after Phase 5 keeps state-machine logic contiguous.- Stage 4 manual revision queue (§13.3 Stage 4): no SLA, manual approval at every step, distinct visual treatment.
- Mandatory PDF screenshot wizard (§13.3.4A): PDF.js iframe viewer, drag-quadrilateral region tool, page-range whole-page capture, “Add more” loop, mandatory ≥1 attachment with reason note.
revision_screenshotstable with the 5-per-flag / 1MB-per-PNG hard caps; insert trigger to enforce.- 30-day purge cron after
revision-history[-1].resolved-at. - Editor-side rendering: thumbnail strip, expand-on-click with quadrilateral overlay preserved.
- Screenshot size cap test: a 1.01 MB PNG is rejected client-side (returned error string from the wizard validator) AND a forged direct API call with a 1.01 MB blob is rejected server-side (defence in depth).
- Max-5 enforcement test: 6th insert into
revision_screenshotsfor the same(document_id, revision_id)violates the insert trigger. - Min-1 enforcement test: revision submission with zero screenshots returns 422; wizard “Submit” button remains disabled in UI.
- Quadrilateral point validator: arrays with !=4 points, or any point outside the PDF page bounds, are rejected.
- Page-range parser: known good inputs (
"3","5-7","3,region") parsed correctly; malformed inputs rejected. - Purge cron test (fake timers): with
purge_afterset to T-1d, the cron deletes the row; withpurge_afterset to T+1d, the row survives. - Playwright e2e: Teacher opens delivered PDF, drags a quadrilateral on page 3, captures, adds a second whole-page capture for page 5, types reason, submits → revision card with both thumbnails visible in Editor’s queue.
revision_screenshots).
PHASE 7 — Client-Side Exports
Why now: PptxGenJS, Reveal.js, TXT, and ZIP all consume the Brief Lexical tree finalised in Phase 4. They are zero-dependency on Phase 5/6 backend work and can ship as soon as the editor stabilises.- PptxGenJS client-side PPTX generation (§17.1), including the shared text-height-estimator utility (build once, reuse for all text blocks).
- Reveal.js presentation mode (live iframe + new tab + fullscreen) and the print-to-PDF path (§17.2, §17.5).
- TXT export (§17.3).
- ZIP export (images + TXT) (§17.4).
- All exporters walk the Brief Lexical tree and resolve image nodes via the
atomic_uid → cloudinary URL + crop transformmap.
- PptxGenJS snapshot test: a fixture Newsletter JSON produces a
.pptxwhose OOXML structure (slide count, image refs, table dimensions) matches the committed snapshot. - Text height estimator unit test: given fixture text strings at the configured slide width + font, the estimator returns heights within ±5% of a measured ground-truth set.
- Auto-paginate-table contract test: a table with rows that overflow one slide produces N+1 slides; row count is preserved end-to-end.
- Image resolution test: an inline image node with
atomic_uidand a non-nullcropproduces a Cloudinary URL containingc_crop,x_,y_,w_,h_params. - Reveal.js HTML snapshot: a fixture JSON produces the expected slide-deck HTML structure.
- TXT export round-trip: text content from a fixture matches
pdftotext-style flattening of the same JSON. - ZIP export structure test: archive contains exactly
/images/*(atomic-UID-named) + a single.txtat root. - Visual regression on Reveal.js HTML render (Playwright + pixelmatch).
- a11y: axe-core sweep on the Reveal.js presentation iframe + the export menu.
PHASE 8 — AI Q&A Service
Why now: Q&A depends on the atomic_uid system (Phase 1), the canvas + Brief tree (Phase 4), and the post-delivery regen mechanic (Phase 5). All prerequisites are now in place.- OpenRouter service backend (§22B): platform API key, model resolution chain (per-Org override → default → 4 fallbacks), 15-attempt retry envelope, Structured Outputs schemas as Pydantic models, validation chain.
- Per-Org monthly token cap + per-request cap enforcement;
openrouter_call_logtelemetry; Super Admin telemetry surface. - Pre-submission Q&A wizard UI (§10.10): count input, news-item selection grid, per-question type customisation, edit-and-accept workflow.
qanode persistence incontent_json;qa_itemstable writes;qa-late-addedandhas-qametadata flags.- PDF and PPTX rendering of Q&A items (tail-section of slides for PPTX; templated section in WeasyPrint).
- Q&A late-add regen (§13.6): post-Phase-5 delivered docs accept Q&A, regen PDF as new minor version, replace Drive FINAL, archive old version, deliver to Teacher.
- Sub-member
permissions.qagating in §21 and §22B.5.
- JSON Schema validator test per Q&A type (shapes per §11.1
qa.items[]):subjective.straightforward\{statement, answer\};objective.direct\{statement, options:\{A,B,C,D\}, correct_option∈A|B|C|D\};objective.statement_analysis\{topic, statements:[s1,s2,s3], options:\{A,B,C,D\}, correct_option\}. Valid payloads pass; invalid (missing option key,correct_optionnot present inoptions,statements.length != 3, statement_analysis option value outside the allowed set\{ "1","2","3","1+2","2+3","1+3","All of the above","None" \}, duplicate option values within an item) fail. - 15-attempt retry chain test: mock OpenRouter returns malformed JSON 3 times → same model retried 3x → first fallback model used → continues through chain. With all 5 models failing 3x, total attempts = 15 and final response is the error envelope.
- Model resolution test: per-Org override
qa_model_overridewins over systemdefault_model; null override falls back; per-Org fallback override replaces system list. - Token cap test: a request whose estimated tokens push
qa_tokens_used_current_cycleoverqa_monthly_token_capis rejected up front with the §22B.4 tooltip copy. Within-cap requests succeed. - Cap reset test: at billing-cycle start,
qa_tokens_used_current_cycleresets to 0. - Statement-analysis business rule: response with !=1 correct option is rejected by validator (retried, not accepted).
source_atomic_uidslinkage test: every generatedqa_itemsrow stores the input news items’ UIDs; aggregation join in Phase 10 will use this.- Provenance split tests (§22B.3.E step 7):
content_json.qa.items[]lean shape: per item, the persisted JSON contains exactly\{qa_id, source_atomic_uids, type, …type-specific render fields…\}and nothing else. A fixture assertion fails ifmodel_used,generated_by_role,accepted_at,slot_index, or any other provenance field leaks into items[].qa_itemsrow completeness: for each accepted item, the row carriesqa_id,qa_id_seq,slot_index,source_atomic_uids,type,payload,generated_by_role,generated_by_id,model_used,is_late_added,accepted_at.- §11.1 byte-equality: the assembled
content_json.qafor a canonical 5-slot fixture is byte-equal (modulo timestamps) to the §11.1 reference snapshot. - First-acceptance metadata stability: top-level
generated_by_role,generated_at,model_usedare written on first acceptance and NOT overwritten by a subsequent late-add wizard run by a different actor on a different model. Verified by inspectingcontent_json.qabefore and after the late-add. - Late-add items[] replacement: a late-add run by a sub-member appends new items[] entries (qa_id seq continues from existing max), top-level fields unchanged.
qa_itemsrows for the late-added items carryis_late_added = trueand the sub-member’sgenerated_by_id.
- Multi-call assembly tests (§22B.3.E):
- Slot-order preservation: a 5-slot wizard run with types
[T1, T2, T1, T3, T2]dispatches 3 OpenRouter calls (one per type group), and the assembledqa.items[]array reflects wizardslot_indexorder (1→5), NOT dispatch order or per-group payload order. qa_idsequencing across types: the same 5-slot run producesqa_idending in001..005in slot order, regardless of which group’s call returned first.- Late-add seq continuation: a second wizard run on a document with existing
qa_id_seq = 5produces new items starting at006; no reuse, no gaps. - Cross-slot ref-fabrication rejection: a mocked T2 response where one item’s
source_refsmixes refs from two different slots (e.g.["n1", "n3"]when slots 2 and 5 supplied disjoint refs) is rejected per §22B.3.C step 5. - All-or-nothing across groups: if the T3 call exhausts its 15-attempt retry chain, the entire wizard run aborts — zero rows in
qa_items, noqanode mutation, even though T1 and T2 calls succeeded. - Concurrent-acceptance race: two parallel “Accept Q&A” requests on the same document (Teacher + sub-member) both succeed; the
SELECT FOR UPDATEonqa_id_seqserialises them; second-committer’s items append after first-committer’s max; no qa_id collisions; totalqa_item_countequals the sum of both wizard runs.
- Slot-order preservation: a 5-slot wizard run with types
- Identity boundary tests (§22B.3.A / §22B.3.D) — non-negotiable:
- Outbound payload assertion: the JSON sent to OpenRouter contains zero occurrences of any
atomic_uidstring. Onlyrefhandles (^n\d+$) appear. Verified by intercepting the HTTP request body and grep-asserting. - Inbound schema strictness: a mocked model response containing an
atomic_uidorsource_atomic_uidsfield is rejected by Pydanticextra="forbid". - Ref fabrication rejection: a mocked response with
source_refs: ["n99"]when the request only sentn1, n2, n3is rejected by §22B.3.C step 5 and counts as a validation failure (retry triggered). qa_idmodel-fabrication rejection: a mocked response containingqa_idis rejected by schema (extra="forbid"); even if it slipped through, the post-processing step would overwrite with the backend-assigned value.qa_idsequencing: two acceptances on the same document produceQA-\{doc-id\}-001andQA-\{doc-id\}-002deterministically; unique constraint(document_id, qa_id)enforced at the DB layer.- Ref→UID swap correctness: post-processing transforms
source_refs: ["n2"]tosource_atomic_uids: [<uid of news item 2>]per the request-scoped map. - Belt-and-braces check: backend re-validates each resolved
source_atomic_uidsentry against the document’s currentNews-Items[*].atomic_uidset before INSERT.
- Outbound payload assertion: the JSON sent to OpenRouter contains zero occurrences of any
- Late-add regen test (Phase-5 doc fixture): adding Q&A on a
publisheddoc bumpsdoc_versionminor; a newpdf_outputsrow appears; Teacher receives notification; Drive FINAL is replaced and prior PDF moves to_archive/. - Late-add billing test: if regen PDF page count is greater than prior
billable-page-count, the delta is added to current cycle; if equal, no billing change. - Late-add blocked-after-invoice test: when
documents.is_billed = true, the “Add Q&A” route returns 409 with the §13.6 tooltip copy. - Sub-member without
permissions.qa = truecalling the Q&A wizard endpoint receives 403. - PDF + PPTX + HTML render snapshot tests: a fixture doc with a
qanode produces outputs with a Q&A tail-section in each format. - Playwright e2e: Teacher runs wizard pre-submit → selects 3 news items → generates 3 Q&A → edits one → accepts → submits document. End-to-end Q&A appears in delivered PDF.
qa node), §13.6, §22B.
PHASE 9 — Templates, Ads, HTML Export
Why now: templates and ad banners are injected by the WeasyPrint pipeline (Phase 5). Until Phase 5 ships, templates have nothing to inject into. HTML export needs the Lexical tree (Phase 4) and the CDN-URL-to-local-path rewriter (small extension on Phase 3 Cloudinary code).- Template system (§19): drag-and-drop micro app, scope controls, multiple-templates-and-assignment, header/footer/full-page types, dynamic field placeholders, injection during WeasyPrint generation.
- Ad banner management (§20): library, scheduling, per-doc-type assignment, injection logic.
- HTML export — Editor-only (§17.7): CDN URL replacement with local relative paths, image bundling.
- Template scope resolver test: given a doc with N pages and a template configured
scope_page_application = "range"withstart = 2, end = 4, the template is injected on pages 2–4 only. - Multiple-template precedence test: when two templates target overlapping scopes, ordering rules from §19.5 are applied deterministically.
- Dynamic field substitution test:
\{\{date\}\},\{\{yt_url\}\},\{\{page_number\}\},\{\{ad_banner\}\},\{\{title\}\},\{\{subtitle\}\}all replaced from the document’s known values. - Ad banner schedule resolver test: with a banner scheduled for “weekdays 09:00–18:00 IST”, the resolver returns it for a Tuesday 14:00 IST request and skips it for Sunday.
- Ad banner doc-type targeting test: a banner with
document_types = ["Newsletter"]is injected into a Newsletter PDF but not a Magazine PDF. - HTML export contract test: every
https://res.cloudinary.com/...in the source HTML is replaced with a relative./images/<filename>path in the export; the corresponding image file exists in the bundle. - HTML export atomic UID preservation test: every entity in the source has a
data-uidattribute in the exported HTML (§11A.6). - a11y: axe-core sweep on the template builder micro-app, the ad banner manager, and the HTML export viewer.
PHASE 10 — Aggregations & Category Extraction
Why now: every aggregation in §14 / §15 / §15A / §15B / §15C reads from atomic Newsletters created via Phase 4. With the atomic source stable and Q&A persistent (Phase 8), aggregations can correctly merge content + Q&A.- Cron triggers via Google Cloud Scheduler for: Compilation, Magazine, Quarterly Collection, Bi-Annual Compendium, Annual Yearbook (§9.2, §9.3, §9.3A–C).
- Manual Editor on-demand override for each.
- Aggregation pipelines (§14, §15, §15A, §15B, §15C): pull from atomic docs within the same publication, never cross-publication.
- Category Extraction (§15D) Editor-only flow.
- Q&A merging across aggregations using
qa_items.source_atomic_uidsas the join key. - Eligibility rules (§14.3) and publication-scoped naming (§9A.4).
- Aggregation eligibility test (§14.3): only
publishedNewsletters within the configured window are included;draft/review/archivedare excluded. - Cross-publication isolation test: a Compilation in publication A must never include atomic docs from publication B (fuzz with multi-publication fixtures).
- Cron schedule test (fake timers): with the trigger configured for “every Friday 18:00 IST”, advancing the fake clock past the boundary fires the job exactly once per period.
- Manual override test: Editor-triggered aggregation succeeds outside the cron window; cron is suppressed for the same window after a manual run (no double-aggregation).
- Q&A merge test: a Compilation that pulls 3 Newsletters each with 2 Q&A items produces a merged Q&A section with 6 items in source order; each item still carries its original
source_atomic_uids. - Category Extraction test (§15D): selected categories from an aggregation produce an Extraction document whose
extraction_categoriesmatches the selection and whose content includes only those categories. - Naming-convention test: aggregation output file names match §14.5, §15.4, §15A.4, §15B.4, §15C.4, §15D.3 exactly.
- Aggregation billing test:
count_in_billing = falseExtractions are excluded from billing cycle totals. - Playwright e2e: Editor manually triggers a Compilation; resulting PDF appears in Drive at the right path and in Teacher’s Kanban.
PHASE 11 — Translation & Mindmap
Why now: translation depends on a stable content schema (Phase 4) + Q&A node (Phase 8) + aggregated docs (Phase 10) to be useful at every output level. Mindmaps need both the Newsletter source (Phase 4) and the SVG → PDF pipeline (extension of Phase 5).- Google Translate API integration (§18): parallel document creation, language switcher, per-type translation toggle.
- Translation coverage: Brief Lexical tree (walking text nodes),
qanode, metadatalanguage-code, multilingual PPTX, multilingual PDF. - Mind Elixir canvas integration (§16): standalone flow + post-Newsletter flow.
- Mindmap SVG → WeasyPrint PDF.
- Mindmap restrictions per publication (§9A.5): atomic-only source.
- Mindmap translation behaviour (§16.5 / §18.7).
- Brief tree translation test: a fixture Brief Lexical tree is translated; text nodes carry translated strings; image nodes pass through unchanged (same
src, sameatomic_uid). - Q&A node translation test: each
qa.items[].statement(andanswer/options/statementsper type) is translated;qa_idis preserved (so aggregation back-mapping still works). - Parallel-doc invariant: translated docs share the source
doc-idlinkage, are markedis-translated = trueon source, and the source content is never mutated. - Per-type toggle test: with
translation-disabled-for = ["mindmap"], the translation pipeline skips that doc type entirely. - Multilingual PPTX test: translated source produces a unified
.pptxwith alternating source/translated slide pairs. - Multilingual PDF test: translated source produces a WeasyPrint PDF that embeds both languages per §18.6.
- Mindmap atomic-only constraint test: attempting to create a Mindmap from a Compilation / Magazine / Yearbook returns 422 with the §9A.5 error.
- Mind Elixir SVG → PDF snapshot test: a fixture mind map renders to a deterministic SVG, which renders to a deterministic PDF page.
- Playwright e2e: Teacher enables translation → switches language → translated content appears in canvas, in delivered PDF, and in PPTX export.
PHASE 12 — Notifications, Comments, Support
Why now: every prior phase has produced events worth notifying about (document delivered, revision flagged, Q&A late-added, publication request, ticket raised). Building notifications after the event-producing surfaces are stable avoids stub-and-rewire churn.- Two-tier notification architecture (§22): in-app + email via Resend.
- Complete event-to-notification mapping (§22.2) — instrument every event from Phases 5–11.
- User notification controls (§22.3).
- Document comments thread (§4.11.3) — dispute-resolution channel for Teacher ↔ Editor.
- Support tickets (§22A): full lifecycle, Super Admin ticket dashboard, attachments, auto-close.
- Event-to-notification table-driven test: for every event in §22.2, assert (in-app channel fired, email channel fired or suppressed per priority rule).
- Resend contract test: rendered email subjects + bodies for each template match snapshot copy from the PRD.
- Notification preference test: with a per-event preference set to off, the corresponding event does not produce a notification.
- Ticket lifecycle state-machine test (§22A.4): valid transitions allowed, invalid blocked; auto-close cron closes tickets after the configured idle window.
- Ticket attachment cap test: per §22A constraints, exceeding the attachment count or size cap is rejected.
- Comment immutability test:
PATCH /comments/:idreturns 405; only Super Admin soft-delete is allowed. - Comment chronological ordering test:
GET /documents/:id/commentsreturns comments ordered bycreated_atASC. - Playwright e2e: Teacher raises a ticket, Super Admin replies, status transitions through
open → in-progress → resolved, auto-close fires after retention window.
PHASE 13 — Billing, Compliance, Super Admin Dashboards
Why now: billing must observe real document volume, real page counts, real Q&A late-add billing deltas, and real translation multipliers — all of which exist only after Phases 5, 8, 10, 11. Building billing before these phases means rewriting calculations.- Razorpay integration (§23.2): platform-managed master account, webhook handler, postpaid and prepaid flows.
- Postpaid billing cycle lifecycle (§23.3) + running totals + retrospective document rules (§23.3A) + billability rules (§23.3B).
- Prepaid + discount commitment (§23.1A): semi-annual, annual, auto-renewal, refund policy.
- Manual invoice generation (§23.4) + invoice archive (§23.5).
- Non-payment access policy (§23.6) — wire the actual enforcement into the placeholder built in Phase 5.
- Super Admin billing dashboard (§23.7).
- Evader detection + compliance enforcement (§23.8): thresholds, grace period for new Orgs, the 4 enforcement actions, compliance dashboard.
- Billing math test:
total_amount = total_pages × (rate_per_page - discount_per_page)for a fixture cycle; rounding rules per §23 covered. - Retrospective detection test (§23.3A): a document created with
document_dateoutside the current cycle is flaggedis_retrospective = trueand routed per the §23.3A.3 invoice layout rules. - Q&A late-add billing test (re-run from Phase 8, now under full Razorpay): page count delta is added to the current cycle, not back-applied to a closed cycle.
- Translation billing test: per-language outputs are billed per §23 rules; ensure no double-billing of the source.
- Razorpay webhook contract test (Pact): valid
payment.capturedpayload moves the invoice topaid; an unsigned or tampered payload is rejected. - Discount commitment activation test: a paid prepaid invoice for the annual plan activates
discount_per_page = 0.10for the commitment window; expiry reverts to default. - Auto-renewal test (§23.1A): with
auto_renewal = true, a tokenised charge fires on commitment-end; failure surfaces per §23 error path. - Non-payment enforcement test: with
billing_status = "overdue"past grace,POST /documents/:id/submitreturns 402 — same behaviour as Phase 5 scaffold but now driven by real Razorpay state. - Evader detection threshold test (§23.8): compliance % below threshold for N cycles triggers the configured enforcement action; grace period for new Orgs is respected.
- Enforcement action state-machine test:
allow / warning / temporarily-suspend / terminatetransitions are valid only along the §23.8 path. - Playwright e2e: Super Admin generates an invoice, Teacher pays via Razorpay test mode, invoice moves to
paid, billing block lifts.
PHASE 14 — Archival, Retention, Recovery
Why now: archival depends on every artefact type existing in the system (documents, images, PDFs, PPTX exports, Q&A, revision screenshots). Recovery depends on archival having shipped at least one cycle.- Image-only year-end archival (§26.4) for Orgs with
full_ay_archive_enabled = false. - Full-AY archival cron (§26A.2): single ZIP per AY containing JSON, versions, PDFs, PPTX (when explicitly saved to Drive), images, Q&A snapshot, revision screenshots.
- HMAC-signed
manifest.json+manifest.sigusing Org-scoped HMAC secret (§26A.3). - Recovery flow (§26A.4): structural validate, Org-match authorisation, HMAC verify, hash verify, refuse on conflict (§26A.5), image re-upload to Cloudinary preserving atomic_uid, URL rewire across all content_json + versions (§26A.6), transactional rollback on any failure.
- Soft delete + tombstone + per-user retention policy (§24.6).
- Audit log + Super Admin deletion flow with mandatory pre-archive to Drive + CSV download.
- HMAC round-trip test:
sign(manifest, secret) → verify(manifest, sig, secret) = true; flipping a single byte ofmanifest.jsonmakesverifyreturn false. - Org-mismatch rejection test: a ZIP whose
manifest.org_iddiffers from the recovering Teacher’sorg_idis rejected with the §26A.4 step-4 error. - Hash verification test: tampering with any file in the ZIP causes
manifest.files[]hash mismatch and rejects recovery; anaudit_logsrow is written with HMAC-failure event. - Conflict refusal test: with even 1 existing document for the target AY, recovery is refused with the §26A.5 message.
- Image re-upload + URL rewire test: a manifest with 100 images is recovered into a fresh Org → 100 new Cloudinary uploads happen → every
content_jsonimage URL (thumbnail + Brief tree image nodes) is rewritten to the new URL via theatomic_uidindex. - Transactional rollback test: a synthetic failure injected at step 9 leaves zero DB rows committed and the temp sandbox purged.
- Archive cron determinism test (fake timers): the cron fires exactly at end-of-grace-month for the Org’s configured AY; not before, not after.
- Image-only fallback test: with
full_ay_archive_enabled = false, the §26.4 image-only flow runs and the full-AY flow is suppressed. - Retention policy test (§24.6): minimum 2-month floor enforced server-side; deletion configs below it are rejected. Soft-deleted rows hard-delete after the 7-day tombstone.
- Audit log deletion test: the SA deletion endpoint requires the pre-archive Drive upload to succeed before any DB delete is executed.
- Playwright e2e: SA toggles full-AY archive ON → cron runs → ZIP appears in Drive → Teacher uploads it back via recovery → all documents and images return; URLs in the canvas resolve to fresh Cloudinary assets.
PHASE 15 — Cross-Cutting Hardening
Why now: hardening must observe the system at full feature-completeness. Performance tuning, error ladders, and accessibility on a half-built surface produces work that gets thrown away.- Error handling & recovery ladders (§28): image upload failure, Drive sync failure, WeasyPrint failure, storage limit, real-time connection loss, generic API errors.
- Compression pipeline end-to-end (§26.5 / §26.6 / §26.7): PDF compression via
ghostscript/pikepdf, WebP storage, JPEG transcode for render, ZIP DEFLATE level 9, gzip on transport. - Monitoring (§30, §31.5): Google Cloud Monitoring + Logging + Error Reporting + Trace. Alert policies on SLA breaches, WeasyPrint failure escalation, OpenRouter token-cap hits, Drive quota warnings.
- Performance pass (§31.1): editor frame budget, Yjs operation latency, snapshot worker throughput, WeasyPrint generation duration.
- Security pass (§31.2): RBAC penetration of every endpoint, HMAC verification on archive recovery, secret rotation policy, input validation at every external boundary.
- Accessibility pass (§31.3): WCAG 2.1 AA across all screens.
- Browser support matrix (§31.7) verified.
- Error-ladder unit test per recovery in §28: each failure mode triggers the specified ladder steps; final step (user notification / pause SLA / etc.) is observed.
- PDF compression test: a fixture PDF is compressed; output is smaller; pdftotext content matches the original byte-for-byte.
- WebP transcode-on-render test: WeasyPrint fetches the Cloudinary
f_jpgvariant for embedding; PptxGenJS fetchesf_jpg(orf_pngwhen alpha is present); HTML export fetches the same. - ZIP DEFLATE level test: ZIP exports use level-9 compression (verify with the central directory inspection).
- Load test (k6): Yjs server handles N concurrent co-edit sessions with op latency under target ms; report regressions in CI.
- Mutation test (Stryker, mutmut) on billing, SLA, atomic UID, HMAC modules: surviving mutants below threshold = pass.
- OWASP ZAP baseline scan against staging: no high-severity findings; medium findings triaged.
- axe-core full-app sweep: zero WCAG 2.1 AA violations across every screen built since Phase 0.
- Browser support matrix test (§31.7): Playwright cross-browser run on Chromium, WebKit, Firefox at the supported versions.
PHASE 16 — Deployment & Launch
- Staging deploy and full end-to-end UAT covering every flow from §10 through §26A.
- Load test the Yjs server under realistic concurrent-co-edit scenarios.
- Production cutover.
- Day-1 on-call runbook covering: WeasyPrint failures, OpenRouter outages, Razorpay webhook lag, Drive quota exhaustion, Yjs server crash recovery.
- Synthetic monitor scripts (Playwright in scheduled mode): hourly synthetic Teacher login + create + submit + delivered loop; pages on failure.
- Smoke test suite on staging: hits one endpoint per service; passes before promotion to prod.
- Disaster recovery drill: restore the Neon DB from a point-in-time backup into staging and verify the recovery time matches the RTO target.
- Rollback test: deploy v-current → deploy v-next → roll back → app and DB are functional at v-current.
- Runbook validation test: each on-call runbook step is exercised in a chaos-engineering drill (kill the Yjs pod, kill the Razorpay webhook listener, etc.) and the runbook resolves the incident.
PRD Coverage Audit (every section mapped to a phase)
This audit confirms the build order touches every section of the v5 PRD. If a future PRD edit adds a section, append it here.Dependency Quick-Reference
Non-Negotiable Sequencing Rules
- Schema before features. Every v5 column and table from §29.2 / §29.3 must exist before any feature that writes to it.
- RBAC server-side, day one. Never ship a UI feature whose access control lives only in the client.
- Atomic UID before any document write. Documents created without a UID generator in place will need backfill.
- Yjs + IndexedDB before the editor goes live. Building the editor on a temporary autosave shim and swapping in Yjs later is a guaranteed rewrite.
- PptxGenJS before any other PPTX work. No
python-pptxregression is acceptable in v5. - Q&A node round-trips through PDF, PPTX, HTML, translation before the wizard ships to production. A skipped renderer means broken docs.
- Archive cron must produce one full cycle in staging before recovery is enabled in production. Recovery without a verified archive on hand is operationally dangerous.
- Tests red before implementation, always. No production code without a failing test that justifies it. The “Tests written first” list in each phase above is the entry condition for that phase — those tests are committed (red) and CI verifies they fail; only then does implementation begin. PR diffs that introduce production code without a matching test commit are blocked at review.
- PRD section coverage is enforced. Every PRD section listed in a phase’s
PRD:footer is mapped to at least one test in the phase’s “Tests written first” list. The mapping is checked by apre-merge-coveragescript that reads the PRD section headers and the test filedescribe()blocks; an uncovered section blocks merge.
32.7 Definition of Done (Per Feature)
A feature is considered “done” only when:- Tests were written first, committed red, then turned green — the commit history shows the test commit landing before the implementation commit (enforced by review).
- Implementation matches this PRD exactly (or any approved deviation is documented).
- All edge cases specified in this PRD are handled.
- Error states are implemented (not just happy path).
- RBAC enforcement is active server-side (not just client-side hiding).
- Notifications fire correctly for the relevant events.
- Data is correctly persisted to the database.
- Google Drive sync triggers correctly where applicable.
- WCAG 2.1 AA is met for all new UI elements (axe-core green).
- The feature has been tested across the three supported browser environments (§31.7) via Playwright cross-browser.
- Unit + integration + contract + snapshot + a11y suites green; coverage threshold not regressed; mutation tests above the configured floor on critical packages.
- PRD section coverage check (sequencing rule 9) passes — every PRD section the feature implements has at least one named test.
- Feature flag (if applicable) is wired and tested in both states.
- Observability: at least one structured log per significant event, one metric per SLO-relevant operation, one trace span per user-visible action.