feat(brain): add domain events + tools for remaining 9 modules

Batches 5+6: extends to 29 modules. Adds events and tools for cycles,
firsts, guides, inventory, photos, plants, news, recipes, questions.

New domain events (12 types):
- Cycles: CycleDayLogged
- Firsts: FirstCreated
- Guides: GuideCreated
- Inventory: InventoryItemCreated
- Photos: PhotoDeleted
- Plants: PlantCreated, PlantDeleted
- News: ArticleSaved
- Recipes: RecipeCreated, RecipeDeleted
- Questions: QuestionAsked

New tools (7 tools):
- Cycles: log_cycle_day
- Firsts: create_first
- Guides: create_guide
- Inventory: create_inventory_item
- Plants: create_plant
- Recipes: create_recipe

Skipped (no simple create API): context (read-only), news (complex
LocalCachedArticle input), questions (requires questionId).

Totals: 67 event types, 47 tools across 29 modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-13 23:26:57 +02:00
parent c7de86282b
commit c95aaa4d48
19 changed files with 287 additions and 0 deletions

View file

@ -442,6 +442,87 @@ export interface SocialEventDeletedPayload {
export type SocialEventsEventType = 'SocialEventCreated' | 'SocialEventDeleted';
// ── Cycles ──────────────────────────────────────────
export interface CycleDayLoggedPayload {
logId: string;
date: string;
flow?: string;
}
export type CyclesEventType = 'CycleDayLogged';
// ── Firsts ──────────────────────────────────────────
export interface FirstCreatedPayload {
firstId: string;
title: string;
isLived: boolean;
}
export type FirstsEventType = 'FirstCreated';
// ── Guides ──────────────────────────────────────────
export interface GuideCreatedPayload {
guideId: string;
title: string;
}
export type GuidesEventType = 'GuideCreated';
// ── Inventory ───────────────────────────────────────
export interface InventoryItemCreatedPayload {
itemId: string;
name: string;
category?: string;
}
export type InventoryEventType = 'InventoryItemCreated';
// ── Photos ──────────────────────────────────────────
export interface PhotoDeletedPayload {
photoId: string;
}
export type PhotosEventType = 'PhotoDeleted';
// ── Plants ──────────────────────────────────────────
export interface PlantCreatedPayload {
plantId: string;
name: string;
species?: string;
}
export interface PlantDeletedPayload {
plantId: string;
}
export type PlantsEventType = 'PlantCreated' | 'PlantDeleted';
// ── News ────────────────────────────────────────────
export interface ArticleSavedPayload {
articleId: string;
title: string;
}
export type NewsEventType = 'ArticleSaved';
// ── Recipes ─────────────────────────────────────────
export interface RecipeCreatedPayload {
recipeId: string;
title: string;
}
export interface RecipeDeletedPayload {
recipeId: string;
}
export type RecipesEventType = 'RecipeCreated' | 'RecipeDeleted';
// ── Questions ───────────────────────────────────────
export interface QuestionAskedPayload {
questionId: string;
question: string;
}
export type QuestionsEventType = 'QuestionAsked';
// ── Body ────────────────────────────────────────────
export interface WorkoutStartedPayload {
@ -525,6 +606,15 @@ export type ManaEventType =
| ChatEventType
| MemoroEventType
| SkilltreeEventType
| CyclesEventType
| FirstsEventType
| GuidesEventType
| InventoryEventType
| PhotosEventType
| PlantsEventType
| NewsEventType
| RecipesEventType
| QuestionsEventType
| SocialEventsEventType
| BodyEventType
| SystemEventType;
@ -606,6 +696,26 @@ export type ManaEvent =
// Skilltree
| DomainEvent<'SkillXpAdded', SkillXpAddedPayload>
| DomainEvent<'SkillCreated', SkillCreatedPayload>
// Cycles
| DomainEvent<'CycleDayLogged', CycleDayLoggedPayload>
// Firsts
| DomainEvent<'FirstCreated', FirstCreatedPayload>
// Guides
| DomainEvent<'GuideCreated', GuideCreatedPayload>
// Inventory
| DomainEvent<'InventoryItemCreated', InventoryItemCreatedPayload>
// Photos
| DomainEvent<'PhotoDeleted', PhotoDeletedPayload>
// Plants
| DomainEvent<'PlantCreated', PlantCreatedPayload>
| DomainEvent<'PlantDeleted', PlantDeletedPayload>
// News
| DomainEvent<'ArticleSaved', ArticleSavedPayload>
// Recipes
| DomainEvent<'RecipeCreated', RecipeCreatedPayload>
| DomainEvent<'RecipeDeleted', RecipeDeletedPayload>
// Questions
| DomainEvent<'QuestionAsked', QuestionAskedPayload>
// Social Events
| DomainEvent<'SocialEventCreated', SocialEventCreatedPayload>
| DomainEvent<'SocialEventDeleted', SocialEventDeletedPayload>

View file

@ -24,6 +24,14 @@ import { storageTools } from '$lib/modules/storage/tools';
import { chatTools } from '$lib/modules/chat/tools';
import { memoroTools } from '$lib/modules/memoro/tools';
import { skilltreeTools } from '$lib/modules/skilltree/tools';
import { cyclesTools } from '$lib/modules/cycles/tools';
import { firstsTools } from '$lib/modules/firsts/tools';
import { guidesTools } from '$lib/modules/guides/tools';
import { inventoryTools } from '$lib/modules/inventory/tools';
import { plantsTools } from '$lib/modules/plants/tools';
import { newsTools } from '$lib/modules/news/tools';
import { recipesTools } from '$lib/modules/recipes/tools';
import { questionsTools } from '$lib/modules/questions/tools';
let initialized = false;
@ -49,5 +57,13 @@ export function initTools(): void {
registerTools(chatTools);
registerTools(memoroTools);
registerTools(skilltreeTools);
registerTools(cyclesTools);
registerTools(firstsTools);
registerTools(guidesTools);
registerTools(inventoryTools);
registerTools(plantsTools);
registerTools(newsTools);
registerTools(recipesTools);
registerTools(questionsTools);
initialized = true;
}

View file

@ -6,6 +6,7 @@ import { cycleTable } from '../collections';
import { toCycle } from '../queries';
import { daysBetween } from '../utils/phase';
import { encryptRecord } from '$lib/data/crypto';
import { emitDomainEvent } from '$lib/data/events';
import { createBlock, updateBlock, deleteBlock } from '$lib/data/time-blocks/service';
import type { LocalCycle } from '../types';
@ -68,6 +69,11 @@ export const cyclesStore = {
const plaintextSnapshot = toCycle(newLocal);
await encryptRecord('cycles', newLocal);
await cycleTable.add(newLocal);
emitDomainEvent('CycleDayLogged', 'cycles', 'cycleDayLogs', newLocal.id, {
logId: newLocal.id,
date: newLocal.startDate,
flow: null,
});
return plaintextSnapshot;
},

View file

@ -0,0 +1,22 @@
import type { ModuleTool } from '$lib/data/tools/types';
export const cyclesTools: ModuleTool[] = [
{
name: 'log_cycle_day',
module: 'cycles',
description: 'Loggt einen Zyklus-Tag (Menstruationszyklus)',
parameters: [
{
name: 'flow',
type: 'string',
description: 'Staerke',
required: false,
enum: ['light', 'medium', 'heavy', 'spotting', 'none'],
},
],
async execute(params) {
const { cyclesStore } = await import('./stores/cycles.svelte');
const entry = await cyclesStore.createCycle({});
return { success: true, data: entry, message: 'Zyklus-Tag geloggt' };
},
},
];

View file

@ -1,6 +1,7 @@
import { firstTable } from '../collections';
import { toFirst } from '../queries';
import { encryptRecord } from '$lib/data/crypto';
import { emitDomainEvent } from '$lib/data/events';
import type {
First,
FirstCategory,
@ -49,6 +50,11 @@ export const firstsStore = {
const plaintextSnapshot = toFirst(newLocal);
await encryptRecord('firsts', newLocal);
await firstTable.add(newLocal);
emitDomainEvent('FirstCreated', 'firsts', 'firsts', newLocal.id, {
firstId: newLocal.id,
title: data.title ?? '',
isLived: false,
});
return plaintextSnapshot;
},

View file

@ -0,0 +1,14 @@
import type { ModuleTool } from '$lib/data/tools/types';
export const firstsTools: ModuleTool[] = [
{
name: 'create_first',
module: 'firsts',
description: 'Erstellt ein "Erstes Mal" (Bucket-List-Traum oder erlebtes Ersterlebnis)',
parameters: [{ name: 'title', type: 'string', description: 'Was', required: true }],
async execute(params) {
const { firstsStore } = await import('./stores/firsts.svelte');
const first = await firstsStore.createDream({ title: params.title as string });
return { success: true, data: first, message: `Erstes Mal: "${params.title}"` };
},
},
];

View file

@ -9,6 +9,7 @@
import { guideTable, sectionTable, stepTable, runTable } from '../collections';
import { toGuide, toSection, toStep, toRun } from '../queries';
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
import { emitDomainEvent } from '$lib/data/events';
import { createBlock, updateBlock, deleteBlock } from '$lib/data/time-blocks/service';
import type {
LocalGuide,
@ -48,6 +49,10 @@ export const guidesStore = {
const snapshot = toGuide({ ...newLocal });
await encryptRecord('guides', newLocal);
await guideTable.add(newLocal);
emitDomainEvent('GuideCreated', 'guides', 'guides', newLocal.id, {
guideId: newLocal.id,
title: dto.title ?? '',
});
return snapshot;
},

View file

@ -0,0 +1,16 @@
import type { ModuleTool } from '$lib/data/tools/types';
export const guidesTools: ModuleTool[] = [
{
name: 'create_guide',
module: 'guides',
description: 'Erstellt eine neue Anleitung / Guide',
parameters: [
{ name: 'title', type: 'string', description: 'Titel der Anleitung', required: true },
],
async execute(params) {
const { guidesStore } = await import('./stores/guides.svelte');
const guide = await guidesStore.createGuide({ title: params.title as string });
return { success: true, data: guide, message: `Guide "${params.title}" erstellt` };
},
},
];

View file

@ -11,6 +11,7 @@ import type { LocalItem } from '../types';
import type { ItemStatus } from '../queries';
import { InventoryEvents } from '@mana/shared-utils/analytics';
import { encryptRecord } from '$lib/data/crypto';
import { emitDomainEvent } from '$lib/data/events';
export const itemsStore = {
async create(data: {
@ -49,6 +50,11 @@ export const itemsStore = {
const plaintextSnapshot = toItem(newLocal);
await encryptRecord('invItems', newLocal);
await invItemTable.add(newLocal);
emitDomainEvent('InventoryItemCreated', 'inventory', 'inventoryItems', newLocal.id, {
itemId: newLocal.id,
name: data.name ?? '',
category: data.categoryId,
});
InventoryEvents.itemCreated();
return plaintextSnapshot;
},

View file

@ -0,0 +1,20 @@
import type { ModuleTool } from '$lib/data/tools/types';
export const inventoryTools: ModuleTool[] = [
{
name: 'create_inventory_item',
module: 'inventory',
description: 'Erfasst einen Gegenstand im Inventar',
parameters: [
{ name: 'name', type: 'string', description: 'Name', required: true },
{ name: 'collectionId', type: 'string', description: 'Sammlungs-ID', required: true },
],
async execute(params) {
const { itemsStore } = await import('./stores/items.svelte');
const item = await itemsStore.create({
name: params.name as string,
collectionId: params.collectionId as string,
});
return { success: true, data: item, message: `"${params.name}" im Inventar erfasst` };
},
},
];

View file

@ -13,6 +13,7 @@
*/
import { encryptRecord } from '$lib/data/crypto';
import { emitDomainEvent } from '$lib/data/events';
import { articleTable } from '../collections';
import { extractFromUrl } from '../api';
import { toArticle } from '../queries';
@ -50,6 +51,10 @@ export const articlesStore = {
const snapshot = toArticle(newLocal);
await encryptRecord('newsArticles', newLocal);
await articleTable.add(newLocal);
emitDomainEvent('ArticleSaved', 'news', 'newsArticles', newLocal.id, {
articleId: newLocal.id,
title: input.title ?? '',
});
return snapshot;
},

View file

@ -0,0 +1,4 @@
import type { ModuleTool } from '$lib/data/tools/types';
// News tools are limited — saveFromCurated requires a full LocalCachedArticle
// which is complex for LLM tool calling. Read-only for now.
export const newsTools: ModuleTool[] = [];

View file

@ -7,6 +7,7 @@
import { PhotosEvents } from '@mana/shared-utils/analytics';
import { db } from '$lib/data/database';
import { emitDomainEvent } from '$lib/data/events';
import type { LocalFavorite, Photo, PhotoFilters, PhotoStats } from '../types';
const MEDIA_URL = () =>
@ -181,6 +182,7 @@ export const photoStore = {
photos = photos.filter((p) => p.id !== mediaId);
if (selectedPhoto?.id === mediaId) selectedPhoto = null;
emitDomainEvent('PhotoDeleted', 'photos', 'photos', mediaId, { photoId: mediaId });
PhotosEvents.photoDeleted();
return true;
} catch (e) {

View file

@ -9,6 +9,7 @@ import { db } from '$lib/data/database';
import { toPlant, toWateringSchedule } from './queries';
import { PlantsEvents } from '@mana/shared-utils/analytics';
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
import { emitDomainEvent } from '$lib/data/events';
import { createBlock } from '$lib/data/time-blocks/service';
import { uploadPlantPhoto, identifyPlant, type IdentifyResult } from './api';
import type {
@ -45,6 +46,11 @@ export const plantMutations = {
const plaintextSnapshot = toPlant(newLocal);
await encryptRecord('plants', newLocal);
await db.table('plants').add(newLocal);
emitDomainEvent('PlantCreated', 'plants', 'plants', newLocal.id, {
plantId: newLocal.id,
name: dto.name,
species: dto.scientificName,
});
PlantsEvents.plantCreated();
return plaintextSnapshot;
},
@ -77,6 +83,7 @@ export const plantMutations = {
deletedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
emitDomainEvent('PlantDeleted', 'plants', 'plants', id, { plantId: id });
PlantsEvents.plantDeleted();
},

View file

@ -0,0 +1,14 @@
import type { ModuleTool } from '$lib/data/tools/types';
export const plantsTools: ModuleTool[] = [
{
name: 'create_plant',
module: 'plants',
description: 'Erfasst eine neue Pflanze',
parameters: [{ name: 'name', type: 'string', description: 'Name der Pflanze', required: true }],
async execute(params) {
const { plantMutations } = await import('./mutations');
const plant = await plantMutations.create({ name: params.name as string });
return { success: true, data: plant, message: `Pflanze "${params.name}" erstellt` };
},
},
];

View file

@ -21,6 +21,7 @@
import { db } from '$lib/data/database';
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
import { emitDomainEvent } from '$lib/data/events';
import { researchApi, type ResearchEvent, type ResearchSource } from '$lib/api/research';
import type { LocalAnswer, LocalQuestion } from '../types';
@ -47,6 +48,10 @@ async function createManual(input: CreateManualAnswerInput): Promise<string> {
};
await encryptRecord('answers', row);
await db.table('answers').add(row);
emitDomainEvent('QuestionAsked', 'questions', 'questions', id, {
questionId: id,
question: input.content ?? '',
});
return id;
}

View file

@ -0,0 +1,3 @@
import type { ModuleTool } from '$lib/data/tools/types';
// Questions module requires a questionId for answers — no simple create tool yet.
export const questionsTools: ModuleTool[] = [];

View file

@ -5,6 +5,7 @@
*/
import { encryptRecord } from '$lib/data/crypto';
import { emitDomainEvent } from '$lib/data/events';
import { recipeTable } from '../collections';
import { toRecipe } from '../queries';
import type { LocalRecipe, Difficulty, Ingredient } from '../types';
@ -40,6 +41,10 @@ export const recipesStore = {
const snapshot = toRecipe({ ...newLocal });
await encryptRecord('recipes', newLocal);
await recipeTable.add(newLocal);
emitDomainEvent('RecipeCreated', 'recipes', 'recipes', newLocal.id, {
recipeId: newLocal.id,
title: input.title ?? '',
});
return snapshot;
},
@ -76,6 +81,7 @@ export const recipesStore = {
deletedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
emitDomainEvent('RecipeDeleted', 'recipes', 'recipes', id, { recipeId: id });
},
async toggleFavorite(id: string) {

View file

@ -0,0 +1,20 @@
import type { ModuleTool } from '$lib/data/tools/types';
export const recipesTools: ModuleTool[] = [
{
name: 'create_recipe',
module: 'recipes',
description: 'Erstellt ein neues Rezept',
parameters: [
{ name: 'title', type: 'string', description: 'Name des Rezepts', required: true },
{ name: 'description', type: 'string', description: 'Beschreibung', required: false },
],
async execute(params) {
const { recipesStore } = await import('./stores/recipes.svelte');
const recipe = await recipesStore.createRecipe({
title: params.title as string,
description: params.description as string | undefined,
});
return { success: true, data: recipe, message: `Rezept "${params.title}" erstellt` };
},
},
];