Last modified by Robert Schaub on 2025/12/24 21:27

From version 1.1
edited by Robert Schaub
on 2025/12/24 20:24
Change comment: Imported from XAR
To version 2.1
edited by Robert Schaub
on 2025/12/24 21:08
Change comment: There is no comment for this version

Summary

Details

Page properties
Content
... ... @@ -1,4 +1,6 @@
1 1  = POC1 API & Schemas Specification =
2 +Version: 0.4.1 (POC1)
3 +Scope: POC1 (Test) — URL/Text → Stage 1 Claim Extraction → Stage 2 Claim Analysis (cached) → Stage 3 Article Assessment → result.json + report.md
2 2  
3 3  ----
4 4  
... ... @@ -5,1414 +5,751 @@
5 5  == Version History ==
6 6  
7 7  |=Version|=Date|=Changes
8 -|0.4.1|2025-12-24|Applied 9 critical fixes: file format notice, verdict taxonomy, canonicalization algorithm, Stage 1 cost policy, BullMQ fix, language in cache key, historical claims TTL, idempotency, copyright policy
9 -|0.4|2025-12-24|**BREAKING:** 3-stage pipeline with claim-level caching, user tier system, cache-only mode for free users, Redis cache architecture
10 -|0.3.1|2025-12-24|Fixed single-prompt strategy, SSE clarification, schema canonicalization, cost constraints
11 -|0.3|2025-12-24|Added complete API endpoints, LLM config, risk tiers, scraping details
10 +|0.4.1|2025-12-24|Codegen-ready canonical contract: locked enums + mapping, deterministic normalization (v1norm1), idempotency, cache_preference semantics, minimal OpenAPI 3.1, URL extraction + SSRF rules, evidence excerpt policy
11 +|0.4|2025-12-24|3-stage pipeline with claim-level caching; cache-only mode for free users; Redis cache architecture
12 +|0.3.1|2025-12-24|Reduced schema naming drift; clarified SSE (progress only); added cost knobs and constraints
13 +|0.3|2025-12-24|Initial endpoint set + draft schemas
12 12  
13 13  ----
14 14  
15 -== 1. Core Objective (POC1) ==
17 +== POC1 Codegen Contract (Canonical) ==
16 16  
17 -The primary technical goal of POC1 is to validate **Approach 1 (Single-Pass Holistic Analysis)** while implementing **claim-level caching** to achieve cost sustainability.
19 +{{info}}
20 +This section is the **authoritative, code-generation-ready contract** for POC1.
21 +If any other page conflicts with this section, **this section wins**.
18 18  
19 -The system must prove that AI can identify an article's **Main Thesis** and determine if supporting claims logically support that thesis without committing fallacies.
23 +**Format note:** This page is authored in **xWiki 2.1** syntax. If exchanged as a transport `.md` file, import it as **xWiki content**, not Markdown.
24 +{{/info}}
20 20  
21 -=== Success Criteria: ===
26 +=== 1) Goals ===
27 +POC1 provides:
28 +* A REST API to submit **either a URL or raw text**
29 +* Asynchronous execution (job-based) with progress/status and optional SSE events
30 +* Deterministic outputs:
31 +** ``result.json`` (machine-readable; schema-validated)
32 +** ``report.md`` (human-readable; rendered from JSON via template)
33 +* Mandatory counter-evidence attempt per scenario (or explicit “not found despite search” uncertainty note)
34 +* Claim-level caching to validate cost sustainability
22 22  
23 -* Test with 30 diverse articles
24 -* Target: ≥70% accuracy detecting misleading articles
25 -* Cost: <$0.25 per NEW analysis (uncached)
26 -* Cost: $0.00 for cached claim reuse
27 -* Cache hit rate: ≥50% after 1,000 articles
28 -* Processing time: <2 minutes (standard depth)
36 +=== 2) Non-goals (POC1) ===
37 +* Full editorial workflow / human review UI
38 +* Full provenance ledger beyond minimal job metadata
39 +* Complex billing/rate-limit systems (simple caps are fine)
40 +* Multi-language i18n beyond best-effort language detection
29 29  
30 -=== Economic Model: ===
42 +=== 3) Pipeline model (locked) ===
43 +* **Stage 1 — Claim Extraction**
44 +** Input: URL-text or pasted text
45 +** Output: claim candidates including canonical claim + ``claim_hash``
46 +* **Stage 2 — Claim Analysis (cached by claim_hash)**
47 +** Input: canonical claim(s)
48 +** Output: scenarios, evidence/counter-evidence, scenario verdicts, plus a claim-level verdict summary
49 +* **Stage 3 — Article Assessment**
50 +** Input: article text + Stage 2 analyses (from cache and/or freshly computed)
51 +** Output: article-level assessment (main thesis, reasoning quality, key risks, context)
31 31  
32 -* **Free tier:** $10 credit per month (~~40-140 articles depending on cache hits)
33 -* **After limit:** Cache-only mode (instant, free access to cached claims)
34 -* **Paid tier:** Unlimited new analyses
53 +=== 4) Locked verdict taxonomies (two enums + mapping) ===
35 35  
36 -----
55 +**Scenario verdict enum** (``ScenarioVerdict.verdict_label``):
56 +* ``Highly likely`` | ``Likely`` | ``Unclear`` | ``Unlikely`` | ``Highly unlikely`` | ``Unsubstantiated``
37 37  
38 -== 2. Architecture Overview ==
58 +**Claim verdict enum** (``ClaimVerdict.verdict_label``):
59 +* ``Supported`` | ``Refuted`` | ``Inconclusive``
39 39  
40 -=== 2.1 3-Stage Pipeline with Caching ===
61 +**Mapping rule (for summaries):**
62 +* If the claim’s primary-interpretation scenario verdict is:
63 +** ``Highly likely`` / ``Likely`` ⇒ ``Supported``
64 +** ``Highly unlikely`` / ``Unlikely`` ⇒ ``Refuted``
65 +** ``Unclear`` / ``Unsubstantiated`` ⇒ ``Inconclusive``
66 +* If scenarios materially disagree (different assumptions lead to different outcomes) ⇒ ``Inconclusive``, and explain why.
41 41  
42 -FactHarbor POC1 uses a **3-stage architecture** designed for claim-level caching and cost efficiency:
68 +=== 5) Deterministic canonical claim normalization (locked) ===
69 +Normalization version: ``v1norm1``
70 +Cache key namespace: ``claim:v1norm1:{language}:{sha256(canonical_claim_text)}``
43 43  
44 -{{mermaid}}
45 -graph TD
46 - A[Article Input] --> B[Stage 1: Extract Claims]
47 - B --> C{For Each Claim}
48 - C --> D[Check Cache]
49 - D -->|Cache HIT| E[Return Cached Verdict]
50 - D -->|Cache MISS| F[Stage 2: Analyze Claim]
51 - F --> G[Store in Cache]
52 - G --> E
53 - E --> H[Stage 3: Holistic Assessment]
54 - H --> I[Final Report]
55 -{{/mermaid}}
72 +Rules (MUST be implemented exactly; any change requires bumping normalization version):
73 +1. Unicode normalize: NFD
74 +2. Lowercase
75 +3. Strip diacritics
76 +4. Normalize apostrophes: ``’`` and ``‘`` → ``'``
77 +5. Normalize percent: ``%`` → `` percent``
78 +6. Normalize whitespace (collapse)
79 +7. Remove punctuation except apostrophes
80 +8. Expand contractions (fixed list for v1norm1)
81 +9. Normalize whitespace again
56 56  
57 -==== Stage 1: Claim Extraction (Haiku, no cache) ====
83 +Contractions list (v1norm1):
84 +* don't→do not, doesn't→does not, didn't→did not, can't→cannot, won't→will not,
85 +* shouldn't→should not, wouldn't→would not, isn't→is not, aren't→are not,
86 +* wasn't→was not, weren't→were not
58 58  
59 -* **Input:** Article text
60 -* **Output:** 5 canonical claims (normalized, deduplicated)
61 -* **Model:** Claude Haiku 4.5.5 (default, configurable via LLM abstraction layer)
62 -* **Cost:** $0.003 per article
63 -* **Cache strategy:** No caching (article-specific)
64 -
65 -==== Stage 2: Claim Analysis (Sonnet, CACHED) ====
66 -
67 -* **Input:** Single canonical claim
68 -* **Output:** Scenarios + Evidence + Verdicts
69 -* **Model:** Claude Sonnet 4.5 (default, configurable via LLM abstraction layer)
70 -* **Cost:** $0.081 per NEW claim
71 -* **Cache strategy:** Redis, 90-day TTL
72 -* **Cache key:** claim:v1norm1:{language}:{sha256(canonical_claim)}
73 -
74 -==== Stage 3: Holistic Assessment (Sonnet, no cache) ====
75 -
76 -* **Input:** Article + Claim verdicts (from cache or Stage 2)
77 -* **Output:** Article verdict + Fallacies + Logic quality
78 -* **Model:** Claude Sonnet 4.5 (default, configurable via LLM abstraction layer)
79 -* **Cost:** $0.030 per article
80 -* **Cache strategy:** No caching (article-specific)
81 -
82 -
83 -
84 -**Note:** Stage 3 implements **Approach 1 (Single-Pass Holistic Analysis)** from the [[Article Verdict Problem>>Test.FactHarbor.Specification.POC.Article-Verdict-Problem]]. While claim analysis (Stage 2) is cached for efficiency, the holistic assessment maintains the integrated evaluation philosophy of Approach 1.
85 -
86 -=== Total Cost Formula: ===
87 -
88 -{{{Cost = $0.003 (extraction) + (N_new_claims × $0.081) + $0.030 (holistic)
89 -
90 -Examples:
91 -- 0 new claims (100% cache hit): $0.033
92 -- 1 new claim (80% cache hit): $0.114
93 -- 3 new claims (40% cache hit): $0.276
94 -- 5 new claims (0% cache hit): $0.438
95 -}}}
96 -
97 -----
98 -
99 -=== 2.2 User Tier System ===
100 -
101 -|=Tier|=Monthly Credit|=After Limit|=Cache Access|=Analytics
102 -|**Free**|$10|Cache-only mode|✅ Full|Basic
103 -|**Pro** (future)|$50|Continues|✅ Full|Advanced
104 -|**Enterprise** (future)|Custom|Continues|✅ Full + Priority|Full
105 -
106 -**Free Tier Economics:**
107 -
108 -* $10 credit = 40-140 articles analyzed (depending on cache hit rate)
109 -* Average 70 articles/month at 70% cache hit rate
110 -* After limit: Cache-only mode
111 -
112 -----
113 -
114 -=== 2.3 Cache-Only Mode (Free Tier Feature) ===
115 -
116 -When free users reach their $10 monthly limit, they enter **Cache-Only Mode**:
117 -
118 -
119 -
120 -==== Stage 3: Holistic Assessment - Complete Specification ====
121 -
122 -===== 3.3.1 Overview =====
123 -
124 -**Purpose:** Synthesize individual claim analyses into an overall article assessment, identifying logical fallacies, reasoning quality, and publication readiness.
125 -
126 -**Approach:** **Single-Pass Holistic Analysis** (Approach 1 from Comparison Matrix)
127 -
128 -**Why This Approach for POC1:**
129 -* ✅ **1 API call** (vs 2 for Two-Pass or Judge)
130 -* ✅ **Low cost** ($0.030 per article)
131 -* ✅ **Fast** (4-6 seconds)
132 -* ✅ **Low complexity** (simple implementation)
133 -* ⚠️ **Medium reliability** (acceptable for POC1, will improve in POC2/Production)
134 -
135 -**Alternative Approaches Considered:**
136 -
137 -|= Approach |= API Calls |= Cost |= Speed |= Complexity |= Reliability |= Best For
138 -| **1. Single-Pass** ⭐ | 1 | 💰 Low | ⚡ Fast | 🟢 Low | ⚠️ Medium | **POC1**
139 -| 2. Two-Pass | 2 | 💰💰 Med | 🐢 Slow | 🟡 Med | ✅ High | POC2/Prod
140 -| 3. Structured | 1 | 💰 Low | ⚡ Fast | 🟡 Med | ✅ High | POC1 (alternative)
141 -| 4. Weighted | 1 | 💰 Low | ⚡ Fast | 🟢 Low | ⚠️ Medium | POC1 (alternative)
142 -| 5. Heuristics | 1 | 💰 Lowest | ⚡⚡ Fastest | 🟡 Med | ⚠️ Medium | Any
143 -| 6. Hybrid | 1 | 💰 Low | ⚡ Fast | 🔴 Med-High | ✅ High | POC2
144 -| 7. Judge | 2 | 💰💰 Med | 🐢 Slow | 🟡 Med | ✅ High | Production
145 -
146 -**POC1 Choice:** Approach 1 (Single-Pass) for speed and simplicity. Will upgrade to Approach 2 (Two-Pass) or 6 (Hybrid) in POC2 for higher reliability.
147 -
148 -===== 3.3.2 What Stage 3 Evaluates =====
149 -
150 -Stage 3 performs **integrated holistic analysis** considering:
151 -
152 -**1. Claim-Level Aggregation:**
153 -* Verdict distribution (how many TRUE vs FALSE vs DISPUTED)
154 -* Average confidence across all claims
155 -* Claim interdependencies (do claims support/contradict each other?)
156 -* Critical claim identification (which claims are most important?)
157 -
158 -**2. Contextual Factors:**
159 -* **Source credibility**: Is the article from a reputable publisher?
160 -* **Author expertise**: Does the author have relevant credentials?
161 -* **Publication date**: Is information current or outdated?
162 -* **Claim coherence**: Do claims form a logical narrative?
163 -* **Missing context**: Are important caveats or qualifications missing?
164 -
165 -**3. Logical Fallacies:**
166 -* **Cherry-picking**: Selective evidence presentation
167 -* **False equivalence**: Treating unequal things as equal
168 -* **Straw man**: Misrepresenting opposing arguments
169 -* **Ad hominem**: Attacking person instead of argument
170 -* **Slippery slope**: Assuming extreme consequences without justification
171 -* **Circular reasoning**: Conclusion assumes premise
172 -* **False dichotomy**: Presenting only two options when more exist
173 -
174 -**4. Reasoning Quality:**
175 -* **Evidence strength**: Quality and quantity of supporting evidence
176 -* **Logical coherence**: Arguments follow logically
177 -* **Transparency**: Assumptions and limitations acknowledged
178 -* **Nuance**: Complexity and uncertainty appropriately addressed
179 -
180 -**5. Publication Readiness:**
181 -* **Risk tier assignment**: A (high risk), B (medium), or C (low risk)
182 -* **Publication mode**: DRAFT_ONLY, AI_GENERATED, or HUMAN_REVIEWED
183 -* **Required disclaimers**: What warnings should accompany this content?
184 -
185 -===== 3.3.3 Implementation: Single-Pass Approach =====
186 -
187 -**Input:**
188 -* Original article text (full content)
189 -* Stage 2 claim analyses (array of ClaimAnalysis objects)
190 -* Article metadata (URL, title, author, date, source)
191 -
192 -**Processing:**
193 -
88 +**Reference implementation (normative):**
194 194  {{code language="python"}}
195 -# Pseudo-code for Stage 3 (Single-Pass)
90 +import re
91 +import unicodedata
196 196  
197 -def stage3_holistic_assessment(article, claim_analyses, metadata):
198 - """
199 - Single-pass holistic assessment using Claude Sonnet 4.5.
200 -
201 - Approach 1: One comprehensive prompt that asks the LLM to:
202 - 1. Review all claim verdicts
203 - 2. Identify patterns and dependencies
204 - 3. Detect logical fallacies
205 - 4. Assess reasoning quality
206 - 5. Determine credibility score and risk tier
207 - 6. Generate publication recommendations
208 - """
209 -
210 - # Construct comprehensive prompt
211 - prompt = f"""
212 -You are analyzing an article for factual accuracy and logical reasoning.
93 +# Canonical claim normalization for deduplication.
94 +# Version: v1norm1
95 +#
96 +# IMPORTANT:
97 +# - Any change to these rules REQUIRES a new normalization version.
98 +# - Cache keys MUST include the normalization version to avoid collisions.
213 213  
214 -ARTICLE METADATA:
215 -- Title: {metadata['title']}
216 -- Source: {metadata['source']}
217 -- Date: {metadata['date']}
218 -- Author: {metadata['author']}
100 +CONTRACTIONS_V1NORM1 = {
101 + "don't": "do not",
102 + "doesn't": "does not",
103 + "didn't": "did not",
104 + "can't": "cannot",
105 + "won't": "will not",
106 + "shouldn't": "should not",
107 + "wouldn't": "would not",
108 + "isn't": "is not",
109 + "aren't": "are not",
110 + "wasn't": "was not",
111 + "weren't": "were not",
112 +}
219 219  
220 -ARTICLE TEXT:
221 -{article}
114 +def normalize_claim(text: str) -> str:
115 + """Canonical claim normalization (v1norm1)."""
116 + if text is None:
117 + return ""
222 222  
223 -INDIVIDUAL CLAIM ANALYSES:
224 -{format_claim_analyses(claim_analyses)}
119 + # 1) Unicode normalization
120 + text = unicodedata.normalize("NFD", text)
225 225  
226 -YOUR TASK:
227 -Perform a holistic assessment considering:
122 + # 2) Lowercase
123 + text = text.lower()
228 228  
229 -1. CLAIM AGGREGATION:
230 - - Review the verdict for each claim
231 - - Identify any interdependencies between claims
232 - - Determine which claims are most critical to the article's thesis
125 + # 3) Remove diacritics
126 + text = "".join(c for c in text if unicodedata.category(c) != "Mn")
233 233  
234 -2. CONTEXTUAL EVALUATION:
235 - - Assess source credibility
236 - - Evaluate author expertise
237 - - Consider publication timeliness
238 - - Identify missing context or important caveats
128 + # 4) Normalize apostrophes (common unicode variants)
129 + text = text.replace("’", "'").replace("‘", "'")
239 239  
240 -3. LOGICAL FALLACIES:
241 - - Identify any logical fallacies present
242 - - For each fallacy, provide:
243 - * Type of fallacy
244 - * Where it occurs in the article
245 - * Why it's problematic
246 - * Severity (minor/moderate/severe)
131 + # 5) Normalize percent sign
132 + text = text.replace("%", " percent")
247 247  
248 -4. REASONING QUALITY:
249 - - Evaluate evidence strength
250 - - Assess logical coherence
251 - - Check for transparency in assumptions
252 - - Evaluate handling of nuance and uncertainty
134 + # 6) Normalize whitespace
135 + text = re.sub(r"\s+", " ", text).strip()
253 253  
254 -5. CREDIBILITY SCORING:
255 - - Calculate overall credibility score (0.0-1.0)
256 - - Assign risk tier:
257 - * A (high risk): ≤0.5 credibility OR severe fallacies
258 - * B (medium risk): 0.5-0.8 credibility OR moderate issues
259 - * C (low risk): >0.8 credibility AND no significant issues
260 -
261 -6. PUBLICATION RECOMMENDATIONS:
262 - - Determine publication mode:
263 - * DRAFT_ONLY: Tier A, multiple severe issues
264 - * AI_GENERATED: Tier B/C, acceptable quality with disclaimers
265 - * HUMAN_REVIEWED: Complex or borderline cases
266 - - List required disclaimers
267 - - Explain decision rationale
137 + # 7) Remove punctuation except apostrophes
138 + text = re.sub(r"[^\w\s']", "", text)
268 268  
269 -OUTPUT FORMAT:
270 -Return a JSON object matching the ArticleAssessment schema.
271 -"""
272 -
273 - # Call LLM
274 - response = llm_client.complete(
275 - model="claude-sonnet-4-5-20250929",
276 - prompt=prompt,
277 - max_tokens=4000,
278 - response_format="json"
279 - )
280 -
281 - # Parse and validate response
282 - assessment = parse_json(response.content)
283 - validate_article_assessment_schema(assessment)
284 -
285 - return assessment
286 -{{/code}}
140 + # 8) Expand contractions (fixed list for v1norm1)
141 + for k, v in CONTRACTIONS_V1NORM1.items():
142 + text = re.sub(rf"\b{re.escape(k)}\b", v, text)
287 287  
288 -**Prompt Engineering Notes:**
144 + # 9) Final whitespace normalization
145 + text = re.sub(r"\s+", " ", text).strip()
289 289  
290 -1. **Structured Instructions**: Break down task into 6 clear sections
291 -2. **Context-Rich**: Provide article + all claim analyses + metadata
292 -3. **Explicit Criteria**: Define credibility scoring and risk tiers precisely
293 -4. **JSON Schema**: Request structured output matching ArticleAssessment schema
294 -5. **Examples** (in production): Include 2-3 example assessments for consistency
295 -
296 -===== 3.3.4 Credibility Scoring Algorithm =====
297 -
298 -**Base Score Calculation:**
299 -
300 -{{code language="python"}}
301 -def calculate_credibility_score(claim_analyses, fallacies, contextual_factors):
302 - """
303 - Calculate overall credibility score (0.0-1.0).
304 -
305 - This is a GUIDELINE for the LLM, not strict code.
306 - The LLM has flexibility to adjust based on context.
307 - """
308 -
309 - # 1. Claim Verdict Score (60% weight)
310 - verdict_weights = {
311 - "TRUE": 1.0,
312 - "PARTIALLY_TRUE": 0.7,
313 - "DISPUTED": 0.5,
314 - "UNSUPPORTED": 0.3,
315 - "FALSE": 0.0,
316 - "UNVERIFIABLE": 0.4
317 - }
318 -
319 - claim_scores = [
320 - verdict_weights[c.verdict.label] * c.verdict.confidence
321 - for c in claim_analyses
322 - ]
323 - avg_claim_score = sum(claim_scores) / len(claim_scores)
324 - claim_component = avg_claim_score * 0.6
325 -
326 - # 2. Fallacy Penalty (20% weight)
327 - fallacy_penalties = {
328 - "minor": -0.05,
329 - "moderate": -0.15,
330 - "severe": -0.30
331 - }
332 -
333 - fallacy_score = 1.0
334 - for fallacy in fallacies:
335 - fallacy_score += fallacy_penalties[fallacy.severity]
336 -
337 - fallacy_score = max(0.0, min(1.0, fallacy_score))
338 - fallacy_component = fallacy_score * 0.2
339 -
340 - # 3. Contextual Factors (20% weight)
341 - context_adjustments = {
342 - "source_credibility": {"positive": +0.1, "neutral": 0, "negative": -0.1},
343 - "author_expertise": {"positive": +0.1, "neutral": 0, "negative": -0.1},
344 - "timeliness": {"positive": +0.05, "neutral": 0, "negative": -0.05},
345 - "transparency": {"positive": +0.05, "neutral": 0, "negative": -0.05}
346 - }
347 -
348 - context_score = 1.0
349 - for factor in contextual_factors:
350 - adjustment = context_adjustments.get(factor.factor, {}).get(factor.impact, 0)
351 - context_score += adjustment
352 -
353 - context_score = max(0.0, min(1.0, context_score))
354 - context_component = context_score * 0.2
355 -
356 - # 4. Combine components
357 - final_score = claim_component + fallacy_component + context_component
358 -
359 - # 5. Apply confidence modifier
360 - avg_confidence = sum(c.verdict.confidence for c in claim_analyses) / len(claim_analyses)
361 - final_score = final_score * (0.8 + 0.2 * avg_confidence)
362 -
363 - return max(0.0, min(1.0, final_score))
147 + return text
364 364  {{/code}}
365 365  
366 -**Note:** This algorithm is a **guideline** provided to the LLM in the system prompt. The LLM has flexibility to adjust based on specific article context, but should generally follow this structure for consistency.
150 +=== 6) Report generation (locked) ===
151 +* ``report.md`` MUST be generated from ``result.json`` using a deterministic template (server-side).
152 +* The LLM MUST NOT be the sole renderer of report markdown.
367 367  
368 -===== 3.3.5 Risk Tier Assignment =====
154 +=== 7) No chain-of-thought storage/exposure (locked) ===
155 +* Store/expose only short, user-visible rationale bullets.
156 +* Never store or expose internal reasoning traces.
369 369  
370 -**Automatic Risk Tier Rules:**
371 -
372 -{{code}}
373 -Risk Tier A (High Risk - Requires Review):
374 -- Credibility score ≤ 0.5, OR
375 -- Any severe fallacies detected, OR
376 -- Multiple (3+) moderate fallacies, OR
377 -- 50%+ of claims are FALSE or UNSUPPORTED
378 -
379 -Risk Tier B (Medium Risk - May Publish with Disclaimers):
380 -- Credibility score 0.5-0.8, OR
381 -- 1-2 moderate fallacies, OR
382 -- 20-49% of claims are DISPUTED or PARTIALLY_TRUE
383 -
384 -Risk Tier C (Low Risk - Safe to Publish):
385 -- Credibility score > 0.8, AND
386 -- No severe or moderate fallacies, AND
387 -- <20% disputed/problematic claims, AND
388 -- No critical missing context
389 -{{/code}}
390 -
391 -===== 3.3.6 Output: ArticleAssessment Schema =====
392 -
393 -(See Stage 3 Output Schema section above for complete JSON schema)
394 -
395 -===== 3.3.7 Performance Metrics =====
396 -
397 -**POC1 Targets:**
398 -* **Processing time**: 4-6 seconds per article
399 -* **Cost**: $0.030 per article (Sonnet 4.5 tokens)
400 -* **Quality**: 70-80% agreement with human reviewers (acceptable for POC)
401 -* **API calls**: 1 per article
402 -
403 -**Future Improvements (POC2/Production):**
404 -* Upgrade to Two-Pass (Approach 2): +15% accuracy, +$0.020 cost
405 -* Add human review sampling: 10% of Tier B articles
406 -* Implement Judge approach (Approach 7) for Tier A: Highest quality
407 -
408 -===== 3.3.8 Example Stage 3 Execution =====
409 -
410 -**Input:**
411 -* Article: "Biden won the 2020 election"
412 -* Claim analyses: [{claim: "Biden won", verdict: "TRUE", confidence: 0.95}]
413 -
414 -**Stage 3 Processing:**
415 -1. Analyzes single claim with high confidence
416 -2. Checks for contextual factors (source credibility)
417 -3. Searches for logical fallacies (none found)
418 -4. Calculates credibility: 0.6 * 0.95 + 0.2 * 1.0 + 0.2 * 1.0 = 0.97
419 -5. Assigns risk tier: C (low risk)
420 -6. Recommends: AI_GENERATED publication mode
421 -
422 -**Output:**
423 -```json
424 -{
425 - "article_id": "a1",
426 - "overall_assessment": {
427 - "credibility_score": 0.97,
428 - "risk_tier": "C",
429 - "summary": "Article makes single verifiable claim with strong evidence support",
430 - "confidence": 0.95
431 - },
432 - "claim_aggregation": {
433 - "total_claims": 1,
434 - "verdict_distribution": {"TRUE": 1},
435 - "avg_confidence": 0.95
436 - },
437 - "contextual_factors": [
438 - {"factor": "source_credibility", "impact": "positive", "description": "Reputable news source"}
439 - ],
440 - "recommendations": {
441 - "publication_mode": "AI_GENERATED",
442 - "requires_review": false,
443 - "suggested_disclaimers": []
444 - }
445 -}
446 -```
447 -
448 -==== What Cache-Only Mode Provides: ====
449 -
450 -✅ **Claim Extraction (Platform-Funded):**
451 -
452 -* Stage 1 extraction runs at $0.003 per article
453 -* **Cost: Absorbed by platform** (not charged to user credit)
454 -* Rationale: Extraction is necessary to check cache, and cost is negligible
455 -* Rate limit: Max 50 extractions/day in cache-only mode (prevents abuse)
456 -
457 -✅ **Instant Access to Cached Claims:**
458 -
459 -* Any claim that exists in cache → Full verdict returned
460 -* Cost: $0 (no LLM calls)
461 -* Response time: <100ms
462 -
463 -✅ **Partial Article Analysis:**
464 -
465 -* Check each claim against cache
466 -* Return verdicts for ALL cached claims
467 -* For uncached claims: Return "status": "cache_miss"
468 -
469 -✅ **Cache Coverage Report:**
470 -
471 -* "3 of 5 claims available in cache (60% coverage)"
472 -* Links to cached analyses
473 -* Estimated cost to complete: $0.162 (2 new claims)
474 -
475 -❌ **Not Available in Cache-Only Mode:**
476 -
477 -* New claim analysis (Stage 2 LLM calls blocked)
478 -* Full holistic assessment (Stage 3 blocked if any claims missing)
479 -
480 -==== User Experience Example: ====
481 -
482 -{{{{
483 - "status": "cache_only_mode",
484 - "message": "Monthly credit limit reached. Showing cached results only.",
485 - "cache_coverage": {
486 - "claims_total": 5,
487 - "claims_cached": 3,
488 - "claims_missing": 2,
489 - "coverage_percent": 60
490 - },
491 - "cached_claims": [
492 - {"claim_id": "C1", "verdict": "Likely", "confidence": 0.82},
493 - {"claim_id": "C2", "verdict": "Highly Likely", "confidence": 0.91},
494 - {"claim_id": "C4", "verdict": "Unclear", "confidence": 0.55}
495 - ],
496 - "missing_claims": [
497 - {"claim_id": "C3", "claim_text": "...", "estimated_cost": "$0.081"},
498 - {"claim_id": "C5", "claim_text": "...", "estimated_cost": "$0.081"}
499 - ],
500 - "upgrade_options": {
501 - "top_up": "$5 for 20-70 more articles",
502 - "pro_tier": "$50/month unlimited"
503 - }
504 -}
505 -}}}
506 -
507 -**Design Rationale:**
508 -
509 -* Free users still get value (cached claims often answer their question)
510 -* Demonstrates FactHarbor's value (partial results encourage upgrade)
511 -* Sustainable for platform (no additional cost)
512 -* Fair to all users (everyone contributes to cache)
513 -
514 514  ----
515 515  
160 +== REST API (POC1) ==
516 516  
162 +=== 1) API Versioning ===
163 +Base path: ``/v1``
517 517  
518 -== 6. LLM Abstraction Layer ==
165 +=== 2) Content types ===
166 +* Requests: ``application/json``
167 +* JSON responses: ``application/json``
168 +* Report download: ``text/markdown; charset=utf-8``
169 +* SSE events: ``text/event-stream``
519 519  
520 -=== 6.1 Design Principle ===
171 +=== 3) Time & IDs ===
172 +* All timestamps: ISO 8601 UTC (e.g., ``2025-12-24T19:31:30Z``)
173 +* ``job_id``: ULID (string)
174 +* ``claim_hash``: sha256 hex string (lowercase), computed over canonical claim + version + language as specified
521 521  
522 -**FactHarbor uses provider-agnostic LLM abstraction** to avoid vendor lock-in and enable:
176 +=== 4) Authentication ===
177 +All ``/v1`` endpoints require:
178 +* Header: ``Authorization: Bearer <API_KEY>``
523 523  
524 -* **Provider switching:** Change LLM providers without code changes
525 -* **Cost optimization:** Use different providers for different stages
526 -* **Resilience:** Automatic fallback if primary provider fails
527 -* **Cross-checking:** Compare outputs from multiple providers
528 -* **A/B testing:** Test new models without deployment changes
529 -
530 -**Implementation:** All LLM calls go through an abstraction layer that routes to configured providers.
531 -
532 -----
533 -
534 -=== 6.2 LLM Provider Interface ===
535 -
536 -**Abstract Interface:**
537 -
538 -{{{
539 -interface LLMProvider {
540 - // Core methods
541 - complete(prompt: string, options: CompletionOptions): Promise<CompletionResponse>
542 - stream(prompt: string, options: CompletionOptions): AsyncIterator<StreamChunk>
543 -
544 - // Provider metadata
545 - getName(): string
546 - getMaxTokens(): number
547 - getCostPer1kTokens(): { input: number, output: number }
548 -
549 - // Health check
550 - isAvailable(): Promise<boolean>
551 -}
552 -
553 -interface CompletionOptions {
554 - model?: string
555 - maxTokens?: number
556 - temperature?: number
557 - stopSequences?: string[]
558 - systemPrompt?: string
559 -}
560 -}}}
561 -
562 -----
563 -
564 -=== 6.3 Supported Providers (POC1) ===
565 -
566 -**Primary Provider (Default):**
567 -
568 -* **Anthropic Claude API**
569 - * Models: Claude Haiku 4.5, Claude Sonnet 4.5, Claude Opus 4
570 - * Used by default in POC1
571 - * Best quality for holistic analysis
572 -
573 -**Secondary Providers (Future):**
574 -
575 -* **OpenAI API**
576 - * Models: GPT-4o, GPT-4o-mini
577 - * For cost comparison
578 -
579 -* **Google Vertex AI**
580 - * Models: Gemini 1.5 Pro, Gemini 1.5 Flash
581 - * For diversity in evidence gathering
582 -
583 -* **Local Models** (Post-POC)
584 - * Models: Llama 3.1, Mistral
585 - * For privacy-sensitive deployments
586 -
587 -----
588 -
589 -=== 6.4 Provider Configuration ===
590 -
591 -**Environment Variables:**
592 -
593 -{{{
594 -# Primary provider
595 -LLM_PRIMARY_PROVIDER=anthropic
596 -ANTHROPIC_API_KEY=sk-ant-...
597 -
598 -# Fallback provider
599 -LLM_FALLBACK_PROVIDER=openai
600 -OPENAI_API_KEY=sk-...
601 -
602 -# Provider selection per stage
603 -LLM_STAGE1_PROVIDER=anthropic
604 -LLM_STAGE1_MODEL=claude-haiku-4
605 -LLM_STAGE2_PROVIDER=anthropic
606 -LLM_STAGE2_MODEL=claude-sonnet-4-5-20250929
607 -LLM_STAGE3_PROVIDER=anthropic
608 -LLM_STAGE3_MODEL=claude-sonnet-4-5-20250929
609 -
610 -# Cost limits
611 -LLM_MAX_COST_PER_REQUEST=1.00
612 -}}}
613 -
614 -**Database Configuration (Alternative):**
615 -
616 -{{{{
180 +=== 5) Error Envelope (all non-2xx) ===
181 +{{code language="json"}}
617 617  {
618 - "providers": [
619 - {
620 - "name": "anthropic",
621 - "api_key_ref": "vault://anthropic-api-key",
622 - "enabled": true,
623 - "priority": 1
624 - },
625 - {
626 - "name": "openai",
627 - "api_key_ref": "vault://openai-api-key",
628 - "enabled": true,
629 - "priority": 2
183 + "error": {
184 + "code": "CACHE_MISS | VALIDATION_ERROR | UNAUTHORIZED | FORBIDDEN | NOT_FOUND | RATE_LIMITED | UPSTREAM_FETCH_ERROR | INTERNAL_ERROR",
185 + "message": "Human readable message",
186 + "details": {
187 + "field_errors": [
188 + {"field": "input_url", "issue": "Must be a valid https URL"}
189 + ]
630 630   }
631 - ],
632 - "stage_config": {
633 - "stage1": {
634 - "provider": "anthropic",
635 - "model": "claude-haiku-4-5-20251001",
636 - "max_tokens": 4096,
637 - "temperature": 0.0
638 - },
639 - "stage2": {
640 - "provider": "anthropic",
641 - "model": "claude-sonnet-4-5-20250929",
642 - "max_tokens": 16384,
643 - "temperature": 0.3
644 - },
645 - "stage3": {
646 - "provider": "anthropic",
647 - "model": "claude-sonnet-4-5-20250929",
648 - "max_tokens": 8192,
649 - "temperature": 0.2
650 - }
651 651   }
652 652  }
653 -}}}
193 +{{/code}}
654 654  
655 655  ----
656 656  
657 -=== 6.5 Stage-Specific Models (POC1 Defaults) ===
197 +== Endpoints ==
658 658  
659 -**Stage 1: Claim Extraction**
199 +=== 1) POST /v1/analyze ===
200 +Creates an asynchronous job. Exactly one of ``input_url`` or ``input_text`` MUST be provided.
660 660  
661 -* **Default:** Anthropic Claude Haiku 4.5
662 -* **Alternative:** OpenAI GPT-4o-mini, Google Gemini 1.5 Flash
663 -* **Rationale:** Fast, cheap, simple task
664 -* **Cost:** ~$0.003 per article
665 -
666 -**Stage 2: Claim Analysis** (CACHEABLE)
667 -
668 -* **Default:** Anthropic Claude Sonnet 4.5
669 -* **Alternative:** OpenAI GPT-4o, Google Gemini 1.5 Pro
670 -* **Rationale:** High-quality analysis, cached 90 days
671 -* **Cost:** ~$0.081 per NEW claim
672 -
673 -**Stage 3: Holistic Assessment**
674 -
675 -* **Default:** Anthropic Claude Sonnet 4.5
676 -* **Alternative:** OpenAI GPT-4o, Claude Opus 4 (for high-stakes)
677 -* **Rationale:** Complex reasoning, logical fallacy detection
678 -* **Cost:** ~$0.030 per article
679 -
680 -**Cost Comparison (Example):**
681 -
682 -|=Stage|=Anthropic (Default)|=OpenAI Alternative|=Google Alternative
683 -|Stage 1|Claude Haiku 4.5.5 ($0.003)|GPT-4o-mini ($0.002)|Gemini Flash ($0.002)
684 -|Stage 2|Claude Sonnet 4.5 ($0.081)|GPT-4o ($0.045)|Gemini Pro ($0.050)
685 -|Stage 3|Claude Sonnet 4.5 ($0.030)|GPT-4o ($0.018)|Gemini Pro ($0.020)
686 -|**Total (0% cache)**|**$0.114**|**$0.065**|**$0.072**
687 -
688 -**Note:** POC1 uses Anthropic exclusively for consistency. Multi-provider support planned for POC2.
689 -
690 -----
691 -
692 -=== 6.6 Failover Strategy ===
693 -
694 -**Automatic Failover:**
695 -
696 -{{{
697 -async function completeLLM(stage: string, prompt: string): Promise<string> {
698 - const primaryProvider = getProviderForStage(stage)
699 - const fallbackProvider = getFallbackProvider()
700 -
701 - try {
702 - return await primaryProvider.complete(prompt)
703 - } catch (error) {
704 - if (error.type === 'rate_limit' || error.type === 'service_unavailable') {
705 - logger.warn(`Primary provider failed, using fallback`)
706 - return await fallbackProvider.complete(prompt)
707 - }
708 - throw error
202 +**Request (AnalyzeRequest)**
203 +{{code language="json"}}
204 +{
205 + "input_url": "https://example.org/article",
206 + "input_text": null,
207 + "options": {
208 + "max_claims": 5,
209 + "cache_preference": "prefer_cache",
210 + "browsing": "on",
211 + "output_report": true
212 + },
213 + "client": {
214 + "request_id": "optional-idempotency-key"
709 709   }
710 710  }
711 -}}}
217 +{{/code}}
712 712  
713 -**Fallback Priority:**
219 +**Options**
220 +* ``max_claims``: integer, 1..50 (default 5)
221 +* ``cache_preference``: ``prefer_cache`` | ``allow_partial`` | ``cache_only`` | ``skip_cache``
222 +** ``prefer_cache``: use cache when available; otherwise run full pipeline
223 +** ``allow_partial``: if Stage 2 cached analyses exist, server MAY skip Stage 1+2 and rerun only Stage 3 using cached analyses
224 +** ``cache_only``: do not run Stage 2 for uncached claims; fail with CACHE_MISS (402) if required claim analyses are missing
225 +** ``skip_cache``: ignore cache and recompute
226 +* ``browsing``: ``on`` | ``off``
227 +** ``off``: do not retrieve evidence; mark evidence items as ``NEEDS_RETRIEVAL`` and output retrieval queries
228 +* ``output_report``: boolean (default true)
714 714  
715 -1. **Primary:** Configured provider for stage
716 -2. **Secondary:** Fallback provider (if configured)
717 -3. **Cache:** Return cached result (if available for Stage 2)
718 -4. **Error:** Return 503 Service Unavailable
719 -
720 -----
721 -
722 -=== 6.7 Provider Selection API ===
723 -
724 -**Admin Endpoint:** POST /admin/v1/llm/configure
725 -
726 -**Update provider for specific stage:**
727 -
728 -{{{{
230 +**Response: 202 Accepted (JobCreated)**
231 +{{code language="json"}}
729 729  {
730 - "stage": "stage2",
731 - "provider": "openai",
732 - "model": "gpt-4o",
733 - "max_tokens": 16384,
734 - "temperature": 0.3
735 -}
736 -}}}
737 -
738 -**Response:** 200 OK
739 -
740 -{{{{
741 -{
742 - "message": "LLM configuration updated",
743 - "stage": "stage2",
744 - "previous": {
745 - "provider": "anthropic",
746 - "model": "claude-sonnet-4-5-20250929"
233 + "job_id": "01J8Y9K6M2Q1J0JZ7E5P8H7Y9C",
234 + "status": "QUEUED",
235 + "created_at": "2025-12-24T19:30:00Z",
236 + "estimated_cost": {
237 + "credits": 420,
238 + "explain": "Estimate may change after Stage 1 (claim count)."
747 747   },
748 - "current": {
749 - "provider": "openai",
750 - "model": "gpt-4o"
751 - },
752 - "cost_impact": {
753 - "previous_cost_per_claim": 0.081,
754 - "new_cost_per_claim": 0.045,
755 - "savings_percent": 44
240 + "links": {
241 + "self": "/v1/jobs/01J8Y9K6M2Q1J0JZ7E5P8H7Y9C",
242 + "events": "/v1/jobs/01J8Y9K6M2Q1J0JZ7E5P8H7Y9C/events",
243 + "result": "/v1/jobs/01J8Y9K6M2Q1J0JZ7E5P8H7Y9C/result",
244 + "report": "/v1/jobs/01J8Y9K6M2Q1J0JZ7E5P8H7Y9C/report"
756 756   }
757 757  }
758 -}}}
247 +{{/code}}
759 759  
760 -**Get current configuration:**
761 -
762 -GET /admin/v1/llm/config
763 -
764 -{{{{
249 +**Cache-only failure: 402 (CACHE_MISS)**
250 +{{code language="json"}}
765 765  {
766 - "providers": ["anthropic", "openai"],
767 - "primary": "anthropic",
768 - "fallback": "openai",
769 - "stages": {
770 - "stage1": {
771 - "provider": "anthropic",
772 - "model": "claude-haiku-4-5-20251001",
773 - "cost_per_request": 0.003
774 - },
775 - "stage2": {
776 - "provider": "anthropic",
777 - "model": "claude-sonnet-4-5-20250929",
778 - "cost_per_new_claim": 0.081
779 - },
780 - "stage3": {
781 - "provider": "anthropic",
782 - "model": "claude-sonnet-4-5-20250929",
783 - "cost_per_request": 0.030
252 + "error": {
253 + "code": "CACHE_MISS",
254 + "message": "cache_only requested but a required claim analysis is not cached.",
255 + "details": {
256 + "missing_claim_hash": "9f2a...c01",
257 + "normalization_version": "v1norm1"
784 784   }
785 785   }
786 786  }
787 -}}}
261 +{{/code}}
788 788  
789 -----
263 +=== 2) GET /v1/jobs/{job_id} ===
264 +Returns job status and progress.
790 790  
791 -=== 6.8 Implementation Notes ===
792 -
793 -**Provider Adapter Pattern:**
794 -
795 -{{{
796 -class AnthropicProvider implements LLMProvider {
797 - async complete(prompt: string, options: CompletionOptions) {
798 - const response = await anthropic.messages.create({
799 - model: options.model || 'claude-sonnet-4-5-20250929',
800 - max_tokens: options.maxTokens || 4096,
801 - messages: [{ role: 'user', content: prompt }],
802 - system: options.systemPrompt
803 - })
804 - return response.content[0].text
266 +**Response: 200 OK (Job)**
267 +{{code language="json"}}
268 +{
269 + "job_id": "01J...",
270 + "status": "RUNNING",
271 + "created_at": "2025-12-24T19:30:00Z",
272 + "updated_at": "2025-12-24T19:31:12Z",
273 + "progress": {
274 + "stage": "STAGE2_CLAIM_ANALYSIS",
275 + "stage_progress": 0.4,
276 + "message": "Analyzing claim 3/8"
277 + },
278 + "links": {
279 + "events": "/v1/jobs/01J.../events",
280 + "result": "/v1/jobs/01J.../result",
281 + "report": "/v1/jobs/01J.../report"
805 805   }
806 806  }
284 +{{/code}}
807 807  
808 -class OpenAIProvider implements LLMProvider {
809 - async complete(prompt: string, options: CompletionOptions) {
810 - const response = await openai.chat.completions.create({
811 - model: options.model || 'gpt-4o',
812 - max_tokens: options.maxTokens || 4096,
813 - messages: [
814 - { role: 'system', content: options.systemPrompt },
815 - { role: 'user', content: prompt }
816 - ]
817 - })
818 - return response.choices[0].message.content
819 - }
820 -}
821 -}}}
286 +Statuses:
287 +* ``QUEUED`` → ``RUNNING`` → ``SUCCEEDED`` | ``FAILED`` | ``CANCELED``
822 822  
823 -**Provider Registry:**
289 +Stages:
290 +* ``STAGE1_CLAIM_EXTRACT``
291 +* ``STAGE2_CLAIM_ANALYSIS``
292 +* ``STAGE3_ARTICLE_ASSESSMENT``
824 824  
825 -{{{
826 -const providers = new Map<string, LLMProvider>()
827 -providers.set('anthropic', new AnthropicProvider())
828 -providers.set('openai', new OpenAIProvider())
829 -providers.set('google', new GoogleProvider())
294 +=== 3) GET /v1/jobs/{job_id}/events ===
295 +Server-Sent Events (SSE) for job progress reporting (no token streaming).
830 830  
831 -function getProvider(name: string): LLMProvider {
832 - return providers.get(name) || providers.get(config.primaryProvider)
833 -}
834 -}}}
297 +Event types (minimum):
298 +* ``job.created``
299 +* ``stage.started``
300 +* ``stage.progress``
301 +* ``stage.completed``
302 +* ``job.succeeded``
303 +* ``job.failed``
835 835  
836 -----
305 +=== 4) GET /v1/jobs/{job_id}/result ===
306 +Returns JSON result.
307 +* ``200`` if job ``SUCCEEDED``
308 +* ``409`` if not finished
837 837  
838 -== 3. REST API Contract ==
310 +=== 5) GET /v1/jobs/{job_id}/report ===
311 +Returns ``text/markdown`` report.
312 +* ``200`` if job ``SUCCEEDED``
313 +* ``409`` if not finished
839 839  
840 -=== 3.1 User Credit Tracking ===
315 +=== 6) DELETE /v1/jobs/{job_id} ===
316 +Cancels a job (best-effort) and deletes stored artifacts if supported.
317 +* ``204`` success
318 +* ``404`` if not found
841 841  
842 -**Endpoint:** GET /v1/user/credit
843 -
844 -**Response:** 200 OK
845 -
846 -{{{{
847 - "user_id": "user_abc123",
848 - "tier": "free",
849 - "credit_limit": 10.00,
850 - "credit_used": 7.42,
851 - "credit_remaining": 2.58,
852 - "reset_date": "2025-02-01T00:00:00Z",
853 - "cache_only_mode": false,
854 - "usage_stats": {
855 - "articles_analyzed": 67,
856 - "claims_from_cache": 189,
857 - "claims_newly_analyzed": 113,
858 - "cache_hit_rate": 0.626
859 - }
860 -}
861 -}}}
862 -
863 -----
864 -
865 -
866 -
867 -==== Stage 2 Output Schema: ClaimAnalysis ====
868 -
869 -**Complete schema for each claim's analysis result:**
870 -
320 +=== 7) GET /v1/health ===
321 +**Response: 200 OK**
871 871  {{code language="json"}}
872 872  {
873 - "claim_id": "claim_abc123",
874 - "claim_text": "Biden won the 2020 election",
875 - "scenarios": [
876 - {
877 - "scenario_id": "scenario_1",
878 - "description": "Interpreting 'won' as Electoral College victory",
879 - "verdict": {
880 - "label": "TRUE",
881 - "confidence": 0.95,
882 - "explanation": "Joe Biden won 306 electoral votes vs Trump's 232"
883 - },
884 - "evidence": {
885 - "supporting": [
886 - {
887 - "text": "Biden certified with 306 electoral votes",
888 - "source_url": "https://www.archives.gov/electoral-college/2020",
889 - "source_title": "2020 Electoral College Results",
890 - "credibility_score": 0.98
891 - }
892 - ],
893 - "opposing": []
894 - }
895 - }
896 - ],
897 - "recommended_scenario": "scenario_1",
898 - "metadata": {
899 - "analysis_timestamp": "2024-12-24T18:00:00Z",
900 - "model_used": "claude-sonnet-4-5-20250929",
901 - "processing_time_seconds": 8.5
902 - }
324 + "status": "ok",
325 + "service": "factharbor-poc1",
326 + "version": "v0.9.105",
327 + "time": "2025-12-24T19:31:30Z"
903 903  }
904 904  {{/code}}
905 905  
906 -**Required Fields:**
907 -* **claim_id**: Unique identifier matching Stage 1 output
908 -* **claim_text**: The exact claim being analyzed
909 -* **scenarios**: Array of interpretation scenarios (minimum 1)
910 - * **scenario_id**: Unique ID for this scenario
911 - * **description**: Clear interpretation of the claim
912 - * **verdict**: Verdict object with label, confidence, explanation
913 - * **evidence**: Supporting and opposing evidence arrays
914 -* **recommended_scenario**: ID of the primary/recommended scenario
915 -* **metadata**: Processing metadata (timestamp, model, timing)
331 +----
916 916  
917 -**Optional Fields:**
918 -* Additional context, warnings, or quality scores
333 +== Idempotency (Required) ==
919 919  
920 -**Minimum Viable Example:**
335 +Clients SHOULD send:
336 +* Header: ``Idempotency-Key: <string>`` (preferred)
337 +or
338 +* Body: ``client.request_id``
921 921  
922 -{{code language="json"}}
923 -{
924 - "claim_id": "c1",
925 - "claim_text": "The sky is blue",
926 - "scenarios": [{
927 - "scenario_id": "s1",
928 - "description": "Under clear daytime conditions",
929 - "verdict": {"label": "TRUE", "confidence": 0.99, "explanation": "Rayleigh scattering"},
930 - "evidence": {"supporting": [], "opposing": []}
931 - }],
932 - "recommended_scenario": "s1",
933 - "metadata": {"analysis_timestamp": "2024-12-24T18:00:00Z"}
934 -}
935 -{{/code}}
340 +Server behavior:
341 +* Same key + same request body → return existing job (``200``) with:
342 +** ``idempotent=true``
343 +** ``original_request_at``
344 +* Same key + different request body → ``409`` with ``VALIDATION_ERROR`` and mismatch details
936 936  
346 +Idempotency TTL: 24 hours (matches minimum job retention)
937 937  
348 +----
938 938  
939 -==== Stage 3 Output Schema: ArticleAssessment ====
350 +== Schemas (result.json) ==
940 940  
941 -**Complete schema for holistic article-level assessment:**
942 -
352 +=== Top-level AnalysisResult ===
943 943  {{code language="json"}}
944 944  {
945 - "article_id": "article_xyz789",
946 - "overall_assessment": {
947 - "credibility_score": 0.72,
948 - "risk_tier": "B",
949 - "summary": "Article contains mostly accurate claims with one disputed claim requiring expert review",
950 - "confidence": 0.85
355 + "job_id": "01J...",
356 + "input": {
357 + "source_type": "url|text",
358 + "source": "https://example.org/article",
359 + "language": "en",
360 + "retrieved_at_utc": "2025-12-24T19:30:12Z",
361 + "extraction": {
362 + "method": "jina|trafilatura|manual",
363 + "word_count": 1234
364 + }
951 951   },
952 - "claim_aggregation": {
953 - "total_claims": 5,
954 - "verdict_distribution": {
955 - "TRUE": 3,
956 - "PARTIALLY_TRUE": 1,
957 - "DISPUTED": 1,
958 - "FALSE": 0,
959 - "UNSUPPORTED": 0,
960 - "UNVERIFIABLE": 0
961 - },
962 - "avg_confidence": 0.82
366 + "claim_extraction": {
367 + "normalization_version": "v1norm1",
368 + "claims": [
369 + {
370 + "claim_hash": "sha256hex...",
371 + "claim_text": "as found in source",
372 + "canonical_claim_text": "normalized claim",
373 + "confidence": 0.72
374 + }
375 + ]
963 963   },
964 - "contextual_factors": [
377 + "claim_analyses": [
965 965   {
966 - "factor": "Source credibility",
967 - "impact": "positive",
968 - "description": "Published by reputable news organization"
969 - },
970 - {
971 - "factor": "Claim interdependence",
972 - "impact": "neutral",
973 - "description": "Claims are independent; no logical chains"
379 + "claim_hash": "sha256hex...",
380 + "claim_verdict": {
381 + "verdict_label": "Supported|Refuted|Inconclusive",
382 + "confidence": 0.63,
383 + "rationale_bullets": [
384 + "Short bullet 1",
385 + "Short bullet 2"
386 + ]
387 + },
388 + "scenarios": [
389 + {
390 + "scenario_id": "01J...ULID",
391 + "scenario_title": "Interpretation / boundary",
392 + "definitions": { "term": "definition" },
393 + "assumptions": ["..."],
394 + "boundaries": { "time": "...", "geography": "...", "population": "...", "conditions": "..." },
395 + "retrieval_plan": {
396 + "queries": [
397 + {"q": "support query", "purpose": "support"},
398 + {"q": "counter query", "purpose": "counter"}
399 + ]
400 + },
401 + "evidence": [
402 + {
403 + "evidence_id": "01J...ULID",
404 + "stance": "supports|undermines|mixed|context_dependent",
405 + "relevance": 0.0,
406 + "summary_bullets": ["..."],
407 + "citation": {
408 + "title": "Source title",
409 + "publisher": "Publisher",
410 + "author_or_org": "Org/Author",
411 + "publication_date": "YYYY-MM-DD",
412 + "url": "https://...",
413 + "retrieved_at_utc": "2025-12-24T19:31:01Z"
414 + },
415 + "excerpt": "optional short quote (max 25 words)",
416 + "reliability_rating": "high|medium|low",
417 + "limitations": ["..."],
418 + "retrieval_status": "OK|NEEDS_RETRIEVAL|FAILED"
419 + }
420 + ],
421 + "verdict": {
422 + "verdict_label": "Highly likely|Likely|Unclear|Unlikely|Highly unlikely|Unsubstantiated",
423 + "probability_range": [0.0, 1.0],
424 + "confidence": 0.0,
425 + "rationale_bullets": ["..."],
426 + "key_supporting_evidence_ids": ["E..."],
427 + "key_counter_evidence_ids": ["E..."],
428 + "uncertainty_factors": ["..."],
429 + "what_would_change_my_mind": ["..."]
430 + }
431 + }
432 + ],
433 + "quality_gates": {
434 + "gate1_claim_validation": "pass|partial|fail",
435 + "gate2_contradiction_search": "pass|partial|fail",
436 + "gate3_uncertainty_disclosure": "pass|partial|fail",
437 + "gate4_verdict_confidence": "pass|partial|fail",
438 + "fail_reasons": []
439 + }
974 974   }
975 975   ],
976 - "recommendations": {
977 - "publication_mode": "AI_GENERATED",
978 - "requires_review": false,
979 - "review_reason": null,
980 - "suggested_disclaimers": [
981 - "One claim (Claim 4) has conflicting expert opinions"
442 + "article_assessment": {
443 + "main_thesis": "string",
444 + "thesis_support": "supported|challenged|mixed|unclear",
445 + "overall_reasoning_quality": "high|medium|low",
446 + "summary": "string",
447 + "key_risks": [
448 + "missing evidence",
449 + "cherry-picking",
450 + "correlation/causation",
451 + "time window mismatch"
452 + ],
453 + "how_claims_connect_to_thesis": [
454 + "Short bullets connecting claim analyses to the thesis assessment"
982 982   ]
983 983   },
984 - "metadata": {
985 - "holistic_timestamp": "2024-12-24T18:00:10Z",
986 - "model_used": "claude-sonnet-4-5-20250929",
987 - "processing_time_seconds": 4.2,
988 - "cache_used": false
457 + "global_notes": {
458 + "limitations": ["..."],
459 + "policy_notes": []
989 989   }
990 990  }
991 991  {{/code}}
992 992  
993 -**Required Fields:**
994 -* **article_id**: Unique identifier for this article
995 -* **overall_assessment**: Top-level assessment
996 - * **credibility_score**: 0.0-1.0 composite score
997 - * **risk_tier**: A, B, or C (per AKEL quality gates)
998 - * **summary**: Human-readable assessment
999 - * **confidence**: How confident the holistic assessment is
1000 -* **claim_aggregation**: Statistics across all claims
1001 - * **total_claims**: Count of claims analyzed
1002 - * **verdict_distribution**: Count per verdict label
1003 - * **avg_confidence**: Average confidence across verdicts
1004 -* **contextual_factors**: Array of contextual considerations
1005 -* **recommendations**: Publication decision support
1006 - * **publication_mode**: DRAFT_ONLY, AI_GENERATED, or HUMAN_REVIEWED
1007 - * **requires_review**: Boolean flag
1008 - * **suggested_disclaimers**: Array of disclaimer texts
1009 -* **metadata**: Processing metadata
464 +=== Mandatory counter-evidence rule (POC1) ===
465 +For each scenario, attempt to include at least one evidence item with:
466 +* ``stance`` ∈ {``undermines``, ``mixed``, ``context_dependent``}
1010 1010  
1011 -**Minimum Viable Example:**
468 +If not found:
469 +* include explicit “not found despite targeted search” note in ``uncertainty_factors``
470 +* and/or include evidence items with ``retrieval_status=FAILED`` indicating the attempted search
1012 1012  
1013 -{{code language="json"}}
1014 -{
1015 - "article_id": "a1",
1016 - "overall_assessment": {
1017 - "credibility_score": 0.95,
1018 - "risk_tier": "C",
1019 - "summary": "All claims verified as true",
1020 - "confidence": 0.98
1021 - },
1022 - "claim_aggregation": {
1023 - "total_claims": 1,
1024 - "verdict_distribution": {"TRUE": 1},
1025 - "avg_confidence": 0.99
1026 - },
1027 - "contextual_factors": [],
1028 - "recommendations": {
1029 - "publication_mode": "AI_GENERATED",
1030 - "requires_review": false,
1031 - "suggested_disclaimers": []
1032 - },
1033 - "metadata": {"holistic_timestamp": "2024-12-24T18:00:00Z"}
1034 -}
1035 -{{/code}}
1036 -
1037 -=== 3.2 Create Analysis Job (3-Stage) ===
1038 -
1039 -**Endpoint:** POST /v1/analyze
1040 -
1041 -==== Idempotency Support: ====
1042 -
1043 -To prevent duplicate job creation on network retries, clients SHOULD include:
1044 -
1045 -{{{POST /v1/analyze
1046 -Idempotency-Key: {client-generated-uuid}
1047 -}}}
1048 -
1049 -OR use the client.request_id field:
1050 -
1051 -{{{{
1052 - "input_url": "...",
1053 - "client": {
1054 - "request_id": "client-uuid-12345",
1055 - "source_label": "optional"
1056 - }
1057 -}
1058 -}}}
1059 -
1060 -**Server Behavior:**
1061 -
1062 -* If Idempotency-Key or request_id seen before (within 24 hours):
1063 -** Return existing job (200 OK, not 202 Accepted)
1064 -** Do NOT create duplicate job or charge twice
1065 -* Idempotency keys expire after 24 hours (matches job retention)
1066 -
1067 -**Example Response (Idempotent):**
1068 -
1069 -{{{{
1070 - "job_id": "01J...ULID",
1071 - "status": "RUNNING",
1072 - "idempotent": true,
1073 - "original_request_at": "2025-12-24T10:31:00Z",
1074 - "message": "Returning existing job (idempotency key matched)"
1075 -}
1076 -}}}
1077 -
1078 -==== Request Body: ====
1079 -
1080 -{{{{
1081 - "input_type": "url",
1082 - "input_url": "https://example.com/medical-report-01",
1083 - "input_text": null,
1084 - "options": {
1085 - "browsing": "on",
1086 - "depth": "standard",
1087 - "max_claims": 5,
1088 -
1089 -* **cache_preference** (optional): Cache usage preference
1090 - * **Type:** string
1091 - * **Enum:** {{code}}["prefer_cache", "allow_partial", "skip_cache"]{{/code}}
1092 - * **Default:** {{code}}"prefer_cache"{{/code}}
1093 - * **Semantics:**
1094 - * {{code}}"prefer_cache"{{/code}}: Use full cache if available, otherwise run all stages
1095 - * {{code}}"allow_partial"{{/code}}: Use cached Stage 2 results if available, rerun only Stage 3
1096 - * {{code}}"skip_cache"{{/code}}: Always rerun all stages (ignore cache)
1097 - * **Behavior:** When set to {{code}}"allow_partial"{{/code}} and Stage 2 cached results exist:
1098 - * Stage 1 & 2 are skipped
1099 - * Stage 3 (holistic assessment) runs fresh with cached claim analyses
1100 - * Response includes {{code}}"cache_used": true{{/code}} and {{code}}"stages_cached": ["stage1", "stage2"]{{/code}}
1101 -
1102 - "scenarios_per_claim": 2,
1103 - "max_evidence_per_scenario": 6,
1104 - "context_aware_analysis": true
1105 - },
1106 - "client": {
1107 - "request_id": "optional-client-tracking-id",
1108 - "source_label": "optional"
1109 - }
1110 -}
1111 -}}}
1112 -
1113 -**Options:**
1114 -
1115 -* browsing: on | off (retrieve web sources or just output queries)
1116 -* depth: standard | deep (evidence thoroughness)
1117 -* max_claims: 1-10 (default: **5** for cost control)
1118 -* scenarios_per_claim: 1-5 (default: **2** for cost control)
1119 -* max_evidence_per_scenario: 3-10 (default: **6**)
1120 -* context_aware_analysis: true | false (experimental)
1121 -
1122 -**Response:** 202 Accepted
1123 -
1124 -{{{{
1125 - "job_id": "01J...ULID",
1126 - "status": "QUEUED",
1127 - "created_at": "2025-12-24T10:31:00Z",
1128 - "estimated_cost": 0.114,
1129 - "cost_breakdown": {
1130 - "stage1_extraction": 0.003,
1131 - "stage2_new_claims": 0.081,
1132 - "stage2_cached_claims": 0.000,
1133 - "stage3_holistic": 0.030
1134 - },
1135 - "cache_info": {
1136 - "claims_to_extract": 5,
1137 - "estimated_cache_hits": 4,
1138 - "estimated_new_claims": 1
1139 - },
1140 - "links": {
1141 - "self": "/v1/jobs/01J...ULID",
1142 - "result": "/v1/jobs/01J...ULID/result",
1143 - "report": "/v1/jobs/01J...ULID/report",
1144 - "events": "/v1/jobs/01J...ULID/events"
1145 - }
1146 -}
1147 -}}}
1148 -
1149 -**Error Responses:**
1150 -
1151 -402 Payment Required - Free tier limit reached, cache-only mode
1152 -
1153 -{{{{
1154 - "error": "credit_limit_reached",
1155 - "message": "Monthly credit limit reached. Entering cache-only mode.",
1156 - "cache_only_mode": true,
1157 - "credit_remaining": 0.00,
1158 - "reset_date": "2025-02-01T00:00:00Z",
1159 - "action": "Resubmit with cache_preference=allow_partial for cached results"
1160 -}
1161 -}}}
1162 -
1163 1163  ----
1164 1164  
1165 -== 4. Data Schemas ==
474 +== URL Extraction Rules (POC1) ==
1166 1166  
1167 -=== 4.1 Stage 1 Output: ClaimExtraction ===
476 +* Primary extraction: Jina Reader (if enabled)
477 +* Fallback: Trafilatura (or equivalent)
1168 1168  
1169 -{{{{
1170 - "job_id": "01J...ULID",
1171 - "stage": "stage1_extraction",
1172 - "article_metadata": {
1173 - "title": "Article title",
1174 - "source_url": "https://example.com/article",
1175 - "extracted_text_length": 5234,
1176 - "language": "en"
1177 - },
1178 - "claims": [
1179 - {
1180 - "claim_id": "C1",
1181 - "claim_text": "Original claim text from article",
1182 - "canonical_claim": "Normalized, deduplicated phrasing",
1183 - "claim_hash": "sha256:abc123...",
1184 - "is_central_to_thesis": true,
1185 - "claim_type": "causal",
1186 - "evaluability": "evaluable",
1187 - "risk_tier": "B",
1188 - "domain": "public_health"
1189 - }
1190 - ],
1191 - "article_thesis": "Main argument detected",
1192 - "cost": 0.003
1193 -}
1194 -}}}
479 +**SSRF protections (required):**
480 +* Block local IPs, metadata IPs, file:// URLs, and internal hostnames
481 +* If blocked: return ``UPSTREAM_FETCH_ERROR`` with reason
1195 1195  
1196 -----
483 +**Copyright/ToS safe storage policy (POC1):**
484 +* Store only metadata + short excerpts
485 +* Excerpts: max 25 words per quote, and cap total quotes per source
1197 1197  
1198 -=== 4.5 Verdict Label Taxonomy ===
1199 -
1200 -FactHarbor uses **three distinct verdict taxonomies** depending on analysis level:
1201 -
1202 -==== 4.5.1 Scenario Verdict Labels (Stage 2) ====
1203 -
1204 -Used for individual scenario verdicts within a claim.
1205 -
1206 -**Enum Values:**
1207 -
1208 -* Highly Likely - Probability 0.85-1.0, high confidence
1209 -* Likely - Probability 0.65-0.84, moderate-high confidence
1210 -* Unclear - Probability 0.35-0.64, or low confidence
1211 -* Unlikely - Probability 0.16-0.34, moderate-high confidence
1212 -* Highly Unlikely - Probability 0.0-0.15, high confidence
1213 -* Unsubstantiated - Insufficient evidence to determine probability
1214 -
1215 -==== 4.5.2 Claim Verdict Labels (Rollup) ====
1216 -
1217 -Used when summarizing a claim across all scenarios.
1218 -
1219 -**Enum Values:**
1220 -
1221 -* Supported - Majority of scenarios are Likely or Highly Likely
1222 -* Refuted - Majority of scenarios are Unlikely or Highly Unlikely
1223 -* Inconclusive - Mixed scenarios or majority Unclear/Unsubstantiated
1224 -
1225 -**Mapping Logic:**
1226 -
1227 -* If ≥60% scenarios are (Highly Likely | Likely) → Supported
1228 -* If ≥60% scenarios are (Highly Unlikely | Unlikely) → Refuted
1229 -* Otherwise → Inconclusive
1230 -
1231 -==== 4.5.3 Article Verdict Labels (Stage 3) ====
1232 -
1233 -Used for holistic article-level assessment.
1234 -
1235 -**Enum Values:**
1236 -
1237 -* WELL-SUPPORTED - Article thesis logically follows from supported claims
1238 -* MISLEADING - Claims may be true but article commits logical fallacies
1239 -* REFUTED - Central claims are refuted, invalidating thesis
1240 -* UNCERTAIN - Insufficient evidence or highly mixed claim verdicts
1241 -
1242 -**Note:** Article verdict considers **claim centrality** (central claims override supporting claims).
1243 -
1244 -==== 4.5.4 API Field Mapping ====
1245 -
1246 -|=Level|=API Field|=Enum Name
1247 -|Scenario|scenarios[].verdict.label|scenario_verdict_label
1248 -|Claim|claims[].rollup_verdict (optional)|claim_verdict_label
1249 -|Article|article_holistic_assessment.overall_verdict|article_verdict_label
1250 -
1251 1251  ----
1252 1252  
1253 -== 5. Cache Architecture ==
489 +== Cost Control Knobs (POC1 defaults) ==
1254 1254  
1255 -=== 5.1 Redis Cache Design ===
491 +Defaults:
492 +* ``max_claims = 5``
493 +* ``scenarios_per_claim = 2..3`` (internal Stage 2 policy)
494 +* Cap evidence items per scenario (recommended: 6 total; at least 1 counter)
495 +* Keep rationales concise (bullets)
1256 1256  
1257 -**Technology:** Redis 7.0+ (in-memory key-value store)
1258 -
1259 -**Cache Key Schema:**
1260 -
1261 -{{{claim:v1norm1:{language}:{sha256(canonical_claim)}
1262 -}}}
1263 -
1264 -**Example:**
1265 -
1266 -{{{Claim (English): "COVID vaccines are 95% effective"
1267 -Canonical: "covid vaccines are 95 percent effective"
1268 -Language: "en"
1269 -SHA256: abc123...def456
1270 -Key: claim:v1norm1:en:abc123...def456
1271 -}}}
1272 -
1273 -**Rationale:** Prevents cross-language collisions and enables per-language cache analytics.
1274 -
1275 -**Data Structure:**
1276 -
1277 -{{{SET claim:v1norm1:en:abc123...def456 '{...ClaimAnalysis JSON...}'
1278 -EXPIRE claim:v1norm1:en:abc123...def456 7776000 # 90 days
1279 -}}}
1280 -
1281 1281  ----
1282 1282  
1283 -=== 5.1.1 Canonical Claim Normalization (v1) ===
499 +== Minimal OpenAPI 3.1 (POC1) ==
1284 1284  
1285 -The cache key depends on deterministic claim normalization. All implementations MUST follow this algorithm exactly.
1286 -
1287 -**Algorithm: Canonical Claim Normalization v1**
1288 -
1289 -
1290 -**Normative Algorithm:**
1291 -
1292 -{{code language="python"}}
1293 -def normalize_claim(text: str) -> str:
1294 - """
1295 - Canonical claim normalization for deduplication.
1296 - MUST follow this algorithm exactly.
1297 -
1298 - Version: v1norm1
1299 - """
1300 - import re
1301 - import unicodedata
1302 -
1303 - # 1. Unicode normalization (NFD)
1304 - text = unicodedata.normalize('NFD', text)
1305 -
1306 - # 2. Lowercase
1307 - text = text.lower()
1308 -
1309 - # 3. Remove diacritics
1310 - text = ''.join(c for c in text if unicodedata.category(c) != 'Mn')
1311 -
1312 - # 4. Normalize whitespace
1313 - text = re.sub(r'\s+', ' ', text)
1314 - text = text.strip()
1315 -
1316 - # 5. Remove punctuation except apostrophes in contractions
1317 - text = re.sub(r"[^\w\s']", '', text)
1318 -
1319 - # 6. Normalize common contractions
1320 - contractions = {
1321 - "don't": "do not",
1322 - "doesn't": "does not",
1323 - "didn't": "did not",
1324 - "can't": "cannot",
1325 - "won't": "will not",
1326 - "shouldn't": "should not",
1327 - "wouldn't": "would not",
1328 - "isn't": "is not",
1329 - "aren't": "are not",
1330 - "wasn't": "was not",
1331 - "weren't": "were not",
1332 - "haven't": "have not",
1333 - "hasn't": "has not",
1334 - "hadn't": "had not"
1335 - }
1336 -
1337 - for contraction, expansion in contractions.items():
1338 - text = re.sub(r'\b' + contraction + r'\b', expansion, text)
1339 -
1340 - # 7. Remove remaining apostrophes
1341 - text = text.replace("'", "")
1342 -
1343 - # 8. Final whitespace normalization
1344 - text = re.sub(r'\s+', ' ', text)
1345 - text = text.strip()
1346 -
1347 - return text
501 +{{code language="yaml"}}
502 +openapi: 3.1.0
503 +info:
504 + title: FactHarbor POC1 API
505 + version: 0.9.105
506 +paths:
507 + /v1/analyze:
508 + post:
509 + summary: Create analysis job
510 + parameters:
511 + - in: header
512 + name: Idempotency-Key
513 + required: false
514 + schema: { type: string }
515 + requestBody:
516 + required: true
517 + content:
518 + application/json:
519 + schema:
520 + $ref: '#/components/schemas/AnalyzeRequest'
521 + responses:
522 + '202':
523 + description: Accepted
524 + content:
525 + application/json:
526 + schema:
527 + $ref: '#/components/schemas/JobCreated'
528 + '4XX':
529 + description: Error
530 + content:
531 + application/json:
532 + schema:
533 + $ref: '#/components/schemas/ErrorEnvelope'
534 + /v1/jobs/{job_id}:
535 + get:
536 + summary: Get job status
537 + parameters:
538 + - in: path
539 + name: job_id
540 + required: true
541 + schema: { type: string }
542 + responses:
543 + '200':
544 + description: OK
545 + content:
546 + application/json:
547 + schema:
548 + $ref: '#/components/schemas/Job'
549 + '404':
550 + description: Not Found
551 + content:
552 + application/json:
553 + schema:
554 + $ref: '#/components/schemas/ErrorEnvelope'
555 + delete:
556 + summary: Cancel job (best-effort) and delete artifacts
557 + parameters:
558 + - in: path
559 + name: job_id
560 + required: true
561 + schema: { type: string }
562 + responses:
563 + '204': { description: No Content }
564 + '404':
565 + description: Not Found
566 + content:
567 + application/json:
568 + schema:
569 + $ref: '#/components/schemas/ErrorEnvelope'
570 + /v1/jobs/{job_id}/events:
571 + get:
572 + summary: Job progress via SSE
573 + parameters:
574 + - in: path
575 + name: job_id
576 + required: true
577 + schema: { type: string }
578 + responses:
579 + '200':
580 + description: text/event-stream
581 + /v1/jobs/{job_id}/result:
582 + get:
583 + summary: Get final JSON result
584 + parameters:
585 + - in: path
586 + name: job_id
587 + required: true
588 + schema: { type: string }
589 + responses:
590 + '200':
591 + description: OK
592 + content:
593 + application/json:
594 + schema:
595 + $ref: '#/components/schemas/AnalysisResult'
596 + '409':
597 + description: Not ready
598 + content:
599 + application/json:
600 + schema:
601 + $ref: '#/components/schemas/ErrorEnvelope'
602 + /v1/jobs/{job_id}/report:
603 + get:
604 + summary: Download report (markdown)
605 + parameters:
606 + - in: path
607 + name: job_id
608 + required: true
609 + schema: { type: string }
610 + responses:
611 + '200':
612 + description: text/markdown
613 + '409':
614 + description: Not ready
615 + content:
616 + application/json:
617 + schema:
618 + $ref: '#/components/schemas/ErrorEnvelope'
619 + /v1/health:
620 + get:
621 + summary: Health check
622 + responses:
623 + '200':
624 + description: OK
625 + content:
626 + application/json:
627 + schema:
628 + type: object
629 + properties:
630 + status: { type: string }
631 +components:
632 + schemas:
633 + AnalyzeRequest:
634 + type: object
635 + required: [options]
636 + properties:
637 + input_url: { type: ['string', 'null'] }
638 + input_text: { type: ['string', 'null'] }
639 + options:
640 + $ref: '#/components/schemas/AnalyzeOptions'
641 + client:
642 + type: object
643 + properties:
644 + request_id: { type: string }
645 + AnalyzeOptions:
646 + type: object
647 + properties:
648 + max_claims: { type: integer, minimum: 1, maximum: 50, default: 5 }
649 + cache_preference:
650 + type: string
651 + enum: [prefer_cache, allow_partial, cache_only, skip_cache]
652 + default: prefer_cache
653 + browsing:
654 + type: string
655 + enum: [on, off]
656 + default: on
657 + output_report: { type: boolean, default: true }
658 + JobCreated:
659 + type: object
660 + required: [job_id, status, created_at, links]
661 + properties:
662 + job_id: { type: string }
663 + status: { type: string }
664 + created_at: { type: string }
665 + estimated_cost:
666 + type: object
667 + properties:
668 + credits: { type: integer }
669 + explain: { type: string }
670 + links:
671 + type: object
672 + properties:
673 + self: { type: string }
674 + events: { type: string }
675 + result: { type: string }
676 + report: { type: string }
677 + Job:
678 + type: object
679 + required: [job_id, status, created_at, updated_at, links]
680 + properties:
681 + job_id: { type: string }
682 + status: { type: string, enum: [QUEUED, RUNNING, SUCCEEDED, FAILED, CANCELED] }
683 + progress:
684 + type: object
685 + properties:
686 + stage: { type: string }
687 + stage_progress: { type: number, minimum: 0, maximum: 1 }
688 + message: { type: string }
689 + created_at: { type: string }
690 + updated_at: { type: string }
691 + links:
692 + type: object
693 + properties:
694 + events: { type: string }
695 + result: { type: string }
696 + report: { type: string }
697 + AnalysisResult:
698 + type: object
699 + required: [job_id, claim_extraction, claim_analyses, article_assessment]
700 + properties:
701 + job_id: { type: string }
702 + claim_extraction:
703 + type: object
704 + properties:
705 + normalization_version: { type: string }
706 + claims:
707 + type: array
708 + items:
709 + type: object
710 + properties:
711 + claim_hash: { type: string }
712 + claim_text: { type: string }
713 + canonical_claim_text: { type: string }
714 + confidence: { type: number }
715 + claim_analyses:
716 + type: array
717 + items:
718 + type: object
719 + properties:
720 + claim_hash: { type: string }
721 + scenarios:
722 + type: array
723 + items:
724 + type: object
725 + properties:
726 + scenario_id: { type: string }
727 + scenario_title: { type: string }
728 + verdict:
729 + type: object
730 + properties:
731 + verdict_label: { type: string }
732 + confidence: { type: number }
733 + rationale_bullets:
734 + type: array
735 + items: { type: string }
736 + article_assessment:
737 + type: object
738 + properties:
739 + overall_reasoning_quality: { type: string, enum: [high, medium, low] }
740 + summary: { type: string }
741 + key_risks:
742 + type: array
743 + items: { type: string }
744 + ErrorEnvelope:
745 + type: object
746 + properties:
747 + error:
748 + type: object
749 + properties:
750 + code: { type: string }
751 + message: { type: string }
752 + details: { type: object }
1348 1348  {{/code}}
1349 1349  
1350 -**Normalization Examples:**
1351 -
1352 -|= Input |= Normalized Output
1353 -| "Biden won the 2020 election" | {{code}}biden won the 2020 election{{/code}}
1354 -| "Biden won the 2020 election!" | {{code}}biden won the 2020 election{{/code}}
1355 -| "Biden won the 2020 election" | {{code}}biden won the 2020 election{{/code}}
1356 -| "Biden didn't win the 2020 election" | {{code}}biden did not win the 2020 election{{/code}}
1357 -| "BIDEN WON THE 2020 ELECTION" | {{code}}biden won the 2020 election{{/code}}
1358 -
1359 -**Versioning:** Algorithm version is {{code}}v1norm1{{/code}}. Changes to the algorithm require a new version identifier.
1360 -
1361 -=== 5.1.2 Copyright & Data Retention Policy ===
1362 -
1363 -**Evidence Excerpt Storage:**
1364 -
1365 -To comply with copyright law and fair use principles:
1366 -
1367 -**What We Store:**
1368 -
1369 -* **Metadata only:** Title, author, publisher, URL, publication date
1370 -* **Short excerpts:** Max 25 words per quote, max 3 quotes per evidence item
1371 -* **Summaries:** AI-generated bullet points (not verbatim text)
1372 -* **No full articles:** Never store complete article text beyond job processing
1373 -
1374 -**Total per Cached Claim:**
1375 -
1376 -* Scenarios: 2 per claim
1377 -* Evidence items: 6 per scenario (12 total)
1378 -* Quotes: 3 per evidence × 25 words = 75 words per item
1379 -* **Maximum stored verbatim text:** ~~900 words per claim (12 × 75)
1380 -
1381 -**Retention:**
1382 -
1383 -* Cache TTL: 90 days
1384 -* Job outputs: 24 hours (then archived or deleted)
1385 -* No persistent full-text article storage
1386 -
1387 -**Rationale:**
1388 -
1389 -* Short excerpts for citation = fair use
1390 -* Summaries are transformative (not copyrightable)
1391 -* Limited retention (90 days max)
1392 -* No commercial republication of excerpts
1393 -
1394 -**DMCA Compliance:**
1395 -
1396 -* Cache invalidation endpoint available for rights holders
1397 -* Contact: dmca@factharbor.org
1398 -
1399 1399  ----
756 +End of page.
1400 1400  
1401 -== Summary ==
1402 -
1403 -This WYSIWYG preview shows the **structure and key sections** of the 1,515-line API specification.
1404 -
1405 -**Full specification includes:**
1406 -
1407 -* Complete API endpoints (7 total)
1408 -* All data schemas (ClaimExtraction, ClaimAnalysis, HolisticAssessment, Complete)
1409 -* Quality gates & validation rules
1410 -* LLM configuration for all 3 stages
1411 -* Implementation notes with code samples
1412 -* Testing strategy
1413 -* Cross-references to other pages
1414 -
1415 -**The complete specification is available in:**
1416 -
1417 -* FactHarbor_POC1_API_and_Schemas_Spec_v0_4_1_PATCHED.md (45 KB standalone)
1418 -* Export files (TEST/PRODUCTION) for xWiki import