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 8-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"
}
**`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.
> **Priority Fix #7 (Fix-It Quality Loophole):** The v3.6 review found the original remediation-tracking design ("re-review score deltas... detect superficial template-filling vs. genuine improvement") was aspirational but not actually specified - there was no evaluation step that distinguished a user who genuinely added a TAM calculation from a user who pasted a single sentence into the relevant section just to make the presence-check pass. Presence-checking ("did the field get filled in") is trivially gameable and undermines the entire retention-loop premise of Fix-It as a credible improvement signal, not a checkbox exercise.
**3-Tier Fix Quality Rubric (evaluation, not presence-checking):**
On resubmission (re-review of a previously scored document, or a follow-up chat/URL submission linked via `reviews.parent_review_id`), each `action_item` that was addressed is evaluated against the *specific gap* it named - not merely against whether the target section now contains text. Evaluation runs as an additional judge-model call scoped narrowly to comparing the before/after content for a given `action_item`, using the original `cited_gaps` and `description` as the rubric anchor.
| Tier | Score | Definition | Scoring Rule |
|---|---|---|---|
| **Superficial** | 1 | The relevant section changed, but the specific gap named in `cited_gaps` is still unaddressed - e.g. a TAM number was added but with no visible methodology, source, or calculation shown; or text was added that mentions the topic without supplying the missing data/evidence. | Triggers when the evaluator can find no calculation, citation, data point, or structural change that actually closes the gap - text presence alone does not clear this tier. |
| **Minimal** | 2 | The gap is nominally addressed but shallow - e.g. a TAM figure is present with a one-line methodology note, but no bottom-up/top-down hybrid breakdown, no source citation, no sensitivity range. Passes a "did they try" bar but not a "would this survive investor scrutiny" bar. | Triggers when the evaluator finds a genuine attempt that directly responds to the cited gap, but missing at least one of: methodology transparency, supporting data/citation, or the specific technique the original `action_item.description` recommended. |
| **Substantive** | 3 | The gap is closed with the rigor the original action item called for - e.g. TAM calculated via the recommended hybrid formula, source-cited, with a stated methodology and range. The addition would plausibly change an informed reader's assessment of that dimension. | Triggers when the evaluator confirms the specific technique/data named in the action item is present *and* internally consistent with the rest of the submission (a number that contradicts other stated figures elsewhere in the document does not qualify, even if superficially "complete"). |
| **Not Attempted** | 0 | No detectable change to the relevant section between submissions. | Default when the diffed section is identical or near-identical to the original. |
> This component is designed but **not built for the 8-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 8-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 8-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.
> This component is designed but **not built for the 8-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}