# SupplierDev — Product Specification

> **Product**: SupplierDev  
> **Purpose**: BMW Supplier Development platform for cycle time analysis, shift output tracking, and continuous improvement management during supplier plant visits.  
> **Platforms**: Web (Next.js), iOS (SwiftUI)  
> **Backend**: Supabase (PostgreSQL + Auth + Storage)

---

## 1. Domain Overview

BMW Supplier Development engineers visit supplier manufacturing plants to:
1. **Measure** cycle times at each production station (time study)
2. **Identify** bottlenecks vs. customer takt time
3. **Track** shift production (target vs. actual output)
4. **Drive** continuous improvement via workshop actions
5. **Compare** QAF (Quality Assessment File) versions over time
6. **Report** findings as structured Excel exports

---

## 2. Core Entities

### 2.1 Visit
Represents a single supplier plant visit/audit session.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | UUID | auto | Primary key |
| `user_id` | UUID | auto | Owner (FK → auth.users) |
| `supplier_name` | string | ✓ | Supplier company name |
| `plant_location` | string | — | Plant city/address |
| `product_name` | string | — | Product being manufactured |
| `visit_date` | date | ✓ | Date of visit (default: today) |
| `customer_takt_time_sec` | decimal | — | Customer demand rate (seconds) |
| `planned_oee` | decimal | — | Target OEE % (0–100, default: 85) |
| `target_cycle_time_sec` | decimal | — | Manufacturing target CT (seconds) |
| `notes` | text | — | Free-form notes |
| `created_at` | timestamp | auto | Creation timestamp |
| `updated_at` | timestamp | auto | Last update timestamp |

**Business rules:**
- A user can only access their own visits (RLS)
- `target_cycle_time_sec` is typically calculated as `customer_takt_time_sec × planned_oee / 100`
- Visits are the root entity; all other entities cascade-delete with it
- A visit must have at least one station before cycle time measurement can start

**Relationships:**
- Has many → Stations (1:N)
- Has many → ShiftRecords (1:N)
- Has many → Actions (1:N)
- Has many → QAFDocuments (1:N)

---

### 2.2 Station
A manufacturing workstation or process step within a visit.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | UUID | auto | Primary key |
| `visit_id` | UUID | ✓ | Parent visit (FK) |
| `step_number` | integer | ✓ | Sequential identifier |
| `station_name` | string | ✓ | Station name (e.g. "M0500", "Endmontage") |
| `description` | text | — | Optional description |
| `planned_cycle_time_sec` | decimal | — | Expected cycle time |
| `is_manual` | boolean | ✓ | Manual vs. automated station |
| `operator_count` | integer | ✓ | Number of operators (default: 1) |
| `sort_order` | integer | ✓ | Display order (default: 0) |
| `created_at` | timestamp | auto | |

**Business rules:**
- `sort_order` controls the station sequence in the Yamazumi chart
- Stations can be reordered by the user
- A station is the **bottleneck** if its average measured CT ≥ all other stations' average CT
- A station is **over-takt** if its average CT > `customer_takt_time_sec`

**Relationships:**
- Belongs to → Visit
- Has many → Measurements (1:N)

---

### 2.3 Measurement
An individual cycle time recording for a station.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | UUID | auto | Primary key |
| `station_id` | UUID | ✓ | Parent station (FK) |
| `cycle_number` | integer | ✓ | Sequential measurement number (1-based) |
| `cycle_time_sec` | decimal | ✓ | Measured cycle duration (seconds) |
| `is_outlier` | boolean | ✓ | Excluded from averages (default: false) |
| `notes` | text | — | Observation notes for this cycle |
| `measured_at` | timestamp | auto | Recording timestamp |

**Business rules:**
- Outliers are excluded from avg/min/max calculations
- Target is typically 10 cycles per station for statistical validity
- `cycle_number` is assigned in sequence; renumber if a measurement is deleted
- Average is calculated over non-outlier measurements only

**Relationships:**
- Belongs to → Station

---

### 2.4 ShiftRecord
Hourly production output for a specific shift.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | UUID | auto | Primary key |
| `visit_id` | UUID | ✓ | Parent visit (FK) |
| `shift_date` | date | ✓ | Date of shift |
| `shift_type` | enum | ✓ | `Frühschicht` \| `Spätschicht` \| `Nachtschicht` |
| `hour_start` | integer | ✓ | Hour (0–23) |
| `target_output` | integer | ✓ | Planned units for this hour |
| `actual_output` | integer | ✓ | Actual units produced |
| `remarks` | text | — | Notes/anomaly description |
| `created_at` | timestamp | auto | |

**Business rules:**
- One record per shift-date + shift_type + hour_start combination (upsert)
- `delta = actual_output - target_output` (negative = shortfall)
- A shift where all hours meet target but total is low = "hidden loss" warning
- Target output typically derived from takt time: `3600 / customer_takt_time_sec`

**Relationships:**
- Belongs to → Visit

---

### 2.5 Action
A continuous improvement action item identified during the visit.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | UUID | auto | Primary key |
| `visit_id` | UUID | ✓ | Parent visit (FK) |
| `action_number` | integer | ✓ | Display number (1-based, auto-assigned) |
| `area` | string | ✓ | Process area / station reference |
| `description` | string | ✓ | What needs to be done |
| `effort` | enum | ✓ | `low` \| `medium` \| `high` (default: `medium`) |
| `benefit` | enum | ✓ | `low` \| `medium` \| `high` (default: `medium`) |
| `status` | enum | ✓ | `open` \| `in_progress` \| `done` \| `rejected` |
| `responsible` | string | — | Assignee name |
| `target_date` | date | — | Expected completion date |
| `time_saving_sec` | decimal | — | Estimated cycle time reduction (seconds) |
| `cost_saving_eur` | decimal | — | Estimated cost savings (EUR) |
| `created_at` | timestamp | auto | |

**Business rules:**
- Status lifecycle: `open` → `in_progress` → `done` or `rejected`
- Actions are ranked by effort/benefit matrix (low effort + high benefit = quick win)
- `action_number` is sequential per visit; renumber if deleted

**Relationships:**
- Belongs to → Visit

---

### 2.6 QAFDocument
A versioned Quality Assessment File upload.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | UUID | auto | Primary key |
| `visit_id` | UUID | ✓ | Parent visit (FK) |
| `version_type` | enum | ✓ | `zur_vergabe` \| `nach_vergabe` \| `aktuell` |
| `file_name` | string | ✓ | Original filename |
| `file_path` | string | ✓ | Supabase Storage path |
| `parsed_data` | jsonb | — | Extracted fields (CT, OEE, costs, reject rate) |
| `uploaded_at` | timestamp | auto | |

**Enum meanings:**
- `zur_vergabe` — At time of supplier award (baseline)
- `nach_vergabe` — After award / during ramp-up
- `aktuell` — Current state

**Business rules:**
- Only one document per version_type per visit (upsert on re-upload)
- `parsed_data` stores: `cycle_time_sec`, `oee_percent`, `cost_per_unit`, `reject_rate_percent`
- Delta comparison is shown when ≥2 versions are present

**Relationships:**
- Belongs to → Visit

---

## 3. User (Auth)

Managed by Supabase Auth. Extended profile stored in `auth.users.user_metadata`:

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Display name |
| `department` | string | BMW department (e.g. "Supplier Development", "Einkauf") |
| `language` | enum | `en` \| `de` |
| `avatar_url` | string | Supabase Storage URL |

---

## 4. Business Rules Summary

### Bottleneck Detection
- Bottleneck = station with highest non-outlier average CT
- Highlighted red if avg CT > `customer_takt_time_sec`
- Highlighted yellow if avg CT > `target_cycle_time_sec` but ≤ `customer_takt_time_sec`
- Highlighted green if avg CT ≤ `target_cycle_time_sec`

### Takt Time Logic
- `customer_takt_time_sec` = demand rate (seconds per unit)
- `target_cycle_time_sec` = `customer_takt_time_sec × planned_oee/100`
- Yamazumi chart shows all stations stacked against these reference lines

### Cycle Time Validity
- Minimum 3 measurements for meaningful average
- 10 measurements is the standard for reliable time study
- Outlier threshold: typically ±2σ from mean (manual flag in current version)

---

## 5. Feature Set

### Authentication
- Email/password sign up + confirmation
- Login / logout
- Password reset via email
- Profile management (name, department, language, avatar)

### Visit Management
- Create, read, update, delete visits
- Dashboard list with status overview
- Bottleneck + KPI strip per visit

### Project Overview — Modul-Kennzahlen (KAR-983)
- On `/project/[id]`, a "Kennzahlen" section shows the state of every module of the
  project at a glance — generic for any project type, not gated to a specific type or
  to demo projects:
  - **Fabrikanalyse**: fulfillment score (average `selected_rating` 1–4 of answered,
    relevant `assessment_responses`, aggregated across all `assessments` of the
    project), share fulfilled (rating 3–4) / partial (2) / not fulfilled (1), count of
    answered questions.
  - **QAF**: the newest `qaf_comparison`, core cost impact (total production cost /
    quotation price / material, each as Δ%) read straight from the persisted
    `qaf_summary_diff` rows via the existing summary-KPI builder — never
    recomputed. A comparison mode that never persists summary-diff rows (e.g.
    Multi-QAF) shows an explicit "no core metrics available" note instead of a wrong
    or empty number.
  - **Wertstrom**: the newest `value_stream_maps` row of the project — lead time
    (Durchlaufzeit), value-added ratio, bottleneck station — derived via the existing
    vaClass-aware VSM metric helpers (QVS-P4), not a bespoke recomputation.
  - **Maßnahmen**: a combined counter across `lsc_measures` and `workshop_actions`
    (each table keeps its own status vocabulary) with three visible buckets —
    open / in progress / done; the total equals the sum of these buckets
    (rejected workshop actions are not counted as measure stock).
  - Demo state: no badge of its own — the sticky project header already renders
    the demo badge for every `/project/[id]` route.
  - Each tile links into its own module (Fabrikanalyse, QAF, Wertstrom, Maßnahmen) and
    degrades independently to a clean "no data" tile — never a crash or an empty gap —
    when the project has nothing yet for that module.
- The project's `order_reason` ("Ausgangslage") and `order_objective` ("Ziele") fields
  are rendered as their own labeled sub-section in the Projektdetails card when set;
  hidden entirely when both are empty.
- **iOS scope**: web-only for this PR; the underlying tables are already
  PostgREST-readable, no new API surface.

### Cycle Time Study
- Stopwatch with requestAnimationFrame precision
- Multi-station measurement (station picker)
- Inline edit/delete measurements
- Outlier flagging
- Notes per cycle
- Progress indicator (target: 10 cycles)
- Excel template import/export

### Takt Diagram
- Yamazumi chart (all stations)
- Cycle detail chart (per station, drill-down)
- Reference lines: customer takt, target CT
- Bottleneck highlighting

### Shift Output Tracking
- Hourly Soll/Ist recording per shift
- 3 shift types (German system)
- Delta calculation per hour
- Hidden-loss detection

### Workshop Actions
- Create/edit/delete actions
- Effort/benefit matrix (3×3)
- Status lifecycle management
- Time + cost saving estimates
- Responsible + target date

### QAF Comparison
- Upload 3 document versions
- Auto-parse cycle time, OEE, cost, reject rate
- Side-by-side delta comparison

### QAF-Differences (SupplierPulse) — auditable diff engine
- **Distinct from the per-visit QAF Comparison above:** a standalone top-level page
  (`/qaf-differences`) that batch-compares full QAF cost documents of the **same part
  number** across quotation rounds, not a single visit's 3 versions.
- Multi-file `.xlsx` upload, grouped strictly by part number; per-file parse errors
  are isolated and reported rather than failing the batch.
- 5-stage process-step matching cascade, field-level cost diffs, structure-change
  detection (new/removed/renamed steps), plausibility checks, and root-cause drivers.
- Every comparison persists an **auditable trail** (engine + parser version, baseline
  selection, step matches, diffs, plausibility issues, audit log) in the `qaf_*` tables.
- Reference-quality 8-sheet Excel export, downloadable from the comparison detail
  page (KAR-840 PR1).
- **Summary-metric engine (KAR-840):** 19 canonical money metrics extracted from the
  Zusammenfassung/SUMMARY sheet with A1-cell provenance, diffed ALT→NEU and persisted
  to `qaf_summary_metric`/`qaf_summary_diff`.
- **Comparison detail view — V11-parity analysis sections (KAR-840):**
  - KPI tiles (quotation price / material / total production costs, Δ % + amounts)
  - QAF form view (the six Zusammenfassung positions, Δ % per line)
  - Cost-structure bucket chart (Material/Manufacturing/Scrap/SG&A, ALT vs. NEU,
    toggleable legend, Δ markers)
  - Cost bridge waterfall (per-metric floating steps, residual, NEU composition)
  - Metrics comparison table with per-value source cells and engine status
  - Tornado "biggest price movers" (top-N filter; per-step fk deltas, or a
    comparison-object fallback for summary-only QAFs; uncertain-match caveat)
  - Negotiation quick wins & big bringers (top-3 cost decreases/increases)
  - Negotiation levers with fixed asks (process-KPI deltas ≥5 %, uncertain-match caveat)
  - Interactive projection ("Hochrechnung & Potenzial in €"): editable year/volume
    table, Δ-per-unit override, defend-quota slider, per-year + cumulative chart;
    inputs debounced-autosave per comparison (KAR-985 — see Autosave note below)
  - One-time payments table (development / special tooling / total)
- **Preis-Kalkulator (KAR-847/848):** a standalone route per comparison
  (`/qaf-differences/[id]/kalkulator`, fixed 1:1 to its comparison) turns manual or
  ALT/NEU-adopted process values (cycle time, parts/cycle, headcount, labour/machine
  rate, inefficiency, material, scrap, SG&A + profit on manufacturing and material)
  into a live per-part price with the same formula as the G60 QAF. Values can be
  filled per click from the ALT or NEU side of the bound comparison (whole set or
  per-field) or from the stopwatch average cycle time. Manual field inputs
  debounced-autosave per comparison (KAR-985 — see Autosave note below).
- **Autosave (KAR-985):** the projection and calculator inputs above persist into
  an additive `qaf_comparison.user_inputs` JSONB namespace bag (`{projection,
  calculator}`, same pattern as `qaf_file.g60_meta`) via a ~800 ms debounced
  save — no explicit Save button, a small "Gespeichert"/"…speichert" indicator is
  the only UI signal. Failsoft by design: any DB error (e.g. the migration not yet
  applied) degrades to a quiet "Speichern derzeit nicht verfügbar" hint (save) or
  "nothing saved yet" (load) — the page stays fully usable either way. This does
  **not** apply to `qaf-g60-detail.tsx`'s own projection instance (the G60 detail
  comparison view), which stays session-only as before.
- **QAF upload transport:** the browser uploads workbooks (≤20 MB each, ≤12 per
  batch) directly to the private `qaf-uploads` storage bucket via signed URLs;
  analysis reads from storage server-side. Originals stay stored for re-ingest.
  iOS must use the same two-step flow (request upload targets → PUT to storage →
  trigger analysis) — multipart uploads to the API will hit platform body limits.
- **G60 detail-QAF comparison (KAR-840 G60 track):** `.xlsm` detail workbooks are
  auto-detected, persisted structurally (cost tabs, INPUT rate card, volumes —
  migration #105) and compared as whole files (`comparison_mode='g60'`, upload
  order = ALT/NEU with baseline review, cross-batch pairing). The G60 detail view
  shows: headline KPIs (total quotation delta, global machine-rate lever, tab
  count with PART-SPECIFIC outliers), six-bucket cost structure + bridge,
  per-tab metric table (11 metrics, PART-SPECIFIC flags), tornado per cost tab,
  production index (ALT=100, log/linear), anomalies & drivers from the INPUT
  card (CMEAN clear names), the three G60 negotiation levers, and the projection
  pre-filled with the QAF's real yearly volumes.
