Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@app/memory-client",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "AAMOS Memory-integration: minneslager, samtyckesregler och \"Vad appen vet om mig\" (spec §32)",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@app/ai-contracts": "workspace:*",
"@app/shared-types": "workspace:*"
}
}
+238
View File
@@ -0,0 +1,238 @@
import type { AamosClient } from "@app/ai-contracts";
import type { MemoryItem, MemoryKind, SignalOrigin } from "@app/shared-types";
/**
* Minnesarkitektur (spec §32):
*
* - plattformens databas (memory_items) är den ANVÄNDARSYNLIGA sanningen:
* allt som visas i "Vad plattformen vet om mig" och allt som kan korrigeras,
* pausas, raderas och exporteras finns där.
* - AAMOS får härleda nya minnesposter (UPDATE_USER_MEMORY-jobbet), men
* förslagen skrivs alltid in i plattformens tabell där användaren äger dem.
* - Personligt minne är ALDRIG automatiskt träningsdata (spec §3233);
* consentSnapshot följer med varje härledning.
*/
export interface MemoryUpdateProposal {
key: string;
kind: MemoryKind;
summarySv: string;
value: unknown;
origin: Exclude<SignalOrigin, "user_stated">;
confidence: number;
expiresAt?: string | null;
}
export interface MemorySyncInput {
scope: "user" | "household";
scopeId: string;
events: Array<{ type: string; occurredAt: string; payload: unknown }>;
existingMemoryKeys: string[];
consentFlags: {
personalization: boolean;
anonymizedImprovement: boolean;
imageTraining: boolean;
};
correlationId?: string;
localeContext?: import("@app/shared-types").LocaleContext;
}
/**
* Kör minnesuppdatering via AAMOS. Returnerar förslag anroparen (workern)
* persisterar dem i memory_items och publicerar MEMORY_UPDATED-event.
* Utan personaliseringssamtycke körs ingenting.
*/
export async function deriveMemoryUpdates(
aamos: AamosClient,
input: MemorySyncInput,
): Promise<MemoryUpdateProposal[]> {
if (!input.consentFlags.personalization) return [];
const result = await aamos.runTask(
"UPDATE_USER_MEMORY",
{
scope: input.scope,
scopeId: input.scopeId,
events: input.events,
existingMemoryKeys: input.existingMemoryKeys,
},
{
subjectRef: pseudonymize(input.scopeId),
consentFlags: input.consentFlags,
...(input.localeContext ? { localeContext: input.localeContext } : {}),
...(input.correlationId ? { correlationId: input.correlationId } : {}),
},
);
if (result.status !== "ok" || !result.output) return [];
return result.output.memoryUpdates.map((u) => ({
key: u.key,
kind: u.kind,
summarySv: u.summarySv,
value: u.value,
origin: u.origin,
confidence: u.confidence,
expiresAt: u.expiresAt,
}));
}
/** Gruppera minnesposter för "Vad plattformen vet om mig"-vyn. */
export interface MemoryOverview {
language: string;
sections: Array<{
kind: MemoryKind;
/** Rubrik på begärt språk (i18n M10). titleSv behålls för bakåtkompatibilitet. */
title: string;
titleSv: string;
items: Array<MemoryItem & { summary: string }>;
}>;
totalCount: number;
pausedCount: number;
}
const KIND_TITLES: Record<string, Record<MemoryKind, string>> = {
sv: {
structured_fact: "Fakta om dig och hushållet",
event: "Händelser vi kommer ihåg",
semantic: "Mönster vi har observerat",
profile_summary: "Din profil i korthet",
recipe_memory: "Recept och måltider",
},
en: {
structured_fact: "Facts about you and your household",
event: "Events we remember",
semantic: "Patterns we've observed",
profile_summary: "Your profile at a glance",
recipe_memory: "Recipes and meals",
},
es: {
structured_fact: "Datos sobre ti y tu hogar",
event: "Eventos que recordamos",
semantic: "Patrones observados",
profile_summary: "Tu perfil de un vistazo",
recipe_memory: "Recetas y comidas",
},
it: {
structured_fact: "Informazioni su di te e la tua famiglia",
event: "Eventi che ricordiamo",
semantic: "Schemi osservati",
profile_summary: "Il tuo profilo in breve",
recipe_memory: "Ricette e pasti",
},
de: {
structured_fact: "Fakten über dich und deinen Haushalt",
event: "Ereignisse, die wir uns merken",
semantic: "Beobachtete Muster",
profile_summary: "Dein Profil auf einen Blick",
recipe_memory: "Rezepte und Mahlzeiten",
},
fr: {
structured_fact: "Infos sur vous et votre foyer",
event: "Événements mémorisés",
semantic: "Habitudes observées",
profile_summary: "Votre profil en un coup d'œil",
recipe_memory: "Recettes et repas",
},
da: {
structured_fact: "Fakta om dig og husstanden",
event: "Hændelser vi husker",
semantic: "Mønstre vi har observeret",
profile_summary: "Din profil i korte træk",
recipe_memory: "Opskrifter og måltider",
},
nb: {
structured_fact: "Fakta om deg og husstanden",
event: "Hendelser vi husker",
semantic: "Mønstre vi har observert",
profile_summary: "Profilen din i korte trekk",
recipe_memory: "Oppskrifter og måltider",
},
fi: {
structured_fact: "Tietoa sinusta ja kotitaloudestasi",
event: "Muistamamme tapahtumat",
semantic: "Havaitut tavat",
profile_summary: "Profiilisi lyhyesti",
recipe_memory: "Reseptit ja ateriat",
},
nl: {
structured_fact: "Feiten over jou en je huishouden",
event: "Gebeurtenissen die we onthouden",
semantic: "Waargenomen patronen",
profile_summary: "Je profiel in het kort",
recipe_memory: "Recepten en maaltijden",
},
pl: {
structured_fact: "Fakty o Tobie i Twoim gospodarstwie",
event: "Zdarzenia, które pamiętamy",
semantic: "Zaobserwowane wzorce",
profile_summary: "Twój profil w skrócie",
recipe_memory: "Przepisy i posiłki",
},
pt: {
structured_fact: "Factos sobre si e o seu agregado",
event: "Eventos que recordamos",
semantic: "Padrões observados",
profile_summary: "O seu perfil em resumo",
recipe_memory: "Receitas e refeições",
},
};
/**
* Deterministisk rendering av summary ur strukturerad value (i18n-spec §23, M10).
* Kända value-former renderas per språk; okända faller tillbaka på summarySv
* (nya minnen får lokaliserad summary direkt från AAMOS via localeContext).
*/
export function renderMemorySummary(
item: Pick<MemoryItem, "summarySv" | "value">,
languageTag: string,
): string {
const lang = languageTag.split("-")[0] ?? "sv";
if (lang === "sv") return item.summarySv;
const v = item.value as Record<string, unknown> | null | undefined;
if (v && typeof v === "object") {
if (typeof v.favoriteCuisine === "string") {
return `Favorite cuisine: ${String(v.favoriteCuisine)}`;
}
if (typeof v.dislikedIngredient === "string") {
return `Avoids: ${String(v.dislikedIngredient)}`;
}
if (typeof v.typicalPortions === "number") {
return `Usually cooks ${v.typicalPortions} servings`;
}
if (typeof v.spicePreference === "string") {
return `Spice preference: ${String(v.spicePreference)}`;
}
}
return item.summarySv;
}
export function buildMemoryOverview(items: MemoryItem[], languageTag = "sv"): MemoryOverview {
const lang = languageTag.split("-")[0] ?? "sv";
const titles = KIND_TITLES[lang] ?? KIND_TITLES.sv!;
const byKind = new Map<MemoryKind, MemoryItem[]>();
for (const item of items) {
const list = byKind.get(item.kind) ?? [];
list.push(item);
byKind.set(item.kind, list);
}
const sections = [...byKind.entries()].map(([kind, list]) => ({
kind,
title: titles[kind],
titleSv: KIND_TITLES.sv![kind],
items: list
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
.map((i) => ({ ...i, summary: renderMemorySummary(i, languageTag) })),
}));
return {
language: lang,
sections,
totalCount: items.length,
pausedCount: items.filter((i) => i.paused).length,
};
}
/** Pseudonymisera id innan det skickas till AAMOS (spec §56: minimization). */
export function pseudonymize(id: string): string {
// Enkel stabil pseudonym riktiga miljöer använder HMAC med rotationsbar nyckel.
return `subj_${Buffer.from(id).toString("base64url").slice(0, 16)}`;
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"types": ["node"]
}
}