@atlasent/sdk 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/behavior.cjs CHANGED
@@ -102,7 +102,8 @@ function redactStateSnapshot(s) {
102
102
  pressure_level: s.pressure_level,
103
103
  body_state: s.body_state,
104
104
  cognitive_load: s.cognitive_load,
105
- readiness_level: s.readiness_level
105
+ readiness_level: s.readiness_level,
106
+ ...s.emotional_vector !== void 0 ? { emotional_vector: s.emotional_vector } : {}
106
107
  };
107
108
  }
108
109
  var ConsentDeniedError = class extends Error {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/behavior.ts"],"sourcesContent":["/**\n * @atlasent/sdk/behavior — consent + redaction helpers for the v2\n * Behavior Conditioning Layer.\n *\n * See `atlasent-docs/docs/V2_BEHAVIOR_CONDITIONING_LAYER.md` for the\n * architecture this module implements.\n *\n * Quick start:\n *\n * ```ts\n * import {\n * ConsentManager,\n * InMemoryBehaviorLedger,\n * redactStateSnapshot,\n * type StateSnapshot,\n * } from \"@atlasent/sdk/behavior\";\n *\n * const consent = new ConsentManager({ userId: \"u_123\" });\n * const ledger = new InMemoryBehaviorLedger();\n *\n * const snapshot: StateSnapshot = { ... };\n * const summary = redactStateSnapshot(snapshot);\n *\n * if (consent.canEmit(\"ledgers-me\", \"behavior.health.mental\")) {\n * await ledger.emit({\n * user_id: snapshot.user_id,\n * source: \"hicoach\",\n * category: \"behavior.health.mental\",\n * entry_state_summary: summary,\n * exit_state_summary: null,\n * relief_delta: null,\n * confidence_score: 1,\n * timestamp: new Date().toISOString(),\n * });\n * }\n * ```\n *\n * The MVP is pure and on-device — no HTTP calls. A future\n * `RemoteBehaviorLedger` will POST to `/v1/behavior/events` once\n * the atlasent-api endpoint ships.\n */\n\n// ---------------------------------------------------------------------------\n// Sensitive-category vocabulary\n// ---------------------------------------------------------------------------\n\n/**\n * Sensitive-category slugs used by behavior policies. Keep in sync\n * with `gxp-starter/packs/hipaa/` rules and\n * `atlasent-api`'s `target.category` field.\n */\nexport type SensitiveCategory =\n | \"behavior.health.mental\"\n | \"behavior.health.adherence\"\n | \"behavior.financial\"\n | \"behavior.minor\";\n\nexport const SENSITIVE_CATEGORIES: readonly SensitiveCategory[] = [\n \"behavior.health.mental\",\n \"behavior.health.adherence\",\n \"behavior.financial\",\n \"behavior.minor\",\n] as const;\n\n// ---------------------------------------------------------------------------\n// Domain types — mirror the model in `bettyc925/hicoach/lib/hicoach/types.ts`.\n// ---------------------------------------------------------------------------\n\nexport type EmotionalState =\n | \"tense\"\n | \"anxious\"\n | \"overwhelmed\"\n | \"flat\"\n | \"frustrated\"\n | \"uncertain\"\n | \"tired\"\n | \"okay\";\n\nexport type BodyState =\n | \"tight\"\n | \"heavy\"\n | \"restless\"\n | \"numb\"\n | \"buzzing\"\n | \"settled\";\n\nexport type ReadinessLevel = \"low\" | \"medium\" | \"high\";\n\n/**\n * A single moment captured before/after a regulation session.\n *\n * This is the **on-device** shape with all raw fields. It is never\n * emitted to a ledger; only the {@link StateEventSummary} projection\n * crosses an app boundary.\n */\nexport interface StateSnapshot {\n id: string;\n user_id: string;\n emotional_state: EmotionalState;\n /** 0..10 */\n intensity: number;\n /** 0..10 */\n stress_level: number;\n /** 0..10 */\n pressure_level: number;\n body_state: BodyState;\n /** 0..10 */\n cognitive_load: number;\n readiness_level: ReadinessLevel;\n /** 0..1 — how sure the user feels about the rating */\n confidence_score: number;\n /** ISO 8601 */\n created_at: string;\n /** Optional free-form note. NEVER part of the redacted summary. */\n note?: string;\n}\n\n/**\n * Redacted summary projection — the only shape that crosses an app\n * boundary. Contains no raw text, no IDs, no timestamps.\n */\nexport interface StateEventSummary {\n emotional_state: EmotionalState;\n intensity: number;\n stress_level: number;\n pressure_level: number;\n body_state: BodyState;\n cognitive_load: number;\n readiness_level: ReadinessLevel;\n}\n\n/**\n * A behavior event written to the cross-app ledger (LedgersMe).\n * The atlasent-api `/v1/behavior/events` endpoint accepts this shape.\n */\nexport interface BehaviorEvent {\n user_id: string;\n source: \"hicoach\" | \"echobloom\" | \"ledgers-me\" | string;\n category: SensitiveCategory;\n entry_state_summary: StateEventSummary;\n exit_state_summary: StateEventSummary | null;\n relief_delta: number | null;\n /** 0..1 */\n confidence_score: number;\n /** ISO 8601 */\n timestamp: string;\n /** Optional list of safety signals that fired during the event. Never raw text. */\n safety_signals?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Consent\n// ---------------------------------------------------------------------------\n\n/**\n * Per-user consent settings. Privacy-first defaults: nothing leaves\n * the device unless the user explicitly opts in.\n */\nexport interface ConsentSettings {\n /** Default false. Opt-in to emit summaries to the ledger. */\n share_state_summaries: boolean;\n /**\n * Default false. When true, suppresses ALL outbound emissions\n * regardless of any other setting. Acts as a global circuit\n * breaker.\n */\n private_only_mode: boolean;\n /**\n * Optional per-receiver allowlist. When non-empty, an event is\n * only emitted to a receiver whose name appears here AND for a\n * category whose slug appears in the receiver's allowed set.\n *\n * Example:\n * ```ts\n * { \"ledgers-me\": [\"behavior.health.mental\"] }\n * ```\n */\n receivers?: Record<string, SensitiveCategory[]>;\n}\n\nexport const DEFAULT_CONSENT: ConsentSettings = Object.freeze({\n share_state_summaries: false,\n private_only_mode: false,\n});\n\n/**\n * Storage abstraction so the helper works in browser\n * (`window.localStorage`), Node, and tests.\n */\nexport interface ConsentStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n}\n\n/**\n * Default in-memory storage (for Node + tests). In a browser, pass\n * `window.localStorage`.\n */\nexport class MemoryStorage implements ConsentStorage {\n private store = new Map<string, string>();\n get(key: string): string | null {\n return this.store.get(key) ?? null;\n }\n set(key: string, value: string): void {\n this.store.set(key, value);\n }\n}\n\nexport interface ConsentManagerOpts {\n userId: string;\n /** Defaults to {@link MemoryStorage}. Pass `localStorage` in browsers. */\n storage?: ConsentStorage;\n /** Defaults to {@link DEFAULT_CONSENT}. */\n defaults?: ConsentSettings;\n}\n\n/**\n * Read/write consent settings; gate emissions through `canEmit`.\n *\n * Apps NEVER hand-roll consent checks. This is the only correct way\n * to decide whether a `BehaviorEvent` may leave the device.\n */\nexport class ConsentManager {\n private readonly key: string;\n private readonly storage: ConsentStorage;\n private readonly defaults: ConsentSettings;\n\n constructor(opts: ConsentManagerOpts) {\n this.key = `atlasent.behavior.consent.${opts.userId}`;\n this.storage = opts.storage ?? new MemoryStorage();\n this.defaults = opts.defaults ?? DEFAULT_CONSENT;\n }\n\n get(): ConsentSettings {\n const raw = this.storage.get(this.key);\n if (!raw) return { ...this.defaults };\n try {\n const parsed = JSON.parse(raw) as Partial<ConsentSettings>;\n return { ...this.defaults, ...parsed };\n } catch {\n return { ...this.defaults };\n }\n }\n\n set(patch: Partial<ConsentSettings>): ConsentSettings {\n const next = { ...this.get(), ...patch };\n this.storage.set(this.key, JSON.stringify(next));\n return next;\n }\n\n /**\n * The single decision point: may we emit a `BehaviorEvent` for\n * this `category` to this `receiver`? Returns `false` whenever\n * any of the following is true:\n *\n * - `private_only_mode` is on.\n * - `share_state_summaries` is off.\n * - A `receivers` allowlist exists and the `(receiver, category)`\n * pair is not in it.\n */\n canEmit(receiver: string, category: SensitiveCategory): boolean {\n const c = this.get();\n if (c.private_only_mode) return false;\n if (!c.share_state_summaries) return false;\n if (c.receivers) {\n const allowed = c.receivers[receiver] ?? [];\n if (!allowed.includes(category)) return false;\n }\n return true;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Redaction\n// ---------------------------------------------------------------------------\n\n/**\n * Project a {@link StateSnapshot} down to the redacted\n * {@link StateEventSummary} shape. Drops `id`, `user_id`,\n * `created_at`, `confidence_score`, and any `note` field. The\n * remaining fields are bounded numeric ranges or closed enums and\n * carry no free-form text.\n */\nexport function redactStateSnapshot(s: StateSnapshot): StateEventSummary {\n return {\n emotional_state: s.emotional_state,\n intensity: s.intensity,\n stress_level: s.stress_level,\n pressure_level: s.pressure_level,\n body_state: s.body_state,\n cognitive_load: s.cognitive_load,\n readiness_level: s.readiness_level,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Ledger\n// ---------------------------------------------------------------------------\n\nexport interface BehaviorLedger {\n /**\n * Emit a behavior event. Implementations MUST validate `consent`\n * before persisting and throw `ConsentDeniedError` when an event\n * would be persisted in violation of the user's settings.\n */\n emit(event: BehaviorEvent): Promise<void>;\n}\n\nexport class ConsentDeniedError extends Error {\n readonly code = \"consent_denied\" as const;\n constructor(\n public readonly receiver: string,\n public readonly category: SensitiveCategory,\n ) {\n super(\n `Consent denies emit to receiver=${receiver} category=${category}`,\n );\n this.name = \"ConsentDeniedError\";\n }\n}\n\n/**\n * On-device ledger for development and demos. A future\n * `RemoteBehaviorLedger` will POST to atlasent-api's\n * `/v1/behavior/events` once that endpoint ships.\n */\nexport class InMemoryBehaviorLedger implements BehaviorLedger {\n private readonly events: BehaviorEvent[] = [];\n constructor(\n private readonly opts: {\n consent: ConsentManager;\n receiver?: string;\n },\n ) {}\n\n async emit(event: BehaviorEvent): Promise<void> {\n const receiver = this.opts.receiver ?? \"in-memory\";\n if (!this.opts.consent.canEmit(receiver, event.category)) {\n throw new ConsentDeniedError(receiver, event.category);\n }\n this.events.push(event);\n }\n\n /** Read all events accepted so far. Test/demo helper. */\n list(): readonly BehaviorEvent[] {\n return [...this.events];\n }\n\n /** Clear the in-memory store. Test helper. */\n clear(): void {\n this.events.length = 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// State-event cache (for biasing AI suggestions with recent context)\n// ---------------------------------------------------------------------------\n\n/**\n * Bounded in-memory ring buffer of recent {@link StateEventSummary}\n * values. The LangChain/LlamaIndex middleware (and similar wrappers)\n * read this to attach `context.session_history` to evaluate calls\n * without ever touching raw snapshots.\n */\nexport class StateEventCache {\n private readonly buf: StateEventSummary[] = [];\n constructor(private readonly capacity: number = 10) {\n if (capacity <= 0) {\n throw new RangeError(\"capacity must be > 0\");\n }\n }\n\n add(summary: StateEventSummary): void {\n this.buf.push(summary);\n if (this.buf.length > this.capacity) this.buf.shift();\n }\n\n recent(n?: number): readonly StateEventSummary[] {\n const k = n ?? this.buf.length;\n return this.buf.slice(-k);\n }\n\n clear(): void {\n this.buf.length = 0;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/behavior.ts"],"sourcesContent":["/**\n * @atlasent/sdk/behavior — consent + redaction helpers for the v2\n * Behavior Conditioning Layer.\n *\n * See `atlasent-docs/docs/V2_BEHAVIOR_CONDITIONING_LAYER.md` for the\n * architecture this module implements.\n *\n * Quick start:\n *\n * ```ts\n * import {\n * ConsentManager,\n * InMemoryBehaviorLedger,\n * redactStateSnapshot,\n * type StateSnapshot,\n * } from \"@atlasent/sdk/behavior\";\n *\n * const consent = new ConsentManager({ userId: \"u_123\" });\n * const ledger = new InMemoryBehaviorLedger();\n *\n * const snapshot: StateSnapshot = {\n * ...\n * emotional_vector: { valence: 0.2, arousal: 0.8, dominance: 0.3 },\n * };\n * const summary = redactStateSnapshot(snapshot);\n *\n * if (consent.canEmit(\"ledgers-me\", \"behavior.health.mental\")) {\n * await ledger.emit({\n * user_id: snapshot.user_id,\n * source: \"hicoach\",\n * category: \"behavior.health.mental\",\n * entry_state_summary: summary,\n * exit_state_summary: null,\n * relief_delta: null,\n * confidence_score: 1,\n * timestamp: new Date().toISOString(),\n * });\n * }\n * ```\n *\n * The MVP is pure and on-device — no HTTP calls. A future\n * `RemoteBehaviorLedger` will POST to `/v1/behavior/events` once\n * the atlasent-api endpoint ships.\n */\n\n// ---------------------------------------------------------------------------\n// Sensitive-category vocabulary\n// ---------------------------------------------------------------------------\n\n/**\n * Sensitive-category slugs used by behavior policies. Keep in sync\n * with `gxp-starter/packs/hipaa/` rules and\n * `atlasent-api`'s `target.category` field.\n */\nexport type SensitiveCategory =\n | \"behavior.health.mental\"\n | \"behavior.health.adherence\"\n | \"behavior.financial\"\n | \"behavior.minor\";\n\nexport const SENSITIVE_CATEGORIES: readonly SensitiveCategory[] = [\n \"behavior.health.mental\",\n \"behavior.health.adherence\",\n \"behavior.financial\",\n \"behavior.minor\",\n] as const;\n\n// ---------------------------------------------------------------------------\n// Domain types — mirror the model in `bettyc925/hicoach/lib/hicoach/types.ts`.\n// ---------------------------------------------------------------------------\n\nexport type EmotionalState =\n | \"tense\"\n | \"anxious\"\n | \"overwhelmed\"\n | \"flat\"\n | \"frustrated\"\n | \"uncertain\"\n | \"tired\"\n | \"okay\";\n\nexport type BodyState =\n | \"tight\"\n | \"heavy\"\n | \"restless\"\n | \"numb\"\n | \"buzzing\"\n | \"settled\";\n\nexport type ReadinessLevel = \"low\" | \"medium\" | \"high\";\n\n/**\n * Dimensional emotional coordinates in PAD space, each bounded 0..1.\n *\n * - `valence`: negative (0) → positive (1) affect\n * - `arousal`: calm (0) → activated (1)\n * - `dominance`: submissive (0) → in-control (1)\n *\n * All dimensions are bounded floats with no free-form text, so the\n * vector is safe to include in a {@link StateEventSummary} that\n * crosses an app boundary.\n */\nexport interface EmotionalVector {\n /** 0..1 — negative → positive affect */\n valence: number;\n /** 0..1 — calm → activated */\n arousal: number;\n /** 0..1 — submissive → in-control */\n dominance: number;\n}\n\n/**\n * A single moment captured before/after a regulation session.\n *\n * This is the **on-device** shape with all raw fields. It is never\n * emitted to a ledger; only the {@link StateEventSummary} projection\n * crosses an app boundary.\n */\nexport interface StateSnapshot {\n id: string;\n user_id: string;\n emotional_state: EmotionalState;\n /** 0..10 */\n intensity: number;\n /** 0..10 */\n stress_level: number;\n /** 0..10 */\n pressure_level: number;\n body_state: BodyState;\n /** 0..10 */\n cognitive_load: number;\n readiness_level: ReadinessLevel;\n /** 0..1 — how sure the user feels about the rating */\n confidence_score: number;\n /** ISO 8601 */\n created_at: string;\n /** Optional free-form note. NEVER part of the redacted summary. */\n note?: string;\n /** Optional PAD-space coordinates. Bounded floats — safe to redact through. */\n emotional_vector?: EmotionalVector;\n}\n\n/**\n * Redacted summary projection — the only shape that crosses an app\n * boundary. Contains no raw text, no IDs, no timestamps.\n */\nexport interface StateEventSummary {\n emotional_state: EmotionalState;\n intensity: number;\n stress_level: number;\n pressure_level: number;\n body_state: BodyState;\n cognitive_load: number;\n readiness_level: ReadinessLevel;\n /** PAD-space coordinates, when the caller supplied them. */\n emotional_vector?: EmotionalVector;\n}\n\n/**\n * A behavior event written to the cross-app ledger (LedgersMe).\n * The atlasent-api `/v1/behavior/events` endpoint accepts this shape.\n */\nexport interface BehaviorEvent {\n user_id: string;\n source: \"hicoach\" | \"echobloom\" | \"ledgers-me\" | string;\n category: SensitiveCategory;\n entry_state_summary: StateEventSummary;\n exit_state_summary: StateEventSummary | null;\n relief_delta: number | null;\n /** 0..1 */\n confidence_score: number;\n /** ISO 8601 */\n timestamp: string;\n /** Optional list of safety signals that fired during the event. Never raw text. */\n safety_signals?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Consent\n// ---------------------------------------------------------------------------\n\n/**\n * Per-user consent settings. Privacy-first defaults: nothing leaves\n * the device unless the user explicitly opts in.\n */\nexport interface ConsentSettings {\n /** Default false. Opt-in to emit summaries to the ledger. */\n share_state_summaries: boolean;\n /**\n * Default false. When true, suppresses ALL outbound emissions\n * regardless of any other setting. Acts as a global circuit\n * breaker.\n */\n private_only_mode: boolean;\n /**\n * Optional per-receiver allowlist. When non-empty, an event is\n * only emitted to a receiver whose name appears here AND for a\n * category whose slug appears in the receiver's allowed set.\n *\n * Example:\n * ```ts\n * { \"ledgers-me\": [\"behavior.health.mental\"] }\n * ```\n */\n receivers?: Record<string, SensitiveCategory[]>;\n}\n\nexport const DEFAULT_CONSENT: ConsentSettings = Object.freeze({\n share_state_summaries: false,\n private_only_mode: false,\n});\n\n/**\n * Storage abstraction so the helper works in browser\n * (`window.localStorage`), Node, and tests.\n */\nexport interface ConsentStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n}\n\n/**\n * Default in-memory storage (for Node + tests). In a browser, pass\n * `window.localStorage`.\n */\nexport class MemoryStorage implements ConsentStorage {\n private store = new Map<string, string>();\n get(key: string): string | null {\n return this.store.get(key) ?? null;\n }\n set(key: string, value: string): void {\n this.store.set(key, value);\n }\n}\n\nexport interface ConsentManagerOpts {\n userId: string;\n /** Defaults to {@link MemoryStorage}. Pass `localStorage` in browsers. */\n storage?: ConsentStorage;\n /** Defaults to {@link DEFAULT_CONSENT}. */\n defaults?: ConsentSettings;\n}\n\n/**\n * Read/write consent settings; gate emissions through `canEmit`.\n *\n * Apps NEVER hand-roll consent checks. This is the only correct way\n * to decide whether a `BehaviorEvent` may leave the device.\n */\nexport class ConsentManager {\n private readonly key: string;\n private readonly storage: ConsentStorage;\n private readonly defaults: ConsentSettings;\n\n constructor(opts: ConsentManagerOpts) {\n this.key = `atlasent.behavior.consent.${opts.userId}`;\n this.storage = opts.storage ?? new MemoryStorage();\n this.defaults = opts.defaults ?? DEFAULT_CONSENT;\n }\n\n get(): ConsentSettings {\n const raw = this.storage.get(this.key);\n if (!raw) return { ...this.defaults };\n try {\n const parsed = JSON.parse(raw) as Partial<ConsentSettings>;\n return { ...this.defaults, ...parsed };\n } catch {\n return { ...this.defaults };\n }\n }\n\n set(patch: Partial<ConsentSettings>): ConsentSettings {\n const next = { ...this.get(), ...patch };\n this.storage.set(this.key, JSON.stringify(next));\n return next;\n }\n\n /**\n * The single decision point: may we emit a `BehaviorEvent` for\n * this `category` to this `receiver`? Returns `false` whenever\n * any of the following is true:\n *\n * - `private_only_mode` is on.\n * - `share_state_summaries` is off.\n * - A `receivers` allowlist exists and the `(receiver, category)`\n * pair is not in it.\n */\n canEmit(receiver: string, category: SensitiveCategory): boolean {\n const c = this.get();\n if (c.private_only_mode) return false;\n if (!c.share_state_summaries) return false;\n if (c.receivers) {\n const allowed = c.receivers[receiver] ?? [];\n if (!allowed.includes(category)) return false;\n }\n return true;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Redaction\n// ---------------------------------------------------------------------------\n\n/**\n * Project a {@link StateSnapshot} down to the redacted\n * {@link StateEventSummary} shape. Drops `id`, `user_id`,\n * `created_at`, `confidence_score`, and any `note` field.\n *\n * `emotional_vector`, when present, is passed through unchanged —\n * it contains only bounded floats and is safe to cross an app boundary.\n */\nexport function redactStateSnapshot(s: StateSnapshot): StateEventSummary {\n return {\n emotional_state: s.emotional_state,\n intensity: s.intensity,\n stress_level: s.stress_level,\n pressure_level: s.pressure_level,\n body_state: s.body_state,\n cognitive_load: s.cognitive_load,\n readiness_level: s.readiness_level,\n ...(s.emotional_vector !== undefined\n ? { emotional_vector: s.emotional_vector }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Ledger\n// ---------------------------------------------------------------------------\n\nexport interface BehaviorLedger {\n /**\n * Emit a behavior event. Implementations MUST validate `consent`\n * before persisting and throw `ConsentDeniedError` when an event\n * would be persisted in violation of the user's settings.\n */\n emit(event: BehaviorEvent): Promise<void>;\n}\n\nexport class ConsentDeniedError extends Error {\n readonly code = \"consent_denied\" as const;\n constructor(\n public readonly receiver: string,\n public readonly category: SensitiveCategory,\n ) {\n super(\n `Consent denies emit to receiver=${receiver} category=${category}`,\n );\n this.name = \"ConsentDeniedError\";\n }\n}\n\n/**\n * On-device ledger for development and demos. A future\n * `RemoteBehaviorLedger` will POST to atlasent-api's\n * `/v1/behavior/events` once that endpoint ships.\n */\nexport class InMemoryBehaviorLedger implements BehaviorLedger {\n private readonly events: BehaviorEvent[] = [];\n constructor(\n private readonly opts: {\n consent: ConsentManager;\n receiver?: string;\n },\n ) {}\n\n async emit(event: BehaviorEvent): Promise<void> {\n const receiver = this.opts.receiver ?? \"in-memory\";\n if (!this.opts.consent.canEmit(receiver, event.category)) {\n throw new ConsentDeniedError(receiver, event.category);\n }\n this.events.push(event);\n }\n\n /** Read all events accepted so far. Test/demo helper. */\n list(): readonly BehaviorEvent[] {\n return [...this.events];\n }\n\n /** Clear the in-memory store. Test helper. */\n clear(): void {\n this.events.length = 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// State-event cache (for biasing AI suggestions with recent context)\n// ---------------------------------------------------------------------------\n\n/**\n * Bounded in-memory ring buffer of recent {@link StateEventSummary}\n * values. The LangChain/LlamaIndex middleware (and similar wrappers)\n * read this to attach `context.session_history` to evaluate calls\n * without ever touching raw snapshots.\n */\nexport class StateEventCache {\n private readonly buf: StateEventSummary[] = [];\n constructor(private readonly capacity: number = 10) {\n if (capacity <= 0) {\n throw new RangeError(\"capacity must be > 0\");\n }\n }\n\n add(summary: StateEventSummary): void {\n this.buf.push(summary);\n if (this.buf.length > this.capacity) this.buf.shift();\n }\n\n recent(n?: number): readonly StateEventSummary[] {\n const k = n ?? this.buf.length;\n return this.buf.slice(-k);\n }\n\n clear(): void {\n this.buf.length = 0;\n }\n}\n"],"mappings":"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","names":[]}
@@ -18,7 +18,10 @@
18
18
  * const consent = new ConsentManager({ userId: "u_123" });
19
19
  * const ledger = new InMemoryBehaviorLedger();
20
20
  *
21
- * const snapshot: StateSnapshot = { ... };
21
+ * const snapshot: StateSnapshot = {
22
+ * ...
23
+ * emotional_vector: { valence: 0.2, arousal: 0.8, dominance: 0.3 },
24
+ * };
22
25
  * const summary = redactStateSnapshot(snapshot);
23
26
  *
24
27
  * if (consent.canEmit("ledgers-me", "behavior.health.mental")) {
@@ -49,6 +52,25 @@ declare const SENSITIVE_CATEGORIES: readonly SensitiveCategory[];
49
52
  type EmotionalState = "tense" | "anxious" | "overwhelmed" | "flat" | "frustrated" | "uncertain" | "tired" | "okay";
50
53
  type BodyState = "tight" | "heavy" | "restless" | "numb" | "buzzing" | "settled";
51
54
  type ReadinessLevel = "low" | "medium" | "high";
55
+ /**
56
+ * Dimensional emotional coordinates in PAD space, each bounded 0..1.
57
+ *
58
+ * - `valence`: negative (0) → positive (1) affect
59
+ * - `arousal`: calm (0) → activated (1)
60
+ * - `dominance`: submissive (0) → in-control (1)
61
+ *
62
+ * All dimensions are bounded floats with no free-form text, so the
63
+ * vector is safe to include in a {@link StateEventSummary} that
64
+ * crosses an app boundary.
65
+ */
66
+ interface EmotionalVector {
67
+ /** 0..1 — negative → positive affect */
68
+ valence: number;
69
+ /** 0..1 — calm → activated */
70
+ arousal: number;
71
+ /** 0..1 — submissive → in-control */
72
+ dominance: number;
73
+ }
52
74
  /**
53
75
  * A single moment captured before/after a regulation session.
54
76
  *
@@ -76,6 +98,8 @@ interface StateSnapshot {
76
98
  created_at: string;
77
99
  /** Optional free-form note. NEVER part of the redacted summary. */
78
100
  note?: string;
101
+ /** Optional PAD-space coordinates. Bounded floats — safe to redact through. */
102
+ emotional_vector?: EmotionalVector;
79
103
  }
80
104
  /**
81
105
  * Redacted summary projection — the only shape that crosses an app
@@ -89,6 +113,8 @@ interface StateEventSummary {
89
113
  body_state: BodyState;
90
114
  cognitive_load: number;
91
115
  readiness_level: ReadinessLevel;
116
+ /** PAD-space coordinates, when the caller supplied them. */
117
+ emotional_vector?: EmotionalVector;
92
118
  }
