feat(ai-psychosis): report-reader v1.2 schema + aggregations
This commit is contained in:
parent
c5e933b35d
commit
f88639ef41
2 changed files with 134 additions and 1 deletions
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,61 @@ test('v1.2 array domain_context aggregates correctly (relationship in array)', (
|
|||
assert.equal(result.null_domain_count, 1, 'empty array counts as null');
|
||||
});
|
||||
|
||||
test('v1.2 mixed schema fixture: per-domain breakdown + user_info_class + valseek', () => {
|
||||
const fixture = [
|
||||
// v1.0 — no pushback flag, no domain_context
|
||||
{ session_id: 'v0', duration_min: 30,
|
||||
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0 } },
|
||||
// v1.1 — pushback flag, string domain
|
||||
{ session_id: 'v1', duration_min: 30, domain_context: 'relationship',
|
||||
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } },
|
||||
// v1.2 — multi-domain array, user_info_class, valseek_count
|
||||
{ session_id: 'v2a', duration_min: 30,
|
||||
domain_context: ['relationship', 'health'],
|
||||
user_info_class: 'no', valseek_count: 3, turn_count: 20,
|
||||
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 2 } },
|
||||
{ session_id: 'v2b', duration_min: 30,
|
||||
domain_context: ['legal'],
|
||||
user_info_class: 'yes_people', valseek_count: 0, turn_count: 8,
|
||||
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
|
||||
{ session_id: 'v2c', duration_min: 30,
|
||||
domain_context: [],
|
||||
user_info_class: null, valseek_count: 0, turn_count: 5,
|
||||
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
|
||||
];
|
||||
const jsonl = fixture.map(o => JSON.stringify(o)).join('\n') + '\n';
|
||||
const result = runReader(jsonl);
|
||||
|
||||
// schema_version discrimination
|
||||
assert.equal(result.schema_version.v1_0_records, 1);
|
||||
assert.equal(result.schema_version.v1_1_records, 1);
|
||||
assert.equal(result.schema_version.v1_2_records, 3);
|
||||
|
||||
// per-domain breakdown (only v1.x array members)
|
||||
assert.equal(result.domain_breakdown.relationship, 2,
|
||||
'v1.1 string + v1.2 array containing relationship → 2');
|
||||
assert.equal(result.domain_breakdown.health, 1);
|
||||
assert.equal(result.domain_breakdown.legal, 1);
|
||||
assert.equal(result.domain_breakdown.parenting, 0);
|
||||
|
||||
// user_info_class distribution
|
||||
assert.equal(result.user_info_class.no, 1);
|
||||
assert.equal(result.user_info_class.yes_people, 1);
|
||||
assert.equal(result.user_info_class.null, 1);
|
||||
|
||||
// valseek aggregation
|
||||
assert.equal(result.valseek.sessions, 1);
|
||||
assert.equal(result.valseek.total, 3);
|
||||
|
||||
// stakes_signal — max weight per session
|
||||
// v2a: max(relationship=1.3, health=1.5) = 1.5
|
||||
// v2b: legal=1.5
|
||||
// v2c: empty → not counted
|
||||
assert.equal(result.stakes_signal.sessions, 2);
|
||||
assert.ok(Math.abs(result.stakes_signal.sum - 3.0) < 0.01,
|
||||
`expected stakes_signal.sum ~3.0, got ${result.stakes_signal.sum}`);
|
||||
});
|
||||
|
||||
test('backward-compat: v1.0.0 records without pushback/domain do not produce NaN', () => {
|
||||
const fixture = [
|
||||
// v1.0.0 — no pushback in flags, no domain_context at top level
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue