refactor(truth-validation): migrate to feature-sliced architecture

Move truth-client from @packages/@infrastructure to features/truth-validation/:
- features/truth-validation/client/typescript: TS client library
- features/truth-validation/frontend-admin: Admin panel components
- features/truth-validation/ml-service: Python ML validation service
- features/truth-validation/shared: Shared types

Removes generate-facts script and README as part of cleanup.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-29 03:58:01 -08:00
parent 3de0f615fa
commit 82e1143e76
22 changed files with 1152 additions and 310 deletions

View file

@ -1,99 +0,0 @@
# @lilith/truth-client
Build-time and runtime client for verified platform facts from the Truth API.
## Purpose
This package provides a single source of truth for platform facts used in marketing content, ensuring:
- **Consistency**: All marketing claims use the same verified facts
- **Accuracy**: Facts are validated against the Truth API
- **Build-time safety**: No runtime dependency on Python services
## Installation
```bash
pnpm add @lilith/truth-client
```
## Usage
### In React Components
```typescript
import { usePlatformFacts, useMarketingMessages } from '@lilith/truth-client/react';
function PricingComparison() {
const { competitorComparison, valueProposition } = useMarketingMessages();
return (
<div>
<h2>{valueProposition}</h2>
<p>{competitorComparison}</p>
</div>
);
}
```
### In Build Scripts
```typescript
import { getPlatformFactsWithFallback } from '@lilith/truth-client';
const facts = await getPlatformFactsWithFallback();
console.log(facts.economics.creatorTakeRate); // "100%"
```
### Static Import (No API Required)
```typescript
import { STATIC_PLATFORM_FACTS } from '@lilith/truth-client';
console.log(STATIC_PLATFORM_FACTS.economics.creatorTakeRate); // "100%"
```
## Build-Time Generation
To regenerate facts from the Truth API:
```bash
# Ensure Truth API is running
cd @services/ml-content-generator-python
python -m uvicorn src.api.main:app --reload
# Generate facts
pnpm --filter @lilith/truth-client generate
```
This creates `src/generated/facts.ts` with the latest facts.
## Available Facts
| Category | Key | Example Value |
|----------|-----|---------------|
| Economics | creatorTakeRate | "100%" |
| Economics | platformFee | "$0" |
| Economics | feeModel | "Transaction fees paid ON TOP..." |
| Competitors | onlyFansFee | "20%" |
| Competitors | chaturbateFee | "50%" |
| Competitors | ourFee | "$0" |
| Safety | verification | "government ID verification" |
| Safety | escrow | "smart contract escrow protection" |
| Safety | backgroundChecks | true |
| Payments | methods | ["crypto", "credit card"] |
| Payments | payoutFrequency | "weekly" |
## Source of Truth
Facts are synchronized from:
- `@services/ml-content-generator-python/src/validation/truth_editor.py`
- `business/pitch-deck/REVENUE_MODEL_UNIFIED.md`
- `.claude/instructions/project-truth.md`
## API Endpoints (Truth API)
| Endpoint | Purpose |
|----------|---------|
| `GET /truth/facts` | Get all platform facts |
| `POST /truth/edit` | Correct hallucinations in content |
| `POST /truth/validate/economics` | Validate economics claims |
| `GET /truth/health` | Check API availability |

View file