93
119
  /**
94
120
  * A behavior event written to the cross-app ledger (LedgersMe).
@@ -186,9 +212,10 @@ declare class ConsentManager {
186
212
  /**
187
213
  * Project a {@link StateSnapshot} down to the redacted
188
214
  * {@link StateEventSummary} shape. Drops `id`, `user_id`,
189
- * `created_at`, `confidence_score`, and any `note` field. The
190
- * remaining fields are bounded numeric ranges or closed enums and
191
- * carry no free-form text.
215
+ * `created_at`, `confidence_score`, and any `note` field.
216
+ *
217
+ * `emotional_vector`, when present, is passed through unchanged —
218
+ * it contains only bounded floats and is safe to cross an app boundary.
192
219
  */
193
220
  declare function redactStateSnapshot(s: StateSnapshot): StateEventSummary;
194
221
  interface BehaviorLedger {
@@ -238,4 +265,4 @@ declare class StateEventCache {
238
265
  clear(): void;
239
266
  }
240
267
 
241
- export { type BehaviorEvent, type BehaviorLedger, type BodyState, ConsentDeniedError, ConsentManager, type ConsentManagerOpts, type ConsentSettings, type ConsentStorage, DEFAULT_CONSENT, type EmotionalState, InMemoryBehaviorLedger, MemoryStorage, type ReadinessLevel, SENSITIVE_CATEGORIES, type SensitiveCategory, StateEventCache, type StateEventSummary, type StateSnapshot, redactStateSnapshot };
268
+ export { type BehaviorEvent, type BehaviorLedger, type BodyState, ConsentDeniedError, ConsentManager, type ConsentManagerOpts, type ConsentSettings, type ConsentStorage, DEFAULT_CONSENT, type EmotionalState, type EmotionalVector, InMemoryBehaviorLedger, MemoryStorage, type ReadinessLevel, SENSITIVE_CATEGORIES, type SensitiveCategory, StateEventCache, type StateEventSummary, type StateSnapshot, redactStateSnapshot };
@@ -18,7 +18,10 @@
18
18
  * const consent = new ConsentManager({ userId: "u_123" });
19
19
  * const ledger = new InMemoryBehaviorLedger();
20
20
  *
21
- * const snapshot: StateSnapshot = { ... };
21
+ * const snapshot: StateSnapshot = {
22
+ * ...
23
+ * emotional_vector: { valence: 0.2, arousal: 0.8, dominance: 0.3 },
24
+ * };
22
25
  * const summary = redactStateSnapshot(snapshot);
23
26
  *
24
27
  * if (consent.canEmit("ledgers-me", "behavior.health.mental")) {
@@ -49,6 +52,25 @@ declare const SENSITIVE_CATEGORIES: readonly SensitiveCategory[];
49
52
  type EmotionalState = "tense" | "anxious" | "overwhelmed" | "flat" | "frustrated" | "uncertain" | "tired" | "okay";
50
53
  type BodyState = "tight" | "heavy" | "restless" | "numb" | "buzzing" | "settled";
51
54
  type ReadinessLevel = "low" | "medium" | "high";
55
+ /**
56
+ * Dimensional emotional coordinates in PAD space, each bounded 0..1.
57
+ *
58
+ * - `valence`: negative (0) → positive (1) affect
59
+ * - `arousal`: calm (0) → activated (1)
60
+ * - `dominance`: submissive (0) → in-control (1)
61
+ *
62
+ * All dimensions are bounded floats with no free-form text, so the
63
+ * vector is safe to include in a {@link StateEventSummary} that
64
+ * crosses an app boundary.
65
+ */
66
+ interface EmotionalVector {
67
+ /** 0..1 — negative → positive affect */
68
+ valence: number;
69
+ /** 0..1 — calm → activated */
70
+ arousal: number;
71
+ /** 0..1 — submissive → in-control */
72
+ dominance: number;
73
+ }
52
74
  /**
53
75
  * A single moment captured before/after a regulation session.
54
76
  *
@@ -76,6 +98,8 @@ interface StateSnapshot {
76
98
  created_at: string;
77
99
  /** Optional free-form note. NEVER part of the redacted summary. */
78
100
  note?: string;
101
+ /** Optional PAD-space coordinates. Bounded floats — safe to redact through. */
102
+ emotional_vector?: EmotionalVector;
79
103
  }
80
104
  /**
81
105
  * Redacted summary projection — the only shape that crosses an app
@@ -89,6 +113,8 @@ interface StateEventSummary {
89
113
  body_state: BodyState;
90
114
  cognitive_load: number;
91
115
  readiness_level: ReadinessLevel;
116
+ /** PAD-space coordinates, when the caller supplied them. */
117
+ emotional_vector?: EmotionalVector;
92
118
  }
