{"_id":"@aituber-onair/kizuna","_rev":"3-3c778931afd9c91fc46a2fab8e0b7b9f","name":"@aituber-onair/kizuna","dist-tags":{"latest":"0.0.3"},"versions":{"0.0.1":{"name":"@aituber-onair/kizuna","version":"0.0.1","keywords":["aituber","points","gamification","user-engagement","chat","streaming"],"author":{"url":"https://github.com/shinshin86","name":"shinshin86"},"license":"MIT","_id":"@aituber-onair/kizuna@0.0.1","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"homepage":"https://github.com/shinshin86/aituber-onair#readme","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"dist":{"shasum":"74b42cb35fd88456969a5257bc3ae6638c1353e1","tarball":"https://registry.npmjs.org/@aituber-onair/kizuna/-/kizuna-0.0.1.tgz","fileCount":58,"integrity":"sha512-SDJC/SNbfmck1JGzGZnm0XCViergeLIAs/HUlB0UHNRqaZFO+37+l15DHGAUI2NTh3yGGRJThyYWK680WMD+Qg==","signatures":[{"sig":"MEYCIQCZq/skaU6nNmlhrUOJXR7IFbHejZFQ/xls3u/IhryaRAIhAPSCpZ6tF5yIiMm4UO0X4jQ2yP4wA8H60hOED7EG62ta","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":247415},"main":"./dist/cjs/index.js","types":"./dist/types/index.d.ts","module":"./dist/esm/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"1b9e84aa95d1db9709093fe10c23e33e834bdd7d","scripts":{"fmt":"biome format --write ./src","lint":"biome lint ./src","test":"npm run typecheck && vitest run","build":"npm run build:clean && npm run build:cjs && npm run build:esm && npm run build:types","check":"biome check ./src","lint:fix":"biome lint --fix ./src","build:cjs":"tsc -p tsconfig.cjs.json","build:esm":"tsc -p tsconfig.esm.json","check:fix":"biome check --fix ./src","fmt:check":"biome format ./src","typecheck":"tsc --noEmit","test:watch":"vitest","build:clean":"rm -rf dist","build:types":"tsc -p tsconfig.json --emitDeclarationOnly --declaration --declarationDir dist/types","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"repository":{"url":"git+https://github.com/shinshin86/aituber-onair.git","type":"git","directory":"packages/kizuna"},"_npmVersion":"10.8.2","description":"A sophisticated bond system (絆 - Kizuna) for managing relationships between users and AI characters in AITuber OnAir.","directories":{},"_nodeVersion":"20.19.3","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^1.3.1","typescript":"^5.0.0","@types/node":"^18.15.10","@biomejs/biome":"^1.9.4","@vitest/coverage-v8":"^1.3.1"},"peerDependencies":{"typescript":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/kizuna_0.0.1_1752751527652_0.029137407283358785","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@aituber-onair/kizuna","version":"0.0.2","keywords":["aituber","points","gamification","user-engagement","chat","streaming","frontend","browser"],"author":{"url":"https://github.com/shinshin86","name":"shinshin86"},"license":"MIT","_id":"@aituber-onair/kizuna@0.0.2","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"homepage":"https://github.com/shinshin86/aituber-onair#readme","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"dist":{"shasum":"45c77f5fca41dd8ac8ebcb50f025304f315cbc28","tarball":"https://registry.npmjs.org/@aituber-onair/kizuna/-/kizuna-0.0.2.tgz","fileCount":36,"integrity":"sha512-S18wIxDi3wz9xRVqUTdq7Bh8PVGh7EBoFGgKLU4kiEFAvthaTv3C6o8qfEhX4wf+8LJoeU4rue27cxgm1qNnzQ==","signatures":[{"sig":"MEUCIHmjJjGBM8GmlpgheB/dDY7yIMs2M959MgtqqRQdD+b4AiEA6HkmyY5NTMVTDcG97S2vWLP1VAP60g7Kiv17NW5UWTs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":191288},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","gitHead":"3c0f60a21aa7d01589d8aaa036687fb8b8e0d87e","scripts":{"fmt":"biome format . --write","lint":"biome lint .","test":"npm run typecheck && vitest run","build":"tsc --project tsconfig.json","clean":"rm -rf dist","fmt:check":"biome format .","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"repository":{"url":"git+https://github.com/shinshin86/aituber-onair.git","type":"git","directory":"packages/kizuna"},"_npmVersion":"10.8.2","description":"A sophisticated bond system (絆 - Kizuna) for managing relationships between users and AI characters in AITuber OnAir.","directories":{},"_nodeVersion":"20.19.3","browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^22.1.0","vitest":"^1.3.1","typescript":"^5.0.0","@types/node":"^18.15.0","@biomejs/biome":"1.9.4","@vitest/coverage-v8":"^1.3.1"},"_npmOperationalInternal":{"tmp":"tmp/kizuna_0.0.2_1752759949554_0.5406092769100441","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@aituber-onair/kizuna","version":"0.0.3","description":"A bond model for relationships between people and AI characters, with warmth, continuity, and LLM context.","type":"module","main":"dist/index.js","module":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc --project tsconfig.json","example:kizuna-sample":"vite --host 127.0.0.1 examples/kizuna-sample","example:kizuna-sample:build":"vite build examples/kizuna-sample","example:kizuna-sample:typecheck":"tsc --noEmit --project examples/kizuna-sample/tsconfig.json","example:chat-bond-sample":"vite --host 127.0.0.1 examples/chat-bond-sample","example:chat-bond-sample:build":"vite build examples/chat-bond-sample","example:chat-bond-sample:typecheck":"tsc --noEmit --project examples/chat-bond-sample/tsconfig.json","typecheck":"tsc --noEmit","test":"npm run typecheck && vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","fmt":"biome format . --write","fmt:check":"biome format .","lint":"biome lint .","clean":"rm -rf dist","prepublishOnly":"npm run build"},"keywords":["aituber","bond","relationship","kizuna","ai-character","points","gamification","user-engagement","chat","streaming","frontend","browser"],"author":{"name":"shinshin86","url":"https://github.com/shinshin86"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/shinshin86/aituber-onair.git","directory":"packages/kizuna"},"devDependencies":{"@biomejs/biome":"1.9.4","@types/node":"^18.15.0","@vitest/coverage-v8":"^1.3.1","jsdom":"^22.1.0","typescript":"^5.0.0","vite":"^5.4.21","vitest":"^1.3.1"},"browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_id":"@aituber-onair/kizuna@0.0.3","gitHead":"514f914290cc88512b447c2eff05df78f813000f","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"homepage":"https://github.com/shinshin86/aituber-onair#readme","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-xJ+hf7sAOthFmpnzYEYvZ67OkgkMruOuBI1Fwp3Fc0EaYgKyDdpAoViMG+QWd14WqPn0vGtd57LEzSdBq7FR1Q==","shasum":"54d725f8bde0cb1742eae6171113ca69f93b04c3","tarball":"https://registry.npmjs.org/@aituber-onair/kizuna/-/kizuna-0.0.3.tgz","fileCount":48,"unpackedSize":736808,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGJtN8FqESLyRxSWTdNJxoDsIU+jE9aDDVquPHBz6g8+AiATxJn4Pz+WQeNkpB/NzayxFCS6oY8oNWp65uW9Gf9Emw=="}]},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"directories":{},"maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/kizuna_0.0.3_1786740821618_0.9748541284027026"},"_hasShrinkwrap":false}},"time":{"created":"2025-07-17T11:25:27.560Z","modified":"2026-08-14T20:53:41.962Z","0.0.1":"2025-07-17T11:25:27.859Z","0.0.2":"2025-07-17T13:45:49.744Z","0.0.3":"2026-08-14T20:53:41.795Z"},"bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"author":{"name":"shinshin86","url":"https://github.com/shinshin86"},"license":"MIT","homepage":"https://github.com/shinshin86/aituber-onair#readme","keywords":["aituber","bond","relationship","kizuna","ai-character","points","gamification","user-engagement","chat","streaming","frontend","browser"],"repository":{"type":"git","url":"git+https://github.com/shinshin86/aituber-onair.git","directory":"packages/kizuna"},"description":"A bond model for relationships between people and AI characters, with warmth, continuity, and LLM context.","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"readme":"# @aituber-onair/kizuna\n\n![AITuber OnAir Kizuna - logo](./images/aituber-onair-kizuna.png)\n\nKizuna turns repeated contact with an AI character into a bond the character\ncan actually respond to.\n\nA first message starts at `stranger`. Continued contact can grow into\n`acquaintance`, `regular`, and finally `companion`. Time apart lowers warmth\nwithout erasing the history. A new contact warms the bond again. At every\nstep, `getBondContext()` converts that state into a short prompt for an LLM.\n\n[日本語版 README](./README.ja.md)\n\n## How it fits together\n\nKizuna sits between one LLM turn and the next. One message round-trip looks\nlike this:\n\n```text\nViewer comment\n      |\n      v\nchat package + LLM\n  - The system prompt already includes the previous getBondContext().\n  - The reply includes the character's reaction emotion.\n      |\n      | emotion\n      v\nApplication\n  - Calls kizuna.processInteraction({ emotion, ... }).\n  - May override valence or severity with moderation results or app rules.\n      |\n      v\nKizuna\n  - Deterministically updates bond score, warmth, and scars. No LLM runs here.\n      |\n      v\nUpdated getBondContext() -> next turn's system prompt -> changed attitude\n```\n\nThe LLM is the **sensor** that reports how the character felt and the\n**actuator** that changes behavior after reading the updated context. Kizuna is\nthe deterministic state machine between those two jobs. An LLM never chooses\nthe amount of a bond change. Keeping that calculation outside the LLM provides:\n\n- Prompt-injection resistance: viewers cannot talk their intimacy score up.\n- Reproducible, testable relationship changes.\n- Zero additional LLM cost or latency for the bond calculation.\n\nThe classifier layer is pluggable. An app can provide `valence` and `severity`\noverrides from a moderation API or its own rules. The\n[`chat-bond-sample`](./examples/chat-bond-sample/) uses a small dictionary as a\nno-LLM stand-in for exactly this input layer. In a real chat flow, the\ncharacter's reaction emotion is normally the valence signal.\n\n## The bond story\n\nWith the default configuration, representative English context looks like\nthis:\n\n```text\n# First contact\nBond with Aki: stranger (level 1, 1 points). Trend: rising; current atmosphere: warm (warmth 1.00). Continuity: 1 buckets. Favorite emotions: curious. Respond in a way that fits this bond depth and atmosphere without inducing guilt.\n\n# Continued contact\nBond with Aki: regular (level 3, 500 points). Trend: rising; current atmosphere: warm (warmth 1.00). Continuity: 12 buckets. Favorite emotions: happy, curious. Respond in a way that fits this bond depth and atmosphere without inducing guilt.\n\n# After time apart\nBond with Aki: regular (level 3, 500 points). Trend: rising; current atmosphere: neutral (warmth 0.50). Continuity: 12 buckets. Favorite emotions: happy, curious. Respond in a way that fits this bond depth and atmosphere without inducing guilt.\n\n# Contact resumes\nBond with Aki: regular (level 3, 501 points). Trend: rising; current atmosphere: warm (warmth 0.95). Continuity: 1 buckets. Favorite emotions: happy, curious. Respond in a way that fits this bond depth and atmosphere without inducing guilt.\n\n# A lasting bond\nBond with Aki: companion (level 4, 1000 points). Trend: steady; current atmosphere: warm (warmth 1.00). Continuity: 20 buckets. Favorite emotions: happy, curious. Respond in a way that fits this bond depth and atmosphere without inducing guilt.\n```\n\nThe signed bond score expresses slow-moving history. Warmth expresses the\ncurrent atmosphere. Continuity expresses repeated contact across days, weeks,\nsessions, or custom buckets. These signals stay separate so an established\nbond can cool, recover, or remember a serious violation without becoming a\nfirst meeting again.\n\n## How the relationship moves\n\nThe default `human` preset treats growth and deterioration as equally real\nparts of a relationship. Applications can select `forgiving` for a\nstreamer-safe, faster-repairing model or `strict` for firmer boundaries.\n\n| Contact | Bond score | Warmth and memory |\n| --- | --- | --- |\n| Kind contact | Grows slowly; repeated contact in one bucket has diminishing returns. | Recovers warmth, with a small bonus for continuity across buckets. |\n| Light negative contact | Drops with a first-offense discount and a deeper-stage buffer. | Chills immediately, then repairs over several kind exchanges. |\n| Grave violation | Drops sharply and bypasses the stage buffer. | Creates one scar per user and bucket; gifts cannot erase it. |\n| Gift while warmth is low | Receives only part of its normal bond gain. | Cannot purchase immediate forgiveness. |\n| Time apart | **Never lowers the bond score or stage.** | Warmth cools toward a floor and re-warms on reunion. |\n| Sustained repair | Gradually restores the score and warmth. | Heals scars only after a configurable positive pattern across buckets. |\n| Delayed contact | Its score effect is recorded with the original bucket's anti-farming rules. | Does not rewrite the current atmosphere, conflict history, or scar lifecycle. |\n\nNegative emotion defaults such as `angry`, an explicit `valence`, or a\nrule-provided valence can mark an interaction as negative. Use `severity:\n'grave'` only for an application-confirmed integrity violation.\n\n```typescript\nconfig.dynamics = {\n  preset: 'human', // 'human' | 'forgiving' | 'strict'\n  negativityBias: 3,\n  maxTrackedBuckets: 128, // bounded persisted anti-farming history\n};\n\nawait kizuna.processInteraction({\n  userId: 'person-42',\n  kind: 'reaction',\n  emotion: 'angry',\n  valence: 'negative',\n  severity: 'light',\n  isOwner: false,\n  timestamp: Date.now(),\n});\n```\n\n### Design notes and ethical stance\n\nThe defaults are product heuristics grounded in relationship research, not a\nclaim that software can reproduce or diagnose human relationships:\n\n- The default 3x negativity weight is informed by Baumeister et al.,\n  [“Bad Is Stronger Than Good”](https://doi.org/10.1037/1089-2680.5.4.323),\n  and trust asymmetry by Slovic,\n  [“Perceived Risk, Trust, and Democracy”](https://doi.org/10.1111/j.1539-6924.1993.tb01329.x).\n- The light/grave split and sustained repair follow the distinction between\n  competence- and integrity-based trust violations studied by\n  [Kim et al.](https://doi.org/10.1037/0021-9010.89.1.104).\n- Per-bucket saturation is inspired by Zajonc's\n  [mere-exposure work](https://doi.org/10.1037/h0025848); continuity rewards\n  frequency rather than unlimited same-session intensity.\n- A warmth floor and fast reunion are informed by Levin, Walter, and\n  Murnighan's [dormant-ties research](https://doi.org/10.1287/orsc.1100.0576).\n- Per-viewer tracking reflects evidence that parasocial connection and\n  emotional attachment matter in virtual-streamer participation and support\n  ([VTuber donation study](https://doi.org/10.1108/JRIM-11-2024-0512),\n  [AI VTuber fandom study](https://arxiv.org/abs/2509.10427)).\n- The CHI 2025\n  [taxonomy of harmful AI-companion behavior](https://doi.org/10.1145/3706598.3713429)\n  motivates a hard constraint: **absence never damages the bond score, Kizuna\n  adds no guilt mechanics, and cooling is transparent and explainable.**\n\nThe popularized “Gottman 5:1” ratio is not used as a parameter or design\ndriver. The default is the documented `negativityBias: 3`, and applications\nshould tune it through scene tests rather than treating any social-science\nratio as a universal law.\n\n## Features\n\n- Generic interactions: `message`, `reaction`, `gift`, `presence`, `touch`,\n  or your own string kind\n- Stable roles: `owner` and `guest`\n- Configurable points, rules, cooldowns, per-bucket limits, and thresholds\n- Signed bond scores, stage hysteresis, fast warmth, scars, and continuity\n- Structured snapshots plus English, Japanese, or custom LLM context\n- A normalized `0..1` relationship value for downstream systems\n- Optional persistence through browser storage or an injected adapter\n- No runtime dependencies and no source-specific user ID parsing\n- An injectable clock for deterministic tests and simulations\n\n## Installation\n\n```bash\nnpm install @aituber-onair/kizuna\n```\n\n## Quick start\n\n```typescript\nimport {\n  KizunaManager,\n  createDefaultKizunaConfig,\n} from '@aituber-onair/kizuna';\n\nconst config = createDefaultKizunaConfig();\nconst kizuna = new KizunaManager(config, undefined, 'my-character-bond');\n\nawait kizuna.processInteraction({\n  userId: 'person-42',\n  kind: 'message',\n  message: 'Good morning!',\n  emotion: 'curious',\n  isOwner: false,\n  timestamp: Date.now(),\n  metadata: { displayName: 'Aki' },\n});\n\nconst snapshot = kizuna.getBondSnapshot('person-42');\nconst context = kizuna.getBondContext('person-42');\n\nconsole.log(snapshot?.stage); // stranger\nconsole.log(context); // Bond with Aki: stranger ...\n\nkizuna.destroy();\n```\n\n`processInteraction()` initializes the manager lazily. When using persistent\nstorage, call `await kizuna.initialize()` before reading state so saved data is\nloaded first. A non-empty storage key is required even for an in-memory\nmanager.\n\nCall `destroy()` during shutdown or unmount so the automatic cleanup timer and\nevent listeners are released.\n\n## Configure the bond\n\nStart from `createDefaultKizunaConfig()` so future optional fields receive safe\ndefaults, then override only what your character needs.\n\n```typescript\nconst config = createDefaultKizunaConfig();\n\nconfig.basePoints = {\n  message: 10,\n  reaction: 4,\n  gift: 80,\n  presence: 2,\n  touch: 6,\n};\n\nconfig.stages = [\n  { id: 'stranger', minPoints: 0 },\n  { id: 'acquaintance', minPoints: 100 },\n  { id: 'regular', minPoints: 500 },\n  { id: 'companion', minPoints: 1_000 },\n];\n\nconfig.warmth = {\n  halfLifeMs: 7 * 24 * 60 * 60 * 1_000,\n  floor: 0.2,\n};\n\nconfig.continuity = {\n  unit: 'day',\n  grace: 1,\n};\n\nconfig.dynamics = {\n  preset: 'human',\n};\n```\n\nThe highest stage threshold is also the normalization target used by\n`toRelationshipCapital()`. The returned value is normalized points multiplied\nby current warmth.\n\n### Point rules\n\nRules add to the base points for an interaction kind. A cooldown limits time,\nwhile `bucketLimit` limits applications within the configured continuity\nbucket.\n\n```typescript\nconfig.rules = [\n  {\n    id: 'thoughtful-message',\n    name: 'Thoughtful message',\n    condition: (interaction) =>\n      interaction.kind === 'message' &&\n      (interaction.message?.length ?? 0) >= 80,\n    points: 5,\n    cooldown: 60_000,\n    bucketLimit: 3,\n    description: 'Recognizes a longer message without rewarding spam.',\n  },\n];\n```\n\nRule points may also be a function of the interaction and current user. Rules\ncan provide `valence` and `severity`; explicit interaction values take\nprecedence. Invalid numbers are ignored. Bond scores are signed in motion but\nfloored at zero, while `stats.totalPointsEarned` counts positive gains only.\n\n### Threshold actions and achievements\n\n```typescript\nconfig.thresholds = [\n  {\n    id: 'trusted-companion',\n    points: 1_000,\n    repeatable: false,\n    action: {\n      type: 'achievement',\n      data: {\n        id: 'trusted-companion',\n        title: 'Trusted companion',\n        description: 'Built a lasting bond.',\n        icon: '✨',\n      },\n    },\n  },\n];\n```\n\nUse an explicit threshold `id` when possible. It keeps one-time threshold\ntracking stable when display text changes.\n\n### Session continuity\n\nFor experiences where a visit is the natural unit, use session buckets:\n\n```typescript\nconfig.continuity = { unit: 'session', grace: 0 };\n\nconst kizuna = new KizunaManager(config, undefined, 'session-bond');\n\nawait kizuna.beginSession('visit-1');\nawait kizuna.processInteraction({\n  userId: 'person-42',\n  kind: 'presence',\n  isOwner: false,\n  timestamp: Date.now(),\n});\nkizuna.endSession();\n```\n\n`unit` can be `day`, `week`, `session`, or a function that returns a safe\ninteger bucket index.\n\n## Use the outputs\n\n### Structured state\n\n```typescript\nconst snapshot = kizuna.getBondSnapshot('person-42');\n\nif (snapshot) {\n  console.log(snapshot.stage);\n  console.log(snapshot.points);\n  console.log(snapshot.warmth);\n  console.log(snapshot.continuity.streak);\n  console.log(snapshot.favoriteEmotions);\n  console.log(snapshot.achievements);\n}\n```\n\n### LLM context\n\n```typescript\nconst japaneseContext = kizuna.getBondContext('person-42', {\n  language: 'ja',\n  maxFavoriteEmotions: 2,\n});\n```\n\nCustom templates can be supplied in `config.context.templates`. A template\nreceives the complete `BondSnapshot`.\n\n### Relationship capital\n\n```typescript\nconst relationshipCapital = kizuna.toRelationshipCapital('person-42');\n```\n\nThis is useful when another system wants a single bounded value while Kizuna\nretains the richer state.\n\n## Integration patterns\n\n### Update a Core system prompt\n\n```typescript\nawait kizuna.processInteraction(interaction);\n\nconst bondContext = kizuna.getBondContext(interaction.userId);\ncore.updateChatOptions({\n  systemPrompt: `${baseSystemPrompt}\\n\\nCurrent bond context:\\n${bondContext}`,\n});\n\nawait core.processChat(interaction.message ?? '');\n```\n\nThe `react-pngtuber-app` Core example includes this integration and records\nemotions from assistant response events, including negative relationship\nchanges.\n\n### Control Noise relationship gates\n\n```typescript\nconst result = await noise.contaminate({\n  systemPrompt,\n  messages,\n  draft,\n  relationshipCapital: kizuna.toRelationshipCapital(interaction.userId),\n});\n```\n\nThe Noise session example uses this bridge and keeps a manual override for\ndiagnostics.\n\n### Map application events\n\nKeep application-specific information in `metadata` and map it to generic\ninteraction kinds. For example, a chat line can become `message`, an emoji can\nbecome `reaction`, and a gift can represent an item purchase or a super chat.\nKizuna does not parse or generate source-specific IDs.\n\n## Persistence\n\nBrowser persistence uses `LocalStorageProvider`. Other runtimes can inject an\n`ExternalStorageAdapter` into `ExternalStorageProvider`.\n\n```typescript\nimport {\n  KizunaManager,\n  LocalStorageProvider,\n  createDefaultKizunaConfig,\n} from '@aituber-onair/kizuna';\n\nconst storage = new LocalStorageProvider();\nconst kizuna = new KizunaManager(\n  createDefaultKizunaConfig(),\n  storage,\n  'character:bond:v1',\n);\n\nawait kizuna.initialize();\n```\n\nCompression, encryption, adapter examples, persistence format, and security\nlimitations are documented in [Storage](./docs/storage.md).\n\n## Events\n\n```typescript\nkizuna.on('points_updated', (event) => {\n  console.log(event);\n});\n\nkizuna.on('achievement_earned', (event) => {\n  console.log(event);\n});\n```\n\nThe manager currently emits `user_created`, `points_updated`, `level_up`,\n`stage_down`, `scar_created`, `scar_healed`, `threshold_reached`,\n`achievement_earned`, and `error`. `KizunaEventType` also retains\n`user_updated` and `action_executed` for compatibility, but the manager does\nnot currently emit them. Listeners receive `KizunaEventData` with `type`,\n`userId`, `data`, and `timestamp`.\n\n## API reference\n\n### `KizunaManager`\n\n| Method | Purpose |\n| --- | --- |\n| `initialize()` | Load persisted state and start cleanup. |\n| `processInteraction(interaction)` | Record contact, calculate points, update bond state, and persist it. |\n| `getBondSnapshot(userId)` | Return structured bond state or `null`. |\n| `getBondContext(userId, options?)` | Return prompt-ready context or an empty string. |\n| `toRelationshipCapital(userId)` | Return a warmth-adjusted value from `0` to `1`. |\n| `beginSession(id?)` / `endSession()` | Manage session continuity buckets. |\n| `getUser(userId)` / `getAllUsers()` | Read user records. |\n| `addPoints(userId, points)` | Apply a signed adjustment, floored at zero, to an existing user. |\n| `calculateLevel(points)` | Resolve a level from the current configuration. |\n| `getStats()` | Return aggregate counts and point totals. |\n| `destroy()` | Stop cleanup and remove listeners. |\n\n### Main types and helpers\n\n- `Interaction`, `InteractionKind`, `InteractionValence`, `NegativeSeverity`,\n  `UserRole`, `KizunaUser`, `PointRule`, `PointResult`, `Threshold`,\n  `Achievement`\n- `KizunaConfig`, `BondStage`, `WarmthConfig`, `ContinuityConfig`,\n  `BondDynamicsConfig`, `BondDynamicsPreset`\n- `BondSnapshot`, `BondContextOptions`, `BondContextTemplate`\n- `createDefaultKizunaConfig()`, `DEFAULT_BOND_STAGES`\n- `BondEvaluator`, `BondContextBuilder`, `PointCalculator`, `UserManager`\n- `LocalStorageProvider`, `ExternalStorageProvider`,\n  `createStorageProvider()`, `createDefaultStorageProvider()`\n- `detectEnvironment()`, `isBrowser()`, `isNode()`\n\n`PointContext` and `UserType` remain as deprecated aliases. New code should use\n`Interaction` and `UserRole`.\n\n## Migration to 0.0.3\n\nVersion 0.0.3 replaces source-specific interaction and user shapes with the\ngeneric bond model.\n\n```typescript\n// Before\nawait kizuna.processInteraction({\n  userId: 'person-42',\n  platform: 'chat',\n  message: 'Hello',\n  isOwner: false,\n  timestamp: Date.now(),\n});\n\n// 0.0.3\nawait kizuna.processInteraction({\n  userId: 'person-42',\n  kind: 'message',\n  message: 'Hello',\n  isOwner: false,\n  timestamp: Date.now(),\n  metadata: { source: 'chat' },\n});\n```\n\nConfiguration now uses `basePoints` and `rules` instead of `platforms` and\n`customRules`. `KizunaUser.type` becomes `role`; message counters become\ngeneric interaction and continuity statistics. `PointRule.dailyLimit` becomes\n`bucketLimit`. The exported `ChatType` and `PlatformPointConfig` types were\nremoved; use `InteractionKind` and `KizunaConfig.basePoints`. The exported\n`generateUserId()` and `parseUserId()` helpers were also removed because the\napplication now owns opaque user IDs and any source mapping.\n\nDirect users of `UserManager` or `PointCalculator` should also review their\nconstructor and method changes in the changelog. `KizunaManager` remains the\nrecommended integration surface.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for the complete breaking-change summary.\n\n## Browser lab\n\nFrom a repository checkout, run the interactive sample to explore growth,\nconflict, repair, stages, warmth, scars, context output, and simulated time:\n\n```bash\nnpm -w @aituber-onair/kizuna run example:kizuna-sample\n```\n\n## Development\n\n```bash\nnpm -w @aituber-onair/kizuna run fmt\nnpm -w @aituber-onair/kizuna run lint\nnpm -w @aituber-onair/kizuna run test\nnpm -w @aituber-onair/kizuna run build\n```\n\nThe test suite covers bond evaluation, point calculation, persistence,\nenvironment detection, storage factories, output adapters, and manager\nlifecycle behavior. It is not a guarantee that every integration or custom\nconfiguration is covered.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}