← Back to Proposals
Confidential - Advisory Review

VerdictTank

AI-powered government contract proposal platform - find, analyze, and win federal contracts

📅 August 2026🏢 IT Pro Partner - Product Division🌐 rfptank.com

Marketing Site

rfptank.com
Not created

Dashboard/App

app.rfptank.com
Not created

API

api.rfptank.com
Not created

Domain

rfptank.com
Purchased

# VerdictTank v3.6 - Technical Architecture

**Status:** Build specification, companion to the v3.6 business proposal.

**Scope:** Extends the v3 architecture (5-stage pipeline, 4 API endpoints, 10 data model tables) with the 9 new v3.6 capabilities: Chat-to-Refine, URL-to-Review, Dual Scoring, Explain-the-Low-Score, Fix-It Remediation, Second-Opinion Audit Agent, Configurable Rules Engine, White-Label for Consultants, and Roast/Boost Community Peer Review.

**Audience:** Engineering. This is the build spec, not the pitch.

1. System Overview

> **v3.6 scope note (Priority Fix #6 / Judge 3 Week 1 Cut List):** The critical review of v3.6 found the 15-service, 21-table build scoped "like a multi-engineer build" for a solo-founder timeline. This document now describes two tiers: the **8-Week MVP** (5 services, shipped) and **Deferred (Post-Launch)** components (5 services, designed but not built until a funnel/demand signal justifies them). Deferred components' schemas and designs are preserved below for continuity - cutting scope does not mean deleting the design work, it means sequencing it after MVP validation. See Section 9 for the revised build sequence.

1.1 Architecture Diagram - 8-Week MVP Scope

┌─────────────────────────────────────────────┐ │ CLIENTS │ │ Web App · Mobile · API Consumers │ └───────────────────┬───────────────────────────┘ │ HTTPS ┌────────────────────▼────────────────────────┐ │ EDGE / API GATEWAY (Caddy) │ │ TLS term · rate limit · auth (JWT/API key) │ │ request logging │ └───────┬───────────────────────────────────────┘ │ ┌─────────────────────▼───────────────────────┐ │ APPLICATION SERVICES (MVP: 2 of 5 kept) │ │ review-api · chat-api │ └───────────┬───────────────────────────────────┘ │ enqueue ┌───────────▼───────────────────────────────────────────────┐ │ JOB QUEUE (Redis + RQ/Celery) │ │ review.pipeline · chat.turn · url.extract │ │ remediation.generate · sanitize.scan │ └───────────┬─────────────────────────────────────────────────┘ │ ┌───────────────────────────▼──────────────────────────────────────────────────┐ │ REVIEW PIPELINE WORKERS (pipeline-worker) │ │ │ │ Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 │ │ Research ──▶ Primary ──▶ Validation ──▶ Cross-Check ──▶ Verdict │ │ Agent Reviewer Reviewer A + B (‖) Aggreg. │ │ (web verify (10-dim, (challenges (independent (majority │ │ + citations) 1-10 + tags primary score) re-score) + dual │ │ idea/proposal) scores) │ │ │ │ [Phase 6 - Audit Agent: DEFERRED, see §1.2 and §9 Post-Launch Track] │ └───────────┬─────────────────────────────────────────────┬──────────────────────┘ │ │ ┌───────────▼───────────────┐ ┌────────────▼──────────────────┐ │ SANITIZATION GATE │ │ POST-PIPELINE GENERATORS │ │ scans: verdict JSON, │ │ Explainer (per-dim text) │ │ explanations, remediation,│ │ Fix-It Generator (action plan)│ │ chat transcripts │ │ (Rules Engine overlay: DEFERRED)│ │ before egress │ └──────────────┬───────────────────┘ └───────────┬───────────────┘ │ │ │ ┌───────────▼────────────────────────────────────────────────▼───────────────┐ │ DATA LAYER │ │ PostgreSQL (primary OLTP, org_id column present but multi-tenant enforcement │ │ (RLS) deferred until White-Label track - see §1.2) │ │ pgvector (corpus embeddings/similarity) │ │ Redis (queue, session cache, rate limits, feature flags) │ │ S3-compatible object store (PDFs, chat exports, uploaded docs, extracted HTML)│ └─────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────────────┐ │ SUPPORTING SERVICES (MVP) │ │ url-extractor (Crawl4AI) · fixit-generator · pdf-generator │ │ cron (prediction T+90/180/365) │ │ [domain-verifier, moderation-queue: DEFERRED - see §1.2] │ └────────────────────────────────────────────────────────────────────────────┘

1.2 Component List - MVP vs. Deferred

Per Priority Fix #6, the 12-week solo-founder MVP ships **5 services**; the remaining **5 services** from the original v3.6 scope are deferred to post-launch and gated on real usage/demand signal, not built speculatively. This directly implements the review's Week 1 Cut List (KEEP: Dual Scoring, Explain, Fix-It MVP, URL-to-Review, Corpus Schema / CUT: Audit Agent, Rules Engine, White-Label, Roast/Boost, and Chat-to-Refine deferred-not-deleted per below).

**MVP - Ship in 8 Weeks (5 services)**

Layer Component MVP Status Notes
App `review-api` **KEEP** Core review submission/status/verdict endpoints; dual-score fields, `/review/url`
App `chat-api` **KEEP (reduced)** Chat-to-Refine, but scoped down to the ghostwriting-guard behavior in §5.1 - no `chat-ws` streaming socket in MVP, REST polling only, to cut infra surface. Positioned as the "un-copilot" (see §5.1) rather than deferred outright, since Judge 3 flagged it as a differentiator once guarded correctly - see rationale note below.
Worker `url-extractor` **KEEP** Crawl4AI-based content extraction + sanitize; Feature 2 (URL-to-Review)
Worker `pipeline-worker` **KEEP** Phases 1-5 (Research → Primary → Validation → Cross-Check → Verdict/Dual-Score Aggregation). Phase 6 (Audit Agent) removed from the worker in MVP.
Worker `fixit-generator` **KEEP** Fix-It Remediation, upgraded to quality-scoring per §5.5 - retention-loop feature, explicitly called out as a KEEP in the review's cut list

> **Note on Chat-to-Refine placement:** The review's Week 1 Cut List names Chat-to-Refine as a CUT ("funnel feature; build when you have a funnel"). This document keeps a *minimal* `chat-api` in MVP scope specifically because the ghostwriting guard in §5.1 is cheap to build (a length/format check, not new infrastructure) and de-risks the single biggest brand-reputation exposure identified by Judge 3 if Chat-to-Refine ships at all, in this MVP or later. If engineering capacity is tighter than modeled, `chat-api` is the first MVP component to cut - in which case Chat-to-Refine moves to the Deferred table below in its entirety, `chat-ws` and all real-time streaming remain deferred either way, and the guard design in §5.1 is preserved for whenever the feature ships.

**DEFERRED - Post-Launch, Built on Demand Signal (5 services)**

Layer Component MVP Status Notes Deferred Trigger
App `rules-api` **CUT (deferred)** CRUD + validation for org rule definitions Revisit once an Enterprise/White-Label deal is in active negotiation and requires it
App `org-api` **CUT (deferred)** white-label org config, branding, API keys Blocked on Minimum Viable Legal (MSA + Privacy Program) per the review's Legal Killers - see §4
App `peer-review-api` **CUT (deferred)** Roast/Boost submission + moderation Moderation overhead + defamation exposure (Judge 4 Legal Killer #4) outweighs launch value
Worker `audit-agent-worker` **CUT (deferred)** Phase 6 of pipeline, second-opinion audit "Enterprise trust problem, not launch problem" per Judge 3; revisit once Enterprise tier has real pilot demand
Worker `moderation-worker` **CUT (deferred)** abuse handling for Roast/Boost Dependent on `peer-review-api`; deferred together
Infra `domain-verifier` **CUT (deferred)** CNAME/TXT verification for white-label custom domains Dependent on `org-api` / White-Label track; also blocked on Minimum Viable Legal

The `rules-evaluator` overlay logic (§5.7) and `Rules Engine` pipeline injection point are deferred along with `rules-api` - no schema or prompt-injection surface for org rules ships in MVP. The full designs for all deferred components remain documented in Sections 5.6-5.9 and 7 of this document as the intended post-launch build, not as speculative dead weight - they represent validated architecture pending a demand signal, not throwaway work.

**MVP Data Layer implications:** Row-level security (RLS) multi-tenant policies (§6.3, §7.4), the `org_configs`/`org_rules`/`org_api_keys`/`rule_templates` tables, and cross-org isolation logic are **not required for MVP** since `org-api` and `rules-api` are deferred - `org_id` columns remain present (nullable) on core tables for forward compatibility, but RLS enforcement, domain routing, and multi-tenant billing are built when the White-Label track is actually resumed, gated on Minimum Viable Legal per Section 4.

2. Complete Data Model

2.1 Entity Relationship Summary

users ──1:N── reviews ──1:1── verdicts ──1:N── dimensions ──1:1── dimension_explanations │ │ │ │ │ └─1:N── remediation_plans (per dimension) │ ├──1:N── judge_scores ──N:1── judges │ ├──1:N── research_citations │ ├──1:N── predictions │ ├──1:N── audit_findings (v3.6) │ ├──1:N── peer_reviews (v3.6) │ └──1:1── chat_sessions (optional, pre-review link) (v3.6) │ ├──1:N── chat_sessions ──1:N── chat_messages (v3.6) │ └──N:1── org_configs ──1:N── org_rules ──N:1── rule_templates (v3.6, enterprise/white-label) corpus_entries (derived from verdicts, searchable, org_id-scoped) audit_log (cross-cutting, all tables)

All new v3.6 tables carry `org_id` (nullable for individual/non-enterprise accounts) to support multi-tenant partitioning for White-Label. All v3 tables are extended with `org_id` as part of the Phase 0 migration described in Section 9.

2.2 v3 Tables (Extended for v3.6)

> **Dual-Score Schema Validation Gate (Priority Fix #2 / Judge 3 & Judge 1 finding, Architecture §2.2 lines 147-148 in the v3.6 review):** The original v3.6 design baked `idea_score`/`proposal_score` directly into the `reviews` and `corpus_entries` tables as permanent NUMERIC columns from day one - a schema bet made before any beta user confirmed that a two-score split is more useful than the v3 single composite score. The review's Priority Fix #2 is explicit: *"Do not bake `idea_score`/`proposal_score` into the corpus before validating with beta users that two scores are wanted. Gate the migration behind user testing, or ship dual-scoring as a non-schema overlay first."*

>

> **v3.6 resolution - ship as overlay, migrate only after validation:**

> 1. **MVP ships dual scoring as a JSONB overlay, not dedicated columns.** The `idea_score`/`proposal_score` values computed at Phase 5 (see §5.3) are written into `verdicts.verdict_json` (already a JSONB column, no migration required) under a `dual_score` key, and into a new `reviews.dual_score_overlay JSONB` column (nullable, additive, zero-downtime to add/drop) rather than as first-class typed `NUMERIC(5,2)` columns on `reviews` or `corpus_entries`.

> 2. **The NUMERIC columns shown below (`idea_score`, `proposal_score` on `reviews`; same on `corpus_entries`, §2.3) are the *target* schema for GA, not the MVP schema.** They are documented here for architectural continuity but are marked `DEFERRED - VALIDATION GATE` and must not be created by the Phase 0 migration until the exit criterion below is met.

> 3. **Validation exit criterion (must pass before the permanent-column migration runs):** a minimum of 25 beta users from the existing $29/mo Inner Circle cohort have used dual-score overlay output across at least 2 reviews each, and post-review survey/interview signal shows a majority preference for two scores over the legacy single composite. This reuses the same beta cohort the review's Priority Fix #1 calls for surveying - one survey instrument serves both purposes.

> 4. **If validation fails** (users prefer one score, or show no measurable preference), the `dual_score_overlay` JSONB column is dropped, `verdict_json.dual_score` continues to carry the value for backward-compatible display only, and the corpus indexing strategy in §2.3/§2.4 reverts to `legacy_composite_score` as the primary sortable/filterable column.

> 5. **Corpus search and percentile lookups (§6.4) run against the JSONB overlay during the validation window** - `idx_corpus_dual_score_overlay` is a GIN index on the JSONB path, functionally equivalent to a B-tree NUMERIC index for the query patterns in §3.2/§3.3 but avoids a schema commitment. Query latency is slightly higher (GIN vs. B-tree) but acceptable at MVP corpus volume; this is re-benchmarked before the permanent migration.

```sql

-- USERS (v3, extended)

CREATE TABLE users (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

org_id UUID REFERENCES org_configs(id), -- v3.6: multi-tenant scoping

email TEXT UNIQUE NOT NULL,

password_hash TEXT,

tier TEXT NOT NULL DEFAULT 'free', -- free|pro|enterprise|white_label|beta

role TEXT NOT NULL DEFAULT 'member', -- member|org_admin|consultant|superadmin

created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

CREATE INDEX idx_users_org ON users(org_id);

-- REVIEWS (v3, extended: dual score columns, submission source, org scoping)

CREATE TABLE reviews (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

org_id UUID REFERENCES org_configs(id),

user_id UUID NOT NULL REFERENCES users(id),

source_type TEXT NOT NULL DEFAULT 'file', -- v3.6: file|url|api

source_url TEXT, -- v3.6: populated when source_type='url'

chat_session_id UUID REFERENCES chat_sessions(id), -- v3.6: link to pre-review coaching

vertical TEXT,

status TEXT NOT NULL DEFAULT 'queued', -- queued|running|audit|done|failed

idea_score NUMERIC(5,2), -- DEFERRED - VALIDATION GATE: do not populate until dual-score validation exit criterion (§2.2) is met; MVP writes to dual_score_overlay instead

proposal_score NUMERIC(5,2), -- DEFERRED - VALIDATION GATE: see idea_score note above

dual_score_overlay JSONB, -- v3.6 MVP: { "idea_score": 78.5, "proposal_score": 41.0 }, non-schema overlay per §2.2 gate; promoted to typed columns above only post-validation

legacy_composite_score NUMERIC(5,2), -- v3: preserved for backward compat

rules_applied JSONB DEFAULT '[]', -- DEFERRED - org_rules snapshot, populated only once Rules Engine (§5.7) ships post-launch

parent_review_id UUID REFERENCES reviews(id), -- v3.6 (§5.5): set when this review is a resubmission addressing a prior remediation_plan

fix_completion_score NUMERIC(3,2), -- v3.6 (§5.5): mean fix_quality_tier (0-3) across parent's action_items, computed on resubmission

created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

completed_at TIMESTAMPTZ

);

CREATE INDEX idx_reviews_org ON reviews(org_id);

CREATE INDEX idx_reviews_user ON reviews(user_id);

CREATE INDEX idx_reviews_status ON reviews(status);

CREATE INDEX idx_reviews_parent ON reviews(parent_review_id);

-- VERDICTS (v3, unchanged shape, verdict_json now carries dual-score payload)

CREATE TABLE verdicts (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

review_id UUID NOT NULL REFERENCES reviews(id) UNIQUE,

verdict_json JSONB NOT NULL, -- see Section 2.5 for v3.6 schema

pdf_url TEXT,

public_share_id TEXT UNIQUE,

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

-- DIMENSIONS (v3, extended: score_type tag for dual scoring)

CREATE TABLE dimensions (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

review_id UUID NOT NULL REFERENCES reviews(id),

name TEXT NOT NULL, -- e.g. "Market Analysis"

score NUMERIC(4,2) NOT NULL, -- 1-10, existing v3 scale

score_type TEXT NOT NULL DEFAULT 'proposal', -- v3.6: 'idea' | 'proposal'

weight NUMERIC(4,3) DEFAULT 1.0,

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

CREATE INDEX idx_dimensions_review ON dimensions(review_id);

-- JUDGES (v3, unchanged)

CREATE TABLE judges (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

role TEXT NOT NULL, -- Research|Primary|Validation|CrossCheckA|CrossCheckB|...

vendor_internal TEXT NOT NULL, -- stripped by sanitization gate before egress

is_audit_agent BOOLEAN NOT NULL DEFAULT false, -- v3.6: flags the Phase 6 role

active BOOLEAN NOT NULL DEFAULT true,

accuracy_score NUMERIC(5,4),

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

-- JUDGE_SCORES (v3, unchanged)

CREATE TABLE judge_scores (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

review_id UUID NOT NULL REFERENCES reviews(id),

judge_id UUID NOT NULL REFERENCES judges(id),

dimension_id UUID REFERENCES dimensions(id),

raw_score NUMERIC(4,2) NOT NULL,

rationale TEXT,

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

-- RESEARCH_CITATIONS (v3, unchanged)

CREATE TABLE research_citations (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

review_id UUID NOT NULL REFERENCES reviews(id),

source_url TEXT NOT NULL,

excerpt TEXT,

retrieved_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

-- PREDICTIONS (v3, unchanged)

CREATE TABLE predictions (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

review_id UUID NOT NULL REFERENCES reviews(id),

predicted_risk TEXT NOT NULL,

check_at TIMESTAMPTZ NOT NULL, -- T+90/180/365

outcome TEXT, -- pending|materialized|avoided

checked_at TIMESTAMPTZ

);

-- CORPUS_ENTRIES (v3, extended: org scoping for white-label segmentation, dual score overlay)

CREATE TABLE corpus_entries (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

org_id UUID REFERENCES org_configs(id), -- v3.6: NULL = shared/general corpus

review_id UUID NOT NULL REFERENCES reviews(id),

vertical TEXT,

idea_score NUMERIC(5,2), -- DEFERRED - VALIDATION GATE, see §2.2; not populated at MVP

proposal_score NUMERIC(5,2), -- DEFERRED - VALIDATION GATE, see §2.2; not populated at MVP

dual_score_overlay JSONB, -- v3.6 MVP: populated instead of the two columns above, see §2.2

embedding VECTOR(1536),

is_public BOOLEAN NOT NULL DEFAULT false,

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

CREATE INDEX idx_corpus_org ON corpus_entries(org_id);

CREATE INDEX idx_corpus_embedding ON corpus_entries USING hnsw (embedding vector_cosine_ops);

CREATE INDEX idx_corpus_dual_score_overlay ON corpus_entries USING gin (dual_score_overlay); -- v3.6 MVP: GIN index serves overlay queries until/unless promoted to typed columns, see §2.2

-- AUDIT_LOG (v3, extended: new event types for v3.6 surfaces)

CREATE TABLE audit_log (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

org_id UUID REFERENCES org_configs(id),

actor_id UUID REFERENCES users(id),

event_type TEXT NOT NULL, -- + v3.6: chat.message, review.url_submit, rule.create,

-- audit_agent.finding, peer_review.submit, org.brand_update

entity_type TEXT,

entity_id UUID,

metadata JSONB DEFAULT '{}',

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);

2.3 New v3.6 Tables

```sql -- CHAT_SESSIONS (Feature 1: Chat-to-Refine) CREATE TABLE chat_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id), org_id UUID REFERENCES org_configs(id), status TEXT NOT NULL DEFAULT 'active', -- active|archived|converted_to_review review_id UUID REFERENCES reviews(id), -- set once user submits for review title TEXT, turn_count INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_chat_sessions_user ON chat_sessions(user_id); -- CHAT_MESSAGES CREATE TABLE chat_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), role TEXT NOT NULL, -- user|coach content TEXT NOT NULL, sanitized BOOLEAN NOT NULL DEFAULT false, -- gate must run before export created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_chat_messages_session ON chat_messages(session_id, created_at); -- DIMENSION_EXPLANATIONS (Feature 4: Explain the Low Score) CREATE TABLE dimension_explanations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), dimension_id UUID NOT NULL REFERENCES dimensions(id) UNIQUE, explanation_text TEXT NOT NULL, cited_gaps JSONB NOT NULL DEFAULT '[]', -- ["no TAM calculation", "no competitor pricing"] sanitized BOOLEAN NOT NULL DEFAULT false, generated_by UUID REFERENCES judges(id), -- always the Primary Reviewer judge created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- REMEDIATION_PLANS (Feature 5: Here's How to Fix It - quality-scored per §5.5) CREATE TABLE remediation_plans ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), review_id UUID NOT NULL REFERENCES reviews(id), dimension_id UUID NOT NULL REFERENCES dimensions(id), action_items JSONB NOT NULL DEFAULT '[]', -- action_items shape: [{ "description": str, "difficulty": 1-3, -- "estimated_time_minutes": int, "template_url": str|null, -- "fix_quality_tier": int|null, -- 0=not_attempted,1=superficial,2=minimal,3=substantive; NULL until resubmission evaluated, see §5.5 -- "fix_quality_rationale": str|null, -- evaluator's one-sentence justification, sanitized per §7.2 -- "evaluated_at": timestamptz|null }] tier_scope TEXT NOT NULL DEFAULT 'full', -- 'summary' (Free) | 'full' (Pro+) disclaimer_version TEXT NOT NULL DEFAULT 'DISC-001', -- v3.6: legal disclaimer version active at generation time, see §2.6 sanitized BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_remediation_review ON remediation_plans(review_id); -- Fix quality aggregate lives on REVIEWS (added to the reviews table above, §2.2): -- reviews.fix_completion_score NUMERIC(3,2) -- mean fix_quality_tier (0-3) across all action_items on resubmission, see §5.5 -- reviews.parent_review_id UUID REFERENCES reviews(id) -- links a resubmission to the review whose remediation plan it's addressing -- AUDIT_FINDINGS (Feature 6: Second-Opinion Audit Agent) [DEFERRED - Post-Launch, see §1.2/§5.6] -- Table shape preserved here for continuity; not created by the MVP Phase 0 migration. CREATE TABLE audit_findings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), review_id UUID NOT NULL REFERENCES reviews(id), finding_type TEXT NOT NULL, -- blind_spot|groupthink|underweighted_dim|tagging_inconsistency|no_finding description TEXT NOT NULL, severity TEXT NOT NULL DEFAULT 'low', -- low|medium|high related_dimension_id UUID REFERENCES dimensions(id), disclaimer_version TEXT NOT NULL DEFAULT 'DISC-001', -- v3.6: see §2.6 sanitized BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_audit_findings_review ON audit_findings(review_id); -- RULE_TEMPLATES (Feature 7: Configurable Review Rules Engine) [DEFERRED - Post-Launch, see §1.2/§5.7] -- Table shape preserved here for continuity; not created by the MVP Phase 0 migration. CREATE TABLE rule_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, category TEXT NOT NULL, -- compliance|brand_voice|required_field|checklist schema_json JSONB NOT NULL, -- JSON Schema describing valid rule params is_system BOOLEAN NOT NULL DEFAULT true, -- system-provided vs org-authored created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ORG_RULES [DEFERRED - Post-Launch, see §1.2/§5.7] CREATE TABLE org_rules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES org_configs(id), template_id UUID REFERENCES rule_templates(id), name TEXT NOT NULL, rule_definition JSONB NOT NULL, -- rule_definition shape: { "condition": {...if/then AST...}, -- "required_fields": [...], "checklist": [...], "severity": "block|warn" } is_active BOOLEAN NOT NULL DEFAULT true, validated_at TIMESTAMPTZ, -- set once syntax + schema validation passes created_by UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_org_rules_org ON org_rules(org_id) WHERE is_active = true; -- ORG_CONFIGS (Feature 8: White-Label for Consultants) CREATE TABLE org_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, tier TEXT NOT NULL DEFAULT 'white_label', -- enterprise|white_label domain TEXT UNIQUE, -- custom domain, e.g. reviews.acceleratorx.com domain_verified BOOLEAN NOT NULL DEFAULT false, domain_verified_at TIMESTAMPTZ, logo_url TEXT, brand_colors JSONB DEFAULT '{}', -- { "primary": "#...", "accent": "#..." } email_template TEXT, -- HTML template with token placeholders corpus_segment TEXT NOT NULL DEFAULT 'shared', -- 'shared' | 'dedicated' api_key_prefix TEXT UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ORG_API_KEYS (supports Feature 8's per-org API key scoping) CREATE TABLE org_api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES org_configs(id), key_hash TEXT NOT NULL UNIQUE, label TEXT, scopes JSONB DEFAULT '["review:write","review:read"]', revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_used_at TIMESTAMPTZ ); -- PEER_REVIEWS (Feature 9: Roast/Boost Community Peer Review) CREATE TABLE peer_reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), review_id UUID NOT NULL REFERENCES reviews(id), user_id UUID NOT NULL REFERENCES users(id), type TEXT NOT NULL, -- roast|boost comment TEXT, visibility TEXT NOT NULL DEFAULT 'submitter_only', -- submitter_only|shared flagged BOOLEAN NOT NULL DEFAULT false, flagged_reason TEXT, moderation_status TEXT NOT NULL DEFAULT 'clean', -- clean|pending_review|removed created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_peer_reviews_review ON peer_reviews(review_id); CREATE UNIQUE INDEX idx_peer_reviews_dedupe ON peer_reviews(review_id, user_id); -- PEER_REVIEW_RATE_LIMITS (supports 3/day/user cap) CREATE TABLE peer_review_rate_limits ( user_id UUID PRIMARY KEY REFERENCES users(id), review_date DATE NOT NULL, count_today INT NOT NULL DEFAULT 0, UNIQUE (user_id, review_date) );

