feat(ai-psychosis): report-reader v1.2 schema + aggregations

This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 21:47:53 +02:00
commit f88639ef41
2 changed files with 134 additions and 1 deletions

View file

@ -22,6 +22,7 @@ export function aggregateSessions(sessions) {
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;
@ -29,6 +30,33 @@ export function aggregateSessions(sessions) {
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;
@ -37,7 +65,10 @@ export function aggregateSessions(sessions) {
const flags = rec.flags || {};
const pushback = flags.pushback;
if (pushback === undefined || pushback === null) v1_0_records += 1;
// 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;
@ -52,6 +83,38 @@ export function aggregateSessions(sessions) {
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 {
@ -70,6 +133,21 @@ export function aggregateSessions(sessions) {
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,
},
};
}