08 — Gutachten-Pipeline: Six-Step Specification (plan 5.3)

Purpose. This document is the implementation spec for the Analyzer expert-report pipeline — the six sequential AI steps (Interview → Document → SWOT → Scoring → Report → Report, prototype agents ag-05…ag-10) that turn a client's questionnaire answers and uploaded documents into a partner-approved expert report with 11 lever scores, SWOT, recommendations, and a rendered PDF. It defines the state machine over ai_runs, the per-step input/output contracts and German prompt skeletons, the deterministic rules engine (score caps, traffic light), the Partner review gates, LLM call conventions against the GRAG platform, failure handling, and final report assembly.

Status / verified against: 2026-07-06, repo document-processing-pipelines @ db63a95; prototype next-monorepo/apps/web (types.ts, mock-data); integration plan tasks/todo.md rev. 2. Review corrections applied: ai-gateway has no structured output (plan 1.14), ledger paths are /ledger/api/v1/ledger/totals|spend (plan 1.6), groundedness spans are English-only, BFF upload/ingest is unauthenticated until plan 1.8 lands.

Sibling docs: architecture in 01-architecture.md, request cookbook in 02-grag-api-cookbook.md, ID formats in 03-id-conventions.md, DB DDL in 06-knoll-db-schema.md, KG projection in 07-kg-schema-knoll-advisory.md, chat lanes in 09-chat-integration.md, upload lane in 10-document-pipeline.md, error taxonomy in 11-resilience-and-errors.md, cost model in 14-cost-model.md.


1. Overview

#StepAgentKindLLM?Retrieval?Groundedness?
1Interviewag-05 Interview-AgentStructured digest of questionnaire + contradiction detection + follow-up questionsyesno (Knoll DB only)yes (vs. rendered answers)
2Documentag-06 Document-AgentFindings per checklist documentyesyes (analysis-KB search/rerank + segments/text:batch)yes
3SWOTag-07 SWOT-AgentSynthesis over 1+2yesno (prior outputs)yes
4Scoringag-08 Scoring-AgentLLM score proposal → deterministic caps + traffic light in codeyesno (prior outputs)yes (rationales)
5Report (sections)ag-09 Expert-Report-AgentLong-form German text per lever + recommendationsyes (11 calls)yes (kb-methodology for style/method)yes, per section
6Report (assembly)ag-10 Report-AgentExecutive Summary + 3 key messages; assembly, PDF, ingestyes (1 call)noyes

Design rules (from plan 5.3 / D3):

  • The pipeline is a Knoll-backend state machine over ai_runs rows. GRAG provides primitives (completions, retrieval, full-text expansion, groundedness, ledger); all sequencing, review gates, and rules live in the Knoll backend. Do not use agent-control for these single-shot steps.
  • Every LLM step goes through POST $GRAG_URL/ai-gateway/api/v1/chat/completions (non-streaming) with a fallback chain. There is no response_format/JSON mode at the gateway (plan 1.14) — structured output = prompt-JSON + Zod validation + bounded repair retries (§6.3).
  • A Partner review gate sits between every step (QMS human-in-the-loop; the prototype's Einstellungen page marks HITL-Freigabe "Pflicht laut QMS – kann nicht deaktiviert werden"). Freigabe actions require the Partner role (plan 2.3).
  • The pipeline honors knoll_ai_settings (detail_level ∈ Compact|Standard|Detailed, caps_enabled, model picker) — see §5.4.
  • Groundedness verdicts are advisory until plan 7.1 calibration — they inform the Partner, never auto-block (§4, "Bands").

2. State machine

2.1 Run and step model

One pipeline run = one attempt to produce an expert-report version for one analysis. One ai_runs row exists per step attempt (columns per plan 2.1: analysis_id, agent_id (ag-05…ag-10), step (text, well-known values interview|document|swot|scoring|report|report per 06-knoll-db-schema.md §3.12 — the "1–6" numbering below is UI ordering only), attempt, status, input/output refs, model, tokens, cost, groundedness_band, error; DDL is normative in 06-knoll-db-schema.md). Each step attempt owns its own pipeline_id (stored in ai_runs.pipeline_id at attempt start, stamped as X-Pipeline-Id on every GRAG call that attempt makes; canonical format knoll-<analysis-shortid>-<step>-<runid> per 03-id-conventions.md §8 — examples below use knoll-4b0c77de-scoring-4e21ac03 = Scoring attempt 4e21ac03 of analysis 4b0c77de). There is no run-level pipeline id: run and per-analysis spend are a Knoll-side sum over the step attempts' ids (§3.4).

2.2 Step states and transitions

                 ┌──────────┐  start   ┌─────────┐  output valid   ┌─────────────────┐
                 │  queued  │ ───────► │ running │ ──────────────► │ awaiting_review │
                 └──────────┘          └────┬────┘                 └──────┬───┬──────┘
                                            │ error/retries exhausted    │   │
                                            ▼                   approve  │   │ reject
                                       ┌────────┐                        ▼   ▼
                                       │ failed │                ┌──────────┐ ┌──────────┐
                                       └───┬────┘                │ approved │ │ rejected │
                                           │ manual retry        └──────────┘ └────┬─────┘
                                           ▼                                       │ spawns
                                    new attempt (queued)                           ▼
                                                                            new attempt (queued)
FromToTriggerSide effects
queuedprevious step approved (step 1: run started)new ai_runs row, attempt=1
queuedrunningworker picks up stepstarted_at set
runningawaiting_reviewoutput passed Zod validation; groundedness scoredoutput persisted; notification "expert-report step pending approval" (plan 2.7)
runningfailedLLM/retrieval error after bounded retries, or JSON invalid after repair retrieserror code set (§7); UI "Fehlgeschlagen"
awaiting_reviewapprovedPartner Freigeben (optionally after Bearbeiten)audit entry; next step row created queued
awaiting_reviewrejectedPartner Ablehnen with commentaudit entry; new attempt created queued with reviewer feedback injected into the prompt (§6.4)
failed(new attempt queued)user clicks "Erneut ausführen"attempt+1; approved outputs of earlier steps are reused

Terminal states per attempt: approved, rejected, failed, cancelled — all values of ai_run_status in 06-knoll-db-schema.md §2.2. Gated pipeline steps terminate at approved (the review gate is mandatory); the enum's succeeded is used only by ungated runs outside this pipeline (lead scoring, Klassifikation etc., 06 §3.12) and never appears here. There is no stored run-level status — it is derived from the step rows: a run is completed when the report step is approved and assembly (§9) succeeded; a run is cancelled when a Partner aborts it, which sets the active step attempt to cancelled (allowed in any non-terminal state; audit entry; no GRAG cleanup needed — all writes so far are Knoll-DB rows plus ledger entries).

2.3 Resume / retry semantics

  • Retry after failed/rejected re-executes only that step. Inputs = the approved outputs of steps 1..n-1 (immutable once approved within a run) + fresh reads of Knoll DB inputs are not taken — the run works on a snapshot (§3, step 1 input freeze) so retries are deterministic w.r.t. inputs.
  • "Neu ab Schritt n" (Partner action): invalidates approvals of steps ≥ n in this run (statuses stay for audit, a new attempt chain starts at step n). Use when e.g. corrected Fragebogen answers arrive mid-run — note the snapshot rule: a data change requires restart from step 1 (the UI must say so).
  • Crash recovery: a step stuck in running past 2 × step timeout (§7) is swept to failed with error=timeout by a janitor job; the completion may still have cost money — the ledger row is the source of truth for spend (§6.5).

2.4 Concurrency rule

At most one active run per analysis. Enforced by a partial unique index on ai_runs (analysis_id) WHERE status IN ('queued','running','awaiting_review') plus a check in the start action — sufficient because steps run strictly sequentially, so one active attempt = one active run. ⚠ This unique index is not yet in 06-knoll-db-schema.md §5 (06's partial index on status is a plain work-queue index, not unique) — add it there when implementing. Starting a new run while one is active requires aborting the old one explicitly. Rationale: steps share the analysis-KB and the review gates address one draft; parallel runs would double spend and confuse approval.

2.5 UI state mapping (plan 6.4)

The prototype's PipelineSchritt.status knows only "Abgeschlossen" | "Läuft" | "Ausstehend" | "Fehlgeschlagen" (lib/types.ts:189-193). The real panel needs one more label:

ai_runs.statusUI label
queuedAusstehend
runningLäuft
awaiting_reviewWartet auf Freigabe (new — extend the type in 6.4)
approvedAbgeschlossen
rejected / failedFehlgeschlagen (with reason tooltip; rejected shows the Partner comment)

3. Shared LLM call conventions (plan 1.14, 7.3)

3.1 Endpoint and headers

Every generation step calls the ai-gateway (verified CompletionRequest: model, messages, temperature, max_tokens, top_p, stop, BYOK api_key/api_base/api_version, cache, fallback (≤5), fallback_policyno response_format, no tools, no stream):

curl -sS -X POST "$GRAG_URL/ai-gateway/api/v1/chat/completions" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT" \
  -H "X-Pipeline-Id: knoll-4b0c77de-scoring-4e21ac03" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "'"$KNOLL_PIPELINE_MODEL"'",
    "messages": [
      {"role": "system", "content": "…German system prompt (§5)…"},
      {"role": "user", "content": "…user template with data…"}
    ],
    "temperature": 0.2,
    "max_tokens": 6000,
    "fallback": ["'"$KNOLL_FALLBACK_MODEL_1"'", "'"$KNOLL_FALLBACK_MODEL_2"'"],
    "fallback_policy": "retriable_only"
  }'
  • X-Tenant-ID is mandatory on every call (grag-client hard-fails without it — plan 2.5; the platform silently falls back to tenant default otherwise). Dev examples use X-Tenant-ID: knoll-dev.
  • X-Pipeline-Id carries the step attempt's pipeline_id (ai_runs.pipeline_id, §2.1) on every GRAG call that attempt makes (completions, retrieval, groundedness) — this is the only per-analysis cost attribution mechanism (plan 1.6/7.3; format and rules in 03-id-conventions.md §8). Run/analysis spend = Knoll-side sum of /ledger/totals over the attempts' ids (§3.4).
  • Response is OpenAI-shaped: choices[0].message.content, usage.{prompt_tokens, completion_tokens, total_tokens} — persist usage into the ai_runs row.

3.2 Model selection and fallback

  • Model ids are LiteLLM provider/model strings. The valid catalog (pricing, context windows, availability) comes from GET $GRAG_URL/ai-gateway/api/v1/models (this feeds the Einstellungen model picker, plan 6.6). The gateway default is openai/gpt-4o-mini — do not rely on it for Gutachten quality; always send an explicit model.
  • $KNOLL_PIPELINE_MODEL comes from knoll_ai_settings (the Settings AI tab's model picker). Configure a 1–2 model fallback chain from the same catalog; fallback_policy: "retriable_only" (429/5xx/timeout) is the right default.
  • Temperature guidance: extraction/scoring steps (1, 2, 4) 0.2; prose steps (3, 5, 6) 0.5.
  • Budget governance: the Kanzlei's provider keys live in the keyvault with monthly_budget_usd + rate_limit_rpm (plan 1.6); the gateway rejects with 402 (budget) / 429 (rate) / 403 (expired key) — handling in §7.

3.3 Structured output (the plan-1.14 mechanism)

The gateway cannot enforce JSON. Every structured step therefore uses the grag-client helper (plan 2.5):

  1. The system prompt ends with: "Antworte NUR mit einem einzigen JSON-Objekt nach dem vorgegebenen Schema. Keine Markdown-Codezäune, kein Text davor oder danach." The user message embeds the JSON Schema of the expected output.
  2. Parse choices[0].message.content: strip a leading/trailing ```json fence if present, JSON.parse, validate with the step's Zod schema (versioned, schema field mandatory, e.g. "interview_digest.v1").
  3. On parse/validation failure: max 2 repair retries. Repair call = same system prompt + the invalid output + the Zod error list + "Korrigiere das JSON so, dass es dem Schema entspricht. Gib nur das korrigierte JSON aus."
  4. After 3 total failures → step failed, error=schema_validation; store the last raw output in the run row for debugging (Knoll DB only — never log it to GRAG).
  5. When platform gap 1.14 lands (gateway passes response_format through to LiteLLM), the helper switches transport but the Zod validation stays.

3.4 Cost attribution and readback

Per-step spend comes from /ledger/totals with that attempt's pipeline_id. Run and per-analysis spend are a Knoll-side sum: select the run's (or analysis's) ai_runs.pipeline_id values, call /totals once per id, sum — /totals takes exactly one pipeline_id, there is no prefix or wildcard query (03-id-conventions.md §8 rule 2). Assembled after each step and shown live in the pipeline panel:

# one call per step attempt; the Knoll backend sums the results for the run total
curl -sS "$GRAG_URL/ledger/api/v1/ledger/totals?pipeline_id=knoll-4b0c77de-scoring-4e21ac03" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT"

Tenant-level budget display uses GET $GRAG_URL/ledger/api/v1/ledger/spend?group_by=service (only service|provider groupings exist — plan 1.6). Note: rerank and groundedness calls emit usd=0 bookkeeping rows; real spend is completions + embeddings.


4. Groundedness convention (all steps)

After each generation, the step scores its output against the context it was given (verified ScoreRequest: response_text, chunks[{text, chunk_id?}], lang_response, lang_context, maxsim_threshold?, entity_labels?, include_per_sentence, include_nli, include_spans):

curl -sS -X POST "$GRAG_URL/groundedness/api/v1/score" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT" \
  -H "X-Pipeline-Id: knoll-4b0c77de-dokument-7b2f91c4" \
  -H "Content-Type: application/json" \
  -d '{
    "response_text": "…generated German section…",
    "chunks": [
      {"text": "…context block 1…", "chunk_id": "doc-7f3:12"},
      {"text": "…context block 2…", "chunk_id": "fragebogen:kap-1"}
    ],
    "lang_response": "de",
    "lang_context": "de",
    "include_nli": true,
    "include_per_sentence": true
  }'
  • include_spans stays false — verbatim evidence spans are English-only (GROUNDEDNESS_SPAN_LANGS=en); German requests would get spans: null + note. Per-sentence scoring and NLI are multilingual and are the German evidence mechanism.
  • chunk_id convention: "<document_id>:<ordinal>" for KB chunks, "questionnaire:<chapter>" / "step<N>:<section>" for non-KB context, so per_chunk verdicts map back.
  • Response: {score, band, nli_groundedness_score?, context_coverage_ratio, context_unused_ratio, per_chunk[], per_sentence[]?, notes[]}. Persist band (and score) to ai_runs.groundedness_band. Handle notes as stable machine keys (e.g. per_sentence_unavailable, band_capped_at_amber, no_response_entities). For step 5's eleven sections use POST /groundedness/api/v1/score/batch {items:[…]} (one item per lever section).
  • Cold start: first call after deploy can take 60–120 s unless GROUNDEDNESS_PRELOAD=true (plan 1.4) — set the step timeout accordingly (§7).
  • Empty chunksband: "unknown"; never send an empty list — skip the call and record unknown locally instead.

Bands → behavior (defaults green ≥ 0.8 / amber ≥ 0.5 are uncalibrated placeholders; all behavior is advisory until plan 7.1 calibration is signed off — never auto-fail a step on band):

BandReview gate behavior
greenGreen badge; no friction.
amberAmber badge + per-sentence weak spots listed (from per_sentence).
redRed warning banner: "Geringe Quellenabdeckung — bitte Aussagen gegen die Belege prüfen." Partner must tick an explicit acknowledgement checkbox before Freigeben; acknowledgement is written to audit_log.
unknownGrey "nicht bewertet" hint (service down or no context).

5. Step specifications

Common conventions: all prompts are German; every factual claim in an output must carry provenance (frage_ids for Fragebogen facts, belege: [{document_id, ordinal}] for KB facts, step_refs for prior-step facts). Placeholders {{…}} are filled by the Knoll backend. JSON Schemas below are the normative v1 contracts (mirror them 1:1 in Zod).

5.1 Step 1 — Interview (ag-05)

Inputs (frozen at run start into the run's input snapshot):

SourceContent
Knoll DB questionnaire_answers × questionnaire_versionsAll answered questions of the analysis: 16 chapters (11 main + IT.0–IT.4), up to 151 questions (canonical Kundenfragebogen count, Anlage C.I.1 raw numbering — ADR-010 Knoll-Analyzer-Docs/wiki/adr/ADR-010-question-count-nodekey.md; = 14-cost-model.md §2.2 F; the prototype mock seeds only 105 of them, questionnaire.ts) plus the IT questionnaire (Anlage C.I.2), rendered as [{question_id, chapter, chapter_title, lever, question_text, type, answer}]
Knoll DB clientsMaster data (company name, legal form, industry, revenue, employees, HGB size class)
Knoll DB analysesPackage, phase (must be ≥ 4 "Analysis & Evaluation" to start a run)

No retrieval — this step reads only Knoll DB (source of truth for answers, plan resource mapping).

System prompt (draft):

Du bist der Interview-Agent der Knoll-Analyzer-Pipeline (Schritt 1 von 6).

Aufgabe: Überführe die Fragebogen-Antworten des Mandanten in ein strukturiertes
JSON nach den 11 Erfolgshebeln der Knoll-Methodik: Marktposition, Strategie,
Marke, Wettbewerb, Produkte & Dienstleistungen, Preispolitik, Kunden, Vertrieb,
Planung & Controlling, Marketingumsetzung, Personal.

Zusätzlich:
1. Widerspruchserkennung: Identifiziere inhaltliche Widersprüche zwischen
   Antworten, auch kapitelübergreifend (Beispiel: Kapitel 7 nennt fünf
   Großkunden mit rund 60 % Umsatzanteil, Kapitel 8 weist keine zentrale
   Vertriebsleitung aus — unbewirtschaftetes Klumpenrisiko).
2. Rückfragen: Formuliere präzise, an den Mandanten adressierte Rückfragen zu
   unklaren, fehlenden oder widersprüchlichen Angaben. Höflich, konkret,
   jeweils mit Anlass.
3. Regelwerk-Fakten: Extrahiere wörtlich die für die deterministischen
   Score-Deckelungen relevanten Antworten, mindestens: Frage 1-8
   (Businessplan vorhanden: Ja/Nein) und Frage 2-5 (schriftliche
   Unternehmensstrategie vorhanden: Ja/Nein).

Regeln:
- Verwende ausschließlich Informationen aus den übergebenen Antworten und
  Stammdaten. Erfinde nichts. Bewerte nicht — die Bewertung erfolgt in
  Schritt 4.
- Jede Aussage referenziert die Frage-ID(s), aus denen sie stammt.
- Nicht beantwortete Fragen erscheinen als Lücken beim betroffenen Hebel.
- Antworte NUR mit einem einzigen JSON-Objekt nach dem vorgegebenen Schema.
  Keine Markdown-Codezäune, kein Text davor oder danach.

User template:

## Client
{{client_master_data_json}}

## Questionnaire answers (analysis {{analysis_id}}, questionnaire version {{questionnaire_version}})
{{answers_json}}

## Output schema (JSON Schema)
{{interview_digest_v1_json_schema}}

Output contract interview_digest.v1:

{
  "schema": "interview_digest.v1",
  "hebel": [
    {
      "hebel": "Marktposition",
      "fakten": [
        {"aussage": "Es liegt kein Businessplan vor.", "frage_ids": ["1-8"], "konfidenz": "hoch"}
      ],
      "luecken": ["Frage 1-4 (Marktanteile je Geschäftsfeld) unbeantwortet"]
    }
  ],
  "regelwerk_fakten": {
    "businessplan_vorhanden": false,
    "schriftliche_strategie_vorhanden": false,
    "quellen": {"businessplan_vorhanden": "1-8", "schriftliche_strategie_vorhanden": "2-5"}
  },
  "widersprueche": [
    {
      "beschreibung": "Kapitel 7 nennt 5 Großkunden mit ~60 % Umsatzanteil, Kapitel 8 weist keine zentrale Vertriebsleitung aus.",
      "frage_ids": ["7-2", "8-4"],
      "schweregrad": "hoch"
    }
  ],
  "rueckfragen": [
    {
      "frage": "Gibt es eine interne Planungsunterlage, die als Businessplan-Ersatz dienen kann?",
      "anlass": "Frage 1-8 mit Nein beantwortet; Deckelung Marktposition droht.",
      "frage_ids": ["1-8"],
      "prioritaet": 1
    }
  ]
}

Constraints: exactly 11 hebel entries in canonical ERFOLGSHEBEL order; konfidenz ∈ hoch|mittel|niedrig; schweregrad ∈ hoch|mittel|niedrig; prioritaet ∈ 1|2|3. regelwerk_fakten booleans are advisory extraction — the rules engine (§5.4… see §6) recomputes them deterministically from the raw answers in Knoll DB (frage 1-8 / 2-5 option "Nein"); on mismatch the deterministic value wins and the mismatch is flagged in the review gate.

Groundedness: response_text = concatenated fakten claims; chunks = the rendered Kapitel answer blocks (chunk_id: "fragebogen:kap-<nr>").

Token budget: input ≈ 151 answers (canonical F, ADR-010 / 14-cost-model.md §2.2) + Stammdaten ≈ 20–40k tokens (single call; the full Fragebogen must fit — pick a model with ≥100k context from the catalog); max_tokens: 6000.

Review gate specifics: the gate shows Widersprüche and Rückfragen prominently; gate action "Rückfragen an Mandant senden" hands the approved rueckfragen list to the email module (plan 2.7). Sending Rückfragen does not block the pipeline — the Partner decides whether to pause (abort/restart later) or continue.

5.2 Step 2 — Dokument (ag-06)

Inputs:

SourceContent
Knoll DB checklist_items + files + grag_refsAll items of the 46-position checklist (Anlage C.I.3, group A + B.1–B.11) with status Received/PartiallyReceived and their GRAG document_ids in the analysis-KB
Step 1 approved outputgaps per lever (targets for gap-filling)
GRAG analysis-KB (kb-analysis-<shortid>)Indexed chunk text via retrieval

Context-assembly recipe (per document, batched per Hebel where documents are shared):

  1. Search + rerank against the analysis-KB collection (collection name = kb_id; German queries route to the multilingual reranker automatically):
curl -sS -X POST "$GRAG_URL/ai-gateway/api/v1/retrieval/collections/kb-analyse-7f3k/search/rerank" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT" \
  -H "X-Pipeline-Id: knoll-4b0c77de-dokument-7b2f91c4" \
  -H "Content-Type: application/json" \
  -d '{
    "query_text": "Umsatzentwicklung und EBIT der letzten Geschäftsjahre",
    "top_k": 12,
    "rerank_top_n": 25,
    "language": "multi"
  }'

Queries come from a fixed German query catalog per lever/document type (versioned in code; e.g. Jahresabschluss → "Umsatzentwicklung und EBIT…", "Eigenkapitalquote und Bilanzsumme"; Preisliste → "Preisstruktur und Rabattsystem"; Organigramm → "Vertriebsorganisation und Verantwortlichkeiten") plus queries generated from step 1 gaps. Response envelope: data.hits[] (key preserved from voyager; each hit has id, score, rank, bm25_rank, dense_rank, rerank_score, rerank_score_norm, and payload.{doc_id, ordinal, vector_upsert_id, text}payload.text is an ~800-char preview) and data.rerank.{retrieval_confidence, dropped_below_threshold, …}. Record retrieval_confidence per query; if none for all queries of a document, emit a gap instead of hallucinating.

  1. Full-text expansion: collect the hits' (doc_id, ordinal) pairs, add neighbor ordinals ±1, dedupe, and resolve full segment text (≤64 keys per call; X-Workspace-ID required — the client workspace from grag_refs):
curl -sS -X POST "$GRAG_URL/kg-service/api/v1/segments/text:batch" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT" \
  -H "X-Workspace-ID: mandant-hartmann-a1b2" \
  -H "X-Pipeline-Id: knoll-4b0c77de-dokument-7b2f91c4" \
  -H "Content-Type: application/json" \
  -d '{"keys": [{"document_id": "doc-7f3", "ordinal": 11},
               {"document_id": "doc-7f3", "ordinal": 12},
               {"document_id": "doc-7f3", "ordinal": 13}]}'

The response is found-only (missing keys silently absent) — fall back to payload.text previews for unresolved keys (documents ingested before KG_INGEST_ENABLED have no kg rows).

  1. Assemble context blocks per document: [Beleg doc-7f3:12] <full text>, capped at the step's context budget (drop lowest-rerank_score blocks first).

System prompt (draft):

Du bist der Dokument-Agent der Knoll-Analyzer-Pipeline (Schritt 2 von 6).

Aufgabe: Werte die hochgeladenen Unterlagen des Mandanten aus (Jahresabschluss,
Preisliste, Marketingplan, Organigramm u. a.). Extrahiere Zahlen und Fakten,
ordne sie den 11 Erfolgshebeln zu und fülle gezielt die Lücken, die der
Fragebogen offen gelassen hat.

Regeln:
- Nutze ausschließlich die übergebenen Belegtexte. Jede Kennzahl und jeder
  Fakt referenziert mindestens einen Beleg in der Form
  {"document_id": "...", "ordinal": N} (aus den [Beleg ...]-Markierungen).
- Rechne nichts hoch und interpoliere nicht; fehlende Werte sind Lücken.
- Gleiche extrahierte Fakten mit den übergebenen Fragebogen-Aussagen ab und
  markiere je Abgleich: bestätigt / widerspricht / ergänzt.
- Antworte NUR mit einem einzigen JSON-Objekt nach dem vorgegebenen Schema.

User template: Client short profile + step 1 gaps + per-document context blocks + document_findings_v1_json_schema.

Output contract document_findings.v1:

{
  "schema": "document_findings.v1",
  "dokumente": [
    {
      "document_id": "doc-7f3",
      "checklist_item_id": "ck-03",
      "typ": "Jahresabschluss",
      "kennzahlen": [
        {"name": "Umsatz 2025", "wert": "42,0", "einheit": "Mio. EUR",
         "belege": [{"document_id": "doc-7f3", "ordinal": 12}]}
      ],
      "fakten": [
        {"aussage": "…", "hebel": "Planung & Controlling",
         "belege": [{"document_id": "doc-7f3", "ordinal": 14}]}
      ],
      "luecken": ["Keine Segmentberichterstattung je Geschäftsfeld enthalten"]
    }
  ],
  "fragebogen_abgleich": [
    {"frage_id": "1-6", "befund": "bestätigt", "kommentar": "…",
     "belege": [{"document_id": "doc-7f3", "ordinal": 12}]}
  ],
  "retrieval_confidence_min": "weak"
}

befund ∈ bestätigt|widerspricht|ergänzt. retrieval_confidence_min is stamped by code (worst confidence over all queries), not by the LLM.

Groundedness: response_text = concatenated fakten/kennzahlen claims; chunks = the expanded segment texts (chunk_id: "<document_id>:<ordinal>").

Token budget: one call per document (or per lever bundle for small docs); context ≤ ~12k tokens per call after expansion, max_tokens: 4000. Typical analysis: 10–25 calls. Empty KB (no indexed documents): the step produces an all-gaps output and still goes to awaiting_review with a warning banner ("Keine Dokumente indexiert") — the Partner decides whether to proceed (deterministic skipping is not allowed; the Businessplan cap in §6 does not depend on this step).

5.3 Step 3 — SWOT (ag-07)

Inputs: approved outputs of steps 1 + 2 only (no retrieval — synthesis, all facts already carry provenance).

System prompt (draft):

Du bist der SWOT-Agent der Knoll-Analyzer-Pipeline (Schritt 3 von 6).

Aufgabe: Erstelle aus dem Gesamtdatenbild (Interview-Digest und
Dokument-Befunde) eine Stärken-Schwächen-Chancen-Risiken-Analyse als
Ankerpunkt für alle nachfolgenden Bewertungen.

Regeln:
- Faktenbasiert und kritisch: Jeder SWOT-Punkt stützt sich auf mindestens
  eine Quelle (frage_ids und/oder belege) aus den übergebenen Daten.
- 3 bis 6 Punkte je Quadrant, jeweils ein prägnanter deutscher Satz.
- Stärken/Schwächen sind interne Befunde, Chancen/Risiken externe bzw.
  zukunftsgerichtete. Ordne jedem Punkt die betroffenen Erfolgshebel zu.
- Antworte NUR mit einem einzigen JSON-Objekt nach dem vorgegebenen Schema.

Output contract swot.v1:

{
  "schema": "swot.v1",
  "staerken": [
    {"text": "Technologieführerschaft bei Sondermaschinen für die Verpackungsindustrie",
     "hebel": ["Produkte & Dienstleistungen"],
     "quellen": {"frage_ids": ["5-2"], "belege": [{"document_id": "doc-7f3", "ordinal": 3}], "step_refs": []}}
  ],
  "schwaechen": [], "chancen": [], "risiken": []
}

The four arrays map 1:1 to ExpertReport.swot.{strengths,weaknesses,opportunities,risks} (strings in the prototype; the structured items are flattened to text for the UI, provenance kept for the review gate). SwotCategory for recommendations later uses the singular vocabulary Strength|Weakness|Opportunity|Risk (lib/types.ts:31).

Groundedness: response_text = all SWOT texts; chunks = step 1 digest sections + step 2 findings blocks (chunk_id: "step1:<lever>" / "step2:<document_id>").

Token budget: input ≈ 8–15k tokens, max_tokens: 3000, single call.

5.4 Step 4 — Scoring (ag-08)

Hybrid: LLM proposes, code disposes.

Inputs: approved outputs 1–3; the active caps computed by the rules engine (§6) from raw Knoll-DB answers; knoll_ai_settings.caps_enabled.

System prompt (draft):

Du bist der Scoring-Agent der Knoll-Analyzer-Pipeline (Schritt 4 von 6).

Aufgabe: Bewerte die 11 Erfolgshebel auf der Skala 1,0 bis 5,0 (eine
Nachkommastelle; 5,0 = beste Bewertung) auf Basis von Interview-Digest,
Dokument-Befunden und SWOT.

Deterministisches Regelwerk (bereits geprüfte, verbindliche Obergrenzen für
diese Analyse): {{aktive_deckelungen_liste}}
Beispiel: "Marktposition maximal 1,5 (kein Businessplan, Frage 1-8)".
Dein Score-Vorschlag für einen gedeckelten Hebel darf die Obergrenze nicht
überschreiten; die Begründung nennt die Deckelung und den fachlichen Befund
dahinter.

Regeln:
- Je Hebel: Score-Vorschlag + Begründung (2-4 Sätze, sachlich, im Stil eines
  Gutachtens) + Quellen (frage_ids/belege).
- Konsistenz zur Methodik: gleiche Faktenlage → gleicher Score. Nutze die
  volle Skala; 3,0 ist keine Verlegenheitsnote.
- Bewerte nur die Hebel, erfinde keine Fakten. Fehlende Datenbasis drückt
  den Score nicht automatisch — sie erscheint als Vorbehalt in der Begründung.
- Antworte NUR mit einem einzigen JSON-Objekt nach dem vorgegebenen Schema.

Output contract scoring_proposal.v1:

{
  "schema": "scoring_proposal.v1",
  "hebel_scores": [
    {"hebel": "Marktposition", "score": 1.5,
     "begruendung": "Es liegt kein Businessplan vor; nach dem Regelwerk der Methodik ist der Hebel damit deterministisch auf 1,5 gedeckelt. …",
     "quellen": {"frage_ids": ["1-8"], "belege": []}}
  ]
}

Exactly 11 entries, canonical order, score ∈ [1.0, 5.0] in 0.1 steps.

Post-processing in code (deterministic, §6): clamp each score to its cap (score = min(llm_score, cap)) when caps_enabled; derive traffic_light per score; compute overall_score; produce the final LeverScore[] ({lever, score, traffic_light, comment}comment = LLM begruendung, prefixed with a standard cap-sentence when code clamped a value the LLM had set higher). The review gate shows LLM proposal vs. capped value side by side whenever clamping changed a number.

Groundedness: response_text = concatenated Begründungen; chunks = step 1–3 outputs.

Token budget: input ≈ 10–15k tokens, max_tokens: 4000, single call.

5.5 Step 5 — Gutachten (ag-09)

Inputs: approved outputs 1–4 (final capped scores!); knoll_ai_settings.detail_level; retrieval from kb-methodology (Methodenhandbuch + historical expert reports in the general workspace, plan 3.3) for Knoll style and method conventions.

Context assembly: per lever, 2–4 search/rerank queries against kb-methodology (e.g. "Empfehlungsformulierung Hebel Vertrieb", "Gliederung Hebel-Abschnitt Gutachten") with top_k: 6, expanded via segments/text:batch (workspace general) — same recipe as §5.2. Plus the lever's facts from steps 1–3 and its final score from step 4.

Execution: one LLM call per lever (11 calls) — keeps context focused and lets a single lever be re-generated after review edits without re-running the rest (retry granularity: the step is approved only when all 11 sections are approved; the gate allows per-section regeneration).

System prompt (draft):

Du bist der Gutachten-Agent der Knoll-Analyzer-Pipeline (Schritt 5 von 6).

Aufgabe: Schreibe für den Erfolgshebel {{lever}} einen strukturierten
Gutachten-Abschnitt im Knoll-Stil: (1) Ist-Zustand, (2) konkrete Probleme,
(3) umsetzbare Empfehlungen mit Zeithorizont.

Stil (gemäß den übergebenen Methodik-Auszügen): sachlich, präzise,
wertschätzend-kritisch, direkte Ansprache vermeiden, deutsche
Fachterminologie der Knoll-Methodik. Der finale Score dieses Hebels ist
{{score}} ({{traffic_light}}) — der Text muss zum Score passen und darf ihn weder
relativieren noch verschärfen.

Detailgrad: {{detail_level}}.
- Kompakt: Ist-Zustand 3-5 Sätze, 1-2 Empfehlungen.
- Standard: Ist-Zustand 5-8 Sätze, 2-3 Empfehlungen.
- Ausführlich: Ist-Zustand 8-14 Sätze inkl. Benchmarks aus den
  Methodik-Auszügen, 3-4 Empfehlungen.

Regeln:
- Nur belegte Aussagen (frage_ids/belege aus den übergebenen Daten).
- Jede Empfehlung: Titel, Beschreibung (2-3 Sätze), SWOT-Bezug
  (Stärke/Schwäche/Chance/Risiko), Priorität 1-3 (1 = höchste),
  Zeithorizont ("0–3 Monate", "3–6 Monate" oder "6–12 Monate").
- Ist der Hebel gedeckelt, benennt der Text die Deckelung und welche
  Maßnahme sie aufhebt (z. B. Businessplan-Erstellung).
- Antworte NUR mit einem einzigen JSON-Objekt nach dem vorgegebenen Schema.

Output contract report_section.v1 (one per lever; step output = array of 11):

{
  "schema": "report_section.v1",
  "hebel": "Vertrieb",
  "ist_zustand": "…",
  "probleme": ["Keine schriftliche Vertriebsstrategie", "…"],
  "abschnitt_text": "…zusammenhängender Fließtext des Abschnitts…",
  "empfehlungen": [
    {"titel": "Schriftliche Vertriebsstrategie mit Neukundenfokus erarbeiten",
     "beschreibung": "…",
     "swot": "Schwäche",
     "prioritaet": 1,
     "zeithorizont": "0–3 Monate"}
  ],
  "quellen": {"frage_ids": ["2-8"], "belege": [{"document_id": "doc-7f3", "ordinal": 21}]}
}

Empfehlungen across all sections are collected, deduplicated and numbered by code into Recommendation[] ({id, lever, title, description, swot, priority, time_horizon}lib/types.ts:178-187); target total 4–8 (mock expert reports have 6).

Groundedness: per section via /score/batch; chunks = that lever's fact blocks + methodology excerpts. Band recorded per section and shown per section in the gate and later in the expert-report UI (plan 6.4 "band per section").

Token budget: per call input ≈ 6–10k, max_tokens: 1500 (Compact) / 2500 (Standard) / 4000 (Detailed). 11 calls — this is the most expensive step (mock durationSeconds: 217).

5.6 Step 6 — Report (ag-10)

Inputs: approved outputs 1–5; deterministic aggregates from code (overall_score, traffic_light, radar data = the 11 scores).

LLM part — one call producing report.v1:

Du bist der Report-Agent der Knoll-Analyzer-Pipeline (Schritt 6 von 6).

Aufgabe: Verfasse (1) eine Executive Summary (Detailgrad {{detail_level}}:
Kompakt ≈ 150 Wörter, Standard ≈ 300, Ausführlich ≈ 500) und (2) exakt drei
Kernbotschaften für das Kundengespräch.

Regeln:
- Die Kernbotschaften sind je ein prägnanter Satz mit klarer Handlungsrichtung,
  priorisiert nach Ergebniswirkung (vgl. Beispielstil: "… lebt von exzellenten
  Produkten – aber ohne Businessplan und ohne schriftliche Strategie fährt das
  Unternehmen auf Sicht.").
- Gesamtscore {{overall_score}} ({{traffic_light}}) und die Hebel-Scores sind fix und
  werden wörtlich übernommen, nicht neu bewertet.
- Nur belegte Aussagen; keine neuen Fakten.
- Antworte NUR mit einem einzigen JSON-Objekt nach dem vorgegebenen Schema.
{
  "schema": "report.v1",
  "executive_summary": "…",
  "kernbotschaften": ["…", "…", "…"]
}

Code part (assembly): see §9. Groundedness: summary + key messages vs. step 1–5 outputs. Token budget: input ≈ 12k, max_tokens: 4000.


6. Deterministic rules engine

Lives in the Knoll backend (versioned module, unit-tested; plan 4.3/5.3). Operates on raw Knoll-DB answers, never on LLM output.

6.1 Caps table (initial rule set, v1)

RuleCondition (deterministic, from questionnaire_answers)EffectSource
R-01Frage 1-8 ("Businessplan vorhanden?") = Nein or unansweredMarketPosition ≤ 1,5Anlage C.I.1 via prototype fragebogen.ts:82 ("Ohne Businessplan wird der Erfolgshebel Marktposition methodisch auf max. 1,5 gedeckelt."); mock expert report g-02; Settings AI tab
R-02Frage 2-5 ("schriftliche Unternehmensstrategie?") = Nein or unansweredStrategy ≤ 2,5Anlage C.I.1 via fragebogen.ts:132 ("Ohne schriftliche Strategie wird der Erfolgshebel Strategie auf max. 2,5 gedeckelt."); ag-08 description

TODO-VERIFY: the ag-08 description says caps like these apply "z. B." (for example) — extract the complete Deckelungsregel catalog from the Methodenhandbuch/Anlagen in Knoll-Analyzer-Docs/ (docx/xlsx; likely Anlage D / Bewertungsmatrix) before pilot; only R-01/R-02 are verified in the prototype sources. New rules append to this table with a rule id and source citation.

Unanswered counts as Nein for cap purposes (conservative; the review gate flags it and step 1 generates a Rückfrage).

6.2 Application semantics

  1. Compute active caps from raw answers at run start; freeze into the run snapshot.
  2. If knoll_ai_settings.caps_enabled = true (default, per Settings mock): pass the active caps into the step 4 prompt ("Regelbasierte Obergrenzen werden vor der KI-Bewertung angewendet" — Settings AI tab) and clamp in code afterwards: final = min(llm_score, cap). Defense in depth — the prompt makes the Begründung coherent, the clamp guarantees the number.
  3. If caps_enabled = false: caps are still computed and displayed in the review gate ("Deckelung würde greifen: MarketPosition ≤ 1,5") but not enforced; the audit entry records that caps were disabled for this run.
  4. Every clamp writes an audit_log entry {rule_id, lever, llm_score, capped_score}.

6.3 Traffic-light derivation (code, never LLM)

Verified against expert-reports.ts:5 and all 22 mock lever scores:

traffic_light(score) = Red    if score < 2.5
                     = Yellow if 2.5 ≤ score ≤ 3.5
                     = Green  if score > 3.5

6.4 Overall score (code)

overall_score = round(mean(score_1..score_11), 1) — unweighted arithmetic mean of the 11 final (capped) lever scores, one decimal; overall traffic_light from the same thresholds. Verified against both mock expert reports (g-01: mean 3.2 ✓, g-02: mean 2.69 → 2.7 ✓). TODO-VERIFY: confirm against the Methodenhandbuch that the overall score is unweighted (the Bewertungsmatrix D.III.2 weighting 35/35/25 applies to service-provider ratings, not levers — but a lever weighting may exist in the source docs).

6.5 Settings consumption (knoll_ai_settings, plan 2.1/6.6)

SettingValuesConsumed by
detail_levelCompact | Standard | Detailed (Settings AI tab)Step 5 section lengths + max_tokens; step 6 summary length
caps_enabledbool, default true§6.2
model pickerLiteLLM id from GET /ai-gateway/api/v1/models§3.2 ($KNOLL_PIPELINE_MODEL)
HITL approvalalways on (QMS requirement, not configurable)§2.2 review gates

Settings are read once at run start into the snapshot (mid-run changes affect the next run).


7. Failure handling

7.1 Error taxonomy → step behavior

(Full taxonomy in 11-resilience-and-errors.md; grag-client raises typed errors, plan 2.5.)

FailureDetectionBehaviorai_runs.errorUser-visible
Provider error / timeout on completiongateway 5xx / network timeoutgateway-side fallback chain first; if the request itself failed: retry once on connection-level errors only (a completed-but-slow call must not be re-sent — cost)llm_errorFehlgeschlagen + "Erneut ausführen"
Invalid JSON after repair retriesZodfail step (§3.3)schema_validationFehlgeschlagen; raw output attached for dev
402 budget exceeded (keyvault)HTTP 402fail step immediately, no retry; notify Partner + operator (plan 2.7, 7.3 80%-alert should have fired earlier)budget_exceededBanner "KI-Budget erschöpft — Analyse pausiert"
429 rate limitedHTTP 429 (+ Retry-After)backoff per Retry-After, max 3 attempts, then failrate_limitedLäuft (verzögert) → Fehlgeschlagen
503 dark flag / cold start / admission gateHTTP 503backoff 30 s → 60 s → 120 s (covers rerank/groundedness model loads of 60–120 s and ~30 s deploy churn), then failservice_unavailableLäuft (verzögert)
Retrieval degraded (data.rerank.degraded: true or confidence unknown)response inspectionproceed with plain hits; stamp warning into step output; review gate shows "Reranking ausgefallen"Warning badge
Retrieval empty for a document0 hits over all queriesLücke in findings, never invented contentListed under Lücken
Groundedness unavailable503 / errordo not fail the step; groundedness_band = unknown + note"nicht bewertet"
segments/text:batch misses keysfound-only responsefall back to payload.text previews
Step timeoutwall clock (step budget: 1/3/4 = 10 min; 2 = 30 min; 5 = 45 min; 6 = 15 min)janitor sweeps runningfailed (§2.3)timeoutFehlgeschlagen
Knoll backend crash mid-stepjanitoras timeouttimeoutFehlgeschlagen

7.2 Idempotency & money safety

  • Completions are not idempotent — every retry is new spend. The attempt counter and per-run spend (Knoll-side sum of ledger totals?pipeline_id= over the run's ai_runs.pipeline_id values, §3.4) are surfaced in the pipeline panel; a run exceeding a configurable soft cost limit (default from 14-cost-model.md) pauses before the next step with a Partner confirmation.
  • Retrieval, segments/text:batch, groundedness and ledger reads are safe to retry freely.
  • All GRAG calls of a step attempt carry that attempt's X-Pipeline-Id (assigned before the first call), so orphaned spend from crashed steps is still attributed — the id exists in ai_runs even if the attempt never finished.

8. Partner review gates (plan 5.3 / 2.3 / 6.4)

8.1 What is presented per step

StepGate content
1Digest per lever (facts with question ids, gaps); contradictions (sorted by severity); follow-up questions list with "send follow-up questions to client" action; regelwerk_fakten vs. deterministic recomputation (mismatch flagged)
2Findings per document (metrics, facts with click-through to evidence full text via stored (document_id, ordinal)), questionnaire cross-check (contradicting items highlighted), gaps, retrieval confidence
3SWOT quadrants with provenance chips
4Score table: LLM proposal → cap → final score → traffic_light; active caps listed; radar preview; overall_score
511 sections with per-section groundedness band, recommendations list; per-section "Neu generieren"
6Executive Summary, 3 key messages, full assembled preview, PDF preview

Every gate additionally shows: model used, tokens, cost so far (ledger), groundedness band (+ red-band acknowledgement checkbox, §4), attempt number.

8.2 Actions

ActionWhoEffect
Freigeben (approve)Partner only (server-side role check, plan 2.3/7.7)step → approved; next step queued; audit entry
Bearbeiten (edit)Partner (Analyst may draft edits; Freigabe stays Partner)inline edit of the output (re-validated against the step's Zod schema); edited=true on the run row; edit diff hash in audit; then Freigeben
Ablehnen (reject)Partnercomment mandatory; step → rejected; new attempt queued with the comment injected as additional instruction: "Hinweis des Prüfers zur vorherigen Fassung: …"
Rückfragen senden (step 1 only)Partner or Analystemails via plan 2.7; logged
Lauf abbrechenPartnerrun cancelled (active step attempt → cancelled, §2.2); audit entry

8.3 Audit entries (audit_log, plan 2.1)

One row per gate action: {actor_user_id, role, action ∈ step_approved|step_edited|step_rejected|run_started|run_cancelled|red_band_acknowledged|cap_applied|caps_disabled_run, analysis_id, pipeline_id, step, attempt, payload (comment/diff-hash/rule_id), created_at}. Final approval of the expert report (step 6 approve) additionally writes the expert_reports version row (§9) and is the QMS-relevant "durch einen Partner freigegeben" record.


9. Final assembly, PDF, ingest (step 6 code part)

After step 6 is approved:

  1. Assemble the expert_reports row (versioned, Knoll DB = source of truth): {analysis_id, status: "Provisional", created_at, overall_score, traffic_light, key_messages[3], lever_scores[11] (score, traffic_light, comment), swot{…}, recommendations[], pipeline: derived from ai_runs} — matching lib/types.ts:195-213. status flips Provisional → Final only via the separate expert-report approval on the expert-report route (plan 6.4), which also triggers the deterministic KG projection (plan 3.6: SCORES {score, traffic_light, analysis_id}, ADDRESSES, RECOMMENDS edges — see 07-kg-schema-knoll-advisory.md; requires platform gap 1.11).
  2. Render PDF in the Knoll backend (renderer is a Knoll implementation choice — e.g. headless-Chromium print of the report route; GRAG plays no part) and store it in the Knoll file store (plan 2.4) with a files row {expert_report_id, sha256, mime: application/pdf}. GRAG keeps no binaries — the file store copy is what the PDF-Export button (plan 6.4) and GDPR Auskunft (7.5) serve.
  3. Ingest into the analysis-KB so future KB-Chat ("Frag die Akte", 09-chat-integration.md) can retrieve the expert report (⚠ blocked for real data by plan 1.8 — the BFF is unauthenticated today; the Authorization header below is the post-1.8 contract):
# 3a. upload (multipart) → docfold conversion job
curl -sS -X POST "$GRAG_URL/next/api/upload/start" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT" \
  -F "kb_id=kb-analyse-7f3k" \
  -F "files=@gutachten-a02-v1.pdf;type=application/pdf"
# → {"jobs":[{"filename":"gutachten-a02-v1.pdf","docfoldJobId":"…","documentId":"…"}]}

# 3b. index
curl -sS -X POST "$GRAG_URL/next/api/ingest/start" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT" \
  -H "Content-Type: application/json" \
  -d '{"kb_id": "kb-analyse-7f3k", "document_id": "<documentId>",
       "docfold_job_id": "<docfoldJobId>", "filename": "gutachten-a02-v1.pdf"}'
# → {"jobId":"…"}  — progress via SSE GET /next/api/orchestrator/events/{jobId}

Record documentId in grag_refs linked to the expert_reports version. A newer expert-report version supersedes the old document (POST /workspaces/api/v1/documents/{id}/supersede, plan 4.4) so retrieval never mixes versions. 4. Notify "expert report ready for approval" (plan 2.7) and update ai_agents run stats (last_run, total_runs derive from ai_runs, plan 6.1).


10. Preconditions checklist (before the first real run)

PreconditionPlan ref
BFF auth deployed (else step 6 ingest + all chat lanes are publicly abusable)1.8 (blocker)
Analysis-KB provisioned + documents indexed; kb-methodology ingested3.1, 3.3, Phase 4
TENANT_SETTINGS_ENABLED, preloads (RERANK_FUSION_PRELOAD, GROUNDEDNESS_PRELOAD), KG_INGEST_ENABLED live0.9, 1.4
KG_RELATION_EXTRACTION_ENABLED=false or knoll-advisory v1 without relationships (else auto-edges pollute the graph)0.7, 1.4, 3.4
Keyvault keys + budget + 80% alert1.6, 7.3
Partner role enforcement server-side2.3, 7.7
German PII posture decided (firewall is OFF and EN-only — questionnaire answers and document text go to the LLM provider un-redacted)1.10
Groundedness calibration status known (bands advisory until 7.1)7.1

Open items (TODO-VERIFY)

  • TODO-VERIFY (§6.1): complete cap-rule catalog from Knoll-Analyzer-Docs/ Methodenhandbuch/Anlagen — only R-01 (no business plan → MarketPosition ≤ 1,5) and R-02 (no written strategy → Strategy ≤ 2,5) are verified from the prototype sources; ag-08's "z. B." wording implies more rules exist.
  • TODO-VERIFY (§6.4): overall-score aggregation — unweighted mean matches both mock expert reports, but confirm no lever weighting exists in the Methodenhandbuch.
  • TODO-VERIFY (§4): live capability check GET $GRAG_URL/groundedness/api/v1/capabilities on app.grag.ai — per_sentence_supported must be true on the deployed backend (schema docs note per-sentence is "local backend only in v1"; if the deployment uses a gateway backend, per-sentence returns null + per_sentence_unavailable note and the review UI must degrade gracefully).
  • TODO-VERIFY (§5.2): whether the analysis-KB collections are created as engine: dense by the BFF ingest lane in the live deployment (free-text query_text search requires a dense collection; shard/ColBERT collections would need caller-side query embeddings).