diff --git a/apps/mana/apps/web/src/lib/data/ai/missions/default-resolvers.ts b/apps/mana/apps/web/src/lib/data/ai/missions/default-resolvers.ts index 1b45b4874..8ef182908 100644 --- a/apps/mana/apps/web/src/lib/data/ai/missions/default-resolvers.ts +++ b/apps/mana/apps/web/src/lib/data/ai/missions/default-resolvers.ts @@ -9,6 +9,7 @@ import { db } from '../../database'; import { decryptRecords } from '../../crypto'; +import { scopedTable } from '../../scope/scoped-db'; import { registerInputResolver } from './input-resolvers'; import { registerInputIndexer } from './input-index'; import type { InputResolver } from './input-resolvers'; @@ -166,15 +167,21 @@ const notesIndexer: InputIndexer = async () => { }; const kontextIndexer: InputIndexer = async () => { - const doc = await db.table('kontextDoc').get('singleton'); - if (!doc) return []; + // Per-Space since Phase 2d.2: the kontextDoc for the active Space is + // the only candidate we surface to the picker. Personal-Space's legacy + // singleton row is matched via the `_personal:` sentinel in + // scopedTable's getInScopeSpaceIds(); Shared/Brand/Family Spaces that + // haven't yet authored a kontextDoc simply return an empty list. + const rows = await scopedTable('kontextDoc').toArray(); + const match = rows[0]; + if (!match) return []; return [ { module: 'kontext', table: 'kontextDoc', - id: 'singleton', + id: match.id, label: 'Kontext-Dokument', - hint: 'Dein zentrales Markdown-Dokument', + hint: 'Dein zentrales Markdown-Dokument für diesen Space', }, ]; }; diff --git a/apps/mana/apps/web/src/lib/modules/kontext/queries.ts b/apps/mana/apps/web/src/lib/modules/kontext/queries.ts index 7fea241dd..1f6fe8d6a 100644 --- a/apps/mana/apps/web/src/lib/modules/kontext/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/kontext/queries.ts @@ -1,14 +1,18 @@ /** - * Kontext module — reactive query for the singleton document. + * Kontext module — reactive query for the active-Space document. * * Content is encrypted at rest. Returns null until first write; the * view calls kontextStore.ensureDoc() on mount to materialise the row. + * + * Per-Space since Phase 2d.2: each Space has its own kontextDoc; + * Personal-Space's legacy singleton row is matched by the in-scope + * set's inclusion of the `_personal:` sentinel. */ import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; -import { db } from '$lib/data/database'; import { decryptRecords } from '$lib/data/crypto'; -import { KONTEXT_SINGLETON_ID, type KontextDoc, type LocalKontextDoc } from './types'; +import { scopedTable } from '$lib/data/scope/scoped-db'; +import type { KontextDoc, LocalKontextDoc } from './types'; export function toKontextDoc(local: LocalKontextDoc): KontextDoc { return { @@ -22,9 +26,10 @@ export function toKontextDoc(local: LocalKontextDoc): KontextDoc { export function useKontextDoc() { return useLiveQueryWithDefault( async () => { - const local = await db.table('kontextDoc').get(KONTEXT_SINGLETON_ID); - if (!local || local.deletedAt) return null; - const [decrypted] = await decryptRecords('kontextDoc', [local]); + const rows = await scopedTable('kontextDoc').toArray(); + const match = rows.find((r) => !r.deletedAt); + if (!match) return null; + const [decrypted] = await decryptRecords('kontextDoc', [match]); return decrypted ? toKontextDoc(decrypted) : null; }, null as KontextDoc | null diff --git a/apps/mana/apps/web/src/lib/modules/kontext/stores/kontext.svelte.ts b/apps/mana/apps/web/src/lib/modules/kontext/stores/kontext.svelte.ts index 712b790c1..9d9bd6766 100644 --- a/apps/mana/apps/web/src/lib/modules/kontext/stores/kontext.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/kontext/stores/kontext.svelte.ts @@ -1,40 +1,59 @@ /** - * Kontext Store — singleton markdown document. + * Kontext Store — per-Space markdown document. * - * `content` is encrypted at rest. The whole module is one row keyed by - * a fixed id so there's no list/detail — just read/write. + * Since Phase 2d.2 the module is Space-scoped: each Space has its own + * kontextDoc. The store finds the row via `getInScopeSpaceIds()` (which + * matches the active Space plus the legacy `_personal:` sentinel + * so Personal-Space's pre-migration singleton row still renders). + * + * `content` is encrypted at rest. The Dexie creating hook stamps + * `spaceId` on new rows automatically — we just pick a fresh UUID. */ import { kontextDocTable } from '../collections'; import { encryptRecord, decryptRecords } from '$lib/data/crypto'; -import { KONTEXT_SINGLETON_ID, type LocalKontextDoc } from '../types'; +import { scopedTable } from '$lib/data/scope/scoped-db'; +import type { LocalKontextDoc } from '../types'; + +async function findForActiveSpace(): Promise { + const rows = await scopedTable('kontextDoc').toArray(); + return rows.find((r) => !r.deletedAt); +} export const kontextStore = { - async ensureDoc(): Promise { - const existing = await kontextDocTable.get(KONTEXT_SINGLETON_ID); - if (existing) return; + /** + * Ensure a kontextDoc exists for the active Space. No-op if one + * already exists. Returns the row so callers can read + write the + * same id. + */ + async ensureDoc(): Promise { + const existing = await findForActiveSpace(); + if (existing) return existing; const newLocal: LocalKontextDoc = { - id: KONTEXT_SINGLETON_ID, + id: crypto.randomUUID(), content: '', }; await encryptRecord('kontextDoc', newLocal); await kontextDocTable.add(newLocal); + // Reload — the creating-hook stamped spaceId/authorId/actor fields. + const created = await kontextDocTable.get(newLocal.id); + if (!created) throw new Error('Failed to create kontextDoc'); + return created; }, async setContent(content: string): Promise { - await this.ensureDoc(); + const row = await this.ensureDoc(); const diff: Partial = { content, updatedAt: new Date().toISOString(), }; await encryptRecord('kontextDoc', diff); - await kontextDocTable.update(KONTEXT_SINGLETON_ID, diff); + await kontextDocTable.update(row.id, diff); }, async appendContent(chunk: string): Promise { - await this.ensureDoc(); - const row = await kontextDocTable.get(KONTEXT_SINGLETON_ID); - const [decrypted] = row ? await decryptRecords('kontextDoc', [row]) : []; + const row = await this.ensureDoc(); + const [decrypted] = await decryptRecords('kontextDoc', [row]); const current = decrypted?.content ?? ''; const separator = current.trim() ? '\n\n---\n\n' : ''; await this.setContent(`${current}${separator}${chunk}`); diff --git a/apps/mana/apps/web/src/lib/modules/kontext/types.ts b/apps/mana/apps/web/src/lib/modules/kontext/types.ts index 0178ce0cf..e951da499 100644 --- a/apps/mana/apps/web/src/lib/modules/kontext/types.ts +++ b/apps/mana/apps/web/src/lib/modules/kontext/types.ts @@ -1,13 +1,24 @@ /** - * Kontext module types — Singleton markdown document. + * Kontext module types — per-Space markdown document. + * + * Since Phase 2d.2 of the space-scoped rollout, each Space can have its + * own kontextDoc (was: user-level singleton keyed by id='singleton'). + * Personal-Space's pre-migration singleton row stays usable because its + * stamped spaceId falls inside the in-scope set returned by + * getInScopeSpaceIds(); fresh rows use random UUIDs. */ import type { BaseRecord } from '@mana/local-store'; +/** + * Legacy singleton id — pre-Phase-2d.2 the whole module was one row + * keyed by this. Kept for backward-compat lookups on Personal-Space + * records that predate the refactor; new rows use crypto.randomUUID(). + */ export const KONTEXT_SINGLETON_ID = 'singleton' as const; export interface LocalKontextDoc extends BaseRecord { - id: typeof KONTEXT_SINGLETON_ID; + id: string; content: string; }