// report-reader.mjs — Aggregates sessions.jsonl into a JSON summary. // Dual-mode: importable (named exports) or directly executable. // Backward-compatible with v1.0.0 records that lack pushback / domain_context. import { readFileSync, existsSync } from 'fs'; export function readSessions(path) { if (!existsSync(path)) return []; return readFileSync(path, 'utf8') .split('\n') .filter(Boolean) .map(line => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean); } export function aggregateSessions(sessions) { let pushback_total = 0; let relationship_domain_count = 0; let other_domain_count = 0; let null_domain_count = 0; let v1_0_records = 0; let v1_1_records = 0; let v1_2_records = 0; let total_end_records = 0; let total_dependency = 0; let total_escalation = 0; let total_fatigue = 0; let total_validation = 0; // v1.2: per-domain counters (each session that includes domain X increments // domain_breakdown[X] by 1 — multi-domain sessions increment multiple). const domain_breakdown = { relationship: 0, legal: 0, parenting: 0, health: 0, financial: 0, professional: 0, spirituality: 0, consumer: 0, personal_dev: 0, }; // v1.2: user_info_class distribution. const user_info_distribution = { yes_people: 0, yes_digital: 0, no: 0, null: 0, }; // v1.2: valseek summary. let valseek_sessions = 0; // sessions with valseek_count > 0 let valseek_total = 0; // sum of valseek_count across all v1.2 records // v1.2: aggregated stakes signal — sum of max-domain-weight across sessions. // (Reported as part of /interaction-report; raw aggregate.) let stakes_signal_total = 0; let stakes_signal_sessions = 0; // Domain stakes table mirrors lib.mjs DOMAIN_STAKES so report-reader stays // standalone (no cross-import). Keep in sync with lib.mjs. const DOMAIN_STAKES = { legal: 1.5, parenting: 1.5, health: 1.5, financial: 1.5, relationship: 1.3, spirituality: 1.2, professional: 1.1, wellbeing: 1.2, lifepath: 1.1, values: 1.2, personal_dev: 1.0, consumer: 1.0, }; for (const rec of sessions) { if (!rec || rec.note === 'no_state_file') continue; if (rec.duration_min === undefined) continue; total_end_records += 1; const flags = rec.flags || {}; const pushback = flags.pushback; // v1.2 discriminator: presence of user_info_class field marks a v1.2 record. const hasUserInfoClass = Object.prototype.hasOwnProperty.call(rec, 'user_info_class'); if (hasUserInfoClass) v1_2_records += 1; else if (pushback === undefined || pushback === null) v1_0_records += 1; else v1_1_records += 1; pushback_total += Number(pushback) || 0; total_dependency += Number(flags.dependency) || 0; total_escalation += Number(flags.escalation) || 0; total_fatigue += Number(flags.fatigue) || 0; total_validation += Number(flags.validation) || 0; // v1.2: domain_context is array; v1.0/v1.1: null or string. Coerce on read. const dc = rec.domain_context; const domains = Array.isArray(dc) ? dc : (dc ? [dc] : []); if (domains.length === 0) null_domain_count += 1; else if (domains.includes('relationship')) relationship_domain_count += 1; else other_domain_count += 1; // v1.2: per-domain breakdown (multi-domain sessions count once per domain). for (const d of domains) { if (Object.prototype.hasOwnProperty.call(domain_breakdown, d)) { domain_breakdown[d] += 1; } } // v1.2 fields if (hasUserInfoClass) { const cls = rec.user_info_class; if (cls === 'yes_people' || cls === 'yes_digital' || cls === 'no') { user_info_distribution[cls] += 1; } else { user_info_distribution.null += 1; } const vs = Number(rec.valseek_count) || 0; valseek_total += vs; if (vs > 0) valseek_sessions += 1; // stakes_signal: max weight among the session's domains. if (domains.length > 0) { let maxW = 1.0; for (const d of domains) { const w = DOMAIN_STAKES[d]; if (typeof w === 'number' && w > maxW) maxW = w; } stakes_signal_total += maxW; stakes_signal_sessions += 1; } } } return { pushback_total, relationship_domain_count, other_domain_count, null_domain_count, total_end_records, flags_total: { dependency: total_dependency, escalation: total_escalation, fatigue: total_fatigue, validation: total_validation, pushback: pushback_total, }, schema_version: { v1_0_records, v1_1_records, v1_2_records, }, // v1.2 aggregations domain_breakdown, user_info_class: user_info_distribution, valseek: { sessions: valseek_sessions, total: valseek_total, }, stakes_signal: { sum: stakes_signal_total, sessions: stakes_signal_sessions, mean: stakes_signal_sessions > 0 ? Number((stakes_signal_total / stakes_signal_sessions).toFixed(2)) : 0, }, }; } if (import.meta.url === `file://${process.argv[1]}`) { const path = process.argv[2]; if (!path) { process.stderr.write('Usage: node report-reader.mjs \n'); process.exit(1); } const result = aggregateSessions(readSessions(path)); process.stdout.write(JSON.stringify(result, null, 2) + '\n'); }