mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(kontext): Phase 2d.2 — kontextDoc is per-Space, not user-singleton
Since Phase 2d.2 of the space-scoped rollout, each Space can have its own kontextDoc. Before this commit, the module was a user-level singleton keyed by id='singleton' — which meant Shared/Brand/Family Spaces saw the user's Personal-Space bio as their AI planner context. Changes: - types.ts: relax LocalKontextDoc.id to plain string (was the literal 'singleton'). KONTEXT_SINGLETON_ID stays as an exported const so legacy Personal-Space rows (stamped before the refactor) are documented; no longer used at write sites. - stores/kontext.svelte.ts: ensureDoc() finds the active-Space row via scopedTable(), creates a fresh UUID row if absent. setContent / appendContent operate on the found-or-created row's id. Personal- Space's legacy 'singleton' row keeps rendering because the `_personal:<userId>` sentinel is inside getInScopeSpaceIds()'s returned set. - queries.ts: useKontextDoc() mirrors the same scopedTable filter. - ai/missions/default-resolvers.ts: kontextIndexer surfaces the active Space's kontextDoc (not hardcoded 'singleton'). Shared-Spaces without a doc yet return an empty candidate list, which is the correct empty-state for the mission-input picker. Type-check clean. No schema change; relies on v28's existing spaceId stamping + the creating-hook's ongoing stamp (kontextDoc is in the kontext module.config). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
219ccd3f2c
commit
8a82f3c543
4 changed files with 67 additions and 25 deletions
|
|
@ -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<KontextDocLike>('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:<userId>` 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<KontextDocLike, string>('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',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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:<userId>` 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<LocalKontextDoc>('kontextDoc').get(KONTEXT_SINGLETON_ID);
|
||||
if (!local || local.deletedAt) return null;
|
||||
const [decrypted] = await decryptRecords('kontextDoc', [local]);
|
||||
const rows = await scopedTable<LocalKontextDoc, string>('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
|
||||
|
|
|
|||
|
|
@ -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:<userId>` 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<LocalKontextDoc | undefined> {
|
||||
const rows = await scopedTable<LocalKontextDoc, string>('kontextDoc').toArray();
|
||||
return rows.find((r) => !r.deletedAt);
|
||||
}
|
||||
|
||||
export const kontextStore = {
|
||||
async ensureDoc(): Promise<void> {
|
||||
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<LocalKontextDoc> {
|
||||
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<void> {
|
||||
await this.ensureDoc();
|
||||
const row = await this.ensureDoc();
|
||||
const diff: Partial<LocalKontextDoc> = {
|
||||
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<void> {
|
||||
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}`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue