257 lines
10 KiB
Markdown
257 lines
10 KiB
Markdown
---
|
|||
|
|
phase: 02-gemini-integration-asset-pipeline
|
||
|
|
plan: 04
|
||
|
|
type: execute
|
||
|
|
wave: 2
|
||
|
|
depends_on: ["02-01", "02-02"]
|
||
|
|
files_modified:
|
||
|
|
- src/companion/companion.ts
|
||
|
|
- src/companion/companion.test.ts
|
||
|
|
autonomous: true
|
||
|
|
requirements: [COMP-01, COMP-02, COMP-03, COMP-04]
|
||
|
|
|
||
|
|
must_haves:
|
||
|
|
truths:
|
||
|
|
- "getGreeting tries Gemini API first, returns fallback text if API unavailable"
|
||
|
|
- "getLetterIntro tries Gemini API first, returns fallback text if API unavailable"
|
||
|
|
- "getForestComment tries Gemini API first, returns fallback text if API unavailable"
|
||
|
|
- "All returned texts are max 25 words, simple German, no performance praise"
|
||
|
|
artifacts:
|
||
|
|
- path: "src/companion/companion.ts"
|
||
|
|
provides: "Companion text module with API-first + fallback strategy"
|
||
|
|
exports: ["getGreeting", "getLetterIntro", "getForestComment"]
|
||
|
|
- path: "src/companion/companion.test.ts"
|
||
|
|
provides: "Tests for companion text module"
|
||
|
|
key_links:
|
||
|
|
- from: "src/companion/companion.ts"
|
||
|
|
to: "src/api/gemini.ts"
|
||
|
|
via: "calls generateText for AI-generated companion dialogue"
|
||
|
|
pattern: "generateText"
|
||
|
|
- from: "src/companion/companion.ts"
|
||
|
|
to: "src/companion/fallbacks.ts"
|
||
|
|
via: "imports fallback functions when API returns null"
|
||
|
|
pattern: "getRandomFallbackGreeting|getFallbackLetterIntro|getRandomFallbackForestComment"
|
||
|
|
- from: "src/companion/companion.ts"
|
||
|
|
to: "src/companion/characters.ts"
|
||
|
|
via: "looks up companion name and personality by type"
|
||
|
|
pattern: "companions"
|
||
|
|
---
|
||
|
|
|
||
|
|
<objective>
|
||
|
|
Create the companion text module that provides AI-generated or fallback text for greetings, letter introductions, and forest comments.
|
||
|
|
|
||
|
|
Purpose: The companion is the emotional core of Zauberwald. This module makes her speak — via AI when possible, via curated fallbacks otherwise.
|
||
|
|
Output: `src/companion/companion.ts` with 3 exported functions, full test coverage.
|
||
|
|
</objective>
|
||
|
|
|
||
|
|
<execution_context>
|
||
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||
|
|
</execution_context>
|
||
|
|
|
||
|
|
<context>
|
||
|
|
@.planning/PROJECT.md
|
||
|
|
@.planning/ROADMAP.md
|
||
|
|
@.planning/STATE.md
|
||
|
|
@.planning/phases/02-gemini-integration-asset-pipeline/02-01-SUMMARY.md
|
||
|
|
|
||
|
|
@src/companion/characters.ts
|
||
|
|
@src/companion/fallbacks.ts
|
||
|
|
@src/api/gemini.ts
|
||
|
|
@src/api/config.ts
|
||
|
|
|
||
|
|
<interfaces>
|
||
|
|
<!-- From src/api/gemini.ts (Plan 01) -->
|
||
|
|
```typescript
|
||
|
|
export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise<string | null>;
|
||
|
|
export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise<boolean>;
|
||
|
|
export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise<void>;
|
||
|
|
```
|
||
|
|
|
||
|
|
<!-- From src/api/config.ts (Plan 01) -->
|
||
|
|
```typescript
|
||
|
|
export interface GeminiConfig {
|
||
|
|
geminiApiKey: string;
|
||
|
|
geminiModel: string;
|
||
|
|
imageModel: string;
|
||
|
|
}
|
||
|
|
export async function loadConfig(): Promise<GeminiConfig | null>;
|
||
|
|
```
|
||
|
|
|
||
|
|
<!-- From src/companion/fallbacks.ts (Plan 02) -->
|
||
|
|
```typescript
|
||
|
|
export type TimeOfDay = 'morgen' | 'mittag' | 'abend';
|
||
|
|
export function getRandomFallbackGreeting(timeOfDay: TimeOfDay): string;
|
||
|
|
export function getRandomFallbackForestComment(): string;
|
||
|
|
export function getFallbackLetterIntro(letter: string): LetterIntro | undefined;
|
||
|
|
```
|
||
|
|
|
||
|
|
<!-- From src/companion/characters.ts -->
|
||
|
|
```typescript
|
||
|
|
export interface CompanionDefinition {
|
||
|
|
type: CompanionType;
|
||
|
|
name: string;
|
||
|
|
emoji: string;
|
||
|
|
personality: string;
|
||
|
|
}
|
||
|
|
export const companions: CompanionDefinition[];
|
||
|
|
```
|
||
|
|
</interfaces>
|
||
|
|
</context>
|
||
|
|
|
||
|
|
<tasks>
|
||
|
|
|
||
|
|
<task type="auto" tdd="true">
|
||
|
|
<name>Task 1: Companion text module with API-first fallback strategy</name>
|
||
|
|
<files>src/companion/companion.ts, src/companion/companion.test.ts</files>
|
||
|
|
<read_first>
|
||
|
|
- src/companion/characters.ts (companion definitions with name, personality)
|
||
|
|
- src/companion/fallbacks.ts (fallback functions from Plan 02)
|
||
|
|
- src/api/gemini.ts (generateText signature, checkRateLimit, incrementApiCall)
|
||
|
|
- src/api/config.ts (loadConfig, GeminiConfig)
|
||
|
|
- SPEC.md section 9.2 (exact prompt templates for greeting, letter intro, forest comment)
|
||
|
|
</read_first>
|
||
|
|
<behavior>
|
||
|
|
getGreeting:
|
||
|
|
- Test: returns AI text when API succeeds and rate limit not exceeded
|
||
|
|
- Test: returns fallback text when loadConfig returns null (no API key)
|
||
|
|
- Test: returns fallback text when generateText returns null (API error)
|
||
|
|
- Test: returns fallback text when rate limit exceeded
|
||
|
|
- Test: passes correct system prompt with companion personality and time of day
|
||
|
|
- Test: calls incrementApiCall after successful API call
|
||
|
|
|
||
|
|
getLetterIntro:
|
||
|
|
- Test: returns AI text when API succeeds
|
||
|
|
- Test: returns fallback LetterIntro text when API unavailable
|
||
|
|
- Test: system prompt includes letter, finger description, and companion personality
|
||
|
|
- Test: returned text includes the letter being introduced
|
||
|
|
|
||
|
|
getForestComment:
|
||
|
|
- Test: returns AI text when API succeeds
|
||
|
|
- Test: returns fallback text when API unavailable
|
||
|
|
- Test: system prompt includes element description and companion personality
|
||
|
|
</behavior>
|
||
|
|
<action>
|
||
|
|
Create `src/companion/companion.ts` (per D-13):
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { companions } from "./characters";
|
||
|
|
import type { CompanionType } from "../types";
|
||
|
|
import { loadConfig } from "../api/config";
|
||
|
|
import { generateText, checkRateLimit, incrementApiCall } from "../api/gemini";
|
||
|
|
import {
|
||
|
|
type TimeOfDay,
|
||
|
|
getRandomFallbackGreeting,
|
||
|
|
getFallbackLetterIntro,
|
||
|
|
getRandomFallbackForestComment,
|
||
|
|
} from "./fallbacks";
|
||
|
|
|
||
|
|
function getTimeOfDay(): TimeOfDay {
|
||
|
|
const hour = new Date().getHours();
|
||
|
|
if (hour < 12) return "morgen";
|
||
|
|
if (hour < 17) return "mittag";
|
||
|
|
return "abend";
|
||
|
|
}
|
||
|
|
|
||
|
|
function getCompanion(type: CompanionType) {
|
||
|
|
return companions.find(c => c.type === type)!;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**getGreeting(characterType: CompanionType, db: IDBDatabase): Promise<string>**
|
||
|
|
1. Call `loadConfig()` — if null, return `getRandomFallbackGreeting(getTimeOfDay())`
|
||
|
|
2. Call `checkRateLimit(db, 'text')` — if false, return fallback
|
||
|
|
3. Build system prompt from SPEC.md 9.2 template:
|
||
|
|
```
|
||
|
|
Du bist {name}, ein(e) {personality} im Zauberwald.
|
||
|
|
Du sprichst mit einem 7-jaehrigen Kind, das Tippen lernt.
|
||
|
|
|
||
|
|
Regeln:
|
||
|
|
- Max. 2 kurze Saetze (insgesamt max. 25 Woerter)
|
||
|
|
- Einfache Sprache, kurze Woerter
|
||
|
|
- Freundlich und warm, nie belehrend
|
||
|
|
- Keine Bewertung von Leistung
|
||
|
|
- Kein Englisch
|
||
|
|
- Beziehe dich auf die Tageszeit: {timeOfDay}
|
||
|
|
```
|
||
|
|
4. User prompt: `"Schreib eine Begruessung."`
|
||
|
|
5. Call `generateText(userPrompt, systemPrompt, config)`
|
||
|
|
6. If result is null, return fallback
|
||
|
|
7. Call `incrementApiCall(db, 'text')`
|
||
|
|
8. Return result
|
||
|
|
|
||
|
|
**getLetterIntro(characterType: CompanionType, letter: string, fingerDescription: string, db: IDBDatabase): Promise<string>**
|
||
|
|
1. Same config/rate-limit pattern
|
||
|
|
2. System prompt from SPEC.md 9.2:
|
||
|
|
```
|
||
|
|
Du bist {name}, ein(e) {personality} im Zauberwald.
|
||
|
|
Du stellst einem 7-jaehrigen Kind den Buchstaben {letter} vor.
|
||
|
|
|
||
|
|
Regeln:
|
||
|
|
- Max. 2 Saetze (max. 25 Woerter)
|
||
|
|
- Erklaere, welcher Finger den Buchstaben drueckt: {fingerDescription}
|
||
|
|
- Verwende eine bildhafte Eselsbruecke, die zum Wald/Natur/Tiere-Thema passt
|
||
|
|
- Einfache Sprache
|
||
|
|
- Kein Englisch
|
||
|
|
```
|
||
|
|
3. User prompt: `"Stelle den Buchstaben vor."`
|
||
|
|
4. On failure: return `getFallbackLetterIntro(letter)?.text ?? "Druecke die leuchtende Taste!"`
|
||
|
|
|
||
|
|
**getForestComment(characterType: CompanionType, elementDescription: string, db: IDBDatabase): Promise<string>**
|
||
|
|
1. Same config/rate-limit pattern
|
||
|
|
2. System prompt from SPEC.md 9.2:
|
||
|
|
```
|
||
|
|
Du bist {name}, ein(e) {personality} im Zauberwald.
|
||
|
|
Ein Kind hat gerade eine Tippuebung abgeschlossen. Im Wald ist ein neues Wesen erschienen: {elementDescription}.
|
||
|
|
|
||
|
|
Regeln:
|
||
|
|
- Max. 2 kurze Saetze (insgesamt max. 25 Woerter)
|
||
|
|
- Druecke Staunen und Freude aus
|
||
|
|
- Einfache Sprache fuer 7-Jaehrige
|
||
|
|
- Kein Lob fuer Leistung, sondern Begeisterung ueber das neue Waldelement
|
||
|
|
- Kein Englisch
|
||
|
|
```
|
||
|
|
3. User prompt: `"Kommentiere das neue Element."`
|
||
|
|
4. On failure: return `getRandomFallbackForestComment()`
|
||
|
|
|
||
|
|
**Tests (src/companion/companion.test.ts):**
|
||
|
|
- Mock `fetch` (via vi.spyOn) for generateText calls
|
||
|
|
- Mock `loadConfig` to return config or null
|
||
|
|
- Use `fake-indexeddb` for rate-limit DB operations
|
||
|
|
- Test each function with API success, API failure, no config, rate-limited
|
||
|
|
- At least 9 test cases total (3 per function)
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>cd /home/dev/workspace/zauberwald && npx vitest run src/companion/companion.test.ts</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- `grep -c "export async function getGreeting" src/companion/companion.ts` returns 1
|
||
|
|
- `grep -c "export async function getLetterIntro" src/companion/companion.ts` returns 1
|
||
|
|
- `grep -c "export async function getForestComment" src/companion/companion.ts` returns 1
|
||
|
|
- `grep "loadConfig" src/companion/companion.ts` shows config loader is used
|
||
|
|
- `grep "checkRateLimit" src/companion/companion.ts` shows rate-limiting is checked
|
||
|
|
- `grep "getRandomFallbackGreeting\|getFallbackLetterIntro\|getRandomFallbackForestComment" src/companion/companion.ts` shows fallbacks imported
|
||
|
|
- `npx vitest run src/companion/companion.test.ts` passes with 0 failures and >= 9 tests
|
||
|
|
</acceptance_criteria>
|
||
|
|
<done>Companion module exports getGreeting, getLetterIntro, getForestComment. Each tries Gemini API first (with rate-limit check), falls back to static texts. All tests pass.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
</tasks>
|
||
|
|
|
||
|
|
<verification>
|
||
|
|
- `npx vitest run src/companion/` — all tests pass
|
||
|
|
- Each function handles: API success, API failure, no config, rate-limited
|
||
|
|
</verification>
|
||
|
|
|
||
|
|
<success_criteria>
|
||
|
|
- getGreeting returns AI greeting or fallback, respecting rate limits
|
||
|
|
- getLetterIntro returns AI letter introduction or fallback
|
||
|
|
- getForestComment returns AI forest comment or fallback
|
||
|
|
- All texts conform to COMP-04: max 25 words, simple German, no performance praise
|
||
|
|
- Tests cover all paths (API success, failure, no config, rate-limited)
|
||
|
|
</success_criteria>
|
||
|
|
|
||
|
|
<output>
|
||
|
|
After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-04-SUMMARY.md`
|
||
|
|
</output>
|