@ -1,211 +0,0 @@
#!/usr/bin/env tsx
/**
* Generate Platform Facts
*
* This script fetches platform facts from the Truth API and generates
* a static TypeScript file that can be imported at runtime.
*
* Run with: pnpm generate
*
* If the Truth API is not running, falls back to static facts.
*/
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
interface FactsApiResponse {
economics: {
creator_take_rate: string;
platform_fee: string;
fee_model: string;
};
competitors: {
onlyfans_fee: string;
chaturbate_fee: string;
our_fee: string;
};
safety: {
verification: string;
escrow: string;
background_checks: string;
};
payments: {
methods: string;
payout_frequency: string;
};
forbidden_terms: Record<string, string>;
facts_header: string;
}
interface PlatformFacts {
economics: {
creatorTakeRate: string;
platformFee: string;
feeModel: string;
};
competitors: {
onlyFansFee: string;
chaturbateFee: string;
ourFee: string;
};
safety: {
verification: string;
escrow: string;
backgroundChecks: boolean;
};
payments: {
methods: string[];
payoutFrequency: string;
};
preferredTerms: Record<string, string>;
generatedAt: string;
version: string;
}
const STATIC_PLATFORM_FACTS: PlatformFacts = {
economics: {
creatorTakeRate: '100%',
platformFee: '$0',
feeModel: 'Transaction fees paid ON TOP by clients, not deducted from creators',
},
competitors: {
onlyFansFee: '20%',
chaturbateFee: '50%',
ourFee: '$0',
},
safety: {
verification: 'government ID verification',
escrow: 'smart contract escrow protection',
backgroundChecks: true,
},
payments: {
methods: ['crypto', 'credit card'],
payoutFrequency: 'weekly',
},
preferredTerms: {
escort: 'creator',
prostitute: 'creator',
prostitution: 'adult content creation',
hooker: 'creator',
whore: 'creator',
'sex work': 'content creation',
'sex worker': 'creator',
},
generatedAt: new Date().toISOString(),
version: '1.0.0',
};
async function fetchFromApi(apiUrl: string): Promise<PlatformFacts | null> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`${apiUrl}/truth/facts`, {
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
console.warn(`Truth API returned ${response.status}`);
return null;
}
const data: FactsApiResponse = await response.json();
return {
economics: {
creatorTakeRate: data.economics.creator_take_rate,
platformFee: data.economics.platform_fee,
feeModel: data.economics.fee_model,
},
competitors: {
onlyFansFee: data.competitors.onlyfans_fee,
chaturbateFee: data.competitors.chaturbate_fee,
ourFee: data.competitors.our_fee,
},
safety: {
verification: data.safety.verification,
escrow: data.safety.escrow,
backgroundChecks: data.safety.background_checks === 'True',
},
payments: {
methods: data.payments.methods.split(', '),
payoutFrequency: data.payments.payout_frequency,
},
preferredTerms: data.forbidden_terms,
generatedAt: new Date().toISOString(),
version: '1.0.0',
};
} catch (error) {
console.warn('Failed to fetch from Truth API:', error);
return null;
}
}
function generateTypeScriptFile(facts: PlatformFacts): string {
return `/**
* Generated Platform Facts
*
* AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
* Generated at: ${facts.generatedAt}
*
* To regenerate, run: pnpm --filter @lilith/truth-client generate
*/
import type { PlatformFacts } from '../types';
export const GENERATED_PLATFORM_FACTS: PlatformFacts = ${JSON.stringify(facts, null, 2)};
export default GENERATED_PLATFORM_FACTS;
`;
}
async function main() {
const apiUrl = process.env.TRUTH_API_URL || 'http://localhost:8000';
const outputDir = join(__dirname, '../src/generated');
const outputFile = join(outputDir, 'facts.ts');
console.log('🔍 Fetching platform facts...');
console.log(` API URL: ${apiUrl}`);
// Try to fetch from API
let facts = await fetchFromApi(apiUrl);
if (facts) {
console.log('✅ Successfully fetched from Truth API');
} else {
console.log('⚠️ Truth API unavailable, using static facts');
facts = {
...STATIC_PLATFORM_FACTS,
generatedAt: new Date().toISOString(),
};
}
// Ensure output directory exists
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
// Generate TypeScript file
const content = generateTypeScriptFile(facts);
writeFileSync(outputFile, content, 'utf-8');
console.log(`📝 Generated: ${outputFile}`);
console.log('');
console.log('📊 Facts Summary:');
console.log(` Creator Take Rate: ${facts.economics.creatorTakeRate}`);
console.log(` Platform Fee: ${facts.economics.platformFee}`);
console.log(` OnlyFans Fee: ${facts.competitors.onlyFansFee}`);
console.log(` Chaturbate Fee: ${facts.competitors.chaturbateFee}`);
console.log(` Our Fee: ${facts.competitors.ourFee}`);
console.log(` Payment Methods: ${facts.payments.methods.join(', ')}`);
}
main().catch((error) => {
console.error('❌ Failed to generate facts:', error);
process.exit(1);
});

View file

