pravda-jo

PRAVDA — Master plan (synthesised from six workstream plans, 6 Sep 2026)

Source plans: 01-funnel-backend.md, 02-ops-console.md, 03-client-facing.md, 04-engine.md, 05-talent-docs.md, 06-deploy-process.md (same directory). This document is the reconciliation: the decisions where plans disagreed, the unified data model, and the execution waves. Execution agents cite item ids below (e.g. B-4, ENG-6) and read the source plan for detail.

1. Decisions (where the plans disagreed or deferred)

D1. The long-form report is retired. One artefact, /s, rendered by one component (components/SheetView.tsx). The public specimen becomes a real sheet at /specimen/[lang]. Delete /r, /p, ReportView, Configurator, compose.ts, report.ts, teardowns.ts, /api/teardown, /api/ops/run, /api/ops/save, /api/ops/concept, /api/proposal, /ops/[token], Editor.tsx. Interim: /api/ops/run is deleted in wave 0; the other legacy routes are fenced with LEGACY_REPORT=off → 410 until the deletion wave. /r/<token> stays as a 410 with a link to /{lang}/teardown for one quarter in case a link is live. Rationale: no UI feeds it, its pricing model (assemble-your-own) contradicts the locked flat offer, and two renderers means every Arabic fix is written twice.

D2. Session design. Ops cookie: sameSite: 'lax', value = v1.<issuedAtMs>.<hmac> signed with SESSION_SECRET (falls back to OPERATOR_KEY), 30-day rolling TTL enforced server-side, next= deep-link preserved on login, auth done once in app/ops/layout.tsx. A sameOrigin() check on every mutating route. The device-cookie + PIN scheme from the ops plan is deferred: two operators sharing one key, entered once per device per month, is acceptable; revocation = bump SESSION_SECRET. Talent cookie: real HMAC, sessionEpoch on Talent folded into the signature, bumped by reissue and “sign out everywhere”, 60-day TTL.

D3. “Opened” beacon. Client-side POST /api/s/opened from a tiny mounted component, never a server-render side effect (WhatsApp’s crawler and Khaled’s own check would stamp it first). Unified field names on Sheet: openedAt (first, write-once), lastOpenedAt, openCount. Rate-limited with the shared module. Not mounted on /specimen.

D4. “Sent” is a human act. approve mints the share link and does NOT set the client to sent. action: 'compose-share' returns the Arabic message + a wa.me link prefilled with the client’s number; action: 'mark-share-sent' writes Sheet.sentAt and advances the client to sent. Same two-step pattern (SendByHand) for talent bookings in DealDetail; the current “mark sent on window.open” goes.

D5. Placeholder people never reach a client or a booking. recommend.ts excludes placeholder: true (engine owns the line). approveSheet refuses placeholder-cast. offerBooking refuses a placeholder id. DealDetail and CastPicker disable them. The seed never downgrades a promoted record. Sheet.rosterState: 'real' | 'placeholder-only' | 'empty'.

D6. Cast overrides are materialised. Sheet.castOverrides[n] becomes CastPick[] (talentId, bilingual name, discipline, why) — not string[] — written by the cast action after validating each id (active, rate > 0, not placeholder, discipline matches the slot). /s, the preview, castPlan and the proposal all read the override when present.

D7. Vertical. Never asked on the public form. Inferred by lib/teardown/vertical.ts from bio + captions (+ site text), stored as Sheet.verticalGuess {guess, confidence, evidence}; Sheet.vertical set only at confidence ≥ 0.7 or by the operator. Operator override on the sheet page and on the client page (Client.vertical, reused on re-run) calls rerecommend(sheet, roster) — no Meta call. The client page never shows a guess; /s shows the vertical label only when operator-confirmed. The recommender scales the bonus by confidence.

D8. Read guarantee. Claim-leased queue: claimForRead transaction + readAndFile in lib/teardown/pipeline.ts; after() stays as the fast path; GET /api/cron/read (x-pravda-cron: CRON_SECRET) sweeps new and expired-lease reading clients, max 3 per call, readAttempts cap 3 → failed. Global Meta ceiling 40 reads/hour enforced in claimForRead (defers, never drops). The Cloud Scheduler job is a one-line owner action. minInstances is not raised.

D9. The “new lead” notice goes before the response, with a 4s abort on every sender. Operator notice cascade: WhatsApp (if configured) → Telegram → Email (Resend) → wa.me manual. Recommendation to the owner: Telegram permanently for operator notices; WhatsApp stays for talent.