- **G60 scenario editor (KAR-840 G4):** live what-if on the NEU quotation —
  global lever factors (machine rate, labour, cycle time, staffing, material,
  inefficiency removal) plus a per-tab, per-row process editor (cycle/emp/
  labour rate/inefficiency/MSS/scrap/material). All figures recompute
  client-side through the delta-anchored pure engine (no overrides = persisted
  analysis exactly); outcome shows Potential vs. Bestandsaufnahme per bucket,
  Δ per piece and total savings over the QAF volumes. Session-only state (V1).
- **Multi-QAF container comparison (KAR-947, `comparison_mode='multi_qaf'`):** a
  Multi-QAF workbook packs several product variants (each with its own dimensions,
  annual/peak/lifetime volumes, and an active/inactive/reserved state) into one file.
  Auto-detected at upload time (`multiQafDetection`, default **ON** since KAR-925 —
  Kais' go, TG 8475; a comparison persisted before that flip keeps its own
  pre-flip detection config on file-replace, so its behavior never silently changes).
  Comparing two such containers produces: a composite-identity-based variant match
  (with a manual per-variant override — confirm/reject a match, with a note; set or
  cleared, each immediately recomputes the persisted result), a container-structure
  diff, a material-matrix diff, a per-variant summary-metrics profile diff, and an
  aggregate volume-weighted cost impact. Every section is Result-version-tolerant: a
  comparison computed before a given sub-feature shipped shows an explicit "not
  available for this comparison" notice, never a silently-empty section that could be
  misread as "no differences".
- **Variante↔Standard-QAF-Vergleich (KAR-948, `comparison_mode=
  'multi_qaf_variant_vs_standard'`):** covers the case where a project has only ONE
  Multi-QAF container on file (no second container to diff against). The user picks
  ONE active variant out of an already-uploaded Multi-QAF container and compares it
  against a confirmed Standard-QAF file of the same project — summary-identity checks,
  a summary-metrics diff, and an explicit list of the standard-engine's detail modules
  that are structurally not applicable to a single-variant compare (never presented as
  empty/zero-diff sections).
- **iOS scope:** web-only for the BMW pilot. iOS may later read the `qaf_*` rows via
  PostgREST; no separate data model needed.

### QAF → Wertstrom-Übernahme (QVS, flag-gated: `qafValueStream`, `true` in `bmw`/`default` since QVS-P7, `false` in `_template`)
- **"Als Wertstrom übernehmen"** — a one-click path from a QAF comparison with real
  manufacturing data (`qaf_manufacturing_step`, the main engine — never the legacy
  `qaf_uploads.parsed_data` path the per-visit QAF Comparison above still uses) into a
  new Wertstromanalyse (VSM), with full field-level source lineage. Replaces manually
  retyping QAF numbers into the VSM editor.
- **Entry points (both open the same preview dialog):** (1) the QAF-Vergleich detail
  page (§10a/§10b — `summary`/`g60` comparison modes only; an ALT/NEU comparison offers
  a side selector, default NEU), (2) a project's QAF tab, listing that project's
  main-engine QAF comparisons with at least one eligible side (separate from — and
  additive to — the existing per-visit QAF Comparison Board on that same page, which
  keeps reading the legacy path unchanged). The button is never shown for a file with
  no detectable manufacturing steps (no dead button) or when the flag is off.