2.4 Full Table Inventory (v3 + v3.6)

2.5 Verdict JSON Schema (v3.6)

# Table Origin Purpose
1 users v3 (extended) account, tier, org membership
2 reviews v3 (extended) review lifecycle, dual scores, source type
3 verdicts v3 final verdict JSON, PDF, share link
4 dimensions v3 (extended) per-dimension score + idea/proposal tag
5 judges v3 (extended) panel roster incl. audit agent flag
6 judge_scores v3 raw per-judge scoring
7 research_citations v3 Phase 1 web verification sources
8 predictions v3 T+90/180/365 outcome tracking
9 corpus_entries v3 (extended) searchable review corpus, org-segmented
10 audit_log v3 (extended) cross-cutting event log
11 chat_sessions v3.6 Chat-to-Refine sessions
12 chat_messages v3.6 Chat-to-Refine turn history
13 dimension_explanations v3.6 Explain-the-Low-Score text
14 remediation_plans v3.6 Fix-It action plans
15 audit_findings v3.6 Second-Opinion Audit Agent output
16 rule_templates v3.6 reusable rule schemas
17 org_rules v3.6 org-specific custom rules
18 org_configs v3.6 white-label branding/domain
19 org_api_keys v3.6 per-org scoped API keys
20 peer_reviews v3.6 Roast/Boost entries
21 peer_review_rate_limits v3.6 abuse/rate control

