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:
1 parent
1c4b3f7dd8
commit
30daa77937
33 files changed
+2275
-301
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import type { SignalType, RiskLevel, ReviewStatus } from './enums'
|
||||
import type { TemporalClass } from './researchLead'
|
||||
|
||||
export type ContactType = 'EMAIL' | 'PHONE' | 'WEBSITE' | 'LINKEDIN' | 'CONTACT_PERSON'
|
||||
export type ContactConfidence = 'HIGH' | 'MEDIUM' | 'LOW'
|
||||
@@ -87,6 +88,38 @@ export interface FutureSignal {
|
||||
affectedTenant?: string
|
||||
/** Externe Quellen, aus denen Livia den Lead verdichtet hat. */
|
||||
researchSources?: SignalSource[]
|
||||
|
||||
// ── Runde 10 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Wann die beschriebene Veränderung stattfindet (§2.9).
|
||||
*
|
||||
* Ausdrücklich nicht dasselbe wie `source.publishedAt` und auch nicht
|
||||
* dasselbe wie `timeHorizonMonths`: Der Horizont ist eine Schätzung in
|
||||
* Monaten, diese Klasse eine belegte Einordnung mit Fundstelle. Optional,
|
||||
* weil Bestandssignale sie nicht tragen — fehlt sie, gilt der Lead als
|
||||
* zeitlich nicht eingeordnet und fällt durch keinen Zeitfilter.
|
||||
*/
|
||||
temporalClass?: TemporalClass
|
||||
/** Die Textstelle, die die zeitliche Einordnung trägt. Ohne sie ist sie nicht prüfbar. */
|
||||
temporalEvidence?: string
|
||||
/** Nur gesetzt, wenn die Quelle ein Datum nennt. Nie geschätzt. */
|
||||
eventDate?: string
|
||||
/**
|
||||
* Ausformuliertes Nachfrageprofil (§2.13B).
|
||||
*
|
||||
* Einzige Wahrheit für Livia und Nora: Livia erstellt es, Nora gleicht es
|
||||
* gegen den Bestand ab. Nora erzeugt bewusst keinen eigenen Text — zwei
|
||||
* Fassungen desselben Profils wären zwei Aussagen über denselben Bedarf.
|
||||
*/
|
||||
semanticSearchProfile?: string
|
||||
/**
|
||||
* Herkunft des Signals. `RESEARCH_RUN` markiert Leads, die ein
|
||||
* Recherchelauf erzeugt hat — sie stehen in derselben Liste wie die
|
||||
* erfassten und unterscheiden sich nur durch dieses Feld, nicht durch eine
|
||||
* eigene Ablage (§2.12).
|
||||
*/
|
||||
origin?: 'SEED' | 'RESEARCH_RUN'
|
||||
}
|
||||
|
||||
// ── Chance / Risiko (Runde 8, §2) ────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Property On — manuell abgelegte Newsquellen (Runde 10, §2.6).
|
||||
*
|
||||
* Nicht jede Quelle ist eine Webseite. Ein Geschäftsbericht, ein
|
||||
* Verbandsrundschreiben, ein Protokoll aus einer Gemeindeversammlung — das
|
||||
* kommt als PDF oder Word-Datei auf den Tisch und hat keine Adresse, die
|
||||
* Livia abrufen könnte.
|
||||
*
|
||||
* Solche Dokumente sind deshalb kein eigener Datentyp mit eigener Behandlung,
|
||||
* sondern eine zweite Art, Text in denselben Recherchelauf zu geben. Was am
|
||||
* Ende beurteilt wird, ist in beiden Fällen Text mit einem Namen und einer
|
||||
* Herkunft.
|
||||
*/
|
||||
|
||||
export const ResearchDocumentKind = {
|
||||
PDF: 'PDF',
|
||||
DOCX: 'DOCX',
|
||||
} as const
|
||||
export type ResearchDocumentKind = typeof ResearchDocumentKind[keyof typeof ResearchDocumentKind]
|
||||
|
||||
export const RESEARCH_DOCUMENT_LABELS: Record<ResearchDocumentKind, string> = {
|
||||
PDF: 'PDF',
|
||||
DOCX: 'Word',
|
||||
}
|
||||
|
||||
/** Was von einem abgelegten Dokument geführt wird. */
|
||||
export interface ResearchDocument {
|
||||
id: string
|
||||
/** Dateiname, wie er beim Ablegen hiess — die einzige Bezeichnung, die der Nutzer wiedererkennt. */
|
||||
name: string
|
||||
kind: ResearchDocumentKind
|
||||
/** Grösse in Bytes; wird angezeigt, damit ein leeres Dokument auffällt. */
|
||||
size: number
|
||||
uploadedAt: string
|
||||
/**
|
||||
* Der extrahierte Text.
|
||||
*
|
||||
* Er wird beim Ablegen einmal gewonnen und danach nie wieder — die
|
||||
* Extraktion ist der teure Teil, und ihn bei jedem Lauf zu wiederholen
|
||||
* würde denselben Text ein zweites Mal ergeben.
|
||||
*/
|
||||
text: string
|
||||
/**
|
||||
* Gesetzt, wenn aus der Datei kein brauchbarer Text zu holen war (etwa ein
|
||||
* gescanntes PDF ohne Textebene). Das Dokument bleibt in der Liste — mit
|
||||
* Hinweis, statt stillschweigend nichts beizutragen.
|
||||
*/
|
||||
extractionNote?: string
|
||||
}
|
||||
|
||||
/** Obergrenze je Datei. Grössere Dateien sprengen den Speicher des Browsers. */
|
||||
export const RESEARCH_DOCUMENT_MAX_BYTES = 15 * 1024 * 1024
|
||||
|
||||
/** Ab wie vielen Zeichen ein extrahierter Text als brauchbar gilt. */
|
||||
export const RESEARCH_DOCUMENT_MIN_CHARS = 200
|
||||
@@ -46,6 +46,24 @@ export interface ResearchLead {
|
||||
/** Die tatsächlich gelesene Originalquelle — muss anklickbar sein (§2). */
|
||||
sourceUrl: string
|
||||
publishedAt?: string
|
||||
|
||||
// ── Runde 10 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Zeitliche Dimension der beschriebenen Veränderung (§2.9). */
|
||||
temporalClass: TemporalClass
|
||||
/** Der Satz aus der Quelle, der die Einordnung trägt — ohne ihn ist sie nicht prüfbar. */
|
||||
temporalEvidence: string
|
||||
/** Nur gesetzt, wenn die Quelle ein Datum nennt. Nie geschätzt. */
|
||||
eventDate?: string
|
||||
/**
|
||||
* Ausformuliertes Nachfrageprofil für Chance-Leads (§2.13B).
|
||||
*
|
||||
* Kein Stichwortfeld: Nora gleicht es gegen den Objektbestand ab, und dafür
|
||||
* muss darin stehen, was die Quelle hergibt — einschliesslich dessen, was sie
|
||||
* nicht hergibt. Ein Profil, das Lücken verschweigt, führt zu Treffern, die
|
||||
* niemand belegen kann.
|
||||
*/
|
||||
semanticSearchProfile?: string
|
||||
}
|
||||
|
||||
/** Ergebnis je abgerufener Quelle — auch im Fehlerfall (§21). */
|
||||
@@ -68,6 +86,8 @@ export interface ResearchRefreshResult {
|
||||
discarded: number
|
||||
/** Beobachtungswürdig, aber ohne bestätigten Flächenbezug. */
|
||||
watchlist: number
|
||||
/** Aussortiert, weil die Ereigniszeit ausserhalb der gewählten Fenster lag (§2.9). */
|
||||
temporalFiltered: number
|
||||
/** Nur `high` und `medium` erscheinen hier. */
|
||||
leads: ResearchLead[]
|
||||
/** Zeitpunkt des Laufs, ISO. */
|
||||
@@ -87,3 +107,44 @@ export interface ResearchRefreshResult {
|
||||
*/
|
||||
analysisError?: string
|
||||
}
|
||||
|
||||
// ── Zeitliche Relevanz (Runde 10, §2.9) ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Wann die beschriebene Veränderung stattfindet — **nicht**, wann der Artikel
|
||||
* erschienen ist.
|
||||
*
|
||||
* Das ist der ganze Punkt dieser Klassifikation: Ein Jahresbericht vom Januar
|
||||
* kann einen Standort für das dritte Quartal ankündigen (`FUTURE`), und eine
|
||||
* Meldung von heute kann einen Umzug vom Frühling beschreiben (`PAST`). Wer
|
||||
* nach Publikationsdatum filtert, bekommt in beiden Fällen das Gegenteil
|
||||
* dessen, was er sucht.
|
||||
*
|
||||
* `UNKNOWN` ist ausdrücklich erlaubt. Wenn die Quelle keine belastbare
|
||||
* zeitliche Aussage enthält, ist «weiss ich nicht» die richtige Antwort —
|
||||
* ein erfundenes Datum wäre die falsche.
|
||||
*/
|
||||
export const TemporalClass = {
|
||||
PAST: 'past',
|
||||
CURRENT: 'current',
|
||||
FUTURE: 'future',
|
||||
UNKNOWN: 'unknown',
|
||||
} as const
|
||||
export type TemporalClass = typeof TemporalClass[keyof typeof TemporalClass]
|
||||
|
||||
/** Reihenfolge im Einstellungsbereich — von abgeschlossen nach angekündigt. */
|
||||
export const TEMPORAL_FILTER_ORDER: TemporalClass[] = [
|
||||
TemporalClass.PAST,
|
||||
TemporalClass.CURRENT,
|
||||
TemporalClass.FUTURE,
|
||||
]
|
||||
|
||||
/**
|
||||
* Die Grenze zwischen «jetzt» und «später», in Wochen.
|
||||
*
|
||||
* Acht Wochen sind keine willkürliche Zahl: Sie entsprechen ungefähr der
|
||||
* Vorlaufzeit, die eine Bewirtschaftung braucht, um auf einen Flächenbedarf
|
||||
* überhaupt noch mit einem Angebot zu reagieren. Alles darunter ist ein
|
||||
* laufender Vorgang, alles darüber eine Planung.
|
||||
*/
|
||||
export const TEMPORAL_CURRENT_WEEKS = 8
|
||||
@@ -120,6 +120,19 @@ export interface AgentSystem {
|
||||
* die Zugänge ohne Schalter führt — fehlt das Feld, gilt der Zugang als aktiv.
|
||||
*/
|
||||
enabled?: boolean
|
||||
/**
|
||||
* Eigener Titel der Quelle (Runde 10, §2.1). Der Katalog benennt seine
|
||||
* Zugänge über `type`; selbst angebundene Quellen haben keinen passenden
|
||||
* Typ und tragen ihren Namen deshalb hier. Fehlt der Titel, gilt das Label
|
||||
* des Typs — so bleiben die Katalogeinträge unverändert.
|
||||
*/
|
||||
title?: string
|
||||
/**
|
||||
* Adresse der Quelle. Bei einer aktiven Lesequelle ist das die Seite, die
|
||||
* Livias Recherchelauf tatsächlich abruft — der angezeigte Link und der
|
||||
* gelesene Link sind damit derselbe Wert.
|
||||
*/
|
||||
url?: string
|
||||
/** Wofür der Agent dieses System nutzt — in Alltagssprache, ohne KI-Jargon. */
|
||||
usage: string
|
||||
/** Klartext zur Berechtigung, z. B. «Schreibt erst nach Freigabe». */
|
||||
@@ -149,6 +162,18 @@ export const AgentSettingKind = {
|
||||
MULTI_SELECT: 'MULTI_SELECT',
|
||||
BOOLEAN: 'BOOLEAN',
|
||||
TIME: 'TIME',
|
||||
/**
|
||||
* Mehrfachauswahl als Chips, jeder einzeln entfernbar (Runde 10, §§2.7–2.9).
|
||||
*
|
||||
* Der Unterschied zu `MULTI_SELECT` ist nicht bloss die Darstellung: Ein
|
||||
* Auswahlfeld kann nur anbieten, was jemand vorher als Option hinterlegt
|
||||
* hat. Quellentypen und Beobachtungsräume sind aber genau das, was sich von
|
||||
* Haus zu Haus unterscheidet — sie brauchen ein Feld, in das man schreiben
|
||||
* kann, was man meint.
|
||||
*/
|
||||
CHIPS: 'CHIPS',
|
||||
/** Abgelegte Dokumente als Datenbasis der Recherche (Runde 10, §2.6). */
|
||||
DOCUMENTS: 'DOCUMENTS',
|
||||
} as const
|
||||
export type AgentSettingKind = typeof AgentSettingKind[keyof typeof AgentSettingKind]
|
||||
|
||||
@@ -179,6 +204,21 @@ export interface AgentSetting {
|
||||
/** Fachlich nicht abschaltbar — z. B. Sinas Quellenzwang. */
|
||||
locked?: boolean
|
||||
lockedReason?: string
|
||||
/**
|
||||
* Erklärt auf Knopfdruck, wie ein Wert zustande kommt (Runde 10, §2.10).
|
||||
*
|
||||
* Gehört ans Datenmodell und nicht in die Komponente: Was ein Schwellenwert
|
||||
* bedeutet, weiss das Personalblatt, nicht das Eingabefeld. Und ein Text,
|
||||
* der eine Berechnung beschreibt, muss neben der Berechnung stehen, damit
|
||||
* beide zusammen geändert werden.
|
||||
*/
|
||||
infoText?: string
|
||||
/**
|
||||
* Nur bei `CHIPS`: ob eigene Werte erfasst werden dürfen. Bei einer
|
||||
* fachlich abgeschlossenen Liste — etwa den drei Zeitfenstern — bleibt es
|
||||
* aus, sonst entstünde ein vierter Wert, den niemand auswerten kann.
|
||||
*/
|
||||
allowCustom?: boolean
|
||||
}
|
||||
|
||||
// ── Kennzahlen & Profil ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user