Gå til innholdet

decision-log

Dette dokumentet er auto-synket fra kildefilene i boligassistent-repoet. Endringer her vil overskrives ved neste sync. Rediger kildefilen direkte.

ADR-022: Admin API-token for maskin-til-maskin-tilgang til admin-endepunkter

Section titled “ADR-022: Admin API-token for maskin-til-maskin-tilgang til admin-endepunkter”

Dato: 2026-04-29 Status: Besluttet

Kontekst

Claude Code må kunne lese og oppdatere produktfeedback i prod-DB-en som del av /plan-phase, /implement-spec og /review-spec (Spec 076). Auth.js-sesjoner krever interaktiv innlogging, CSRF-håndtering og cookie-juggling — ikke egnet for ikke-interaktive shell-scripts. Direkte prod-DB-tilgang fra dev-maskin reverserer ADR-021-separasjonen mellom dev og prod.

Alternativer vurdert:

  • Auth.js-sesjon i scripts: krever email+passord i .env, CSRF-token-handshake, cookie-jar i shell. Skjørt og sikkerhetsmessig like dårlig som token (passord er bredere skoped enn et formål-spesifikt token).
  • Per-bruker JWT/PAT: overkill for et 2-bruker privat prosjekt. Krever utstedelses-endepunkt, lagring per bruker, rotering per bruker.
  • OAuth2 client credentials: samme overkill, krever IdP-oppsett.
  • Direkte prod-DB-credentials: bryter ADR-021-separasjonen og sprer prod-tilgang bredere enn nødvendig.

Beslutning

