# QAF-V2 — Session d320f3d7 (06.08.2026 ab 22:36), Fortsetzung nach Fable-5-Wechsel

Vorgänger-Übergabe: `ACTIVE_TASK_ea2719a0.md` + `/home/aria/.claude/qaf-v2-handover-2026-08-06/`
(Vorbefund-Kopie, `offene-entscheidungen-kais.md`, Bundle — Bundle nicht mehr nötig, Push erfolgt).

## Erledigt diese Session

1. **#452-Branch gepusht** (Kais-Go 22:48): `qaf-v2/loop3-vorbefund-katalog-unverdrahtet`
   remote auf `96fbf8b`, per `ls-remote` verifiziert. CI lief dabei NICHT (Actions-Outage) —
   nach Recovery Läufe neu starten.
2. **Loop 3 Schritt 1 gebaut** auf Stack-Branch `qaf-v2/loop3-schritt1-katalog-sprachfaehig`
   (Basis = #452-Branch, NICHT main):
   - `all-differences.ts`: `formulaFindingKind` (Extract aus `FormulaComparisonKind`) +
     `deltaNullReason` (Extract aus `DiffStatus`) als orthogonale Pflichtfelder (nullable);
     `validateTraceability` meldet neu `delta_reason_contradiction`
   - Produzenten (`differences-from-tooling`, `-findings`, `reference-case`): `null` mit Kommentar
   - Tests: `all-differences.test.ts` +5 Fälle; Fixtures in `ai-ready`/`negotiation-levers`/
     `view-specs`/`table-specs`-Tests ergänzt
   - CHANGELOG-Eintrag (oben) + `governance/scorecard/bypass-log.md`: Barrel-Bestandsaufnahme
     (20 Deep-Imports vs. ADR-019 R4, Sunset benannt)
   - Verifiziert: typecheck grün · Modul-Tests 191 Dateien/3365 grün · Mutations-Probe
     (Prüfung aus → genau 1 Test rot)
3. Daily Log 06.08. nachgetragen (war leer bis auf Session-Start).

## Läuft gerade

- **Actions-Wache** (Task btjtsmcba): 5-Min-Poll githubstatus, weckt bei Wechsel ODER nach
  60 Min (Herzschlag). Actions seit ~15:35 `major_outage`.
- **Gates-Kette** (Background): `npm run test` → `npm run build` →
  `CHECK_FORBIDDEN_LEVEL=error npm run check:portability`.

## Nächste Schritte

1. ✅ ERLEDIGT 23:25: Gates grün (Output gelesen: 473|12 skipped, 7072 Tests, 0 rot; Build ✓;
   Portability strict ✓) → Stack-Branch gepusht (`e371a01` remote verifiziert) →
   **PR #453 erstellt** mit Base `qaf-v2/loop3-vorbefund-katalog-unverdrahtet`.
2. ✅ RECOVERY 02:09 (07.08.): Actions `operational` (4-fach verifiziert). Erledigt:
   #452-Zombies (portability-matrix, semgrep — 15-Min-Timeout-Signatur) per
   `gh run rerun --failed` neu; #453–#455 hatten NULL Runs → leere CI-Trigger-Commits
   (`c753651`/`d6ff3a3`/`57dcfe7`) gepusht + remote verifiziert. CI-Wache (Task bjzyoxcqg)
   läuft über alle 4 PRs.
2b. ✅ 02:25 (07.08.): **#452 GEMERGT** (`a6edeef` auf origin/main, verifiziert; squash, Branch
   steht noch). #453-Base auf main umgehängt → volle CI-Suite läuft (Workflows filtern
   `branches: [main]` — Stack-PRs bekommen CI erst nach Umhängen; Erkenntnis dokumentiert).
   ⚠ #453-Files-Tab zeigt #452-Änderungen kosmetisch doppelt (merge-base-Artefakt bei
   Squash-Stacks) — Merge-Ergebnis korrekt; Rebase/-force nur mit Kais-Go.
   Deploy+CI-Wache läuft (Task b3sf0v2kn): main-Deploy a6edeef (war pending) + #453-Checks.
2c. ✅ main-Deploy a6edeef = success (02:30, Policy-Beobachtung erfüllt, Doku-PR ohne
   Funktions-Smoke). ⚠ NEUER BEFUND: #453 bekommt trotz Base=main und frischem
   Sync-Commit (`871313a`, remote verifiziert) NULL Workflow-Runs — Actions-Event-Backlog
   nach dem Outage vermutet (Reruns liefen, weil sie existierende Runs anfassen; NEUE
   Event-Trigger hängen). Kein weiteres Trigger-Stapeln. Wache v2 (Task-Skript
   ci-453-watch.sh) exitet erst, wenn `build-check` im Check-Set EXISTIERT und nichts
   pending — „hat Checks ≠ hat alle Checks" ist die Lehre aus Wache v1.
2d. ✅ URSACHE GEFUNDEN ~03:15: #453 war CONFLICTING/DIRTY gegen main (CHANGELOG-Kopf
   nach #452-Squash) — **GitHub startet KEINE pull_request-Runs ohne baubaren
   Test-Merge-Ref**. Kein Backlog (main-Push-Runs + #454-PR-Run vor dem Merge liefen
   sofort). Fix: `git merge origin/main` im Branch, Konflikt gelöst (Schritt-1-Eintrag
   über Vorbefund), Merge-Commit `430f4cd` gepusht → MERGEABLE/UNSTABLE. Wache
   (bh3wjp8rv, build-check-Bedingung) läuft weiter.
2e. ✅ #453 GEMERGT ~03:20 (`5f6b3c7`; Suite nach Konflikt-Fix sofort grün, Ist-Check
   CLEAN separat). ✅ #454 in der Schleife: Base→main, main eingemergt (`bb1bb48`),
   MERGEABLE, 10 Runs laufen; Wache b9jfekdyb (ci-pr-watch.sh 454).
   Hinweis: DIRTY direkt nach Push = Mergeability-Cache, einmal neu abfragen.
2f. ✅✅ MEILENSTEIN ~03:45 (07.08.): **ALLE 4 PRs GEMERGT** — main:
   `ac5199c` (#455) ← `6c0bc07` (#454) ← `5f6b3c7` (#453) ← `a6edeef` (#452).
   Jeder Merge: volle grüne Suite + separater Ist-Check. Deploy-Wache auf ac5199c
   läuft (Task bdrfgcqlc). Branches #452–#455 STEHEN GELASSEN (Kais-Go fürs Löschen
   abwarten). UI-Sichtbarkeit der neuen V2-Elemente NICHT behauptet — Kais-Blick
   morgen (flag-rollout-verify-prod-profile-Lehre).
2g. WENN Deploy-Wache weckt: success → dokumentieren, fertig mit der Merge-Kette;
   failure → Rollback-Pfad der Policy, Kais SOFORT. Danach nächster Bau-Block:
   Schritt 4 laut Bauplan 3c (ZUERST Verbraucher-Frage klären!). Lokalen main
   aktualisieren (git checkout main && git pull) vor neuem Branch.
3. ✅ FERTIG ~00:20 (07.08.): Schritt 2 komplett — Gates grün (474|12 / 7086 / 0 rot, Build ✓,
   Portability strict ✓), **PR #454** erstellt (Base = #453-Branch), remote `7be46d5` verifiziert.
   Stack: #452 ← #453 ← #454. Referenzseiten-Abschaltbedingung bewusst offen, an Kais
   übergeben (steht im PR-Text).
3b. ✅ FERTIG ~01:25 (07.08.): Schritt 3 komplett — Gates grün (474|12 / 7088 / 0 rot),
   **PR #455** erstellt (Base = #454-Branch), remote `42c66d6` verifiziert.
   Stack: #452 ← #453 ← #454 ← #455.
