feat(livia): Runde 10 Prompt 2 — eine Leadliste, dynamische Quellen, zeitliche Relevanz

Livia arbeitet nicht mehr neben dem Leadbestand, sondern in ihn hinein.

- Systemzugänge tragen Titel, Beschreibung und Adresse; die Liste zeigt je
  Eintrag nur noch Aktiv/Inaktiv und Lesen/Schreiben — kein Berechtigungsbalken,
  kein Auge-Icon, keine Lese-/Schreibgruppen
- Die Quellenregistry ist dynamisch: Der Lauf liest genau die aktiven
  Lesezugänge mit Adresse aus Livias Personalblatt. Für die drei gepflegten
  Quellen greift weiterhin ihr eigener Leser, für alles andere ein allgemeiner —
  eine Übersichtsseite, bis zu zehn Unterseiten, kein Spider. Freie Adressen
  werden auf http/https geprüft und gegen interne Netze gesperrt
- Manuell abgelegte PDF- und Word-Dokumente fliessen in denselben Lauf. Text
  wird beim Ablegen nativ extrahiert (ZIP + Flate über DecompressionStream,
  ohne neue Abhängigkeit), Datei und Text liegen in IndexedDB
- Neuer harter Filter «Zeitliche Relevanz»: das Modell beurteilt die
  Ereigniszeit der Veränderung, nicht das Publikationsdatum. «unknown» fällt
  bewusst nicht durch — ein erfundenes Datum wäre die schlechtere Antwort
- Quellentypen und Beobachtungsraum sind Chips mit Freitext; jeder Wert lässt
  sich einzeln entfernen, auch die vorgegebenen
- Info-Knopf bei «Zeitliche Relevanz» und «Mindestrelevanz» erklärt, wie der
  Wert entsteht — ohne erfundene Prozentgewichte
- Gefundene Leads gehen in den zentralen Signalbestand statt in eine zweite
  Ergebnisliste; die Herkunft steht am Signal (origin: RESEARCH_RUN)
- Der Lauf erzeugt einen echten Protokolleintrag mit den Zahlen des Laufs
- Lead-Detail: zeitliche Einordnung mit Fundstelle, semantisches Suchprofil bei
  Chancen mit «Matching starten», und bei Risiko ausdrücklich «Kein internes
  Objekt eindeutig zugeordnet» statt einer beliebigen Mock-Immobilie
- Aufgabenliste nach §11 bereinigt, «Unsichere Signale in Review Queue legen»
  von Nora übernommen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Benjamin SutterandClaude Opus 5 committed 2026-09-12 23:42:59 +02:00
1 parent 1c4b3f7dd8
commit 30daa77937
33 files changed
+2275 -301

No files matched your search

+30 -1
View File
@@ -2,14 +2,27 @@ import type { IFutureSignalProvider, FutureSignalFilters } from './IFutureSignal
import type { FutureSignal } from '../domain/futureSignal'
import type { ReviewStatus } from '../domain/enums'
import { mockFutureSignals } from '../mock-data/futureSignals'
import { TEAM_STORAGE_KEYS, loadVersioned, persistVersioned } from './teamPersistence'
// Mutation overrides — applied on top of the live mockFutureSignals import.
// Reading directly from mockFutureSignals (not a copied store) ensures that
// Vite HMR changes to the mock data are always reflected immediately.
const mutationOverrides = new Map<string, Partial<FutureSignal>>()
/**
* Leads aus Rechercheläufen (Runde 10, §2.12).
*
* Sie liegen in derselben Ablage wie der Seed und werden von `resolve()` in
* dieselbe Liste gegeben — es gibt bewusst keinen zweiten Bestand, den Nora
* separat abfragen müsste. Getrennt ist nur die Persistenz: der Seed steht im
* Code, die erzeugten Leads im Local Storage, damit ein Reload sie behält.
*/
let researchSignals: FutureSignal[] =
loadVersioned<FutureSignal[]>(TEAM_STORAGE_KEYS.RESEARCH_LEADS) ?? []
function resolve(): FutureSignal[] {
return mockFutureSignals.map(s => {
// Erzeugte Leads zuerst: der jüngste Lauf steht oben, wo man ihn sucht.
return [...researchSignals, ...mockFutureSignals].map(s => {
const override = mutationOverrides.get(s.id)
return override ? { ...s, ...override } : s
})
@@ -43,4 +56,20 @@ export const MockupFutureSignalProvider: IFutureSignalProvider = {
mutationOverrides.set(id, { ...existing, reviewStatus: status, updatedAt: new Date().toISOString() })
return resolve().find(s => s.id === id)!
},
/**
* Leads eines Recherchelaufs aufnehmen.
*
* Gleiche Kennung heisst gleicher Sachverhalt: Ein zweiter Lauf über
* denselben Artikel ersetzt den bestehenden Eintrag, statt ihn zu
* verdoppeln. Sonst wüchse die Liste bei jedem Knopfdruck um Dubletten, und
* niemand könnte mehr sagen, welcher Stand gilt.
*/
async addResearchSignals(signals: FutureSignal[]) {
const bekannt = new Map(researchSignals.map(s => [s.id, s]))
for (const s of signals) bekannt.set(s.id, s)
researchSignals = [...bekannt.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt))
persistVersioned(TEAM_STORAGE_KEYS.RESEARCH_LEADS, researchSignals)
return researchSignals.length
},
}