export function extractCanonicalNames(dungeonData) { const names = { npcs: [], rooms: [], factions: [] }; if (dungeonData.npcs) { dungeonData.npcs.forEach(npc => { if (npc.name) names.npcs.push(npc.name.trim()); }); } if (dungeonData.rooms) { dungeonData.rooms.forEach(room => { if (room.name) names.rooms.push(room.name.trim()); }); } if (dungeonData.coreConcepts) { const factionMatch = dungeonData.coreConcepts.match(/Primary Faction[:\s]+([^.]+)/i); if (factionMatch) names.factions.push(factionMatch[1].trim()); } return names; } export function validateContentCompleteness(dungeonData) { const issues = []; const checks = [ ['title', 0, 'Missing title'], ['flavor', 20, 'Flavor text too short'], ['hooksRumors', 4, 'Expected at least 4 hooks'], ['rooms', 5, 'Expected at least 5 rooms'], ['encounters', 6, 'Expected at least 6 encounters'], ['npcs', 4, 'Expected at least 4 NPCs'], ['treasure', 4, 'Expected at least 4 treasures'], ['randomEvents', 6, 'Expected 6 random events'], ['plotResolutions', 4, 'Expected at least 4 plot resolutions'] ]; checks.forEach(([key, min, msg]) => { const val = dungeonData[key]; if (!val || (Array.isArray(val) ? val.length < min : val.trim().length < min)) { issues.push(`${msg}${Array.isArray(val) ? `, got ${val?.length || 0}` : ''}`); } }); dungeonData.rooms?.forEach((r, i) => { if (!r.description || r.description.trim().length < 20) { issues.push(`Room ${i + 1} (${r.name}) description too short`); } }); dungeonData.encounters?.forEach((e, i) => { if (!e.details || e.details.trim().length < 30) { issues.push(`Encounter ${i + 1} (${e.name}) details too short`); } }); dungeonData.npcs?.forEach((n, i) => { if (!n.trait || n.trait.trim().length < 30) { issues.push(`NPC ${i + 1} (${n.name}) description too short`); } }); return issues; } export function validateContentQuality(dungeonData) { const issues = []; const vagueWords = /\b(some|various|several|many|few|things|stuff|items|objects)\b/gi; const checkVague = (text, ctx) => { if (!text) return; const matches = text.match(vagueWords); if (matches?.length > 2) { issues.push(`${ctx} contains vague language: "${matches.slice(0, 3).join('", "')}"`); } }; checkVague(dungeonData.flavor, 'Flavor text'); dungeonData.rooms?.forEach(r => checkVague(r.description, `Room "${r.name}"`)); dungeonData.encounters?.forEach(e => checkVague(e.details, `Encounter "${e.name}"`)); dungeonData.npcs?.forEach(n => checkVague(n.trait, `NPC "${n.name}"`)); dungeonData.rooms?.forEach(r => { if (r.description?.length < 50) { issues.push(`Room "${r.name}" description too short`); } }); return issues; } export function validateContentStructure(dungeonData) { const issues = []; dungeonData.rooms?.forEach((r, i) => { if (!r.name?.trim()) issues.push(`Room ${i + 1} missing name`); if (r.name?.split(/\s+/).length > 6) issues.push(`Room "${r.name}" name too long`); }); dungeonData.encounters?.forEach((e, i) => { if (!e.name?.trim()) issues.push(`Encounter ${i + 1} missing name`); if (e.name?.split(/\s+/).length > 6) issues.push(`Encounter "${e.name}" name too long`); if (e.details && !e.details.match(/^[^:]+:\s/)) { issues.push(`Encounter "${e.name}" details missing location prefix`); } }); dungeonData.npcs?.forEach((n, i) => { if (!n.name?.trim()) issues.push(`NPC ${i + 1} missing name`); if (n.name?.split(/\s+/).length > 4) issues.push(`NPC "${n.name}" name too long`); }); return issues; } export function validateNarrativeCoherence(dungeonData) { const issues = []; const factionMatch = dungeonData.coreConcepts?.match(/Primary Faction[:\s]+([^.]+)/i); const factionName = factionMatch?.[1]?.trim(); if (dungeonData.encounters && dungeonData.rooms) { const roomNames = dungeonData.rooms.map(r => r.name.trim().toLowerCase()); dungeonData.encounters.forEach(e => { const locMatch = e.details?.match(/^([^:]+):/); if (locMatch) { const locName = locMatch[1].trim().toLowerCase(); if (!roomNames.some(rn => locName.includes(rn) || rn.includes(locName))) { issues.push(`Encounter "${e.name}" references unknown location "${locMatch[1]}"`); } } }); } if (factionName) { const factionLower = factionName.toLowerCase(); const refs = (dungeonData.npcs?.filter(n => n.trait?.toLowerCase().includes(factionLower)).length ?? 0) + (dungeonData.encounters?.filter(e => e.details?.toLowerCase().includes(factionLower)).length ?? 0); if (refs < 2) { issues.push(`Faction "${factionName}" poorly integrated (${refs} references)`); } } return issues; }