3c. SCHRITT 4 — BAUPLAN (Fakten gesichert ~02:00, differ.ts KOMPLETT gelesen):

   **Erhobene Fakten (nicht neu lesen):**
   - `diffSteps(alt: QAFRow|null, neu, bands?, formulaEngineEnabled?)` in differ.ts →
     `FieldDiff[]` über NUMERIC_FIELDS (aus lib/qaf/comparison). FieldDiff trägt status
     (DiffStatus), formulaFinding (orthogonal, 3 Kinds), formulaInputsChanged.
   - **Grund-Trio-Semantik (Quelle für deltaNullReason!):** `nicht_berechenbar` = beide
     Seiten leer (computeNumericDelta:90) · `nicht_anwendbar` = eine Seite explizit „n.a."
     (computeFieldDiffWithNotApplicable:145, via rawText + isNotApplicableValue; hat Vorrang
     vor Formel-Overrides) · `blockiert` = R2-Pflichtfeld-Blockade KAR-889 — Entstehungsort
     NICHT in differ.ts, vor Gebrauch in compare.ts/rule-engine verifizieren.
   - `QAFRow` hat Provenienz: `sourceCells`, `rawText`, `formulas` je Feld → MAT/MFG-Sätze
     können ECHTE `DifferenceCell`s tragen (anders als SUM/Weg 4!). valueState-Ableitung
     dabei streng: formulas[key] vorhanden → formula_and_cached; Wert da ohne Formel-Grid →
     NICHT constant behaupten (Grid optional!) — Ableitungsregel sauber definieren, die 5
     Spezifikations-Zustände sind testgenagelt (cell-state.test).
   - material-mapping.ts (25K) + manufacturing-mapping.ts (8.7K) NOCH NICHT gelesen —
     Struktur der Zeilen-Zuordnung unbekannt. MATERIAL_FIELD_KEY_TO_CANONICAL: 29 Einträge,
     QAF_FIELD_KEY_TO_CANONICAL (Fertigung): 22 (canonical-fields.ts).

   **✅ VERBRAUCHER-FRAGE GEKLÄRT (~04:00, Reads auf aktuellem main ac5199c):**
   V2-Seite (`app/qaf-differences/[id]/v2/page.tsx`, 148 Z.) rendert NUR Header + Kacheln +
   Waterfall — keine MAT/MFG-Sektionen. **`table-specs.ts` IST der gebaute Verbraucher**
   (Kopf: „Material, Fertigung und Werkzeuge zeigen dieselbe Tabelle", U-08; `rowsFromRecords
   (records, namen)` = Katalog-Brücke, `buildTable` mit Filter-Transparenz). Schritt-4-Schnitt:
   Produzenten + Katalog-Merge + **eine Tabellen-Sektion auf der V2-Seite via table-specs** —
   schließt die zwei letzten toten Module (table-specs 0 Importe, deep-links bleibt separat) an.
   **NOCH ZU KLÄREN im Bau-Zug:** Rehydrierung — die V2-Seite liest qaf_comparison/qaf_file/
   qaf_summary_metric; MAT/MFG-Diffs liegen persistiert in qaf_manufacturing_diff (+ Material-
   Pendant? Spalten prüfen: `status TEXT` existiert lt. types.ts-Kommentar, formula/value_state
   fehlen dort genauso → MAT/MFG-Sätze auf DB-Pfad ebenfalls Weg 4/cells=[], echte Zellen nur
   auf Mappen-Pfad via QAFRow.sourceCells). Analog `summaryMetricsFromRows` (rehydrate.ts)
   bauen oder erweitern.
   - Reihenfolge: differences-from-manufacturing.ts ZUERST (diffSteps-Basis klar, Trio real),
     dann differences-from-material.ts (nach material-mapping-Lektüre).

   **✅ REHYDRIERUNGS-KLÄRUNG (~04:20, aus Migration qaf-differences.sql):**
   - `qaf_manufacturing_diff` (Migration :169): field, alt_value, neu_value, delta_absolute,
     delta_percent, delta_percentage_points, **status** (trägt Grund-Trio + die 2
     überschreibenden Formel-Status — persistiert lt. types.ts), source_alt, source_neu,
     step_match_id (FK qaf_step_match → Stations-Zuordnung/anchorRow). KEINE formula/
     value_state-Spalten → DB-Pfad = Weg 4 (cells=[]); formulaFindingKind auf DB-Pfad:
     aus status ableitbar für formel_geaendert→wert_gleich und formel_zu_konstante;
     der dritte Fall (wert_geaendert) ist in-memory-only und fehlt dort — dokumentieren.
   - **KEIN qaf_material_diff!** Tabellen: batch/file/part/summary_metric/manufacturing_step/
     comparison/step_match/summary_diff/manufacturing_diff/structure_change/plausibility_issue/
     root_cause/export/audit_log. Material-Diffs sind NICHT persistiert → MAT auf der
     DB-gespeisten V2-Seite braucht entweder neue Migration (Kais-Gate Stufe 4! — könnte mit
     der Kriterium-2-Migration formula/value_state EIN Paket werden) oder bleibt
     Mappen-Pfad-only. → In Kais' Entscheidungsliste aufnehmen.
   - **✅ INPUT-FORMAT ENTSCHIEDEN (~04:45, am Code):** Option B — DB-Zeilen-Subset direkt,
     nach dem Muster von `summary-view.ts` (`SummaryDiffRowData` = „Subset of qaf_summary_diff
     columns", pure View-Models über persistierte Zeilen; `[id]/page.tsx:556` lädt
     qaf_manufacturing_diff schon heute). MFG-Produzent bekommt `ManufacturingDiffRowData`
     { field, alt_value, neu_value, delta_absolute, status, source_alt, source_neu } (+
     Stations-Kontext aus qaf_step_match für anchorRow/Beschreibung). Mappen-Pfad kann später
     einen zweiten Eingang (FieldDiff[]) bekommen — analog zu buildOverviewRuns zwei Eingängen.

   **⚠ ERSTE ENTWURFSFRAGE des MFG-Produzenten (bewusst NICHT um 04:45 am Kontext-Limit
   entschieden — braucht Spec-Blick B13/§13 + material-mapping-fixture-Buckets):**
   Was erzeugt eine Zeile mit Grund-Trio-Status (`nicht_berechenbar`/`nicht_anwendbar`/
   `blockiert`)? Kandidaten:
   (a) KEIN Differenzsatz, aber gezählte benannte Meldung (analog skippedDe des
       SUM-Produzenten) — „kein feststellbarer Unterschied, Grund X";
   (b) Satz MIT deltaNullReason und einem ehrlichen kind — Kandidat: `nicht_anwendbar` mit
       Wert→n.a.-Übergang IST eine Veränderung (kind 'changed'? 'cleared' wäre falsch, n.a.
       ≠ leer); `nicht_berechenbar` (beidseitig leer) ist dagegen KEINE Differenz → (a).
       `blockiert` (R2-Pflichtfeld): altValue/neuValue bleiben sichtbar (KAR-891-Muster) —
       Differenz-Charakter hängt an der Wertlage.
   ✅ ENTSCHIEDEN ~05:00 aus Primärquellen (differ.ts:130-135, types.ts:71-76): KEINE Sätze
   fürs Trio; n.a./blockiert benannt in `withheldDe`, nicht_berechenbar/konstant satzlos ohne
   Meldung. deltaNullReason bleibt ungesetzt (ehrlich dokumentiert im Produzenten-Kopf).

   **✅ SCHRITT 4a GEBAUT (~05:05):** `differences-from-manufacturing.ts` + 9 Tests grün,
   Commit `e1986c4` auf Branch `qaf-v2/loop3-schritt4-mfg-produzent` (ab main!), remote
   verifiziert. Exporte statt Kopien: KIND_BY_STATUS/VERB_BY_STATUS (differences-from-summary),
   STEP_FIELD_LABELS (rule-engine). KEIN PR bis 4b steht.

   **SCHRITT 4b — TABELLEN-SEKTION (nächster Zug, Fakten komplett):**
   - `rowsFromRecords(records, names)`: names = Map<differenceId, {award: string[], current:
     string[], matchType, bucket?}> — leere Map fällt auf r.kind zurück, Namensspalten leer.
     TableRow hat KEIN descriptionDe → **Stations-/Feldnamen MÜSSEN über die names-Map rein.**
     Dafür Produzent erweitern (analog SUM-Muster): Rückgabe `nameByTuple:
     Map<sortKeyTuple, {stationDe, feldLabelDe}>` + Helper `tableNamesByDifferenceId(records,
     nameByTuple)` → Map für rowsFromRecords (award=[stationDe], current=[feldLabelDe]? NEIN —
     semantisch: names sind Zeilen-NAMEN je Stand; sinnvoll: award/current = [stationDe ·
     feldLabel] beidseitig gleich, matchType default r.kind). Kurz am TableRow-Rendering
     entscheiden.
   - Query-Vorlage (aus `[id]/page.tsx:551-558`): qaf_step_match (id, alt_step_id,
     neu_step_id, match_status, …) + qaf_manufacturing_diff (step_match_id, field, alt_value,
     neu_value, delta_absolute, …, status) je comparison_id; source_alt/source_neu dort NICHT
     im Select — ERGÄNZEN (Spalten existieren lt. Migration). Stationsnamen: qaf_manufacturing_step
     (prozessbezeichnung/positionsnummer — Select prüfen) über alt/neu_step_id.
   - Neue Client-Komponente `components/qaf-differences/qaf-v2-differences-table.tsx`
     (BMW-CI, U-08: buildTable-Filter-Transparenz „N Zeilen ausgeblendet, Summe X fehlt"
     anzeigen!) + Sektion in v2/page.tsx unter der Brücke + withheldDe/skippedDe +
     validateTraceability-Meldungen in den traceability-Block der Seite integrieren
     (MFG-Katalog separat von SUM-Katalog bauen — Bereiche disjunkt, IDs kollidieren nicht;
     Gesamt-Index ist spätere Arbeit).
   - ✅ 4b GEBAUT + PR ERSTELLT (~06:15): Commit `5531353`, Gates grün (475|12 / 7097 / 0 rot,
     Build ✓, Portability ✓ 1377 Dateien), **PR #456** gegen main, remote verifiziert.
     CI-Wache läuft (ci-pr-watch.sh 456). Bei Suite-Grün: Ist-Check separat → Merge (squash) →
     Deploy-Beobachtung. Kein Stack, kein Konflikt-Thema erwartet (Base main von Geburt).
   - ✅ #456 GEMERGT (`2c4130f`) + Deploy success (~06:32). Schritt 4 komplett.

   **SCHRITT 5 (`scope_removed`) = KAIS-ENTSCHEIDUNG, kein Bau (~07:15 erhoben):**
   Die Master-Spec konstatiert das Fehlen 3× (0.4/44.3/45.3), definiert die
   ERKENNUNGS-Regel aber nirgends ("ersatzlos" kommt in der Spec nicht vor); DIF-005
   (Traceability-Matrix, P0) gibt nur die Richtung „Struktur- und Kostenstatus trennen".
   Ist-Zustand: `classify()` in material-mapping.ts:576 schickt JEDEN matchType 'removed'
   nach price_removed; der Fixture-Referenzfall erwartet 1× scope_removed mit delta 0.
   Zwei Regel-Kandidaten für Kais:
   (a) **zahlenbasiert**: removed + awardKosten≈0 → scope_removed (Spiegelbild der
       Spec-8.5-Regel für added+0; trifft die Fixture exakt; ABER: von mir abgeleitet,
       nicht spezifiziert — wäre eine stille Spec-Erweiterung);
   (b) **explizite Registry-Markierung** je Posten/Familie („Umfang ersatzlos"), wie der
       Fixture-Kommentar sie nennt — spec-treuer Mechanik-Ansatz, braucht fachliche
       Pflege-Entscheidung, WER markiert.
   Audit führt scope_removed zudem im P1-Paket MIT D15–D17 + R-03 (eine Taxonomie-Tranche).
   → An Kais: Regel a, Regel b, oder ganze Tranche als eigener Loop. NICHT nachts erfinden
   („Fachliche Primärquellen dürfen nicht still durch allgemeines Wissen ersetzt werden").
   ✅ KAIS 09:31: „Option a" → GEBAUT (Commit `91a3fe3`), Mutations-Probe bestanden,
   **PR #457 GEMERGT (`66880b9`)** ~10:10, Deploy-Wache läuft.

## PROGRAMM „QAF-VERGLEICH KOMPLETT" (Kais-Mandat 18:38, 07.08.)

Reihenfolge: 1. Kriterium 2 ✅ gebaut (Branch `qaf-v2/kriterium2-summary-provenance`,
Commit `329e66c`, remote; **MERGE-GATE: Kais-Apply der Migration ZUERST** — SQL bei ihm,
nach „applied": Live-Schema verifizieren (list_tables: 3 Spalten) → PR erstellen → Suite →
Ist-Check → Merge → Deploy) · 2. MAT komplett — **BAUPLAN (Fakten erhoben ~19:35):**
   ⚠ KERN-BEFUND: `buildMaterialMappings`/`materialEffects` (material-mapping.ts, inkl.
   frischer scope_removed-Taxonomie) haben NULL Produktions-Aufrufer (nur Barrel-Export
   :1406) — dieselbe Vorbefund-Klasse „gebaut, nie verdrahtet". compare.ts nimmt
   materialRows (MaterialRow[] je Seite, material-parser) entgegen, ruft das Mapping aber
   nie. Block 2 = ERSTVERDRAHTUNG + Persistenz + Katalog + Tabelle:
   (a) compare.ts: buildMaterialMappings(alt, neu) + classify → Vergleichs-Output
       (Feld materialMappings? — Output-Struktur von compareQafPair ansehen);
   (b) Migration qaf_material_diff (Muster qaf_manufacturing_diff; Spalten aus
       MaterialMapping: mapping_id, award_rows int[], current_rows int[], award_names
       text[], current_names text[], match_type, match_rule_id, match_details_de,
       cost_award, cost_current, delta, effect, zero_cost bool; comparison_id+project_id
       FKs; RLS wie Schwester-Tabellen per qaf-rls-project-access-model — ACHTUNG: die
       #115-Policies decken NUR die 17 Bestandstabellen; neue Tabelle braucht eigene
       read/write-Policies nach deren Muster im selben Migration-File);
   (c) actions.ts persistiert beim Vergleich (Muster der anderen Diffs; recompare:
       RECOMPARE_REPLACED_CORE_TABLES + 'qaf_material_diff'!);
   (d) differences-from-material.ts: ClassifiedMapping → RawDifference (area MAT; kind:
       matchType/effect-Mapping — scope_added→added, price_removed/scope_removed→removed?
       NEIN: effect-Semantik nutzen: scope_added/scope_activated→added?? SAUBER KLÄREN am
       Referenzfall; Zellen: Kosten-Zelle je Position aus MaterialRow.sourceCells
       (`Blatt!Zelle`-Format :572) — die Mappings tragen nur Zeilen → Zellen-Zuordnung
       über die Rows; anchorRow currentRows[0] ?? awardRows[0]; names → Tabellen-Map);
   (e) V2-Seite: zweite U-08-Sektion „Material-Differenzen" (gleiche Komponente);
   (f) Rehydrierung materialDiffsFromRows für DB-Pfad.
   Migration = zweites Kais-Apply-Paket (mit Kriterium-2-Paket bündeln!).
   **(g) ERSTE BAU-AUFGABE — MaterialRow→MappablePosition (~20:00 erhoben):**
   Die Ableitung existiert NIRGENDS im Repo — der Golden-Case-Test (material-mapping-
   golden-case.real-files.test.ts:53 `toMappable`) leitet aus dem REFERENZLAUF-JSON ab
   (ReferencePosition: row/name/mat_cost_aw/group), nicht aus dem App-MaterialRow.
   Feld-Zuordnung ist fachlich (registry-semantics-Risiko!): name=partDesignation?
   cost=? (welches MaterialRow-Kostenfeld ≙ mat_cost_aw — über canonical-fields.ts
   MATERIAL_FIELD_KEY_TO_CANONICAL mit evidence abgleichen, NICHT raten), row=aus
   sourceCells-Blattzeile, group=Baugruppen-Feld (existiert es am MaterialRow?).
   Nach der Zuordnung: Golden-Case-Test um einen App-Pfad-Fall erweitern (toMappable
   aus MaterialRow MUSS dieselben Bucket-Zahlen liefern wie der Referenz-Weg — das
   ist die Äquivalenz-Probe der Ableitung).
   **(g)-ERGEBNISSE (~20:15, Branch qaf-v2/mat-komplett ab Block-1-Branch/Stack):**
   - cost = `materialCost` (CORE-Feld, evidence mat_calc_material_cost „Kalkulatorische
     Materialkosten [AW]" = Positions-Gesamt in AW ≙ Referenz mat_cost_aw) ✓ belegt
   - row = sheetRowNumber (Non-Values-Teil von MaterialRow, :572) — exakten Feldnamen
     beim Bau prüfen
   - name = materialDesignation ODER partDesignation — **NICHT semantisch raten,
     EMPIRISCH am Golden-Fall entscheiden**: beide Varianten durch buildMaterialMappings
     jagen, welche reproduziert die Referenz-Buckets? (Golden-Mappen liegen lokal in
     GOLDEN_DIR; App-Material-Parse aus der Mappe: den Ingest-Weg von actions.ts
     nachschlagen — welche Funktion macht Mappe→MaterialRow[]? → GEKLÄRT:
     **`parseMaterialSheet(excelWb)`** — actions.ts:1089, Import :91; async, nimmt das
     ExcelJS-Workbook direkt)
   - **⚠ group: MaterialRow HAT KEIN Baugruppen-Feld** (29 Leitfaden-Felder ohne
     „Baugruppe"; Referenz-groups kamen aus dem forensischen Lauf). → group=null im
     App-Pfad, gruppenbasierte merge-Regeln greifen nicht → Äquivalenz-Probe wird bei
     consolidation abweichen. Die Differenz MESSEN und als benannte Abdeckungsgrenze
     dokumentieren (Muster „bekannte Abweichungen zur Referenz"-Test); Parser-Erweiterung
     um eine Baugruppen-Spalte wäre eigenes Feature (nicht Block 2).

## BLOCK-1-STAND (~19:45): KOMPLETT SAUBER, wartet nur auf Kais-Apply
Vollsuite final 7104 passed / 0 failed. HICE-Golden bewusst aktualisiert mit
Strip-Beweis (nur provenanceAlt/Neu differieren, byte-identischer Resthash),
Commit `6321826` gepusht. Nach Kais' „applied": list_tables-Verify (3 Spalten)
→ PR erstellen → Suite → Ist-Check → Merge → Deploy-Beobachtung. · 3. Follow-up-PR deploy-451.md+arithmetik-patch (Handover-
Ordner) · 4. Branch-Aufräumen #452–#457 (gh pr list --base je Branch vorher) · 5. UI-Smoke
Headless+Screenshots · 6. D15–D17/R-03 Spec-Erhebung (bauen wenn definiert, sonst Fragen).
GitHub-Token-Vorfall gelöst (Memory gh-token-rotation-single-source).

## 🎉 LOOP 3 KOMPLETT (~10:10, 07.08.)

Vorbefund-Reihenfolge 1–5 vollständig produktiv: #453 (sprachfähig) → #454 (SUM +
Kriterium 1) → #455 (5. Schlüssel) → #456 (MFG + Tabelle) → #457 (scope_removed).
Plus #452 (Vorbefund selbst). Jeder PR: Bau → lokale Gates → volle Suite → separater
Ist-Check → Squash-Merge → Deploy verifiziert.

**Offen, alles Kais:** UI-Blick auf die V2-Seite · MAT-Migration (ggf. Paket mit
Kriterium-2-Spalten formula/value_state) · Referenzseiten-Abschaltung ·
Branch-Aufräumen #452–#457 (vorher `gh pr list --base` je Branch) ·
offene-entscheidungen-kais.md (Wert-Golden, FORBIDDEN_TERMS, quelle-Feld) ·
Follow-up-PR deploy-451.md + arithmetik-messung.patch (Handover-Ordner).
D15–D17 + R-03 (Taxonomie-Tranche) als möglicher nächster Loop.
   - Fachliche Prüfung der bestehenden Tooling-/Findings-Übersetzungen (Roadmap-Aufgabe,
     keine Test-Lücke) als eigene Review-Dimension mitführen.
   - Schritt 5 danach: `scope_removed` über NOT_YET_IMPLEMENTED-Marker in
     `material-mapping-fixture.test.ts:104` (Set + skip-Schleife :110/:119 = der Ansatzpunkt).
4. Separater PR nach #452: `deploy-451.md` + `arithmetik-messung.patch`
   (im Handover-Ordner, CHANGELOG-Kollision vermeiden).

## Offen bei Kais (unverändert, `offene-entscheidungen-kais.md`)

Wert-Golden auf anonymisierten Mappen · FORBIDDEN_TERMS um Lieferantennamen ·
`quelle`-Feld in manifest.json/build_manifest.py.

## Arbeitsweise (von Kais durchgesetzt)

Nach jedem Arbeitsschritt 2 Zeilen Telegram (chat_id 1164395546, markdownv2) ·
jeder Zug endet mit Arbeit UND gesetzter Weckung · bei externer Blockade Weckung
lang stellen, nicht häufiger melden · Repo-Policy: Push/PR/Merge ohne Rückfrage
erlaubt, Gates bleiben Pflicht.

## ⭐ AKTUELLER STAND (~23:00, 07.08.) — WARTE-ZUSTAND, 5/6 Programm-Blöcke fertig

Blöcke: 1 Kriterium 2 ✅ gebaut (Branch kriterium2-summary-provenance, wartet auf Apply) ·
2 MAT komplett ✅ gebaut (**PR #458**, Stack, wartet auf Apply) · 3 Nachtrag ✅ **#459
gemergt (`8a450a5`) + deployt** · 4 Branch-Aufräumen ✅ (6 gelöscht, 3 aktive übrig) ·
5 UI-Smoke wartet designbedingt auf Apply+Merges · 6 Spec-Erhebung ✅ (D15–D17+R-03:
3×P0 gefordert, NIRGENDS definiert — User-Guide-PDF geprüft; 4 präzise Fragen an Kais,
TG 9959, + Angebot Begleitdokument direkt zu lesen).

## NEUER LOOP (Kais-Antworten 08.08. 17:15): D15–D17 + R-03 als weiche Befunde

Kais' Linie (TG 9970, Interpretation bestätigt TG 9971): **weiche Befunde statt harter
Gates, Mensch entscheidet, System liefert Evidenz.** (1) D15 melden-nie-blockieren ·
(2) D16 „flexibel, Befund" · (3) D17 „Make-or-Buy treffen wir anhand der Analyse" =
System liefert Indizien · (4) R-03 „flexibel" = deklarierbar + Evidenz, undetermined ok.

BAUPLAN (Branch ab main, PR 1 = Detektoren, PR 2 = R-03-UI):
- **D16 zuerst** (klarster): compareQafPair vergleicht Identitätsfelder
  (summary.partNumber/supplier/requestVersion/changeIndex) alt↔neu → PlausibilityIssue
  `identity_mismatch` je Abweichung mit beiden Werten als Evidenz; Paarung läuft weiter.
- **D15**: ref.fileName enthält Inhalt-Sachnummer nicht (nur wenn PN vorhanden) →
  niedrigschwelliger Befund `filename_mismatch`. Konservativ, Falsch-Positive beachten.
- **D17**: Anteile materialCosts/manufacturingCosts an totalProductionCosts je Stand aus
  summaryDiffs; Verschiebung > konservative Schwelle (Named-Config wie DifferBands!) →
  Befund `make_or_buy_indication` („Fertigungstiefen-Indiz — Analyse erforderlich").
- **R-03** (PR 2): qaf_comparison.comparison_mode (existiert, TEXT, heute null)
  deklarierbar — kleine Typ-Liste + undetermined, Server-Action zum Setzen, V2-Header
  zeigt Deklaration + Evidenzliste (Identitäts-/Header-Felder beider Stände). KEIN Raten.
- HICE-Golden wird durch neue plausibility-Issues anschlagen → Strip-Beweis-Prozess.

## ⭐⭐ MERGE-KETTE LÄUFT (08.08. ~18:15) — Apply erfolgt + verifiziert

Kais' Apply-Meldung 18:05 („SQL: Success" ×2, msg 9980/9981). **Live-verifiziert** (nicht
nur geglaubt): `qaf_material_diff` in list_tables (RLS an, 0 Zeilen) ✓ · 3 Spalten per
REST-Select HTTP 200 (anon-Key, leeres Array = RLS, 200 = Spalten existieren) ✓.
MIGRATIONS.md #118 auf APPLIED committet (bf60490).

**Stand der Kette:** (1) ✅ **#460 GEMERGT** (Suite komplett grün, Ist-Check CLEAN,
Squash `7ecd8e6`) → ✅ **Prod-Deploy success** (Vercel G5xrqzrus9L1jrCBoJF7boY2dYZu,
16:27) — Kriterium 2 ist LIVE. (2) ✅ **#458 GEMERGT** (16:39: Suite grün nach
main-Merge mit 3 gelösten Konflikten — CHANGELOG alle Einträge · MIGRATIONS #118+#119
APPLIED · HICE-Hash ours, bewiesen per lokalem 4/4-Lauf; Ist-Check CLEAN, Squash
`49a273f`). Remote-Branches beider PRs gelöscht (Base-Check vorher leer); lokale
Branches stehen noch (Hook blockt `branch -D` ohne Kais-Ok — Kosmetik, nicht fragen).
Working Tree auf aktuellem main. (3) ✅ **#458-Deploy success** (Vercel
95XUy7mSNtuK7coVEgB1XTsW18Y1, 16:41). (4) ✅ **Block 5 Smoke**: beide QAF-Routen
307→Login (kein 500/404), Root 308→/projektanlage (bmw-Profil), Supabase-API-Logs
nur 2xx. NICHT behauptet: eingeloggte Sichtbarkeit — Kais' Blick; qaf_material_diff
leer bis zum ersten Recompare (erwartet, Sektion zeigt Leerzustand). (5) ✅
**Deployment-Protokoll geschrieben**: `docs/qaf-v2/loop-reports/deploy-460-458-2026-08-08.md`
— liegt UNTRACKED auf main, mit dem nächsten Loop-PR einchecken (Nachtrag-Muster #459)!

## 🔨 NEUER LOOP LÄUFT (08.08. ab ~18:50): D15/D16/D17 gebaut, PR 1 in Arbeit

Branch `qaf-v2/plausibility-weiche-befunde` (ab main). Gebaut + verifiziert:
- **D16**: 4 Identitätsfelder in checkSummaryIdentityPlausibility (supplier_mismatch/
  supplier_no_mismatch pruefen · request_version_changed/change_index_changed hinweis),
  beide Werte als Evidenz, bothPresentAndDiffer-konservativ.
- **D15**: checkFilenamePartNumber je Seite (filename_part_number_mismatch, hinweis),
  [A-Z0-9]-normalisiert, Mindestlänge 6; in compare.ts je ALT/NEU verdrahtet.
- **D17**: checkMakeOrBuyIndication (make_or_buy_indication, hinweis, EIN Befund mit
  beiden Anteilspaaren) — Schwelle als EngineConfig-Sektion makeOrBuyIndication
  (10 pp Startwert), configVersion 1.4.0→**1.5.0**, SECTION_KEYS+validate+
  resolvePersistedEngineConfig erweitert (rehydrate.ts, Fallback-Tradeoff dokumentiert).
- Tests: plausibility.test.ts +14 (38 gesamt), engine-config.test.ts +2 (Pins auf 1.5.0).
- **Cross-Language-Fixtures**: 10 fileName um PN ergänzt (D15 feuerte korrekt auf
  synthetische Namen — Fixture-Fix, nicht Check-Fix; Muster `<id>-<pn>.xlsx`).
- **HICE-Golden aktualisiert MIT Strip-Beweis**: einziger neuer Befund = D15 auf NEU
  (260401_QAF_HICE_Aktuell.xlsx trägt 5B5F9B5 nicht; ALT still, weil PN dort nicht
  geparst). Strip (Befund raus + Section raus + Version zurück) → byte-identisch
  7d6812b5… ✓. Kein D16/D17 auf den echten Dateien (Identität stimmt, Schwelle hält).
- **3 Mutations-Proben**: D16-Schleife leer → exakt 4 rot · D15 stumm → exakt 1 rot ·
  D17-Schwelle ∞ → exakt 2 rot (beide Feuerwege). Reste-Grep sauber.
- typecheck ✓ · check:portability strict ✓ · CHANGELOG-Eintrag oben drauf.
- ✅ Vollsuite 7134/0 + Build grün → 2 Commits (120f38c Feature + Protokoll) →
  **PR #461 erstellt**, CI-Wache bhncdbw55 läuft. Nach Suite: Ist-Check separat →
  Squash-Merge → Deploy-Wache → dann R-03.
- **R-03-ERKUNDUNGS-BEFUND (wichtig!)**: `comparison_mode` ist NICHT frei — es ist
  ein geschlossenes TECHNISCHES Modus-System (`lib/qaf-differences/internal/
  comparison-mode.ts`: summary/g60/multi_qaf/multi_qaf_variant_vs_standard/
  supplier_benchmark, normalizeComparisonMode mappt Unbekanntes → 'summary',
  Exhaustiveness-Guard, Dispatch-Rules je Modus). Die fachliche R-03-Deklaration
  („gleiches Teil über Zeit / anderer Lieferant / andere Variante / undetermined")
  ist ORTHOGONAL dazu → NICHT dieselbe Spalte mitbenutzen (Doppelbedeutungs-Drift,
  genau was das Modul beerdigt hat). Empfehlung (a): neue nullable TEXT-Spalte
  `qaf_comparison.declared_relation` (Migration #120, operator-applied, ein ALTER,
  Editor-tauglich) + kleine geschlossene Werteliste + undetermined-Default im Code +
  Server-Action + V2-Header-Anzeige mit Evidenzliste (Identitäts-/Header-Felder
  beider Stände). Migration als LETZTES Stück bauen (Apply-Tanz minimieren);
  Kais die Spalten-Frage in EINEM Satz vorlegen, kein Blocker (undetermined-
  Verhalten existiert heute schon implizit).
- Transparenz für Kais: D15 feuert künftig auf jeder Datei ohne PN im Namen (echte
  BMW-Namen oft Datum+Kontext) — bewusst niedrigschwellig, justierbar am Objekt.
- ✅ **#461 GEMERGT + DEPLOYT** (Squash 8d9a04b, Deploy success 19:41 — D15/D16/D17 LIVE).

## 🔨 R-03 GEBAUT (08.08. ~19:50) — PR 2 in Arbeit, Gates laufen

Branch `qaf-v2/plausibility-weiche-befunde`?? NEIN — R-03 baut auf main nach
#461-Merge; Working Tree = main + uncommitted R-03-Stand. Umsetzung:
- **declared-comparison-type.ts (neu, lib/internal + Barrel-Export)**: Kap.-21-Liste
  (temporal_change/supplier_benchmark/site_comparison/variant_comparison/undetermined),
  asymmetrische Boundaries: normalize (Lesen, Fremd→undetermined) vs isDeclared
  (Schreiben, Fremd→ablehnen). Header-ComparisonType = Alias darauf (eine Quelle).
  + COMPARISON_TYPE_EVIDENCE_ISSUE_TYPES (9 Identitäts-Issue-Typen inkl. D15/D16).
- **Migration #120** supabase-migration-qaf-declared-comparison-type.sql (+Rollback,
  im Scratchpad für Versand): qaf_comparison.declared_comparison_type TEXT nullable,
  COMMENT, editor-tauglich. MIGRATIONS.md Sektion 7u. **KEIN deployment_only_blocker**:
  page.tsx liest die Spalte über SEPARATEN fehlertoleranten Select (error →
  undetermined + Hinweis „Migration ausstehend"), Action meldet 42703 klar.
  Rollback-Warnung: Deklarationen sind Menschen-Entscheidungen, NICHT ableitbar!
- **Action declareComparisonType** (actions.ts Ende): uuid-Check, isDeclared-
  Validierung, getClaims-Auth, Existenz-Check, Update, qaf_audit_log
  (comparison_type_declared), 42703-Sonderfall.
- **UI**: qaf-v2-declared-type.tsx (Client: select+Übernehmen+router.refresh) +
  page.tsx-Sektion „Vergleichsart deklarieren + Evidenz" (Dateinamen, PN,
  Identitäts-Befunde aus qaf_plausibility_issue via .in(EVIDENCE_TYPES));
  Header comparisonType={declaredType} statt hartkodiert.
- Tests: declared-comparison-type.test.ts 5/5 · 2 Mutations-Proben (Lese-Boundary
  durchlässig → genau 1 rot; Schreib-Boundary durchlässig → genau 1 rot) ·
  typecheck ✓ · portability strict ✓ · CHANGELOG-Eintrag oben.
- ✅ Vollsuite 7139/0 + Build → Branch qaf-v2/r03-vergleichsart-deklarierbar →
  **PR #462**. Erster CI-Lauf ROT: NICHT die Tests, sondern das **lint-baseline-Gate**
  (react/no-unescaped-entities — straight quote in neuem JSX-Text). Fix 6693501,
  Memory gespeichert (kadi-push-gates-include-lint-baseline: check:lint-baseline
  gehört ins lokale Gate-Set, Workflow-Dateien sind die Gate-Wahrheit). Zweiter
  Lauf komplett grün → Ist-Check CLEAN → ✅ **#462 GEMERGT** (Squash c17e2ac,
  18:27 UTC, --delete-branch). Deploy-Wache b8pz96bhd läuft.
- ✅ **Apply-Paket #120 an Kais versandt** (TG 9997–10000, Migration+Rollback als
  Dateien) — EXPLIZIT entspannt: nichts wartet, Seite degradiert ehrlich; er meldet
  „applied", DANN Live-Verify (REST-Select auf declared_comparison_type, HTTP 200
  = Spalte da) + MIGRATIONS.md #120 auf APPLIED (kleiner Folge-PR oder mit dem
  nächsten PR bündeln — CHANGELOG-Kollision beachten).
- ✅ **#462 DEPLOYT** (success 18:28 UTC, Smoke 307→Login ok). Loop-Abschluss an
  Kais gemeldet (TG 10001). Deployment-Protokoll geschrieben:
  `docs/qaf-v2/loop-reports/deploy-461-462-2026-08-08.md` — **UNTRACKED auf main**,
  mit dem nächsten PR einchecken (z. B. dem #120-APPLIED-Update)!

## ⭐ END-ZUSTAND (08.08. ~20:35) — ALLES GEBAUTE LIVE, Ball bei Kais

**Fertig + live:** #460 Kriterium 2 · #458 Material komplett · #461 D15/D16/D17 ·
#462 R-03. Migrationen #118+#119 applied+verifiziert; **#120 bei Kais** (entspannt,
Paket TG 9998–10000, Seite degradiert bis dahin ehrlich).

**Bei Kais' „applied" für #120:** Live-Verify per REST-Select
(`qaf_comparison?select=declared_comparison_type&limit=1`, HTTP 200 = da; Key via
get_publishable_keys) → MIGRATIONS.md #120 auf APPLIED + Sektion 7u-Titel + beide
untracked Deploy-Protokolle (460-458 + 461-462) als kleiner Doku-PR (CHANGELOG-Kopf
beachten). **Stille Wache läuft** (1h-Rhythmus): prüft den Select auch OHNE Meldung —
falls Kais applied ohne zu schreiben, Doku-PR direkt; KEINE SQL-Reminder an ihn (Vereinbarung!).

**10.08. ~19:10 — PRODUKTIV-BEWEIS:** Kais hat ein frisches HICE-Paar hochgeladen
(Comparison c2c671e1-0717-4268-9b52-2c4bc1c0dc83, Klassik-Screenshot) → am Live-Schema
verifiziert: qaf_material_diff **0→61 Zeilen** (Block-2-Persistenz arbeitet produktiv!),
qaf_summary_metric +33 (Provenienz-Pfad), qaf_plausibility_issue +1443, qaf_comparison
39→40. V2-Link mit seiner ID geschickt (TG 10033). **Prod-Domain ist app.kaiis.de**
(Custom Domain — für Smokes nutzbar, vercel.app geht auch). #120 weiter unapplied
(Wache läuft), Vergleichsart-Sektion zeigt ihm den ehrlichen Hinweis.

## 🎯 NEUER AUFTRAG (10.08. ~19:25, TG 10034–10038): Zusammengeführte Ansicht + Loop-Fahrplan

Kais' Feedback (2 PDFs beider Ansichten): „zu unübersichtlich — zusammenführen,
grafisch wo sinnvoll, Unverändertes nicht auflisten (aber in JSON/Backend behalten)".
**GO erteilt**: Zusammenführung wird DIE Vergleichsseite, V1 bleibt parallel,
„Abschaltung später". Sein PDF fand außerdem einen echten Bug: Evidenzliste zeigte
rohen @@BI1@@-Umschlag → **Fix-PR #463** (decodeBilingual .de; Wache bflc784rz läuft;
nach grün: Ist-Check separat → Squash-Merge → Deploy-Wache).

**⭐ GROSSES MANDAT (TG 10039):** „komplett autonom mit mehreren Agenten und sehr
sorgfältig umsetzen" = Fahrplan automatisch abarbeiten. #463 gemergt+deployt.

**PUNKT-1-BAU (Branch qaf-v2/ansicht-zusammengefuehrt, 10.08. ~19:45–20:25):**
- 3 Explore-Memos: (1) V1-Anatomie (buildBridge/buildBuckets/buildMetricsTable pure
  über qaf_summary_diff; Charts portabel; qaf_part-Identität; unchangedPhrase =
  nur Kopftext, NIE Zeilenfilter — Kais' Wunsch ist echtes Novum), (2) V2-Datenfluss
  (kein qaf_part/qaf_summary_diff-Select; QafBridgeChart ignoriert Toleranz =
  F-01-Falle!, QafV2Waterfall setzt die Regel um), (3) Filter-Substrat (95er-Wand
  MESSUNG: 100% formel_geaendert Δ0! changedOnly-Falle: TableRow verliert
  formulaFindingKind + delta:null; richtiger Punkt = Record-Ebene; Exporte
  importieren table-specs nicht → strukturell unberührbar).
- GEBAUT: record-partition.ts (3-Wege: wertOderStruktur/formelOhneWertaenderung/
  ohneWertaenderung; 6 Tests + Mutations-Probe exakt) · qaf_part-Select+supplier
  im Header · Kostenstruktur-Sektion (immer sichtbar, 2 benannte Leerzustände) ·
  Brücken-GATE (QafBridgeChart NUR bei residualWithinTolerance, sonst Waterfall
  MIT benanntem Residuum-Betrag) · MFG/MAT: Haupttabelle + 2 <details>-Gruppen ·
  Lade-Fehler-Banner (alle Selects error-erfasst, Klassik-Muster).
- 2 REVIEW-AGENTEN eingearbeitet: Silent-Failure-Hunter (5 Findings: error-
  Verschlucken F1/F2 CRITICAL, Brücken-Fallback unerklärt F3, Kostenstruktur
  verschwindet still F4, Vollständigkeits-Claim F5 → ALLE gefixt via
  ladeFehlerQuellen-Banner + Sektions-Zustände) · Code-Reviewer (1 Finding conf 85:
  Zwei-Quellen-Widerspruch Kacheln(live,2/10%-Ampel) vs Brücke(persistiert,
  Engine-Bänder) → GELÖST über Ein-Quellen-Schnitt: OverviewRun.summaryDiffs
  exponiert + rowsFromMetricDiffs-Adapter (summary-view.ts, +3 Tests) —
  qaf_summary_diff-Select WIEDER ENTFERNT, alles aus run.summaryDiffs).
- Stand: typecheck ✓ lint-baseline ✓ 9 neue Tests ✓ · **FINALE GATES LAUFEN
  (bbu83rgi7: lint+portability+Vollsuite+Build)** → dann Commit (record-partition,
  rows-adapter, overview-run, summary-view, page, Barrel, CHANGELOG — sw.js NICHT!)
  → PR → Wache → Ist-Check → Merge → Deploy-Wache → Kais-Meldung.
- Cross-Page-Farbdifferenz (V2-Kacheln 2/10% vs Klassik-Badges 10/25/50) = bekannte
  bewusste Design-Differenz, im PR benennen, NICHT still ändern (Produktentscheid).
- ✅ **#464 GEMERGT (fb0a9fdf) + DEPLOYT (success 18:51)** — zusammengeführte
  Ansicht LIVE, Kais gemeldet (TG 10045). PUNKT 1 KOMPLETT.

## 🔨 FAHRPLAN PUNKT 2 (Loop 4 + Loop 8) — Erkundungs-Stand 10.08. ~20:55

**Loop-4-Befund (RTM Zeilen 37–45 + program-status:189):** Die RTM (Stand 03.08.)
listet DQ-015/016/017 + CLS-003 als „Fehlt" — sie SIND aber exakt D15/D16/D17/R-03
(live seit #461/#462)! FOR-002 = formel_zu_konstante (Formel-Engine, existiert),
DIF-005 = scope_removed (Loop 3 Schritt 5, erledigt). **Echter Rest-Zuschnitt
Loop 4:** (a) V2-Header-comparability ABLEITEN statt hart 'undetermined' —
regelbasiert aus den persistierten Plausibility-Issues (part_number_mismatch →
not_comparable · D16-Identitäts-Issues → limited · sonst comparable, wenn der
Lauf Issues führen KANN; Ableitung = Prüf-Aussage, kein Raten — pure Funktion +
4 Probenarten!), (b) sichtbarer Reviewstatus „Einordnung ausstehend" bei
declared undetermined, (c) Gegenproben-Audit der D15/D16/D17-Tests (Memory
counter-probe: Verweigerung testen), (d) program-status.md: Loop 4 auf
abgeschlossen mit PR-Verweisen (#461/#462 + neuer PR; Baseline-RTM NICHT
umschreiben — Abweichung dokumentieren, das ist die dokumentierte Konvention).
**Loop 8** (18 Anforderungen, program-status:266): capability/-Schicht existiert
(module-capability-resolver + capability-detector, KAR-959 real-files-Tests) —
Verdrahtung in V2-Navigation („kein leerer Hauptreiter, Coverage sichtbar,
Excel-Terminologie"). EIGENER PR nach Loop 4.
**Vorgehen wie Punkt 1:** Analyse-Agent(en) für comparability-Ableitungsregeln
+ capability-Resolver-Ist, dann bauen, Review-Agenten, Gates, PR je Loop.

**LOOP 4 GEBAUT + REVIEWED (10.08. ~21:15, uncommitted auf Branch
qaf-v2/ansicht-zusammengefuehrt — Working Tree!):** comparability.ts
(deriveComparability: part_number_mismatch→not_comparable · supplier/
supplierNo/variant→limited · sonst comparable mit Begründung; Hinweis-Typen
bewegen NIE; ComparabilityStatus-Typ zog ins lib, Header aliasiert) + 10 Tests
(alle 4 Probenarten + Mutations-Probe traf exakt) + „Einordnung ausstehend"-
Reviewstatus (status-yellow-Chip) + program-status Loop 4 abgeschlossen (RTM-
Abweichung benannt: DQ-015/016/017+CLS-003 WAREN D15/D16/D17+R-03!). REVIEW-
Agent-Findings ALLE eingearbeitet: (1) Alt-Läufe vor D16 → engine_version-
Stempel-Gate (isEngineConfigVersionAtLeast ≥1.5.0, jetzt im Barrel) mit
ehrlicher Grenz-Begründung statt falschem Clean-Run-Claim, (2) 2 Testlücken
(limited+hint gemischt, 2 limited-Typen) geschlossen, (3) Header-Zähler
„Vergleichbar (1)" nur noch bei tone!=good, (4) reference-case.ts: letzte
lib→components-Import-Kante aufgelöst (Typen aus internal), (5) program-status
PR-Nummern-Spekulation neutralisiert. Schnell-Gates grün. **FINALE
Vollsuite+Build LAUFEN (bv5oq6m61)** → Commit (comparability.ts+test,
page.tsx, qaf-v2-header.tsx, reference-case.ts, index.ts, program-status.md,
CHANGELOG — sw.js NICHT) → PR „Loop 4 abgeschlossen" → Wache → Ist-Check →
Merge → Deploy-Wache → Kais-Meldung → DANN Loop 8 (Capability-Navigation,
eigener Zyklus mit Analyse-Agent zuerst).
✅ **LOOP 4 KOMPLETT**: PR #465 gemergt (b7bc974d) + deployt (success 21:42).
**LOOP 8 TEIL 1 GEBAUT+REVIEWED (uncommitted, ~22:05):** Analyse-Memo:
capability_matrix wird seit KAR-959 je Datei in qaf_file.g60_meta persistiert,
NIE gelesen; für SBM/LOGISTICS/RMR/LC_CN/CO2E einzige Quelle (keine Diff-
Tabellen); Klassik gated via production-view/SectionAvailability; keine
Tabs im Repo → „Hauptreiter" = Loop-10-Territorium, BEWUSST offen (program-
status Loop 8 „teilweise", Teil 2 benannt). GEBAUT: capability/coverage-view.ts
(Boundary: null=„kein Capability-Stand" NIE als MISSING; Fremdstatus sichtbar;
DE-Labels-Map = erste Modul→Namen-Registry) + V2-Sektion „Abdeckung der
QAF-Bereiche" (8 Module × beide Stände, JSONB-Pfad-Alias-Select
capability_matrix:g60_meta->capability_matrix — lädt NUR den Schlüssel).
Silent-Failure-Review: Pfad-Syntax ✓ (->-Objekt korrekt, gegen simvsm-
Präzedenz), Zip ✓, ABER CRITICAL: erstes Promise.all (files/metrics)
verschluckte error → Coverage hätte „vor Bereichs-Erfassung hochgeladen"
GELOGEN → files/metricsErr jetzt in ladeFehlerQuellen + eigener Ladefehler-
Zustand der Sektion. Boundary gehärtet (a: status fehlt/mistyped = eigener
Text · b: fremde Modul-Keys als Zusatzzeilen statt verschwinden · c: Duplikate
= „widersprüchlich") + Render auf Modul-Key-Union statt Index-Zip (extras
können Längen verschieben!) + 3 neue Tests (9 gesamt) + Mutations-Probe
(null-Boundary gekippt → exakt Negativ-Test rot). modelVersion bewusst
ungegatet (dokumentiert: reine Wiedergabe). Schnell-Gates ✓. ✅ **LOOP 8 TEIL 1 KOMPLETT**: Suite-Flake (1 rot ohne Name im tail) durch
Wiederholung als Flake belegt (7165/0 + capability/ isoliert grün + CI-Suite
grün = Drittbeleg; transparent im PR-Body). PR #466 gemergt (be58b4b0) +
deployt (success 22:51). Kais-Bilanz TG 10049 (4 PRs seit Mandat, alle live).

**FAHRPLAN PUNKT 3 GESTARTET (~23:00): Loop 5 (Source Artifact/Reparse) +
Loop 6 (kanonischer JSON-Vertrag).** Analyse-Agent läuft (Ingest-Pfad/Bytes-
Verbleib, Storage-Bucket-Konventionen, minimaler Loop-5-Schnitt inkl.
Migration-Bedarf [Bucket+Policies+qaf_file.storage_path — Paket an Kais
entspannt wie #120!], Klassik-JSON-Knopf-Ist vs 03_Canonical_Schema vs
buildV2Export[0 Aufrufer], Exit-Kriterien wörtlich). **MEMO-ÜBERRASCHUNG: Loop 5 braucht KEINE Migration** — Bytes werden seit
KAR-840 im qaf-uploads-Bucket aufbewahrt (Signed-URL-only, KEINE storage-
Policies, bewusst), qaf_file.storage_key existiert seit Gründung UNBEFÜLLT
(0 Zuweisungen repo-weit). Loop 6 ist GROSS (3 nicht-interoperable JSON-
Formen: 03_Canonical_Schema 20-Key-Graph unimplementiert · buildV2Export
ungenutzt/andere Spec · buildViewModelExport = der Klick-Export) → eigener
Zyklus SPÄTER. **LOOP 5 TEIL 1 GEBAUT (~23:30, uncommitted):** storage_key
an allen 3 qaf_file-Inserts (actions.ts Summary/MultiQAF/G60) ·
createQafSourceDownloadUrl (RLS-Select-Gate → Admin createSignedUrl 300s;
Register-Eintrag service-role-intent-register.md im 7-Spalten-Format!) ·
qaf-v2-source-download.tsx (Klick→URL minten→window.open; hasSource=false →
explizite „nicht hinterlegt"-Aussage) · V2-Evidenz zeigt je Stand Download/
Fehlt-Status · CHANGELOG + program-status Loop 5 „teilweise" (Teil 2 =
Reparse-Flow + SRC-002-Kette, benannt). Typecheck+lint ✓. **SECURITY-REVIEW: CRITICAL GEFUNDEN + GEFIXT** — Confused Deputy an der
SIGNIER-Stelle (nicht Ingest, der war sauber): storage_key ist freie TEXT-
Spalte, per PostgREST-Update auf fremde Pfade setzbar; meine Action hätte
blind signiert. Und Migration #117 (offenes Modell, „not yet applied")
hätte das später TRIVIAL gemacht. FIX: project_id mitselektieren +
qafStoragePathShape(project_id)-Guard an der Signier-Stelle (Muster der
Schwesterfunktionen), Log qaf.source_download_url path_mismatch, Register-
Zeile präzisiert (Prefix-Guard + beide Log-Keys). Reviewer-Nebenbefund
(unterhalb Schwelle, notiert): reine _read-Nutzer bekommen erstmals ROH-
Workbook-Zugriff (mehr als geparste Felder) — Produktentscheid, bei Kais
ansprechen wenn relevant. ✅ **LOOP 5 TEIL 1 KOMPLETT**: Gates grün → PR #467 → Suite grün → Ist-Check
CLEAN → gemergt (6723620c) → **deployt (success 23:44)**. Kais-Meldung TG
10051 (Abend-Bilanz: 6 PRs) + unblockierende Produktfrage offen (Roh-Excel
für Nur-Lese-Nutzer ok? — „ein Wort genügt"). Daily-Log 2026-08-10 angelegt
(6 PRs, Review-Trefferquote 100%, Lehren). Lokale Branch-Refs sammeln sich
(ansicht-zusammengefuehrt, loop4-abschluss-ampel, loop8-teil1-coverage,
loop5-teil1-quellzugriff + alte — branch -D braucht Kais-Ok, Kosmetik).

## 🔨 LOOP 6 GROSS-ZYKLUS GESTARTET (~23:50): Design-Schritt läuft

✅ **DESIGN-MEMO LIEGT VOR** (Scratchpad: loop6-design-memo.md — VOLLTEXT
dort lesen bei Wiederaufnahme!). Essenz: **21** Pflicht-Keys (nicht 20);
Schema-Kopie liegt byte-identisch in schemas/qaf-comparison-canonical-v1.0.json
(dort kompilieren!); Etappe 1 IST machbar: entities/facts/relationships/
mapping_groups/manual_overrides/unmapped_regions dürfen LEER sein (kein
minItems), differences OHNE fact_ids valide. **ajv v8 + ajv-formats als
devDependencies** (transitive v6 kann KEIN draft/2020-12; Ajv2020 aus
'ajv/dist/2020'); KEINE Zod-Nacherzählung (= verbotene Parallel-Registry).
sha256 der source_artifacts zur EXPORTZEIT aus Storage (App-Layer, nicht
lib!). Decimal-als-STRING direkt aus QAF_CONVENTIONS.rounding formatieren
(NICHT canonicalNumber→String — zwei Rundungsschichten!). hashableDocument
wiederverwendbar, aber HASH_EXCLUDED_PATHS neu ziehen (run_manifest.
canonical_content_hash-Selbstbezug). Enum-Mappings je eigene Tabelle+Tests
(kind→difference_status 9→8 · Capability AVAILABLE/…→available/… mit
DERIVABLE→partial · Recon 3→7 · Severity fast 1:1). schema_version const
"1.0.0" (v2-export hatte falsch '2.0'). analysis_areas: 24 Keys (19+5:
capabilities_and_coverage, formula_and_value_changes, logistics_and_
customs_analysis, local_content_and_co2e_analysis, negotiation_and_actions
— fehlende als not_computed/null). **Dokumentierte Abweichungen:** (5.1)
finding/action haben KEIN difference_ids-Feld → Differenz-Bezüge via
extensions."supplierpulse.current_engine" (finding_difference_ids/
action_difference_ids-Maps); (5.3) RTM ordnet JSN-* Loop 8 zu, Roadmap-Text
Loop 6 → benennen, nach Roadmap bauen; (5.4) NUR Länge-2-Arrays aus
Pair-Engine, multi-qaf/-Ordner TABU (deferred). run_manifest: 5 Policy-
Versionen vorerst alle = engineConfig.configVersion (dokumentiert),
ai_used/ui_used const false, KEIN Zeitstempel im Schema (gut für Hash!).
**ETAPPE 1a GEBAUT (Branch qaf-v2/loop6-etappe1-canonical-export, ~00:20):**
canonical-export.ts (buildCanonicalExport 21 Keys · decimalString EINE
Formatierungsschicht · Compile-erzwungene Enum-Tabellen inkl. moved→changed
[9. Kind bestätigt] · deriveAnalysisStatus · withCanonicalContentHash-
Selbstbezug · extensions-Block für finding/action-difference_ids) + 11 Tests
(ajv gegen schemas/-Original — 3 Pattern-Runden als Bauleiter: sha256:-Präfix
an 3 Stellen + capabilityStatus.reason_code Pflicht) + Mutations-Probe
(complete-Regel gekippt → exakt 2 Status-Tests rot) + Barrel + package.json
(ajv@^8, ajv-formats@^3 dev) + CHANGELOG + program-status Loop 6 „begonnen".
**SCHNITT-ENTSCHEID: Etappe 1a OHNE Server-Aufrufer** — der käme heute nur
per Duplizierung der V2-Seiten-Komposition (gegen die Ein-Quellen-Lehre);
Etappe 1b = Aufrufer + Extraktion der Seiten-Komposition nach lib (eine
Quelle Seite+Export), dann Etappe 2 Graph, Etappe 3 ViewModel-Ablösung.
✅ **ETAPPE 1a KOMPLETT**: Review fand 2 Echte (decimalString einheitenblind
→ jetzt QAF_CONVENTIONS.rounding-bewusst mit F-11-Testfall 5579440.0003921185
→'5579440' bei EUR + 1e21-Guard · keine Negativ-Proben → 3 Schema-Gegenproben
ergänzt). 16/16 Tests · alle Gates · **PR #468 gemergt (63339291) + deployt
(success 00:57 Mo)**. Kais TG 10053. Enum-Bestätigungen des Reviews notiert:
newly_filled→new KORREKT (reactivated ist Multi-QAF-Timeline-Konzept, wäre
Fähigkeits-Vortäuschung); renamed/merged/split→changed schema-erzwungen
(mapping_type sitzt an mappingRelation = Etappe 2).

## 🔨 ETAPPE 1b GEBAUT (~01:20, uncommitted auf Branch loop6-etappe1-canonical-export)

Umgesetzt: OverviewRun +summaryRecords/+summaryDifferenceIdsByMetricKey
(additiv, eine Quelle) · exportQafCanonicalJson in actions.ts (Selects wie
Seite, hashOf mit qafStoragePathShape-Guard VOR admin.download [Register-
Zeile ergänzt!], Komposition aus den EINEN Producern, metric_id=summary.
<metricKey> aus echter Map, ohne Quelle → klare Abbruch-Meldung [sha256
Pflicht], createHash über Platzhalter-Doc → withCanonicalContentHash) ·
qaf-v2-canonical-export.tsx (Blob-Download-Insel) · Seiten-Knopf ·
CHANGELOG 1b-Absatz. Schnell-Gates ✓ (typecheck, lint-baseline,
portability, 19 Modul-Tests). **FINALE Vollsuite+Build LAUFEN.**
✅ PR #469 erstellt, Suite grün. **REVIEW FAND 4 CRITICALS + 3 MEDIUMS —
ALLE EINGEARBEITET (uncommitted auf Branch loop6-etappe1b-export-knopf!):**
(C1) hashOf → diskriminiertes Ergebnis (missing_source/path_mismatch/
download_failed je getrennte Meldung + Logs inkl. download_threw-Catch)
· (C2) mfg.withheldDe/skippedDe + mat.skippedDe + run.traceabilityDe als
not_verifiable/warning-Checks in den Export (Zurückgehaltenes verschwindet
nie) · (C3) engineVersion vom REALEN differ-Stempel ('.engine' existierte
NIE — toter Zweig!), parserVersion vom Zeilen-parser-Stempel mit Fallback
· (C4) try/catch in Action-hashOf UND Insel (storage-js kann rejecten!) +
revoke-setTimeout + DOM-append · (M1) CANONICAL_HASH_PLACEHOLDER exportiert
+ Verifizierer-Rezept dokumentiert (kompakt hashen, Datei eingerückt!).
✅ **ETAPPE 1b KOMPLETT**: Fix-Commit 484a210 → PR-Suite grün → Ist-Check
CLEAN → **#469 gemergt (025880e0) + deployt (success 02:03 Mo)**. Kais TG
10054 (8 Merges seit Go). Daily-Log 11.08. angelegt. #120-Wache neu
(bqujm4rmn, 6h). Folge-Punkte notiert: Action-Test-Nachrüstung
(Mock-Muster get-calculator-params.test.ts) · Etappe 2 (Graph) · Etappe 3
(ViewModel-Ablösung + Seiten-Komposition auf Export-Funktion).

## 🔨 LOOP 11 TEIL 1 GEBAUT (~02:40, uncommitted auf Branch loop6-etappe1b… NEIN: Working Tree nach #469-Merge auf dem alten Branch — beim Commit FRISCHER Branch ab main!)

Analyse-Memo-Kernfunde: font-display = TOTE Klasse app-weit (~32 Dateien,
nie definiert, Headings erbten Body-Font!) · BMW-002 uppercase-Verstöße
(8 V2-Titel) · --qaf-bucket-increase/-decrease NIE definiert (Fallback-Hex
griff immer, #6b8fa8 palettenfremd) · qaf-stand-Fallbacks veraltet ·
KEIN Tooling fängt das („Token-Audit besteht" hatte kein Skript) ·
Loop 12: PPTX für QAF existiert NICHT (pptxgenjs 5× woanders bemustert),
presentation_package-Slot leer — Reihenfolge: erst Slot-Inhalt im
kanonischen JSON, DANN PPTX als dünne Schicht (2 PRs).
GEBAUT (Loop 11 Teil 1): .font-display-Utility (repariert app-weit —
sichtbare gewollte Korrektur!) · --qaf-bucket-increase/-decrease Light+Dark
(Werte=Ist, Farbänderung=eigener Entscheid) · Waterfall ohne Fallback-Hex ·
8 Titel Sentence Case (Eyebrows bleiben) · scripts/check-design-tokens.mjs
(lose Vars + abweichende Fallbacks; GEGENPROBE bestanden: alter stand-alt-
Drift exakt gefangen) + in check:portability-Kette · program-status Loop 11
teilweise (offen: TST-003 visuelle Regression, app-weiter Sentence-Sweep) ·
CHANGELOG. Schnell-Gates ✓ (portability-Kette inkl. neuem Gate grün!).
✅ **LOOP 11 TEIL 1 KOMPLETT**: PR #470 → Suite grün → Review-Agent: 4
Entwarnungen (Blast-Radius [Condensed schmaler, kein Layout-Bruch;
truncate-Stelle profitiert], Tailwind-Kollision [unlayered gewinnt, kein
--font-display-Theme-Key], Sentence-Texte korrekt, Dark-Werte plausibel)
+ 1 Finding conf 90: Regex-Hex-Zwang ließ non-hex-Fallbacks KOMPLETT
durchrutschen → [^)]*-Fix + damit gefangener ECHTBEFUND --status-yellow-soft
(nie definiert, transparent griff immer) → Token definiert Light #FFF8E1 /
Dark #4A3F1F. Fix-Commit 59ab28e → Suite grün → Ist-Check CLEAN →
**gemergt (9463f4bf) + deployt (success 03:10)**. Kais TG 10056 (Merge Nr. 8).

## 🔨 LOOP 12 PR 1 GEBAUT (~03:30, uncommitted auf Branch loop11-teil1-token-audit — beim Commit FRISCHER Branch ab main!)

buildPresentationPackage in canonical-export.ts (pure: overview-Folie
[Zähler+alle IDs] · top_differences nach |Δ| desc mit localeCompare-Tiebreak
· data_quality/actions NUR wenn Inhalte [Gegenprobe: nichts fabriziert] ·
jede Folie mit difference_/finding_/action_ids [Exit-Kriterium]) +
populatedArea('presentation_package', dedupte difference_ids, praesentation)
in buildCanonicalExport + 4 Tests (Referenz-Integrität gegen die Sammlungen
· Top-N-Determinismus · Gegenprobe · Schema-Validität mit befülltem
package) = 20/20 · typecheck ✓ · CHANGELOG + program-status Loop 12
„begonnen". ✅ **LOOP 12 PR 1 KOMPLETT**: Gates grün → PR #471 → Kurz-Review: 2
Entwarnungen (NaN unerreichbar via decimalString-Garantie · Slide-Duplikate
schema-belegt konform, uniqueItems fehlt bewusst) + 1 moderates Finding
(localeCompare ohne Locale = Host-Locale-abhängig, Azure-Risiko) → ordinaler
Tiebreak-Fix eef291d → Suite grün → **gemergt (b24e1015)**, Deploy-Wache
läuft. **⚠ SELBSTKORREKTUR: Ist-Check und Merge in EINEM Befehl gebündelt
(Regel-Verstoß never-bundle-status-check-and-merge — Ausgang korrekt,
Gate nur formal; ab jetzt wieder STRIKT getrennte Befehle!).**

**PR 2 GEBAUT (~04:00, uncommitted auf Branch loop12-pr1-… — beim Commit
FRISCHER Branch ab main!):** KEIN neuer pptxgenjs-Code — Wiedergabe-Kette:
exportQafCanonicalJson (existiert) → presentation_package.slides →
lib/export/qaf-presentation-adapter.ts (pure: Slides 1:1 → ExportConfig;
decimal-Strings WÖRTLICH, fehlend='—' nie fabriziert, unbekannte kinds =
ehrlicher Hinweis; Kennung IMMER erste Spalte) → bestehender exportToPptx →
qaf-v2-pptx-export.tsx (Insel mit try/catch + revoke-Timing + DOM-append
[die 1b-Lehren]) → Button auf V2-Seite neben Canonical-Export. 4 Adapter-
Tests ✓ typecheck ✓ lint ✓. CHANGELOG + program-status Loop 12 „teilweise"
(offen benannt: Export-Profil-Versionierung/Angleichung = Loop-6-Etappe 3).
✅ **PUNKT 4 KOMPLETT (04:31)**: PR #472 gemergt (fc073157, Ist-Check/Merge
GETRENNT ✓) + deployt. Review-Zyklus: Kernfund bare-catch mit falscher
„Server-Log"-Behauptung → Details in UI-Meldung (no-console-Regel; erst
console.error versucht → Lint-Rot wurde von tail-Pipe MASKIERT und ging
in einen Push [pipefail-Falle in neuer Form! PIPESTATUS nutzen] → im
Folge-Commit regelkonform gefixt, Remote verifiziert 4995dcc) + Adapter
verwirft Nicht-Objekt-Zeilen sichtbar. Kais TG 10058: Punkt-4-Meldung +
**LOOP-13-PRODUKTFRAGEN GESTELLT (unblockierend): (1) BMW-Daten an
externen LLM erlaubt? Welcher Dienst? (2) Budget/Modell-Klasse?** —
Loop 13 liegt SAUBER bis zu seinem Entscheid (ai_ready ist fertig,
KEINE eigenmächtige LLM-Anbindung!).

✅ **LOOP 14 KOMPLETT (05:31)**: PR #473 gemergt (eb10f17, Ist-Check/Merge
getrennt ✓) + deployt + Kais gemeldet (TG 10059, Merge Nr. 11).
CI-Lauf 1 war ROT (eigener Fehler: Profilnamen im Kommentar → R-01
forbidden-strings; Nachlauf-Gates zu schmal) → Fix 9722ddc, Memory
late-edit-rerun-matching-gates. Fahrplan-Punkt 5: Loop 14 ✅, Loop 13
wartet auf Kais-Entscheide (TG 10058) — Punkt 5 damit so weit wie
unblockiert möglich.

✅ **ETAPPE 2a KOMPLETT (06:40)**: PR #474 gemergt (1ccdd97, Ist-Check/
Merge getrennt ✓) + deployt + Kais gemeldet (TG 10062, Merge Nr. 12).
CI-Lauf 1 durchweg grün. Review fand ERSTMALS kein Critical/Major.

## ➡️ LAUFEND: PR #475 (Loop 5 Teil 2 Reparse) — CI-Wache btlj2yp6v

Commit 1dacb90 gepusht+verifiziert. Review (Silent-Failure-Hunter,
7 MAJOR / 0 CRITICAL) EINGEARBEITET: logger.error-Argfolge (Kontext war
im err-Slot — Muster existiert ~15× im Bestand, NUR meine Stelle
gefixt!) · try/catch Action+Insel (SDK wirft für unerwartete Klassen;
keine error.tsx unter qaf-differences → wäre Full-Page-Error) ·
Doku-Claims auf bewiesene Abdeckung präzisiert · 4 geerbte Risiken in
program-status benannt (Replace-Race last-write-wins [Advisory-Lock =
Folge-Arbeit], Replace-Testlücke repo-weit, Audit-Ununterscheidbarkeit,
compensate ungetestet). Gates nach Einarbeitung KOMPLETT neu: 7205/152sk
+ build + portability + lint grün.

✅ #475 GEMERGT (26d5a9e) + deployt (07:46) + Kais gemeldet (TG 10067,
Merge Nr. 13).

⚠️ **„Drift-Fix-PR (KLEIN)" IST GESTRICHEN — WAR EINE FALLE:** Das
refreshPlausibility-Weglassen an den 3 UI-Stellen ist BEWUSSTES
KAR-899-Design (Doku am Options-Typ ~Z. 2793: Issues nicht
matching-abhängig, aus last-write-wins qaf_part NICHT re-derivierbar —
Flag-Flip würde Issues VERFÄLSCHEN). Realer Rest-Drift = Ampel liest
identityChecksComplete allein vom Stempel → sauberer Fix ist
DESIGN-ARBEIT an der Ampel-Herleitung (in program-status Loop 5
präzisiert, Doku-Änderung noch UNCOMMITTET auf Branch
qaf-v2/loop5-teil2-reparse-Nachfolger — in nächsten PR mitnehmen).

✅ #476 GEMERGT (da97c019, Doku KAR-899-Präzisierung; Deploy-Wache
bho71e13t läuft — Doku-only, keine Kais-Einzelmeldung; Merge Nr. 14).
#120-Wache erneuert (bw80mk7zt, still, 6h).

✅ **ETAPPE 3 KOMPLETT (08:53)**: #477 gemergt (00c80a6, getrennt ✓) +
deployt. Merge Nr. 15. Kais Sammel-Meldung TG 10068 (Guten-Morgen-Stand
15 Merges + #476-Doku + **unblockierende Tab-Frage: echte Reiter wie
altes Tool ODER Sprungmarken? Foto/Satz genügt**).

## ➡️ LAUFEND: Loop-8/10-Tab-Analyse-Agent

Agent kartiert: program-status/Roadmap/RTM Loop 8+10 wörtlich ·
V11-Referenz-Navigation (verbindliche Reiter-Liste?) · bestehende
Tab-/Anker-Bausteine im Repo · Capability-Gate („Tab nur wenn Bereich
abgedeckt" aus coverageFromPersistedMatrix) · kleinster Schnitt +
Fallen (URL-State vs. useState, Deep-Links, Ladefehler-Banner immer
sichtbar, Loop-10-Zerlegung nicht komplett vorziehen). **Kais-Antwort
auf Tab-Frage (TG 10068) VOR Bau-Beginn einarbeiten, falls eingetroffen
— visuelle Richtungsentscheidung ist seine** (Memory:
ask-for-reference-before-visual-rework). Danach Zuschnitt → Zyklus.
Offen bei Kais: Tab-Frage (TG 10068) · Loop-13-Entscheide (TG 10058) ·
Roh-Excel (TG 10050) · #120 (Wache bw80mk7zt) · lokale branch -D.

## (erledigt) 📐 ETAPPE-3-ZUSCHNITT-ANALYSE

Memo-Kern (Analyse-Agent, vollständig im Verlauf): V2-Seite und
exportQafCanonicalJson duplizieren SPALTENIDENTISCH die 7 Selects +
buildOverviewRun/differencesFromMfg/Mat/buildCatalog (page.tsx:85-244
vs. actions.ts:5039-5090,5143-5167). Schnitt:
1. **app/qaf-differences/composition.ts (NEU, OHNE 'use server'!):**
   loadComparisonComposition(supabase, comparisonId) → Selects +
   Producer EINMAL; Fehler je Quelle STRUKTURIERT zurück (Seite→Banner
   [#466-Härtung erhalten!], Export→Abbruch — Semantiken NICHT
   vereinheitlichen!). KEIN sha256/byteSize darin (Export-exklusiv,
   on-Klick). In actions.ts NICHT exportieren ('use server' macht
   alles zur Action!).
2. Seite behält Presentation-Producer on top (buckets/bridge/partition/
   tables/coverage/comparability) — kanonische analysis_areas sind erst
   6/24 befüllt, „Seite liest Areas" ist HEUTE nicht möglich (ehrlich
   dokumentieren).
3. buildViewModelExport (Klassik) BLEIBT — Paritätsregel.
4. Parität strukturell (eine Composition, TypeScript hält sync) — kein
   A/B-Zahlenvergleich (Repo-Muster).
5. FALLEN: decimalString≠moneyFormatter (keine zweite Rundungsschicht!)
   · kein Storage-Download je GET · Loop-10-Tab-Territorium NICHT
   betreten · beide Aufrufer haben review-gehärtete Fehlerpfade — EXAKT
   erhalten.
Danach: Loop 8 Teil 2/Loop 10 Tabs (eigene Analyse). Offen bei Kais:
Loop-13-Entscheide (TG 10058) · Roh-Excel (TG 10050) · #120 (unapplied,
keine Reminder) · lokale branch -D-Sammlung.

## (alter Stand) Loop 5 Teil 2 Bau-Notizen

**Zuschnitt-Entscheidung:** In-Place-Reparse verworfen (900 Zeilen
Parse-Glue = Duplikations-/Extraktionsrisiko). Stattdessen:
**Reparse = replaceComparisonFile mit Bucket-KOPIE der eigenen
Originaldatei** (erbt Parse-vor-Schreiben, Guards, Carry-Forward,
insert-first-Rollback, refreshPlausibility:true). KRITISCH selbst
gefunden: replace-compensate REMOVED upload.path → mit Original-Pfad
hätte Fehlschlag das ORIGINAL GELÖSCHT → deshalb Kopie (Original in
jedem Ausgang unantastbar). Gebaut: reparseComparisonFile (Gates:
summary-only [G60/Multi-QAF deferred], Legacy-Meldung, Prefix-Guard;
Nachräumen der Kopie bei !ok) · qaf-v2-reparse.tsx (zweistufig) ·
Seiten-Einbau mit gespiegeltem Gate · Register-Zeile (storage.copy/
remove) · 5 Tests (Kern-Gegenprobe: JEDER remove trifft NUR die Kopie)
· program-status Loop 5 Teil 2 + benannter Bestands-Befund
(recompare-Stempel/Zeilen-Drift bei refreshPlausibility-Weglassung —
NICHT gefixt, dokumentiert) · CHANGELOG. Typecheck grün, 5/5 Tests grün.

**Nach Review+Gates:** Findings einarbeiten (dann VOLLES Set neu!) →
frischer Branch ab main → Commit (-F!) → PR → Wache → getrennter
Ist-Check → Merge → Deploy → Kais (Merge Nr. 13).

## Erledigt-Protokoll PR #474 (Kurz)

**Etappe 2a GEBAUT** (709cfe3, gepusht+verifiziert): canonical-graph.ts
(pure) — part-Entities + same_part_as + SUM-Facts mit Zell-Provenienz
(sheet=Schema-Pflicht → Seiten ohne sheet withheld in extensions, nie
erfunden; MFG/MAT=Etappe 2b, DB-Lücke sheet-Spalte) + raw_value exakt /
normalized_value nur bei ableitbarer Einheit + value_state-Mapping
exhaustiv + base/comparison_fact_ids + entity_graph_integrity-Check
(validateCanonicalGraph inkl. ID-Dedup). mapping_groups begründet leer
(grenzt an deferred Multi-QAF). Analyse-Memo-Fallen alle umschifft
(ai-ready-Fact-Kollision, metric_id-Format summary.*, keine
Parallel-Registry). Review-Agent: KEIN Critical/Major; 3 MINORs
eingearbeitet (relationship_id-Dedup, 2 Testlücken → 11 neue Tests).
Mutations-Probe: Mapping verfälscht → exakt 3 rot. Finale Gates auf
ENDSTAND komplett neu (Lektion!): Suite 7200/152sk + build + portability
strict + lint ALLE grün. pipefail-Zwischenfall: `npm test | tail` im
Background maskierte Exit → Wiederholung ungepiped (Marker-Muster).
program-status: Loop-6/12-PR-Felder + Tabelle nachgezogen.

**Nach Suite grün:** getrennter Ist-Check → Merge (--delete-branch;
Achtung: gh wechselt lokal auf main!) → Verify → deploy-sha-watch →
Kais-Meldung (Merge Nr. 12). Danach nächster Loop-Rest (Kandidaten:
Loop 5 Teil 2 Reparse/SRC-002 · Loop 6 Etappe 3 ViewModel-Parität ·
Loop 8 Teil 2/Loop 10 Tabs) — je Analyse zuerst.
Offen bei Kais: Loop-13-Entscheide (TG 10058) · Roh-Excel (TG 10050) ·
#120 (unapplied, KEINE Reminder) · lokale branch -D-Sammlung.

**Loop-14-Kernbefund (Analyse-Agent):** Brücke QVS P1–P7 war VOR dem
Loop-Programm gebaut, flag-on produktiv seit 18.07. (P1–P5=KAR-970–974,
Flag-on P7=KAR-976) → Loop 14 = Verifikation statt Neubau. PR #473
(2c0b16e, gepusht+verifiziert): V2-Seite „Als Wertstrom übernehmen" in
Klassik-Parität (isQvsEligibleComparisonMode summary/g60, Multi-QAF-Pfad
NICHT berührt) + comparisonError-Throw + Loop-Report mit 4/4
Exit-Kriterien-Nachweis + RTM-Widerspruch dokumentiert (keine der 9 IDs
zeigt auf die Brücke; VSM-001 an Loop 15) + program-status-Tabelle an
Detailsektionen angeglichen (Drift seit Loop 4) + Deploy-Protokoll
#463–#472 + qaf-actions-Kommentar (flag-on). Review-Agent VOR PR: 3
Findings (KAR-Bereich falsch · RTM-Aussage 5/9 · Tabellen-Widerspruch)
alle eingearbeitet. Gates lokal: Suite 7189/152sk + build + portability
strict + lint-baseline + Nachlauf grün. Commit-Falle notiert: „…" in
Commit-Message bricht Shell-Quoting → IMMER -F Datei.

**Nach Suite grün:** getrennter Ist-Check → Merge (--delete-branch) →
Verify → deploy-sha-watch → Kais-Meldung „Loop 14 fertig" (Merge Nr. 11;
Loop 13 wartet auf seine LLM-Entscheide TG 10058 — Punkt 5 dann komplett).
Danach: Punkt 6 (Loop 7 NUR Kais-Go) · Punkt 7 (Loop 15 Abschaltung).
Offen bei Kais: Loop-13-Fragen (TG 10058) · Roh-Excel (TG 10050) · #120
(REST-Check 04:47: weiter unapplied, KEINE Reminder) · lokale branch -D. DANACH Punkt 5 (Loop 13+14) ·
6 (Loop 7 nur Kais-Go) · 7 (Loop 15). Offen: #120-Wache läuft (bqujm4rmn) ·
Roh-Excel-Frage TG 10050 · Action-Test-Nachrüstung · Etappe 2/3 Loop 6.

## ➡️ DANACH: Fahrplan Punkt 4 (Loop 11 Design + Loop 12 Exporte)

Zuschnitt (aus Design-Memo Etappe 1b+3-Vorarbeit): (1) V2-Seiten-Komposition
(Selects→differencesFromMfg/Mat→buildCatalog→run/summaryDiffs-Ableitungen)
als lib-Funktion extrahieren (z.B. internal/v2-run-from-rows.ts — Server-
tauglich, nimmt die geladenen Zeilen als Input, PURE), Seite ruft sie;
(2) Server-Action exportQafCanonicalJson(comparisonId): lädt dieselben
Zeilen + qaf_part + storage-Bytes (sha256/byteSize via Admin-download —
Register-Eintrag!), baut CanonicalExportInput aus der EINEN Komposition,
contentHash über Platzhalter-Dokument (sha256 hex) → withCanonicalContentHash
→ Download als JSON; (3) V2-Seite bekommt „Kanonischer Export (JSON)"-Knopf
(Client-Insel wie source-download). Metric-IDs: SUM via metricKeyByTuple-Map
aus differencesFromSummary/overview-run — prüfen was exponiert ist.
Danach: Fahrplan-Punkt 4 (Loop 11 Design + Loop 12 Exporte/Präsentation) ·
5 (13 KI + 14 Wertstrom) · 6 (Loop 7 NUR Kais-Go) · 7 (Loop 15). Offen bei
Kais: #120-Apply (still prüfen) · Roh-Excel-Produktfrage (TG 10050) ·
Referenzseiten-Abschaltung · lokale branch -D. Fahrplan danach: Punkt 4 (Loop 11+12), 5 (13+14), 6 (Loop 7 nur
Kais-Go), 7 (Loop 15). #120 still per REST prüfen bei Gelegenheit
(letzter Stand: unapplied).

**Fahrplan an Kais gesendet (TG 10038), Reihenfolge:** (1) Zusammengeführte Ansicht
(V1-Optik + V2-Substanz: Überblick/Kacheln/Brücke oben, Differenz-Tabellen NUR mit
echten Abweichungen + Klappzeile „N unverändert", Kennungen+Fundstellen bleiben;
zahlt auf Loop 9+10) → (2) Loop 4 Klassifikation + Loop 8 Capability-Navigation →
(3) Loop 5+6 Source-Artifact/JSON-Vertrag → (4) Loop 11+12 Design+Exporte →
(5) Loop 13 KI + Loop 14 Wertstrom → (6) Loop 7 Multi-QAF (deferred, nur auf sein Go)
→ (7) Loop 15 V1-Abschaltung zuletzt. **Offene Frage an ihn: nach Punkt 1
automatisch weitermachen?** (Antwort abwarten; Punkt 1 selbst IST freigegeben.)

**Design-Fakten für Punkt 1 (aus den PDFs):** V1 zeigt: Überblick-Karte mit
Identität (Lieferant Hanon Systems, Variante KF1 — die Klassik-Seite HAT
Identitätsdaten! Quelle prüfen: qaf_part?), 3 KPI-Kacheln, Form-Ansicht (6 Positionen),
Bucket-Balken, Kostenbrücke (Waterfall mit Residuum), Kennzahlen-Tabelle NUR
Veränderte mit Status-Badges. V2 zeigt: ehrliche Badges, Vergleichsart+Evidenz,
dünne Preisbrücke, U-08-Tabellen MFG (95 Zeilen, fast alle Δ 0,00 — probable_match-
Review-Zeilen!) + MAT (27, mit echten Effekten). Kais' JSON/Backend-Wunsch: Filter
NUR in der Ansicht, Katalog/Persistenz/Export unverändert vollständig.

**Nur bei Kais:** #120-Apply (Wache läuft) · Referenzseiten-Abschaltung (= Loop 15,
später) · offene-entscheidungen-kais.md · lokale Branch-Refs (branch -D braucht Ok).

## ➡️ KETTE KOMPLETT — nächste Session startet hier

**Das ganze PO-Programm „QAF-Vergleich komplett" ist durch** (6/6 Blöcke; Block 5
= Smoke erledigt, inhaltlicher UI-Blick bei Kais). Nächster Schritt = NEUER LOOP
(Bauplan oben): Branch ab main, D16 identity_mismatch → D15 filename_mismatch →
D17 make_or_buy_indication (PR 1, Named-Config-Schwelle, HICE-Strip-Prozess
erwartet), dann R-03 comparison_mode (PR 2). Dabei mitnehmen: das untracked
Deploy-Protokoll + lokale Branch-Refs kriterium2/mat/nachtrag löschen (Kais-Ok
für `branch -D` nötig, Hook). Offen bei Kais: UI-Blick + Recompare (füllt MAT),
Referenzseiten-Abschaltung, offene-entscheidungen-kais.md, D15–D17-Interpretation
korrigierbar.

**Danach offen:** (b) NEUER LOOP D16→D15→D17 (PR 1) + R-03 (PR 2) nach Bauplan oben —
Kais' „flexibel"-Linie beachtet: weiche Befunde, keine Gates. (c) Alt-Punkte: UI-Blick,
Referenzseiten-Abschaltung, offene-entscheidungen-kais.md.
