const fs = require('fs'); const content = fs.readFileSync('temp/rich-outline-fail_57_1779015509994.txt', 'utf-8'); const marker = '========== AI εŽŸε§‹ε“εΊ” '; const markerIdx = content.indexOf(marker); const headerEndIdx = content.indexOf(' ==========', markerIdx); const aiStart = content.indexOf('\n', headerEndIdx) + 1; const aiEnd = content.indexOf('\n\n', aiStart); const aiResponse = content.substring(aiStart, aiEnd); console.log('AI response length:', aiResponse.length); // Extract the substring around the error and try to parse it const pos = 1624; const start = Math.max(0, pos - 500); const end = Math.min(aiResponse.length, pos + 200); const snippet = aiResponse.substring(start, end); console.log('\n=== Snippet around error (500 before, 200 after pos 1624) ==='); console.log('Start offset:', start, 'End offset:', end); console.log('JSON snippet:', JSON.stringify('...' + snippet + '...')); // Now let's try to create a mini JSON with just the part around the error // and see if we can identify the issue const beforeSnippet = aiResponse.substring(0, pos); const afterSnippet = aiResponse.substring(pos); // Try parsing a truncated version that ends at the error position console.log('\n=== Try parsing up to error position ==='); try { JSON.parse(beforeSnippet); console.log('Parsed OK (unexpected)'); } catch(e) { console.log('Parse error at:', e.message); } // Try parsing from error position onwards console.log('\n=== Try parsing from error position ==='); try { const data = JSON.parse('{' + afterSnippet); console.log('Parsed OK, chapters:', data.chapters?.length); } catch(e) { console.log('Parse error at:', e.message.substring(0, 80)); } // Let me check if maybe there's a weird character right before position 1624 // that I'm missing console.log('\n=== Detailed byte analysis ==='); for (let i = 1618; i < 1635; i++) { const code = aiResponse.charCodeAt(i); console.log(' pos', i, ':', 'U+' + code.toString(16).toUpperCase().padStart(4, '0'), JSON.stringify(aiResponse[i])); } // Let's also check if maybe there's a BOM or something console.log('\nFirst char:', aiResponse.charCodeAt(0), JSON.stringify(aiResponse[0])); console.log('First 3 chars:', aiResponse.substring(0, 3).split('').map(c => c.charCodeAt(0))); // Try using TextEncoder to get actual bytes const encoder = new TextEncoder(); const bytes = encoder.encode(aiResponse); console.log('\n=== UTF-8 bytes around position 1624 (converted to code units) ==='); // Since the string is 18719 chars but UTF-8 bytes is 38333, // that means most chars are 3-byte Chinese chars // Let's find the byte offset for UTF-16 position 1624 let utf8Offset = 0; for (let utf16Idx = 0; utf16Idx < 1624; utf16Idx++) { const code = aiResponse.charCodeAt(utf16Idx); if (code <= 0x7F) utf8Offset += 1; else if (code <= 0x7FF) utf8Offset += 2; else if (code <= 0xFFFF) utf8Offset += 3; else utf8Offset += 4; } console.log('UTF-8 offset for UTF-16 pos 1624:', utf8Offset); console.log('UTF-8 bytes around that position:'); for (let i = utf8Offset - 5; i < utf8Offset + 10; i++) { if (i >= 0 && i < bytes.length) { console.log(' byte', i, ':', bytes[i], String.fromCharCode(bytes[i])); } } // Now check if those UTF-8 bytes form valid UTF-8 characters console.log('\n=== Checking UTF-8 validity around position ==='); // 3-byte UTF-8 for CJK: 1110xxxx 10xxxxxx 10xxxxxx // Let's see what the bytes around utf8Offset actually represent for (let i = utf8Offset - 3; i < utf8Offset + 6; i++) { if (i >= 0 && i < bytes.length) { const b = bytes[i]; console.log(' byte', i, ':', b, 'binary:', b.toString(2).padStart(8, '0')); } }