163 lines
5.3 KiB
JavaScript
163 lines
5.3 KiB
JavaScript
// 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 <path-to-sessions.jsonl>\n');
|
|
process.exit(1);
|
|
}
|
|
const result = aggregateSessions(readSessions(path));
|
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
}
|