```json

{

"review_id": "uuid",

"status": "done",

"idea_score": 78.5,

"proposal_score": 41.0,

"dual_score_overlay": { "idea_score": 78.5, "proposal_score": 41.0 },

"framing_summary": "Your idea is strong; your pitch needs work.",

"disclaimer": "This is an automated critique generated by AI models, not professional advice. Scores and commentary are opinions, not facts. Do not rely on this output for investment, procurement, legal, or funding decisions without independent professional review. See full disclaimer at https://verdicttank.com/legal/disclaimer.",

"dimensions": [

{

"name": "Market Analysis",

"score": 4,

"score_type": "idea",

"explanation": {

"text": "Market Analysis: 4/10 - no TAM calculation, no competitor pricing data, assumes zero competition.",

"cited_gaps": ["no TAM calculation", "no competitor pricing data", "zero-competition assumption"],

"disclaimer": "AI-generated critique, not professional advice."

},

"remediation": {

"tier_scope": "full",

"disclaimer": "This remediation plan is an automated suggestion, not professional or legal advice. Validate independently before acting.",

"quality_tier": "not_yet_evaluated",

"action_items": [

{

"description": "Calculate TAM using the top-down/bottom-up hybrid formula",

"difficulty": 2,

"estimated_time_minutes": 90,

"template_url": "https://cdn.verdicttank.com/templates/tam-calc.xlsx"

},

{

"description": "Add a competitor pricing comparison table",

"difficulty": 1,

"estimated_time_minutes": 45,

"template_url": "https://cdn.verdicttank.com/templates/competitor-pricing.xlsx"

}

]

}

}

],

"audit": {

"status": "deferred_post_mvp",

"findings": [],

"tagging_consistency_check": "not_run",

"note": "Second-Opinion Audit Agent (Phase 6) is deferred post-MVP per §1.2. Field shape is preserved for forward compatibility; populated once the Audit Agent worker ships. When populated, audit.disclaimer carries the same 'no professional advice' language as other surfaces."

},

"rules_applied": [],

"corpus_percentile": { "idea_score": 80, "proposal_score": 30 },

"public_share_id": "vt-8x2k",

"generated_at": "2026-08-11T00:00:00Z"

}

2.6 Legal Disclaimers - "No Professional Advice" (All Output Surfaces)

2.6.1 AI Disclaimer Injection Middleware (v3.7)

v3.6 established disclaimer coverage as a fixed, non-sanitizable, non-themeable serializer field (§2.6, DISC-001). v3.7 adds enforcement: a disclaimer version is bound to the session at render time, and the system refuses to render a review report if that binding is missing or stale, rather than relying on the serializer always having the current copy available.

Schema changes

ObjectChangeNotes
disclaimers (new table) id, content, version, active_from, active_until, created_at Versioned legal copy. Only one row may have active_until IS NULL and active_from <= now() at a time per disclaimer family - the "currently active" disclaimer.
review_sessions.disclaimer_version (new column) References disclaimers.id Bound at generation time. Nullable at the column level (for legacy rows predating this migration), but the middleware rule below treats NULL as a hard block for any render request.

Middleware rule

At render time, if review_session.disclaimer_version is NULL or references an inactive disclaimer, refuse render (HTTP 409 Conflict). The frontend must fetch the active disclaimer and display it before the review report renders.

HTTP/1.1 409 Conflict Content-Type: application/json { "error": "disclaimer_binding_missing_or_stale", "message": "This review session's disclaimer_version is unset or references an inactive disclaimer. The active disclaimer must be fetched and displayed before the report can render.", "review_session_id": "rs_8x2k4p", "disclaimer_version": null, "active_disclaimer_endpoint": "GET /legal/disclaimer/active" }

Render sequence enforced by the middleware:

  1. Frontend requests the review report.
  2. Middleware checks review_sessions.disclaimer_version against the currently active row in disclaimers (active_from <= now() AND (active_until IS NULL OR active_until > now())).
  3. If missing or stale: return 409 with the active-disclaimer endpoint reference. No report body is returned in this response.
  4. Frontend calls GET /legal/disclaimer/active, displays it, and (on the write path that generates or re-renders the session) binds the current active disclaimer's id to review_sessions.disclaimer_version.
  5. Only then does the report render. This closes the gap where a stored verdict could be displayed under a disclaimer version that has since been superseded without the surface reflecting that.
This is a stricter enforcement mode than v3.6's DISC-001 design, which bound disclaimer version at generation time but did not gate render on it staying current. v3.7 treats a stale or missing binding as a fail-closed condition, consistent with the fail-closed pattern already used for explanation.text (§5.4) and the top-level disclaimer field in the verdict schema (§2.6) - the report simply does not render rather than rendering with an implicit, unverified assumption that the disclaimer shown is current.

Disclaimer text (canonical, versioned)

"This content, including any score, critique, verdict, audit finding, or remediation plan, is an automated output generated by artificial intelligence models. It is not professional advice of any kind..."

This text is stored as the content of the active row in disclaimers, not hardcoded in any template or serializer - the version bump described in v3.6 §2.6 ("a config change, not a code change") is now literally a new row insert plus an active_until update on the prior row, with the middleware rule above guaranteeing no render surface can silently continue showing superseded copy after the cutover.

3. API Reference

3.1 Authentication

3.2 v3 Endpoints (Unchanged Contract, Extended Payload)

`rules_applied` ships as an always-empty array in MVP since the Rules Engine (`rules-api`) is deferred per §1.2 - the field is preserved in the schema for forward compatibility rather than removed, avoiding a breaking payload change when the Rules Engine ships post-launch. All freeform text fields (`explanation.text`, `action_items[].description`, `audit.findings[].description`) are sanitized before this object leaves the pipeline - see Section 7.2. All `disclaimer` fields are **not** sanitizable/removable content - they are fixed legal boilerplate injected by the response serializer after sanitization, never model-generated, and cannot be stripped by any per-review customization (org branding, White-Label templates, tier). See §2.6 for the full disclaimer policy. **Origin:** Judge 4&#x27;s Legal Killer #2 (AI Liability / Defamation / Tortious Interference - ranked EXTREME) and Priority Fix #9 of the v3.6 review: *&quot;Every verdict, explanation, audit finding, and remediation action item must carry conspicuous language... This is the first line of defense against Rank 2 liability.&quot;* No disclaimer, limitation-of-liability language, or indemnity existed anywhere in v3.6 prior to this revision - Section 15/Risk Assessment covered only technical/product risk, not the liability exposure of an AI system whose scores can be blamed for a lost deal, declined funding round, or reputational harm. **Standard disclaimer text** (canonical copy, referenced by ID `DISC-001` so all surfaces stay in sync on future legal-copy revisions): &gt; *&quot;This is an automated critique generated by AI models, not professional advice. Scores, explanations, audit findings, and remediation suggestions are AI-generated opinions, not verified facts or expert judgments. Do not rely on this output for investment, procurement, legal, regulatory, or funding decisions without independent professional review. VerdictTank and IT Pro Partner disclaim liability for decisions made in reliance on this output. Full terms: https://verdicttank.com/legal/disclaimer.&quot;* **Coverage - every output surface carries it, no exceptions:** | Output Surface | Placement | Enforcement Mechanism | |---|---|---| | Verdict JSON (`verdict_json`) | Top-level `disclaimer` field + per-dimension `explanation.disclaimer` + per-remediation `remediation.disclaimer` (see §2.5) | Injected by the API/PDF serializer post-sanitization; schema validation rejects a verdict payload missing the top-level field (fail-closed, same enforcement pattern as `explanation.text` in §5.4) | | PDF reports | Persistent footer on every page + prominent banner directly below the score header on page 1 | Hardcoded in the PDF template (not model-generated, not org-brand-overridable - see White-Label note below) | | Public share reports (web) | Sticky banner above the fold, not a footnote or dismissible tooltip | Rendered server-side in the share-page template, not client-injectable/removable | | API responses (`/verdict/{id}`, `/verdict/{id}/pdf`, `/reviews/{id}/remediation`, `/reviews/{id}/audit`) | `disclaimer` field present on every response object that carries scores, explanations, findings, or remediation | Contract-tested: API response schema tests fail CI if any scored/explained/remediated response type omits the field | | Chat-to-Refine transcripts | Session-start system message (&quot;I&#x27;m a coach, not an advisor - nothing here is professional advice&quot;) + persistent footer in chat UI | Rendered client-side from a fixed string, reinforces the &quot;un-copilot&quot; brand framing in §5.1 | | Audit findings (when the Audit Agent ships post-MVP, §1.2) | Same `disclaimer` field pattern as explanations/remediation, applied at design time so no retrofit is needed when Phase 6 ships | Schema shape reserved in §2.5 now; enforcement added to the audit-findings serializer at build time | | Corpus/percentile displays | Inline caveat: &quot;Percentile rankings are relative to other AI-scored submissions, not a market or investment benchmark&quot; | Rendered alongside `corpus_percentile` wherever it&#x27;s displayed | **Non-negotiable design constraints:** - **Not sanitizable, not removable.** The sanitization gate (§7.2) scans for information leakage; it must never scan or strip the disclaimer field itself. This is enforced by keeping disclaimer injection as a distinct post-sanitization serialization step, not part of the model-generated content that passes through the gate. - **Not White-Label-brand-overridable.** Per Judge 4&#x27;s Legal Killer #3/#5 concerns about White-Label trade-secret and IP exposure, `org_configs.email_template`/`brand_colors` theming (§5.8) is explicitly scoped to *cannot* touch disclaimer copy, placement, or prominence - this is enforced at the template-rendering layer (disclaimer blocks are rendered from a separate, non-themeable partial, injected after the brand template resolves). Applies once White-Label ships post-MVP; documented now so it&#x27;s not an afterthought when `org-api` is un-deferred. - **Versioned copy.** `DISC-001` is versioned; if legal counsel revises the disclaimer text (e.g. after the Minimum Viable Legal review the critical review calls for - MSA + Privacy Program + Content Safety Stack), the version bump is a config change, not a code change, and old stored verdicts retain the disclaimer version active at generation time (`verdict_json.disclaimer_version`) for auditability. - **This is a mitigation, not a legal opinion.** This architecture doc specifies the *engineering* implementation of conspicuous disclaimers per Priority Fix #9. It does not substitute for the Minimum Viable Legal framework (White-Label MSA, Privacy Program, Content &amp; Safety Stack) the review calls out as a precondition for accepting White-Label customers - that legal work is out of scope for this architecture doc and remains a hard gate on the White-Label track (§1.2) regardless of engineering completeness here. - **User-facing (web app):** JWT bearer token, short-lived access token + refresh token, issued by `auth-api` (unchanged from v3). - **Programmatic (Enterprise/API tier):** API key in `Authorization: Bearer vt_live_...` header. Keys are scoped per-org (`org_api_keys`) - v3.6 adds org scoping on top of v3&#x27;s flat per-user API keys. - **White-Label custom domains:** requests arriving on a verified custom domain (`org_configs.domain`) are resolved to `org_id` at the edge before hitting any endpoint; all endpoints below implicitly filter/write with that `org_id`. **`POST /api/verdicttank/review`** Upload-based review submission (file). Response now includes dual score fields once complete. ```json // Response (200, once status=done) { &quot;review_id&quot;: &quot;uuid&quot;, &quot;idea_score&quot;: 78.5, &quot;proposal_score&quot;: 41.0, &quot;status&quot;: &quot;done&quot; }

