imajin/@packages/imajin-config/tests/coverage-analysis.test.ts
Claude Code 2dd98de686 deps-upgrade(packages-specific): ⬆️ Update dependencies in package configurations across the monorepo
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-03-28 14:57:10 -07:00

336 lines
12 KiB
TypeScript

/**
* Coverage Analysis Test Suite
*
* Tests the SubjectTemplates helpers against real attribute data from the
* Attributes microservice to analyze filter coverage and identify gaps.
*
* Run with: pnpm attrs:analyze
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, it, expect, beforeAll } from 'vitest';
import { SubjectTemplates } from '../src/subjects.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
// =============================================================================
// Types
// =============================================================================
interface AttributeRow {
attributeCode: string;
enumValue: string;
slug: string;
displayValue: string;
category: 'semantic' | 'non-semantic';
}
interface MatchResult {
slug: string;
attributeCode: string;
displayValue: string;
matchedTemplateId: string | null;
matchedTemplateName: string | null;
}
interface CoverageReport {
generatedAt: string;
totalSemanticFilters: number;
matchedFilters: number;
unmatchedFilters: number;
coveragePercent: string;
matchedByTemplate: Record<string, string[]>;
unmatchedByAttribute: Record<string, Array<{ slug: string; displayValue: string }>>;
allResults: MatchResult[];
}
// =============================================================================
// CSV Parser
// =============================================================================
function parseCSV(content: string): AttributeRow[] {
const lines = content.trim().split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map((line) => {
const values = line.split(',');
const row: Record<string, string> = {};
headers.forEach((header, i) => {
row[header] = values[i] || '';
});
return row as unknown as AttributeRow;
});
}
// =============================================================================
// Template Matching
// =============================================================================
/**
* Find which template matches a given filter slug.
* Returns null if the filter falls through to the default solo-female template.
*/
function findMatchedTemplate(slug: string): { id: string; name: string } | null {
const templates = SubjectTemplates.helpers.getAllTemplates();
for (const template of templates) {
const hasMatch = template.triggerFilters.some(
(trigger) => trigger.toLowerCase() === slug.toLowerCase()
);
if (hasMatch) {
return { id: template.id, name: template.name };
}
}
return null;
}
// =============================================================================
// Report Generation
// =============================================================================
function generateReport(results: MatchResult[]): CoverageReport {
const matched = results.filter((r) => r.matchedTemplateId !== null);
const unmatched = results.filter((r) => r.matchedTemplateId === null);
// Group matched by template
const matchedByTemplate: Record<string, string[]> = {};
for (const result of matched) {
const key = `${result.matchedTemplateId} (${result.matchedTemplateName})`;
if (!matchedByTemplate[key]) {
matchedByTemplate[key] = [];
}
matchedByTemplate[key].push(result.slug);
}
// Group unmatched by attribute code
const unmatchedByAttribute: Record<string, Array<{ slug: string; displayValue: string }>> = {};
for (const result of unmatched) {
if (!unmatchedByAttribute[result.attributeCode]) {
unmatchedByAttribute[result.attributeCode] = [];
}
unmatchedByAttribute[result.attributeCode].push({
slug: result.slug,
displayValue: result.displayValue,
});
}
return {
generatedAt: new Date().toISOString(),
totalSemanticFilters: results.length,
matchedFilters: matched.length,
unmatchedFilters: unmatched.length,
coveragePercent: ((matched.length / results.length) * 100).toFixed(1),
matchedByTemplate,
unmatchedByAttribute,
allResults: results,
};
}
function writeReport(report: CoverageReport, outputPath: string): void {
const outputDir = dirname(outputPath);
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(outputPath, JSON.stringify(report, null, 2));
}
// =============================================================================
// Test Suite
// =============================================================================
describe('SubjectTemplates Coverage Analysis', () => {
let semanticFilters: AttributeRow[];
let report: CoverageReport;
beforeAll(() => {
const csvPath = join(__dirname, 'fixtures', 'attributes.csv');
const csvContent = readFileSync(csvPath, 'utf-8');
const allAttributes = parseCSV(csvContent);
// Filter to only semantic attributes
semanticFilters = allAttributes.filter((a) => a.category === 'semantic');
// Run analysis
const results: MatchResult[] = semanticFilters.map((attr) => {
const match = findMatchedTemplate(attr.slug);
return {
slug: attr.slug,
attributeCode: attr.attributeCode,
displayValue: attr.displayValue,
matchedTemplateId: match?.id ?? null,
matchedTemplateName: match?.name ?? null,
};
});
report = generateReport(results);
// Write report to output directory
const outputPath = join(__dirname, 'output', 'coverage-report.json');
writeReport(report, outputPath);
// Also write a human-readable summary
const summaryPath = join(__dirname, 'output', 'coverage-summary.txt');
writeSummary(report, summaryPath);
});
it('should analyze semantic filter coverage', () => {
expect(report.totalSemanticFilters).toBeGreaterThan(0);
console.log(`\n=== Coverage Analysis Results ===`);
console.log(`Total semantic filters: ${report.totalSemanticFilters}`);
console.log(`Matched filters: ${report.matchedFilters}`);
console.log(`Unmatched filters: ${report.unmatchedFilters}`);
console.log(`Coverage: ${report.coveragePercent}%`);
console.log(`\nReport written to: tests/output/coverage-report.json`);
console.log(`Summary written to: tests/output/coverage-summary.txt`);
});
it('should have reasonable coverage (baseline check)', () => {
// This test establishes a baseline - adjust threshold as coverage improves
const coveragePercent = parseFloat(report.coveragePercent);
expect(coveragePercent).toBeGreaterThan(30); // Expect at least 30% coverage
});
it('should report unmatched filters grouped by attribute', () => {
console.log(`\n=== Unmatched Filters by Attribute ===`);
for (const [attrCode, filters] of Object.entries(report.unmatchedByAttribute)) {
console.log(`\n${attrCode}:`);
for (const filter of filters) {
console.log(` - ${filter.slug} (${filter.displayValue})`);
}
}
});
it('should report matched filters grouped by template', () => {
console.log(`\n=== Matched Filters by Template ===`);
for (const [templateKey, slugs] of Object.entries(report.matchedByTemplate)) {
console.log(`\n${templateKey}:`);
console.log(` ${slugs.join(', ')}`);
}
});
});
// =============================================================================
// Summary Generator
// =============================================================================
function writeSummary(report: CoverageReport, outputPath: string): void {
const lines: string[] = [
'================================================================================',
' SubjectTemplates Coverage Analysis Summary',
'================================================================================',
'',
`Generated: ${report.generatedAt}`,
'',
'--- Statistics ---',
`Total Semantic Filters: ${report.totalSemanticFilters}`,
`Matched Filters: ${report.matchedFilters}`,
`Unmatched Filters: ${report.unmatchedFilters}`,
`Coverage: ${report.coveragePercent}%`,
'',
'================================================================================',
' UNMATCHED FILTERS BY ATTRIBUTE',
'================================================================================',
'',
'These filters exist in the Attributes microservice but do not match any',
'SubjectTemplate. Consider adding them as triggers to existing templates',
'or creating new templates if appropriate.',
'',
];
// Sort by number of unmatched (most unmatched first)
const sortedUnmatched = Object.entries(report.unmatchedByAttribute).sort(
(a, b) => b[1].length - a[1].length
);
for (const [attrCode, filters] of sortedUnmatched) {
lines.push(`--- ${attrCode} (${filters.length} unmatched) ---`);
for (const filter of filters) {
lines.push(` ${filter.slug.padEnd(25)} ${filter.displayValue}`);
}
lines.push('');
}
lines.push('================================================================================');
lines.push(' MATCHED FILTERS BY TEMPLATE');
lines.push('================================================================================');
lines.push('');
for (const [templateKey, slugs] of Object.entries(report.matchedByTemplate)) {
lines.push(`--- ${templateKey} ---`);
lines.push(` Triggers: ${slugs.join(', ')}`);
lines.push('');
}
lines.push('================================================================================');
lines.push(' SUGGESTED ACTIONS');
lines.push('================================================================================');
lines.push('');
lines.push('High Priority (body-type, ethnicity, hair-color have many common filters):');
lines.push('');
// Generate simple suggestions based on attribute categories
const suggestions: string[] = [];
if (report.unmatchedByAttribute['body-type']) {
const bodyTypes = report.unmatchedByAttribute['body-type'].map((f) => f.slug);
suggestions.push(
`1. Add body-type triggers to templates:`,
` - Athletic template: consider adding "slim", "toned", "fit"`,
` - BBW template: consider adding "full-figured", "hourglass"`,
` - Petite template: already covers "petite", "small"`,
` Missing: ${bodyTypes.join(', ')}`,
''
);
}
if (report.unmatchedByAttribute['ethnicity']) {
const ethnicities = report.unmatchedByAttribute['ethnicity'].map((f) => f.slug);
suggestions.push(
`2. Add ethnicity triggers to templates:`,
` - Asian template: consider adding regional variants`,
` - Latina template: consider adding country-specific variants`,
` - Ebony template: consider adding regional variants`,
` - Create new templates: middle-eastern, indian, european`,
` Missing: ${ethnicities.join(', ')}`,
''
);
}
if (report.unmatchedByAttribute['hair-color']) {
const hairColors = report.unmatchedByAttribute['hair-color'].map((f) => f.slug);
suggestions.push(
`3. Add hair-color triggers to templates:`,
` - Blonde template: consider adding "platinum-blonde", "dirty-blonde"`,
` - Brunette template: consider adding "brown-hair", "black-hair"`,
` - Create new templates: colored-hair (pink, blue, purple)`,
` Missing: ${hairColors.join(', ')}`,
''
);
}
if (report.unmatchedByAttribute['age-group']) {
const ageGroups = report.unmatchedByAttribute['age-group'].map((f) => f.slug);
suggestions.push(
`4. Add age-group triggers to templates:`,
` - Young template: consider adding "twenties", "college"`,
` - Mature template: consider adding "thirties", "forties", "fifties"`,
` Missing: ${ageGroups.join(', ')}`,
''
);
}
lines.push(...suggestions);
lines.push('================================================================================');
const outputDir = dirname(outputPath);
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(outputPath, lines.join('\n'));
}