- **Preview dialog:** source file name, an editable suggested title
  (`[Lieferant] · [Projekt] · [Variante] · QAF-Wertstrom`), a per-step checklist
  (deselect individual steps before creating), a coverage line ("18 Schritte · 15 mit
  Zykluszeit · 12 mit Kosten"), non-blocking warnings, a collapsible list of fields that
  could not be extracted (with the reason), and a duplicate-import banner ("Öffnen" the
  existing Wertstrom / "Trotzdem neu erstellen") when this exact QAF file was already
  imported before. If the duplicate check itself fails, the dialog says so explicitly
  ("Duplikat-Prüfung nicht verfügbar") — it never silently reports "0 duplicates".
- **Creation is transactional and idempotent** (one Wertstrom + one audit/import record
  per confirmed preview, re-submitting the same confirmation is a no-op); on success the
  browser is redirected straight into the new Wertstrom's editor with a success toast
  reporting the partial-import balance ("18 Schritte importiert, 3 ohne Zykluszeit").
- **Editor extensions (additive — existing manually-built Wertströme are unaffected):**
  - A subtle, colourless marker on a node that came from a QAF import, plus a per-field
    "importiert"/"geändert" tag once a value is edited after import (never on a
    manually created node).
  - **"Quelle anzeigen"** on an imported node opens all of its captured QAF source
    fields — spreadsheet cell, original value, unit, parser confidence.
  - **Value-added classification** is a 4-way choice (Wertschöpfend / Notwendig, nicht
    wertschöpfend / Nicht wertschöpfend / Ungeklärt) instead of the old binary
    "Wertschöpfend" checkbox; imported steps default conservatively (never
    auto-"Wertschöpfend" — the classification the legacy Excel import used to hardcode).
    The old checkbox's underlying value is kept as read-only, unmodified compatibility
    data and shown as a hint until a step is explicitly classified.
  - **QAF-Soll/Stoppuhr-Ist comparison:** a step with both a QAF cycle time and a
    matching measured cycle time (by process name, same join key the existing
    QAF↔Prozess-Step mapping already uses) shows both plus the delta — display-only,
    never auto-overwrites the editor's cycle-time field.
  - The Wertstromanalyse list shows "aus QAF · <Dateiname>" on an entry that came from
    this flow.
- **Never fabricates data:** a field the QAF genuinely does not contain stays visibly
  empty ("—"), never a placeholder number; a step whose position is ambiguous is
  imported with the source row order and a non-blocking warning, never silently
  reordered or dropped.
- **iOS scope:** web-only, behind the flag. No iOS work until the flag is evaluated for
  general availability.
- **Multi-QAF variants (QVS-P4, additive):** when the source QAF is one side of a
  Multi-QAF-Varianten-vs-Standard comparison, the preview gains a Varianten-Sektion
  listing an EVIDENCE-SCOPED variant set (label, volume; Review-Fix 1, PR #338) — always
  the ONE variant the comparison was actually run against, plus any OTHER active variant
  provably bound to the SAME manufacturing profile (`variantProfileBindings`), never
  "every active variant in the container" unconditionally (a container may legitimately
  run variants on different manufacturing profiles/plants, and the comparison itself
  never checks manufacturing compatibility) — with a strategy choice: "Geteilter
  Fertigungsfluss" (default, one Wertstrom, every node tagged with that evidence-scoped
  key set) or "Je Variante ein Wertstrom" (N Wertströme with identical steps, one variant
  tag AND its own distinguishing title suffix each — Review-Fix 3). A degraded
  variant-context check surfaces its own visible "nicht verfügbar" notice, never silently
  identical to "no Multi-QAF context" (Review-Fix 8) nor a silently untagged create
  (Review-Fix 7). Multi-QAF has no per-variant step-level data (a shared cost-profile
  aggregate, not a process-step list) — cycle times are never fabricated per variant, and
  the section is never shown for a plain Multi-QAF-vs-Multi-QAF comparison (neither side
  has manufacturing steps to import from). "Je Variante" is N independent creates, not one
  transaction — a mid-sequence failure is reported per variant (toast type reflects the
  actual outcome — success/info/error, never a hardcoded green — Review-Fix 2/10), never
  hidden.
- **`sum_planned_capacity`/`sum_lot_size` (QVS-P4):** the QAF Summary sheet's
  "Plankapazität [Teile/Jahr]" and "Fertigungslosgröße [Teile]" premises (previously
  unparsed) now feed the import record's engine context and a Kapazitäts-Kontext metric
  (exact `lotsPerYear = plannedCapacity ÷ lotSize`, no fabricated Kundentakt derivation —
  that would need an unparsed working-hours-per-year constant).
- **Kennzahlen-Erweiterung (QVS-P4, `vsm-metrics.ts`, additive — existing
  Bottleneck/VA-Quote untouched):** a vaClass-aware VA-Quote (va/nnva/nva/ungeklärt
  separately, legacy binary-only nodes marked "abgeleitet"), Setup-/Warte-/
  Transportzeit-Summen (node-level; `null` when no node carries the field, never a 0-lie),
  and a Kosten-/Scrap-Rollup per Währung (mixed currencies always kept in separate
  buckets, never silently summed — scrap is AW-denominated, its own bucket). Every metric
  ships a Formel/Datenbasis/Ausschlüsse explain object. Computed and unit-tested; not yet
  wired into an editor panel (follow-up).
- **Reimport & Synchronisation (QVS-P5, additive, Spec 21):** an imported Wertstrom (any
  node still carrying `qafSource`) can be re-synced against a QAF source — either the
  file it was originally created from (re-checks whether the source itself changed; a
  `qaf_file` row never mutates in place, so this is a legitimate "nothing changed"
  confirmation) or a DIFFERENT/newer `qaf_file` upload in the same project (the "A newer
  QAF revision is uploaded" scenario). The delta engine classifies every matched step AND
  every one of its fields into exactly one of the 6 statuses Spec 21 names: **Unverändert**,
  **Quelle geändert**, **Lokal geändert**, **Konflikt** (changed on both axes since the
  last sync), **Neu in Quelle**, **In Quelle entfernt**. Matching a step across two
  different QAF uploads (row indexes are not comparable across files — every upload gets
  fresh, unrelated `qaf_manufacturing_step` rows) uses the process name as the identity;
  a name whose count differs between the two sides (e.g. one "Prüfen" became two) is
  never force-matched — it surfaces as its own, honestly unresolved "nicht eindeutig
  zuordenbar" state instead of guessing which one corresponds to which.
  - **Selective adoption, never silent:** a Übernahme-Plan can apply all source changes,
    a hand-picked subset, or none. "Lokal geändert" fields are NEVER touched by a bulk
    "alle übernehmen" — the only way to move a Konflikt-Feld (changed both source-side
    and locally) is an explicit per-field decision ("Quelle übernehmen" / "Lokal
    behalten"). A step removed from the source defaults to staying in the Wertstrom
    (explicit "entfernen" required to actually delete it). Adopting a field also
    refreshes that field's own source-lineage entry (cell/original value/confidence) so
    "Quelle anzeigen" never shows stale provenance after a sync.
  - **Separate comparison copy:** instead of updating the existing Wertstrom, "Als
    separaten Vergleichs-Wertstrom anlegen" creates an entirely independent Wertstrom
    from the chosen source (reuses the normal one-click creation path unchanged),
    titled `<ursprünglicher Titel> · Reimport <Datum>` — the original Wertstrom and its
    import history are untouched by this option.
  - **Persistence:** no new migration. The existing `value_stream_imports` audit record
    is updated in place on every reimport (its `import_snapshot` becomes the new sync
    baseline; a `reimportHistory` entry inside `engine_context` keeps the full time
    series of what was adopted/kept/removed and when) rather than inserting a new row
    per sync — a deliberate, schema-compatible choice (`engine_context` has no closed
    shape) over the "N import rows over time" pattern the schema also supports (used
    instead by the separate-comparison-copy option above, which genuinely is a new,
    independent import).
  - **Entry point:** a "Synchronisieren" action in the Wertstrom editor toolbar, shown
    only when the Wertstrom has at least one QAF-imported node (an old, manually built,
    or Excel/LSC-imported Wertstrom never sees it) and the flag is on.

### Wertstromanalyse — Szenarien, Kantentypen, Lager-Untertypen, Provenance (Wertstrom P2, KAR-878/KAR-986)
Datenmodell-Erweiterung des VSM-Editors (`value_stream_maps`) — the data model itself
(A7/A8/A12 fields, the `scenario_kind`/`parent_value_stream_id` columns) is additive and
not gated behind a feature flag. The A9 **create-action** (below) IS gated — review-fix
F2 (adversarial review, PR #353) corrected the original "kein Feature-Flag" claim here:
CAPABILITY-MATRIX.md explicitly scopes the Soll-Zustand-Erstellung UI to P5, and P5 is on
the matrix's own flag-mandatory list. No UI redesign ("das UX-Herz" — Quick-Start-Wizard,
Auto-Layout, Panel-Redesign — is P3/P4); this phase only makes the new data shapes
creatable/visible.

- **Informationsfluss-Kantentyp (A7):** a connection can be `materialFlow` (default,
  every pre-existing connection) or `information`. Chosen via a small Material/
  Information toggle shown while drawing a connection, plus an optional free-text
  frequency ("täglich", …) for information flow. Renders dashed on the canvas
  (material flow stays solid) — no new symbol matrix, matching the Simplicity-Ansatz.
- **Lager-Untertypen (A8):** the existing generic inventory element gains an optional
  "Lager-Art" choice — FIFO-Bahn, Supermarkt, or Push (one select on the same element,
  not three separate palette entries) — plus an optional max-capacity field shown only
  for FIFO ("FIFO mit Kapazität"). Absent = today's generic/undifferentiated inventory,
  unchanged. Validated by the A11 engine (review-fix F4): a negative capacity is
  critical, a FIFO quantity above its capacity is a warning (a real lane can genuinely
  overflow — a modelling hint, not an impossibility). Switching away from FIFO clears
  the capacity value, not just its input (review-fix F10) — the canvas card no longer
  keeps showing a stale "/ <alte Kapazität>" for a since-changed Lager-Art.
- **Szenarien — Ist/Soll (A9):** "Soll-Zustand erstellen" in the editor toolbar — gated
  behind the `wertstromScenarios` flag (`config/profiles/`, off in every profile until
  P5, review-fix F2) — duplicates the current Wertstrom (nodes/connections/layout,
  node/connection ids preserved) into a new, independent map with a traceable link back
  to the source (`parent_value_stream_id`) and `scenario_kind: 'future'`; the source map
  is never modified. `is_demo` is always `false` on the new map regardless of the
  source's (review-fix F1 — a user-created Soll-Zustand is never seed content). Only an
  Ist-Wertstrom (`scenario_kind: 'current'`) can be duplicated — chaining off an
  already-derived scenario is rejected (button hidden client-side, 400 server-side,
  review-fix F3), so a scenario's `parent_value_stream_id` always points at the
  Ist-Wurzel, never at an intermediate scenario. If the editor has unsaved changes when
  "Soll-Zustand erstellen" is clicked, it saves them first — a failed save aborts the
  duplicate entirely rather than silently deriving the new Soll from a stale server copy
  (review-fix F5). The Wertstromanalyse list shows a small "Soll"/"Alternative" badge for
  a derived scenario (nothing shown for an ordinary Ist-Wertstrom); both the button and
  the badge are hidden when the flag is off. Side-by-side KPI comparison and reopening a
  scenario's delta view are explicitly **P5** — this phase covers creation + listing +
  labelling only, per execution-prompt §9.1/§9.4.
- **Provenance ("Herkunft", A12):** a node can carry one data-quality tag — geplant /
  gemessen / berechnet / importiert / angenommen — shown as a small read-only badge next
  to the Zykluszeit field (review-fix F11 — moved off the node header, which read as a
  node-wide claim it never was; all 3 write paths only ever tag `cycleTimeSec`). Set
  automatically by the two stopwatch/LSC paths that already existed (Capability-Matrix
  "RETAIN EXISTING"): a live inline stopwatch measurement and the LSC-Workshop bulk
  import tag "gemessen" when a real `cycle_measurements` average exists, "geplant" when
  they fall back to a step's planned cycle time. Never silently overwrites: hand-editing
  the Zykluszeit field resets provenance to unknown rather than leaving a stale claim
  (review-fix F6), and replacing an already-set value with a lower-confidence one
  (confidence order measured > calculated = imported > planned > assumed) asks for
  confirmation first (review-fix F7 — e.g. the Stoppuhr-Picker's "geplant" pick over an
  existing "gemessen" value). Absent = unknown, no badge shown (never a noisy
  "unbekannt" badge on every node).
- **iOS scope:** web-only (the VSM editor itself is web-only per the existing QVS
  section above); no separate iOS data model needed for these additive JSONB fields.

### Wertstromanalyse — Quick-Start-Wizard, Auto-Layout, Timeline-Leiter v2 (Wertstrom P3, KAR-878/KAR-986)
"Das UX-Herz" (Capability-Matrix A1/A2/A4) — complete behind the new `wertstromUxV2`
feature flag (`config/profiles/`, `false` in every profile; Phasenplan-Pflicht for P3).
Existing editor behaviour is byte-identical while the flag is off.

- **A1 Quick-Start-Wizard (`components/wertstrom/vsm-quick-start-wizard.tsx` +
  `vsm-wizard-logic.ts`):** a 5-step guided flow, opened via a "Neu mit
  Assistent"/"Mit Assistent erstellen" button on the Wertstrom list page (gated,
  next to the existing "Neue Wertstromanalyse"):
  1. **Produkt & Bedarf** — Titel (required to finish, not to advance), Projekt
     (optional), Produkt (optional, free text), Bedarfsmenge (optional).
  2. **Arbeitszeit** — Std./Schicht, Schichten/Tag, Pausen (Min./Schicht), Arbeitstage;
     all optional (§8.2 simple work-time model).
  3. **Prozessschritte** — repeatable Name + optional Zykluszeit (s) rows, "+
     Prozessschritt hinzufügen"; at least one named step is required to advance past
     this step (the one non-skippable step — a Wertstrom needs at least one process).
  4. **Bestände zwischen Prozessen** (optional) — one toggle + optional Menge (Stk) per
     gap between two named process steps; skipped entirely when fewer than two steps
     are named.
  5. **Ergebnis** — summary (title, step/inventory counts, Taktzeit via the P1 engine's
     `computeTakt`) and "Fertigstellen".

  "Fertigstellen" is the ONLY point that touches the network — every earlier step is
  local React state, so closing/cancelling the wizard at any point creates zero rows
  (no data litter). It calls the EXISTING `POST /api/wertstrom` (title/description/
  project_id) followed by the EXISTING `PUT /api/wertstrom/[id]` (nodes/connections,
  with the `If-Match` optimistic-locking header from the POST response) — no new route,
  no server-contract change. The built graph is Lieferant + one node per named process
  step + optional Lager-Nodes + Kunde, connected in a single linear materialFlow chain
  (`buildLinearConnectionPairs`, the existing P0 helper) and positioned by the new
  Auto-Layout function (below). A process step's cycle time is tagged `provenance:
  'planned'` (a plan value, never a measurement) — only on a step that actually has a
  cycle time. The Kunde node's `demand` field is set from Bedarfsmenge when given.
  "Produkt" has no dedicated column (Nicht-Scope: no migration) — folded into
  `description` as `"Produkt: <Name>"` when entered, a real, readable home for the
  value rather than dropping it silently. Every step stays skippable (§10.3 Smart
  Defaults) EXCEPT Prozessschritte; missing working-time/demand data makes the
  Ergebnis step's Taktzeit honestly "nicht berechenbar" (with the engine's own
  exclusion reasons shown) rather than a guessed number.
- **A2 Auto-Layout (`components/wertstrom/vsm-auto-layout.ts`, `computeAutoLayout`):** a
  pure, deterministic function — additive/unflagged, inert without a caller. Orders
  Lieferant→Prozesse→Kunde via a topological sort over materialFlow connections
  (Kahn's algorithm, deterministic array-order tie-break; anything the graph can't
  resolve — a disconnected node, a cycle — is appended rather than dropped). Splices
  Bestands-Nodes onto a row below the main line, at the x-midpoint between their
  resolved materialFlow predecessor/successor (staggered when two collide onto the
  same slot); a node whose ONLY connections are `kind: 'information'` is placed on a
  row above instead (production-control-style nodes), matching §11.1's "supplier
  left, customer right, production control/information above, inventories between
  processes". Used by the Quick-Start-Wizard for its initial layout AND by a new,
  flag-gated "Automatisch anordnen" button in the editor's toolbar — the button always
  confirms first (`window.confirm`) since it overwrites every node's manual position;
  manual dragging remains fully available afterwards (this is a one-shot
  repositioning action, not a persistent constraint).
- **A4 Timeline-Leiter v2 (`components/wertstrom/vsm-timeline-ladder.tsx`):** renders
  the P1 engine's `computeTimelineLadder` (`lib/vsm-engine`) below the canvas — a
  proportional segmented bar (Wertschöpfend/NVA-Prozesszeit/Wartezeit/Bestandszeit/
  Transportzeit), Durchlaufzeit (DLZ), and Process Cycle Efficiency (PCE), all derived
  live from the editor's `nodes` state (the UI never recomputes — P1 doctrine). PCE
  shows "nicht berechenbar" rather than a fabricated 0% when no VA time was measured;
  DLZ is flagged "(ohne Bestandszeit)" when inventory nodes exist but coverage isn't
  computable (no demand/shift-model input available in the plain editor — an honest,
  visible gap, not a silent underestimate). The engine's `formula`/`dataBasis`/
  `exclusions` (MetricExplain) are reachable via a native `<details>` disclosure —
  always in the DOM, keyboard-operable, not a hover-only tooltip. Replaces the
  pre-existing 1-Prozentbalken ONLY while the flag is on; the old bar is untouched and
  byte-identical when it is off.
- **Scope decisions:** no migration, no new DB columns, no engine-formula changes (P3
  only consumes `computeTakt`/`computeTimelineLadder`, both already shipped in P1). No
  new API route — the Wizard's "Fertigstellen" deliberately reuses the two existing
  Wertstrom endpoints verbatim rather than widening `CreateValueStreamMapBody`; the
  `wertstromUxV2Gate` predicate therefore has no dedicated route call site this phase
  (unlike `wertstromScenariosGate`) and is instead the single source of truth both
  `page.tsx` files use to resolve the `wertstromUxV2Enabled` prop. A11y-Basis: every
  wizard input has a real `<label htmlFor>`, the step progress list uses
  `aria-current="step"`, the wizard container is `role="dialog"`/`aria-modal`, Escape
  closes it, and focus follows the active step's heading. Data-Tabellen-View/
  Excel-Paste (A14), Panel-Redesign (A13), Szenarien-UI-Ausbau (A9-UI/P5), and
  SimVSM-Import (A16) are explicitly NOT part of this phase (Capability-Matrix
  phase plan).
- **iOS scope:** web-only (the VSM editor itself is web-only); no separate iOS work.

### Wertstromanalyse — Panel-Redesign, Tabellen-View, OEE-Link, Standard-Seite (Wertstrom P4, KAR-878/KAR-986)
Capability-Matrix A13/A14/B1–B4 — complete behind the EXISTING `wertstromUxV2` feature
flag (no new flag: P4 continues the P3 UX program). Existing editor behaviour is
byte-identical while the flag is off — the old flat Properties Panel branch is
untouched code, only additionally nested under `wertstromUxV2Enabled ? … : …`.

- **A13 Progressive Disclosure (`components/wertstrom/vsm-editor.tsx`):** the
  Properties Panel now has three tiers instead of one flat field list (§7.1/§7.2/
  §10.2):
  1. **Essential** (always visible): Name, cycle time, number of operators, plus NEW
     Availability (%) (B3) for process nodes; quantity/storage sub-type for
     inventory; transport time + NEW frequency (B4) for transport; units/day for
     customer/supplier; time value for time-value nodes.
  2. **Advanced** (native `<details>`, collapsed by default): the pre-existing
     time-breakdown/capacity/process-type fields PLUS the **9 previously UI-less
     editable fields** from `lib/vsm-types.ts` (only ever populated via QAF import
     before this phase): parts per cycle, location, currency, scrap rate, scrap
     cost per unit, machine-hour rate, labor-hour rate (per hour), setup cost per
     unit, cost per unit — German labels per §3.5 (no internal codes). All 9 were
     already part of `VsmNodeEditableFieldKey` (P2), so the existing
     provenance/fieldStatus derivation in `updateNode` covers them with zero logic
     changes — "imported"/"modified" tags travel with each field automatically.
     Value-added classification (vaClass selector/checkbox) moved here too (§7.2
     lists "value-adding status" as Advanced).
  3. **Expert** (its own `<details>`, collapsed by default): MTBF (min)/MTTR (min)
     for the B3 OEE link.

  All three tiers use a native, UNCONTROLLED `<details>`/`<summary>` (no React
  `open` prop) — a React-controlled `open` on `<details>` stops reliably re-closing
  after one real browser toggle (observed React/DOM limitation, not jsdom-only;
  matches the pre-existing uncontrolled "Details & Ausschlüsse" disclosure in
  `vsm-timeline-ladder.tsx`).
- **A14 Data Table + Excel-Paste (new files `vsm-table-view.tsx` +
  `vsm-table-paste.ts`):** a "Canvas ↔ Tabelle" toggle in the toolbar (flag-gated).
  The table shows one row per **process step** (`type === 'process'` — matches the
  existing file-based Excel import's scope exactly; other node types stay
  Canvas-only this phase, see Scope decisions) with inline edit of Name/cycle
  time/number of operators — calling the SAME `updateNode` closure the Properties
  Panel uses (identical `nodes`/`connections` state, no second source of truth).
  A missing-data marker (muted dot + `sr-only` text) appears per cell for "no
  data", never for a real 0. Excel-Paste: copy cells from Excel/Sheets
  (tab-separated) → paste into a textarea → live preview (optional header-row
  detection, else a fixed default column order) → "Übernehmen" appends new
  process nodes (linearly chained to each other, same explicit non-behaviour as
  the existing file-based Excel import — never auto-attached to the pre-existing
  chain). Reuses `numericCellOrUndefined` (`vsm-excel.ts`, now exported) rather
  than a second nullish-parsing implementation. The right Properties Panel column
  stays visible in table mode too — clicking a row selects that node there for
  Advanced/Expert editing. A11y: focus moves to the table's heading on switching
  into it (`tabIndex={-1}` + `.focus()`, same pattern as the Wizard's per-step
  focus); `aria-live="polite"` for the view-switch announcement and the paste row
  count; the table scrolls horizontally inside its own container (360px-capable,
  never the page body).
- **B3 OEE link:** new `calculateAvailabilityFromMtbfMttr` function in
  `lib/oee/engine.ts` (Availability = MTBF/(MTBF+MTTR), minutes in, fraction 0–1
  out) — `lib/oee` already had three independent OEE=A×P×Q formulas but no
  MTBF/MTTR derivation; this closes that gap inside `lib/oee` (not duplicated
  elsewhere). The Properties Panel shows a "→ berechnete Verfügbarkeit: X%" hint
  with an explicit "Übernehmen" button once both MTBF and MTTR are set (never
  written silently into `availabilityPct`). The effective capacity/utilization
  display reuses `lib/vsm-engine`'s pre-existing `computeEffectiveCapacity`/
  `computeUtilization` (P4 is the first phase to actually call these from the UI —
  previously 0 callers outside `lib/vsm-engine` itself); `availabilityPct` is
  passed as `NodeCapacityOverrides.availabilityPct`, never written into `node.oee`
  (that field means a full MEASURED OEE and takes the capacity engine's
  double-counting-guarded priority branch, which ignores overrides).
- **B4 External transport:** new Standard field `transportFrequency` (free text,
  e.g. "3x täglich") on transport nodes, alongside the existing transport time;
  distance moved to Advanced. The rest (number of transporters, vehicle capacity,
  way-back time) deliberately not built (SimVSM complexity, P4-Brief Nicht-Scope).
- **B2 Simple work-time model (Standard-Seite):** `vsm-timeline-ladder.tsx` gains
  an editable "Arbeitszeitmodell" form (hours/shift, shifts/day, break
  minutes/shift) — closes a gap the P3 Timeline-Leiter itself documented ("kein
  Kundenbedarf/Arbeitszeitmodell hinterlegt" because "the editor has no persisted
  place to source those from"). Persisted additively in `layout.shiftModel`
  (existing jsonb column, same bucket as `layout.viewport` — no migration).
  Demand rate [units/day] is summed from existing customer-node fields (no
  re-entry). `PUT /api/wertstrom/[id]`'s layout merge treats `shiftModel`
  asymmetrically from `viewport`: a request that omits `shiftModel` LEAVES an
  already-saved value untouched (unlike `viewport`, which is unconditionally
  overwritten) — `viewport` always has a live value from the editor, `shiftModel`
  is genuinely optional data that a naive copy of the `viewport` pattern would
  have silently wiped (caught by a test, see `route.test.ts`).
- **B1 Multi-product:** no code change needed — the editor never had a
  multi-product concept, "single product Standard" is already the only existing
  state. Expert-tier build-out (per-process product rows) remains P4-Brief
  Nicht-Scope.
- **Contract changes** (additive, symbol-grep performed, no consumers affected):
  `VsmNode` (`lib/vsm-types.ts`) + `VsmNodeSchema` (`lib/api/schemas.ts`) gain
  `availabilityPct`/`mtbfMin`/`mttrMin`/`transportFrequency`; `VsmLayoutSchema`
  gains `shiftModel`; `numericCellOrUndefined` (`vsm-excel.ts`) is now exported
  (was module-private).
- No new API contract beyond the additive fields, no new DB column/migration, no
  engine-formula changes (only consumes `computeEffectiveCapacity`/
  `computeUtilization`/`computeTimelineLadder`, all P1).
- **Scope decisions:** the Data Table shows process nodes ONLY (not
  inventory/transport/customer/supplier/machine/time-value) — a uniform column
  schema only fits a uniform row semantic, and the existing Excel import already
  establishes "process step" as this codebase's table-shaped concept.
  Excel-Paste covers Essential fields only (Name/cycle time/number of operators),
  not the file import's Advanced time breakdown. Scenario-UI build-out (P5),
  SimVSM import (P6), presentation/exports (P7), i18n/E2E (P8) are explicitly NOT
  part of this phase (Capability-Matrix phase plan).
- **iOS scope:** web-only (the VSM editor itself is web-only); no separate iOS work.

### Wertstromanalyse — Szenarien-Navigation, KPI-Delta-Vergleich, Parameter-Abweichungen (Wertstrom P5, KAR-878/KAR-986)
The A9-UI build-out execution-prompt §9.1–§9.4 promised and P2 deferred (P2 only shipped
the `scenario_kind`/`parent_value_stream_id` data model + "Soll-Zustand erstellen").
Complete behind the pre-existing `wertstromScenarios` flag (`config/profiles`,
`wertstromScenariosGate` — introduced in P2, `false` in every profile until an operator
flips it on; this phase fills the gate with real UI, it does not change the default).
Existing list/editor behaviour is byte-identical while the flag is off.

- **Szenarien-Navigation (§9-adjacent, Kern-Anforderung 1 "Zugehörigkeit + Wechsel"):**
  shown on both existing GET surfaces, zero new queries.
  - **List** (`vsm-list-client.tsx`): an Ist row with scenarios gets an expandable
    "N Szenario(en)" `<details>` listing each by title (Öffnen-link); a scenario row
    gets an "aus &lt;Ist-Titel&gt;" back-link. Both derived CLIENT-SIDE from the `maps`
    array the component already receives in full — the list query never had a
    `.limit()` and already selected `parent_value_stream_id`/`scenario_kind` (P2), so
    parent/child relationships are computable with the data already in hand.
  - **Editor** (new `vsm-scenario-nav.tsx`): a "Szenarien (n)" toolbar disclosure on an
    Ist map (per-scenario Öffnen + Vergleichen links) or an "Abgeleitet von …" link +
    Vergleichen-button on a scenario map (its parent's data is already loaded, see
    below — no navigation needed to open the comparison).
- **§9.3 Scenario Comparison (new `vsm-scenario-compare.ts` pure module +
  `vsm-scenario-compare-view.tsx`):** a third editor view mode ("Vergleich", alongside
  Canvas/Tabelle) shows the P5-Brief's "Kern-KPIs" subset of §9.3's full KPI list —
  Takt-Auslastung Engpass, Durchlaufzeit (DLZ), wertschöpfende Zeit (VA), Process Cycle
  Efficiency (PCE), Bestandsreichweite gesamt, Engpass-Node — as current/scenario
  value, absolute delta, percentage delta, and a Verbesserung/Verschlechterung/
  unverändert/nicht-vergleichbar/informativ judgment (never color alone — always a
  trend arrow + text label). `Δ` on VA-Zeit and Engpass-Node is deliberately
  judgment-free ("informativ") — more/less VA time or a different bottleneck NODE isn't
  inherently better or worse on its own; PCE (the VA/DLZ ratio) carries the real
  judgment. §9.3's remaining KPIs (output, capacity, operators, shifts, OEE, scrap,
  rework, risks, measures) are a deliberate scope cut, not built this phase (see Scope
  decisions).
  - Computed via `computeScenarioKpiSnapshot`, which calls `lib/vsm-engine`'s
    `computeBottleneckV2` (P1, previously 0 UI callers anywhere in the repo — P4's OEE
    link only ever wired up `computeEffectiveCapacity`/`computeUtilization` for a
    single node) and `computeTimelineLadder` (P1/P3) — ONCE per side, each with that
    side's OWN `ScenarioCompareContext` (own linked project's `customer_takt_time_sec`,
    own `layout.shiftModel`, own demand rate) — never a blended/shared context. A
    `describeContextMismatch` banner surfaces it explicitly when the two sides'
    contexts differ (different linked project, or a different Arbeitszeitmodell)
    instead of silently comparing apples to oranges.
  - Column order is always Ist → Soll/Alternative regardless of which map the user is
    currently viewing (P5-Brief "KPI-Delta-Vergleich Ist↔Soll").
  - Honest nulls throughout: a missing Kundentakt or unmeasured VA time renders "nicht
    berechenbar" with a "nicht vergleichbar" judgment, never a fabricated 0 or a fake
    delta (`absoluteDelta`/`percentDelta` are `null`, not `0`, whenever either side is
    `null`).
- **§9.4 Parameter Deviation Visualization (same view, `<details open>` panel — "Keep
  this information in a details panel. Do not overload the main value stream view"):**
  per-node field deviations (Zykluszeit, Operatoren, OEE, Verfügbarkeit, Ausschussrate,
  Bestand, Lager-Art) between the Ist and Soll/Alternative node with the SAME id (P2's
  Node-ID preservation across "Soll-Zustand erstellen" exists exactly for this
  matching). `Lager-Art` (`inventoryKind`, A8) is the one categorical field in that list
  — Review-Fix F3 (adversarial review, PR #356) added it after review found it silently
  missing a Push -> Supermarkt-only change; it renders as a plain old-value ->
  new-value text pair (German labels, same as the editor's own "Lager-Art" dropdown),
  no Δ column entry (no numeric distance between two categories). Unedited fields are
  never listed. Nodes only on one side are sorted into "Neu"/"Entfernt" sections.
  Byte-identical node sets (the state right after "Soll-Zustand erstellen", before any
  edits) render "Keine Abweichungen gefunden" rather than three empty section headers.
  Two data-model gaps are surfaced honestly rather than silently dropped: "Nacharbeit"
  (Rework) has no `VsmNode` field at all — only `scrapRate` is comparable
  (`lib/vsm-engine/internal/quality.ts`'s own P1 module doc already documents
  `reworkRatePct`/`reworkTimeSec` as deliberately not added to the node shape) — and
  per-field reason/author/change-date (the execution-prompt's own §9.4 wishlist) are
  not captured anywhere in the data model (`VsmNode.fieldStatus` only ever tracks
  'imported'/'modified', no who/when/why). A separate, deliberate SCOPE decision (not a
  data-model gap — the field exists) applies to connections/Kanten: §9.4 (like §9.3) is
  node-PARAMETER-scoped, not a topology diff — a Kanten-only change (new/removed/
  re-routed connection, or a changed `kind`/`frequency`, Capability-Matrix A7) is
  consequently not visible here; `ScenarioCompareSide` deliberately never loads
  `connections` for the OTHER side at all (see its own doc comment, Review-Fix F6,
  adversarial review PR #356) — comparing edges is a bigger, undecided future scope,
  not built this phase.
- **Empty/Edge-States:** a scenario whose `parent_value_stream_id` is `null` (either the
  Ist-Ursprung was deleted — the FK is `ON DELETE SET NULL`, P2 — or a pre-P2 row) shows
  an honest "Vergleich nicht möglich" message instead of a dead button or silently
  omitting the feature. A `?compare=<id>` link that no longer resolves (deleted, or
  never a real child of this map) renders the same honest message inside the compare
  view rather than a blank screen. Comparing two scenarios of the same parent is
  structurally unreachable, not just UI-hidden: the P2 duplicate route only ever derives
  a scenario FROM `scenario_kind: 'current'`, so `parent_value_stream_id` always points
  at the Ist-Wurzel — there is no code path where the "other side" could ever be a
  second scenario.
- **Scope decision — no new API route:** the comparison's "other side" data travels over
  the EXISTING `value_stream_maps` read path (`app/wertstrom/[id]/page.tsx`, a Server
  Component fetch, same convention the list page already uses for
  `sourceFileNameByVsmId`) rather than a new `GET` route — for a scenario map, its
  (single, structurally guaranteed) parent is fetched eagerly; for an Ist map, the page
  now also reads `?compare=<scenarioId>` from `searchParams` and fetches that ONE
  sibling's full row, scoped by `parent_value_stream_id = id` (defends against a
  hand-edited id on top of RLS). Sibling identity for the nav list itself
  (id/title/scenario_kind, no nodes/connections) is fetched separately and stays cheap
  regardless of how many scenarios exist or whether a comparison is ever opened.
- **Contract changes:** none to the API/DB — this phase is entirely additive new
  components/modules plus new OPTIONAL props on `VsmEditor` (`projectLabel`,
  `scenarioParent`, `scenarioChildren`, `compareTarget`,
  `compareTargetRequestedButMissing` — all defaulted so any caller that predates this
  feature stays byte-compatible, same convention as `sourceFileNameByVsmId`).
  `computeDemandUnitsPerDay` (the demand-rate formula `vsm-editor.tsx` already had
  inline) is extracted into `vsm-metrics.ts` as a pure, exported, tested function —
  byte-identical behavior, now reused by the comparison's OTHER side too instead of
  being duplicated.
- **No engine-formula changes** — this phase only consumes `computeBottleneckV2`/
  `computeTimelineLadder` (both P1, unmodified). No new DB column/migration (the P2
  columns already cover everything this phase needs).
- **Nicht-Scope (hard, unchanged from the P5-Brief):** scenario merge/promote-to-Ist,
  reopen-flows (scenario lifecycle/status), comparing more than 2 maps at once, SimVSM
  import (P6), exporting a comparison (P7), any migration, any engine-formula change.
- **iOS scope:** web-only (the VSM editor itself is web-only); no separate iOS work.

### Wertstromanalyse — Management-Analyse, Präsentationsmodus, Export-Familie (Wertstrom P7, KAR-878/KAR-986)
execution-prompt §11.6/§18/§19. Complete behind the pre-existing `wertstromUxV2` flag (`true`
in `default`/`bmw` since P3 — Kais tests this live; every new P7 surface is additively gated
on the SAME flag, no new permission code, no profile change). Three additive pieces, all
client-side/pure — no new DB column/migration, no engine-formula change.

**Engpass-Modell-Grundsatz-Entscheidung (P7 fix-round, adversarial review):** everything that
shows the user "Engpass" now uses `computeBottleneckV2` — the Editor-Canvas-Badge and P4
Tabellen-Ansicht (`vsm-editor.tsx`), the XLSX-Export Status-Spalte (`vsm-export-xlsx.ts`), and
the Copilot-Export "Kennzahlen"-Zeile (`wertstrom-docx.ts`/`wertstrom-md.ts`) all previously
called the legacy `findBottleneckId` heuristic directly and could silently show a DIFFERENT
"wo ist der Engpass" answer than the Management-Analyse/Präsentationsmodus/PDF sections in the
very same view or document. `findBottleneckId` (`vsm-metrics.ts`) itself is untouched — it
remains `computeBottleneckV2`'s own internal degraded-fallback formula (no takt, or fewer than
2 usable stations) and is still used by `lib/reporting/project-kpis.ts` (an unrelated feature,
out of this phase's scope). The one deliberately UNCHANGED concept: `hasCtWarning` (the
CT-over-Takt warning shown in the P4 Tabellen-Ansicht) is a different judgment (a station's own
cycle time vs. takt, independent of which node is "the" bottleneck), not part of this
unification.

- **§18 Management-Analyse (A15, new `lib/vsm-engine/internal/management-analysis.ts`,
  `computeManagementAnalysis`):** rule-based, deterministic German sentence generator — NO
  LLM/API call. Reads only already-shipped P1 engine results
  (`computeBottleneckV2`/`computeTimelineLadder`/`computeInventoryCoverage`/
  `computeCumulativeYield`/`runValidation`) plus optional caller context
  (Kundenbedarf/Takt/Arbeitszeitmodell/Szenario-Vergleich). Every §18 bullet (Kundenbedarf,
  Takt, primärer + sekundäre Engpässe, DLZ, VA-Zeit, VA-Anteil, Bestands-Hotspots,
  Qualitätsverluste, Kapazitätsrisiken, Verbesserungshebel, offene Maßnahmen, Ist/Soll-Effekte)
  produces one or more `AnalysisStatement { text, sourceMetric, values, confidence }` —
  traceable by construction (`sourceMetric` names the real engine field/function behind the
  sentence). Missing data basis ⇒ an explicit "nicht bewertbar, weil…" statement (engine's own
  confidence/`KNOWN_VALIDATION_GAPS` surfaced, never overridden) — `null` is never rendered as
  `0`. "Ist/Soll-Effekte" only appears when the caller supplies an active P5 comparison
  (`computeKpiDeltaRows` result) — otherwise the category is entirely absent, not a placeholder.
  UI: new `vsm-management-analysis-view.tsx`, a P4-panel-style `<details>`/`<summary>` +
  `role="heading"` section under the Timeline-Leiter, Klartext first (§14.1), sourceMetric/
  values/confidence behind a per-sentence "Details" toggle.
  **Documented gap:** "offene Maßnahmen" has no linked data source in this editor today (no
  workshop-actions↔Wertstrom join exists) — the module accepts an optional `openMeasures`
  parameter for a future caller with real data; today it always renders the honest
  "nicht bewertbar" statement. Not fabricated, not silently dropped.
- **§11.6 Präsentationsmodus (A17, new `vsm-presentation-mode.tsx`):** full-screen overlay
  (Fullscreen API with a same-viewport CSS fallback when unsupported/denied), structurally
  has no editing controls (a read-only component, not a hidden-controls variant of the editor),
  fit-to-screen via the existing P3 `computeFitView`, KPI header (Kundentakt/Engpass/
  Durchlaufzeit/PCE via `computeBottleneckV2`/`computeTimelineLadder` — "Engine-Kern-KPIs",
  same P5 vocabulary), Ist/Soll-Deltas when a comparison is active, ESC (both native
  fullscreen-ESC and a plain `keydown` listener, so it works in fallback mode too) plus a
  visible "Beenden"-button. No text below `text-sm` (14px) anywhere in this component. The map
  itself renders via `vsm-export-svg.ts`'s deterministic SVG renderer as an `<img
  src="data:image/svg+xml,...">` — never `dangerouslySetInnerHTML` (an `<img>` cannot execute
  markup regardless of node-name content, no sanitizer dependency needed).
  **Engpass-Modell:** the bottleneck highlight uses Engpass v2 (`computeBottleneckV2`) — self-
  consistent with the same view's own KPI header. P7 fix-round correction (adversarial review):
  the Editor-Canvas-Badge originally still used the legacy `findBottleneckId` heuristic here,
  producing an undisclosed, contradictory second "wo ist der Engpass" answer within the same
  editor viewport — the Grundsatz-Entscheidung "v2 überall, wo 'Engpass' draufsteht" (see the
  XLSX/Copilot-Export bullets below) reverses that: the Canvas-Badge and the P4 Tabellen-Ansicht
  now also read `computeBottleneckV2`, so there is no longer a Canvas-vs-Präsentationsmodus
  divergence to caveat here.
- **§19 Export-Familie (A17, new `vsm-export-dialog.tsx` + `vsm-export-{pdf,xlsx,json,svg}.ts`):**
  ONE primary "Wertstrom exportieren" action (§19.1 "not many equal download buttons") opening a
  format-choice dialog (existing shadcn/Radix `Dialog` primitive, first real usage in this repo).
  Runs client-side over the editor's already-loaded data (no new API route) for 4 of 5 formats;
  the 5th reuses the EXISTING gated Copilot-Export server actions.
  1. **Management-Report (PDF):** `buildWertstromManagementReportConfig` assembles the EXISTING
     `ExportConfig` shape `lib/export/export-service.ts`'s `exportToPdf` (jsPDF) already renders
     for the Visit-/Projekt-Export — no new PDF stack. Covers every §19.2 bullet the current data
     model supports (Projekt-Kontext, Bedarf, Arbeitszeitmodell, Takt, Prozess-/Bestandstabelle,
     Material-/Informationsfluss, Timeline, Engpass-Analyse, Qualität, Ist-KPIs, Soll-KPIs +
     Szenario-Vergleich wenn aktiv, Kaizen-Hebel, Maßnahmen, Annahmen, Datenqualitäts-Hinweise).
  2. **Visuelle Karte (SVG + PNG):** `vsm-export-svg.ts` — hand-rolled deterministic SVG string
     (reuses P0/P3 node/port/path geometry verbatim; node colors resolved to constant light-mode
     hex from `globals.css`, documented, since a standalone exported file has no CSS-var access —
     NOT a new dependency). PNG conversion client-side via the Canvas API (`svgToPngBlob`,
     2× scale). No html2canvas, no new dependency. Same renderer also draws the Präsentationsmodus
     map (one visual language, not two).
  3. **Daten-Tabelle (XLSX):** `buildWertstromDataTableConfig` — same `exportToXlsx`
     (ExcelJS) infra; columns = the P4 Tabellen-Ansicht's own columns (Name/Zykluszeit/
     Mitarbeiter/Status) plus one Provenance column. Row scope = every zeittragende
     Stations-Node (`process`+`machine`), numeric cells as real numbers (not stringified) — P7
     fix-round corrections (adversarial review): originally `process`-only (a machine-only
     Wertstrom rendered zero rows) with the legacy `findBottleneckId` heuristic for the Status
     column ("a byte-faithful mirror of the on-screen table, not a second bottleneck opinion" —
     that reasoning is reversed by the same Grundsatz-Entscheidung below) and numeric fields
     written as text. Now uses `computeBottleneckV2` (with a "Methodik"-footer disclosing this),
     consistent with PDF/SVG/PNG/Präsentationsmodus/Management-Analyse.
  4. **Copilot-Report:** behind the EXISTING `copilotExports` gate (`false` in every profile,
     unchanged) — extends the pre-existing `buildWertstromCopilotDocx`/`buildWertstromCopilotMd`
     (Demo-067 C/E) with 3 new additive sections: a "Copilot-Anweisung" section carrying the
     §19.3 instruction block **verbatim in English**, plus a German translation (this repo is
     German-first); a "Management-Analyse" section (Business-Interpretation = the §18 sentences
     above); and an explicit "Provenance-Übersicht" (measured/planned/imported/calculated/assumed
     counts, `null` shown as "Ohne Herkunftsangabe", never hidden). Every pre-P7 field/section is
     untouched (existing `wertstrom-docx.test.ts`/`wertstrom-md.test.ts` assertions stay green).
  5. **Technisches JSON:** `vsm-export-json.ts` — full nodes/connections/layout/Szenario-Metadaten
     passthrough with a `schemaVersion`/`adapter` field (no library needed).
  `vsm.is_demo` is not special-cased anywhere in this export family (exportable like any other
  map, per Brief).
- **Contract changes:** none to the API/DB — entirely new, additive client-side modules plus 2
  optional fields on `WertstromDocxInput` (`demandUnitsPerDay`/`shiftModel`, threaded through by
  `copilot-export-actions.ts`'s existing loader, same derivation `page.tsx` already uses for the
  live editor).
- **Nicht-Scope (declared):** DOCX export format (P8/TODO, execution-prompt §19.1 only lists PDF/
  SVG/PNG/XLSX/Copilot/JSON as the primary-action choices, DOCX is listed in §19's format-support
  preamble but not in §19.1's actual selection list); "Produkt"/"Wertstrom-Scope" as report fields
  (no such columns exist on `projects`/`value_stream_maps` — surfaced as a documented gap in the
  Management-Report's own Annahmen-Abschnitt, not fabricated from title/description); the visual
  map is NOT embedded inline in the PDF (the shared `ExportSection` shape used by every other PDF
  export in this repo has no image-section type — a cross-cutting change beyond this phase; the
  map is its own export instead); no Simulation; no new PDF/Canvas stack; no new permission code.
- **iOS scope:** web-only (the VSM editor itself is web-only); no separate iOS work.

### Wertstromanalyse — Excel-Roundtrip-Import (Kais-Auftrag TG9112/TG9113, KAR-878/KAR-986)
"Ich will den Wertstrom mit Excel importieren können — Vorlage herunterladen, ausfüllen,
wieder hochladen." Complete behind the pre-existing `wertstromUxV2` flag (no new flag/gate/
permission code). Deliberately no phase number in this heading — the P7 section above
already uses "P8" informally for a different future item (DOCX export); see this feature's
CHANGELOG entry.

**Architecture:** entirely CLIENT-SIDE (ExcelJS was already in the editor's client bundle
since P7's `vsm-export-xlsx.ts`/`lib/export/export-service.ts` — verified before writing any
code). Card creation reuses the EXISTING `POST /api/wertstrom` + `PUT /api/wertstrom/[id]`
routes verbatim — no new server route, no new authz/ownership logic, no idempotency
machinery (a double import creates a second card, exactly like a double Wizard-"Fertigstellen"
— the Vorschau step makes this visible before it happens, same as the Wizard's own posture).

- **Vorlagen-Format = Export-Format (`vsm-export-xlsx.ts`):** the existing "Wertstrom
  exportieren" → "Daten-Tabelle (XLSX)" output IS the roundtrip template format. A new
  "Wertstrom" meta-sheet (Titel, Kundenbedarf, Arbeitszeitmodell — the same 3
  `ShiftModelInput` fields P4 already persists) sits BEFORE the station table. The station
  table sheet gains 4 additive trailing columns: OEE (%), Ausschuss (%), Bestand davor
  (Stk), Typ — the 3 fields Kais' TG9113 commitment named that the pre-existing 5-column
  sheet (Name/Zykluszeit/Anzahl Mitarbeiter/Status/Herkunft) never had, plus "Typ"
  (Prozess/Maschine, case-insensitive, defaults to Prozess — Fix-Runde PR #362, C4: without
  it every "machine"-type station silently became a generic "process" node on reimport);
  the pre-existing 5 stay byte-identical in name/order/meaning. "Bestand davor" is resolved
  from the Inventory node(s) connected immediately upstream via a materialFlow connection —
  SUMMED when there is more than one (Fix-Runde PR #362, C13: with a visible Status-column
  note, e.g. "Bestand: mehrere Puffer summiert" — never a silent first-wins pick), and
  created on reimport for ANY explicitly-set value including a real `0` (Fix-Runde PR #362,
  C3: leer≠0 doctrine). It is not a field on the process node itself.
  A %-formatted OEE/Ausschuss Excel cell (autoformat from typing "85%") is normalized ×100
  with an info note rather than taken as a raw 0–1 fraction (Fix-Runde PR #362, C1 —
  CRITICAL: previously a silent ~100× capacity-calculation error); without a %-format, a
  parsed OEE value between 0 and 1 gets a plausibility nudge (Ausschuss deliberately exempt
  — a real sub-1 scrap rate is plausible).
  **Repo-reality deviation (declared):** the pre-existing sheet was named `'Prozessschritte'`,
  not `'Stationen'` as Kais' TG9113 commitment (and this feature's own brief) assumed —
  renamed to `'Stationen'` (a tab-caption-only change; columns/order/semantics of the
  pre-existing sheet are untouched) so the shipped file matches the customer promise.
- **Vorlagen-Download (`buildVsmImportTemplateConfig`):** a blank template — same two
  sheets plus one dedicated "Hinweise" sheet (Pflichtfeld nur Name, Einheiten, "leer lassen
  = nicht erfasst — NICHT 0 eintragen") and ONE clearly-marked example row, recognized and
  skipped on import ONLY while byte-identical to what was shipped (editing any single field
  turns it into real data). Reachable both from the new Import-Dialog and (additively) from
  the existing Export-Dialog.
- **Parser (`vsm-import-xlsx.ts`):** reads both sheets by name (order-independent). Native
  Excel number cells parse directly; text cells reuse the EXISTING P4 canonicalization —
  `numericCellOrUndefined` (`vsm-excel.ts`) and `parseGermanTolerantCell`
  (`vsm-table-paste.ts`, now exported for this reuse) — not reimplemented. A blank cell is
  always `undefined` ("nicht erfasst"), never a fabricated `0`; a real `0` (e.g. "0 %
  Ausschuss") stays a real, distinguishable `0`. Unrecognized/ambiguous number text and
  genuinely non-numeric ("Müll") cells each get their own warning with an Excel cell
  reference (e.g. "Stationen!C4: …"), never a silent drop. Unknown extra columns are
  ignored with one warning per column (not per row); an unrecognized label on the
  "Wertstrom" meta-sheet gets the same treatment (Fix-Runde PR #362, C8 — names the
  expected labels; the sheet's own title row is exempt). A missing "Wertstrom" sheet
  degrades to Stationen-only with a filename-derived title (editable in Vorschau); a
  missing/unrecognizable "Stationen" sheet degrades to zero stations with an explicit
  warning, never a crash. Sheet-name lookup is case-/whitespace-tolerant (Fix-Runde PR
  #362, C9), matching this same parser's column/label matching. A Date/time-typed cell in
  a numeric column gets a readable Hinweis instead of a raw JS `Date.toString()` (Fix-Runde
  PR #362, C10). The exporter's own "(unbenannt)" display-fallback for an empty station
  name is treated as an empty name on reimport, not a real persisted name (Fix-Runde PR
  #362, C11).
- **Wertstrom-Erzeugung (`buildImportedVsmGraph`):** builds Lieferant + one Prozess-/
  Maschinen-Node (per the Stationen-row's "Typ" — Fix-Runde PR #362, C4) per Stationen-Zeile
  (+ an Inventory-Node directly before it whenever "Bestand davor" is EXPLICITLY set,
  including a real `0` — Fix-Runde PR #362, C3: leer≠0 doctrine) + Kunde, connected as a
  single linear materialFlow chain — deliberately mirrors the P3 Quick-
  Start-Wizard's own `buildWizardGraph` shape-for-shape (unconditional Lieferant/Kunde
  boundary nodes, `isValueAdded: true` on every process node, the SAME
  `buildLinearConnectionPairs`/`computeAutoLayout` reused, not reimplemented) so the Excel
  import produces the identical graph shape every other "build a fresh linear Wertstrom"
  path in this codebase already produces, rather than inventing a second convention.
  Kundenbedarf is written to the Kunde-Node's `demand` field — the exact Wizard convention,
  no second demand concept. `provenance: 'imported'` is set on a process node only when it
  actually carries at least one imported scalar value (cycleTimeSec/numWorkers/oee/
  scrapRate) — a Name-only row makes no provenance claim for data it doesn't have.
- **UI-Flow (`vsm-import-xlsx-dialog.tsx`, gated behind `wertstromUxV2`, own entry point
  "Aus Excel-Vorlage" / "Mit Excel-Vorlage erstellen" — independent of the P6
  `wertstromSimvsmImport` flag, next to it in the Wertstrom-Liste):** Datei wählen
  (client-side .xlsx/2 MB check) → Vorschau (Titel editierbar, Projekt-Zuordnung optional,
  Stations-Tabelle, Hinweise mit Zellbezug as `role="alert"`, "X Stationen, Y Bestände
  werden angelegt") → Bestätigen → Erfolg (link to the new card). "Bestätigen" mirrors the
  Wizard's own F1/F2/F3 error-hardening byte-for-byte: the POST/PUT sequence is inside
  try/catch (a network failure REJECTS, an HTTP error RESOLVES with ok:false — both always
  clear the busy state), `created` (component state) is checked first so a retry after a
  failed PUT skips straight to the PUT instead of re-POSTing a second orphan row, and a PUT
  failure/network error after a successful POST carries a direct "im Editor öffnen und dort
  speichern" recovery link — but ONLY for a file the user hasn't since replaced: picking a
  DIFFERENT file via "Andere Datei wählen" resets the retry state so Bestätigen POSTs a
  fresh card instead of grafting the new file's stations onto the old one (Fix-Runde PR
  #362, C16 — CRITICAL). A11y: real `<h2>/<h3>` headings, Escape + a local focus-trap
  identical to the Wizard's, focus returns to the triggering element on close (Fix-Runde PR
  #362, C6 — same WAI-ARIA pattern `vsm-presentation-mode.tsx` already implements),
  Hinweise text uses the AA-contrast-safe `text-amber-700 dark:text-amber-400` (Fix-Runde
  PR #362, C7 — not `text-warning`, which fails WCAG AA at ~2.85:1 on the light theme). A
  value outside the `VsmNodeSchema` bounds (OEE/Ausschuss 0–100, Zykluszeit/Mitarbeiter/
  Bestand ≥0) is flagged in the Vorschau and blocks "Bestätigen" with a clear summary
  (Fix-Runde PR #362, C5) instead of letting the server's atomic, all-stations-or-nothing
  PUT rejection be the only, uninformative backstop.
- **Roundtrip-Pflichttest:** a card with known nodes is rendered through the REAL
  `exportToXlsx` (ExcelJS, not mocked) and then the REAL parser — the resulting stations/
  meta are asserted equivalent (the Status/Herkunft columns are tolerated/ignored, per
  design). `components/wertstrom/__tests__/vsm-import-xlsx.test.ts`.
- **Scope decisions:** V1 is a linear chain only (no branching, matching the Wizard's own
  V1 scope and Kais' explicit "V1 = lineare Kette"); no idempotency/duplicate-detection (a
  double import is a second card, visible in Vorschau before it happens); the Stationen-
  columns (OEE/Ausschuss/Zykluszeit/Mitarbeiter/Bestand) get a client-side Vorschau range
  check mirroring `VsmNodeSchema` (Fix-Runde PR #362, C5), but the "Wertstrom" meta-sheet's
  own numeric fields (Kundenbedarf, shift-model figures) do not — a shift model with an
  out-of-range value still fails the SAME way a Wizard-entered one would (server-side Zod
  only, an accepted, pre-existing, deliberately out-of-scope gap for those specific fields).
- **iOS scope:** web-only (the VSM editor itself is web-only); no separate iOS work.

### Wertstromanalyse — Kaizen-Marker + Maßnahmen-Verknüpfung (Wertstrom P8.1, KAR-878/KAR-986)
execution-prompt §15.5 "Measure Management", Capability-Matrix A18 ("Kaizen-Marker-Element +
'Maßnahme erstellen' am Element, Link-Feld statt Duplikat"). Complete behind the pre-existing
`wertstromUxV2` flag (unchanged `true` in `default`/`bmw`) — no new permission code, no profile
change. **Architecture decision: NO DDL.** `workshop_actions` (existing table, existing Workshop-
Modul `app/project/[id]/workshop/`) is untouched — the link lives entirely on the VSM side, as two
additive-nullish `VsmNode` fields (P2 convention): `kaizenNote?: string` and
`measureRefs?: string[]` (`lib/vsm-types.ts`, `lib/api/schemas.ts` `VsmNodeSchema`).

**Anlage-Pfad Repo-Realität (declared deviation from the brief):** the brief assumed the existing
measure-creation path might be a Server-Action or API route. It is neither — `workshop_actions`
rows are created with a plain client-side `supabase.from('workshop_actions').insert(...)` call
(`components/workshop/workshop-client.tsx`'s `addAction()`), RLS-enforced
(`workshop_actions_own`: `project_id IN (SELECT id FROM projects WHERE user_id = auth.uid())`),
consistent with this repo's API-First Principle ("no custom API routes unless…"). The new
"Maßnahme erstellen" dialog reuses this exact path verbatim — no parallel route.

- **Kaizen-Marker (Baustein 1):** a Node-Panel action ("Kaizen-Chance markieren"/"Entfernen",
  P4-tier panel, any node type — a Kaizen-Chance/Engpass/Risiko is not process-only) sets/clears
  `kaizenNote`. Presence of the field (even `''`) IS the marker, independent of whether text has
  been typed yet. Canvas shows a dezent ⚡-Badge on the node header (amber-700/dark:amber-400 —
  the AA-safe warning idiom PR #362 established, reused rather than a new `--vsm-*` variable or
  the contrast-failing `text-warning`), `title`/`aria-label` carry the note text. The P4
  Tabellen-Ansicht gets a "Kaizen" column with the same icon (title = hover text). Both are gated
  behind `wertstromUxV2Enabled` — a flag-off session renders byte-identical even for a node that
  already carries `kaizenNote` from an earlier flag-on session.
- **"Maßnahme erstellen" (Baustein 2, new `vsm-measure-dialog.tsx`):** ONE dialog (Radix `Dialog`,
  controlled — mounted/unmounted by `vsm-editor.tsx`, not self-triggering like the pre-existing
  Export-Dialog), reachable from two places sharing the same component: the Node-Panel (once a
  node is Kaizen-marked) and the Management-Analyse's Engpass-Absatz (only when a real primary
  bottleneck exists — `bottleneck-primary` confidence !== `'not-computable'`). Titel/Herkunft
  prefilled from context ("Engpass X entlasten" / the Kaizen-Notiz text; Herkunft = "Aus Wertstrom
  '…', Element/Management-Analyse '…'"). `workshop_actions` has no dedicated title column (no
  DDL) — `description` carries `"<Titel>\n\nHerkunft: <Text>"`, parsed back apart for display by
  `measureTitle()` (`vsm-measures.ts`). Owner/Fälligkeit/Priorisierung map onto the EXISTING
  columns verbatim: `responsible`/`target_date`/`effort`+`benefit` (this schema has no single
  "Priorität" field — the Aufwand/Nutzen pair already IS that concept, per the pre-existing
  Workshop-Modul's own Effort/Benefit-Matrix; reused, not invented). New measures are tagged
  `area: 'Wertstrom'` (a fixed literal on the existing freeform column) so they're recognizable in
  the pre-existing Workshop-Liste. Gated to a project-linked card ("NUR bei projekt-verknüpfter
  Karte") — otherwise the action renders disabled with a visible "Karte zuerst einem Projekt
  zuordnen" hint (discoverable, not hidden).
  **Save timing (corrected — this paragraph was stale):** the original "Judgment call" here
  described `measureRefs` as appended via the generic `updateNode`/isDirty/Speichern/Autosave path,
  same as any other field-edit. That was superseded during the SAME PR's adversarial-review Fix-
  Runde (K1, "Orphan-Maßnahme", confirmed major): an insert already commits a real `workshop_actions`
  row BEFORE the link is queued, so a lost/delayed link left a genuine foreign object silently
  orphaned (not just an unsaved field, unlike every other edit in this panel). Fixed: `onCreated`
  (`linkMeasureToNode`, vsm-editor.tsx) now fires a small, IMMEDIATE, targeted PUT — `nodes` only —
  right after the insert, and the dialog stays open with a specific message on anything other than
  success (see `vsm-measure-dialog.tsx`'s own doc comment). P8.2a (Baustein 4 below) reuses this
  SAME corrected function for its second link path.
- **Anzeige (Baustein 3):** the Node-Panel lists linked measures (Titel/Status/"Im Workshop-Modul
  öffnen" — links to the module, not a specific in-page anchor, since the existing Workshop-Client
  has none and adding one is out of scope). A dead ref (the `workshop_actions` row was deleted) is
  shown honestly as "Maßnahme nicht mehr vorhanden" with a Remove action — never silently filtered
  out of the list.
- **P7-Anschluss (Baustein 4):** `app/wertstrom/[id]/page.tsx` fetches every `workshop_actions` row
  of the linked project (ONE query, RLS) when `vsm.project_id` is set, passed to `VsmEditor` as
  `projectMeasures` (`null` when no project — distinct from `[]`, mirroring the `openMeasures`
  contract's own "no data source" doctrine). `vsm-measures.ts`'s `deriveOpenMeasureInputs` resolves
  every node's `measureRefs` against it (status `open`/`in_progress` = "offen", deduped by id) and
  is threaded into `computeManagementAnalysis`'s pre-existing `openMeasures` parameter (P7,
  documented as never populated until now) — the §18 "offene Maßnahmen" sentence becomes real
  (count + titles) instead of "nicht bewertbar". PDF/Copilot exports (which call
  `computeManagementAnalysis` directly, not through the React view) are NOT wired to this — out of
  scope for this phase, they keep the P7 "nicht bewertbar" behavior unchanged.
- **Szenario-Verhalten (Baustein 5):** `POST /api/wertstrom/[id]/duplicate` copies `kaizenNote`
  unchanged (a content field, like `notes`) but strips `measureRefs` from every node
  (`stripMeasureRefsForDuplicate`) — a measure belongs to the Ist-Zustand; a copied reference would
  double-count it as "open" against both the Ist AND the newly derived Szenario.
- **P8.2a-Addendum — "Bestehende Maßnahme verknüpfen" (Baustein 4, KAR-878/KAR-986, TODO.md
  P8.2-Kandidat aus PR #363 Fix-Runde K1):** the SECOND path into `measureRefs` — P8.1 above only
  built "erstellen". New `vsm-link-measure-dialog.tsx` (`VsmLinkMeasureDialog`) picks an
  ALREADY-EXISTING `workshop_actions` row of the linked project (any status, not just "offen") and
  links it via the exact SAME `linkMeasureToNode` (K1 Sofort-PUT) the create-flow uses — no parallel
  persistence path, no new query (reuses `vsm-editor.tsx`'s already-loaded `measures` state). STRIKT
  im P8.1-Rahmen: identical `canManageMeasures`/`project_id` gate (Node-Panel button, "Bestehende
  Maßnahme verknüpfen", sits right below "Maßnahme erstellen"), identical dreiwertige RLS-Ehrlichkeit
  for display. "Duplikat-Verknüpfung verhindert" (Brief-Testanforderung) has TWO layers: the picker
  filters an already-linked measure out of its own option list, AND `linkMeasureToNode` itself
  (`isMeasureAlreadyLinked`, `vsm-measures.ts`) makes a repeat-link idempotent (`'ok'`, no second
  array entry) — a shared, unit-tested guard both the create-flow (defense in depth) and this new
  flow rely on. Unlike the create-flow, a failed link attempt here carries NO orphan risk (nothing
  new was created) — the dialog simply shows an error and can be retried. **Nicht-Scope (declared,
  Stufe-4-Migration mit Kais):** the §15.5 5-field gap (root cause/target state/expected effect/
  actual effect/linked KPI) named in P8.1's own "Nicht-Scope" above remains open — this Baustein
  adds a linking UI, no schema change.
- **Contract changes:** `VsmNode`/`VsmNodeSchema` gain `kaizenNote?`/`measureRefs?` (additive,
  nullish, `.passthrough()`-tolerant — an existing saved Wertstrom round-trips unaffected).
  `VsmManagementAnalysisView` gains `openMeasures?`/`vsmTitle?`/`projectId?`/`onCreateMeasure?`
  (all optional, byte-compat default for the export call sites and any pre-P8.1 test). No DB
  migration, no new API route, no new permission code.
- **Nicht-Scope (declared):** the §15.5 field list this phase does NOT add to `workshop_actions`
  (no DDL, TODO.md "P8-DoD"): root cause, target state, expected effect, actual effect, linked
  KPI. Excel/JSON/SVG/PDF export and Copilot export do not surface `kaizenNote`/`measureRefs` (not
  requested this phase; the fields pass through the technical-JSON export's full-node dump
  untouched, just unrendered elsewhere). No new `NodeType` for Kaizen-Hinweise (distinct from the
  still-open SimVSM-import gap in TODO.md "Wertstrom P6" `noteVSM`/`noteAnnotation" — that gap is
  about IMPORTING a Kaizen-Hinweis into a new node type and remains unresolved; this phase's
  marker is a manually-set field on an EXISTING node, not a new node kind).
- **iOS scope:** web-only (the VSM editor itself is web-only); no separate iOS work.

### Wertstromanalyse — Referenz-Beispielwertstrom "Elektronisches Steuergerät" (Kais-Auftrag, KAR-878)
Kais' direct order (bindend, "vollständig autonom"): a fully-worked, richly-connected reference
value stream — 2nd-Tier supplier (AluForm Components GmbH, fictional) → Tier-1 (E-Motion Systems
GmbH, fictional) → customer (BMW, this repo's real product context) — demonstrating every existing
Wertstrom capability (P0–P8.1) end-to-end, plus two genuinely new capabilities the order named
that nothing prior actually built: a classic zigzag/Treppen-Zeitlinie and a CT-vs-Takt bar chart.
**Architecture decision: NO DDL, additive-only.** No new `NodeType`, no new `VsmNode`/
`VsmConnection` field, no migration, no new API route, no new permission, no profile change — every
requirement maps onto fields/mechanisms that already existed (see "5 kritische Fallen" below).

- **Seed-Datensatz (`lib/demo/seeds/wertstrom-seed.ts`, `DEMO_VSM_REFERENCE_ECU`):** 24 nodes / 30
  connections (23 materialFlow, 7 information). Standalone (`project_id: null`, "kein Projekt" — an existing, tested display path;
  a dedicated `projects`/`supplier_master_data` row was judged unnecessary scope for a
  visit-independent flagship example). `id: DEMO_VSM_IDS[8]` (the shared pool extended by one, not
  a separate pack) — seeded/removed through the existing `'wertstrom'` demo pack, zero route
  changes. Every number (cycle times, setup times, availability, scrap, quantities, batch sizes,
  the 2-shift/8h/30min-break work model) is copied verbatim from Kais' order §3–§9; only the
  company names are invented.
- **Taktzeit computed live, not stored (§4):** `layout.shiftModel` + the customer node's `demand`
  feed `lib/vsm-engine`'s pre-existing `computeTakt`/`computeNetAvailableTimePerDay` — reproduces
  Kais' own worked example exactly (900 min/Tag → 54.000 s → 112,5 s Takt). `vsm-editor.tsx` now
  falls back to this live computation (`taktTimeSecFromProject ?? liveTaktFallback.taktTimeSec`)
  **only** when no linked project supplies `customer_takt_time_sec` — every pre-existing Wertstrom
  with a real project-level takt is byte-identical (this was previously the sole takt source for
  the whole editor, a QVS-P4-era scope decision, not a hard constraint — see that phase's own
  "Takt-Vergleich bleibt an `projects.customer_takt_time_sec`" note).
- **Zickzack-/Treppen-Zeitlinie (§8, new `lib/vsm-engine/internal/timeline-sequence.ts` +
  `components/wertstrom/vsm-timeline-zigzag.tsx`):** the pre-existing "Timeline-Leiter"
  (`vsm-timeline-ladder.tsx`) is a single proportional bar, not the classic alternating-above/below
  staircase Kais' order describes in detail — a real, previously-unbuilt gap, not a naming
  mismatch. `computeTimelineSequence` orders every node via Kahn's topological sort over
  materialFlow edges (chosen over a plain DFS: a DFS does not wait for a node's OTHER predecessors
  at a genuine merge point — this reference wertstrom has one, the Aluminiumgehäuse-Eingangssupermarkt
  and the Leiterplattenbestückung-FIFO-Kette both feeding "Gehäusevorbereitung" — and would silently
  mis-order everything reachable only via the untaken path). Segment width is schematic (equal per
  node, duration labeled), not literal-time-proportional — a linear time axis would make every
  process segment an invisible sliver next to a multi-day inventory peak. Rework/reprocessing loops
  (a node with exactly one incoming + one outgoing materialFlow edge to/from the SAME host) are
  excluded from the graph before sorting (cycle-safe by construction) and shown as a `↺` annotation
  on their host instead of a full-weight sequence step — they do not affect 100% of units. Category
  sums are asserted equal to `computeTimelineLadder`'s own totals (never a second, diverging figure,
  §17.12). Click-to-select is bidirectional with the canvas (shared `selectedNodeId`).
- **Zykluszeit-Taktzeit-Diagramm (§9, new `components/wertstrom/vsm-ct-takt-chart.tsx`):**
  horizontal Recharts bar per process/machine node with a set cycle time (a floor via `type`/
  `cycleTimeSec` filtering, not a hardcoded process list — a user-added process appears
  automatically), colored under/near/over Takt (a documented 90% "near" threshold, same posture as
  `bottleneck.ts`'s own `SECONDARY_WATCH_THRESHOLD_PCT`), a dashed `ReferenceLine` at the live
  Takt, and a non-color-only "Größter Engpass" text label (A11y). Also surfaces the two §15
  Kennzahlenübersicht items no other existing view renders: kumulierter First Pass Yield
  (`computeCumulativeYield`, product of `100 − scrapRate` across every node with a known scrap
  rate) and the theoretical daily capacity of the bottleneck (`computeNetAvailableTimePerDay`(
  shiftModel) ÷ `computeEffectiveCapacity`(bottleneck node).effectiveCycleTimeSec — the SAME
  capacity engine `computeBottleneckV2` already uses, never a second capacity concept). Bar click
  ↔ canvas/Zeitlinie selection is bidirectional and now also **brings the node into view**
  (`vsm-editor.tsx`'s new `panToNode`/`selectAndRevealNode` — re-centers the canvas viewport on the
  selected node at the current zoom; a direct canvas click does not trigger this, since the node is
  visible there by definition and an unprompted re-center would be unwanted UX churn).
  **Tier-1-Scope (§9 "alle relevanten Prozesse DES TIER-1-LIEFERANTEN") + A11y-Fix
  (Referenzwertstrom-Fixrunde, KAR-878):** an optional `tier1NodeIds` prop, resolved by
  `vsm-editor.tsx` from `layout.zones` (see K-ZONEN below), scopes the chart to the Tier-1
  supplier's own processes when zones are defined — the reference Wertstrom shows exactly the 5
  Kais-named E-Motion processes, not also AluForm's 3 2nd-Tier processes (`undefined` = no zones =
  every process/machine node charts, unchanged fallback for every other Wertstrom). The chart also
  carries `role="img"` + a dynamic `aria-label` (Taktzeit + Prozessanzahl + Engpass), and a
  `sr-only` accessible table mirrors the bidirectional Balken↔Prozess-Kopplung as real, focusable,
  keyboard-/Screenreader-operable rows (Recharts' SVG bars themselves are not independently
  focusable, and — like every Recharts chart in this repo — not clickable in jsdom either; the
  accessible table is both the A11y fix and the only real-DOM surface `__tests__/
  vsm-ct-takt-chart.test.tsx` can exercise for Kais §19.15 "korrekte Zuordnung von Diagrammbalken
  und Prozess").
- **Nacharbeits-Rückfluss (§7, "5 kritische Fallen" Falle 2):** deliberately modeled WITHOUT a
  literal graph cycle — EOL → Nacharbeit → the SAME downstream Prüf-/Freigabepuffer the good parts
  already flow into (a side-branch that re-converges, not a back-edge into EOL itself). This is a
  genuine DAG (verified: a DFS/Kahn cycle search over the seed's materialFlow edges finds none) —
  `runValidation` reports zero `structural.unexplained-loop` issues for this Wertstrom.
  **Fix (K-REWORK, Referenzwertstrom-Fixrunde, KAR-878) superseding an earlier draft of this
  section:** the earlier draft (correctly) noted that `computeTimelineSequence`'s rework-loop
  detection originally required a LITERAL same-host cycle (incoming AND outgoing materialFlow edge
  to/from the SAME node) — a shape this seed's deliberate side-branch topology does NOT match — and
  concluded the Nacharbeit node would render as an ordinary, unflagged sequence block, violating
  Kais §7 ("Der Rückfluss muss visuell verständlich sein und darf nicht mit dem normalen
  Hauptmaterialfluss verwechselt werden"). Rather than accept that violation or switch to a literal
  cycle (which would have reintroduced a `structural.unexplained-loop` warning), the detection
  itself was generalized: `findReworkLoopNodeIds` (`lib/vsm-engine/internal/timeline-sequence.ts`)
  now ALSO recognizes a parallel side-path — a node whose incoming edge comes from a host P and
  whose outgoing edge lands on a node H that P ALSO reaches via its own direct edge (exactly this
  seed's EOL → Nacharbeit → Prüfpuffer, alongside the direct EOL → Prüfpuffer edge) — as a
  rework-loop appendage attached to the host, same as the literal-cycle shape. Nacharbeit now IS
  excluded from the Zickzack-Zeitlinie's main sequence and DOES render the `↺`-annotation on the
  EOL block, while the seed's topology (and its zero-warnings `runValidation` result) stays
  unchanged — both requirements met simultaneously, no trade-off needed. Generic tests (not
  seed-specific ids) cover the new shape plus a negative case (a plain pass-through node — e.g. a
  transport step — with no parallel shortcut edge is NOT misdetected as a rework loop),
  `lib/vsm-engine/__tests__/timeline-sequence.test.ts`.
  No numeric corruption either way: `bottleneck.ts`/`timeline-ladder.ts`/`capacity.ts`/
  `inventory-coverage.ts` never traverse `VsmConnection` at all (flat `nodes.filter(...)` only). The
  rework node's `type` is `'timevalue'`, deliberately NOT `'process'` — `isStationNode` treats every
  `process`/`machine` node as a 100%-weight capacity station, which a first draft using `'process'`
  exposed as a real bug (the rework node's raw 180 s outranked EOL's ~133 s effective cycle time as
  "the" bottleneck, caught by `__tests__/wertstrom-seed-reference-ecu.test.ts`, never shipped) —
  `'timevalue'` is an existing, semantically-fitting type `isStationNode` does not match, so no new
  `NodeType` was needed. **Documented, accepted limitation:** both `computeTimelineLadder` and
  `computeTimelineSequence`'s linear category sums still add the node's 180 s at full (100%) weight
  rather than 2,2%-probability-weighted — a pre-existing engine property (Gap G6, "no parallel-/
  conditional-path weighting"), immaterial here in magnitude (180 s against a total lead time
  dominated by multi-day inventory coverage) but disclosed, not hidden.
- **Produktionssteuerung/PPS (§5, Falle 5):** no new `NodeType` — a plain `process` node,
  `isValueAdded: false`, no `cycleTimeSec`, connected via `kind: 'information'` edges to the
  customer, the 2nd-Tier supplier, and the first Tier-1 station (Push-Auslöser), same pattern
  `top-management-demo-vsm.ts` already established for a non-physical station.
  **Korrektur (Referenzwertstrom-Fixrunde, KAR-878) einer früheren Kausal-Behauptung dieses
  Abschnitts:** PPS wird mit einer eigenen Ausschluss-Zeile aus der Engpass-Kandidatenliste
  genommen (`computeBottleneckV2`, `stationNodes`-Filter), aber die Engpass-Konfidenz dieses
  Wertstroms ist bereits UNABHÄNGIG davon `'medium'` — keiner der 8 echten Prozessknoten trägt
  `oee`/`capacityPerHour` (nur `availabilityPct` per Override, das `allWellMeasured` in
  `bottleneck.ts` heute nicht berücksichtigt). Empirisch verifiziert: `computeBottleneckV2` liefert
  `'medium'` sowohl MIT als auch OHNE PPS im `nodes`-Array — PPS' Process-Typisierung senkt die
  Konfidenz nicht von `'high'` auf `'medium'`, sie entfernt lediglich die eine PPS-spezifische
  Ausschluss-Zeile aus `explain.exclusions`. Ein neuer `NodeType` hätte diesen Effekt daher NICHT
  vermieden (die vorherige Aussage dazu war falsch) — die Aufwandsabschätzung für einen neuen
  `NodeType` (`vsm-config.ts`, `validation.ts` `labelForType`, `timeline-ladder.ts`
  `isExcludedFromProcessCategories`, jeder `vsm-editor.tsx`-Type-Branch, die Excel/PDF/SVG/XLSX-
  Export-Type-Switches, `vsm-auto-layout.ts`) bleibt zutreffend, ist aber gegenüber einem
  nicht-existenten Nutzen abzuwägen — bewusst NICHT umgesetzt, kein neuer `NodeType`.
  **Zusätzlicher Fix (K-PPS, dieselbe Fixrunde):** PPS ist ein reiner Informationsfluss-/
  Steuerungsknoten (keine `materialFlow`-Kante) und wurde von der Zickzack-Zeitlinie
  (`computeTimelineSequence`) vor dieser Fixrunde fälschlich NICHT ausgeschlossen — es erschien als
  chronologisch unsinniger "?"-Block nach der BMW-Auslieferung (Array-Index-Tie-Break sortierte ihn
  ans Ende). `computeTimelineSequence` schließt jetzt jeden Node ohne jede `materialFlow`-Kante
  strukturell aus (`isInformationOnlyNode`, `lib/vsm-engine/internal/timeline-sequence.ts`) — PPS
  erscheint nicht mehr auf der Zeitlinie, analog zum bestehenden customer/supplier-Ausschluss.
- **Firmengrenzen (§11, K-ZONEN, Referenzwertstrom-Fixrunde KAR-878):** Kais' eigenständiger
  "muss"-Satz ("Die Grenzen zwischen 2nd Tier, Tier 1 und BMW müssen visuell erkennbar sein",
  separat von der "Empfohlene Struktur"-Liste) war im ursprünglichen Ship unimplementiert und
  undokumentiert. Additiv gelöst über eine neue, optionale `layout.zones`-Struktur (`VsmZone[]`,
  `parseZonesFromLayout`/`computeZoneBounds`/`computeZoneDividers` in `vsm-geometry.ts` — selbes
  "neuer Key im bestehenden `layout`-jsonb-Bucket, keine Migration"-Muster wie `layout.shiftModel`,
  P4/B2): `vsm-editor.tsx` rendert dezente vertikale Trennlinien + Zonen-Labels auf dem Canvas, NUR
  wenn `zones` definiert ist (jede Bestandskarte ohne `zones` bleibt unverändert). Der Referenz-Seed
  definiert 3 Zonen (AluForm/2nd-Tier, E-Motion/Tier-1 inkl. PPS, BMW/Kunde). Ein optionales
  `role`-Feld je Zone (`'second-tier-supplier' | 'tier-1-supplier' | 'customer'`) macht die
  Firmenzuordnung strukturell abfragbar — dieselbe Zone, die die Trennlinie zeichnet, liefert auch
  den `tier1NodeIds`-Filter für das Zykluszeit-Taktzeit-Diagramm (§9, siehe oben) — EIN Mechanismus
  löst beide Lücken, kein zweites, unabhängiges "welche Firma"-Konzept.
- **BMW as literal seed data (Falle 1):** `check:forbidden`'s `FORBIDDEN_TERMS` list matches "BMW"
  as a standalone word anywhere under `app/`/`components/`/`lib/`/`hooks/`/`config/` outside a
  handful of allowlisted paths (ADR 010 R1) — this repo's own confidentiality tooling, unrelated to
  BMW being the product's real customer. Every literal "BMW" in the new seed/test files carries the
  existing `// allow-customer-string` line-escape (the SAME pattern `top-management-demo-vsm.ts`
  already established for `'BMW Werk (Demo)'`) — `CHECK_FORBIDDEN_LEVEL=error npm run
  check:forbidden` is clean.
- **Tooltips (§12.3):** Kais' three example hint sentences (Taktzeit/Zykluszeit/Durchlaufzeit),
  verbatim, as local (non-DB) `FieldHintContent` objects reusing the existing `FieldHint` component
  unchanged — no `lsc_tooltips` migration needed (no matching `'vsm.*'` key exists for these three
  general CONCEPTS, as opposed to the per-FIELD hints that already exist, e.g. `vsm.cycle_time`
  explains the "Zykluszeit (s)" input field, not the concept). Each rendered ONLY while its section
  is expanded (not in the always-mounted `<summary>`) — an earlier version rendered them
  unconditionally and broke an unrelated existing `vsm-editor.test.tsx` assertion via an
  `aria-label` substring collision (`getByLabelText(/Zykluszeit/)` matching the hint button too);
  fixed before shipping, not left as a known issue.
  **Fix (C11, Referenzwertstrom-Fixrunde KAR-878):** each of the three texts previously ALSO
  rendered a second time as a native, non-focusable HTML `title` attribute inside the child
  component itself (`vsm-ct-takt-chart.tsx`/`vsm-timeline-zigzag.tsx`) — a genuine duplicate
  (§22.16 "Oberfläche wirkt nicht überladen"), keyboard-/screen-reader-unreachable on top of that.
  Every text now lives exactly once, at the most relevant section: Taktzeit → Management-Analyse
  (the KPI-/Analyse-Bereich directly above the CT-Takt-Diagramm), Zykluszeit → CT-Takt-Diagramm
  (where cycle times are actually compared), Durchlaufzeit → Zickzack-Zeitlinie. The child
  components' native `title` duplicates were removed; the Durchlaufzeit-Zeitlinie's own
  DLZ:Bearbeitungszeit-Kennzahl (§8) — a real value, not a hint duplicate — stays in place.
- **Known pre-existing gap, newly exercised (not fixed here):** `vsm-auto-layout.ts`'s `topoOrder`
  (Kahn's algorithm, no cycle-exclusion) deadlocks on a 2-node materialFlow back-edge — every node
  topologically after such a cycle falls into its "leftover, original array order" bucket instead
  of a real position. No prior seed had a real cycle to expose this. This reference wertstrom ships
  with a fully hand-placed layout (unaffected), but clicking the pre-existing "Automatisch anordnen"
  button on it would hit the bug. `computeTimelineSequence` (this PR) solves the equivalent problem
  correctly by excluding identified rework-loop nodes/edges from the graph before sorting — the
  same fix is recommended for `vsm-auto-layout.ts` as a follow-up (see Abschlussbericht).
- **Weitere Fixes (Referenzwertstrom-Fixrunde, KAR-878 — adversarielles Multi-Verifier-Review von
  PR #365, 18 CONFIRMED Findings auf 13 Kerne dedupliziert):** Live-Takt-Vergleich jetzt SYMMETRISCH
  für beide Seiten jedes Ist/Soll-Vergleichs, jede Seite ihr eigener Takt aus IHREM shiftModel+
  Bedarf (`app/wertstrom/[id]/page.tsx`, C6), UND der Live-Takt-Fallback selbst jetzt hinter
  `wertstromUxV2Enabled` (C12, "Flag AUS ⇒ byte-gleich" auch für einen bestehenden projektlosen
  Wertstrom mit shiftModel+Bedarf). CT-Takt-Diagramm: `role="img"`+aria-label+`sr-only`
  Alternativ-Tabelle (C7, siehe oben). ↺-Nacharbeitsschleifen-Text jetzt `amber-700`/
  `dark:amber-400` (C13) statt `--status-yellow` (eine Badge-, keine Textfarbe — ≈1,3:1 statt
  WCAG-AA-4,5:1-Kontrast). de-DE-Zahlenformat (Komma statt Punkt) in beiden neuen Komponenten
  (C15: `112,5s`/`88,8%`/`822,9 : 1`). Kaizen-Hinweis + VSM-`description` der EOL-Station
  formulieren den Engpass jetzt ohne eingebackene Zahlen (C17 — nach einer erlaubten Live-Bearbeitung
  von Bedarf/Schichtmodell wäre der alte, hartcodierte "12,5s über Takt"-Text falsch geblieben,
  §22.23). PPS-Konfidenz-Kausalfehler in diesem Dokument korrigiert (C14, siehe oben).
- **Nicht-Scope (declared):** Undo/Redo and an optional canvas grid overlay (Kais §12.3 items 6/12)
  remain absent from the editor — a pre-existing gap (P8-Restliste A19), not introduced or widened
  here, and not among Kais' own 25 §22 acceptance criteria. Export surfaces (Excel/PDF/SVG/Copilot)
  do not render the new Zickzack-Zeitlinie/CT-Takt-Diagramm (UI-only per Kais §9's own "separat
  einblendbarer Bereich" framing).
- **iOS scope:** web-only (the VSM editor itself is web-only); no separate iOS work.

### Wertstromanalyse — VSM-Standard-Visualisierung: Symbolbibliothek, Canvas-Zeitlinie, VA-Sichtbarkeit, Informationsfluss (Kais-Live-Feedback 23.07., KAR-878)
Direct live feedback from Kais on the open reference Wertstrom (23.07., annotated screenshots): the
emoji icon set "trägt nicht" (does not carry the professional look), the zigzag timeline is "nicht
nach dem Standard" (a separate section with its own scale instead of living directly under the value
stream), the information-flow area above the process row is "viel zu unübersichtlich" (icons would
help), and value-added status is visually weak. Four bausteine, additive/no-DDL (no new `NodeType`,
no new `VsmNode`/`VsmConnection` field beyond one additive `layout.timelineOffsetY` key, no migration,
no new permission).

- **Baustein 1 — SVG symbol library (`components/wertstrom/vsm-symbols.tsx`):** 19 stroke-based
  React components (24x24 viewBox, `currentColor`, `size` prop, `aria-hidden`) covering Kais' §10
  list of 20 named symbols (two deliberate merges: supplier+customer share one factory pictogram,
  matching Rother/Shook's actual "Outside Sources" convention rather than inventing two; Pull+Kanban
  share one circle glyph, per the brief's own scope). `vsm-config.ts`'s `NODE_CONFIG.icon` is now a
  typed `VsmSymbolKey` (was a raw emoji character) resolved through `VSM_SYMBOL_COMPONENTS`/
  `VsmSymbolByKey` at every call site (Sidebar-Palette, Canvas-Nodes, Properties-Panel headers,
  Tabellen-View's Kaizen column, the P8.1 Kaizen-/Engpass-canvas-badges, the Szenario-Vergleichsview's
  node-type icons). A new `resolveNodeSymbolKey(node, nodes, connections)` picks a MORE SPECIFIC
  symbol than the bare type when real structural data justifies it — never a name/text guess:
  `inventoryKind` ('supermarket'/'fifo') selects the matching Lean icon over the generic inventory
  triangle; a `transport` node whose outgoing materialFlow edge lands on a `customer` node gets the
  "Versand" (shipping) icon instead of the generic Lkw-Transport icon; a `process` node wired up
  EXCLUSIVELY through `kind: 'information'` edges (same predicate `lib/vsm-engine`'s
  `isInformationOnlyNode` already uses) gets the PPS icon — **K6-Fix (Referenzwertstrom-Fixrunde 2,
  KAR-878):** this is a structural HEURISTIC, not a confirmed PPS identity — the data model has no
  field distinguishing "the central Produktionssteuerung" from any other purely-informational
  process a user might wire up the same way (Qualitätsbüro, EDI-Gateway, …), so `VSM_SYMBOL_LABELS.pps`
  (vsm-config.ts) is phrased "Informationsknoten (z. B. Produktionssteuerung/PPS)", not a bare
  confident "PPS" claim. **P8.2a-Update (Baustein 2 "PPS als echtes Datenfeld"):** that candidate is
  now built — `VsmNode.isPps?: true` (sparse, `.passthrough()`-tolerant, no DDL) WINS over the
  heuristic when set (`resolveNodeSymbolKey` checks it first, even overriding a node's own materialFlow
  connections — Judgment-Call, siehe Abschlussbericht), and `resolveNodeSymbolLabel` shows the
  confident "Produktionssteuerung (PPS)" wording specifically for that case, leaving the honest
  heuristic-only wording above unchanged for every node nobody has explicitly marked. The reference
  seed's PPS node (`ecuNid(23)`) now carries the field; SimVSM's `productionControl` mapping sets it
  too (`mapping-registry.ts` `targetIsPps`). Not part of the Excel-Roundtrip (same declared-non-scope
  as every other P2+/P8.1 VsmNode addition). **P8.2a-Review Fix-Runde (K4) — correction:** the
  confident-label RENDERING at `vsm-editor.tsx`'s Canvas-Node-Header and the OLD flat Properties-Panel
  was unconditional (unflagged) until now — since `isPps` is sparse/additive data (unaffected by this
  fix) but the reference/demo seed sets it on `ecuNid(23)` regardless of profile, a `wertstromUxV2:
  false` profile that loaded the reference seed would have seen the confident wording main never
  showed. `resolveNodeSymbolLabel` now takes a `confidentLabelEnabled` argument (defaults `true` for
  every pre-existing caller outside this fix's scope, e.g. the Vergleichs-Ansicht, gated on its own
  unrelated `wertstromScenariosEnabled`); both previously-unconditional `vsm-editor.tsx` call sites
  now pass `wertstromUxV2Enabled` explicitly. DATA stays additive/ungated — only the confident-wording
  RENDERING is now behind the flag, same split as the flowControl overlay's own correction below. The PPS symbol
  itself was also redrawn as a real crosshair (2 lines fully crossing its circle) — previously
  near-identical to `VsmSymbolMachine`'s short external ticks at the 12-16px sizes both actually
  render at. Edge arrowheads split into two markers — `vsm-arrow-material` (wide/blunt,
  "Push-Pfeil-Stil") for materialFlow, `vsm-arrow-info` (slim/open, "elektronischer" look) for
  information — replacing the one shared arrowhead marker before this. **K10-Fix (Referenzwertstrom-
  Fixrunde 2, KAR-878) — corrected count (historical):** FOUR symbols, not two, were fully built/
  tested but not wired into any live render path: `VsmSymbolQualityCheck`/`VsmSymbolInfoManual` (no
  `VsmNode`/`VsmConnection` field marks a process as a quality-check station or an information edge
  as "manual" vs. the assumed default "electronic") AND `VsmSymbolPushArrow`/`VsmSymbolKanbanPull`
  (no `VsmConnection` field distinguished push- from pull-/kanban-controlled edges either). **P8.2a-
  Update (Baustein 3 "Push/Pull/Kanban-Kantensemantik"):** the latter two ARE now wired — a new
  additive `VsmConnection.flowControl?: 'push' | 'pull' | 'kanban'` (materialFlow-only, validated at
  the schema level) drives an overlay icon at the edge midpoint, on top of the unchanged
  `vsm-arrow-material` arrowhead (never a replacement — pixel-identical without the field, per this
  Baustein's own doctrine). `flowControl` has NO computational effect anywhere in `lib/vsm-engine` —
  purely a visual/fachlich marker (Kais §5/§10). The gap-list is now down to TWO:
  `VsmSymbolQualityCheck`/`VsmSymbolInfoManual` remain unwired (still no field for either concept) —
  declared gap, not a silent omission, candidate for a future phase. **P8.2a-Review Fix-Runde (K4) —
  correction:** the overlay icon's RENDERING was deliberately left unflagged when this Baustein
  shipped ("renders whenever the field is present, regardless of the editing flag") — that reasoning
  addressed the DATA (field absent vs. present) but missed that the profile-independent reference/demo
  seed (`lib/demo/seeds/wertstrom-seed.ts`) sets `flowControl: 'kanban'` unconditionally on two edges.
  A `wertstromUxV2: false` profile (e.g. a fresh customer profile cloned from
  `config/profiles/_template.ts`) that loads that seed would have seen a NEW icon `main` never had —
  the exact "existing/loaded data ⇒ ungated new rendering" pattern the K12-precedent (CHANGELOG.md,
  C12, "major") already named and fixed by gating the RENDERING, not the data. Now: the overlay icon
  block in `vsm-editor.tsx` is gated behind `wertstromUxV2Enabled` too — `flowControl` itself stays
  additive/ungated data (still no migration, still sparse), only its VISUAL element moved behind the
  flag.
- **Baustein 2 — canvas-integrated standard timeline (`components/wertstrom/vsm-canvas-timeline.tsx`,
  replaces `vsm-timeline-zigzag.tsx`):** Kais, verbatim: "Die Zeitlinie … muss direkt unter dem
  Wertstrom sein, nur nach oben/unten verschiebbar, damit es mit dem Wertstrom verlinkt ist und
  erkannt wird." The band is no longer a separate `<svg>` in a collapsible section — it renders as
  raw SVG primitives (no wrapping `<svg>` of its own) spliced directly into the SAME `<svg>`
  vsm-editor.tsx already draws connections/zone-dividers in, inside the same pan/zoom-transformed
  world div — it inherits the canvas's own zoom/pan for free, no separate transform math. Alignment:
  every segment's x/width comes from `computeTimelineBandSegmentPositions` (vsm-geometry.ts), which
  reads it DIRECTLY off the segment's own node (`node.x`, `NODE_W`) — never a running sequence-index
  layout the way the replaced component worked. The band's vertical position is one shared `offsetY`
  (persisted additively as `layout.timelineOffsetY`, same "new key in the existing jsonb bucket, no
  migration" precedent `layout.viewport`/`layout.shiftModel`/`layout.zones` already established) —
  adjustable via a drag handle rendered inside the SVG (delegates its mousedown into vsm-editor.tsx's
  EXISTING `DragState`/pan-zoom-drag machinery, a new `'timeline-offset'` branch, never a second
  independent drag system) or a fixed +/− stepper pill anchored outside the canvas transform (so its
  buttons stay a constant screen size regardless of zoom). `resolveTimelineOffsetY`/
  `TIMELINE_BAND_GAP`/`TIMELINE_BAND_MIN_GAP`/`TIMELINE_BAND_UPPER_LANE_H` (vsm-geometry.ts) keep the
  band's upper lane from ever visually overlapping the node row, while still letting a user pull it
  as close as Kais' "direkt … kleben" asks for. The classic upper/lower zigzag split (Kais §8: NVA
  wait/inventory/transport above the midline, VA/NVA process time below) is unchanged — reuses
  `lib/vsm-engine`'s `computeTimelineSequence`/`computeTimelineLadder` verbatim, this is a rendering-
  only change. The rework-loop annotation now uses `VsmSymbolReworkLoop` instead of the literal "↺"
  character. The per-segment node-NAME text label the replaced component needed (its segments had no
  spatial relationship to the real node) is deliberately DROPPED here — redundant once x-alignment
  IS the correlation mechanism, and duplicating it caused real `getByText(nodeName)` collisions
  against ~30 existing call sites once the band became always-rendered instead of hidden behind a
  closed `<details>` (see CHANGELOG.md for the exact fix). Flag: `wertstromUxV2` (unchanged — the
  section it replaces was already fully flag-gated, so flag-off stays byte-identical).
- **Baustein 3 — VA/NVA visibility:** every process/machine/timevalue node (the same population
  `lib/vsm-engine`'s timeline engines classify as VA-eligible) now always shows one of three lines —
  "✓ Wertschöpfend", "NVA", or "Ungeklärt" — plus a thin green/grey footer bar for the first two
  (`--success`/`--status-grey`, existing tokens, decorative/`aria-hidden`, text is the AA-authoritative
  signal, not the bar alone; no bar at all for "Ungeklärt", see K8-Fix below). Before this, only the
  `true` case rendered anything; `false`/`undefined` were visually identical (nothing) — Kais:
  "Wertschöpfend soll besser sichtbar sein." **K8-Fix (Referenzwertstrom-Fixrunde 2, KAR-878):** the
  original `resolveIsValueAdded()` (binary) collapsed a genuine `vaClass==='nva'/'nnva'` classification,
  `vaClass==='unknown'` (a REAL, user-chosen 4-way-selector value — `VA_CLASS_LABELS.unknown` ===
  "Ungeklärt", qvs-field-labels.ts), and "never classified at all" onto the identical "NVA" text — a
  user who explicitly picked "Ungeklärt" saw the same definitive claim as a confirmed non-value-adding
  node. Replaced by `resolveVaBadgeState()`, returning `'va' | 'nva' | 'unknown'`; `vaClass` still
  decides when set (mirrors the engine's own `isVaNode` priority for `'va'`/the two NVA variants), but
  `'unknown'` is now its own bucket, same reading `deriveVaClassSelection` (vsm-field-status.ts,
  Review-Fix 2, PR #337) already gives the 4-way selector for an absent `vaClass`. **P8.2a-Update
  (Baustein 1 "isVaNode-unknown-Alignment"):** the residual gap declared here — `lib/vsm-engine`'s own
  internal `isVaNode`/timeline classification folding `'unknown'` into its NVA bucket for the DLZ/BZ
  sums and segment coloring — is closed: `timeline-ladder.ts`/`timeline-sequence.ts` now carry a
  tri-state `classifyVaState`, a dedicated `unknownTimeSec`/`'unknown-process'` posten (additive —
  DLZ/BZ totals numerically unchanged, only the bucket LABEL moved from a silent NVA attribution to an
  honest own line), rendered neutrally (`--status-grey`, not the NVA red) in the canvas timeline, the
  Timeline-Leiter summary bar, and the PDF export; `management-analysis.ts`'s lead-time confidence check
  was aligned to the new bucket too (see CHANGELOG.md). Unflagged (the checkbox/line it replaces was already unconditional) —
  a compact legend lives in the Sidebar-Palette, worded "Wertschöpfend (VA)"/"Nicht wertschöpfend
  (NVA)" (not the bare badge words, which would collide with the existing isValueAdded checkbox's own
  "Wertschöpfend" label in any exact-text lookup). **P8.2a-Review Fix-Runde (K1) — correction:** the
  "is closed" claim two sentences above was only HALF true — Baustein 1's `classifyVaState` only
  peeled off the EXPLICIT `vaClass === 'unknown'` literal; a node where NEITHER `vaClass` NOR
  `isValueAdded` was ever set (never classified at all — reachable via `lib/simvsm-import` for any
  SimVSM process class without an `isValueAdding` parameter) still fell into the NVA bucket, the exact
  badge/timeline divergence this whole addendum exists to close, just for a structurally equivalent
  input `classifyVaState` had not yet covered. Now genuinely closed: an entirely-unset node is
  `'unknown'` in the engine too, for both `timeline-ladder.ts` and `timeline-sequence.ts` (see their
  own `classifyVaState` doc comments). **Declared, NOT a further gap:** this still does not make the
  engine identical to every other VA/NVA consumer in this codebase — `computeVaClassBreakdown`
  (vsm-metrics.ts) deliberately answers a DIFFERENT, four-way question for its own report
  (`derivedVa`/`derivedNonVa`/explicit `nva`+`nnva`/`unknown` — distinguishing "derived from the legacy
  boolean" from "explicitly classified" even within the non-VA/VA sides), and keeps that distinction
  unchanged by this fix. `classifyVaState` (the timeline engine) and `resolveVaBadgeState` (the canvas
  badge) are now honestly THREE-WAY (va / nonVa / unknown) and mutually consistent with each other —
  they were never meant to, and still do not, reproduce `computeVaClassBreakdown`'s finer four-way
  split. A reader comparing the Timeline-Leiter/PDF "Ungeklärt" line against the Management-Analyse's
  own VA-breakdown should expect this narrower two-plus-unknown granularity there, not a fourth
  "derived" sub-line.
- **Baustein 4 — information-flow decluttering:** `computeInformationEdgeRoutes` (vsm-geometry.ts) —
  orthogonal "bus" routing for `kind: 'information'` connections ONLY (materialFlow keeps `buildPath`
  verbatim, untouched, per the brief's conservative scope). Each edge routes from its source's
  top-center up to a shared horizontal collection lane above the topmost node, across, then down into
  the target's top-center — replacing the free-crossing bezier curves Kais annotated as "viel zu
  unübersichtlich." Deterministic lane fan-out (sorted by source/target x-midpoint, round-robin offset
  around the base lane) bundles parallel edges from a hub (e.g. PPS) instead of letting them cross.
  The always-visible text label (some of the reference seed's information edges carry an 80+
  character combined label) is replaced by a small `VsmSymbolInfoElectronic` glyph on the edge, with
  the full text moved to a native `<title>` hover tooltip — Kais' own "Icons usw. würden hier helfen"
  suggestion, applied literally instead of just re-laying-out the same wall of text.
- **API:** `layout.timelineOffsetY` — `VsmLayoutSchema` (lib/api/schemas.ts), merged in
  `app/api/wertstrom/[id]/route.ts` PUT with a simpler contract than `shiftModel`'s three-way
  (absent/null/value): only absent-vs-present, no null-delete branch, since 0 is always a valid
  resting value for this field (no meaningful "explicit delete" distinct from "reset to 0").
- **Declared limitation:** two elements sharing an identical canvas x (a genuine parallel-branch
  topology — e.g. the reference seed's two Eingangssupermärkte feeding one downstream process) land
  in overlapping timeline-band positions; `computeTimelineBandSegmentPositions` nudges each
  additional one a few px sideways so both stay visible, but this is a cosmetic fan-out, not real
  collision-avoidance layout — same "documented, not solved" posture as `computeTimelineSequence`'s
  own pre-existing Gap G6 ("Parallelpfade werden nicht taktkorrekt abgebildet").
- **iOS scope:** web-only (the VSM editor itself is web-only); no separate iOS work.
- **Weitere Fixes (Referenzwertstrom-Fixrunde 2, KAR-878 — adversarielles Multi-Verifier-Review von
  PR #367, 21 CONFIRMED Findings auf 13 Kerne dedupliziert):** K1 (CRITICAL) informationEdgeRoutes'
  `<g>` bekommt jetzt `pointerEvents: 'auto'` — vorher NIE hoverbar (geerbtes `pointer-events:none`),
  die `<title>`-Tooltip war tot. K2 Band-Text (Segment-Dauer, §8-Summenbox, Nacharbeits-Hinweis)
  gegen-skaliert per `1/zoom` (Chrome-Element-Technik) + Basisgrößen auf 12-13px — vorher beim
  geseedeten Referenz-Zoom (0.32) praktisch unlesbar. K3 `layout.timelineOffsetY` nur noch bei
  `wertstromUxV2Enabled` im PUT-Body (Flag-AUS-Byte-Gleichheit wiederhergestellt). K4 oberer Clamp
  (`TIMELINE_BAND_OFFSET_MAX`, 2000px) + Server-Merge-Clamp + Zod-Sanity-Ceiling + "Zeitlinie
  zurücksetzen"-Control + `aria-valuemin`/`aria-valuemax`/`aria-valuetext` am Drag-Griff. K5
  Zeitlinien-Segment stoppt jetzt mousedown-Propagation (verhinderte Pan-Drag-Hijack + Selektions-
  Verlust). K6 PPS-Symbol ist jetzt ein echtes Fadenkreuz (statt Machine-ähnlichen Ticks) + ehrliches
  Heuristik-Label. K7 `VSM_SYMBOL_LABELS` + title/aria-label an allen Symbol-Render-Stellen;
  Vergleichs-Ansicht löst Symbole jetzt über `resolveNodeSymbolKey` auf (Supermarkt/FIFO-Treue). K8
  VA/NVA-Canvas-Badge dreiwertig (`resolveVaBadgeState`) — "Ungeklärt" kollabierte vorher fälschlich
  auf "NVA". K9 `computeInformationEdgeRoutes`' Sammel-Lane kollidiert nicht mehr mit dem obersten
  Hub-Node (PPS-Szenario). K10 Symbol-Inventar-Doku korrigiert (vier, nicht zwei, unverdrahtete
  Symbole — s. o.). K11 VA-Legende + Zeitlinien-Steuerleiste auf 12px + größere Klickflächen (nur
  diese zwei Flächen). K12 Zeitlinien-Drag-Griff respektiert jetzt einen aktiven Connect-Gesture. K13
  Ende-zu-Ende-Tests für beide Zeitlinien-Interaktionspfade (Drag + Stepper) ergänzt. Explizit KEIN
  Fix: Bausteine 1/3/4 bleiben unflagged (Parent-Entscheid nach Verifier-Dissens — die ersetzten
  Renderpfade waren bereits vor diesem PR unconditional, ein Flag-Gate würde doppelte Renderpfade für
  nie geflaggten Bestand schaffen).

### Export
- Full Excel report (.xlsx, 4 sheets)
- Raw CSV export (cycle measurements)

### Copilot-Exporte (Demo-067 C/E, KAR-982/KAR-984)
Feature-gated behind `copilotExports` (`config/profiles/`) — `true` in `bmw`, `false` in
`default`/`_template` (same introduction pattern `qafValueStream` used — see above). Generic
across every project (no project-specific logic) — the flag only controls whether the button
renders and whether the server action accepts the call.

Four exports (module scope: one Fabrikanalyse/QAF-comparison/Wertstrom, or the whole project)
meant for a consultant to load into an external LLM tool (e.g. Microsoft Copilot, ChatGPT,
Claude) and keep working with — each available as **DOCX or Markdown** (Demo-067 E, KAR-984:
Markdown reads better when the workflow is "paste the file into an AI chat window", no Office
viewer needed), plus an XLSX variant for the Gesamtprojekt-Export (DOCX/XLSX only, no Markdown —
the module-summary spreadsheet is a distinct shape, not a chat-paste narrative). Every export
opens with a shared AI-instruction block (what the document is, how an LLM should use it, a
glossary of LSC/QAF/VSM/FA) and, only when the source record's `is_demo` is true, a
fictional-data disclaimer. Content is German throughout, identical between the DOCX and Markdown
rendering of the same export (same input contract, same section order — only the markup format
differs: docx Paragraph/Table vs. Markdown headings/GFM pipe-tables).

- **Fabrikanalyse-Export** (`app/fabrikanalyse/[projectId]/[assessmentId]/`): one assessment —
  Steckbrief, overall score/progress, per-category summary table, and the full question/
  rating/comment detail (same grouping as the existing `exportForAISummary` Markdown export).
- **QAF-Vergleich-Export** (`app/qaf-differences/`): one `qaf_comparison` — renders the same
  `QafComparisonResult` the 8-sheet XLSX reference export is built from (management summary,
  structure changes, top cost drivers, the full field-diff table, plausibility findings). Only
  available for the standard/summary comparison mode, same restriction the existing XLSX export
  has (G60/Multi-QAF have their own dedicated exports).
- **Wertstrom-Export** (`app/wertstrom/`): one value-stream map — process-step and connection
  tables plus the same derived KPIs the editor shows (VA-Quote, Rüst-/Warte-/Transportzeit-Summen,
  cost/scrap rollup per currency, bottleneck). Wertstrom P7 (KAR-878/KAR-986, execution-prompt
  §19.3) additions: a "Copilot-Anweisung" section with the §19.3 instruction block verbatim
  (English original + German translation), a "Management-Analyse" section (Business-
  Interpretation, the §18 rule-based sentences — see the Wertstrom P7 section above), and an
  explicit "Provenance-Übersicht" (measured/planned/imported/calculated/assumed node counts).
  This DOCX/Markdown pair is also offered as the "Copilot-Report" choice inside the P7 Export-
  Familie's unified "Wertstrom exportieren" dialog (same server actions, same `copilotExports`
  gate — not duplicated).
- **Gesamtprojekt-Export** (`app/project/[id]/export/`, DOCX + Markdown + XLSX): project
  Stammdaten, Workshop/Agenda, Maßnahmen, and a short rollup per module (Fabrikanalyse/
  QAF-Vergleich/Wertstrom/LSC) — the module rollups are deliberately light (counts + a few label
  lines); the full data for one module is what that module's own Copilot-Export is for.

Delivery: "Muster B" (server action builds the file server-side, returns base64, client
downloads it) — same shape `exportQafComparisonXlsx` uses. Builders live in the pure
`lib/copilot-export/` module (no DB access); the server actions (two per module — DOCX/Markdown;
three for Gesamtprojekt) do the RLS-scoped reads. UI: one button per module surface offering a
small format choice (DOCX/Markdown) once `copilotExports` is on; a single-format button (the
Gesamtprojekt XLSX export) stays a plain button.

### Management Overview
- Cross-visit KPI aggregation
- Bottleneck bar chart (all visits)
- Project detail table

### Field Hints / Tooltips (admin-configurable)
- Input fields show an inline help icon (`FieldHint`) with a label, an explanation,
  and — for calculated fields — a formula plus a worked example.
- Content is stored in `lsc_tooltips` (bilingual de/en, `formula`/`example` columns),
  keyed by `<module>.<field>` (e.g. `workshop.observed_ct`). German is the source
  language; English is optional with a German fallback.
- Admins edit the texts in Stammdaten → "Feld-Hinweise" (`/repository/masterdata/feld-hinweise`);
  RLS allows all authenticated users to read, only admin/masteradmin to write.
- Rollout is incremental per module (pilot: Workshop-Erfassung). Missing keys render
  no tooltip (graceful).
- **iOS scope:** web-only for now. iOS can later read the same `lsc_tooltips` rows via
  PostgREST to mirror the hints — no separate data model needed.