Admin-endepunkter (/api/v1/admin/*) aksepterer enten session-cookie med role: admin eller bearer-token via Authorization: Bearer <token>-header.

  • Tokenet er én statisk verdi lagret som ADMIN_API_TOKEN env-var på Render (encrypted secret) og samme verdi i lokal .env
  • Generert med openssl rand -base64 48 (entropi tilstrekkelig for stat. resistance)
  • Sammenlignes med crypto.timingSafeEqual for å unngå timing-leak
  • API-en kaster ved oppstart i NODE_ENV=production hvis env-var mangler (jf. engineering-standards seksjon 1: «Secrets — aldri fallback-verdier»)
  • Tokenet brukes kun mot admin-endepunkter, ikke som generell user-auth-erstatning
  • Rotasjon: kun ved kjent kompromittering — oppdater Render-env og lokal .env samtidig

Begrunnelse

  • Statisk admin-token er enklest mulige løsning som dekker behovet for et 2-bruker privat prosjekt
  • Bevarer ADR-021-separasjonen — ingen prod-DB-credentials på dev-maskin
  • Auth.js-sesjon-pathen åpner for fremtidig admin-UI uten ekstra arbeid
  • Konstanttidssammenligning beskytter mot timing-baserte angrep mot kort token-prefix

Konsekvenser

  • Ny middleware requireAdminTokenOrSession i packages/auth/src/middleware.ts
  • Ny env-variabel ADMIN_API_TOKEN (Render + .env + .env.example + turbo.json i build/dev/test-tasks)
  • Ny env-variabel BOLIG_API_URL (default https://bolig.bomedai.com) for at scripts kan peke på lokalt API i dev
  • Bootstrap-validering i apps/api/src/index.ts — API-en starter ikke i produksjon uten gyldig token
  • Tokenet logges aldri (bekreftes via Pino-redact-default — ingen Authorization i request-logging)
  • Rate-limiteren apiLimiter (/api/v1) dekker disse endepunktene
  • Spec 076 er primær konsument; admin-UI som konsument er fremtidsrett, ikke i scope

Hva som ikke besluttes her (utsettes):

  • Per-bruker tokens / PAT-utstedelse: utsatt til reelt behov oppstår (multi-bruker)
  • Separat admin-CORS-policy: ikke aktuelt så lenge admin-API-er kun kalles serverside fra scripts
  • Auditlogg av admin-handlinger: vurderes ved senere admin-funksjoner som faktisk endrer brukerdata (PATCH-feedback er trygt nok uten audit)

ADR-021: Hostingplattform — Render + Cloudflare R2 erstatter hjemme-PC + Cloudflare Tunnel + MinIO

Section titled “ADR-021: Hostingplattform — Render + Cloudflare R2 erstatter hjemme-PC + Cloudflare Tunnel + MinIO”

Dato: 2026-04-27 Status: Besluttet

Kontekst

Applikasjonen kjørte på utviklers hjemme-PC via Docker Compose, eksponert gjennom Cloudflare Tunnel til bolig.bomedai.com. Tjenesten er nede når PC-en er av eller nettverket svikter, har ingen automatisk failover, ingen IaC, og avhenger fullstendig av at én fysisk maskin er online. Skalering til 3–4 eiendommer × 2 brukere (6–8 brukere totalt) krever en plattform med uptime-garantier og uten tidssensitive ad hoc-vedlikehold fra utvikler.

Alternativer vurdert:

  • Hetzner CX22 + Coolify/Dokploy (~€5/mnd): billigst, men krever Linux-administrasjon, manuell oppgradering av Postgres, OS-patching. Strider mot kravet om minimal sysadmin.
  • Fly.io Machines + Neon Postgres (~$25/mnd): Docker-native, EU-region, men to separate leverandører å koordinere; mer initial-oppsett.
  • Railway (~$20/mnd flat): docker-compose-importerer enkelt, men mindre moden Postgres-backup-historikk og svakere Postgres-tooling enn Render.
  • Render (~$26/mnd): mest managed, automatisk Postgres-backup (7-dagers PITR inkludert i Basic-plan), automatisk SSL via Let’s Encrypt, auto-deploy fra GitHub, rollback med ett klikk. Beste match mot kravet om minimal sysadmin.

MinIO (AGPL-3.0) holdes som S3-kompatibelt lokalt i Docker Compose, men erstattes av Cloudflare R2 i produksjon. R2 har gratis egress og en free tier (10 GB lagring + 1M Class A-ops/mnd) som dekker bruk for 4 eiendommer komfortabelt.

Beslutning

  1. Hostingplattform: Render Web Service (Starter $7) + Render PostgreSQL (Basic 1 GB, $19) + Cloudflare R2 (free tier). Total: ~$26/mnd hosting + uendret ~$2–12/mnd Gemini-API.

  2. Kombinert Express-tjeneste i produksjon: Én container serverer både REST API (/api/*) og den bygde React-SPAen via express.static() med SPA-fallback. Dette eliminerer cross-origin cookie-problematikk (Auth.js sameSite='lax' fungerer naturlig same-origin) og holder antall services på Render til ett.

  3. Objektlagring: MinIO-klienten erstattes med @aws-sdk/client-s3 v3 + @aws-sdk/s3-request-presigner. Samme kode brukes lokalt mot MinIO og i prod mot R2 — kun S3_ENDPOINT-env varierer. URL-rewriting-blokken i getPresignedUrl() fjernes (R2-presignerte URL-er er direkte brukbare).

  4. DNS: Cloudflare DNS-only (grey cloud). CNAME bolig.bomedai.com<service>.onrender.com. Render håndterer Let’s Encrypt-cert. Cloudflare Tunnel og cloudflared-containeren utfases.

  5. CI/CD: Auto-deploy fra main-branch. Render leser render.yaml Blueprint på rotnivå for deklarativ services-konfig. preDeployCommand kjører Drizzle-migrasjoner før hver swap. Helsesjekk på /health blokkerer container-swap til ny build er klar.

  6. Bakgrunnsjobber: pg-boss kjører fortsatt i samme Express-prosess som API (uendret fra dagens oppsett). Splitt til dedikert Background Worker først ved 10+ brukere eller dokumentert CPU-saturering.

  7. Backup: Render Postgres Basic inkluderer 7-dagers PITR. Daglig kryptert pg_dump til R2 settes opp som GitHub Actions-cron som ekstra sikkerhet.

Begrunnelse

  • Renders Starter-tier har ingen sleep ved inaktivitet (kun Free-tier sover).
  • Combined service eliminerer en hel klasse av cross-origin cookie- og CORS-problemer som ville oppstått med separate Static Site + API-services.
  • R2 er S3-kompatibel: ingen kodemessig forskjell mot MinIO bortsett fra endpoint og presigning-mekanikken (som forenkles fordi URL-rewriting ikke trengs).
  • Cloudflare DNS-only unngår orange clouds 100-sek HTTP-timeout som kan kutte AI-chat-streaming eller lange Gemini Files API-kall.
  • Skalering: Standard-planen ($25) er ett-klikks-oppgradering uten nedetid når RAM eller CPU blir flaskehals — ikke premature optimization å starte på Standard.

Konsekvenser

  • Nye filer: Dockerfile.render (rot, multi-stage som bygger web + api), render.yaml (rot, Blueprint), apps/api/src/scripts/migrate.ts (kalt av preDeployCommand).
  • Endrede filer: apps/api/src/lib/minio.ts og services/minio.service.ts omskrives til AWS SDK v3 (filnavn beholdes for diff-stabilitet). apps/api/src/index.ts får express.static() + SPA-fallback i produksjons-grenen. turbo.json, .env.example, docker-compose.yml får S3-vars som erstatter MINIO_-vars. packages/db/src/migrate.ts refaktorert til å eksportere runMigrations-funksjon.
  • Beholdes uendret: Lokal docker-compose.yml med MinIO + Postgres (kun cloudflared-tjenesten og production-profilen fjernes). apps/web/Dockerfile og apps/web/nginx.conf brukes kun lokalt — ikke på Render.
  • Datamigrering: Eksisterende eiendomsdata, embeddings og bilder migreres via pg_dump/restore + rclone sync. Gjennomføres som planlagt nedetidsvindu med delta-sync ved cutover.
  • pgvector-aktivering: Render Postgres aktiverer ikke pgvector automatisk — CREATE EXTENSION IF NOT EXISTS vector; må kjøres manuelt via Render PSQL-konsoll før første migrate (init.sql i Docker Compose kjøres ikke på managed Postgres).
  • Rollback-strategi: Lokal Docker-volume og MinIO-data beholdes som kald sikkerhetskopi i 14 dager etter cutover. CNAME kan flippes tilbake til Cloudflare Tunnel ved alvorlig regresjon.
  • services-and-licensing.md oppdateres med Render Web Service, Render PostgreSQL og Cloudflare R2 i kostnadstabellen. MinIO-AGPL-blokken markeres som “kun lokal dev”.

Hva som ikke besluttes her (utsettes til senere):

  • Migrasjon til Render Standard-plan ($25): trigges reaktivt ved 502-feil eller RAM > 80 % konsekvent.
  • Cloudflare orange cloud (proxied): vurderes etter at SSE-streaming og lange AI-kall er bekreftet stabile på grey cloud.
  • Splitt av pg-boss til Background Worker: utløses ved 10+ brukere eller dokumentert CPU-saturering.
  • ISO 27001 / SOC 2 / EU-residency-krav: ikke aktuelt mens dette er privat bruk.

ADR-020: Samtalehukommelse — nøkkelfakta lagres i DB og injiseres i neste sesjon

Section titled “ADR-020: Samtalehukommelse — nøkkelfakta lagres i DB og injiseres i neste sesjon”

Dato: 2026-04-17 Status: Besluttet

Kontekst

Brukere forventer at AI-assistenten husker viktige fakta på tvers av sesjoner — f.eks. at brukeren har allergier mot fuktprodukter, at badet er under planlegging eller at de foretrekker å gjøre enklere ting selv. Gemini 2.5 Flash har 1M token kontekstvindu, som er mer enn nok for én hel sesjon inkludert historikk og hentet kontekst. Men sesjoner er avgrenset — en ny sesjon starter uten hukommelse fra forrige.

Beslutning

Etter at en sesjon avsluttes (f.eks. når brukeren navigerer bort eller eksplisitt avslutter), ber AI modellen identifisere opptil 10 nøkkelfakta fra samtalen som bør huskes. Disse skrives til conversation_memory-tabellen. Ved neste sesjon hentes aktive minner og injiseres i system-prompten som en kompakt blokk.

Minner utløper ikke automatisk, men kan deaktiveres (is_active = false) av bruker eller av modellen (dersom et faktum er utdatert).

Begrunnelse

  • Full historikk-injeksjon per sesjon: mulig men unødvendig — nøkkelfakta er mer presist og billigere
  • Vektor-basert «long-term memory»: overkill for 2 brukere med fokusert brukshistorikk
  • DB-lagring: enkel, testbar, reviderbar — ingen ekstra infrastruktur

Konsekvenser

  • Ny tabell: conversation_memory (se entities.md)
  • AI-endepunkt ber modellen om å generere minner etter siste assistent-tur i sesjonen
  • System-prompt inkluderer [HUKOMMELSE FRA TIDLIGERE SESJONER]-blokk
  • Minner er per bruker (ikke per eiendom) — to brukere på samme hus har separate minner
  • Ingen automatisk utløp: minner akkumuleres over tid og filtreres aktivt

ADR-019: Tool use-mønster — 4 strukturerte verktøy, ikke ReAct-agent

Section titled “ADR-019: Tool use-mønster — 4 strukturerte verktøy, ikke ReAct-agent”

Dato: 2026-04-17 Status: Besluttet

Kontekst

AI-assistenten trenger tilgang til live data fra boligdatabasen (observasjoner, vedlikeholdsplan, forbedringer, tilstandsavvik). Alternativene er: (a) ReAct-agent som iterativt kaller verktøy og resonnerer, eller (b) forhåndsdefinerte verktøy modellen kan kalle i én omgang.

Beslutning

4 forhåndsdefinerte verktøy eksponeres for AI-modellen via Gemini function calling:

VerktøyInputOutput
søk_eiendomsdataquery, propertyIdHybrid-søk (semantisk + full-tekst) over alle entiteter
hent_vedlikeholdpropertyId, sesong?Vedlikeholdsplan med oppgaver og status
hent_observasjonerpropertyId, romId?Observasjoner filtrert per rom
hent_forbedringerpropertyId, status?Forbedringsidéer filtrert på status (vurderer/planlagt/kjøpt)

Brave Search integreres som 5. verktøy (søk_eksternt) for reell-tid data (priser, reguleringer, produkter) — dette er en separat integrasjon med egne env-variabler og kostnadskonsekvenser (se ADR-019b når Brave Search implementeres).

Begrunnelse

  • ReAct-agenter er uforutsigbare, vanskelige å teste og kan gå i uendelige resonnerings-løkker
  • 4 begrensede verktøy gir kontrollerbar, testbar og deterministisk oppførsel
  • Gemini function calling er stabilt og veldokumentert
  • Brave Search skilles ut fordi det er en ekstern integrasjon med separate kostnader og env-vars

Konsekvenser

  • Verktøy implementeres som TypeScript-funksjoner med Zod-validert input
  • Modellen kaller verktøy i ett steg (ikke iterativt) — svar injiseres i neste prompt-tur
  • Testene kan mocke verktøy-output og verifisere at modellen bruker resultatet korrekt
  • Brave Search er separat spec (Spec 046) og separat env-var (BRAVE_SEARCH_API_KEY)

ADR-018: Chat-arkitektur — SSE streaming, Gemini 2.5 Flash, alltid re-hent fra DB

Section titled “ADR-018: Chat-arkitektur — SSE streaming, Gemini 2.5 Flash, alltid re-hent fra DB”

Dato: 2026-04-17 Status: Besluttet

Kontekst

AI-assistenten genererer svar som kan ta 5–30 sekunder. Brukere forventer progressivt svar (streaming), ikke et langt tomrom etterfulgt av fullstendig tekst. Parallelt er det risiko for «context poisoning» — at hallusinerte fakta fra tidlige assistent-turer akkumuleres og behandles som fakta i påfølgende turer.

Beslutning

  1. SSE (Server-Sent Events) som streaming-transport — ikke WebSocket
  2. Gemini 2.5 Flash (gemini-2.5-flash) med 1M token kontekstvindu
  3. Alltid re-hent fra DB per samtaletur — ikke stol på AI-omtalte fakta som kilde
  4. System-prompt skiller mellom [FAKTUM FRA DB] og [MODELLVURDERING]

Begrunnelse

  • SSE er HTTP-native: ingen ekstra infrastruktur (WebSocket krever upgrade-handshake og holder tilkobling oppe — unødvendig for request-response-mønsteret i en AI-chat)
  • Gemini 2.5 Flash: #1 på mange benchmarks, 1M kontekstvindu tillater full samtalehistorikk uten å trunkere — eliminerer behov for strikse tur-limiter
  • Re-hent fra DB: forhindrer at hallusinerte fakta akkumuleres (context poisoning). Modellen kan tro at “badet ble renovert i 2022” fordi den sa det i tur 3 — med DB-re-hent er det kun verifiserte fakta som kan siteres som sannhet

Konsekvenser

  • Express SSE-endepunkt: res.setHeader('Content-Type', 'text/event-stream'), res.write('data: ...\n\n') per chunk
  • Frontend: EventSource API eller fetch med ReadableStream
  • Samtalehistorikk: siste N turer sendes med i hvert kall (Gemini 1M context = ingen streng grense)
  • Tool-kall (ADR-019) kjøres i kontekst av DB-re-hent per tur, ikke mellomlagret i historikk
  • Ingen WebSocket-infrastruktur i prosjektet (se services-and-licensing.md)

ADR-017: Søkestrategi — hybrid RRF (pgvector cosine + PostgreSQL full-tekst, norsk)

Section titled “ADR-017: Søkestrategi — hybrid RRF (pgvector cosine + PostgreSQL full-tekst, norsk)”

Dato: 2026-04-17 Status: Besluttet

Kontekst

Semantisk søk alene fungerer dårlig for boligdata: modellnummer (NIBE S1255-16), TG-koder (TG2), rom-IDer og norske tekniske termer er ikke representert godt i embedding-rommet. Ren full-tekst-søk mister semantisk likhet (f.eks. «vannlekkasje» vs. «fuktskade»).

Beslutning

Hybrid søk kombinert med Reciprocal Rank Fusion (RRF):

  1. Semantisk søk: cosine similarity mot content_embeddings via pgvector
  2. Full-tekst søk: to_tsquery + to_tsvector med norsk Snowball-stemmer (norwegian)
  3. RRF-kombinasjon: score = 1/(k + rank_semantic) + 1/(k + rank_fulltext) (k=60)

Implementeres som én PostgreSQL-funksjon som returnerer rangerte resultater.

Begrunnelse

  • Ingen ekstra infrastruktur: pgvector og PostgreSQL full-tekst er allerede tilgjengelig
  • Norsk Snowball-stemmer er innebygd i PostgreSQL: to_tsvector('norwegian', text)
  • RRF er enkel, robust og veldokumentert — ikke avhengig av hyperparameter-tuning
  • Håndterer både modellnummer (full-tekst) og semantisk likhet (embedding) i ett kall

Konsekvenser

  • content_embeddings-tabell (ADR-016) er forutsetning
  • PostgreSQL-funksjon hybrid_search(query, propertyId, limit) implementeres i Drizzle via SQL-tag
  • søk_eiendomsdata-verktøyet (ADR-019) kaller denne funksjonen
  • tsv_index på relevante tekst-kolonner i eksisterende tabeller (Drizzle-migrasjoner)

ADR-016: Embedding-modell — gemini-embedding-001 med MRL, HNSW-indeks i pgvector

Section titled “ADR-016: Embedding-modell — gemini-embedding-001 med MRL, HNSW-indeks i pgvector”

Dato: 2026-04-17 Status: Besluttet

Kontekst

Semantisk søk over boligdata krever en embedding-modell som håndterer norsk tekst med høy kvalitet. text-embedding-004 (tidligere brukt i planlegging) ble deprecated januar 2026. pgvector med 3072-dimensjonale vektorer er kostbart på disk og indeks-størrelse for de mengdene vi snakker om (estimert <10K vektorer totalt for 2 brukere).

Beslutning

  • Modell: gemini-embedding-001 (output_dimensionality=1536)
  • MRL (Matryoshka Representation Learning): trim fra 3072 → 1536 dims, gir <0.3% kvalitetstap
  • Indeks: HNSW i pgvector (m=16, ef_construction=64), 95%+ recall
  • Chunk-strategi: semantiske chunks per entitet (ikke fast window) — ett chunk per felt-gruppe

Begrunnelse

  • gemini-embedding-001 er #1 MTEB Multilingual (68.32) og støtter norsk nativt
  • MRL-trim halverer vektor-størrelsen (fra 3072 til 1536) med minimalt kvalitetstap
  • HNSW: ingen treningsfase (IVFFlat krever minst 100 vektorer per centroid og re-training ved datavekst), inkrementelle inserts, 95%+ recall
  • Samme API-nøkkel (GEMINI_API_KEY) — ingen ny avhengighet

Konsekvenser

  • pgvector extension: allerede tilgjengelig i PostgreSQL-imaget
  • Ny tabell: content_embeddings (se entities.md)
  • Embedding-jobb: pg-boss-worker trigges etter CRUD-hendelser (create/update), ikke synkront
  • Batch-jobb: embed:all script for å indeksere eksisterende data ved Fase 5-oppstart
  • Env-var: ingen ny (bruker GEMINI_API_KEY)
  • Kostnad: ~$0.00004 per 1000 tokens — estimert <$0.01/mnd for løpende oppdateringer

ADR-015: CommonJS-modulkonfigurasjon og pnpm-deploy for Node.js produksjonsbygg

Section titled “ADR-015: CommonJS-modulkonfigurasjon og pnpm-deploy for Node.js produksjonsbygg”

Dato: 2026-04-16 Status: Besluttet

Kontekst

Prosjektet brukte opprinnelig module: "ESNext" og moduleResolution: "bundler" i alle TypeScript-konfigurasjoner — optimalisert for Vite og tsx-kjøring i dev. Dette fungerte i utvikling, men feiler i produksjons-Docker-bygg av tre grunner:

  1. moduleResolution: "bundler" genererer import-setninger uten .js-extension. Node.js ESM krever eksplisitte extensions (import './routes/index.js', ikke ./routes/index).

  2. Workspace-pakker (auth, db, types) eksporterte TypeScript-kildefiler (src/index.ts). Plain node (uten tsx) kan ikke laste .ts-filer direkte.

  3. pnpms symlink-baserte node_modules fungerer ikke i Docker multi-stage builds — symlinker fra builder-stagen finnes ikke i runner-stagen.

Beslutning

  1. CommonJS for alle server-side pakker: module: "CommonJS", moduleResolution: "node" i tsconfig.json for apps/api, packages/auth, packages/db, packages/types. moduleResolution: "node" er den eneste korrekte oppløsningen for Node.js runtime.

  2. dist/-exports i alle workspace-pakker: Alle package.json i workspace-pakker eksporterer ./dist/index.js, ikke TypeScript-kildefiler.

  3. pnpm deploy for Docker runner-stage: Bruker pnpm --filter @boligassistent/api deploy --prod /deploy i builder-stagen. Dette lager en standalone-katalog med alle avhengigheter flattened — ingen symlinker, fungerer i isolert runner-stage.

  4. Ekskludering av ESM-only scripts: src/migrate.ts bruker import.meta.url (ESM-only). Ekskludert fra packages/db/tsconfig.json — kjøres kun via tsx src/migrate.ts direkte.

  5. X-Forwarded-Proto fra Cloudflare: nginx skal sende $http_x_forwarded_proto (Cloudflares verdi) videre til Express, ikke regenerere verdien fra intern HTTP-skjema. Express trust proxy: 1 + Auth.js bruker denne headeren for å generere riktige HTTPS-redirects.

Konsekvenser

  • Produksjonsbygg kompilerer og starter korrekt med node dist/index.js
  • tsx-kjøring i dev er uendret — bruker fortsatt bundler-oppløsning via Vite
  • migrate.ts kan ikke kompileres av tsc (ekskludert), men kjøres direkte via tsx
  • Auth.js genererer korrekte https://-redirect-URLer bak Cloudflare Tunnel

ADR-014: Gemini Files API som standard for PDF-ekstraksjon

Section titled “ADR-014: Gemini Files API som standard for PDF-ekstraksjon”

Dato: 2026-03-29 Status: Besluttet

Kontekst

Spec 007 (salgsoppgave-import) ekstraherte tekst fra PDF via pdf-parse og sendte tekstslice til Gemini som en streng. En typisk salgsoppgave er 60–100 sider; tilstandsrapporter er 100–160 sider. Teksttrunkering er ikke akseptabelt for høy-presisjon ekstraksjon (prinsipp: AI-funksjonalitet skal ha få begrensninger og levere presisjon og kvalitet).

Spec 011 (tilstandsrapport-import) krever ekstraksjon av alle TG-avvik — et oppgave der visuell forståelse side for side er vesentlig bedre enn rå tekst.

Beslutning

Bruk GoogleAIFileManager (fra @google/generative-ai/server) som standard metode for PDF-ekstraksjon. PDF-bufferen skrives til en midlertidig fil, lastes opp til Gemini Files API, modellen kaller med fileData-referanse, og filen slettes fra Gemini etter ekstraksjon.

Samme mønster brukes for begge ekstraksjonspaths:

  • gemini.extractor.ts (salgsoppgave) — oppgradert fra tekst-slice til Files API
  • tilstandsrapport-extractor.ts (tilstandsrapport) — ny fil, bruker Files API fra start

Begrunnelse

  • Ingen tekstgrense: Gemini leser hele PDF-en som et visuelt dokument side for side
  • Presisjon: visuell layout (tabeller, avsnitt, TG-markering) bevares
  • Konsistens: ett mønster for begge ekstraktorer
  • Personvern: filer slettes eksplisitt fra Gemini etter ekstraksjon
  • Samme GEMINI_API_KEY og SDK — ingen ny avhengighet

Konsekvenser

  • gemini.extractor.ts: signaturen endres fra (pdfText: string) til (pdfBuffer: Buffer, fileName: string)
  • ingestion.worker.ts: sender buffer og file_name direkte til ekstraktor (ikke lenger pdfText)
  • Tmp-fil opprettes i os.tmpdir() og slettes i finally-blokk (lekker ikke)
  • Lagringstid på Gemini-siden: 48 timer (eksplisitt sletting gjøres uansett)
  • Prising: ~$0.15 per salgsoppgave, ~$0.38 per tilstandsrapport (innenfor $2–10/mnd-budsjettet)

ADR-013: Bytt AI-leverandør fra Anthropic til Google Gemini

Section titled “ADR-013: Bytt AI-leverandør fra Anthropic til Google Gemini”

Dato: 2026-03-18 Status: Besluttet

Kontekst

Spec 007-lite (Salgsoppgave-import) var planlagt med @anthropic-ai/sdk og ANTHROPIC_API_KEY. Brukeren har ikke tilgang til Anthropic API-nøkkel, men har opprettet en Google Gemini API-nøkkel.

Beslutning

Bytt AI-leverandør for PDF-ekstraksjon fra Anthropic Claude til Google Gemini:

  • SDK: @anthropic-ai/sdk@google/generative-ai (Apache 2.0-lisens)
  • Env-variabel: ANTHROPIC_API_KEYGEMINI_API_KEY
  • Modell: gemini-2.0-flash (støtter structured JSON output via responseMimeType)
  • Structured output: responseMimeType: 'application/json' + responseSchema i generationConfig

Begrunnelse

  • Tilgjengelighet: brukeren har Gemini-nøkkel, ikke Anthropic
  • Kostnad: Gemini 2.0 Flash er langt billigere (~$0.01 vs ~$0.25 per salgsoppgave-import)
  • Kvalitet: tilstrekkelig for strukturert JSON-ekstraksjon fra norske salgsoppgaver

Konsekvenser

  • apps/api/package.json: @anthropic-ai/sdk fjernes, @google/generative-ai legges til
  • .env.example: ANTHROPIC_API_KEYGEMINI_API_KEY
  • apps/api/CLAUDE.md: env-var-tabell oppdatert
  • docs/architecture/services-and-licensing.md: Claude API-seksjon erstattet med Gemini-seksjon
  • Ingen kodeendringer i src/ ennå — implementeres som del av Spec 007-lite
  • Prinsippet «AI foreslår → bruker godkjenner → system lagrer» (ADR-010) er uendret

ADR-012: Innføring av Building-entitet mellom Property og Floor

Section titled “ADR-012: Innføring av Building-entitet mellom Property og Floor”

Dato: 2026-03-18 Status: Besluttet

Kontekst

Den opprinnelige modellen hadde Property som eneste toppentitet og blandet tomt-konseptet (adresse, tomteareal, ownership) med bygningskonseptet (byggeår, BRA, type). En bruker med hus, garasje og dukkehus på én tomt har behov for å modellere tre separate bygninger — noe som ikke var mulig med én Property-entitet.

Endringen gjøres før Spec 001 implementeres, slik at vi unngår migrasjonsgjeld.

Beslutning

Innfør Building som ny entitet mellom Property og Floor.

  • Property representerer tomten: adresse, tomteareal, eierform
  • Building representerer én fysisk bygning på tomten: byggeår, BRA, type
  • Floor tilhører Building (ikke direkte Property)
  • SupportSpace tilhører Building (nullable)
  • BuildingSystem tilhører Building (ikke Property)
  • OutdoorSpace tilhører fortsatt Property (det er tomtens uteområder)
  • building legges til som gyldig verdi i alle location_ref_type-enums

Begrunnelse

Uten Building-entitet er det umulig å holde etasjer, rom, systemer og tilstand adskilt per bygning på en eiendom med flere bygninger. Endringen er minimal og ikke overingeniering — den løser et reelt, konkret brukerbehov uten å innføre unødvendig kompleksitet.

Konsekvens

  • BuildingSystem.property_idBuildingSystem.building_id
  • Floor.property_id + nytt Floor.building_id (behold property_id for ytelse)
  • SupportSpace får nullable building_id
  • DB-tabellene housesproperties, house_permissionsproperty_permissions
  • Ny buildings-tabell i DB-skjema
  • Spec 001 og Spec 002 oppdateres til å inkludere Building der relevant

ADR-011: Express 5 routing — ingen wildcards, Auth.js uten path-parameter

Section titled “ADR-011: Express 5 routing — ingen wildcards, Auth.js uten path-parameter”

Dato: 2026-03-18 Status: Besluttet

Kontekst

Under Fase 0-verifikasjon feilet API-oppstart med PathError: Missing parameter name at index 11: /api/auth/*. Express 5 bruker path-to-regexp v8 som bryter bakoverkompatibiliteten med Express 4s /*-wildcard-syntaks.

Forsøk på å bruke navngitt wildcard /*splat løste PathError, men Auth.js (@auth/express) mottok da feil URL-format og returnerte 400 Bad request på alle auth-endepunkter.

Beslutning

Auth.js monteres uten wildcard:

app.use('/api/auth', ExpressAuth(authConfig))

Generelt prinsipp for alle routes i Express 5: bruk app.use('/prefix', handler) for sub-rute-matching — wildcards trengs ikke for dette. Navngitt wildcard (/*splat) brukes kun når path-resten eksplisitt skal fanges og brukes i handleren.

Begrunnelse

app.use(path, handler) i Express matcher alle requests der URL starter med path, uavhengig av hva som kommer etter. Dette er tilstrekkelig for montering av sub-applikasjoner og third-party middleware som Auth.js.

Konsekvens

Alle fremtidige route-registreringer i apps/api følger dette mønsteret. Se engineering-standards.md seksjon 9 for fullstendig oversikt over Express 5 breaking changes.


ADR-010: Ingestion Engine — design av onboarding-pipeline

Section titled “ADR-010: Ingestion Engine — design av onboarding-pipeline”

Dato: 2026-03-17 Status: Besluttet

Kontekst Brukerbehov B22–B27 beskriver et onboarding-behov: bruker laster opp salgsoppgave og takstrapport, AI ekstraherer strukturerte data, bruker godkjenner før noe lagres. Dokumentet “Brukerbehov ved oppstart” foreslo fire nye entiteter: SourceDocument, ExtractionJob, ExtractedFact, ReviewItem.

Beslutninger

1. Ikke opprette SourceDocument — bruk eksisterende Document Document finnes allerede med doc_type: takstrapport/salgsoppgave. Å lage en ny SourceDocument-entitet ville skapt to overlappende “dokument”-konsepter. I stedet: legg til ingestion_status-felt på Document.

2. ReviewItem slås inn i ExtractedFact ReviewItem hadde kun to felt (extracted_fact, user_action). Disse legges direkte på ExtractedFact (user_action, user_corrected_value, reviewed_at, reviewed_by). Enklere skjema, ingen ekstra join, fullstendig audit-trail bevares.

3. ExtractedFact utvides med field_path og suggested_value Forslaget manglet presisjon om hva faktumet gjelder. field_path (f.eks. rooms[0].area_m2) og suggested_value (JSONB) gjør det mulig å mappe fakta direkte til entitetsfelter og bygge en strukturert review-UI.

4. pg-boss som jobbkø — ikke Redis/BullMQ Asynkron PDF-prosessering krever en jobbkø. pg-boss bruker eksisterende PostgreSQL og krever ingen ny infrastruktur. BullMQ (Redis) ville lagt til ny tjeneste. MIT-lisens. Ingen kostnader.

5. Plassering i roadmap: Spec 007, Fase 4 Ingestion Engine bygges etter Spec 001–006 fordi extraction-pipelinen mapper direkte til kjerneentitetene — de må være stabile definert først. Naturlig å bygge parallelt med AI-assistenten da begge bruker Claude API.

Begrunnelse for kritisk designprinsipp «AI foreslår → bruker godkjenner → system lagrer» er ufravikelig fordi:

  • AI gjør feil — særlig på norske takstrapporter med ustrukturert layout
  • Brukeren må ha tillit til at data er korrekt
  • Feil data i kjernemodellen er verre enn ingen data (jf. prioritering i CLAUDE.md)

Kostnadsimplikasjon Parsing av én takstrapport (60 sider ≈ 80 000 tokens) koster $0.25 ved Sonnet 4.6-priser. Onboarding med 2–3 dokumenter totalt ≈ $0.50–0.75. Engangsbeløp, godt innenfor prosjektets kostnadsprofil ($2–10/mnd). Ingen flagg nødvendig.

Konsekvenser

  • Document får nytt felt ingestion_status
  • Spec 007 opprettes for Ingestion Engine
  • apps/api/src/ingestion/ modul legges til monorepo-strukturen
  • pg-boss legges til som avhengighet (dokumentert i services-and-licensing.md)

ADR-009: ImprovementDependency fremfor separat Project-entitet

Section titled “ADR-009: ImprovementDependency fremfor separat Project-entitet”

Dato: 2026-03-17 Status: Besluttet

Kontekst Brukerbehov B20 (“Kan vi kombinere kjøkken og bad-oppussing?”) og B21 (“Hva bør vi IKKE gjøre nå?”) krever strukturert modellering av avhengigheter mellom forbedringsprosjekter. Et utkast (“Forslag utvidelse modell”) foreslo en ny Project-entitet med ProjectDependency.

Beslutning Ikke innføre en separat Project-entitet. I stedet:

  1. Fjerne fritekst-feltet dependencies fra ImprovementIdea
  2. Legge til ny ImprovementDependency-junction-tabell med strukturert dependency_type
  3. ImprovementIdea fungerer som “prosjekt” for større tiltak via kobling til Tasks og andre idéer

Begrunnelse

  • Project i forslaget var nesten identisk med ImprovementIdea — duplikat-problem
  • Å innføre en ny entitet ville kreve svar på: “Hva er forskjellen på et Project og en ImprovementIdea?”
  • ImprovementDependency gir den strukturerte avhengighetsmodelleringen som B20/B21 krever
  • Simpler modell er enklere å vedlikeholde og forklare til AI-assistenten

Konsekvenser

  • ImprovementIdea.dependencies (fritekst) fjernes — erstattes av ImprovementDependency
  • Spec 005 (ImprovementIdeas og Tasks) utvides til å inkludere ImprovementDependency
  • AI-assistenten kan nå resonere om sekvens og samkjøring basert på strukturerte data

ADR-008: ProductCandidate slås sammen med InteriorAsset(considering)

Section titled “ADR-008: ProductCandidate slås sammen med InteriorAsset(considering)”

Dato: 2026-03-17 Status: Besluttet

Kontekst Brukerbehov B15 og B16 (produktsøk og produktvurdering) krever at systemet kan evaluere et produkt mot romstørrelse, designretning og eksisterende møbler. Et utkast (“Forslag utvidelse modell”) foreslo en separat ProductCandidate-entitet i tillegg til eksisterende InteriorAsset og ProductReference.

Beslutning Ikke innføre ProductCandidate som separat entitet. I stedet:

  1. Utvide InteriorAsset med feltene price_nok, fits_room, matches_design
  2. InteriorAsset med status=considering er “produktkandidat”
  3. ProductReference (enkel lenkestump) beholdes for ustrukturerte referanser
  4. ShoppingTracker (prissporing) utsettes til Post-MVP og behandles separat

Begrunnelse

  • Tre overlappende entiteter (ProductReference, InteriorAsset, ProductCandidate) ville gitt tvetydig livssyklus og duplisert data
  • InteriorAsset med status=considering er allerede en “produktkandidat” — bare mangler noen evalueringsfelt
  • Livssyklusen blir tydelig: considering → owned/removed/planned
  • Enklere for AI-assistenten å arbeide med én entitetstype

Konsekvenser

  • InteriorAsset får nye felt: price_nok, fits_room, matches_design
  • room_id gjøres nullable — en vurdert sofa trenger ikke rom-tilknytning ennå
  • ShoppingTracker (prissporing med cron-jobb) er Post-MVP og ikke en del av MVP-scope
  • LightingPlan (belysningsplanlegging) er Post-MVP — krever Brave Search + LightingPlan-entitet

ADR-001: Relasjonsdatabase (PostgreSQL) fremfor grafdatabase

Section titled “ADR-001: Relasjonsdatabase (PostgreSQL) fremfor grafdatabase”

Dato: 2026-03-17 Status: Besluttet

Kontekst Domenemodellen er naturlig en property graph med mange relasjoner på kryss og tvers. Grafdatabaser (Neo4j, Amazon Neptune) ville modellert dette naturlig.

Beslutning Bruke PostgreSQL med fremmednøkler og polymorfiske referanser, ikke en grafdatabase.

Begrunnelse

  • PostgreSQL er velprøvd, godt dokumentert og eies av utviklere med SQL-kompetanse
  • pgvector gir semantisk søk uten ekstern vector store
  • Drizzle ORM gir typesikker tilgang uten overhead
  • Relasjonene i dette systemet er ikke så komplekse at graftraversal er nødvendig
  • Enklere drift og backup — én database, ikke to systemer
  • Grafdatabase kan introduseres senere dersom spørremønsteret krever det

Konsekvenser Polymorfiske referanser (location_ref_type + location_ref_id) brukes der én entitet kan peke til flere typer. Dette gir noe svakere databaseintegritet, men er akseptabelt for dette bruksområdet.


ADR-002: Issue og ImprovementIdea som separate entiteter

Section titled “ADR-002: Issue og ImprovementIdea som separate entiteter”

Dato: 2026-03-17 Status: Besluttet

Kontekst Begge entiteter representerer «ting som bør gjøres med boligen». Man kunne modellert dem som én tabell med en type-enum.

Beslutning Holde Issue og ImprovementIdea som helt separate entiteter med separate tabeller.

Begrunnelse

  • Issue er et faktum (noe er galt) — ImprovementIdea er et ønske (noe kan bli bedre)
  • De har ulike felter: Issue har TG-kode og severity, ImprovementIdea har motivasjon og backlog-status
  • Sammenblanding ville gjort prioritering og rapportering vanskeligere
  • AI-assistenten trenger å skille mellom «hva er galt» og «hva ønsker vi»

Konsekvenser Task kan peke til enten et Issue (via source_issue_id) eller en ImprovementIdea (via source_improvement_id). Begge felter kan være null.


ADR-003: Equipment og InteriorAsset som separate entiteter

Section titled “ADR-003: Equipment og InteriorAsset som separate entiteter”

Dato: 2026-03-17 Status: Besluttet

Kontekst Begge er fysiske objekter i et rom. Man kunne brukt én generisk «asset»-tabell.

Beslutning Equipment (teknisk utstyr) og InteriorAsset (møbler, dekor) er separate entiteter.

Begrunnelse

  • Equipment tilhører et BuildingSystem og har vedlikeholdsbehov
  • InteriorAsset tilhører DesignLayer og har stilattributter
  • En varmepumpe og en sofa har fundamentalt ulike datamodeller
  • AI-kontekst for «interiørrådgivning» skal ikke inkludere teknisk utstyr

Konsekvenser Ingen. Begge har egne tabeller og egne relasjoner.


ADR-004: DesignDirection som reviderbar, ikke historisk

Section titled “ADR-004: DesignDirection som reviderbar, ikke historisk”

Dato: 2026-03-17 Status: Besluttet

Kontekst Designretning endrer seg over tid. Spørsmålet er om man skal bevare historikk (alle versjoner) eller bare nåværende tilstand.

Beslutning DesignDirection har en status-enum (draft / active / revised / archived) og updated_at, men ingen full versjonshistorikk i MVP.

Begrunnelse

  • Full versjonshistorikk er kompleksitet som ikke gir verdi i MVP
  • Status-feltet gir nok kontekst til å forstå at retningen er endret
  • Dersom historikk viser seg viktig kan event sourcing legges til senere

Konsekvenser Historikk må dokumenteres manuelt (f.eks. som DecisionNote) dersom det er ønskelig.


ADR-005: Repo-basert prosjektminne og spec-driven development

Section titled “ADR-005: Repo-basert prosjektminne og spec-driven development”

Dato: 2026-03-17 Status: Besluttet

Kontekst For å sikre at Claude Code jobber kontrollert og konsistent trengs et system for prosjektminne, specs og arbeidsstyring.

Beslutning Bruke filbasert spec-driven development med dokumenter i repoet som eneste sannhetskilde. CLAUDE.md definerer arbeidsregler. Specs definerer krav. Evaluations dokumenterer verifikasjon.

Begrunnelse

  • Claude Code leser filer direkte — filbasert minne er mer pålitelig enn instruksjoner i minnet
  • Specs i repoet versjonskontrolleres og kan revideres som kode
  • Strukturert arbeidsflyt reduserer risiko for at Claude tar uønskede beslutninger
  • Gir brukeren full oversikt og kontroll over hva som implementeres

Konsekvenser Alt arbeid skal gå via spec → task → implementasjon → verifikasjon. Ingen ad-hoc implementasjon uten spec-grunnlag.


ADR-006: Polymorfiske referanser for stedskobling

Section titled “ADR-006: Polymorfiske referanser for stedskobling”

Dato: 2026-03-17 Status: Besluttet

Kontekst Mange entiteter (Issue, Observation, Task, SafetyItem) kan knyttes til ulike typer steder: Room, OutdoorSpace, SupportSpace, BuildingSystem. Alternativet er separate fremmednøkkelkolonner for hver type.

Beslutning Bruke to-kolonne polymorfisk referanse: location_ref_type TEXT + location_ref_id UUID.

Begrunnelse

  • Separate fremmednøkler (room_id, outdoor_space_id, …) gir mange nullable kolonner
  • Polymorfisk referanse er enklere å lese og vedlikeholde
  • Svakhet: ingen databasemessig fremmednøkkel-integritet — løses med applikasjonsvalidering

Konsekvenser Applikasjonstesting må verifisere at location_ref_id faktisk peker på en eksisterende entitet av riktig type. Legg til valideringslogikk i service-laget.


ADR-007: pgvector for AI-kontekst fremfor ekstern vector store

Section titled “ADR-007: pgvector for AI-kontekst fremfor ekstern vector store”

Dato: 2026-03-17 Status: Besluttet

Kontekst AI-assistenten trenger semantisk søk for å finne relevante entiteter. Alternativer: Pinecone, Weaviate, Qdrant (eksterne tjenester), eller pgvector (PostgreSQL-utvidelse).

Beslutning Bruke pgvector som PostgreSQL-utvidelse.

Begrunnelse

  • Holder alt i én database — enklere drift og backup
  • Ingen ekstern avhengighet i MVP
  • Cosine similarity-søk i pgvector er tilstrekkelig for dette bruksomfanget
  • Kan migreres til ekstern vector store later uten store kodeendringer

Konsekvenser Embeddings lagres som vector(1536) kolonner på relevante tabeller. Embedding-oppdatering må kjøres ved create og update av relevante entiteter.