# qaf-value-stream

> Module created via `npm run new:module qaf-value-stream`.

## Owner

SupplierPulse / QAF (QVS-Programm)

## Purpose

QVS-P1 (KAR-970): maps parsed QAF manufacturing rows into VSM (Wertstrom)
process nodes with full source lineage — pure functions only, no DB access
and no UI wiring yet.

QVS-P2 (KAR-971): wires those pure functions to real `qaf_file` /
`qaf_manufacturing_step` rows and adds the transactional persistence path
(preview → duplicate hint → creation) into the new `value_stream_imports`
audit table.

QVS-P3 (KAR-972): the UI — entry points on `app/qaf-differences/[id]` and
`app/project/[id]/qaf`, a shared preview dialog
(`components/wertstrom/qvs-import-preview-dialog.tsx`), and editor
extensions in `components/wertstrom/vsm-editor.tsx` (provenance/`fieldStatus`
display, "Quelle anzeigen", the `vaClass` 4-way selector, the QAF-vs-measured
cycle-time comparison). No new DB objects and no changes to the P2 services'
behaviour — two small additive surface changes only:
`QvsImportPreview.sourceFileName` (preview.ts) and the exported
`severityForQvsWarningCode` (creation.ts), both display-only. The flag stayed
off in all 3 profiles through P3, pending corpus validation (P6) and the
flag-on PR (P7) — see Purpose above for the current P7 status (Review-Fix
PR #341).

QVS-P4 (KAR-973): Multi-QAF variant handling in the preview (`multi-qaf-
context.ts` — surfaces the ONE variant a `multi_qaf_variant_vs_standard`
comparison was actually run against (`engine_version.selectedVariantId`) plus
any OTHER active variant provably bound to the SAME manufacturing profile
(`variantProfileBindings`) — never "all container variants" unconditionally,
see Review-Fix 1/gap-analysis G5: a container's variants may legitimately run
on different manufacturing profiles/plants, and the comparison itself never
checks manufacturing compatibility); "geteilter Fluss" tags one Wertstrom's
nodes with that evidence-scoped variant-key set, "je Variante ein Wertstrom"
creates N Wertströme sequentially via `createValueStreamsForVariants` (each
with its own `· <Variantenlabel>` title suffix, Review-Fix 3, so the N
resulting Wertströme stay distinguishable), never fabricating per-variant
step data — Multi-QAF has no step-level granularity, see gap-analysis G5), additive
`vsm-metrics.ts` Kennzahlen (`components/wertstrom/vsm-metrics.ts` —
vaClass-aware VA-Quote, Setup-/Warte-/Transport-Summen, Kosten-/Scrap-Rollup
per Währung, capacity/lot-size context; existing `computeTimeline`/
`findBottleneckId` untouched), and 2 new file-level parser fields
(`sum_planned_capacity`/`sum_lot_size`, `lib/qaf-differences`'s
`summary-parser.ts`, threaded through `qaf-source.ts`'s
`QvsFileLevelContext`). Still flag-gated off in all 3 profiles through P4 (see current P7 status
under Purpose above, Review-Fix PR #341).

See `reports/qaf-value-stream-architecture.md` (Leitentscheidungen E1–E7) and
`reports/qaf-value-stream-field-mapping.md` + `mappings/qaf-to-value-stream-mapping.yaml`
(the versioned field contract, `MAPPING_VERSION = 'qvs-1'`) for the full
rationale. Feature-gated behind `qafValueStream` (`config/profiles/`) — **`true` in `default` and
`bmw` since P7 (PR #341, 2026-07-18), after P6 corpus-validation evidence**; `false` only in
`_template` (copy template, never a real deployment — no third production profile exists). Rollback:
`docs/runbooks/qaf-value-stream-rollout.md` (flag-only revert, no schema touch).

## Public API

The public surface is the named exports of `index.ts`. Anything in
`internal/` is private and may change without notice.

### P1 — pure functions (no DB, no I/O)

- **`mapQafRowsToVsmNodes(rows, opts?)`** — the mapper. `QAFRow[]` →
  `{ nodes, connections, warnings, excludedRows, missingFieldReasons }`. Pure,
  synchronous, deterministic (injectable `idFactory`). Never writes a cost
  field into a time field, never fabricates a missing value, never auto-sets
  `vaClass: 'va'`.
- **`MAPPING_VERSION`** — `'qvs-1'`, must equal `version:` in
  `mappings/qaf-to-value-stream-mapping.yaml` (guarded by a drift test).
- **`assessManufacturingCapability(rows, parseMeta?)`** — capability-based
  (not template-based) eligibility + per-field coverage over already-parsed
  rows.
- **`conservativeVaClass(processName)`** — pattern-based `va`/`nnva`/`nva`/`unknown`
  classification. Never returns `'va'` automatically.
- **`buildDefaultTitle({ supplier?, project?, variant? })`** — `"[Lieferant] ·
  [Projekt] · [Variante] · QAF-Wertstrom"`, only the parts that exist. Source
  file name is never part of the title.
- **Types** — `MapperOptions`, `MapperResult`, `ExcludedRow`,
  `MissingFieldReason(Code)`, `QvsWarning(Code)`, `CapabilityAssessment`, `IdFactory`.

### P2 — DB-backed services (RLS-scoped Supabase client only, never `lib/supabase/admin.ts`)

- **`loadQafSourceRows(supabase, qafFileId)`** — fetches `qaf_file` +
  `qaf_manufacturing_step` (source-ordered), RLS-scoped. Returns `null` for
  "not found or not visible to caller" — never a distinguishable "forbidden"
  (no existence leak, architecture.md §9).
- **`computePreviewToken(input)`** — deterministic fingerprint of a source
  snapshot (file id/hash/version + included row set). Used both as a
  staleness check (has the source changed since preview?) and as the
  idempotency key `create_value_stream_from_qaf` enforces via a partial
  unique index.
- **`buildQafValueStreamPreview(supabase, qafFileId, opts?)`** →
  `QvsImportPreview` — capability + mapped nodes/connections + duplicate
  hints + `previewToken` + suggested title. Read-only.
- **`findExistingImports(supabase, { projectId, fileHash })`** → `FindExistingImportsResult`
  (`{ items, degraded, degradedReason? }`) — prior imports of the same source
  file into the same project ("Öffnen/Neu" hint, never a block). `degraded:
  true` means the query itself failed (e.g. the table is missing) — callers
  MUST surface "Duplikat-Check nicht verfügbar" in that case, never read the
  empty `items` as a genuine zero-duplicates result (Review-Fix 3).
- **`createValueStreamFromQaf(supabase, confirmation, variantTagging?)`** —
  transactional creation (`value_stream_maps` + `value_stream_imports` in one
  Postgres transaction via the `create_value_stream_from_qaf` RPC). Re-derives
  nodes/connections from the DB rather than trusting a client-supplied node
  array; idempotent on `previewToken` re-submit; enforces E4 (`project_id`
  always derived from the QAF source). `variantTagging` (QVS-P4, optional —
  omitted is byte-identical to P2/P3) tags every node with the given variant
  key(s) and persists `variant_selector`.

### P4 — Multi-QAF variants + additive metrics (KAR-973)

- **`loadMultiQafVariantContext(supabase, qafFileId)`** →
  `QvsVariantContext | null` — `null` when this file is not the standard side
  of a `multi_qaf_variant_vs_standard` comparison (the common case, not an
  error); `{variants, degraded, sharedProfileConfirmed}` otherwise. `degraded:
  true` when a qualifying comparison/container exists but its data (incl. the
  comparison's own `selectedVariantId`, or that id's resolution to a real
  active variant) couldn't be read — RLS-scoped only (`qaf_comparison` +
  `qaf_file.g60_meta`), no admin client, no workbook re-parse. `variants`
  (Review-Fix 1) is evidence-scoped: always the compared variant itself, plus
  any OTHER active variant `variantProfileBindings` provably ties to the SAME
  profile as the compared variant — `sharedProfileConfirmed` is `true` only
  when that binding evidence existed at all (`false` means `variants`
  conservatively contains just the compared variant, binding unknown — never
  a guess about the rest of the container).
- **`createValueStreamsForVariants(supabase, confirmation, variants)`** →
  `QvsPerVariantOutcome[]` — "je Variante ein Wertstrom": N sequential
  `createValueStreamFromQaf` calls (NOT one transaction across all N — each
  variant's own create is transactional, a mid-sequence failure is reported
  per variant, never hidden), each producing IDENTICAL nodes tagged with
  exactly one variant key and (Review-Fix 3) its own
  `${confirmation.title} · ${variantLabel}` title, so the N resulting
  `value_stream_maps` rows stay distinguishable in the `/wertstrom` list.
  Every call gets a distinct persisted `engine_context.previewToken` suffix
  so the RPC's idempotency guard never collapses two different variants'
  creates into one.
- **`components/wertstrom/vsm-metrics.ts`** (additive, existing
  `computeTimeline`/`findBottleneckId` untouched — same known linear-sum/no-
  graph-traversal limitation, Gap G6): `computeVaClassBreakdown` (va/nnva/
  nva/unknown separately, legacy binary-only nodes marked `derivedVa`/
  `derivedNonVa`, provably degenerates to `computeTimeline`'s own split when
  no node has `vaClass`), `computeSetupTimeSum`/`computeWaitTimeSum`/
  `computeTransportTimeSum` (`totalSec: null` — never a 0-lie — when no node
  carries the field), `computeCostPerUnitRollup`/`computeScrapCostRollup`
  (per-currency buckets, never summed across a currency mix — KAR-938;
  scrap is bucketed by the captured Angebotswährung, not `node.currency`),
  `computeCapacityContext` (exact `lotsPerYear` from the 2 new QVS-P4 fields;
  deliberately NO derived customer-Takt — no exact formula exists without an
  unparsed working-hours-per-year constant). Every metric returns a
  `MetricExplain` (formula/data basis/exclusions).

## Dependencies

- `@/lib/qaf-parser` — `QAFRow`/`QAFRowValues`/`QAFFieldKey`/`QAFParseMeta`/`TEXT_FIELDS` (read-only; this module never re-parses).
- `@/lib/vsm-types` — `VsmNode`/`VsmConnection`/`VsmNodeQafSource*` (QVS-P1 additive fields).
- `@/components/wertstrom/vsm-geometry` — `NODE_W` (pure geometry constant, no React/DOM) for linear node layout.
- `@/lib/qaf-differences` (QVS-P4, public barrel only — no deep `internal/` import): `deserializeMultiQafContainer`/`MultiQafContainer`/`VariantDefinition` for `multi-qaf-context.ts`'s variant-summary derivation.
- `@supabase/supabase-js` — `SupabaseClient` type only, for the P2/P4 services (`qaf-source.ts`/`preview.ts`/`duplicates.ts`/`creation.ts`/`multi-qaf-context.ts`). The concrete client is always injected by the caller (`app/wertstrom/qaf-actions.ts`, via `@/lib/supabase/server`'s RLS-scoped `createClient()`) — this module never imports `@/lib/supabase/admin.ts`.

## Tests

Co-located under `internal/__tests__/`. Run via `npx vitest run lib/qaf-value-stream`.

`internal/__tests__/mapper.real-files.test.ts` and
`internal/__tests__/preview.real-files.test.ts` are env-gated
(`describe.skipIf(!existsSync('/home/aria/work/qaf-corpus'))`) — real-corpus
regression, only runs where the confidential corpus is present. Same
confidentiality discipline as KAR-943: no corpus filename/cell value/price is
ever a committed literal; files are discovered dynamically at run time and
every assertion is structural.

`internal/__tests__/persistence-drift.test.ts` guards the `p_payload` shape
`creation.ts` actually sends against `schemas/qaf-value-stream-import.schema.json`
(analogous to `mapping-drift.test.ts` for the P1 mapper vs. the mapping YAML) —
catches a `warnings`/`missing_field_reasons` persistence-shape regression the
same way that drift test catches a mapper/YAML one.

QVS-P4 additions: `internal/__tests__/multi-qaf-context.test.ts` (variant
context derivation, `degraded` vs. `null` distinction), the variant-tagging/
`createValueStreamsForVariants` cases in `creation.test.ts`, and
`components/wertstrom/__tests__/vsm-metrics.test.ts` (new metrics + a
regression lock proving `computeVaClassBreakdown` degenerates to
`computeTimeline`'s own split when no node has `vaClass`). The 2 new parser
fields' real-corpus coverage is `lib/qaf-differences/internal/__tests__/
summary-parser-qvs-file-level-fields.real-files.test.ts` (same env-gating as
the two real-files tests above) — see that file's header for the measured
coverage and why it differs from the P1 corpus-evidence scan's figures.

RLS coverage for `value_stream_imports` lives in
`__tests__/security/rls-value-stream-imports-isolation.test.ts`; the
`create_value_stream_from_qaf` RPC itself (manual authorization check,
idempotency, `p_import_id` collision, project-deletion survival) is
regression-tested in
`__tests__/security/rpc-create-value-stream-from-qaf.test.ts`. Both are
gated on `RLS_TEST_DATABASE_URL`, run via `scripts/rls-test/run.sh`, not here.

## Conventions (ADR 019)

- No deep imports into other modules' `internal/` directories.
- This module never imports from `lib/customers/**` (ADR 010 / ADR 015).
- Customer-specific behaviour goes via the active composition profile (ADR 013).

### P5 — Reimport & Synchronisation (KAR-974)

- **`internal/sync.ts`** (pure, no DB) — `computeSyncDelta({sourceNodes, snapshotNodes, liveNodes})` →
  `QvsSyncDelta`: matches every `qafSource`-carrying step across the 3 inputs and classifies
  it AND each of its `VsmNodeEditableFieldKey` fields into one of Spec 21's 6 statuses
  (`UNCHANGED|SOURCE_CHANGED|LOCAL_CHANGED|BOTH_CHANGED|NEW_IN_SOURCE|REMOVED_FROM_SOURCE`).
  `snapshotNodes`↔`liveNodes` matching is exact (stable `id`, unchanged by the editor).
  `snapshotNodes`↔`sourceNodes` matching is name-based (`rowIndex` is not comparable across
  two different `qaf_file` uploads — see module header) — a name with an equal, >1 count on
  both sides pairs by preserved row order; a name whose count DIFFERS between the two sides
  is never force-matched, it becomes `unmatched` (own bucket, excluded from the 6 statuses
  and from every adoption plan, per the task's "ehrlich 'unmatched' statt raten" instruction).
  `applySyncAdoption(delta, liveNodes, connections, plan, importId)` applies a
  `QvsSyncAdoptionPlan` (`mode: 'all'|'selected'|'none'` for SOURCE_CHANGED fields/NEW_IN_SOURCE
  steps, ALWAYS-consulted `conflictResolutions`/`removedStepDecisions` for BOTH_CHANGED/
  REMOVED_FROM_SOURCE regardless of mode) — a LOCAL_CHANGED field is NEVER touched by any
  plan, a BOTH_CHANGED field only by an explicit per-field `conflictResolutions` entry.
  Adopting a field also refreshes that field's own `qafSource.fields[qafKey]` provenance
  (never the node-level `qafSource.importId`, which stays the original creation's).
- **`internal/reimport.ts`** (DB-wired) — `loadValueStreamSyncDelta`/
  `applyValueStreamSyncSelection`/`listReimportSourceCandidates`/
  `createComparisonValueStreamFromSync`. `sourceQafFileId` defaults to the value stream's own
  recorded `qaf_file_id` (a legitimate no-op "confirm nothing changed" check — `qaf_file` rows
  never mutate in place, every upload including a byte-identical re-upload gets a fresh row)
  or can be any other `qaf_file` in the same project (`listReimportSourceCandidates`).
  `applyValueStreamSyncSelection` staleness-checks a `deltaToken` (sha256 over value-stream
  id/import id/source id/source previewToken/`value_stream_maps.updated_at`) before writing —
  the same "reload rather than confirm a stale decision" discipline `previewToken` gives
  `createValueStreamFromQafAction`. **No new migration**: persistence is 2 sequential
  PostgREST requests (`value_stream_maps.nodes/connections` UPDATE, then the EXISTING
  `value_stream_imports` row's UPDATE — never a new row per reimport, unlike the "N imports
  over time" pattern ADR-024 accepts as a general schema consequence; a reimport instead
  appends to `engine_context.reimportHistory`, capped at 20 entries) — `value_stream_maps` is
  written FIRST so a 2nd-write failure never leaves the snapshot claiming a sync that never
  actually reached the live document (see that file's header for the full ordering
  rationale). `createComparisonValueStreamFromSync` ("separater Vergleichs-Wertstrom", Spec 21)
  is a thin wrapper reusing `buildQafValueStreamPreview`/`createValueStreamFromQaf` wholesale —
  an entirely independent Wertstrom titled `<original> · Reimport <Datum>`
  (`naming.ts` `buildReimportComparisonTitle`, injectable `Date` for deterministic tests),
  the original Wertstrom/import untouched.
- **UI** (`components/wertstrom/qvs-reimport-logic.ts` + `qvs-reimport-dialog.tsx`) — same
  "pure logic extracted for testability" convention as `qvs-preview-logic.ts`. Entry point:
  a "Synchronisieren" toolbar button in `vsm-editor.tsx`, shown only when the Wertstrom has
  ≥1 `qafSource`-carrying node (old/manual/Excel/LSC Wertströme never see it). Status
  colors/labels, per-field/-step checkboxes, "X übernehmen, Y behalten, Z Konflikte" summary
  (Spec 21's own wording), and the separate-comparison-copy action all live in the dialog;
  see `UI_FLOWS.md` §10f for the full flow.
- No new DB objects, no schema/RLS change (`schemas/qaf-value-stream-import.schema.json`
  documents the additive `engine_context.reimportHistory` shape only — `engine_context` has
  no `additionalProperties: false`, so this is documentation, not a migration). Flag stayed off in
  all 3 profiles through P5 (see current P7 status under Purpose above, Review-Fix PR #341).

## Scope (P2/P3/P4 done)

P2: migration `value_stream_imports` (+ rollback, RLS `_own`/`_admin`) +
preview/duplicates/creation services + server actions
(`app/wertstrom/qaf-actions.ts`). The migration is delivered as a file only —
not applied by this repo's code (operator-applied via Supabase SQL Editor);
still not applied as of P4.

P3 (KAR-972): the UI entry points, shared preview dialog, and editor
extensions described above — see `PRODUCT_SPEC.md` ("QAF → Wertstrom-
Übernahme") and `UI_FLOWS.md` §10e for the full user-facing flow.

P4 (KAR-973): Multi-QAF variant handling (Varianten-Sektion in the same
preview dialog, "geteilter Fluss"/"je Variante" strategy — see
`UI_FLOWS.md` §10e addendum and `PRODUCT_SPEC.md`), additive `vsm-metrics.ts`
Kennzahlen, and the 2 new file-level parser fields. No new migration, no
existing behavior changed.

P5 (KAR-974): Reimport & Synchronisation (Spec 21) — see the "P5" bullets
above, `PRODUCT_SPEC.md`, and `UI_FLOWS.md` §10f. No new migration; the P2
migration (`value_stream_imports`) **was applied to production on
2026-07-18** (operator-applied ~10:31 CEST; same-day verification against
the live project: table exists, RLS enabled, 0 rows — canonical record in
`MIGRATIONS.md` §7o). P5's code assumes nothing beyond that empty-table
baseline, and its own automated tests (RLS/DB-path) still run exclusively
against the local PG17 test harness (`scripts/rls-test/run.sh`), never
against production, and skip locally when that harness isn't available. See
`reports/qaf-value-stream-architecture.md` §7 for the full phase plan. **P6 (corpus validation) and
P7 (flag-on) are both done (Review-Fix PR #341):** `qafValueStream` is `true` in `default`+`bmw`
since 2026-07-18 (PR #341) — see Purpose above and `docs/runbooks/qaf-value-stream-rollout.md` for
rollback.