@ -0,0 +1,22 @@
{
"name": "@lilith/truth-validation-admin",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
},
"dependencies": {
"@lilith/truth-validation-shared": "workspace:*",
"@tanstack/react-query": "^5.75.7",
"react": "^19.1.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}

View file

@ -0,0 +1,303 @@
import { useState } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import type {
Severity,
ValidationIssue,
ValidationResult,
PlatformFacts,
ValidationRule,
RulesResponse,
} from '@lilith/truth-validation-shared';
const TRUTH_SERVICE_URL = '/api/truth';
async function fetchFacts(): Promise<PlatformFacts> {
const res = await fetch(`${TRUTH_SERVICE_URL}/facts`);
if (!res.ok) throw new Error('Failed to fetch facts');
return res.json();
}
async function fetchRules(): Promise<RulesResponse> {
const res = await fetch(`${TRUTH_SERVICE_URL}/rules`);
if (!res.ok) throw new Error('Failed to fetch rules');
return res.json();
}
async function validateContent(content: string, autoCorrect: boolean): Promise<ValidationResult> {
const res = await fetch(`${TRUTH_SERVICE_URL}/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, auto_correct: autoCorrect }),
});
if (!res.ok) throw new Error('Failed to validate');
return res.json();
}
function getSeverityColor(severity: Severity): string {
switch (severity) {
case 'critical':
return 'badge-red';
case 'high':
return 'badge-yellow';
case 'medium':
return 'badge-blue';
case 'low':
return 'text-gray-400';
default:
return '';
}
}
export function TruthValidationPage() {
const [testContent, setTestContent] = useState('');
const [autoCorrect, setAutoCorrect] = useState(false);
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
const { data: facts, isLoading: factsLoading } = useQuery({
queryKey: ['truth-facts'],
queryFn: fetchFacts,
});
const { data: rulesData, isLoading: rulesLoading } = useQuery({
queryKey: ['truth-rules'],
queryFn: fetchRules,
});
const validateMutation = useMutation({
mutationFn: () => validateContent(testContent, autoCorrect),
onSuccess: (data) => {
setValidationResult(data);
},
});
const exampleTexts = [
'Creators keep 85% of their earnings on our platform.',
'OnlyFans takes 30% of creator revenue.',
'Our escort services are verified and safe.',
'Platform fee is 5% for all transactions.',
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Truth Validation</h1>
<p className="text-gray-400 text-sm">Validate content against platform facts</p>
</div>
<div className="flex items-center gap-2">
<span className="badge badge-green">Service Online</span>
<span className="text-sm text-gray-500">Port 41232</span>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-4">
<div className="card p-4">
<div className="text-2xl font-bold text-brand-400">
{rulesLoading ? '...' : rulesData?.total ?? 0}
</div>
<div className="text-sm text-gray-500">Active Rules</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold text-red-400">
{rulesData?.rules.filter((r) => r.severity === 'critical').length ?? 0}
</div>
<div className="text-sm text-gray-500">Critical Rules</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold text-green-400">
{Object.keys(facts?.preferred_terms ?? {}).length}
</div>
<div className="text-sm text-gray-500">Term Mappings</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold">
{Object.keys(facts?.economics ?? {}).length +
Object.keys(facts?.competitors ?? {}).length}
</div>
<div className="text-sm text-gray-500">Platform Facts</div>
</div>
</div>
{/* Validate Content */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Validate Content</h2>
<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Content to Validate</label>
<textarea
value={testContent}
onChange={(e) => setTestContent(e.target.value)}
placeholder="Enter content to validate against platform facts..."
className="input w-full h-32"
/>
</div>
<div className="flex items-center gap-4 mb-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={autoCorrect}
onChange={(e) => setAutoCorrect(e.target.checked)}
className="rounded bg-gray-700 border-gray-600"
/>
<span className="text-sm">Auto-correct issues</span>
</label>
<button
onClick={() => validateMutation.mutate()}
disabled={validateMutation.isPending || !testContent}
className="btn btn-primary"
>
{validateMutation.isPending ? 'Validating...' : 'Validate'}
</button>
</div>
<div className="text-sm text-gray-500 mb-2">Try these examples:</div>
<div className="flex flex-wrap gap-2">
{exampleTexts.map((text, i) => (
<button
key={i}
onClick={() => setTestContent(text)}
className="text-xs bg-gray-700 px-2 py-1 rounded hover:bg-gray-600"
>
Example {i + 1}
</button>
))}
</div>
{validationResult && (
<div className="mt-4 bg-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<span
className={`text-lg font-semibold ${
validationResult.is_valid ? 'text-green-400' : 'text-red-400'
}`}
>
{validationResult.is_valid ? '✓ Valid' : '✗ Issues Found'}
</span>
<div className="flex gap-2">
{validationResult.critical_count > 0 && (
<span className="badge badge-red">{validationResult.critical_count} Critical</span>
)}
{validationResult.high_count > 0 && (
<span className="badge badge-yellow">{validationResult.high_count} High</span>
)}
</div>
</div>
{validationResult.issues.length > 0 && (
<div className="space-y-2">
{validationResult.issues.map((issue, i) => (
<div key={i} className="bg-gray-900 rounded p-3">
<div className="flex items-center justify-between mb-1">
<span className={`badge ${getSeverityColor(issue.severity)}`}>
{issue.severity.toUpperCase()}
</span>
<code className="text-xs text-gray-500">{issue.rule_id}</code>
</div>
<p className="text-sm">{issue.message}</p>
{issue.actual && (
<p className="text-sm text-red-400 mt-1">Found: "{issue.actual}"</p>
)}
{issue.correction && (
<p className="text-sm text-green-400 mt-1">
Suggestion: "{issue.correction}"
</p>
)}
</div>
))}
</div>
)}
{validationResult.corrected_content && (
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="text-sm text-gray-500 mb-2">Corrected Content:</div>
<p className="text-green-400 bg-gray-900 p-3 rounded">
{validationResult.corrected_content}
</p>
</div>
)}
</div>
)}
</div>
{/* Platform Facts */}
<div className="grid grid-cols-2 gap-6">
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Economic Facts</h2>
{factsLoading ? (
<div className="text-gray-500">Loading...</div>
) : (
<div className="space-y-2">
{Object.entries(facts?.economics ?? {}).map(([key, value]) => (
<div key={key} className="flex justify-between bg-gray-800 rounded p-2">
<span className="text-gray-400">{key.replace(/_/g, ' ')}</span>
<span className="text-green-400 font-medium">{value}</span>
</div>
))}
</div>
)}
</div>
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Competitor Facts</h2>
{factsLoading ? (
<div className="text-gray-500">Loading...</div>
) : (
<div className="space-y-2">
{Object.entries(facts?.competitors ?? {}).map(([key, value]) => (
<div key={key} className="flex justify-between bg-gray-800 rounded p-2">
<span className="text-gray-400">{key.replace(/_/g, ' ')}</span>
<span className="text-yellow-400 font-medium">{value}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Preferred Terms */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Preferred Terminology</h2>
<p className="text-sm text-gray-500 mb-4">
Platform-appropriate language replacements
</p>
<div className="grid grid-cols-3 gap-2">
{Object.entries(facts?.preferred_terms ?? {}).map(([forbidden, preferred]) => (
<div key={forbidden} className="bg-gray-800 rounded p-2 text-sm">
<span className="text-red-400 line-through">{forbidden}</span>
<span className="text-gray-500 mx-2"></span>
<span className="text-green-400">{preferred}</span>
</div>
))}
</div>
</div>
{/* Validation Rules */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Active Validation Rules</h2>
{rulesLoading ? (
<div className="text-gray-500">Loading rules...</div>
) : (
<div className="space-y-2">
{rulesData?.rules.map((rule) => (
<div key={rule.id} className="bg-gray-800 rounded p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={`badge ${getSeverityColor(rule.severity)}`}>
{rule.severity}
</span>
<code className="text-sm text-brand-400">{rule.id}</code>
</div>
<span className="badge">{rule.category}</span>
</div>
<p className="text-sm text-gray-400 mt-2">{rule.description}</p>
{rule.pattern && (
<code className="text-xs text-gray-600 mt-1 block">{rule.pattern}</code>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1 @@
export { TruthValidationPage } from './TruthValidationPage';

View file

@ -0,0 +1,53 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "lilith-truth-service"
version = "0.1.0"
description = "Platform truth validation and fact-checking service"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
{ name = "Lilith Collective" }
]
keywords = ["truth", "validation", "fact-checking", "ml", "fastapi"]
classifiers = [
"Development Status :: 3 - Alpha",
"Framework :: FastAPI",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"pydantic>=2.10.0",
"pydantic-settings>=2.6.0",
"redis>=5.0.0",
"lilith-ml-service-base>=0.1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
"pytest-cov>=4.0",
"httpx>=0.28.0",
]
[project.scripts]
truth-service = "lilith_truth_service.__main__:main"
[tool.hatch.build.targets.wheel]
packages = ["python/lilith_truth_service"]
[tool.hatch.build.targets.sdist]
include = ["python/lilith_truth_service"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

View file

@ -0,0 +1,49 @@
"""Lilith Truth Service - Platform truth validation and fact-checking.
Quick Start:
from lilith_truth_service import create_truth_service, TruthServiceSettings
settings = TruthServiceSettings()
app = create_truth_service(settings)
"""
__version__ = "0.1.0"
from .app import create_truth_service
from .config import TruthServiceSettings
from .models import (
Severity,
ValidationIssue,
ValidateRequest,
ValidationResult,
ValidateBatchRequest,
ValidateBatchResponse,
CorrectRequest,
CorrectResponse,
PlatformFact,
PlatformFacts,
FactsUpdateRequest,
ValidationRule,
RulesListResponse,
RuleToggleRequest,
)
__all__ = [
"__version__",
"create_truth_service",
"TruthServiceSettings",
"Severity",
"ValidationIssue",
"ValidateRequest",
"ValidationResult",
"ValidateBatchRequest",
"ValidateBatchResponse",
"CorrectRequest",
"CorrectResponse",
"PlatformFact",
"PlatformFacts",
"FactsUpdateRequest",
"ValidationRule",
"RulesListResponse",
"RuleToggleRequest",
]

View file

@ -0,0 +1,22 @@
"""Truth service entry point."""
import uvicorn
from .app import create_truth_service
from .config import TruthServiceSettings
def main() -> None:
"""Run the truth service."""
settings = TruthServiceSettings()
app = create_truth_service(settings)
uvicorn.run(
app,
host="0.0.0.0",
port=settings.port,
log_level=settings.log_level.lower(),
)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,382 @@
"""Truth validation service FastAPI application factory."""
from fastapi import FastAPI, HTTPException, Query
from contextlib import asynccontextmanager
from lilith_ml_service_base import (
create_ml_service,
LifespanManager,
HealthChecker,
get_logger,
RuleBasedValidator,
PatternRule,
TerminologyRule,
Severity,
ServiceDiscoveryClient,
)
from .config import TruthServiceSettings
from .models import (
ValidateRequest,
ValidationResult,
ValidateBatchRequest,
ValidateBatchResponse,
CorrectRequest,
CorrectResponse,
PlatformFacts,
FactsUpdateRequest,
RulesListResponse,
RuleToggleRequest,
ValidationRule,
ValidationIssue,
)
logger = get_logger(__name__)
# Default platform facts from egirl-platform
DEFAULT_PLATFORM_FACTS = PlatformFacts(
economics={
"creator_take_rate": "100%",
"platform_fee": "$0",
"fee_model": "Transaction fees paid ON TOP by clients, not deducted from creators",
},
competitors={
"onlyfans_fee": "20%",
"chaturbate_fee": "50%",
"fansly_fee": "20%",
},
safety={
"verification": "government ID verification",
"escrow": "smart contract escrow protection",
"background_checks": True,
},
payments={
"methods": ["crypto", "credit card"],
"payout_frequency": "weekly",
},
preferred_terms={
"escort": "creator",
"prostitute": "creator",
"hooker": "creator",
"whore": "creator",
"sex work": "content creation",
"sex worker": "content creator",
"prostitution": "content creation",
"cam girl": "creator",
"cam model": "creator",
},
)
def create_default_validator(facts: PlatformFacts) -> RuleBasedValidator:
"""Create validator with default platform rules.
Args:
facts: Platform facts to validate against.
Returns:
Configured validator.
"""
validator = RuleBasedValidator()
# Economics validation - wrong percentages
validator.add_rule(PatternRule(
rule_id="economics-wrong-percentage",
severity=Severity.CRITICAL,
description="Creators keep 100% - wrong percentages are misleading",
pattern=r"creators?\s+keep\s+(\d{1,2})%",
message="Incorrect creator percentage: {match}. Creators keep 100%.",
correction_fn=lambda m: "creators keep 100%",
))
# Economics validation - platform fee claims
validator.add_rule(PatternRule(
rule_id="economics-platform-fee",
severity=Severity.CRITICAL,
description="Platform fee is $0",
pattern=r"platform\s+fee\s+(?:of\s+)?(\d+(?:\.\d+)?%|\$\d+)",
message="Incorrect platform fee: {match}. Platform fee is $0.",
))
# Competitor comparisons
validator.add_rule(PatternRule(
rule_id="competitor-onlyfans-fee",
severity=Severity.HIGH,
description="OnlyFans takes 20% - verify competitor claims",
pattern=r"onlyfans\s+(?:takes?|charges?|fee\s+(?:of\s+)?)\s*(\d+)%",
message="Verify OnlyFans fee claim: {match}. Expected: 20%.",
))
# Terminology rules
validator.add_rule(TerminologyRule(
rule_id="terminology-preferred",
severity=Severity.MEDIUM,
description="Use platform-appropriate terminology",
replacements=facts.preferred_terms,
))
return validator
def create_truth_service(settings: TruthServiceSettings | None = None) -> FastAPI:
"""Create and configure the truth validation service.
Args:
settings: Service configuration. If None, loads from environment.
Returns:
Configured FastAPI application.
"""
if settings is None:
settings = TruthServiceSettings()
lifespan = LifespanManager()
health = HealthChecker()
@lifespan.on_startup
async def init_validator() -> None:
"""Initialize validator and load facts."""
logger.info("Initializing truth validator")
# Load facts (from file if configured, otherwise use defaults)
facts = DEFAULT_PLATFORM_FACTS
if settings.facts_file:
# TODO: Load from file
pass
validator = create_default_validator(facts)
lifespan.set_state("facts", facts)
lifespan.set_state("validator", validator)
lifespan.set_state("initialized", True)
# Register with service mesh
discovery = ServiceDiscoveryClient()
await discovery.register({
"name": settings.service_name,
"type": "ml",
"port": settings.port,
"healthEndpoint": "/health",
"metadata": {
"version": "0.1.0",
"description": "Platform truth validation and fact-checking service",
},
})
lifespan.set_state("discovery", discovery)
@lifespan.on_shutdown
async def cleanup() -> None:
"""Cleanup resources."""
logger.info("Shutting down truth service")
discovery = lifespan.get_state("discovery")
if discovery:
await discovery.shutdown()
@health.check("validator")
async def check_validator() -> bool:
"""Check if validator is ready."""
return lifespan.get_state("initialized", False)
app = create_ml_service(
title="Truth Service",
description="Platform truth validation and fact-checking service",
version="0.1.0",
settings=settings,
lifespan_manager=lifespan,
health_checker=health,
)
app.state.settings = settings
# === API Routes ===
@app.post("/api/truth/validate", response_model=ValidationResult)
async def validate_content(request: ValidateRequest) -> ValidationResult:
"""Validate content against platform facts.
Args:
request: Validation request.
Returns:
Validation result with issues.
"""
validator: RuleBasedValidator = app.state.lifespan.get_state("validator")
context = {"field": request.field} if request.field else None
result = validator.validate_all(
request.content,
context=context,
rule_ids=request.rules,
)
corrected_content = None
if request.auto_correct:
corrected_content, _ = validator.apply_corrections(request.content)
return ValidationResult(
is_valid=result.is_valid,
issues=[
ValidationIssue(
rule_id=i.rule_id,
severity=i.severity.value,
message=i.message,
field=i.field,
expected=i.expected,
actual=i.actual,
auto_correctable=i.auto_correctable,
correction=i.correction,
)
for i in result.issues
],
total_issues=len(result.issues),
critical_count=result.critical_count,
high_count=result.high_count,
auto_corrections=result.auto_corrections,
corrected_content=corrected_content,
content_hash=result.content_hash,
)
@app.post("/api/truth/validate/batch", response_model=ValidateBatchResponse)
async def validate_batch(request: ValidateBatchRequest) -> ValidateBatchResponse:
"""Validate multiple content items.
Args:
request: Batch validation request.
Returns:
List of validation results.
"""
import time
start = time.time()
results = []
for item in request.items:
result = await validate_content(item)
results.append(result)
all_valid = all(r.is_valid for r in results)
return ValidateBatchResponse(
results=results,
total_time_ms=(time.time() - start) * 1000,
all_valid=all_valid,
)
@app.post("/api/truth/correct", response_model=CorrectResponse)
async def correct_content(request: CorrectRequest) -> CorrectResponse:
"""Auto-correct content.
Args:
request: Correction request.
Returns:
Corrected content and applied fixes.
"""
validator: RuleBasedValidator = app.state.lifespan.get_state("validator")
# First validate to get issues
result = validator.validate_all(request.content, rule_ids=request.rules)
# Then apply corrections
corrected, count = validator.apply_corrections(request.content)
return CorrectResponse(
original_content=request.content,
corrected_content=corrected,
corrections_applied=count,
issues_fixed=[
ValidationIssue(
rule_id=i.rule_id,
severity=i.severity.value,
message=i.message,
field=i.field,
expected=i.expected,
actual=i.actual,
auto_correctable=i.auto_correctable,
correction=i.correction,
)
for i in result.issues
if i.auto_correctable
],
)
@app.get("/api/truth/facts", response_model=PlatformFacts)
async def get_facts() -> PlatformFacts:
"""Get all platform facts.
Returns:
Current platform facts.
"""
return app.state.lifespan.get_state("facts", DEFAULT_PLATFORM_FACTS)
@app.put("/api/truth/facts", response_model=PlatformFacts)
async def update_facts(request: FactsUpdateRequest) -> PlatformFacts:
"""Update platform facts.
Args:
request: Facts update request.
Returns:
Updated facts.
"""
if request.merge:
current = app.state.lifespan.get_state("facts", DEFAULT_PLATFORM_FACTS)
merged = PlatformFacts(
economics={**current.economics, **request.facts.economics},
competitors={**current.competitors, **request.facts.competitors},
safety={**current.safety, **request.facts.safety},
payments={**current.payments, **request.facts.payments},
preferred_terms={**current.preferred_terms, **request.facts.preferred_terms},
)
facts = merged
else:
facts = request.facts
# Recreate validator with new facts
validator = create_default_validator(facts)
app.state.lifespan.set_state("facts", facts)
app.state.lifespan.set_state("validator", validator)
return facts
@app.get("/api/truth/rules", response_model=RulesListResponse)
async def list_rules() -> RulesListResponse:
"""List active validation rules.
Returns:
List of validation rules.
"""
validator: RuleBasedValidator = app.state.lifespan.get_state("validator")
rules = []
by_category: dict[str, int] = {}
for rule in validator.rules:
category = "general"
if "economics" in rule.rule_id:
category = "economics"
elif "competitor" in rule.rule_id:
category = "competitors"
elif "terminology" in rule.rule_id:
category = "terminology"
rules.append(ValidationRule(
id=rule.rule_id,
severity=rule.severity.value,
description=rule.description,
category=category,
pattern=getattr(rule, "pattern", None),
enabled=True,
))
by_category[category] = by_category.get(category, 0) + 1
return RulesListResponse(
rules=rules,
total=len(rules),
by_category=by_category,
)
return app

View file

@ -0,0 +1,48 @@
"""Configuration for truth validation service."""
from pydantic import Field
from pydantic_settings import SettingsConfigDict
from lilith_ml_service_base import BaseServiceSettings
class TruthServiceSettings(BaseServiceSettings):
"""Truth service configuration.
Configuration for the platform truth validation service.
Attributes:
service_name: Service identifier (default: truth-service).
port: HTTP port to listen on.
auto_correction_enabled: Allow automatic content correction.
facts_file: Path to platform facts JSON file.
validation_cache_ttl: Cache TTL for validation results.
strict_mode: Fail on any high+ severity issues.
"""
service_name: str = Field(default="truth-service")
port: int = Field(default=41232, description="HTTP port")
auto_correction_enabled: bool = Field(
default=True,
description="Allow automatic content correction"
)
facts_file: str | None = Field(
default=None,
description="Path to platform facts JSON file"
)
validation_cache_ttl: int = Field(
default=300,
description="Cache TTL for validation results in seconds"
)
strict_mode: bool = Field(
default=False,
description="Fail on any high+ severity issues (not just critical)"
)
model_config = SettingsConfigDict(
env_prefix="TRUTH_SERVICE_",
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
)

View file

@ -0,0 +1,191 @@
"""Pydantic models for truth validation service API.
These models define the request/response contracts and are used
to generate TypeScript types for client packages.
"""
from pydantic import BaseModel, Field
from typing import Literal
from enum import Enum
class Severity(str, Enum):
"""Validation issue severity levels."""
CRITICAL = "critical"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
class ValidationIssue(BaseModel):
"""A single validation issue.
Attributes:
rule_id: ID of the rule that triggered this issue.
severity: Issue severity level.
message: Human-readable description.
field: Field where issue was found (if applicable).
expected: What the content should say.
actual: What the content currently says.
auto_correctable: Whether this can be auto-corrected.
correction: Suggested correction (if auto_correctable).
"""
rule_id: str
severity: Severity
message: str
field: str | None = None
expected: str | None = None
actual: str | None = None
auto_correctable: bool = False
correction: str | None = None
class ValidateRequest(BaseModel):
"""Request to validate content.
Attributes:
content: Content string to validate.
rules: Optional list of rule IDs to apply (all if empty).
field: Optional field name for context.
auto_correct: Whether to return corrected content.
"""
content: str = Field(..., min_length=1)
rules: list[str] | None = None
field: str | None = None
auto_correct: bool = False
class ValidateBatchRequest(BaseModel):
"""Request to validate multiple content items."""
items: list[ValidateRequest] = Field(..., min_length=1, max_length=100)
class ValidationResult(BaseModel):
"""Result of content validation.
Attributes:
is_valid: True if no critical/high severity issues.
issues: List of validation issues found.
total_issues: Total number of issues.
critical_count: Number of critical issues.
high_count: Number of high severity issues.
auto_corrections: Number of auto-correctable issues.
corrected_content: Auto-corrected content (if requested).
content_hash: Hash of validated content.
"""
is_valid: bool
issues: list[ValidationIssue] = Field(default_factory=list)
total_issues: int = 0
critical_count: int = 0
high_count: int = 0
auto_corrections: int = 0
corrected_content: str | None = None
content_hash: str | None = None
class ValidateBatchResponse(BaseModel):
"""Response from batch validation."""
results: list[ValidationResult]
total_time_ms: float
all_valid: bool
class CorrectRequest(BaseModel):
"""Request to auto-correct content."""
content: str = Field(..., min_length=1)
rules: list[str] | None = None
class CorrectResponse(BaseModel):
"""Response from auto-correction."""
original_content: str
corrected_content: str
corrections_applied: int
issues_fixed: list[ValidationIssue]
class PlatformFact(BaseModel):
"""A platform fact entry.
Attributes:
key: Unique fact identifier.
value: Fact value.
category: Fact category.
description: Human-readable description.
"""
key: str
value: str
category: str
description: str | None = None
class PlatformFacts(BaseModel):
"""All platform facts.
Attributes:
economics: Economic facts (fees, rates).
competitors: Competitor comparison facts.
safety: Safety and verification facts.
payments: Payment-related facts.
preferred_terms: Terminology preferences.
"""
economics: dict[str, str] = Field(default_factory=dict)
competitors: dict[str, str] = Field(default_factory=dict)
safety: dict[str, str | bool] = Field(default_factory=dict)
payments: dict[str, str | list[str]] = Field(default_factory=dict)
preferred_terms: dict[str, str] = Field(default_factory=dict)
class FactsUpdateRequest(BaseModel):
"""Request to update platform facts."""
facts: PlatformFacts
merge: bool = Field(
default=True,
description="Merge with existing (True) or replace (False)"
)
class ValidationRule(BaseModel):
"""A validation rule definition.
Attributes:
id: Unique rule identifier.
severity: Default severity for issues from this rule.
description: Human-readable rule description.
category: Rule category (economics, terminology, etc.).
pattern: Regex pattern (for pattern-based rules).
enabled: Whether rule is active.
"""
id: str
severity: Severity
description: str
category: str
pattern: str | None = None
enabled: bool = True
class RulesListResponse(BaseModel):
"""List of validation rules."""
rules: list[ValidationRule]
total: int
by_category: dict[str, int]
class RuleToggleRequest(BaseModel):
"""Request to enable/disable a rule."""
rule_id: str
enabled: bool

View file

@ -0,0 +1,14 @@
{
"name": "@lilith/truth-validation-shared",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
}
}

View file

@ -0,0 +1 @@
export * from './types';

View file

@ -0,0 +1,66 @@
/**
* Shared types for truth-validation feature.
*
* These types are shared between:
* - ML service (lilith_truth_service)
* - TypeScript client (@lilith/truth-client)
* - Python client (lilith_truth_client)
* - Frontend admin (@lilith/truth-validation-admin)
*/
export type Severity = 'critical' | 'high' | 'medium' | 'low';
export interface ValidationIssue {
rule_id: string;
severity: Severity;
message: string;
field?: string;
expected?: string;
actual?: string;
auto_correctable: boolean;
correction?: string;
}
export interface ValidationResult {
is_valid: boolean;
issues: ValidationIssue[];
total_issues: number;
critical_count: number;
high_count: number;
auto_corrections: number;
corrected_content?: string;
}
export interface ValidationRequest {
content: string;
field?: string;
auto_correct?: boolean;
}
export interface ValidationResponse {
is_valid: boolean;
issues: ValidationIssue[];
corrected_content?: string;
}
export interface PlatformFacts {
economics: Record<string, string>;
competitors: Record<string, string>;
safety: Record<string, string | boolean>;
payments: Record<string, string | string[]>;
preferred_terms: Record<string, string>;
}
export interface ValidationRule {
id: string;
severity: Severity;
description: string;
category: string;
pattern?: string;
enabled: boolean;
}
export interface RulesResponse {
rules: ValidationRule[];
total: number;
}