**`GET /api/verdicttank/status/{id}`** - unchanged polling contract; `status` enum extended with `audit` (Phase 6 in progress).

**`GET /api/verdicttank/verdict/{id}/pdf`** - unchanged; PDF generator now renders dual-score header and per-dimension explanation/remediation sections.

**`GET /api/verdicttank/corpus/search`** - unchanged query contract; results now include `idea_score` and `proposal_score` columns instead of a single blended score, and are `org_id`-scoped for white-label callers.

3.3 New v3.6 Endpoints

**`POST /api/verdicttank/review/url`** (Feature 2: URL-to-Review)

```json

// Request

{ "url": "https://example.com/pitch-deck", "vertical_hint": "fintech" }

// Response (202 Accepted)

{ "review_id": "uuid", "status": "queued", "source_type": "url" }

Validation: URL scheme allowlist (http/https), DNS resolution check, max content size 10MB, extraction timeout 30s (hard cap 45s), robots.txt respected, content sanitized before entering the pipeline (see Section 7.2). **`POST /api/verdicttank/chat/start`** (Feature 1) ```json // Request: { &quot;initial_message&quot;: &quot;I&#x27;m building a marketplace for...&quot; } // Response: { &quot;session_id&quot;: &quot;uuid&quot;, &quot;reply&quot;: &quot;Tell me more about who pays on this marketplace.&quot; }

