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`,
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
{"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`.
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.
@ -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:
- `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
- `total_end_records` — number of complete sessions
- `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
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
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 |
|--------|----------|
| Relationship-flavored | {relationship_domain_count} |
| Other / not classified | {null_domain_count + other_domain_count} |
| Relationship | {domain_breakdown.relationship} |
| 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
patterns associated with relational decision support appeared at least once
during the session, not that the entire session was about relationships.
Skip rows with count 0 unless none have data, in which case show
"No domain context recorded." Domain detection is heuristic and conservative
— 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)

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', () => {
const fixture = [
{ 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.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');
});