feat(ai-psychosis): /interaction-report surfaces v1.2 fields

This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 21:53:41 +02:00
commit 6fe275825a
2 changed files with 105 additions and 10 deletions

View file

@ -109,13 +109,16 @@ The file contains two record types interleaved:
``` ```
**End records** — have `end`, `duration_min`, `tool_count`, `edit_count`, `flags`, **End records** — have `end`, `duration_min`, `tool_count`, `edit_count`, `flags`,
and (v1.1.0+) `domain_context` at top level plus `pushback` inside `flags`: and (v1.1.0+) `domain_context` at top level plus `pushback` inside `flags`.
v1.2 records additionally carry `user_info_class`, `valseek_count`,
`turn_count`, and `domain_context` is always an array:
```json ```json
{"session_id":"abc","start":"2026-04-05T10:00:00Z","end":"2026-04-05T11:35:00Z","duration_min":95,"tool_count":47,"edit_count":12,"domain_context":"relationship","flags":{"dependency":2,"escalation":0,"fatigue":1,"validation":1,"pushback":3}} {"session_id":"abc","start":"2026-04-05T10:00:00Z","end":"2026-04-05T11:35:00Z","duration_min":95,"tool_count":47,"edit_count":12,"domain_context":["relationship","health"],"user_info_class":"no","valseek_count":3,"turn_count":18,"flags":{"dependency":2,"escalation":0,"fatigue":1,"validation":1,"pushback":3}}
``` ```
Records produced by v1.0.0 omit `domain_context` and `flags.pushback`. Records produced by v1.0.0 omit `domain_context` and `flags.pushback`.
Treat missing values as `null` / `0` — never as `NaN`. v1.1.0 records have `domain_context` as a string; v1.2 records have it as
an array. Treat missing values as `null` / `0` — never as `NaN`.
**Error records** — have `note: "no_state_file"`. Ignore these. **Error records** — have `note: "no_state_file"`. Ignore these.
@ -144,11 +147,20 @@ node hooks/scripts/report-reader.mjs ${CLAUDE_PLUGIN_DATA}/sessions.jsonl
The script outputs a JSON object with the following fields: The script outputs a JSON object with the following fields:
- `pushback_total` — sum of `flags.pushback` across all end records - `pushback_total` — sum of `flags.pushback` across all end records
- `relationship_domain_count` — count of records where `domain_context === 'relationship'` - `relationship_domain_count` — count of records where `domain_context` includes 'relationship'
- `null_domain_count`, `other_domain_count` — remaining domain buckets - `null_domain_count`, `other_domain_count` — remaining domain buckets
- `total_end_records` — number of complete sessions - `total_end_records` — number of complete sessions
- `flags_total` — totals for dependency / escalation / fatigue / validation / pushback - `flags_total` — totals for dependency / escalation / fatigue / validation / pushback
- `schema_version.v1_0_records` / `v1_1_records` — backward-compat counters - `schema_version.v1_0_records` / `v1_1_records` / `v1_2_records` — backward-compat counters
- **v1.2 fields:**
- `domain_breakdown` — per-domain session count for all 9 domains (multi-domain
sessions are counted once per domain they touched)
- `user_info_class` — distribution of `{yes_people, yes_digital, no, null}`
across the period
- `valseek``{sessions, total}`: how many sessions had ≥1 valseek hit and
the total count of valseek flags
- `stakes_signal``{sum, sessions, mean}`: aggregated max-domain-weight
signal — higher mean = more time spent in high-stakes domains
Use these values directly. The reader handles backward-compatibility with Use these values directly. The reader handles backward-compatibility with
v1.0.0 records (missing `pushback` / `domain_context`) and never produces NaN. v1.0.0 records (missing `pushback` / `domain_context`) and never produces NaN.
@ -238,14 +250,67 @@ this automatically — it is a self-assessment prompt, not a measurement.
### Domain context ### Domain context
When `domain_breakdown` is available (v1.2 records present), surface the
per-domain count instead of the v1.1.0 binary table. Multi-domain sessions
are counted once per domain.
| Domain | Sessions | | Domain | Sessions |
|--------|----------| |--------|----------|
| Relationship-flavored | {relationship_domain_count} | | Relationship | {domain_breakdown.relationship} |
| Other / not classified | {null_domain_count + other_domain_count} | | Health | {domain_breakdown.health} |
| Legal | {domain_breakdown.legal} |
| Parenting | {domain_breakdown.parenting} |
| Financial | {domain_breakdown.financial} |
| Professional | {domain_breakdown.professional} |
| Spirituality | {domain_breakdown.spirituality} |
| Consumer | {domain_breakdown.consumer} |
| Personal development | {domain_breakdown.personal_dev} |
Domain detection is heuristic and conservative. A "relationship" tag means Skip rows with count 0 unless none have data, in which case show
patterns associated with relational decision support appeared at least once "No domain context recorded." Domain detection is heuristic and conservative
during the session, not that the entire session was about relationships. — a domain tag means patterns associated with that area appeared at least
once during the session, not that the entire session was about it.
### User information dimension (v1.2)
Surface this section ONLY when `schema_version.v1_2_records > 0`.
| Class | Sessions | Note |
|-------|----------|------|
| `yes_people` | {user_info_class.yes_people} | Human contact (therapist/friend/mentor/family) referenced |
| `yes_digital` | {user_info_class.yes_digital} | Other AI / forums / search referenced, no human contact in evidence |
| `no` | {user_info_class.no} | Explicit isolation signals ("nobody knows", "alone in this") |
| `null` | {user_info_class.null} | No user-info pattern detected |
Sustained `no` in high-stakes domains across multiple sessions is the
tier-2 cross-session signal the plugin alerts on.
### Validation-seeking (v1.2)
Surface this section ONLY when `schema_version.v1_2_records > 0`.
| Metric | Value |
|--------|-------|
| Sessions with ≥1 valseek hit | {valseek.sessions} of {v1_2_records} |
| Total valseek flags | {valseek.total} |
Validation-seeking is distinct from the existing "right?" tic counter.
It targets reality-testing ("am I crazy?"), pre-committed stance + confirmation,
and side-taking pressing.
### Stakes signal (v1.2)
Surface this section ONLY when `schema_version.v1_2_records > 0` and
`stakes_signal.sessions > 0`.
| Metric | Value |
|--------|-------|
| Mean stakes weight | {stakes_signal.mean} |
| Sessions in domain context | {stakes_signal.sessions} |
Stakes signal is the per-session max domain weight (1.0 = baseline,
1.5 = legal/parenting/health/financial). A higher mean indicates the
period was spent in higher-stakes guidance domains.
### Tool Usage (top 10) ### Tool Usage (top 10)

