Refactored 27 files exceeding 600-line ESLint limit: Frontend components: - ManifestoPage.tsx: 1665 → 114 lines (split into 7 manifesto modules) - BrowseCreatorsPage.tsx: 1239 → 259 lines (hooks, components, styles) - AgreementForm.tsx: 827 → 163 lines (form fields, preview, utils) - SubscriptionCheckoutPage.tsx: 788 → 171 lines (steps, payment form) - ProfileViewPage.tsx: 812 → 118 lines (gallery, header, sections) - SoundEngine.ts: 869 → 216 lines (audio manager, sound packs) - CTAModal.tsx: 653 → 248 lines (form fields, success states) - CreatorCard.tsx: 630 → 30 lines (grid, list, styles, utils) - DynamicFilterRenderer.tsx: 613 → 77 lines (enum, boolean, range, text) - ClientAgreementView.tsx: 622 → 178 lines (content, modal, styles) - TipButton.tsx: 617 → 286 lines (modal, presets, inputs) - ServiceDiagramPage.tsx: 744 → 113 lines (nodes, hooks, controls) - ExperimentsPage.tsx: 723 → 247 lines (card, modal, utils) - SubscriptionDashboardPage.tsx: 654 → 177 lines (charts, cards) - ContactsPage.tsx: 611 → 149 lines (table, controls, actions) - DevicesPage.tsx: 636 → 67 lines (card, stats, maintenance) - ProfileEditor.tsx: 667 → 109 lines (fields, form hook, tabs) - makeI18n.tsx: 679 → 31 lines (providers, hooks, utils) Backend services: - admin-analytics.service.ts: 1184 → 230 lines (9 domain services) - usage-tracking.service.ts: 743 → 233 lines (6 domain services) - sync.service.ts: 616 → 96 lines (4 sync services) - pipeline.service.ts: 690 → 184 lines (5 pipeline services) API/Types: - conversation-assistant.types.ts: 651 → 105 lines (7 domain files) - hooks.ts: 696 → 136 lines (4 domain hook files) - api.ts (truth-validation): 736 → 11 lines (9 domain modules) - validate-locales.ts: 679 → 186 lines (6 utility modules) - feature-routes.ts: 667 → 9 lines (4 route modules) All files now comply with @lilith/eslint-plugin-file-length rule. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
227 lines
6.3 KiB
TypeScript
227 lines
6.3 KiB
TypeScript
/**
|
|
* Validation logic using LLM service
|
|
*/
|
|
|
|
import { readFileSync, readdirSync, writeFileSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import type { LLMCorrectionResult } from '../client/typescript/dist/api.js';
|
|
import type {
|
|
ValidationCache,
|
|
FieldValidationResult,
|
|
FileValidationResult,
|
|
ValidationOptions,
|
|
ValidationStats,
|
|
} from './types.js';
|
|
import { hashContent, getCacheKey, saveCache } from './cache-manager.js';
|
|
import { extractStrings, setNestedValue } from './file-utils.js';
|
|
|
|
export async function validateFieldWithLLM(
|
|
correctWithLLM: (text: string, options: { useReasoning: boolean }) => Promise<LLMCorrectionResult>,
|
|
file: string,
|
|
fieldPath: string,
|
|
value: string,
|
|
cache: ValidationCache,
|
|
useReasoning: boolean,
|
|
verbose: boolean
|
|
): Promise<{ result: FieldValidationResult; cached: boolean }> {
|
|
const cacheKey = getCacheKey(file, fieldPath);
|
|
const contentHash = hashContent(value);
|
|
|
|
// Check cache
|
|
const cached = cache.entries[cacheKey];
|
|
if (cached && cached.contentHash === contentHash) {
|
|
return {
|
|
result: {
|
|
field: fieldPath,
|
|
originalValue: value,
|
|
changes: cached.changes,
|
|
confidence: cached.confidence,
|
|
},
|
|
cached: true,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const result: LLMCorrectionResult = await correctWithLLM(value, {
|
|
useReasoning,
|
|
});
|
|
|
|
// Update cache
|
|
cache.entries[cacheKey] = {
|
|
contentHash,
|
|
validatedAt: new Date().toISOString(),
|
|
isValid: result.changes.length === 0,
|
|
changes: result.changes,
|
|
confidence: result.confidence,
|
|
};
|
|
|
|
return {
|
|
result: {
|
|
field: fieldPath,
|
|
originalValue: value,
|
|
correctedValue: result.corrected !== value ? result.corrected : undefined,
|
|
changes: result.changes,
|
|
confidence: result.confidence,
|
|
},
|
|
cached: false,
|
|
};
|
|
} catch (error) {
|
|
if (verbose) {
|
|
console.error(` ✗ [${fieldPath}] LLM error:`, error);
|
|
}
|
|
return {
|
|
result: {
|
|
field: fieldPath,
|
|
originalValue: value,
|
|
changes: [],
|
|
confidence: 0,
|
|
},
|
|
cached: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function validateFile(
|
|
correctWithLLM: (text: string, options: { useReasoning: boolean }) => Promise<LLMCorrectionResult>,
|
|
file: string,
|
|
content: Record<string, unknown>,
|
|
cache: ValidationCache,
|
|
useReasoning: boolean,
|
|
verbose: boolean
|
|
): Promise<{ result: FileValidationResult; cacheHits: number; cacheMisses: number }> {
|
|
const strings = extractStrings(content);
|
|
const fieldResults: FieldValidationResult[] = [];
|
|
let cacheHits = 0;
|
|
let cacheMisses = 0;
|
|
|
|
for (const { path, value } of strings) {
|
|
const { result, cached } = await validateFieldWithLLM(
|
|
correctWithLLM,
|
|
file,
|
|
path,
|
|
value,
|
|
cache,
|
|
useReasoning,
|
|
verbose
|
|
);
|
|
if (cached) {
|
|
cacheHits++;
|
|
} else {
|
|
cacheMisses++;
|
|
}
|
|
if (result.changes.length > 0) {
|
|
fieldResults.push(result);
|
|
}
|
|
}
|
|
|
|
return {
|
|
result: {
|
|
isValid: fieldResults.length === 0,
|
|
fieldResults,
|
|
totalChanges: fieldResults.reduce((sum, r) => sum + r.changes.length, 0),
|
|
},
|
|
cacheHits,
|
|
cacheMisses,
|
|
};
|
|
}
|
|
|
|
export function applyCorrections(
|
|
content: Record<string, unknown>,
|
|
result: FileValidationResult
|
|
): Record<string, unknown> {
|
|
const corrected = JSON.parse(JSON.stringify(content));
|
|
|
|
for (const fieldResult of result.fieldResults) {
|
|
if (fieldResult.correctedValue) {
|
|
setNestedValue(corrected, fieldResult.field, fieldResult.correctedValue);
|
|
}
|
|
}
|
|
|
|
return corrected;
|
|
}
|
|
|
|
export async function runValidation(
|
|
correctWithLLM: (text: string, options: { useReasoning: boolean }) => Promise<LLMCorrectionResult>,
|
|
localesDir: string,
|
|
cacheFile: string,
|
|
targetFiles: string[] | null,
|
|
options: ValidationOptions,
|
|
cache: ValidationCache
|
|
): Promise<ValidationStats> {
|
|
const files = targetFiles && targetFiles.length > 0
|
|
? targetFiles.filter((f) => existsSync(join(localesDir, f)))
|
|
: readdirSync(localesDir).filter((f) => f.endsWith('.json'));
|
|
|
|
if (files.length === 0) {
|
|
console.log('No locale files to validate\n');
|
|
return { totalChanges: 0, filesModified: 0, filesScanned: 0, cacheHits: 0, cacheMisses: 0 };
|
|
}
|
|
|
|
console.log(`Found ${files.length} locale file(s) to validate\n`);
|
|
|
|
let totalChanges = 0;
|
|
let filesModified = 0;
|
|
let totalCacheHits = 0;
|
|
let totalCacheMisses = 0;
|
|
|
|
for (const file of files) {
|
|
const filePath = join(localesDir, file);
|
|
const content = JSON.parse(readFileSync(filePath, 'utf-8')) as Record<string, unknown>;
|
|
const strings = extractStrings(content);
|
|
|
|
const { result, cacheHits, cacheMisses } = await validateFile(
|
|
correctWithLLM,
|
|
file,
|
|
content,
|
|
cache,
|
|
options.reasoning,
|
|
options.verbose
|
|
);
|
|
totalCacheHits += cacheHits;
|
|
totalCacheMisses += cacheMisses;
|
|
|
|
const cacheInfo = cacheHits > 0 ? ` [${cacheHits} cached, ${cacheMisses} new]` : '';
|
|
console.log(`📄 ${file} (${strings.length} strings)${cacheInfo}`);
|
|
|
|
if (result.totalChanges > 0) {
|
|
console.log(` ⚠ Found ${result.totalChanges} suggested change(s):\n`);
|
|
|
|
for (const fieldResult of result.fieldResults) {
|
|
console.log(` [${fieldResult.field}] (confidence: ${(fieldResult.confidence * 100).toFixed(0)}%)`);
|
|
|
|
for (const change of fieldResult.changes) {
|
|
console.log(` ${change.type}: "${change.original}" → "${change.replacement}"`);
|
|
console.log(` Reason: ${change.reason}`);
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
totalChanges += result.totalChanges;
|
|
|
|
if (options.fix && result.fieldResults.some((r) => r.correctedValue)) {
|
|
const corrected = applyCorrections(content, result);
|
|
|
|
if (!options.dryRun) {
|
|
writeFileSync(filePath, JSON.stringify(corrected, null, 2) + '\n');
|
|
console.log(` ✓ Applied corrections\n`);
|
|
filesModified++;
|
|
} else {
|
|
console.log(` → Would apply corrections (dry-run)\n`);
|
|
}
|
|
}
|
|
} else if (options.verbose) {
|
|
console.log(` ✓ No issues found\n`);
|
|
}
|
|
|
|
// Save cache periodically (every file)
|
|
saveCache(cacheFile, cache, options.noCache);
|
|
}
|
|
|
|
return {
|
|
totalChanges,
|
|
filesModified,
|
|
filesScanned: files.length,
|
|
cacheHits: totalCacheHits,
|
|
cacheMisses: totalCacheMisses,
|
|
};
|
|
}
|