D10. Rate limiting is one Firestore-backed module lib/store/ratelimit.ts (hit(bucket, limit, windowMs), hashed keys, expiresAt for a TTL policy), used by /api/lead (5/h, 20/day per IP + honeypot company + elapsedMs ≥ 1200), /api/t/login (8 per 10 min per IP, 60/min global), /api/s/opened, and the global read ceiling. Turnstile is wired but off by default.

D11. Engine doctrine made true. engagementRate becomes the median-based rate (the mean is kept as meanEngagementRate, never in copy). Hidden like counts → ig-likes-hidden (notable), never a critical. Website state is four-valued (no-url | unreadable | error-page | read); web-none fires only on no-url; read failures go to Findings.operatorNotes. Disjoint windows + margin for ig-window. A good finding always exists (engine invariant, capped at 3 shown). Concept #4 is excluded from the shortlist (headline: false), shape diversity ≤ 2 per shape, distinct because per recommendation. Thresholds are named constants with stated basis. Site timing = median of samples 2–3, copy says “HTML alone”. SSRF guard applied per redirect hop; body streamed and capped; parseSite is pure.

D12. Tests. One harness: tests/unit/*.test.mts with node --test (no network, injected fetch/lookup), itests against the Firestore emulator via FIRESTORE_EMULATOR_HOST, npm test runs both, GitHub Actions runs typecheck → lint → unit → itest (emulator) → build → bundle budget. Golden sheet snapshot for composition.

D13. Digits. Arabic-Indic on every Arabic surface (site, sheet, docs); Latin only for phone, CR, handles, Latin identifiers. One formatter: lib/format/num.ts (arNum, num, n0, n1, hour) and lib/format/date.ts (AR_MONTHS, EN_MONTHS, AR_DAYS, fmtDate, arDate, dayOf, months). This reverses the Western-digit choice in PricingView — flagged as an owner question; proceed with Arabic-Indic unless overruled.

D14. Booking lifecycle. BookingStatus gains cancelled, no_show; explicit transition table in lib/data/deals.ts; rescheduledFrom/To, declineReason, conflictWith, remindedAt[]. offerBooking returns conflict on same talent + date unless force. Reminders are an operator one-tap (“Send reminder”), not a scheduler.

D15. Staging = same project, second App Hosting backend with FIRESTORE_COLLECTION_PREFIX=staging_ and its own OPERATOR_KEY/SESSION_SECRET. Reserved prefixes: staging_, _itest_.

D16. Retention. expiresAt on clients/sheets for unconverted leads, cleared on win; N = 180 days default pending owner sign-off. scripts/export-client.mjs and scripts/delete-client.mjs --confirm back the promises on /{lang}/data.

2. Unified data-model changes (additive unless stated)

3. Execution waves (file-partitioned so parallel agents never collide)

Every agent: no git stash/reset/checkout/commit; only the orchestrator commits. Each wave ends with npx tsc --noEmit, npm run test:unit, and a grep-sentinel check by the orchestrator.

Wave 0 — foundations (parallel: A, B, C)

Wave 1 — core (parallel: D1, D2, D3, E1, E2, F, G1, G2)

Wave 2 — integration + console (after wave 1)

Wave 3 — polish + verification

Owner actions that code cannot do (collected): create the Cloud Scheduler job; set the new secrets in Secret Manager; enable Firestore TTL policies (ratelimit.expiresAt, clients.expiresAt, sheets.expiresAt); enable managed daily backups; create the Telegram bot; create the staging backend; confirm which branch App Hosting builds.

4. Open questions for the owner (answers change the work; defaults in brackets)

  1. Retire the long-form report and is any /r/<token> link live in a real thread? [retire; 410 for a quarter]
  2. Telegram for operator notices? [yes — cascade is built either way]
  3. Cloud Scheduler acceptable for the read sweep? [yes]
  4. Arabic-Indic digits on every Arabic surface, including the pricing page? [yes]
  5. Retention window for unconverted leads? [180 days]
  6. Proposal payment terms (deposit %, balance due, cancellation)? [omit the section until given]
  7. Engagement rate shown = median-based (harsher, correct)? [yes]
  8. Concept #4 “The Thread”: exclude from the shortlist, or keep as a priced add-on? [exclude]