From 7b0afdb5410beb8138d7b4ee2b6f38551742fd1a Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 1 May 2026 21:22:51 +0200 Subject: [PATCH] feat(ai-psychosis): add v1.2 thresholds and domain-stakes table --- plugins/ai-psychosis/hooks/scripts/lib.mjs | 41 ++++++++++++ plugins/ai-psychosis/tests/lib.test.mjs | 78 ++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 plugins/ai-psychosis/tests/lib.test.mjs diff --git a/plugins/ai-psychosis/hooks/scripts/lib.mjs b/plugins/ai-psychosis/hooks/scripts/lib.mjs index a7b6bcf..4610ff3 100644 --- a/plugins/ai-psychosis/hooks/scripts/lib.mjs +++ b/plugins/ai-psychosis/hooks/scripts/lib.mjs @@ -131,6 +131,47 @@ export const COOLDOWN_HARD = 3600; // v1.1.0 — counting threshold; tier-reduction logic is v1.2 scope export const THRESHOLD_PUSHBACK_FLAGS = 2; +// --- v1.2 thresholds and domain-stakes table --- +// +// Sources: Anthropic guidance paper Appendix (April 2026), Figure A1 (stakes), +// Figure A4 (domain pushback rates). All domain identifiers are SINGULAR to +// match v1.1.0's `state.domain_context = 'relationship'` convention. + +export const TIER1_TURN_THRESHOLD = 15; +export const TIER2_SESSION_THRESHOLD = 3; +export const THRESHOLD_VALSEEK_FLAGS = 3; + +// Domain stakes weights — Figure A1 high/very-high stakes domains carry +// higher multipliers; consumer/personal_dev are baseline 1.0. +export const DOMAIN_STAKES = Object.freeze({ + 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, + default: 1.0 +}); + +// Pushback in these domains signals validation-pressing (Figure A4 — relationships +// 21%, spirituality 19%); pushback alert fires. +export const HIGH_SYCOPHANCY_DOMAINS = Object.freeze(['relationship', 'spirituality']); + +// High-stakes guidance domains (Figure A1 high/very-high). Tier-1 user-info +// alert fires only when domain_context intersects this set. +export const HIGH_STAKES_DOMAINS = Object.freeze(['legal', 'parenting', 'health', 'financial']); + +// Info-seeking domains where pushback signals healthy self-advocacy (Figure A4 — +// parenting 7.9%, legal/health/financial 80–94% pushback rate). Pushback alert +// is suppressed when domain_context is entirely within this set. +export const INFO_DOMAINS = Object.freeze(['legal', 'parenting', 'health', 'financial', 'professional']); + // --- Session counting --- export function sessionsToday() { diff --git a/plugins/ai-psychosis/tests/lib.test.mjs b/plugins/ai-psychosis/tests/lib.test.mjs new file mode 100644 index 0000000..0e59fb6 --- /dev/null +++ b/plugins/ai-psychosis/tests/lib.test.mjs @@ -0,0 +1,78 @@ +// Unit tests for shared library constants and helpers. +// Sanity-checks that v1.2 thresholds and domain-stakes table are exported +// with the expected shape. Detector-level behaviour is covered in +// per-detector test files (user-info, validation-seeking, stakes-matrix). + +import { test, describe } from 'node:test'; +import assert from 'node:assert/strict'; + +import { + TIER1_TURN_THRESHOLD, + TIER2_SESSION_THRESHOLD, + THRESHOLD_VALSEEK_FLAGS, + DOMAIN_STAKES, + HIGH_SYCOPHANCY_DOMAINS, + HIGH_STAKES_DOMAINS, + INFO_DOMAINS, +} from '../hooks/scripts/lib.mjs'; + +describe('v1.2 thresholds', () => { + test('tier-1 turn threshold is 15', () => { + assert.equal(TIER1_TURN_THRESHOLD, 15); + }); + + test('tier-2 session threshold is 3', () => { + assert.equal(TIER2_SESSION_THRESHOLD, 3); + }); + + test('valseek high-stakes flag threshold is 3', () => { + assert.equal(THRESHOLD_VALSEEK_FLAGS, 3); + }); +}); + +describe('DOMAIN_STAKES table', () => { + test('default weight is 1.0', () => { + assert.equal(DOMAIN_STAKES.default, 1.0); + }); + + test('high-stakes domains weighted 1.5', () => { + assert.equal(DOMAIN_STAKES.legal, 1.5); + assert.equal(DOMAIN_STAKES.parenting, 1.5); + assert.equal(DOMAIN_STAKES.health, 1.5); + assert.equal(DOMAIN_STAKES.financial, 1.5); + }); + + test('high-sycophancy domains weighted between 1.2 and 1.3', () => { + assert.equal(DOMAIN_STAKES.relationship, 1.3); + assert.equal(DOMAIN_STAKES.spirituality, 1.2); + }); + + test('table is frozen (immutable)', () => { + assert.equal(Object.isFrozen(DOMAIN_STAKES), true); + }); + + test('uses singular domain identifiers (relationship, not relationships)', () => { + assert.equal(DOMAIN_STAKES.relationship, 1.3); + assert.equal(DOMAIN_STAKES.relationships, undefined); + }); +}); + +describe('domain classification arrays', () => { + test('HIGH_SYCOPHANCY_DOMAINS contains relationship and spirituality', () => { + assert.deepEqual([...HIGH_SYCOPHANCY_DOMAINS], ['relationship', 'spirituality']); + assert.equal(Object.isFrozen(HIGH_SYCOPHANCY_DOMAINS), true); + }); + + test('HIGH_STAKES_DOMAINS contains legal, parenting, health, financial', () => { + assert.deepEqual([...HIGH_STAKES_DOMAINS], ['legal', 'parenting', 'health', 'financial']); + assert.equal(Object.isFrozen(HIGH_STAKES_DOMAINS), true); + }); + + test('INFO_DOMAINS adds professional to HIGH_STAKES_DOMAINS', () => { + assert.deepEqual( + [...INFO_DOMAINS], + ['legal', 'parenting', 'health', 'financial', 'professional'] + ); + assert.equal(Object.isFrozen(INFO_DOMAINS), true); + }); +});