Eng Review: AI Page Builder for Raklet (Hybrid)¶
Generated by /plan-eng-review on 2026-05-22 Branch: master Repo: rakletv3 Status: COMPLETE (all 4 sections + outside voice integrations locked) Design doc: rakletai-master-design-20260521-103533-ai-page-builder.md
Step 0 — Scope Challenge¶
Validation gate skipped (acknowledged risk). Design doc's Assignment said
run a 5-customer demand pass BEFORE eng review. CEO chose to proceed anyway.
If validation surfaces unexpected signal, large parts of this plan may need
re-cutting. Per feedback_review_chain_scope_creep.
Complexity check fires. Hybrid touches: onboarding flow, Social Page editor, new LLM service layer, new template-token rendering layer, prompt management, eval harness, cost controls, scraping pool. Well past 8-file / 2-new-service threshold. Justified by the explicit CEO choice; not silently expanded.
What already exists (reuse, don't rebuild): - Social Page storage (HTML in existing table) — V1 reuses, no new schema. - Onboarding funnel — V1 inserts steps, no new framework. - Member / event / payment data APIs — exist, need audit for AI consumability. - Existing prod deploy pipeline (master → test → prod promotion). - browse-tests Playwright DNA — reusable as a base for production scraping pool (but NOT the same worker pool; see Section 1). - Existing admin T&S workflow — extends for AI generation audit log.
Section 1 — Architecture (LOCKED)¶
Pipeline diagram¶
INTAKE STAGE 1 STAGE 2 STAGE 3 ASSEMBLY
URL ────┐ ┌─ Design system ─┬─ Page structure ─┬─ Per-page content ─┬─ Token-wire pages
PDF ────┼─Input───┤ inference │ planning │ generation │ + nav + theme
Desc ────┘ │ (colors/tone) │ (which pages, │ (HTML for each │ → save as Social
│ │ nav items) │ page) │ Page rows
│ │ │ │
LLM call #1 LLM call #2 LLM calls #3..#N No LLM
~2-5 sec ~2-5 sec ~5-15 sec each ~1 sec
(parallel)
DATA-AWARE TOKENS (resolved server-side at page render time, NOT generation):
{{members.directory}} → MembersService at render
{{events.upcoming}} → EventsService at render
{{membership.plans}} → MembershipService at render
Decisions¶
D2 — LLM provider: Anthropic Claude + Azure OpenAI (dual provider).
- Thin IAiGeneration interface with two implementations:
AnthropicGenerationProvider, AzureOpenAIGenerationProvider.
- Config flag picks provider per generation. Outputs + cost + latency logged
for offline comparison.
- Quality eval harness (offline) compares outputs from both providers on the
same prompts. Real $/portal comparison, not $/token (provider pricing
models differ; Anthropic prompt caching savings don't translate).
- Azure OpenAI Service is region-pinned, has separate rate limits, needs
explicit model deployments per region. Provisioning is its own setup step.
D3 — Execution model: Streaming progressive reveal. - Server-Sent Events on .NET MVC. EventSource on client. - Stage 1 + Stage 2 emit JSON events ("design system inferred", "page list: Home, About, Members, Events"). Stage 3 streams page-by-page HTML. - Client-side partial HTML parsing: defer paint until each page's HTML is fully streamed (chunk boundaries are not safe DOM boundaries). - Abort = HTTP cancellation cascades to LLM stream cancellation. Stop billing for unreceived tokens. - Backpressure: client closes mid-generation → server cancels LLM stream.
D4 — Cost ceiling + abuse: - Onboarding (all signups, free trial + paid): no per-user cap; bounded by signup volume (~300/mo). Worst case ~$60/mo. - In-product chat: PAID CUSTOMERS ONLY (gated by Raklet plan). - Per-user: 10 generations/hour, 50/day. - Per-org: 200 generations/day hard cap (configurable). - Provider-side: monthly hard cap at Anthropic + Azure of $N (TBD with finance) that auto-throttles. Alerts at 50% / 80% of cap. - New spec addition: onboarding step "what's your website?" captures URL during signup. Fallback to PDF or description if no website.
D5 — URL scraping: Headless browser (Playwright), separate production pool. - Reuse Playwright DNA from browse-tests but run on a SEPARATE production worker pool (queue + workers). Do NOT share with CI fixture pool. - Scrape returns clean HTML + screenshots + extracted text → all three fed to multimodal LLM (Claude or GPT-4o). - Hard timeout 15s per scrape. Retry once on transient failure. - Failure path: scrape fails (404, JS crash, CAPTCHA) → UI says "we couldn't fetch your site, paste a PDF or describe instead." - robots.txt: honor disallow (rare on org sites, but check). - Cost: ~$0.01 per scrape on internal infra.
D6 — Content safety + IP policy: Two-layer. - Layer 1 — Domain-match guardrail (CEO's idea): - Custom-domain email + URL on same domain → scrape allowed. - Free-provider email (Gmail/Hotmail/Outlook/Yahoo) OR domain mismatch → URL scraping disabled. Fall back to PDF or description. - Layer 2 — Provider safety classifiers: - Anthropic + Azure built-in classifiers refuse adult/illegal/violent content. - Log every generation (input + output + user ID + provider) for reactive ban workflow. - Reactive ban via existing admin T&S workflow extension.
D7 — Pipeline staging: Invisible to user. - Stages run sequentially server-side. User sees streaming portal output (nav → pages), not labeled checkpoints. - "Confirmation gate" + "full transparency" + "advanced mode" all deferred to V1.5 or killed; V1 = clean magic.
Eng-only architectural picks (no CEO decision needed)¶
E1 — Token rendering layer placement: Razor partials.
- Raklet is .NET MVC. Tokens like {{members.directory}} get pre-processed
server-side at page render into Razor partial invocations (e.g.
@Html.Partial("_MembersDirectory")). Existing Raklet partial infra reused.
- New IAiTokenResolver service maps token string → partial name + model.
- No new templating engine (Liquid/Handlebars). Stay on Razor.
E2 — Portal-level state model: OrgPortalConfig table.
- New small table: OrgPortalConfig (one row per org) stores theme colors,
nav structure, AI-generation metadata (provider used, timestamps,
source-of-truth: URL/PDF/desc).
- Per-page HTML still lives in existing Social Page table; the
OrgPortalConfig row is the umbrella concept.
- Schema change goes in its own migration branch per
feedback_migration_branch_first.
E3 — Generation prompt management: Versioned in source.
- Prompts live in Raklet.Services/AiGeneration/Prompts/*.txt (or similar).
- Each prompt has a version number embedded. Generation log records prompt
version used.
- Eval harness compares prompt versions across providers.
E4 — Watermark metadata (TODO candidate):
- Add <meta name="generator" content="Raklet AI YYYY-MM-DD"> to every
AI-generated page. Audit trail for any later IP dispute. ~1 hour add.
E5 — Production scraping pool ≠ browse-tests CI pool.
- Separate worker process and queue. Conflating them risks CI traffic
flooding the AI flow and vice versa. Per feedback_test_helpers_not_in_bundled_tree
principle.
Section 2 — Code Quality (LOCKED)¶
Eng-only picks (no CEO decision needed)¶
CQ1 — Cancellation tokens end-to-end.
Every async path (SSE → LLM stream → Playwright scrape → DB writes) takes
a CancellationToken. Wire from the HTTP request token down to provider
SDK calls. Abort = whole chain stops cleanly.
CQ2 — Token resolver defensive defaults.
Every token ({{members.directory}}, {{events.upcoming}},
{{membership.plans}}) renders an empty-state component, not a crash, when
underlying data is empty/deleted. Test each token against zero, one,
many, and deleted-source-row cases.
CQ3 — Prompt versioning in source.
Prompts live in Raklet.Services/AiGeneration/Prompts/*.txt. Each file has
a version comment at the top. Generation log records the version used. No
DB-backed prompt management in V1.
CQ4 — Eval harness project: separate.
New Raklet.AiEval console project. NOT under main test suite (different
lifecycle, different reviewers, different runtime needs). Runs offline
against captured generation fixtures.
CQ5 — Error UX inventory: define before ship. Every failure mode gets a defined user-facing message before V1 ships. Failure modes inventory: - LLM provider 5xx → "Our AI is taking a break, try again in a minute." - LLM refused content → "We couldn't generate this content. Try a different description." - Scrape failed (404/CAPTCHA/JS error) → "We couldn't reach your site. Paste a PDF or describe your org instead." - Cost cap hit → "You've reached today's generation limit. Upgrade or retry tomorrow." - Browser closed mid-generation → resume option ("you have a draft in progress") on next login. - Network drop mid-stream → graceful client-side reconnect, server picks up from last completed page. - No UI gets a raw 500 page.
D8 — Audit log: Azure Storage Table + Blob (LOCKED)¶
Schema:
- Storage Table AiGenerationLog: PartitionKey = org_id,
RowKey = {ISO timestamp}_{guid}, columns: user_id, provider
(anthropic|azure-openai), model, prompt_version, cost_usd,
latency_ms, status (success|refused|error|cancelled), input_kind
(url|pdf|desc), surface (onboarding|in-product), input_blob_url,
output_blob_url.
- Blob container ai-generation-payloads: full input + full output
HTML. One blob per generation, URL referenced from table row.
- SQL remains source of truth for AI-derived insights that flow back
into customer data (org theme colors, member tag suggestions, etc.). A
separate analytics job consumes the audit log to write back to SQL —
decoupled from the live generation path.
Why this shape: - Lookup pattern is by org or by date, partition+row key fits perfectly. - Storage Table is ~10× cheaper than CosmosDB at this volume, ~3× cheaper than indexed SQL rows for this access pattern. - Blob keeps fat payload off the indexed store; cheap; auditable. - SQL stays clean of high-churn audit data.
Eng note (capture in PR description, not a CEO decision): Storage Table has a 1 MB row limit. Generated HTML can exceed this for multi-page portals → always put HTML in Blob, never inline in the row. The table row is metadata only.
Section 3 — Tests (LOCKED)¶
Coverage diagram¶
68 code/flow paths identified, all GAP (plan-stage). 4 CRITICAL regressions (must-have tests, IRON RULE). 8 LLM eval categories. Full diagram in conversation transcript above and in test plan artifact.
D9 — Eval scope: EN-only at V1 (LOCKED)¶
- V1 eval harness covers English-only generation.
- Other locales (DE/FR/SV/ZH) generate via prompt locale variable but are NOT quality-gated by automated eval.
- Per-locale errors surface from audit log; V2 expands eval matrix based on signup volume.
Test strategy summary¶
- Unit + integration (.NET xUnit/NUnit): every component in the coverage
diagram with a unit test for happy path + each error branch. Mocked
providers via
IAiGenerationtest doubles. - E2E (browse-tests Playwright): 22 flows covering the user journeys
in the diagram. New
payment-regression-style suite:ai-portal-regression. - LLM eval (new
Raklet.AiEvalconsole project): - 8 eval categories: HTML validity, XSS safety, portal-quality vs human baseline, Anthropic vs Azure comparison, regression on prompt changes.
- Fixtures: 20 archetype org descriptions (chess club, alumni group, professional association, nonprofit, etc.) in English.
- Regressions (IRON RULE — no AUQ):
- Editing an existing (non-AI) Social Page still works after chat UI replaces textarea.
- Rendering an existing Social Page returns byte-identical output after token resolver layer lands.
- Interrupted signup resumes cleanly through new onboarding steps.
- Mobile app reads and renders AI-generated pages correctly.
Test plan artifact¶
Separate file at
~/.gstack/projects/rakletv3/rakletai-master-eng-review-test-plan-20260522.md
for consumption by /qa and /qa-only.
Section 4 — Performance (LOCKED)¶
Eng-only picks (no CEO decision needed)¶
P1 — Token resolver batching. Members/events/membership tokens fetch
their source data in a single batched query, not N+1. Defensive
pattern: every resolver receives an IEnumerable<int> of IDs or a filter,
returns the batch.
P2 — Playwright pool sizing. Start with 4 worker processes; scale up if queue depth > 10. Use existing browse-tests pool sizing as a starting point but as a SEPARATE production pool.
P3 — SSE connection pool ceiling. .NET MVC holds a thread per open SSE stream. Cap concurrent in-flight generations at 50 server-wide via semaphore; queue overflow returns "try again in a minute" UX.
P4 — Cost-cap middleware caching. Cache cap status in-memory per request scope; don't re-query Storage Table per LLM call. Invalidate on generation completion.
D10 — Stage 3 timing: Sequential per-page (LOCKED)¶
- Pages generate one at a time. User sees page 1 at ~5s, page 2 at ~10s, etc.
- Wall-clock ~30s for 6 pages. Feels alive.
- Failure isolation: one page fails → others continue. Failed page can be retried without re-generating the whole portal.
- Rate-limit safer: smaller per-second token burst.
Production latency budget¶
- Stage 1 (design system): 2–5s
- Stage 2 (page structure): 2–5s
- Stage 3 (per-page, sequential ×6): ~30s
- Assembly + DB writes: ~1s
- Total onboarding portal generation: ~40s p50, ~60s p95.
- In-product single page: ~5–8s.
Section 5 — Outside Voice Integrations (LOCKED)¶
Independent Claude subagent surfaced 10 findings against the locked plan. Six were material. Two were strategic tensions for the CEO; four were must-have additions that I'm applying directly.
D12 — Data-aware moat at onboarding: Accept empty states as CTAs (LOCKED)¶
- Empty
{{members.directory}}→ "Invite your first members" CTA + button. - Empty
{{events.upcoming}}→ "Add your first event" CTA + button. - Empty
{{membership.plans}}→ "Set up your first plan" CTA + button. - Reframes the data-aware moat: not instant demo wow, but a portal that grows into itself as the org populates Raklet. Empty-state CTAs onboard new orgs into Raklet's core features.
- Design doc framing for V1 demo material should explicitly NOT promise "look at your live members directory" before the user has members.
D13 — Onboarding latency: Background WebJob + notification (REVISES D3)¶
- Onboarding generation moves from synchronous SSE streaming to BACKGROUND WebJob. D3 streaming UX revised: applies only to in-product chat surface.
- New onboarding flow: user submits intake → "We're building your portal — explore your dashboard while we work, you'll get an email + in-app notification when it's ready (~60 sec)." → user lands on dashboard → notification fires → portal appears.
- In-product chat surface KEEPS streaming SSE (single-page, ~5-8s, alive reveal makes sense at that timescale).
- Cost cancellation: if user closes browser mid-onboarding, generation completes in background (already-paid tokens). Output saved, available on next login.
- New components: WebJob/Hangfire job runner (or existing Raklet job infra — needs eng audit), in-app notification system (audit existing Raklet notification UX), email template.
Must-have additions (applied directly, no CEO decision needed)¶
OV1 — Token resolver SSTI allowlist (security).
IAiTokenResolver ships with an explicit whitelist of approved token
names: members.directory, events.upcoming, events.past, events.calendar,
membership.plans, membership.signup, org.contact, org.about.
LLM-emitted tokens not on the list render as inert text (not Razor partial
invocations). New tokens added by explicit code review.
OV2 — Prompt injection hardening (security).
- All scraped HTML + extracted PDF text wrapped in XML tags
(<untrusted_input>...</untrusted_input>) before being concatenated
into the LLM prompt.
- System prompt explicitly instructs the LLM to treat content inside
<untrusted_input> as data to summarize, not instructions to follow.
- Anthropic prompt-caching keys exclude the untrusted-input section so
cached prefix can't be poisoned across users.
- Adversarial corpus added to eval harness (20+ prompt-injection variants
in the XSS safety eval category).
OV3 — Feature flag + cohort split (validation gate). - V1 ships behind a cohort flag: 50% of new signups get the AI flow, 50% continue with current onboarding. Existing customers get the in-product chat for paid orgs only (no cohort split needed — they self-select). - Cohort assignment is sticky per org (consistent experience across sessions). - Kill switch: feature flag can disable AI flow entirely for all signups. - Conversion lift now measurable: AI-cohort signup→first-published-page vs control-cohort signup→first-published-page.
OV4 — Cost ceiling math correction. - Revised model: each portal generation includes initial + average 3 regenerations (per-page edits, theme tweaks, content rewrites). - 300 onboarding signups/mo × ~$0.20 base × 3 (regenerate multiplier) = ~$180/mo onboarding cost. - In-product chat: paid customers × estimated 5 generations/customer/month × $0.05 = bounded by paid count, expect $500–$1500/mo at current scale. - Revised V1 total estimate: $700–$2000/mo, not the original $300–$600. - Monthly hard cap at provider level: target $3000/mo (~50% buffer over high estimate) — confirm with finance before V1 ship.
OV5 — Eval baseline: AI vs current Raklet textarea, not just provider A/B.
- New eval category in Raklet.AiEval: side-by-side comparison of AI-generated
output vs a hand-built reference page using the existing textarea editor.
- Reference pages: 5 archetype orgs, each built once via current textarea
(sampled from existing high-quality Raklet customer pages), once via AI
generation from the same org's PDF/description.
- LLM-judge scores both; humans spot-check the judge's calls. Target: AI
output scores ≥ reference 80% of the time.
- Without this baseline, "AI passes design QA ≥80%" is unmeasurable.
OV6 — Mobile sequencing fix. - Mobile rendering audit happens BEFORE V1 onboarding ships, not during. - Audit task: render 5 existing Social Pages from the Raklet mobile app; confirm tokens (when added) also render mobile-correctly. - If mobile audit surfaces gap: V1 AI-generated pages are flagged "web-only preview" until mobile rendering ships in V1.1. NOT a CRITICAL regression blocker for V1.
Outside voice findings noted but not changing the plan¶
- #3 (2barevents is N=1): Acknowledged. The 5-customer demand pass (deferred by CEO at Step 0) remains the right counter to this risk. Re-flagging it as the highest-priority follow-on action.
- "Cut V1 in half": CEO already made this call at Step 0 with full knowledge of the scope-creep risk. Not re-litigating. The locked decisions D12 + D13 + OV3 materially reduce the V1 surface anyway (background generation + empty-state CTAs + cohort flag = smaller bet than the original Hybrid as written).
TODOs to file (Linear recommended when V1 implementation starts)¶
T1 — 5-customer demand pass (CRITICAL pre-ship). What: 2-minute Loom showing 2barevents portal generation; show to 5 named Raklet customers across org types; collect specific quotes about whether they'd re-do their portal with this and what would make it a no-brainer. Why: counters the "2barevents is N=1" risk surfaced by the outside voice and matches the design doc's deferred Assignment. Five quotes also feed V1 marketing copy. Pros: Validates demand with named evidence before maximalist engineering investment. Catches mismatches between CEO's instinct and customer reality. Cons: ~2 days of CEO time. Risks surfacing data that forces V1 re-cut. Context: Was the original deferred Assignment from the design doc; CEO chose to proceed with full eng review anyway at Step 0. Now an explicit TODO so it doesn't drop. Depends on: nothing.
T2 — Mobile rendering audit (V1 pre-ship gate). What: render 5 existing Social Pages from the Raklet mobile app; confirm the token resolver layer (once it lands) also renders mobile-correctly. Document any rendering gaps. Why: per OV6, mobile rendering for AI pages is currently an open question that was inappropriately listed as a CRITICAL regression. Audit first, then we know what V1 ships with. Pros: Catches mobile gap before V1 ships; allows "web-only preview" fallback if needed. Cons: ~1 day of mobile team time. Context: User added Claude access to the mobile repo during /office-hours; that's where this audit lives. Depends on: nothing. Can run in parallel with backend implementation.
T3 — HTML <meta> generator watermark.
What: every AI-generated page gets <meta name="generator" content="Raklet AI YYYY-MM-DD">
in its <head>. Generation source (URL / PDF / desc) tagged as a separate
<meta> for audit trail.
Why: provides legal / IP audit trail for any later content dispute.
~1 hour of eng work for permanent provenance.
Pros: Audit trail at near-zero cost. Disambiguates AI-generated content
from human-created Social Pages.
Cons: Negligible. Single line in the assembly stage.
Context: Surfaced as a TODO candidate during eng review; CEO accepted.
Depends on: assembly stage of generation pipeline being implemented.
V2 candidates (parked in this doc, not yet filed)¶
- In-product portal-level regenerate (existing customers redo whole portal).
- Drag-and-drop power-user editor (gated on V1 demand signal).
- Multi-language eval expansion to DE/FR/SV/ZH (currently EN-only at V1).
- Provider failover (Anthropic ⇄ Azure OpenAI on 5xx).
- Confirmation gate UX (Stage 1 design preview before Stage 2/3).
- Streaming progressive reveal at onboarding (revisit if background path underperforms).
Completion Summary¶
- Step 0: Scope Challenge — CEO chose full eng review on Hybrid plan (acknowledged scope-creep risk; not re-litigated).
- Architecture Review: 6 strategic decisions locked (D2–D7). 5 eng-only picks captured (E1–E5).
- Code Quality Review: 1 strategic decision locked (D8). 5 eng-only picks captured (CQ1–CQ5).
- Test Review: 68 paths in coverage diagram, all GAP (plan-stage). 4 CRITICAL regressions. 8 LLM eval categories. 1 strategic decision (D9). Test plan artifact written.
- Performance Review: 1 strategic decision locked (D10). 4 eng-only picks captured (P1–P4).
- Outside Voice: ran (Claude subagent, codex CLI unavailable). 10 findings; 2 cross-model tensions resolved (D12 + D13 — both REVISED the plan); 4 must-have additions applied (OV1–OV6); 1 finding noted but not re-litigated; 1 ("cut V1 in half") already addressed at Step 0.
- NOT in scope: written (7 items deferred).
- What already exists: written (6 reuse points identified).
- TODOs proposed: 3 V1 pre-ship items captured (T1–T3); 6 V2 candidates parked.
- Failure modes / critical gaps: 0 ungaurded (every failure mode has defined UX in CQ5).
- Parallelization: see below.
- Unresolved decisions: 0.
- Lake Score: 13/14 recommendations chose complete option (only D5 used existing infra over fancier vendor — appropriate).
Worktree parallelization¶
Implementation lanes (module-level):
| Step | Modules touched | Depends on |
|---|---|---|
| L1 — LLM provider plumbing | Raklet.Services/AiGeneration/ (new), Raklet.Eval/ (new) |
— |
| L2 — Storage Table audit log | Raklet.Services/AiGeneration/, Azure infra |
— |
| L3 — Playwright prod scraping | Raklet.Services/AiScraping/ (new), Azure WebJobs |
— |
| L4 — Token resolver (Razor) | Raklet.Web/, Raklet.Services/AiGeneration/ |
L1 |
L5 — OrgPortalConfig schema migration |
EF migrations | — |
| L6 — Cost-cap middleware + paid-gate | Raklet.Web/Middleware/ |
L2 |
| L7 — Onboarding flow changes (background WebJob) | Raklet.Web/Onboarding/, Hangfire |
L1, L5 |
| L8 — In-product chat UI (SSE) | Raklet.Backend/Content/scripts/, Raklet.Web/ |
L1 |
| L9 — Cohort feature flag | Raklet.Web/, existing flag infra |
— |
Lanes that can run in parallel from day 1: L1, L2, L3, L5, L9 (all independent). After those land: - L4 (after L1) — depends on provider interface. - L6 (after L2) — depends on audit log shape. - L7 (after L1 + L5) — needs both LLM service and schema. - L8 (after L1) — needs LLM service interface.
Conflict flag: L4 and L7 both touch Raklet.Web/ views — coordinate
on PR sequencing or accept rebase friction.
NOT in scope (V1)¶
- Drag-and-drop editor (deferred to possible V2; gated on V1 demand signal).
- In-product portal-level regenerate (existing customers redo whole portal via Wizard) — V2.
- Custom-LLM-per-org / BYO-key. V2.
- Multi-LLM router beyond Anthropic + Azure OpenAI. V2.
- "Confirmation gate" UX between Stage 1 and 2. Killed for V1.
- Advanced/transparency stage view. V2 at earliest.
- Marketing landing page for the feature. Separate marketing plan.
Open questions (still)¶
- LLM-provider monthly cost ceiling $ — needs finance input.
- Mobile app surface — is generated portal viewable from Raklet mobile? Audit existing mobile rendering for HTML pages from Social Page table.
- Multi-language: default to org account locale for LLM generation. Prompt needs locale variant for DE/EN/FR/SV/ZH. Eng to design.
- Data API consumability audit — do existing Members/Events/Membership APIs return shapes consumable by Razor partial models? Likely needs adapter.
- Onboarding-step UX wireframe — where exactly does "what's your website" appear? Belongs in /plan-design-review.