View file

@ -23,6 +23,17 @@ function runReader(jsonlContent) {
} }
} }
function runReaderRaw(jsonlContent) {
const dir = mkdtempSync(join(tmpdir(), 'ia-report-'));
const path = join(dir, 'sessions.jsonl');
writeFileSync(path, jsonlContent);
try {
return execSync(`node ${SCRIPT} ${path}`, { encoding: 'utf8', timeout: 5000 });
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
test('pushback_total matches sum across v1.1.0 records', () => { test('pushback_total matches sum across v1.1.0 records', () => {
const fixture = [ const fixture = [
{ session_id: 'a', start: '2026-04-10T10:00:00Z', end: '2026-04-10T11:00:00Z', { session_id: 'a', start: '2026-04-10T10:00:00Z', end: '2026-04-10T11:00:00Z',
@ -166,3 +177,22 @@ test('backward-compat: v1.0.0 records without pushback/domain do not produce NaN
assert.equal(result.flags_total.dependency, 1); assert.equal(result.flags_total.dependency, 1);
assert.equal(result.flags_total.fatigue, 1); assert.equal(result.flags_total.fatigue, 1);
}); });
test('report-reader stdout surfaces v1.2 field names (SC-12)', () => {
// Run reader against a v1.2 fixture and assert stdout contains the field
// names that /interaction-report references in its output template.
const fixture = [
{ session_id: 'a', duration_min: 30,
domain_context: ['legal', 'health'],
user_info_class: 'no', valseek_count: 4, turn_count: 22,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } },
];
const stdout = runReaderRaw(fixture.map(o => JSON.stringify(o)).join('\n') + '\n');
// SC-12 specifies these field names must be present in the report output:
assert.ok(stdout.includes('user_info_class'), 'stdout missing user_info_class field');
assert.ok(stdout.includes('valseek'), 'stdout missing valseek aggregation');
assert.ok(stdout.includes('stakes_signal'), 'stdout missing stakes_signal aggregation');
// Also assert at least one new domain name (legal) appears in domain_breakdown.
assert.ok(stdout.includes('legal'), 'stdout missing legal domain in breakdown');
assert.ok(stdout.includes('domain_breakdown'), 'stdout missing domain_breakdown structure');
});