**`POST /api/verdicttank/chat/{session_id}/message`**

```json

// Request: { "message": "Both sides pay a transaction fee." }

// Response: { "reply": "...", "turn_count": 4 }

Also available as **`WSS /ws/verdicttank/chat/{session_id}`** for real-time streaming token-by-token (see Section 5.1). REST polling variant is the fallback for clients that can&#x27;t hold a socket. **`GET /api/verdicttank/chat/{session_id}/history`** Returns sanitized transcript (`chat_messages` where `sanitized=true`). Export triggers gate scan if not already sanitized. **`POST /api/verdicttank/rules`** (Feature 7, Enterprise/White-Label only) ```json { &quot;name&quot;: &quot;SOC2 disclosure required&quot;, &quot;template_id&quot;: &quot;uuid&quot;, &quot;rule_definition&quot;: { &quot;condition&quot;: {&quot;if&quot;: &quot;vertical == &#x27;fintech&#x27;&quot;}, &quot;required_fields&quot;: [&quot;compliance.soc2_status&quot;], &quot;severity&quot;: &quot;block&quot; } }

Response `201` on pass, `422` with schema violation details on rule-syntax failure.

**`GET /api/verdicttank/rules`** / **`PATCH /api/verdicttank/rules/{id}`** / **`DELETE /api/verdicttank/rules/{id}`** - standard CRUD, org-scoped.

**`POST /api/verdicttank/org/branding`** (Feature 8, White-Label)

```json

{ "logo_url": "...", "brand_colors": {"primary": "#0B1B33"}, "email_template": "<html>...</html>" }

**`POST /api/verdicttank/org/domain`** - initiates CNAME/TXT verification (see Section 5.6). **`POST /api/verdicttank/reviews/{id}/peer-review`** (Feature 9) ```json { &quot;type&quot;: &quot;roast&quot;, &quot;comment&quot;: &quot;Your CAC assumption ignores paid acquisition entirely.&quot; }

Rate-limited to 3/user/day (enforced via `peer_review_rate_limits`, Redis-cached counter). Returns `429` past cap.

**`POST /api/verdicttank/reviews/{id}/peer-review/{peer_review_id}/report`** - abuse reporting, pushes into `moderation-queue`.

**`GET /api/verdicttank/reviews/{id}/remediation`** - standalone fetch of the Fix-It plan (also embedded in verdict JSON).

**`GET /api/verdicttank/reviews/{id}/audit`** - standalone fetch of audit findings (Enterprise tier).

4. Pipeline Flow

4.1 Updated 6-Stage Pipeline

INTAKE EGRESS │ │ ▼ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ File Upload │ │ URL Extract │ │ Chat-to- │ │ API submission │ │ │ (existing) │ │ (Crawl4AI) │ │ Refine draft │ │ (Enterprise) │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └────────┬───────────┘ │ └──────────────────┴──────────────────┴────────────────────┘ │ │ │ normalize to standard intake format │ │ │ ▼ │ ┌────────────────────────────────┐ │ │ [Rules Engine pre-check: │ DEFERRED - Post-Launch │ │ DEFERRED, see §1.2] │ (see §1.2, §5.7) │ └────────────────┬────────────────┘ │ │ │ ┌────────────────────────────────▼─────────────────────────────────────┐ │ │ PHASE 1: Research Agent │ │ │ Live web verification + citation gathering │ │ └────────────────────────────────┬─────────────────────────────────────┘ │ │ │ ┌────────────────────────────────▼─────────────────────────────────────┐ │ │ PHASE 2: Primary Reviewer │ │ │ 10-dim brutal critique, 1-10 per dim │ │ │ + score_type tag (idea|proposal) per dim, overlay-only [v3.6, §2.2] │ │ │ + per-dimension explanation generated inline [v3.6, Feature 4] │ │ │ [org rule dimensions: DEFERRED - see §1.2, §5.7] │ │ └────────────────────────────────┬─────────────────────────────────────┘ │ │ │ ┌────────────────────────────────▼─────────────────────────────────────┐ │ │ PHASE 3: Validation Reviewer - challenges primary score │ │ └────────────────────────────────┬─────────────────────────────────────┘ │ │ │ ┌───────────────────────┴───────────────────────┐ │ ▼ ▼ │ ┌──────────────────┐ ┌──────────────────┐ │ │ PHASE 4: │ │ PHASE 4: │ │ │ Cross-Check A │ (parallel) │ Cross-Check B │ │ └─────────┬──────────┘ └─────────┬──────────┘ │ └───────────────────────┬─────────────────────────┘ │ ▼ │ ┌──────────────────────────────────┐ │ │ PHASE 5: Majority Verdict │ │ │ aggregates scores, builds │ │ │ idea_score + proposal_score │ │ │ as dual_score_overlay [v3.6, §2.2] │ │ └──────────────────┬───────────────────┘ │ │ │ ┌──────────────────▼───────────────────┐ │ │ [PHASE 6: Audit Agent - DEFERRED, │ │ │ Post-Launch, see §1.2 / §5.6] │ │ └──────────────────┬───────────────────┘ │ │ │ ┌──────────────────▼───────────────────┐ │ │ POST-PIPELINE: Fix-It Generator │ │ │ (Pro+; lightweight summary on Free) │ │ │ quality-scored, 3-tier rubric [v3.6, §5.5]│ │ └──────────────────┬───────────────────┘ │ │ │ ┌──────────────────▼───────────────────┐ │ │ SANITIZATION GATE (extended) │ │ │ scans verdict JSON, explanations, │ │ │ remediation text │ │ │ (audit findings: N/A, Phase 6 deferred) │ │ └──────────────────┬───────────────────┘ │ │ │ ┌──────────────────▼───────────────────┐ │ │ DISCLAIMER INJECTION (post-sanitization) │──────────────────────┐ │ &quot;no professional advice&quot; on every │ │ │ scored/explained/remediated surface [§2.6]│ │ └──────────────────┬───────────────────┘ │ │ │ ┌──────────────────▼───────────────────┐ │ │ PDF Generation + Corpus Write │──────────────────────┘ │ (dual-score overlay, org-segmented) │ └───────────────────────────────────────┘

4.2 Data Flow Notes

5. Component Architecture

5.1 Chat-to-Refine - "Un-Copilot" Coaching (Ghostwriting Guard)

> **Priority Fix #4 (Judge 3):** The v3.6 review found the original prompt-level ghostwriting mitigation ("system prompt says don't ghostwrite, flag suspicious output for manual review") reactive and non-deterministic - a coaching model instructed not to ghostwrite can still ghostwrite a full paragraph, and "flag for manual spot-check" catches it *after* the user already received it. Judge 3's recommendation: build a deterministic, real-time **output-length guard** that rejects multi-paragraph output before it reaches the user, and lean into an explicit **"un-copilot"** brand position - VerdictTank coaches by asking better questions, it does not write your pitch for you, and it says so out loud.

**Real-time output-length guard (deterministic, not prompt-level):**

1. **Every coach turn response is validated before it is returned to the user - not sampled, not spot-checked, every single turn.** The guard runs synchronously in the `chat-api` request path immediately after the coaching-model call returns and before the response is persisted to `chat_messages` or sent to the client.

2. **Rejection criteria (any one trips the guard):**

3. **On rejection, the system does not simply retry the same prompt.** It re-prompts the coaching model with an explicit corrective instruction appended to the turn: *"Your previous response was too long / not a question. Respond with exactly one Socratic question (under 280 characters) that helps the user find the gap themselves. Do not draft content for them."* This re-prompt is capped at 2 retries; if the model still fails to produce a compliant response on the 3rd attempt, the system falls back to a pre-written generic Socratic prompt from a static bank (e.g. *"What's the one number in here you're least sure of?"*) rather than ever showing the user a rejected response.

4. **Every guard trip is logged** (`chat_messages.guard_rejected BOOLEAN`, `chat_messages.guard_rejection_reason TEXT`, `chat_messages.retry_count INT`) for the quarterly manual audit in item 6 below, and to catch coaching-model/prompt regressions early (a spike in guard-trip rate on a model update is itself an alert-worthy signal, not just an audit finding).

5. **The guard is enforced server-side only** - never trust a client-side check for this; the rejection logic lives in `chat-api`, not the web frontend, so it can't be bypassed by a different client hitting the same endpoint.

**"Un-copilot" brand positioning (Judge 3's recommendation, product + UX + copy, not just engineering):**

5.2 URL Extractor

5.3 Dual Scorer

1. Phase 2 Primary Reviewer prompt requires each dimension output to include `score_type: "idea"|"proposal"`.

2. Phases 3-4 (Validation, Cross-Check A/B) inherit and can challenge the tag, not just the score.

3. Phase 5 aggregation buckets by tag, computes two independent weighted composites on a 0-100 scale (existing dimensions are 1-10; composite = weighted mean × 10, adjusted by corpus-relative calibration carried over from v3's percentile logic).

4. Phase 6 Audit Agent explicitly checks tagging *consistency* across judges - if Judge A tags "Market Analysis" as `idea` and Judge B tags it `proposal`, that's a flagged `tagging_inconsistency` finding, not a silent average.

5.4 Explainer

5.5 Fix-It Generator - Single-Tier Evaluator (v3.7 Correction)

v3.7 scope correction: the v3.6 design specified a 3-tier quality rubric (Superficial / Minimal / Substantive, scored 0-3) to evaluate remediation depth on resubmission. This tier system has no calibration data behind it - the depth thresholds were authored before a single Fix-It session existed. Shipping an uncalibrated 3-tier rubric as if it were a validated measurement risks presenting arbitrary distinctions to users as authoritative grading. v3.7 replaces it with a single-tier binary evaluator for the MVP.

5.5.1 Fix-It Report Structure (v3.7)

The Fix-It report is a distinct view from the full proposal re-score. Three components, in fixed order of prominence:

1. Primary view - Before/After Delta Comparison Table

One row per dimension. Shows the original score, the per-fix delta (positive, negative, or zero, per the single-tier pass/fail result on the action items tied to that dimension), and the estimated new score. This table is the report - not a sidebar to it.

DimensionOriginal ScoreFix DeltaEstimated New Score
Market Sizing (TAM/SAM/SOM)4/10+2 (pass)6/10 (estimated)
Competitive Moat5/100 (fail - gap not closed)5/10 (unchanged)
Unit Economics3/10+3 (pass)6/10 (estimated)
"Estimated new score" is explicitly labeled as an estimate, not a re-score. It is derived from the pass/fail evaluator's delta heuristic, not from re-running the panel. This distinction is load-bearing for the disclaimer surface in Block E and must not be presented as equivalent to a real Phase 5 re-aggregation.

2. Secondary - Estimated Impact Summary Metric

A single summary metric at the top of the report: Estimated Impact - the weighted mean delta across all fixed dimensions. This metric is renamed from "completion score" (v3.6 terminology) because "completion" implied a finished-state judgment the evaluator does not make; "Estimated Impact" correctly frames the number as a directional signal derived from the delta table below it, not a certified outcome.

v3.6 labelv3.7 labelWhy renamed
fix_completion_score / "Completion Score"estimated_impact_score / "Estimated Impact""Completion" implies a verified end-state; the single-tier evaluator only confirms individual gaps were addressed, not that the proposal is "complete." "Estimated" is carried into the label itself to match the estimate-not-re-score framing in the delta table.

3. Separate action - "Re-score Full Proposal"

Re-running all 10 dimensions from scratch is not bundled with the fix report. It is a distinct, explicitly-labeled button ("Re-score Full Proposal") that triggers a full Phase 1-5 pipeline re-run against the resubmitted content, consuming a review slot per the tier's quota rules. The fix report's estimated deltas are a free, immediate, lightweight signal; a full re-score is a separate, billable, panel-verified action. Conflating the two in the UI - or in the underlying request - risks presenting an estimate as a verdict.

Fix-It Report (GET /reviews/{id}/fixit-report) ┌─────────────────────────────────────────────┐ │ Estimated Impact: +1.7 (weighted mean delta) │ ← summary metric (renamed) ├─────────────────────────────────────────────┤ │ Dimension │ Orig │ Δ │ Est. New │ ← primary delta table │ Market Sizing │ 4/10 │ +2 │ 6/10 │ │ Competitive Moat │ 5/10 │ 0 │ 5/10 │ │ Unit Economics │ 3/10 │ +3 │ 6/10 │ └─────────────────────────────────────────────┘ [ Re-score Full Proposal ] ← separate button, separate endpoint, separate quota consumption, NOT auto-triggered

The Fix-It MVP ships with a single-tier binary evaluator: does this change materially improve the section? Pass/Fail. No depth ranking. The 3-tier quality rubric (Superficial/Minimal/Substantive) is deferred to Phase 2, gated on:

  • (a) Volume gate: 100+ Fix-It sessions completed with the single-tier evaluator.
  • (b) Data gate: corpus contains sufficient before/after pairs to calibrate depth thresholds.
  • (c) Demand gate: user survey post-100 sessions confirms demand for depth-grading.
Attributev3.6 (superseded)v3.7 (current)
Evaluation model3-tier rubric, 0-3 scaleBinary pass/fail
Storage columnaction_items.fix_quality_tier INT (0-3)action_items.fix_quality_pass BOOLEAN
Rationale fieldfix_quality_rationale TEXTfix_quality_rationale TEXT (retained, one-sentence justification, sanitized per §7.2)
Aggregate metricfix_completion_score (0-3 mean)fix_completion_score redefined as pass-rate across attempted items (0-100%) - see Block B
StatusSupersededMVP / 3-tier rubric Deferred - Phase 2

The evaluator retains the anti-gaming design from v3.6: action items describe what's missing, not how the panel weighs it, and the pass/fail judgment is scoped narrowly to whether the specific gap named in cited_gaps is closed - not whether the section merely changed. A token edit that doesn't address the named gap fails; presence-checking alone does not clear the bar. What changes in v3.7 is the output resolution (two states, not four) and the removal of a depth scale that had never been validated against real user behavior.

Deferral is not a downgrade in rigor - the pass/fail bar still requires the evaluator to confirm the specific technique or data the original action item called for is present and internally consistent with the rest of the submission. It is a reduction in the number of distinctions the system claims to be able to make until there is evidence it can make them reliably.

5.6 Audit Agent [DEFERRED - Post-Launch, see §1.2]

> This component is designed but **not built for the 12-week MVP**. Per the review's Week 1 Cut List, the Audit Agent addresses "an enterprise trust problem, not a launch problem" - it is retained here in full so the design isn't lost, and is revisited once Enterprise-tier pilot demand justifies the build (see §1.2 Deferred Trigger table).

5.7 Rules Engine [DEFERRED - Post-Launch, see §1.2]

> This component is designed but **not built for the 12-week MVP**. Per the review's Week 1 Cut List, the Rules Engine is a White-Label/Enterprise dependency with no MVP demand signal. Retained here as the intended post-launch build.

1. **Hard block (pre-check):** rules marked `severity: "block"` run before Phase 1 - e.g. a required compliance field missing halts the review immediately, avoiding wasted pipeline spend.

2. **Soft/scored (Phase 2 injection):** rules marked `severity: "warn"` are appended to the Primary Reviewer prompt as additional evaluation dimensions (`dimensions.name` prefixed, `source='org_rule'`), scored 1-10 alongside the standard rubric.

5.8 White-Label Engine [DEFERRED - Post-Launch, see §1.2]

> This component is designed but **not built for the 12-week MVP**, and additionally gated on Minimum Viable Legal (MSA + Privacy Program) per the review's Legal Killers before any Enterprise/White-Label customer is onboarded - see §2.6 and §1.2.

5.8.1 White-Label Provisioning Gate (v3.7)

v3.6 described the White-Label track as "blocked on Minimum Viable Legal" in prose (§1.2, §5.8). v3.7 converts that prose gate into an enforced boolean precondition evaluated by the provisioning controller itself, not left to process discipline or documentation review.

white_label_provisioning_allowed = rls_deployed == true AND org_isolation_verified == true AND dpa_signed == true AND sub_processor_registry_published == true AND external_counsel_approved == true

All five conditions must be green before any White-Label tenant is provisioned. This gate lives in the provisioning controller, not in documentation.

ConditionVerified byFailure mode if false
rls_deployedDatabase migration status check against org_id RLS policies (§7.4)Cross-org data leakage risk if bypassed
org_isolation_verifiedIndependent verification pass (per the "no more than 3 accounts until segmentation is independently verified" launch condition, §5.8)Corpus segmentation cannot be trusted
dpa_signedLegal/ops record - executed Data Processing Addendum on file for the tenantNo lawful basis for processing customer end-user data as a processor
sub_processor_registry_publishedPublic sub-processor list (LLM providers, infra vendors) live at a stable URLTransparency obligation to tenant's own customers unmet
external_counsel_approvedSign-off recorded from external counsel on the White-Label MSA/DPA/SCCs bundleLegal exposure not independently reviewed

Attempting to create a White-Label tenant without all gates green returns HTTP 423 Locked with a JSON body listing which gates failed:

HTTP/1.1 423 Locked Content-Type: application/json { "error": "white_label_provisioning_blocked", "message": "Cannot provision White-Label tenant: one or more required gates are not satisfied.", "gates": { "rls_deployed": true, "org_isolation_verified": true, "dpa_signed": false, "sub_processor_registry_published": false, "external_counsel_approved": false }, "failed_gates": [ "dpa_signed", "sub_processor_registry_published", "external_counsel_approved" ] }
The gate is enforced at the provisioning controller layer - the same layer that creates org_configs rows and issues the first org_api_keys record. It is checked on every provisioning attempt, not once at White-Label track kickoff, so a regression (e.g. a sub-processor falling off the published registry) blocks new tenant creation even after the track has shipped. Existing tenants are not retroactively de-provisioned by a gate flipping false; the gate governs new provisioning only, per the controller/processor boundary in Block D.

5.9 Community Layer (Roast/Boost) [DEFERRED - Post-Launch, see §1.2]

> This component is designed but **not built for the 12-week MVP**. Per Judge 4's Legal Killer #4 (defamation/moderation exposure), the moderation overhead and liability surface outweigh launch-stage value - retained here as the intended post-launch build once moderation tooling and Minimum Viable Legal are in place.

6. Infrastructure

6.1 Server Layout

┌────────────────────────────────────────────────────────────────────┐ │ Edge tier (2x app3-class instances, HA pair) │ │ Caddy - TLS termination, org-domain routing, rate limiting │ └────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────┐ │ App tier (autoscaling pool, 3-6 instances) │ │ review-api · chat-api · rules-api · org-api · peer-review-api │ │ chat-ws (sticky sessions via Redis pub/sub for horizontal scale) │ └────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────┐ │ Worker tier (autoscaling pool, scales with queue depth) │ │ pipeline-workers (Phases 1-6) · url-extractor · fixit-generator │ │ sanitize-scanner · pdf-generator · domain-verifier · cron │ └────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────┐ │ Data tier │ │ PostgreSQL 16 (primary + read replica) w/ pgvector extension │ │ Redis 7 (queue + cache + rate limits + feature flags) │ │ S3-compatible object store (Wasabi/S3) for PDFs, transcripts, │ │ URL snapshots, uploaded source docs │ └────────────────────────────────────────────────────────────────────┘

6.2 Docker Services

```yaml

services:

edge-caddy: {image: caddy:2, ports: ["443:443"]}

review-api: {build: ./services/review-api}

chat-api: {build: ./services/chat-api}

chat-ws: {build: ./services/chat-ws}

rules-api: {build: ./services/rules-api}

org-api: {build: ./services/org-api}

peer-review-api: {build: ./services/peer-review-api}

pipeline-worker: {build: ./workers/pipeline, deploy: {replicas: 4}}

url-extractor: {build: ./workers/url-extractor} # wraps Crawl4AI

fixit-generator: {build: ./workers/fixit-generator}

audit-agent-worker: {build: ./workers/audit-agent}

sanitize-scanner: {build: ./workers/sanitize-scanner}

pdf-generator: {build: ./workers/pdf-generator}

domain-verifier: {build: ./workers/domain-verifier}

moderation-worker: {build: ./workers/moderation}

cron: {build: ./workers/cron} # T+90/180/365 predictions

postgres: {image: pgvector/pgvector:pg16, volumes: ["pgdata:/var/lib/postgresql/data"]}

redis: {image: redis:7-alpine}

6.3 Database Choices

6.4 Caching

6.5 Deployment Options - ITPP-INFRA Shared vs Dedicated

7. Security Model

7.1 Authentication & Authorization

7.2 Sanitization Gate (Extended for v3.6)

7.3 Prompt Injection Defense (New Surface: Rules Engine + URL Extraction)

7.4 Multi-Tenant Isolation (White-Label)

7.4.1 Controller / Processor Role Map (v3.7)

v3.6 treated multi-tenant isolation (§7.4) as a purely technical RLS problem. v3.7 adds the legal role classification underneath it: VerdictTank's relationship to submitted content and end-user data is not uniform across tiers, and the agreements/legal basis required differ accordingly.

RoleTierLegal BasisAgreements Required
VerdictTank as Controller Free, Pro, Enterprise (direct users) Performance of contract + Legitimate interest ToS, Privacy Policy
VerdictTank as Joint Controller Enterprise (org admin + users) Performance of contract ToS, Enterprise DPA, Data Processing Addendum
VerdictTank as Processor White-Label (post-MVP) Customer's instructions DPA, SCCs/IDTA, Sub-processor list
LLM API calls (all tiers) Controller Legitimate interest Sub-processor training guard (no training on API data)
The role shift from Controller (direct tiers) to Processor (White-Label) is the reason the provisioning gate in Block C exists as a hard technical block rather than a soft policy note: provisioning a White-Label tenant without the DPA/SCCs/sub-processor registry in place would mean VerdictTank is processing a customer's end-user data under someone else's instructions with no executed agreement establishing that relationship - a materially different (and unmitigated) exposure than the Controller role it carries for Free/Pro/Enterprise direct users.

Practical implications for engineering:

  • Sub-processor training guard: every LLM API vendor in the pipeline (research agent, primary reviewer, validation/cross-check reviewers, verdict aggregation) must be contractually confirmed as not training on submitted API data, regardless of tier - this is a Controller-role obligation that applies uniformly, not something that only matters once White-Label ships.
  • Joint Controller (Enterprise) implications: org admins configuring rules, branding, or reviewing org-wide analytics act as a joint controller alongside VerdictTank for their org's users - this is why Enterprise carries a dedicated DPA even though it is not a Processor relationship.
  • Processor (White-Label) implications: once the provisioning gate in Block C is satisfied, VerdictTank's processing of that tenant's end-user submissions is governed entirely by the tenant's own instructions under the signed DPA/SCCs - VerdictTank cannot unilaterally repurpose that tenant's submissions into the shared corpus (see org_configs.corpus_segment = 'dedicated', §5.8).

7.5 Abuse & Rate Limiting

8. Cost Model

8.1 Per-Review Cost Breakdown (v3.6, Pro/Enterprise Tier)

8.2 Margin Check by Tier

8.3 Cost Attribution by Component (Engineering View)

9. Implementation Sequence

Phase 0 - Foundation (Weeks 1-3)

Phase 1 - Coach & Intake (Weeks 4-6)

Phase 2 - Transparency & Depth (Weeks 7-12)

Phase 3 - Fix-It & Trust (Weeks 13-19)

Phase 4 - Distribution & GA (Weeks 20-25)

10. Failure Modes