93
119
  /**
94
120
  * A behavior event written to the cross-app ledger (LedgersMe).
@@ -186,9 +212,10 @@ declare class ConsentManager {
186
212
  /**
187
213
  * Project a {@link StateSnapshot} down to the redacted
188
214
  * {@link StateEventSummary} shape. Drops `id`, `user_id`,
189
- * `created_at`, `confidence_score`, and any `note` field. The
190
- * remaining fields are bounded numeric ranges or closed enums and
191
- * carry no free-form text.
215
+ * `created_at`, `confidence_score`, and any `note` field.
216
+ *
217
+ * `emotional_vector`, when present, is passed through unchanged —
218
+ * it contains only bounded floats and is safe to cross an app boundary.
192
219
  */
193
220
  declare function redactStateSnapshot(s: StateSnapshot): StateEventSummary;
194
221
  interface BehaviorLedger {
@@ -238,4 +265,4 @@ declare class StateEventCache {
238
265
  clear(): void;
239
266
  }
240
267
 
241
- export { type BehaviorEvent, type BehaviorLedger, type BodyState, ConsentDeniedError, ConsentManager, type ConsentManagerOpts, type ConsentSettings, type ConsentStorage, DEFAULT_CONSENT, type EmotionalState, InMemoryBehaviorLedger, MemoryStorage, type ReadinessLevel, SENSITIVE_CATEGORIES, type SensitiveCategory, StateEventCache, type StateEventSummary, type StateSnapshot, redactStateSnapshot };
268
+ export { type BehaviorEvent, type BehaviorLedger, type BodyState, ConsentDeniedError, ConsentManager, type ConsentManagerOpts, type ConsentSettings, type ConsentStorage, DEFAULT_CONSENT, type EmotionalState, type EmotionalVector, InMemoryBehaviorLedger, MemoryStorage, type ReadinessLevel, SENSITIVE_CATEGORIES, type SensitiveCategory, StateEventCache, type StateEventSummary, type StateSnapshot, redactStateSnapshot };
package/dist/behavior.js CHANGED
@@ -71,7 +71,8 @@ function redactStateSnapshot(s) {
71
71
  pressure_level: s.pressure_level,
72
72
  body_state: s.body_state,
73
73
  cognitive_load: s.cognitive_load,
74
- readiness_level: s.readiness_level
74
+ readiness_level: s.readiness_level,
75
+ ...s.emotional_vector !== void 0 ? { emotional_vector: s.emotional_vector } : {}
75
76
  };
76
77
  }
