AI-powered government contract proposal platform - find, analyze, and win federal contracts
# 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.
> **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.
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.
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.
> **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);
| # | 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"
}
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.
| Object | Change | Notes |
|---|---|---|
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. |
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.
Render sequence enforced by the middleware:
review_sessions.disclaimer_version against the currently active row in disclaimers (active_from <= now() AND (active_until IS NULL OR active_until > now())).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.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.
"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.
**`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.
**`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" }
**`POST /api/verdicttank/chat/{session_id}/message`**
```json
// Request: { "message": "Both sides pay a transaction fee." }
// Response: { "reply": "...", "turn_count": 4 }
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>" }
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).
> **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):**
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.
The Fix-It report is a distinct view from the full proposal re-score. Three components, in fixed order of prominence:
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.
| Dimension | Original Score | Fix Delta | Estimated New Score |
|---|---|---|---|
| Market Sizing (TAM/SAM/SOM) | 4/10 | +2 (pass) | 6/10 (estimated) |
| Competitive Moat | 5/10 | 0 (fail - gap not closed) | 5/10 (unchanged) |
| Unit Economics | 3/10 | +3 (pass) | 6/10 (estimated) |
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 label | v3.7 label | Why 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. |
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.
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:
| Attribute | v3.6 (superseded) | v3.7 (current) |
|---|---|---|
| Evaluation model | 3-tier rubric, 0-3 scale | Binary pass/fail |
| Storage column | action_items.fix_quality_tier INT (0-3) | action_items.fix_quality_pass BOOLEAN |
| Rationale field | fix_quality_rationale TEXT | fix_quality_rationale TEXT (retained, one-sentence justification, sanitized per §7.2) |
| Aggregate metric | fix_completion_score (0-3 mean) | fix_completion_score redefined as pass-rate across attempted items (0-100%) - see Block B |
| Status | Superseded | MVP / 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.
> 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).
> 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.
> 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.
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.
All five conditions must be green before any White-Label tenant is provisioned. This gate lives in the provisioning controller, not in documentation.
| Condition | Verified by | Failure mode if false |
|---|---|---|
rls_deployed | Database migration status check against org_id RLS policies (§7.4) | Cross-org data leakage risk if bypassed |
org_isolation_verified | Independent verification pass (per the "no more than 3 accounts until segmentation is independently verified" launch condition, §5.8) | Corpus segmentation cannot be trusted |
dpa_signed | Legal/ops record - executed Data Processing Addendum on file for the tenant | No lawful basis for processing customer end-user data as a processor |
sub_processor_registry_published | Public sub-processor list (LLM providers, infra vendors) live at a stable URL | Transparency obligation to tenant's own customers unmet |
external_counsel_approved | Sign-off recorded from external counsel on the White-Label MSA/DPA/SCCs bundle | Legal 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:
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.
> 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.
```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}
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.
| Role | Tier | Legal Basis | Agreements 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) |
Practical implications for engineering:
org_configs.corpus_segment = 'dedicated', §5.8).