diff --git a/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs b/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs index 97bea9b..9d9afab 100644 --- a/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs +++ b/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs @@ -137,7 +137,19 @@ state.esc_flags = newEsc; state.fatigue_flags = newFat; state.val_flags = newVal; state.pushback_count = (Number(state.pushback_count) || 0) + pbReactiveHit + pbPreemptiveHit; -if (domainHit === 1 && !state.domain_context) state.domain_context = 'relationship'; + +// v1.2: domain_context is always an array. Coerce v1.1.0 string shape on read. +if (domainHit === 1) { + if (typeof state.domain_context === 'string') { + state.domain_context = state.domain_context ? [state.domain_context] : []; + } + if (!Array.isArray(state.domain_context)) { + state.domain_context = []; + } + if (!state.domain_context.includes('relationship')) { + state.domain_context.push('relationship'); + } +} writeState(state); // Check if any thresholds crossed diff --git a/plugins/ai-psychosis/hooks/scripts/report-reader.mjs b/plugins/ai-psychosis/hooks/scripts/report-reader.mjs index b4579bc..83443a1 100644 --- a/plugins/ai-psychosis/hooks/scripts/report-reader.mjs +++ b/plugins/ai-psychosis/hooks/scripts/report-reader.mjs @@ -46,9 +46,11 @@ export function aggregateSessions(sessions) { 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; - if (dc === null || dc === undefined) null_domain_count += 1; - else if (dc === 'relationship') relationship_domain_count += 1; + 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; } diff --git a/plugins/ai-psychosis/hooks/scripts/session-end.mjs b/plugins/ai-psychosis/hooks/scripts/session-end.mjs index 73c78be..cbba605 100644 --- a/plugins/ai-psychosis/hooks/scripts/session-end.mjs +++ b/plugins/ai-psychosis/hooks/scripts/session-end.mjs @@ -39,7 +39,11 @@ const escFlags = Number(state.esc_flags) || 0; const fatFlags = Number(state.fatigue_flags) || 0; const valFlags = Number(state.val_flags) || 0; const pushbackCount = Number(state.pushback_count) || 0; -const domainContext = state.domain_context || null; +// v1.2: domain_context is always written as array. Coerce v1.1.0 string shape. +const domainContextRaw = state.domain_context; +const domainContextArray = Array.isArray(domainContextRaw) + ? domainContextRaw + : (domainContextRaw ? [domainContextRaw] : []); const startIso = state.start_iso || ''; // Compute duration @@ -56,7 +60,7 @@ appendJsonl(SESSIONS_LOG, { duration_min: durationMin, tool_count: toolCount, edit_count: editCount, - domain_context: domainContext, + domain_context: domainContextArray, flags: { dependency: depFlags, escalation: escFlags, diff --git a/plugins/ai-psychosis/tests/interaction-report.test.mjs b/plugins/ai-psychosis/tests/interaction-report.test.mjs index a5c8615..1770a03 100644 --- a/plugins/ai-psychosis/tests/interaction-report.test.mjs +++ b/plugins/ai-psychosis/tests/interaction-report.test.mjs @@ -62,6 +62,29 @@ test('relationship_domain_count matches fixture count', () => { assert.equal(result.null_domain_count, 2); }); +test('v1.2 array domain_context aggregates correctly (relationship in array)', () => { + const fixture = [ + // v1.2 — multi-domain array containing 'relationship' + { session_id: 'a', duration_min: 30, domain_context: ['relationship', 'health'], + flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } }, + // v1.2 — array without 'relationship' + { session_id: 'b', duration_min: 30, domain_context: ['legal'], + flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } }, + // v1.2 — empty array (no domain detected this session) + { session_id: 'c', duration_min: 30, domain_context: [], + flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } }, + // v1.1 — string shape (must still aggregate as relationship) + { session_id: 'd', duration_min: 30, domain_context: 'relationship', + flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } }, + ]; + const jsonl = fixture.map(o => JSON.stringify(o)).join('\n') + '\n'; + const result = runReader(jsonl); + assert.equal(result.relationship_domain_count, 2, + 'v1.2 array containing relationship + v1.1 string both increment relationship counter'); + assert.equal(result.other_domain_count, 1, 'v1.2 ["legal"] is "other" until Step 14 adds per-domain breakdown'); + assert.equal(result.null_domain_count, 1, 'empty array counts as null'); +}); + 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 diff --git a/plugins/ai-psychosis/tests/prompt-analyzer.test.mjs b/plugins/ai-psychosis/tests/prompt-analyzer.test.mjs index 0122f35..2d26784 100644 --- a/plugins/ai-psychosis/tests/prompt-analyzer.test.mjs +++ b/plugins/ai-psychosis/tests/prompt-analyzer.test.mjs @@ -418,13 +418,14 @@ describe('pushback integration (state accumulation + same-invocation valence)', assert.equal(s.fatigue_flags, 1); }); - it('sets domain_context to "relationship" on positive match', () => { + it('sets domain_context to ["relationship"] on positive match (v1.2 array shape)', () => { const s = runPrompt("my partner won't listen to me"); - assert.equal(s.domain_context, 'relationship'); + assert.deepEqual(s.domain_context, ['relationship']); }); it('keeps domain_context null on technical "function relationship" (false-positive guard)', () => { const s = runPrompt('function relationship between input and output'); + // No domainHit → state.domain_context stays as fresh-state null (untouched). assert.equal(s.domain_context, null); }); }); diff --git a/plugins/ai-psychosis/tests/session-end.test.mjs b/plugins/ai-psychosis/tests/session-end.test.mjs index ed6c110..e48f72a 100644 --- a/plugins/ai-psychosis/tests/session-end.test.mjs +++ b/plugins/ai-psychosis/tests/session-end.test.mjs @@ -64,13 +64,13 @@ describe('session-end', () => { assert.equal(records[0].note, 'no_state_file'); }); - it('persists pushback_count and domain_context (v1.1.0)', () => { + it('persists pushback_count and coerces v1.1.0 string domain to array', () => { dir = setupTestDir(); createStateFile(dir, 's4', { start_epoch: Math.floor(Date.now() / 1000) - 120, start_iso: '2026-01-01T10:00:00Z', tool_count: 2, edit_count: 1, dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0, - pushback_count: 3, domain_context: 'relationship', + pushback_count: 3, domain_context: 'relationship', // v1.1.0 string shape last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0, }); runHook('session-end.mjs', { session_id: 's4', cwd: '/tmp' }, dir); @@ -78,7 +78,25 @@ describe('session-end', () => { const end = records.find(r => r.end); assert.ok(end); assert.equal(end.flags.pushback, 3); - assert.equal(end.domain_context, 'relationship'); + // v1.2: end record always carries an array, even when state had a string. + assert.deepEqual(end.domain_context, ['relationship']); + }); + + it('writes v1.2 multi-domain array unchanged when state already has array', () => { + dir = setupTestDir(); + createStateFile(dir, 's4b', { + start_epoch: Math.floor(Date.now() / 1000) - 120, start_iso: '2026-01-01T10:00:00Z', + tool_count: 2, edit_count: 1, + dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0, + pushback_count: 1, + domain_context: ['relationship', 'health'], + last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0, + }); + runHook('session-end.mjs', { session_id: 's4b', cwd: '/tmp' }, dir); + const records = readJsonl(join(dir, 'sessions.jsonl')); + const end = records.find(r => r.end); + assert.ok(end); + assert.deepEqual(end.domain_context, ['relationship', 'health']); }); it('backward-compat: state without pushback_count yields flags.pushback === 0 (not NaN/undefined)', () => { @@ -97,6 +115,7 @@ describe('session-end', () => { assert.equal(end.flags.pushback, 0); assert.notEqual(end.flags.pushback, undefined); assert.ok(!Number.isNaN(end.flags.pushback)); - assert.equal(end.domain_context, null); + // v1.2: empty domain becomes [] (not null) — always an array on disk. + assert.deepEqual(end.domain_context, []); }); });