77
78
  var ConsentDeniedError = class extends Error {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/behavior.ts"],"sourcesContent":["/**\n * @atlasent/sdk/behavior — consent + redaction helpers for the v2\n * Behavior Conditioning Layer.\n *\n * See `atlasent-docs/docs/V2_BEHAVIOR_CONDITIONING_LAYER.md` for the\n * architecture this module implements.\n *\n * Quick start:\n *\n * ```ts\n * import {\n * ConsentManager,\n * InMemoryBehaviorLedger,\n * redactStateSnapshot,\n * type StateSnapshot,\n * } from \"@atlasent/sdk/behavior\";\n *\n * const consent = new ConsentManager({ userId: \"u_123\" });\n * const ledger = new InMemoryBehaviorLedger();\n *\n * const snapshot: StateSnapshot = { ... };\n * const summary = redactStateSnapshot(snapshot);\n *\n * if (consent.canEmit(\"ledgers-me\", \"behavior.health.mental\")) {\n * await ledger.emit({\n * user_id: snapshot.user_id,\n * source: \"hicoach\",\n * category: \"behavior.health.mental\",\n * entry_state_summary: summary,\n * exit_state_summary: null,\n * relief_delta: null,\n * confidence_score: 1,\n * timestamp: new Date().toISOString(),\n * });\n * }\n * ```\n *\n * The MVP is pure and on-device — no HTTP calls. A future\n * `RemoteBehaviorLedger` will POST to `/v1/behavior/events` once\n * the atlasent-api endpoint ships.\n */\n\n// ---------------------------------------------------------------------------\n// Sensitive-category vocabulary\n// ---------------------------------------------------------------------------\n\n/**\n * Sensitive-category slugs used by behavior policies. Keep in sync\n * with `gxp-starter/packs/hipaa/` rules and\n * `atlasent-api`'s `target.category` field.\n */\nexport type SensitiveCategory =\n | \"behavior.health.mental\"\n | \"behavior.health.adherence\"\n | \"behavior.financial\"\n | \"behavior.minor\";\n\nexport const SENSITIVE_CATEGORIES: readonly SensitiveCategory[] = [\n \"behavior.health.mental\",\n \"behavior.health.adherence\",\n \"behavior.financial\",\n \"behavior.minor\",\n] as const;\n\n// ---------------------------------------------------------------------------\n// Domain types — mirror the model in `bettyc925/hicoach/lib/hicoach/types.ts`.\n// ---------------------------------------------------------------------------\n\nexport type EmotionalState =\n | \"tense\"\n | \"anxious\"\n | \"overwhelmed\"\n | \"flat\"\n | \"frustrated\"\n | \"uncertain\"\n | \"tired\"\n | \"okay\";\n\nexport type BodyState =\n | \"tight\"\n | \"heavy\"\n | \"restless\"\n | \"numb\"\n | \"buzzing\"\n | \"settled\";\n\nexport type ReadinessLevel = \"low\" | \"medium\" | \"high\";\n\n/**\n * A single moment captured before/after a regulation session.\n *\n * This is the **on-device** shape with all raw fields. It is never\n * emitted to a ledger; only the {@link StateEventSummary} projection\n * crosses an app boundary.\n */\nexport interface StateSnapshot {\n id: string;\n user_id: string;\n emotional_state: EmotionalState;\n /** 0..10 */\n intensity: number;\n /** 0..10 */\n stress_level: number;\n /** 0..10 */\n pressure_level: number;\n body_state: BodyState;\n /** 0..10 */\n cognitive_load: number;\n readiness_level: ReadinessLevel;\n /** 0..1 — how sure the user feels about the rating */\n confidence_score: number;\n /** ISO 8601 */\n created_at: string;\n /** Optional free-form note. NEVER part of the redacted summary. */\n note?: string;\n}\n\n/**\n * Redacted summary projection — the only shape that crosses an app\n * boundary. Contains no raw text, no IDs, no timestamps.\n */\nexport interface StateEventSummary {\n emotional_state: EmotionalState;\n intensity: number;\n stress_level: number;\n pressure_level: number;\n body_state: BodyState;\n cognitive_load: number;\n readiness_level: ReadinessLevel;\n}\n\n/**\n * A behavior event written to the cross-app ledger (LedgersMe).\n * The atlasent-api `/v1/behavior/events` endpoint accepts this shape.\n */\nexport interface BehaviorEvent {\n user_id: string;\n source: \"hicoach\" | \"echobloom\" | \"ledgers-me\" | string;\n category: SensitiveCategory;\n entry_state_summary: StateEventSummary;\n exit_state_summary: StateEventSummary | null;\n relief_delta: number | null;\n /** 0..1 */\n confidence_score: number;\n /** ISO 8601 */\n timestamp: string;\n /** Optional list of safety signals that fired during the event. Never raw text. */\n safety_signals?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Consent\n// ---------------------------------------------------------------------------\n\n/**\n * Per-user consent settings. Privacy-first defaults: nothing leaves\n * the device unless the user explicitly opts in.\n */\nexport interface ConsentSettings {\n /** Default false. Opt-in to emit summaries to the ledger. */\n share_state_summaries: boolean;\n /**\n * Default false. When true, suppresses ALL outbound emissions\n * regardless of any other setting. Acts as a global circuit\n * breaker.\n */\n private_only_mode: boolean;\n /**\n * Optional per-receiver allowlist. When non-empty, an event is\n * only emitted to a receiver whose name appears here AND for a\n * category whose slug appears in the receiver's allowed set.\n *\n * Example:\n * ```ts\n * { \"ledgers-me\": [\"behavior.health.mental\"] }\n * ```\n */\n receivers?: Record<string, SensitiveCategory[]>;\n}\n\nexport const DEFAULT_CONSENT: ConsentSettings = Object.freeze({\n share_state_summaries: false,\n private_only_mode: false,\n});\n\n/**\n * Storage abstraction so the helper works in browser\n * (`window.localStorage`), Node, and tests.\n */\nexport interface ConsentStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n}\n\n/**\n * Default in-memory storage (for Node + tests). In a browser, pass\n * `window.localStorage`.\n */\nexport class MemoryStorage implements ConsentStorage {\n private store = new Map<string, string>();\n get(key: string): string | null {\n return this.store.get(key) ?? null;\n }\n set(key: string, value: string): void {\n this.store.set(key, value);\n }\n}\n\nexport interface ConsentManagerOpts {\n userId: string;\n /** Defaults to {@link MemoryStorage}. Pass `localStorage` in browsers. */\n storage?: ConsentStorage;\n /** Defaults to {@link DEFAULT_CONSENT}. */\n defaults?: ConsentSettings;\n}\n\n/**\n * Read/write consent settings; gate emissions through `canEmit`.\n *\n * Apps NEVER hand-roll consent checks. This is the only correct way\n * to decide whether a `BehaviorEvent` may leave the device.\n */\nexport class ConsentManager {\n private readonly key: string;\n private readonly storage: ConsentStorage;\n private readonly defaults: ConsentSettings;\n\n constructor(opts: ConsentManagerOpts) {\n this.key = `atlasent.behavior.consent.${opts.userId}`;\n this.storage = opts.storage ?? new MemoryStorage();\n this.defaults = opts.defaults ?? DEFAULT_CONSENT;\n }\n\n get(): ConsentSettings {\n const raw = this.storage.get(this.key);\n if (!raw) return { ...this.defaults };\n try {\n const parsed = JSON.parse(raw) as Partial<ConsentSettings>;\n return { ...this.defaults, ...parsed };\n } catch {\n return { ...this.defaults };\n }\n }\n\n set(patch: Partial<ConsentSettings>): ConsentSettings {\n const next = { ...this.get(), ...patch };\n this.storage.set(this.key, JSON.stringify(next));\n return next;\n }\n\n /**\n * The single decision point: may we emit a `BehaviorEvent` for\n * this `category` to this `receiver`? Returns `false` whenever\n * any of the following is true:\n *\n * - `private_only_mode` is on.\n * - `share_state_summaries` is off.\n * - A `receivers` allowlist exists and the `(receiver, category)`\n * pair is not in it.\n */\n canEmit(receiver: string, category: SensitiveCategory): boolean {\n const c = this.get();\n if (c.private_only_mode) return false;\n if (!c.share_state_summaries) return false;\n if (c.receivers) {\n const allowed = c.receivers[receiver] ?? [];\n if (!allowed.includes(category)) return false;\n }\n return true;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Redaction\n// ---------------------------------------------------------------------------\n\n/**\n * Project a {@link StateSnapshot} down to the redacted\n * {@link StateEventSummary} shape. Drops `id`, `user_id`,\n * `created_at`, `confidence_score`, and any `note` field. The\n * remaining fields are bounded numeric ranges or closed enums and\n * carry no free-form text.\n */\nexport function redactStateSnapshot(s: StateSnapshot): StateEventSummary {\n return {\n emotional_state: s.emotional_state,\n intensity: s.intensity,\n stress_level: s.stress_level,\n pressure_level: s.pressure_level,\n body_state: s.body_state,\n cognitive_load: s.cognitive_load,\n readiness_level: s.readiness_level,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Ledger\n// ---------------------------------------------------------------------------\n\nexport interface BehaviorLedger {\n /**\n * Emit a behavior event. Implementations MUST validate `consent`\n * before persisting and throw `ConsentDeniedError` when an event\n * would be persisted in violation of the user's settings.\n */\n emit(event: BehaviorEvent): Promise<void>;\n}\n\nexport class ConsentDeniedError extends Error {\n readonly code = \"consent_denied\" as const;\n constructor(\n public readonly receiver: string,\n public readonly category: SensitiveCategory,\n ) {\n super(\n `Consent denies emit to receiver=${receiver} category=${category}`,\n );\n this.name = \"ConsentDeniedError\";\n }\n}\n\n/**\n * On-device ledger for development and demos. A future\n * `RemoteBehaviorLedger` will POST to atlasent-api's\n * `/v1/behavior/events` once that endpoint ships.\n */\nexport class InMemoryBehaviorLedger implements BehaviorLedger {\n private readonly events: BehaviorEvent[] = [];\n constructor(\n private readonly opts: {\n consent: ConsentManager;\n receiver?: string;\n },\n ) {}\n\n async emit(event: BehaviorEvent): Promise<void> {\n const receiver = this.opts.receiver ?? \"in-memory\";\n if (!this.opts.consent.canEmit(receiver, event.category)) {\n throw new ConsentDeniedError(receiver, event.category);\n }\n this.events.push(event);\n }\n\n /** Read all events accepted so far. Test/demo helper. */\n list(): readonly BehaviorEvent[] {\n return [...this.events];\n }\n\n /** Clear the in-memory store. Test helper. */\n clear(): void {\n this.events.length = 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// State-event cache (for biasing AI suggestions with recent context)\n// ---------------------------------------------------------------------------\n\n/**\n * Bounded in-memory ring buffer of recent {@link StateEventSummary}\n * values. The LangChain/LlamaIndex middleware (and similar wrappers)\n * read this to attach `context.session_history` to evaluate calls\n * without ever touching raw snapshots.\n */\nexport class StateEventCache {\n private readonly buf: StateEventSummary[] = [];\n constructor(private readonly capacity: number = 10) {\n if (capacity <= 0) {\n throw new RangeError(\"capacity must be > 0\");\n }\n }\n\n add(summary: StateEventSummary): void {\n this.buf.push(summary);\n if (this.buf.length > this.capacity) this.buf.shift();\n }\n\n recent(n?: number): readonly StateEventSummary[] {\n const k = n ?? this.buf.length;\n return this.buf.slice(-k);\n }\n\n clear(): void {\n this.buf.length = 0;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/behavior.ts"],"sourcesContent":["/**\n * @atlasent/sdk/behavior — consent + redaction helpers for the v2\n * Behavior Conditioning Layer.\n *\n * See `atlasent-docs/docs/V2_BEHAVIOR_CONDITIONING_LAYER.md` for the\n * architecture this module implements.\n *\n * Quick start:\n *\n * ```ts\n * import {\n * ConsentManager,\n * InMemoryBehaviorLedger,\n * redactStateSnapshot,\n * type StateSnapshot,\n * } from \"@atlasent/sdk/behavior\";\n *\n * const consent = new ConsentManager({ userId: \"u_123\" });\n * const ledger = new InMemoryBehaviorLedger();\n *\n * const snapshot: StateSnapshot = {\n * ...\n * emotional_vector: { valence: 0.2, arousal: 0.8, dominance: 0.3 },\n * };\n * const summary = redactStateSnapshot(snapshot);\n *\n * if (consent.canEmit(\"ledgers-me\", \"behavior.health.mental\")) {\n * await ledger.emit({\n * user_id: snapshot.user_id,\n * source: \"hicoach\",\n * category: \"behavior.health.mental\",\n * entry_state_summary: summary,\n * exit_state_summary: null,\n * relief_delta: null,\n * confidence_score: 1,\n * timestamp: new Date().toISOString(),\n * });\n * }\n * ```\n *\n * The MVP is pure and on-device — no HTTP calls. A future\n * `RemoteBehaviorLedger` will POST to `/v1/behavior/events` once\n * the atlasent-api endpoint ships.\n */\n\n// ---------------------------------------------------------------------------\n// Sensitive-category vocabulary\n// ---------------------------------------------------------------------------\n\n/**\n * Sensitive-category slugs used by behavior policies. Keep in sync\n * with `gxp-starter/packs/hipaa/` rules and\n * `atlasent-api`'s `target.category` field.\n */\nexport type SensitiveCategory =\n | \"behavior.health.mental\"\n | \"behavior.health.adherence\"\n | \"behavior.financial\"\n | \"behavior.minor\";\n\nexport const SENSITIVE_CATEGORIES: readonly SensitiveCategory[] = [\n \"behavior.health.mental\",\n \"behavior.health.adherence\",\n \"behavior.financial\",\n \"behavior.minor\",\n] as const;\n\n// ---------------------------------------------------------------------------\n// Domain types — mirror the model in `bettyc925/hicoach/lib/hicoach/types.ts`.\n// ---------------------------------------------------------------------------\n\nexport type EmotionalState =\n | \"tense\"\n | \"anxious\"\n | \"overwhelmed\"\n | \"flat\"\n | \"frustrated\"\n | \"uncertain\"\n | \"tired\"\n | \"okay\";\n\nexport type BodyState =\n | \"tight\"\n | \"heavy\"\n | \"restless\"\n | \"numb\"\n | \"buzzing\"\n | \"settled\";\n\nexport type ReadinessLevel = \"low\" | \"medium\" | \"high\";\n\n/**\n * Dimensional emotional coordinates in PAD space, each bounded 0..1.\n *\n * - `valence`: negative (0) → positive (1) affect\n * - `arousal`: calm (0) → activated (1)\n * - `dominance`: submissive (0) → in-control (1)\n *\n * All dimensions are bounded floats with no free-form text, so the\n * vector is safe to include in a {@link StateEventSummary} that\n * crosses an app boundary.\n */\nexport interface EmotionalVector {\n /** 0..1 — negative → positive affect */\n valence: number;\n /** 0..1 — calm → activated */\n arousal: number;\n /** 0..1 — submissive → in-control */\n dominance: number;\n}\n\n/**\n * A single moment captured before/after a regulation session.\n *\n * This is the **on-device** shape with all raw fields. It is never\n * emitted to a ledger; only the {@link StateEventSummary} projection\n * crosses an app boundary.\n */\nexport interface StateSnapshot {\n id: string;\n user_id: string;\n emotional_state: EmotionalState;\n /** 0..10 */\n intensity: number;\n /** 0..10 */\n stress_level: number;\n /** 0..10 */\n pressure_level: number;\n body_state: BodyState;\n /** 0..10 */\n cognitive_load: number;\n readiness_level: ReadinessLevel;\n /** 0..1 — how sure the user feels about the rating */\n confidence_score: number;\n /** ISO 8601 */\n created_at: string;\n /** Optional free-form note. NEVER part of the redacted summary. */\n note?: string;\n /** Optional PAD-space coordinates. Bounded floats — safe to redact through. */\n emotional_vector?: EmotionalVector;\n}\n\n/**\n * Redacted summary projection — the only shape that crosses an app\n * boundary. Contains no raw text, no IDs, no timestamps.\n */\nexport interface StateEventSummary {\n emotional_state: EmotionalState;\n intensity: number;\n stress_level: number;\n pressure_level: number;\n body_state: BodyState;\n cognitive_load: number;\n readiness_level: ReadinessLevel;\n /** PAD-space coordinates, when the caller supplied them. */\n emotional_vector?: EmotionalVector;\n}\n\n/**\n * A behavior event written to the cross-app ledger (LedgersMe).\n * The atlasent-api `/v1/behavior/events` endpoint accepts this shape.\n */\nexport interface BehaviorEvent {\n user_id: string;\n source: \"hicoach\" | \"echobloom\" | \"ledgers-me\" | string;\n category: SensitiveCategory;\n entry_state_summary: StateEventSummary;\n exit_state_summary: StateEventSummary | null;\n relief_delta: number | null;\n /** 0..1 */\n confidence_score: number;\n /** ISO 8601 */\n timestamp: string;\n /** Optional list of safety signals that fired during the event. Never raw text. */\n safety_signals?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Consent\n// ---------------------------------------------------------------------------\n\n/**\n * Per-user consent settings. Privacy-first defaults: nothing leaves\n * the device unless the user explicitly opts in.\n */\nexport interface ConsentSettings {\n /** Default false. Opt-in to emit summaries to the ledger. */\n share_state_summaries: boolean;\n /**\n * Default false. When true, suppresses ALL outbound emissions\n * regardless of any other setting. Acts as a global circuit\n * breaker.\n */\n private_only_mode: boolean;\n /**\n * Optional per-receiver allowlist. When non-empty, an event is\n * only emitted to a receiver whose name appears here AND for a\n * category whose slug appears in the receiver's allowed set.\n *\n * Example:\n * ```ts\n * { \"ledgers-me\": [\"behavior.health.mental\"] }\n * ```\n */\n receivers?: Record<string, SensitiveCategory[]>;\n}\n\nexport const DEFAULT_CONSENT: ConsentSettings = Object.freeze({\n share_state_summaries: false,\n private_only_mode: false,\n});\n\n/**\n * Storage abstraction so the helper works in browser\n * (`window.localStorage`), Node, and tests.\n */\nexport interface ConsentStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n}\n\n/**\n * Default in-memory storage (for Node + tests). In a browser, pass\n * `window.localStorage`.\n */\nexport class MemoryStorage implements ConsentStorage {\n private store = new Map<string, string>();\n get(key: string): string | null {\n return this.store.get(key) ?? null;\n }\n set(key: string, value: string): void {\n this.store.set(key, value);\n }\n}\n\nexport interface ConsentManagerOpts {\n userId: string;\n /** Defaults to {@link MemoryStorage}. Pass `localStorage` in browsers. */\n storage?: ConsentStorage;\n /** Defaults to {@link DEFAULT_CONSENT}. */\n defaults?: ConsentSettings;\n}\n\n/**\n * Read/write consent settings; gate emissions through `canEmit`.\n *\n * Apps NEVER hand-roll consent checks. This is the only correct way\n * to decide whether a `BehaviorEvent` may leave the device.\n */\nexport class ConsentManager {\n private readonly key: string;\n private readonly storage: ConsentStorage;\n private readonly defaults: ConsentSettings;\n\n constructor(opts: ConsentManagerOpts) {\n this.key = `atlasent.behavior.consent.${opts.userId}`;\n this.storage = opts.storage ?? new MemoryStorage();\n this.defaults = opts.defaults ?? DEFAULT_CONSENT;\n }\n\n get(): ConsentSettings {\n const raw = this.storage.get(this.key);\n if (!raw) return { ...this.defaults };\n try {\n const parsed = JSON.parse(raw) as Partial<ConsentSettings>;\n return { ...this.defaults, ...parsed };\n } catch {\n return { ...this.defaults };\n }\n }\n\n set(patch: Partial<ConsentSettings>): ConsentSettings {\n const next = { ...this.get(), ...patch };\n this.storage.set(this.key, JSON.stringify(next));\n return next;\n }\n\n /**\n * The single decision point: may we emit a `BehaviorEvent` for\n * this `category` to this `receiver`? Returns `false` whenever\n * any of the following is true:\n *\n * - `private_only_mode` is on.\n * - `share_state_summaries` is off.\n * - A `receivers` allowlist exists and the `(receiver, category)`\n * pair is not in it.\n */\n canEmit(receiver: string, category: SensitiveCategory): boolean {\n const c = this.get();\n if (c.private_only_mode) return false;\n if (!c.share_state_summaries) return false;\n if (c.receivers) {\n const allowed = c.receivers[receiver] ?? [];\n if (!allowed.includes(category)) return false;\n }\n return true;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Redaction\n// ---------------------------------------------------------------------------\n\n/**\n * Project a {@link StateSnapshot} down to the redacted\n * {@link StateEventSummary} shape. Drops `id`, `user_id`,\n * `created_at`, `confidence_score`, and any `note` field.\n *\n * `emotional_vector`, when present, is passed through unchanged —\n * it contains only bounded floats and is safe to cross an app boundary.\n */\nexport function redactStateSnapshot(s: StateSnapshot): StateEventSummary {\n return {\n emotional_state: s.emotional_state,\n intensity: s.intensity,\n stress_level: s.stress_level,\n pressure_level: s.pressure_level,\n body_state: s.body_state,\n cognitive_load: s.cognitive_load,\n readiness_level: s.readiness_level,\n ...(s.emotional_vector !== undefined\n ? { emotional_vector: s.emotional_vector }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Ledger\n// ---------------------------------------------------------------------------\n\nexport interface BehaviorLedger {\n /**\n * Emit a behavior event. Implementations MUST validate `consent`\n * before persisting and throw `ConsentDeniedError` when an event\n * would be persisted in violation of the user's settings.\n */\n emit(event: BehaviorEvent): Promise<void>;\n}\n\nexport class ConsentDeniedError extends Error {\n readonly code = \"consent_denied\" as const;\n constructor(\n public readonly receiver: string,\n public readonly category: SensitiveCategory,\n ) {\n super(\n `Consent denies emit to receiver=${receiver} category=${category}`,\n );\n this.name = \"ConsentDeniedError\";\n }\n}\n\n/**\n * On-device ledger for development and demos. A future\n * `RemoteBehaviorLedger` will POST to atlasent-api's\n * `/v1/behavior/events` once that endpoint ships.\n */\nexport class InMemoryBehaviorLedger implements BehaviorLedger {\n private readonly events: BehaviorEvent[] = [];\n constructor(\n private readonly opts: {\n consent: ConsentManager;\n receiver?: string;\n },\n ) {}\n\n async emit(event: BehaviorEvent): Promise<void> {\n const receiver = this.opts.receiver ?? \"in-memory\";\n if (!this.opts.consent.canEmit(receiver, event.category)) {\n throw new ConsentDeniedError(receiver, event.category);\n }\n this.events.push(event);\n }\n\n /** Read all events accepted so far. Test/demo helper. */\n list(): readonly BehaviorEvent[] {\n return [...this.events];\n }\n\n /** Clear the in-memory store. Test helper. */\n clear(): void {\n this.events.length = 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// State-event cache (for biasing AI suggestions with recent context)\n// ---------------------------------------------------------------------------\n\n/**\n * Bounded in-memory ring buffer of recent {@link StateEventSummary}\n * values. The LangChain/LlamaIndex middleware (and similar wrappers)\n * read this to attach `context.session_history` to evaluate calls\n * without ever touching raw snapshots.\n */\nexport class StateEventCache {\n private readonly buf: StateEventSummary[] = [];\n constructor(private readonly capacity: number = 10) {\n if (capacity <= 0) {\n throw new RangeError(\"capacity must be > 0\");\n }\n }\n\n add(summary: StateEventSummary): void {\n this.buf.push(summary);\n if (this.buf.length > this.capacity) this.buf.shift();\n }\n\n recent(n?: number): readonly StateEventSummary[] {\n const k = n ?? this.buf.length;\n return this.buf.slice(-k);\n }\n\n clear(): void {\n this.buf.length = 0;\n }\n}\n"],"mappings":"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","names":[]}