# Changelog — @onlineapps/service-wrapper

All notable changes to this package. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format and semantic versioning within the internal-monorepo convention (all biz-service consumers ship matching changes in the same session).

## [Unreleased]

## [13.0.2] — 2026-10-15

pin: `@onlineapps/conn-infra-mq` 7.0.0 → 7.0.1
pin: `@onlineapps/conn-orch-orchestrator` 8.0.0 → 8.0.1
pin: `@onlineapps/conn-orch-registry` 9.0.0 → 9.0.1
pin: `@onlineapps/infrastructure-tools` 5.0.0 → 5.0.1

- Pin-only vydání: kód balíčku se od 13.0.1 nezměnil. Čtyři přepinované knihovny vydávají pin-only patch, jehož
  jediná změna kódu je pin `@onlineapps/mq-client-core` 6.0.0 → 6.0.1 (jejich tarbally 7.0.0 / 8.0.0 / 9.0.0 / 5.0.0
  pinovaly 6.0.0). Vlastní pin wrapperu na `@onlineapps/mq-client-core` je 6.0.1 už od 13.0.1; strom služby
  s 13.0.1 ale nesl pod těmito čtyřmi vnořenou 6.0.0, takže oprava conf `mq-publish-buffer` 003 (cesta (b) po
  fatálu odmítá: `MQ_CONNECTION_FATAL`, `buffered: false`) do MQ transportu služby nedošla — MQ konektor
  wrapper bere z `@onlineapps/conn-infra-mq` (`src/ServiceWrapper.js:16`). d.1498.

## [13.0.1] — 2026-10-15

### Changed — deklaraci `sensitive_input_fields` při bootu posuzuje `@onlineapps/logger-contract` `isSensitiveFieldsDeclaration` (nález BIZ-DOCS (4) k vlně 5, d.1474)

- `assertDeclaredSensitiveInputFields` (FÁZE 0.1b) měl vlastní pravidlo tvaru (`isWellFormedRule` +
  místní test unikátnosti) vedle jediného pravidla platformy, `isSensitiveFieldsDeclaration`
  z `@onlineapps/logger-contract` 2.1.0 (pin beze změny), které stejným exportem uplatňuje i
  orchestrátor. Změřeno čtením obou: dnes jde o týž predikát (pole, každá položka neprázdný řetězec,
  bez duplicit), takže žádný vstup je nerozliší — druhá kolej by se rozešla až změnou kontraktu.
  Teď rozhoduje kontrakt; wrapper jen jmenuje operaci a ponechává svou hlášku
  (`[ServiceWrapper] Invalid sensitive_input_fields declaration - … Fix: …`, znění beze změny).
- `isWellFormedRule` zůstává pro pravidlo na drátě (`resolveSensitiveRule`) — mimo rozsah tohoto nálezu.
- Testy: `tests/unit/d1474-sensitiveFieldsOneRail.test.js` (wrapper se ptá kontraktu s deklarovanou
  hodnotou a řídí se jeho odpovědí oběma směry; kontrolní případy se skutečným kontraktem).

### Fixed — `operationMutatesLookup` předává `mutates` beze změny; chybějící `REDIS_URL` ve FÁZI 0.4 je trvalá chyba (nálezy BIZ-DOCS (1) a (3) k vlně 5, d.1474)

- **`mutates` bez tichého převodu.** Hook vracel `mutates: !!(spec && spec.mutates === true)`, takže
  přítomná neboolovská deklarace (`"yes"`, `1`, `null`) došla orchestrátoru jako `false` (čte) a
  odmítnutí `@onlineapps/conn-orch-orchestrator` 8.0.0 (`WorkflowOrchestrator._stepMutatesFlag`, d.1309:
  `undefined` = čte, jiný ne-boolean → `[WorkflowOrchestrator] Invalid operation spec - "mutates" of
  operation '<op>' must be a boolean; got … Fix: …`) bylo na cestě přes wrapper nedosažitelné. Teď
  `mutates: spec ? spec.mutates : undefined` — stejně jako `resource_type`, `sensitive_input_fields`,
  `retry` a `timeout_ms`; pravidlo má jednoho vlastníka, orchestrátor. Operace bez deklarace (nebo
  neznámá) dává `undefined` místo vymyšleného `false`; orchestrátor ji čte jako dosud jako čtení.
- **FÁZE 0.4: chybějící `REDIS_URL` = konfigurace, ne výpadek.** `_waitForInfrastructureGate` čte
  `REDIS_URL` přes `requirePlatformEnv` (obal z d.1292 povýšený na funkci modulu, sdílený s
  `_initializeSecrets`), který chybu `requireEnv` ze `@onlineapps/service-common` beze změny znění
  (`[ServiceConfig] Missing environment variable - REDIS_URL is required. … Fix: set REDIS_URL in
  config/env-active/shared.env.`) označí `bootDisposition: permanent`; FÁZE 0.4 dispozici čte z chyby
  (`error.bootDisposition !== BOOT_DISPOSITION.PERMANENT`) jako 0.7 / 0.16–0.18 místo konstanty `true`.
  Brána, která nedočká zdravé infrastruktury, zůstává přechodná.
- Testy: `tests/unit/d1474-bizDocsFindings.test.js` (hook proti skutečnému `WorkflowOrchestrator`
  z pinu, boot přes `bootstrap()`); upravené piny `requireEnvOwningFile` (přímé `requireEnv('REDIS_URL'`
  1, přes obal 2) a `errorHandlerLogger` (neznámá operace: `mutates` `undefined`); IT
  `operationSpecLookup` (skutečný orchestrátor odmítne `"yes"`) a `infrastructureGate` (chybějící
  `REDIS_URL` trvalé, nezdravá platforma bez razítka).

## [13.0.0] — 2026-10-12

### Changed — meze deadline kroku (STEP_TIMEOUT_MIN_MS/STEP_TIMEOUT_MAX_MS/DEFAULT_STEP_TIMEOUT_MS) čte stepDeadline.js z @onlineapps/handler-contract 1.1.0 místo vlastních literálů; MIN_/MAX_STEP_TIMEOUT_MS zůstávají jako aliasy; load-guard při načtení; pin handler-contract 1.0.0 → 1.2.0 (STEP 1 ze SSOT v běhu R4) (d.1382, conf handler-contract 002)

### Changed — věta FÁZE 0.10 jmenuje vlastníka jména health fronty: topologii jádra, ne service-common (d.1365, dialog 10 Q3 B)

- Hláška `[ServiceWrapper] Health heartbeat publisher did not start - … Expected: …` posílala čtenáře
  pro jméno fronty do `@onlineapps/service-common src/defaults.js` (`infrastructureHealthQueueName`).
  Service-common 5.0.0 jméno odebral (d.1334) a jediným vlastníkem je topologie
  `@onlineapps/mq-client-core` (`topology.infrastructureHealthChecksQueue`), kterou jako výchozí
  bere `@onlineapps/infrastructure-tools` `createHealthPublisher` (d.1360). Věta to teď říká.
- Kód wrapperu se nemění: FÁZE 0.10 předává `createBaseClientAdapter(…, { ...wrapper.heartbeat }, …)`
  a jméno sám neskládá — `wrapper.heartbeat.queueName` je jen volitelný override, výchozí config
  (`config/runtime-defaults.json`) ho nenese, takže jméno určí topologie.
- Testy: `tests/unit/ServiceWrapper.heartbeatFamily.test.js` (Expected věty hodnotou + věta nejmenuje
  `infrastructureHealthQueueName`). S knihovnami ze zdroje (service-common 5.0.0, infrastructure-tools
  d.1360, mq-client-core) jsou `heartbeatFamily`, `HealthStatusPublisher` a IT `d729` zelené — vazba
  z d.1356 (`queue name is not a usable queue - got undefined`) je pryč.

### Changed — marker odmítnutí Redisu čte wrapper od vlastníka `@onlineapps/redis-contract` 0.2.0 (d.1356, přenos d.1235/d.1242/d.1244 nad wrapper 13)

- `REDIS_CONNECTION_REFUSED` (FÁZE 0.7 discovery i 0.16 cache — trvalá dispozice odmítnutí
  serverem, d.1242) se importuje z `@onlineapps/redis-contract` (kontrakt toho, co odmítnutí je
  a jaký kód nese, d.1333), ne ze `@onlineapps/service-common` — jednoho z jeho vrhačů.
  Conn-base-cache s ním vrhá od d.1333, service-common exportuje tutéž hodnotu. Nová závislost
  `"@onlineapps/redis-contract": "0.2.0"` (exaktní pin = SSOT po d.1333; lock dorovná publish/
  kaskáda — týž postup jako d.1333 u conn-base-cache). Dvojníci service-common ve dvou unit
  sadách (`cachePhase`, `registryBootRefusal`) nesli marker jen kvůli 4.1.0 bez exportu — mrtví,
  odebráni.
- Přenos větve `wip/d1235-discovery-auth` nad wrapper 13: část d.1244 o `wrapper.timeout`/
  `defaultTimeout` se nepřenáší (wrapper 13 klíč odebral, d.1264b); README tabulka fází nese
  řádky 0.7/0.16 větve a 0.17–0.19 wrapperu 13.
- Sonda d.1329 (`orphan-sweep` — žádná sada neskládá broker jméno z `process.pid`/`Date.now()`)
  hlásila `cachePhase.integration.test.js` `REQUIREPASS_CONTAINER = …${process.pid}` z d.1244: je to
  jméno docker kontejneru helperu `requirepassRedis`, ne broker jméno — přejmenováno na výjimku,
  kterou sonda zná (`CONTAINER_NAME`, jako `redisPassword`); věta sondy jmenuje obě sady.
- Testy: `tests/unit/d1356-refusalMarkerOneRail.test.js` (import od vlastníka, žádný import ze
  service-common ani literál, pin = SSOT, kontrola: hodnota vlastníka = hodnota obou vrhačů).

### Fixed — FÁZE 0.7 ověří discovery spojení příkazem; chybějící i špatné heslo zastaví boot (d.1235; klíč stropu = slovo vlastníka)

- Klient `registry-discovery` se stavěl `createRedisClient(...)` + `await connect()` — socket, ne
  spojení (d.639: „`ready`, not `connect`"). Změřeno d.1224 (2026-10-04) proti `requirepass` Redisu
  platformního obrazu: (a) `REDIS_URL` bez hesla → 0.7 PASSED, první lookup kroku workflow dostal
  `NOAUTH` bez typu → `UNKNOWN` → DLQ bez retry, služba zaregistrovaná a „zdravá" (tvar incidentu
  B88); (b) špatné heslo → `connect()` se s výchozí reconnect-strategií node-redisu nikdy neusadí
  (`WRONGPASS` na kanálu `error`, ~2,4 pokusu/s), boot visí v 0.7 bez FAILED, bez exitu, restart
  policy nic nedostane.
- Teď `connectRedis({ purpose: 'registry-discovery', redisUrl, disableOfflineQueue: true, timeoutMs,
  logger })` z `@onlineapps/service-common`: `connect()` + PING pod stropem. Odmítnutí serverem →
  0.7 FAILED hned s verdiktem serveru a `Fix: correct the credential in REDIS_URL`; Redis, který
  neodpoví → 0.7 FAILED na stropu (přechodná chyba, restart policy). Vlastní spojení bez `keyPrefix`
  (d.1091) i `disableOfflineQueue: true` (d.1093) zůstávají — `connectRedis` volbu předává dál.
- Strop = `REGISTRY_DISCOVERY_TIMEOUT_MS` (`this.discoveryTimeoutMs`, čte konstruktor, d.1130):
  jedno „jak dlouho čekáme na projekci registru" — na odpověď při připojení i na jeden lookup.
  Doporučení leada; klíč potvrzuje vlastník (alternativa: vlastní platformní klíč jen pro
  připojení; `config/shared-env.json` zatím popisuje jen lookup — srovná se s rozhodnutím).
- Dispozice: odmítnutí serverem je trvalé, ale catch 0.7 čte dál jen `MQ_CONNECTION_FATAL`.
  `connectRedis` (nainstalovaná 4.1.0 i devel d.1227) na vyhozené chybě žádný marker nenese a
  wrapper nečte text hlášky ani vlastní seznam prefixů odpovědí (`change-discipline.md` § One rail
  per concern; týž postoj jako u brokeru, kde marker vyslovuje `@onlineapps/mq-client-core` a
  wrapper ho jen čte). Marker patří vlastníkovi otázky, service-common — rozhodnutí vlastníka. Do
  té doby je odmítnutí ohlášeno jako přechodné: restart smyčka s verdiktem v logu, ne visící boot
  a ne PASSED nad nepoužitelnou discovery.
- **(d.1242) Dispozice odmítnutí = trvalá, čtená z markeru** (rozhodnutí leada: varianta A
  d.1235 §5). `@onlineapps/service-common` d.1242 vyslovuje odmítnutí serverem v `connectRedis`
  jako `code: 'REDIS_CONNECTION_REFUSED'` / `reason: 'server-refused'` — táž hodnota, kterou
  `@onlineapps/conn-base-cache` od d.1237 nese na odmítnutém `connect()`. Catch FÁZE 0.7 i FÁZE
  0.16 teď navíc k dosavadnímu čtení (`MQ_CONNECTION_FATAL`, `bootDisposition`) označí chybu
  `markBootDisposition(error, PERMANENT)`, nese-li tento kód; konstanta se importuje JEDNOU
  z `@onlineapps/service-common` (wrapper má service-common i conn-base-cache jako závislosti,
  jeden kód pro oba Redis klienty), nikdy není vypsána ručně, a text hlášky se nečte. Srovnání je
  hlídané `typeof error.code === 'string'`: proti nainstalované 4.1.0 je import `undefined` a
  chyba bez kódu by se jinak shodovala (`undefined === undefined`) — žádný fallback, žádná druhá
  kolej, jen bezpečné „nic". Chyba bez markeru (transport, strop, hláška bez kódu) zůstává
  přechodná (kontroly).
  Testy: unit `ServiceWrapper.registryBootRefusal` +3 (0.7: chyba s kódem → `Permanent:`, exit 1,
  boot stojí, `bootFailure`; kontroly: táž hláška bez kódu → `Transient:`, chyba bez `code` →
  `Transient:`), `ServiceWrapper.cachePhase` +3 (0.16 totéž; dosavadní kontrola ECONNREFUSED →
  `Transient:` beze změny) — dvojník service-common (`...actual` + konstanta) jako v ostatních
  unit sadách wrapperu; RED proti zdroji před změnou: 2 failed, 18 passed; GREEN 20/20.
  IT `discoveryAuth`: tři pojmenované limity přejmenovány na „red until released and pinned"
  (limit WRONGPASS ve FAILED řádku → d.1227; oba limity PERMANENT → d.1242) — s nainstalovanou
  4.1.0 dál červené, podle očekávání.
- **(d.1244) Zbytky d.1242 — dvě nepravdivé věty a chybějící IT odmítnutí cache serverem.**
  Komentář konstruktoru (`discoveryTimeoutMs`) a README § `wrapper.registry` tvrdily, že catch 0.7
  „klasifikuje každou chybu kromě `MQ_CONNECTION_FATAL` jako přechodnou" — po d.1242 nepravda
  (odmítnutí discovery Redisu s `REDIS_CONNECTION_REFUSED` je trvalé). Obě místa teď říkají
  „chyba bez dispozičního markeru je přechodná" a za seznam markerů odkazují na jedno místo
  (catch 0.7, resp. řádek 0.7 tabulky fází) — žádný druhý opis (`doc-code-binding.md`).
  Komentář u `OrchestratorConnector.create` a README § `wrapper.timeout` tvrdily, že
  `defaultTimeout` je pro `@onlineapps/conn-orch-orchestrator` povinný — nepravda (pinutá 7.0.0
  ho nikde nečte, změřeno d.1242 i d.1244). Rozhodnutí o deadlinu kroku mezitím padlo (conf
  `step-error-classification` 005): wrapper 13 `wrapper.timeout` i předání `defaultTimeout`
  odebral (d.1264b, § „Removed — BREAKING — `wrapper.timeout`…“), takže tato část d.1244 se
  při přenosu nad wrapper 13 nepřenáší (d.1356).
  IT `cachePhase` +2 bloky proti živému `requirepass` Redisu platformního obrazu (helper
  `tests/helpers/requirepassRedis.js`, ne kopie): URL bez hesla → 0.16 FAILED s `NOAUTH
  Authentication required.` a `Fix: correct the credential in REDIS_URL` ve verdiktu, exit 1,
  INIT nikdy (zelené už s nainstalovanou `conn-base-cache` 2.1.0 — ta verdikt serveru do
  hlášky píše od d.566, jen bez kódu); pojmenovaný limit `Permanent:` ve verdiktu — s 2.1.0
  červený (hláška končí `Transient:`), protože marker `code: 'REDIS_CONNECTION_REFUSED'` nese
  až d.1237 (`78810193`, nevydáno, nepinuto): red until released and pinned; kontrola správné
  heslo → 0.16 PASSED, INIT PASSED, `_probeCacheHealth()` healthy. Změřeno: 10/11, 1 limit.
- Testy: unit `tests/unit/d1091-platformRedisOneRail.test.js` +4 (továrna je `connectRedis`
  s `disableOfflineQueue: true` a `timeoutMs` z klíče; vrácený klient jde registru; odmítnutí
  registr nestaví; kontrola: strop i deadline lookupu jsou jedna hodnota konstruktoru).
  IT `tests/integration/discoveryAuth.integration.test.js` (nový, živý `requirepass` Redis
  platformního obrazu přes helper `tests/helpers/requirepassRedis.js`, který od teď používá
  i `redisPassword` místo vlastní kopie startu kontejneru): bez hesla → 0.7 FAILED s `NOAUTH`
  a Fix na heslo, verdikt pod stropem, exit 1, registr nepostaven (RED před opravou: 0.7 PASSED);
  špatné heslo → 0.7 FAILED do stropu + rezervy teardownu (RED před opravou: jest timeout, boot
  visel); kontrola správné heslo → 0.7 PASSED, `discoveryRedis.isReady === true`, INIT PASSED.
  Pojmenované limity, každý vlastní případ, dnes červené: verdikt `WRONGPASS` ve FAILED řádku
  (nainstalované service-common 4.1.0 dá na stropu větu lhůty; zelené od vydání a pinu d.1227)
  a dispozice PERMANENT u odmítnutí (marker neexistuje, viz výše). Dvojníci service-common ve
  čtyřech unit sadách (`discoveryDeadline`, `consoleChannel`, `noRegistryUrl`,
  `specificationEndpoint`) dvojí `connectRedis` místo `createRedisClient`.

### Added — stráž redigovaného vstupu: `[REDACTED]` v deklarovaném poli odmítá zprávu (d.1280, 0.2b-1053)

- Slovo vlastníka (dialog 3): stráž ve wrapperu. `redactSensitiveDeep` z `@onlineapps/logger-contract`
  nahrazuje hodnoty deklarovaných citlivých polí placeholderem `REDACTED_PLACEHOLDER` v kopiích
  pro logy, trasy a události (`secretbox.md` §8); exekuční zpráva takovou kopií být nesmí —
  handler by se autentizoval řetězcem `[REDACTED]`, uložil ho jako heslo nebo poslal jako token
  a běh by hlásil úspěch.
- `invokeOperation` (po `ContextBuilder.build`, před validací vstupu): každé deklarované pole,
  jehož hodnota kdekoli v `input` (shoda podle jména klíče do hloubky, chůze jako
  `redactSensitiveDeep`) je placeholder → `InvalidEnvelopeError` → obálka 400 `INVALID_ENVELOPE`,
  handler se nevolá, vstupní schéma se nevaliduje. Hláška `[ServiceWrapper] Redacted input - field
  '<pole>' of operation '<op>' carries the redaction placeholder instead of a value: the submitter
  sent a redacted copy of the input … Fix: …` jmenuje pole (seřazená, každé jednou), nikdy hodnotu
  — ani placeholder, ani sousední hodnoty vstupu. Wrapper sám nic nerediguje.
- Pravidlo (rozhodnutí leada N1): klíč `sensitive_input_fields` zprávy (orchestrátor 8 posílá
  seřazené unikátní sjednocení deklarací receptu) → stráž použije ten; klíč chybí (přímá dispatch
  cesta `_dispatchViaInvokeOperation`, bez receptu) → vlastní deklarace operace
  `operations.json` `sensitive_input_fields`, čtená jediným čtenářem `sensitiveFieldsForOperation`
  z logger-contractu; klíč přítomný v jiném tvaru než pole neprázdných řetězců (`'password'`,
  `[1]`, `{}`, `['']`, `null`) → odmítnutí jménem (`INVALID_ENVELOPE`, `Got: <typ>`, nikdy obsah).
  Placeholder v NEdeklarovaném poli projde — stráž pravidlo uplatňuje, nehádá ho; `[]` projde.
- Jedna chůze: `src/redactedInputGuard.js` (`resolveSensitiveRule`, `findRedactedFields`,
  `assertNoRedactedInput`). Logger-contract chůzi, která hodnotu HLEDÁ bez kopie stromu,
  neexportuje (`redactSensitiveDeep` klonuje a nahrazuje) — lokální minimum, kandidát na export
  logger-contractu (nález d.1280). `REDACTED_PLACEHOLDER` se bere z logger-contractu, druhá
  konstanta nevzniká.
- Pro koho: podavatel, který wrapperu posílá kopii po `redactSensitiveDeep` místo exekuční zprávy,
  dostane odmítnutí jménem pole; správný podavatel nic nemění.
- Testy: `tests/unit/d1280-redacted-input-guard.test.js` (16: 1 vrchní pole, 2 vnořené, 2b v poli a
  více polí, 3 bez klíče = deklarace operace, 4 ×5 tvarů, kontroly 5–7b, přímá dispatch cesta
  + klasifikace `permanent`).

### Fixed — konfigurační pády v 0.17/0.18/0.19 jsou permanentní, ne přechodné (d.1292, nález d.1288/2)

- Konvence fází „bez značky = přechodná“ (0.16) hlásila konfigurační chyby jako přechodné:
  `_initializeState` chybějící `service.name`, `_initializeSecrets` chybějící platformní env
  (`REDIS_URL`, `SECRETS_MASTER_KEYS` — `requireEnv` service-commonu, bez značky),
  `_initializeOrchestrator` chybějící `service.env`/logger a chyby konstrukce
  (`ErrorHandlerConnector`, `OrchestratorConnector.create`, `_assertRetiredTimeoutKey`,
  kontrakt orchestrátoru), `_initializeInvocationContext` (ContextBuilder/ErrorMapper). Operátor
  četl „Transient: a restart … can succeed“ u pádu, který restart nikdy nezmění.
- Změna: vlastní konfigurační throwy nesou `markBootDisposition(PERMANENT)` (state
  `serviceName`; orchestrátor `service.env`, logger); `requireEnv` v `_initializeSecrets` je
  obalen (`requirePlatformEnv`) a chybějící env odchází permanentní — hláška service-commonu
  beze změny. FÁZE 0.19 čte dispozici z chyby s OPAČNÝM výchozím stavem: nic v ní nemluví se
  závislostí (kontext, error handler i orchestrátor se staví nad handly, které otevřely dřívější
  fáze), proto bez značky = permanentní; jen chyba označená TRANSIENT se opakuje. 0.16–0.18
  nechávají „bez značky = přechodná“ pro `connect()` konektorů (Redis bez odpovědi).
- Beze změny: Redis/broker, který odmítl spojení → přechodná (kontroly `cachePhase`, d1287
  „secrets connector that cannot connect“).
- Testy: `d1287-stateSecretsPhases` (+1: chybějící `SECRETS_MASTER_KEYS` → 0.18 permanentní),
  `d1288-orchestratorPhase` (kontext nejde postavit → 0.19 permanentní; dřív přechodná),
  `bootFailureAttribution` (0.19 orchestrátor odmítne → permanentní).

### Added — FÁZE 0.19 „Invocation Context & Orchestrator“: poslední krok bootu bez fáze ji dostal (d.1288)

- `_initializeInvocationContext()` (ContextBuilder + ErrorMapper nad snímkem konektorů) a
  `_initializeOrchestrator()` (WorkflowOrchestrator, jen s MQ klientem) běžely mezi 0.18 a 0.8
  bez fáze (README tabulka: řádek `—`); pád šel do vnějšího catche jako `INIT`. Rozhodnutí jako
  u 0.16–0.18 (d.142: číslo = identita fáze): `0.19`, další volné číslo, jedna fáze pro obě
  poloviny v pořadí, v jakém boot běží. Nikdy `SKIPPED` (kontext se staví při každém bootu): bez
  MQ `PASSED` s důvodem `the orchestrator is not prepared: no MQ client (wrapper.mq.enabled is
  not true)`, s MQ `PASSED` prostý; `FAILED` přes `_handleInitializationError` s dispozicí
  čtenou z chyby (bez značky = přechodná, jako u 0.16). Komentář `_handleInitializationError`
  jmenuje nový exemplář „pád mezi fázemi“: `_registerMQHooks()`.
- README tabulka fází: řádek `—` „v3 handler pipeline“ opraven na **0.1b Operations-Handler
  Alignment** (kód ji hlásí od d.195; tabulka ji nesla bez čísla), řádek `—` „Workflow
  orchestrator prepared“ nahrazen 0.19.
- Testy: `tests/unit/d1288-orchestratorPhase.test.js` (2: bez MQ celá kniha hodnotami s
  `0.19:PASSED` + důvod + trvání; kontext, který nejde postavit → 0.19 FAILED, souhrn jmenuje
  0.19, ne INIT), `ServiceWrapper.bootFailureAttribution.test.js` (+1: orchestrátor odmítne →
  jedna FAILED řádka 0.19; kontrola INIT přesunuta z `_initializeInvocationContext` na
  `_registerMQHooks`, který fázi dál nemá), snímky knihy fází v 8 sadách (+ `0.19:PASSED`).

### Documentation — kniha fází a startovní hlášky jdou na stdout i do loggeru ZÁMĚRNĚ (d.1288, W45 uzavřen)

- README § The logger: odstavec, že `[FÁZE …]` řádky, `PHASE_<status>` JSON a startovní hlášky
  (`_logPhase`, `_logSourceFailure`, `_logStartup`) jdou na oba kanály záměrně — viditelnost bootu
  i tam, kde logger píše jinam nebo ještě není (0.1, padlá 0.12) — s odkazem na
  `tests/unit/ServiceWrapper.consoleChannel.test.js`, který drží obě půlky (vše ostatní jen
  loggerem). Rozhodnutí leada (d.1287 STOP → d.1288): obě koleje zůstávají; W45 z fronty mizí.
  Kód beze změny.

### Added — FÁZE 0.17 „State Connector“ a 0.18 „Secrets Connector“: obě inicializace mají fázi (d.1287)

- Do d.1287 běžely `_initializeState()` a `_initializeSecrets()` mezi FÁZÍ 0.16 a kontextem
  invokace bez fáze (README tabulka: řádek `—`, „both throw“); pád v nich šel do vnějšího catche
  `initialize()` a kniha fází ho zapsala jako `INIT` — atribuci pro výjimku MEZI fázemi. Operátor
  četl `Boot stopped in phase INIT` pro konektor s vlastním blokem konfigurace, `connect()` a
  odmítnutími jménem (`state.host/port/password/db`, chybějící `wrapper.cache.url`;
  `secrets.url`, `secrets.masterKeys`). Změřeno testem: `secrets.url` odmítnuté jménem → kniha
  `INIT:FAILED`, souhrn „Boot stopped in phase INIT“.
- Rozhodnutí (1) vlastní fáze, ne (2) zahrnutí pod 0.16: tabulka fází (kontrakt) je nejmenovala
  jako fázi, ale FAILED řádek „Cache Connector“ pro odmítnutí stavu by jmenoval špatnou věc; číslo
  je identita fáze (d.142, d.535) — `0.17` State Connector, `0.18` Secrets Connector, další volná,
  v pořadí, v jakém boot obě už spouštěl. `SKIPPED` s důvodem (`the service declares no
  wrapper.state.enabled: true` / `…secrets…`), `PASSED` s trváním, `FAILED` přes
  `_handleInitializationError` s dispozicí čtenou z chyby jako u 0.16 (klíč odmítnutý jménem =
  permanentní, Redis bez odpovědi = přechodná). `bootFailure` a souhrn „Boot stopped in phase
  0.17/0.18 (…)“ jmenují fázi.
- Pro koho: kniha fází každé služby získává dva řádky (`0.17`, `0.18`) mezi `0.16` a `0.10`;
  kdo kníhu čte strojově (nikdo změřený — conf `scheduler` 020), vidí dvě nové fáze.
- Testy: `tests/unit/d1287-stateSecretsPhases.test.js` (5: `state.host` → 0.17 FAILED permanentní,
  0.18 nedosažena; `secrets.url` → 0.18 FAILED permanentní, ne INIT; konektor secrets bez
  spojení → 0.18 FAILED přechodná; kontrola: nic deklarováno → celá kniha hodnotami s 0.17/0.18
  SKIPPED a důvody; kontrola: obě připojí → 0.17 PASSED, 0.18 PASSED v pořadí, boot dokončen),
  `ServiceWrapper.bootFailureAttribution.test.js` (+2: pád uvnitř 0.18 — jedna FAILED řádka,
  `bootFailure` i souhrn jmenují 0.18), snímky knihy fází v 7 sadách (+ `0.17:SKIPPED`,
  `0.18:SKIPPED`).

### Added — FÁZE 0.1b odmítá vadnou deklaraci `sensitive_input_fields` jménem operace (d.1285, nález d.1280/2)

- Deklarace, kterou stráž redigovaného vstupu ani pravidlo receptu nemohou uplatnit, se dosud
  projevila až při první přímé dispatch operace jako `Error` 500 — po registraci a odběru fronty.
  Nově ji kontroluje `assertDeclaredSensitiveInputFields` (`src/redactedInputGuard.js`) ve FÁZI
  0.1b hned za `assertDeclaredStepTimeouts`: klíč chybí = žádná deklarace; přítomný musí být
  pole unikátních neprázdných řetězců (`[]` včetně). Jiný tvar (`'password'`, `[1]`, `['']`,
  `['a','a']`, `{}`, `null`) zastaví boot hláškou `[ServiceWrapper] Invalid sensitive_input_fields
  declaration - operation '<op>': must be an array of unique non-empty field names (string[]),
  got <hodnota>. Fix: …`; fáze 0.1b zapíše FAILED, verdikt souladu se nepíše, registrace neběží.
- Minimální vlastní kontrola s odkazem, ne druhá definice: registrační kontrakt
  `@onlineapps/service-validator-core` `operationsSchemaValidator.js` (2.2.0) kontroluje `retry`
  a `timeout_ms` (d.1273) a o `sensitive_input_fields` mlčí — nález d.1285 pro kontrakt
  (`registration-wire.md`, `schema-v3.md`); až pravidlo převezme, wrapper ho cituje.
- Testy: `tests/unit/d1285-sensitiveFieldsDeclaredAtBoot.test.js` (12: 6 tvarů, první vadná
  operace, kontrola platné/prázdné/chybějící; skutečná FÁZE 0.1b nad dočasným stromem služby
  s resolvovatelným handlerem — FAILED jménem operace, soulad nevolán; kontroly platná deklarace
  a chybějící klíč bootují jako dřív).

### Fixed — nečitelný `validation-failure.json` se hlásí a zahazuje, nepolyká se jako „žádné selhání“ (d.1285, nález d.1282/2)

- `_readValidationFailureFile` vracelo `null` na KAŽDOU chybu čtení i parsování; `null` čte
  startovní cesta jako „žádné předchozí selhání“ — cooldown tiše zmizel a soubor bez práva čtení
  navíc shodil následný zápis (`EACCES` z `_writeValidationFailureFile`, změřeno testem)
  (`architecture-principles.md` §3, §4).
- Nově: `ENOENT` = jediná tichá odpověď (`null`, bez řádku). Jakákoli jiná chyba (poškozený JSON,
  `EACCES`, …) = jeden WARN řádek `[ServiceWrapper][startup] Validation failure file unreadable -
  <cesta>: <důvod>. Treated as a changed cause: …` (+ `exception.*`, `failurePath`) a chování
  jako změněná příčina: soubor se smaže (`_clearValidationFailureFile`), cooldown nečeká, zápis
  po pokusech nese `attemptCount` = pokusy tohoto bootu (1 trvalý) a aktuální `causeFingerprint`.
  Druhé čtení v témže bootu už soubor nenajde → řádek jednou.
- Testy: `tests/unit/d1285-failureFileUnreadable.test.js` (5: poškozený JSON — WARN s cestou a
  důvodem, smazán, `null`; poškozený JSON před selhávajícím bootem → bez čekání, soubor 1;
  `EACCES` (chmod 000, test odmítá běžet jako root) → WARN s kódem, soubor 1; kontroly ENOENT
  tichý, správný soubor čten beze slova).

### Fixed — čítač pokusů restart-cooldownu patří k příčině; jiná příčina začíná od nuly (d.1282, INFRA-DOCS ID262)

- Vada (rozhodnutí leada: vada, ne záměr): `_applyRestartCooldown` se vracel, dokud
  `attemptCount < REVALIDATION_FAST_BACKOFF_MS.length`, DŘÍV než porovnal `causeFingerprint`;
  `_ensureValidationProof` pak do `conn-runtime/validation-failure.json` sčítal
  `attemptCount + attemptsMade` bez ohledu na příčinu. Příčina A ×3 boty a opravený strom
  selhávající na nové příčině B zapsaly B čtyřku; B dostalo šestihodinový cooldown po méně než
  šesti vlastních pokusech (zbytek doplnily pokusy A). Změřeno testem: soubor `{3, A}` + 1 trvalý
  pokus B → `attemptCount` 4 (má být 1); `{5, A}` + 1 B → 6 → další boot B čekal 300 000 ms;
  `{5, A}` + přechodný B běh (6 pokusů) → 11 (má být 6).
- Oprava: `_applyRestartCooldown` porovnává příčinu PRVNÍ, pak teprve práh počtu. Jiný
  `causeFingerprint` než ve stromu → stdout/log řádek `[ServiceWrapper][startup] Validation cause
  changed since the last failure - retrying fresh` s `previousAttempts` a `firstFailure` staré
  příčiny, soubor se smaže, běh startuje od nuly a zápis po pokusech nese novou příčinu
  s `attemptCount` = pokusy TOHOTO bootu (1 trvalý, 6 přechodných) a `firstFailure` této příčiny.
  Stejná příčina sčítá jako dosud; A ×6 téže příčiny → cooldown (strop 300 000 ms za boot) jako
  dosud; úspěch soubor maže, ať nesl kteroukoli příčinu.
- Pro koho: služba, které operátor opravil příčinu a která hned selže na jiné, už nečeká
  na cooldown, který si nevysloužila.
- Testy: `tests/unit/d1282-cooldownCounterPerCause.test.js` (7; hodnoty čtené ze souboru na
  disku — A×3→B, A×5→B další boot bez čekání, A×5→přechodný B, kontroly: stejná příčina sčítá,
  A×6 cooldown, bez souboru, úspěch maže).

### Fixed — README a komentáře o `health:shutdown` říkají pravdu (d.1280, nález d.1275)

- README „When MQ is lost for good“ tvrdil, že transport událost `health:shutdown` emituje ze své
  kontroly zdraví; komentáře `_registerMQHooks` a `_initializeMQ` popisovaly
  `criticalHealthShutdown*` jako klíče, které událost „zbrojí“. Po `@onlineapps/mq-client-core` 6
  (d.1275, slovo vlastníka dialog 7 (4)) událost ani oba klíče neexistují; wrapper je nikdy
  nepředával a posluchače odebral 2026-09-07 (§ „Removed — the MQ `health:shutdown` policy that
  could never apply“). Přepsáno na: trvalé kritické zdraví se hlásí (`health:critical`,
  `healthCriticalCallback`) a o životě procesu nerozhoduje; proces končí wrapper jednou kolejí
  `_shutdownFatally()`. `healthCheckInterval` zůstává klíčem transportu (čte ho jen periodická
  smyčka, kterou wrapper nechává vypnutou). `tests/unit/ServiceWrapper.lifecycleExits.test.js`
  beze změny tvrzení, zelený.

### Added — BREAKING — deadline POKUSU kroku: `timeout_ms` per operace, vynucuje wrapper (d.1264b)

- Slovo vlastníka: conf `step-error-classification` 005, varianta A — deadline jednoho běhu
  handleru per operace; platformní výchozí 60 000 ms; operace smí v `operations.json` vedle
  `retry` deklarovat `timeout_ms` (celé číslo 1 000–300 000 ms). Jiný tvar se odmítá JMÉNEM
  OPERACE (`[ServiceWrapper] Invalid timeout_ms declaration - operation '<op>': … got <hodnota>.
  Fix: …`), nikdy se nenahrazuje výchozí hodnotou — při bootu ve FÁZI 0.1b
  (`assertDeclaredStepTimeouts`) i v `invokeOperation`.
- `ctx.abortSignal` je signál `AbortController`u, který wrapper pro pokus vlastní (dosud
  no-op): abortuje ho deadline (důvod = `StepTimeoutError`) i vlastní signál volajícího
  (propojený, ne nahrazený). `invokeOperation` závodí handler s deadlinem (`Promise.race`);
  vypršení = obálka 504 `STEP_TIMEOUT`, `type` `TIMEOUT`, `details: { timeout_ms, operation }`
  (504 = `ErrorTypes.TIMEOUT` v `error-handler-core` `ErrorClassifier.js`). O opakování
  rozhoduje orchestrátor. Handler, který doběhne po deadlinu: výsledek se zahodí, jeden `warn`.
  Časovač pokus nepřežije. `STEP_TIMEOUT` je kód wrapperu (`WRAPPER_RESERVED_ERROR_CODES`).
- Hook `operationMutatesLookup` předává `timeout_ms` orchestrátoru beze změny (jako `retry`).
- Pro koho: handler s dlouhou prací poslouchá `ctx.abortSignal`; operace, jejíž p99 přesahuje
  60 s, deklaruje `timeout_ms` (conf 005 (2): p99 z produkce změří BIZ-general před vydáním).
- Testy: `tests/unit/ServiceWrapper.stepDeadline.test.js` (falešné hodiny),
  `tests/integration/stepDeadline.integration.test.js` (skutečný boot, handler, hodiny;
  odmítnutý boot 0.1b), `operationSpecLookup.integration.test.js` (`timeout_ms` přes hook).

### Removed — BREAKING — `wrapper.timeout` a předání `defaultTimeout` orchestrátoru (d.1264b)

- `config/runtime-defaults.json` klíč nenese, `_initializeOrchestrator` ho nečte ani nepředává;
  konfigurace, která ho ještě nese, zastaví fázi jménem: `[ServiceWrapper] Retired config key -
  "wrapper.timeout" is no longer read. … Fix: remove the key from config/service/config.json;
  declare timeout_ms per operation in operations.json.` Žádná biz služba klíč nedeklaruje.
- Čtyři otázky (`change-discipline.md` § Removing something removes its declaration):
  1. **Proč vznikl:** strop běhu workflow, kterým se konstruoval orchestrátor (`defaultTimeout`,
     tehdy jeho povinná volba).
  2. **Která část koncepce ho nesla:** orchestrátor jako vlastník časového limitu kroku. Ta část
     neplatí — orchestrátor volbu zrušil v d.99a-quater (`da7aa876`, „defaultTimeout zaniká —
     povinný parametr bez konzumenta“); instalovaný orchestrátor ji ve `src/` nemá.
  3. **Proč ho nikdo nečte:** ustoupil rozhodnutí — konzument ho odstranil, wrapper ho dál
     posílal (d.1261 změřeno: krok neměl žádný platformní deadline).
  4. **Je náhrada koncepčnější:** ano — deadline je deklarace operace (`timeout_ms`, týž záznam
     jako `retry`), vynucuje ji jediná komponenta, která handler spouští, a vypršení je chyba
     třídy TIMEOUT, o níž rozhoduje politika opakování (conf 005).
- Testy: `ServiceWrapper.errorHandlerLogger.test.js` § `wrapper.timeout is retired`; fixtury 7 sad
  bez `timeout: 30000`; `d733-oneRequiredSettingReader` (šest čtení, příklad ve Fix bez `timeout`).

### Changed — jeden tvar chyby na řádku logu: `exception.*` (d.1264b)

- Nález BIZ-pdfgen (5. 10.): `ErrorMapper` psal `exception.message`/`exception.type`,
  `ServiceWrapper.js` tutéž třídu selhání jako `error: <text>` (revalidace, MQ ladder, teardown, …)
  a na dvou místech podával celé `Error` jako meta. Nyní všech 26 míst v `ServiceWrapper.js`
  píše `...errorLogFields(err)` (`err`, `exception.message`, `exception.type`), meta fáze
  (`_logPhase`) totéž; záznam selhání zdroje (`_logSourceFailure`, není `Error`) nese
  `exception.message`. Souhrn teardownu: pole `errors` → `failures` (prvek = krok + `exception.*`).
- Mimo pravidlo záměrně: strukturované záznamy knihy fází na stdout (`PHASE_*`, pole `error`)
  a záznam `errorHandler.logError()` (`UnifiedLogSchema` z `error-handler-core`, cizí tvar).
- Testy: `tests/unit/d1264b-oneErrorLogShape.test.js` (pravidlo nad zdrojem, pozitivní kontrola),
  `d998b-consumerCancelWired`, `ServiceWrapper.shutdownCompletesEveryStep`,
  `ServiceWrapper.heartbeatFamily`.

### Changed — BREAKING — `input` operační zprávy je povinný objekt; obě `|| {}` zrušeny (d.1264)

- Slovo vlastníka: conf `cookbook-step-input` 001, varianta A — `input` je na task kroku
  povinný objekt (prázdné `{}` výslovně), mezi receptem a handlerem chybějící vstup nikdo
  nedoplňuje.
- `invokeOperation` odmítne zprávu, jejíž `input` chybí nebo není objekt (`null`, řetězec,
  pole, číslo): `InvalidEnvelopeError` → obálka 400 `INVALID_ENVELOPE`, handler se nevolá,
  vstupní schéma se nevaliduje. Hláška `[ServiceWrapper] Missing required field - input is
  required on an operation message and must be an object … Got: <typ>. Fix: …` jmenuje typ,
  který přišel.
- `_dispatchViaInvokeOperation` předává `message.input` beze změny (dřív `|| {}`) a odmítnutí
  vynáší stejně jako odmítnutí `workflow_id`/`correlation_id` z ContextBuilderu: `code`
  `INVALID_ENVELOPE`, `status` 400 → klasifikace doručení `permanent`, zpráva jde po prvním
  pokusu do `<service>.dlq`. Jedna kontrola, v jediném vstupu, kterým jdou obě cesty.
- Pro koho: služba, jejíž recept nebo vlastní publikace posílá operační zprávu bez `input`,
  doplní `"input": {}`. Kontrakt receptu (cookbook-core schéma + validátor) nese dávka d.1258.
- Testy: `tests/unit/ServiceWrapper.stepInputRequired.test.js`,
  `tests/integration/stepInputRequired.integration.test.js` (živý broker, `x-death`); 13 task
  fixtur ve 4 sadách dostalo `input: {}`.

### Added — hook `operationMutatesLookup` předává orchestrátoru `retry` operace (d.1264)

- Odpověď hooku nese vedle `mutates`, `resource_type` a `sensitive_input_fields` i
  `retry: spec ? spec.retry : undefined` — deklaraci politiky opakování kroku z téhož záznamu
  `operations.json` (`{ max_attempts, delays_ms }`, conf `step-error-classification` 002
  varianta B, 004 hodnoty). Wrapper ji NEVALIDUJE a nepřepisuje; validuje ji a odmítá jménem
  orchestrátor (`resolveRetryPolicy`). Operace bez deklarace odpovídá `undefined` → platí
  platformní výchozí orchestrátoru.
- Testy: `tests/unit/ServiceWrapper.errorHandlerLogger.test.js` § retry declaration,
  `tests/integration/operationSpecLookup.integration.test.js` (skutečný orchestrátor).

### Removed — BREAKING — `wrapper.errorHandling.{maxRetries,retryDelay,retryMultiplier}` (d.1264)

- Klíče vypadly z `ERROR_HANDLING_KEYS` a konfigurace, která je ještě nese, zastaví fázi
  orchestrátoru jménem každého z nich (kolej `dlqEnabled`): `[ServiceWrapper] Retired config
  key - "wrapper.errorHandling.<klíč>" is no longer read. Expected: a step is retried by
  @onlineapps/conn-orch-orchestrator under the retry policy of its operation; nothing in this
  wrapper retries a step. Fix: remove the key from config/service/config.json; retry declare
  per operation in operations.json (conf step-error-classification 004).` Víc klíčů najednou =
  jedno odmítnutí, které je jmenuje všechny.
- Konektoru `@onlineapps/conn-infra-error-handler` se dál předávají jen
  `circuitBreakerEnabled` a `compensationEnabled`. `error-handler-core` se nemění.
- Čtyři otázky (`change-discipline.md` § Removing something removes its declaration):
  1. **Proč vznikly:** nastavovaly počet pokusů a backoff `RetryHandler`u v
     `@onlineapps/error-handler-core` — zamýšlený knoflík „kolikrát a jak dlouho opakovat“
     pro volání obalená error-handler konektorem (`executeWithRetry`, akce `retry`
     v `handleError`).
  2. **Která část koncepce je nesla:** vrstva L2 errors (error-handler konektor jako
     obecný opakovač). Klasifikace chyb v té vrstvě platí dál (orchestrátor volá
     `classifyError`); opakování KROKU v ní ale není — to platforma drží v orchestrátoru
     (conf `step-error-classification` 001: opakování patří platformě, ne receptu).
  3. **Proč je dnes nikdo nečte:** nikdo je nezapojil. Wrapper volá na konektoru jen
     `logError()` (core → `classifier` + `logSchema`, `retryHandler` ne), orchestrátor jen
     `classifyError()`; `executeWithRetry`, `handleError`, `shouldRetry`,
     `calculateBackoff` ani `createErrorResponse` nevolá nikdo ze `src/` wrapperu ani
     orchestrátoru. Opakování kroku bylo vždy natvrdo v `WorkflowOrchestrator` (`retryConfig`);
     d.1214 / F10 to změřilo jako „ineffective“.
  4. **Je náhrada koncepčnější:** ano — politika je deklarace operace v `operations.json`
     (týž záznam, který už nese `mutates` a `sensitive_input_fields`), čte ji jediná
     komponenta, která o opakování kroku rozhoduje (orchestrátor), a vadnou odmítne jménem;
     výchozí hodnota má jednoho vlastníka (conf 004).
- Testy: `tests/unit/ServiceWrapper.errorHandlerLogger.test.js` (přepsána sada
  `wrapper.errorHandling`), `tests/integration/errorHandlingRetryKeysRetired.integration.test.js`
  (skutečný konektor + orchestrátor).

### Tests — fronty fixture služby `acting-person` patří sweepu tieru (d.1342, výjimka z d.1329)

- `tests/integration/acting-person.integration.test.js` pojmenovával fixture službu jen náhodným
  `RUN` (UUID bez „999“, kvůli sondě úniku — d.762c: číslice hodin `178999…` v `correlation_id`
  zčervenaly sadu), takže fronty, které z jména deklaruje wrapper pod testem (`<svc>.queue`,
  `.dlq`, `.workflow`, `.register`), neodpovídaly `parseRunName` (`tests/runName.js`, d.1329)
  a po SIGKILL zůstávaly na dev brokeru — d.1329 to zapsal jako vědomou výjimku.
- Nově `SERVICE = acting-person-fixture-${TIER_RUN}-${RUN}`: token tieru `sw-<ms>-<pid>` pro sweep
  + náhodná část pro jedinečnost. ID, která sonda úniku může potkat v obálce (`workflow_id`,
  `correlation_id`, `step_id`), zůstávají jen z náhodného `RUN`; sonda čte `ctx.person_id`,
  hlášku odmítnutí (jmenuje operaci, ne službu) a výsledek — číslice hodin do nich nedojdou.
- Testy: tatáž sada +2 (`its queue names carry this run's tier token …` — RED `Received: null`,
  GREEN; KONTROLA: ID sondy jsou jen náhodná, `parseRunName` = null, bez „999“). `tests/runName.js`
  komentář výjimky přepsán.

### Tests — jména IT tieru nesou token běhu `sw-<ms>-<pid>` a sirotky po SIGKILL maže sweep v globalSetup (d.1329, nález d.1320 (c))

Fronty, které sady tohoto tieru nechávají na sdíleném dev brokeru — ty, které deklarují samy
(`<service>.queue/.dlq`, `<service>.register`), i ty, které si z jména služby deklaruje wrapper
pod testem — jsou SUT k operacím z vlastního spojení: `exclusive` nejde, `autoDelete` bez
konzumenta nenastane, a běh ukončený před `afterAll` je nechával napořád. Od d.1329:

- `tests/runName.js`: `RUN = sw-<createdAtMs>-<pid>` (marker `sw` = service-wrapper, kebab-case,
  platný uvnitř jména služby `/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/`) a `parseRunName(name)` — jediné
  místo, které říká, co je tohoto tieru (token ohraničený `.`/`-`/začátkem/koncem); fronty živých
  služeb (`biz-*.queue`), platformní fronty i jména jiných tierů parsují na `null`. 25 sad bere
  `RUN` odsud (dřív 24 vlastních `${pid}-${Date.now()}` — dvě bez pomlčky — a jedno pid-only
  `d1076-heartbeat-<pid>`; bez markeru nebyl tier k poznání). `acting-person` si náhodný běh
  nechává záměrně (jeho sonda úniku nesmí v obálce najít číslice hodin) — mimo sweep, zapsáno.
- `tests/config.js` vydává `parseRunName` a `orphanAgeMs` (`DEFAULT_ORPHAN_AGE_MS`, 60 min);
  `tests/integration/setup.js` po sondě brokeru a bráně alarmu volá `runOrphanSweep(...)` týmž
  čtenářem `createMgmtAsk` z d.1326 (jeden čtenář, hlásí smetené i ponechané, fronta tieru, která
  nejde smazat, běh ukončí) — tělo sweepu je repa (`scripts/tests/sweepOrphanQueues.js`, d.1320).
- `tests/integration/orphan-sweep.integration.test.js` proti brokeru: fronta s tokenem starého
  běhu založená a opuštěná → pryč (mgmt 404); KONTROLA mladá fronta tohoto běhu i cizí jméno
  netknuty; druhý sweep nic; parser poznává tvary sad i wrapperu a odmítá cizí; sonda nad zdroji:
  žádná sada neskládá jméno z `process.pid`/`Date.now()` sama; `setup.js` má jediný `createMgmtAsk(`
  a sweep až za bránou alarmu. `src/**` netknuto.

### Tests — brána „broker pod alarmem“ IT tieru má jedno tělo v repu (d.1326)

- `tests/brokerAlarms.js` (`assertNoBrokerAlarm`, vlastní `fetch`) odstraněn — byl třetí kopií
  jednoho pravidla vedle `conn-orch-registry` (d.775) a `mq-client-core` (d.807). Tělo je teď
  `api/scripts/tests/brokerAlarm.js` (`refuseBrokerUnderAlarm({ ask, context, where })`) vedle
  `sweepOrphanQueues.js` (d.1320); čtenář management API tohoto tieru je `createMgmtAsk` z
  téhož místa (balíček vlastní kolej nemá), ve tvaru d.1324 `{ rabbitmq, timeouts, tier }` —
  rozpočty `tests/config.js` `TEST_TIMEOUTS` (`connection` 5000, `operation` 10000 ms, hodnoty
  d.1324/d.744b: mlčící broker končí tier v jeho vlastním rozpočtu, ne v globálním jestovém).
  `tests/integration/setup.js` obě spojuje.
- Co se změnilo ve větách: alarm = „The broker is under a resource alarm and is blocking
  publishers - <resource> on <node> (HTTP 503 from <host:port>/api/health/checks/alarms) …
  Fix: free what the alarm names (`rabbitmq-diagnostics -q alarms` …)“ (nejpřesnější z trojice:
  alarmovaný broker odpovídá a jen blokuje publishery); 503 bez seznamu = alarm „unreported“
  (dřív „did not answer … without an alarm list“); mlčení/odmítnuté spojení = věta čtenáře
  `[service-wrapper/integration] The management API did not answer - …`; HTTP jiný než 200/503 = „did not
  answer the alarm check - HTTP <n>“ (zachováno).
- `tests/unit/brokerAlarms.test.js` drží totéž nad sdíleným tělem a `createMgmtAsk` přes
  skutečný loopback server (alarm, více alarmů, 401, 503 bez seznamu, ECONNREFUSED, mlčící
  server, heslo nikdy ve větě).

### Tests — IT `d1105-shutdownDeadline` i `redisPassword` berou adresu svých kontejnerů z `DOCKER_HOST` jednou kolejí (d.1213)

- Test, ne chování: `d1105-shutdownDeadline` publikoval své Redis a RabbitMQ napevno na
  `127.0.0.1` a spojoval se tam; v CI běží daemon jako sidecar `docker`, jehož loopback job
  nesdílí, takže sada čekala 120 s na broker, na který nikdy nedosáhla (MR !30, 4 testy).
- Výběr adresy podle `DOCKER_HOST`, dosud vlastní kopie v `redisPassword`, je nyní jeden
  helper `tests/helpers/dockerDaemonEndpoint.js` (unit `tests/unit/dockerDaemonEndpoint.test.js`);
  obě sady ho používají a porty nechávají vybrat daemon (`docker port`).

## [12.0.1] — 2026-10-08

pin: `@onlineapps/conn-base-monitoring` 2.2.2 → 2.2.3

- Kód balíčku se od 12.0.0 nezměnil (kaskáda MC-PATCH: monitoring-core 4.2.0 → conn-base-monitoring
  2.2.3). Další změny v tarballu: generovaný výčet povinností uniformy v README nově jmenuje řádek
  `L-PACK-CHANGELOG` (`e519721e`, d.1257). Oddíly `### Tests` v `[Unreleased]` výš se týkají jen
  `tests/`, které `.npmignore` z tarballu vynechává — 12.0.1 je nenese a patří další verzi.

## [12.0.0] — 2026-10-03

pin: `@onlineapps/conn-base-cache` 2.0.1 → 2.1.0
pin: `@onlineapps/conn-base-monitoring` 2.2.1 → 2.2.2
pin: `@onlineapps/conn-infra-mq` 5.0.0 → 6.0.0
pin: `@onlineapps/conn-infra-secrets` 3.0.0 → 3.1.0
pin: `@onlineapps/conn-orch-cookbook` 6.0.0 → 7.0.0
pin: `@onlineapps/conn-orch-orchestrator` 6.0.0 → 7.0.0
pin: `@onlineapps/conn-orch-registry` 7.0.0 → 8.0.0
pin: `@onlineapps/conn-orch-validator` 13.0.0 → 14.0.0
pin: `@onlineapps/infrastructure-tools` 4.0.0 → 4.1.0
pin: `@onlineapps/mq-client-core` 5.0.0 → 5.1.0
pin: `@onlineapps/service-common` 4.0.0 → 4.1.0

### Changed — citace vyřazeného „RFC §“ nahrazeny ukazateli na živé uzly; znění jedné hlášky `ContextBuilder` (d.1167)

- Hláška `[ContextBuilder] mqMessage.envelope.tenant_id is required for bundle_scope="…"`: konec
  `tenancy must come from the MQ envelope (see RFC §6.3 Bug 1)` nahrazen
  `tenancy must come from the MQ envelope. Fix: api/docs/biz/20-tenancy/tenant-context.md § 5`.
  Test, který hlášku tvrdí doslova, se srovná při re-pinu na tuto verzi
  (`api_biz/hello/tests/integration/tenantGreetingCount.handler.test.js`).
- Komentáře a JSDoc v `src/ContextBuilder.js`, `src/HandlerRegistry.js`, `src/ServiceWrapper.js`,
  `src/index.js` a `tests/unit/ServiceWrapper.test.js` citovaly archivované a smazané invokační RFC
  přezdívkou „RFC §5.x/§6.3“. Místo nich `@see api/docs/biz/…md § <nadpis>` (`handler-dispatch.md`,
  `operation-context.md` § Release, `tenant-context.md` § 5, ADR `0001-http-loopback-removal.md`);
  `API.md` přegenerováno. Test `tests/unit/noRfcNickname.test.js` hlídá celé `src/**/*.js`
  (kontrolní případ: `RFC 6901` v `SchemaValidator.js` projde).

### Changed — BREAKING — vstupní brána dosazuje `default` ze schématu; `precompile()` odmítá `default` tam, kde se neuplatní (d.1164)

- `SchemaValidator` staví Ajv s `useDefaults: true`: chybí-li ve vstupu klíč, jehož vlastnost pod
  `properties` deklaruje `default`, `validateInput` ho do vstupu zapíše (týž objekt, který dostane
  handler). Explicitní `null` se nenahrazuje; přítomná hodnota se nemění; `strict`, `allErrors`,
  `removeAdditional: false` a „žádná koerce“ zůstávají.
- `precompile()` při bootu odmítne (`[SchemaValidator] … - Expected/Fix`, jmenuje operaci a JSON
  Pointer každého místa) `default` kdekoli ve VÝSTUPNÍM schématu a ve vstupním všude mimo vlastnost
  pod `properties` (vnořené objekty a `items` pole včetně) — kořen, větve `anyOf`/`oneOf`/`allOf`/`not`/
  `if`/`then`/`else`, `additionalProperties`, `patternProperties`, samotné `items`, `$defs`. Dnes
  0 výskytů ve všech `operations.json`, brána přichází se souladem.
- Pro koho: služby, které drží druhou kopii téže hodnoty v kódu, ji při re-pinu na tuto verzi ruší —
  ingest (`retry-pending-items`, `reconcile-source-send-status`, `retry-source-ack` `limit`;
  `check-source-send-status` `triggered_by`) a property (`layer-audit` `limit`, `offset`).
  Hodnota má pak jediného vlastníka, deklaraci.
- Komentář „locked per RFC §5.7“ (vyřazené RFC) nahrazen odkazy na `docs/biz/10-invocation/handler-dispatch.md`
  a `docs/governance/confirmations/input-validation-single-rail.md` 003; `API.md` přegenerováno.
  Test `tests/unit/d1164-inputDefaults.test.js`.

### Changed — hláška FÁZE 0.10 neradí env `INFRASTRUCTURE_HEALTH_QUEUE` (d.1158, INFRA-DOCS W44)

- `[ServiceWrapper] Health heartbeat publisher did not start - …`: část `Expected:` jmenuje jméno fronty
  z `wrapper.heartbeat.queueName`, jinak od jediného vlastníka `@onlineapps/service-common`
  `src/defaults.js` (`infrastructureHealthQueueName`, `getInfrastructureHealthConfig()`), které žádný env
  nemění; interval dál z `wrapper.heartbeat.publishInterval`, jinak `INFRASTRUCTURE_HEALTH_PUBLISH_INTERVAL`.
  Dřív hláška jmenovala `INFRASTRUCTURE_HEALTH_QUEUE`, který service-common od d.1137 nečte.
  `docs/CONFIGURATION_GUIDE.md` (řádek `heartbeat.queueName`) srovnán. Test
  `ServiceWrapper.heartbeatFamily.test.js` (celé znění `Expected:`, kontrola začátku a `Fix:`).

### Fixed — identita certifikátu v logu registrace bez řetězu `||` a bez vymyšleného `'none'`/`'unknown'` (W45, INFRA-DOCS `4929f854`)

- `Service registered: <služba>` nese `certificateIds` = jen pole certifikátu, která existují (`id`,
  `certificateId`), a bez certifikátu žádné; dřív `certificateId: id || certificateId || 'none'`.
  `✓ Certificate received: id=… certificateId=…` jmenuje přítomná pole; certifikát bez nich =
  `✓ Certificate received` (dřív `certificateId || id || 'unknown'`). Pomocník
  `_certificateIdFields()`; §3 bez fallbacků platí i pro logovou řádku. Test
  `ServiceWrapper.consoleChannel.test.js` (jen `certificateId` / jen `id` / obě / žádné / bez certifikátu).
- Odmítnutá registrace bez `message` už nepíše vymyšlené `message=none` do chyby
  `did not receive certificate` — pole se jmenuje, jen když ho registr poslal (test v
  `ServiceWrapper.test.js`). Komentář u čtení `workflow_id` opraven: hlavičky brány nesou
  `workflow_id`, ne `workflowId` (gateway `shared/workflowMessageHeaders.js`, d.1134).

### Changed — BREAKING — `REGISTRY_DISCOVERY_TIMEOUT_MS` a `BIZ_HEARTBEAT_INTERVAL_MS` čte konstruktor, ne FÁZE 0.7 (d.1130, 0.2b-814)

- Oba klíče čte konstruktor `ServiceWrapper` jednou, privátními čtečkami `_discoveryTimeoutMs()`
  a `_heartbeatIntervalMs()` do polí `this.discoveryTimeoutMs` a `this.heartbeatIntervalMs`
  (vzor `BIZ_SHUTDOWN_TIMEOUT_MS`, d.1105a); `_initializeRegistry()` používá pole. Dřív se četly
  ve FÁZI 0.7, jejíž catch klasifikuje každou chybu mimo `MQ_CONNECTION_FATAL` jako přechodnou —
  chybějící nebo nekladný klíč tak vedl k restartové smyčce místo odmítnutí konfigurace
  (`architecture-principles.md` §4; nález BIZ-DOCS `b36b5044`). Hlášky beze změny
  (`[ServiceConfig] Missing environment variable - … is required.` / `[ServiceWrapper] Invalid
  environment variable - … is <N> milliseconds. … Fix: set … in config/env-active/shared.env.`).
- Co se změní pro volající: `new ServiceWrapper(...)` bez obou klíčů teď vyhodí hned (i v testech
  konzumentů, které wrapper jen konstruují). Testy: `ServiceWrapper.discoveryDeadline.test.js`,
  `ServiceWrapper.specificationEndpoint.test.js` (odmítnutí konstruktorem, hodnota čtená jednou,
  kontrola `shutdownTimeoutMs`), IT `discoveryDeadline` a `heartbeatCadence` (větve selhání);
  `tests/platformEnv.js` nastavuje všechny tři klíče konstruktoru z `config/shared-env.json`.

### Changed — BREAKING — `shutdown()` má termín `BIZ_SHUTDOWN_TIMEOUT_MS`, po něm exit 1; signál končí 0 jen po čistém teardownu (d.1105)

- Nový povinný platformní klíč `BIZ_SHUTDOWN_TIMEOUT_MS` (`config/shared-env.json`, hodnota 20000,
  consumer `@onlineapps/service-wrapper`); čte ho konstruktor `ServiceWrapper` přes `requireNumberEnv`
  bez výchozí hodnoty — chybějící, necelé nebo nekladné číslo zastaví službu dřív, než cokoli otevře.
  Rozhodnutí vlastníka `api/docs/governance/confirmations/biz-shutdown-deadline.md` 001.
- `shutdown()` nastaví hlídač před prvním krokem; když teardown termín přesáhne (Redis nebo broker
  drží spojení a neodpovídá — změřeno d.1102), zapíše synchronně jednu řádku na stderr
  (`[ServiceWrapper] Shutdown did not finish within <N> ms - pending step '<krok>'; released: …;
  not reached: … - Fix: …`) a ukončí proces **exit kódem 1**. Kryje všechny volající: signál,
  `_handleInitializationError()` i `_shutdownFatally()`, nad kterou dřív nic SIGKILL neposlalo
  a proces zůstal viset navždy. Pořadí kroků se nemění (d.998b).
- Handler SIGTERM/SIGINT z `bootstrap()` (`src/signalShutdown.js`): `exit(0)` jen po čistém teardownu,
  odmítnutí `shutdown()` → `exit(1)` s jednou řádkou (dřív neošetřené odmítnutí se stackem); druhý
  signál během běžícího teardownu se ignoruje s jednou řádkou, druhý `shutdown()` se nespustí.
  Odstraněna mrtvá větev `stopHealthHeartbeat()` — `ServiceWrapper` má `shutdown()` vždy.
- Co se rozbije: prostředí bez `BIZ_SHUTDOWN_TIMEOUT_MS` (konstruktor odmítne:
  `[ServiceConfig] Missing environment variable - BIZ_SHUTDOWN_TIMEOUT_MS is required. …`) — klíč
  přichází generovaným `shared.env`; biz compose musí deklarovat `stop_grace_period` nad termínem
  (30s pro 20000, re-pin vlákny BIZ). Testy: `tests/unit/d1105-shutdownDeadline.test.js`,
  `tests/unit/d1105-signalShutdown.test.js`, IT `d1105-shutdownDeadline` (vlastní Redis a RabbitMQ
  v kontejnerech, `docker pause`: Redis → exit 1 na `cache-disconnect`, broker → exit 1 na
  `consumers`, fatální cesta → exit 1, kontrola → exit 0).

### Changed — BREAKING — master klíče SecretBoxu jen z env `SECRETS_MASTER_KEYS`; `wrapper.secrets.masterKeys` odmítnut (d.1098)

- `_initializeSecrets` četl klíče jako `secrets.masterKeys || SECRETS_MASTER_KEYS` — dvojkolej, a ta
  první vedla tajemství přes verzovaný `config/service/config.json`. Proměnná je
  `SECRETS_MASTER_KEYS` všude (`docs/architecture/secretbox.md` §9) a hodnota žije jen
  v `config/env-active/shared.env` (`config/shared-env.json`). Teď jen `requireEnv('SECRETS_MASTER_KEYS', …)`.
- `wrapper.secrets.masterKeys` se odmítá jménem (trvalá chyba bootu s `Fix:`); žádná
  `api_biz/*/config/service/config.json` ani šablona `templates/business-service` ho nepíše.
- Testy: `tests/unit/ServiceWrapper.redisPassword.test.js` (klíče z env, chybějící env, klíč
  v configu odmítnut), IT `redisPassword` (secrets s klíči z env; klíč v configu odmítnut před
  spojením).

### Changed — BREAKING — secrets berou adresu jen z `REDIS_URL`; `wrapper.secrets.url` odmítnut (d.1096)

- `_initializeSecrets` četl adresu jako `secrets.url || wrapper.cache.url || REDIS_URL` —
  trojkolej. Projekce SecretBox `state:meta:secret:*` je platformní (píše ji vlastník SecretBoxu,
  čte každá konzumující služba), stejně jako `registry:services` po d.1091; adresa je teď jen
  `requireEnv('REDIS_URL', …)`. `wrapper.cache.url` dál čtou jen konektory cache a stavu.
- `wrapper.secrets.url` se odmítá jménem (trvalá chyba bootu s `Fix:`), ne tiše ignoruje; žádná
  `api_biz/*/config/service/config.json` ho nepíše (měřeno: `secrets` nese jen `enabled`).
- Co se rozbije: prostředí bez `REDIS_URL` s doslovnou `wrapper.cache.url` — boot skončí hláškou
  `[ServiceConfig] Missing environment variable - REDIS_URL is required. …`. Každá biz služba
  má `REDIS_URL` v prostředí (píše ho do `wrapper.cache.url`). Testy: `tests/unit/ServiceWrapper.redisPassword.test.js` (sekce
  secrets), IT `redisPassword` (secrets přes `REDIS_URL`, cache URL bez hesla jako kontrola).

### Fixed — úklid discovery klienta nevisí na neodpovídajícím Redisu (d.1096)

- `shutdown()` zavíral klienta `registry-discovery` přes `quit().catch(() => disconnect())`.
  node-redis vyřídí `quit()` až po všech dlužných odpovědích, takže proti Redisu, který přestal
  odpovídat, `quit()` nikdy neskončí a teardown se zastavil na kroku `discovery-redis` i se vším
  za ním. Termín kolem QUIT nepomůže: po `quit()` je klient označen za zavřený a `disconnect()`
  hodí `The client is closed`, socket zůstane otevřený (změřeno na `@redis/client` 1.6.1).
- Teď jen `disconnect()`: klient jen čte, konzumenti jsou už zrušeni a proces po teardownu končí,
  takže dlužná odpověď by se ztratila tak jako tak. Testy:
  `tests/unit/d1096-discoveryCloseNoWait.test.js`,
  `tests/integration/discoveryCloseNoWait.integration.test.js` (falešný RESP server, který
  na QUIT neodpoví; socket zavřený ze strany serveru).

### Changed — BREAKING — discovery má termín a nečeká na ležící Redis (d.1093)

- Varianta A z d.1090 (slovo vlastníka, conf `biz-discovery-redis` 003): FÁZE 0.7 čte nový
  povinný platformní klíč `REGISTRY_DISCOVERY_TIMEOUT_MS` (`shared.env`, deklarace v
  `api/config/shared-env.json`) přes `requireNumberEnv` a předá ho klientovi registru jako
  `discoveryTimeoutMs`. Chybějící klíč nebo hodnota ≤ 0 zastaví boot před stavbou discovery
  klienta, s názvem klíče a `Fix:`.
- Klient `registry-discovery` se staví s `disableOfflineQueue: true`: dotaz při ležícím Redisu
  skončí hned místo čekání v offline frontě node-redis bez konce (d.1088, d.1090).
- Podmínka na hodnotu (komentář u čtení klíče): pokusy × termín + backoff orchestrátoru ≪ TTL
  `<service>.workflow` (300 s).
- Vyžaduje vydané `@onlineapps/service-common` s volbou `disableOfflineQueue` (d.1093) a
  `@onlineapps/conn-orch-registry` s `discoveryTimeoutMs` (d.1093); s dnes pinovanými verzemi
  se volba i termín tiše ignorují. Do přepnutí pinů proto zůstává červený právě jeden IT případ:
  `tests/integration/discoveryDeadline.integration.test.js` › „the discovery client it hands over
  refuses a lookup at once once Redis is gone“ (nainstalované service-common 4.0.0 volbu
  `disableOfflineQueue` nezná → `Received: "pending"`; s lokálním service-common 4/4 zelené).

### Changed — BREAKING — platformní Redis má jednu kolej: `REDIS_URL` (d.1091)

- Brána infrastruktury (FÁZE 0.4, `_waitForInfrastructureGate` — čte `infrastructure:health:*`)
  i klient discovery (FÁZE 0.7, `purpose: 'registry-discovery'` — čte projekci registru
  `registry:services`) berou adresu jen z `requireEnv('REDIS_URL', …)`. Dřív
  `wrapper.cache.url || REDIS_URL`: konfigurace cache služby s platformním klíčem jako tichým
  druhým zdrojem (`architecture-principles.md` §3; `change-discipline.md` § One rail per
  concern). U služby s `cache.enabled: false` bez `cache.url` nesl druhý zdroj jiný význam, aniž
  to řekl.
- Co se rozbije: prostředí, které má `wrapper.cache.url` zapsané doslova a `REDIS_URL` nemá —
  boot teď skončí hláškou `[ServiceConfig] Missing environment variable - REDIS_URL is required.
  … Fix: set REDIS_URL in config/env-active/shared.env.` Všech osm biz služeb píše
  `wrapper.cache.url = "${REDIS_URL}"`, takže klíč v prostředí mají; konfigurace se nemění.
- `wrapper.cache.url` dál čtou jen konektory cache a stavu (beze změny). Testy:
  `tests/unit/d1091-platformRedisOneRail.test.js`; IT `infrastructureGate` ukazuje `REDIS_URL`
  na svou databázi přes nový pomocník harnessu `pointRedisAtDatabase()` (`tests/config.js`).

### Changed — heartbeat registru je jedna smyčka, a to klientova (d.1076)

- Wrapper už netočí vlastní `setInterval` kolem `registryClient.sendHeartbeat()` s vlastním
  počítadlem neúspěchů. FÁZE 0.7 zapne smyčku pinovaného klienta
  `registryClient.startHeartbeat({ maxFailures, onFatal })`: rozpočet je dál
  `wrapper.registry.maxHeartbeatFailures` (týž klíč, tatáž hláška fail-fast při bootu pro chybějící,
  `0`, záporný i neceločíselný rozpočet — žádná změna konfigurace), `onFatal` vede na tutéž
  teardown kolej `_shutdownFatally()` (log §5 hlášky klienta → `shutdown()` → `exit 1`). Kadenci
  určuje jen konstruktor klienta (`heartbeatInterval` z `BIZ_HEARTBEAT_INTERVAL_MS`);
  `startHeartbeat(serviceName)` už interval nebere. Pole `heartbeatTimer` odešlo.
- `shutdown()` jako první krok volá `registryClient.stopHeartbeat()` — dřív, než
  `unregister()`, který smyčku zastaví až po své publikaci, tedy právě tehdy ne, když publikace
  selže.
- Změna chování, kterou nese klient: první beat jde hned při zapnutí smyčky (dřív až po prvním
  intervalu), takže rozpočet `N` se vyčerpá za `(N − 1) × interval` od zapnutí.
- Dvě těla jednoho konceptu (`change-discipline.md` § One rail per concern), nález INFRA
  2026-09-28. Testy: `tests/unit/d1076-heartbeatOneLoop.test.js`,
  `tests/integration/d1076-heartbeatOneLoop.integration.test.js` (wrapper + pinovaný klient).

### Fixed — IT testy nezapisují na disk ani heslo k brokeru; úklid kořenů ve `finally` (d.1108)

- `registryBootRefusal` psal do `<root>/src/config.js` celou `RABBITMQ_URL` včetně hesla
  (`wrapper.mq.url` ve fixture FÁZE 0.7 i FÁZE 0.2). Fixtury teď nesou `"${RABBITMQ_URL}"`;
  kontrolní případ „nikdo neposlouchá“ (port 1) nastaví před bootem vlastní klíč
  `TESTING_RABBITMQ_NOWHERE_URL` a fixture nese jen `"${TESTING_RABBITMQ_NOWHERE_URL}"`; klíč
  z prostředí odchází ve `finally`. Ostatní sady URL brokeru do souboru nepíšou (config drží
  v paměti nebo mají `mq: { enabled: false }`).
- Pomocník d.1107 se přejmenoval na `tests/helpers/credentialOnDisk.js` a hlídá obě pověření
  (`REDIS_URL`, `RABBITMQ_URL`): `credentialForms()` pro obě URL (fail-fast bez hesla),
  boolean `rootCarriesCredential(root)`.
- `afterAll` dalších jedenácti sad, které zakládají kořen `sw-*` (`acting-person`,
  `correlationIdOneRail`, `d701`, `d741`, `envelopeDispatchKey`, `envelopeRejection`,
  `error-hierarchy`, `logger-bootstrap` ×2, `phase-book`, `reservedErrorCode`,
  `stepIdOneRail`), maže kořen ve `finally`. Kód balíčku (`src/**`) se nemění.

### Fixed — IT testy nezapisují heslo k Redisu na disk a uklízejí `$TMPDIR/sw-*` (d.1107)

- Šest integračních sad (`cacheHealth`, `cachePhase`, `d700`, `d705`, `state-in-context`,
  `registryBootRefusal`) psalo do generovaného `<root>/src/config.js` pod `$TMPDIR/sw-*` celou
  `REDIS_URL` včetně hesla; přerušený běh soubor nechal ležet a transform cache Jestu
  (`$TMPDIR/jest_*`) si z každého takového `config.js` uložila kopii. Fixtury teď deklarují
  `wrapper.cache.url = "${REDIS_URL}"` — týž zástupný tvar jako `config.json` biz služby — a
  wrapper ho dosadí z prostředí běžce (`ConfigLoader.resolveEnvPlaceholdersDeep` v
  `_processConfig`; `docs/standards/RUNTIME_CONFIG_RULES.md` § JSON placeholders).
- Kontrola je boolean nad každým souborem kořene (`tests/helpers/credentialOnDisk.js` od d.1108,
  vzor d.1104 — nikdy `toContain` nad obsahem); kontrolní případ: wrapper URL s pověřením
  opravdu drží.
- `afterAll` těch sad maže kořen v `finally`, i když shutdown hodí výjimku. Sonda
  `fixtures/wrapper-shutdown-probe.js` zakládá kořen ve `workDir` rodičovské sady (ten maže
  její `afterAll`), ne přímo v `$TMPDIR` — běh `keep` je zabit SIGKILL a sám nic neuklidí.
- Kód balíčku (`src/**`) se nemění.

### Tests — testy staví registry klienta s `discoveryTimeoutMs` jako produkční cesta (d.1200)

- Test, ne chování: pět míst, kde si testy staví `ServiceRegistryClient` samy
  (`d1019-registrationMetadata`, `d1076-heartbeatOneLoop`, `d998-registryConsumerClosedOnShutdown`,
  `registrationPayloadKeys` v integračním tieru, `ServiceWrapper.lifecycleExits` v unit tieru),
  nepředávalo `discoveryTimeoutMs`, povinný od kontraktu registry 8.0.0 (d.1093); se zdrojem
  registry z `release/w4` proto padalo 11 integračních a 3 unit testy hláškou
  `Missing constructor option - discoveryTimeoutMs is required`. Všechna teď předávají
  `Number(process.env.REGISTRY_DISCOVERY_TIMEOUT_MS)` — klíč, který `tests/platformEnv.js` nastavuje
  oběma tierům z deklarace `config/shared-env.json` a který konstruktor wrapperu čte do
  `this.discoveryTimeoutMs` (d.1130) — nikdy literál. Tvrzení beze změny; `src/**` se nemění.

## [11.0.0] — 2026-09-27

pin: `@onlineapps/conn-base-state` 1.0.3 → 2.0.0
pin: `@onlineapps/conn-infra-error-handler` 3.0.1 → 4.0.0
pin: `@onlineapps/conn-infra-mq` 4.1.2 → 5.0.0
pin: `@onlineapps/conn-orch-cookbook` 5.0.3 → 6.0.0
pin: `@onlineapps/conn-orch-orchestrator` 5.3.0 → 6.0.0
pin: `@onlineapps/conn-orch-registry` 6.0.3 → 7.0.0
pin: `@onlineapps/conn-orch-validator` 12.2.0 → 13.0.0
pin: `@onlineapps/handler-contract` 1.0.0 (nová závislost)
pin: `@onlineapps/infrastructure-tools` 3.1.2 → 4.0.0
pin: `@onlineapps/mq-client-core` 4.2.0 → 5.0.0
pin: `@onlineapps/service-validator-core` 2.1.0 → 2.2.0

### Removed — BREAKING — listener sdílené fronty workflow.control (d.1060)

Fronta `workflow.control` je zrušená (`docs/governance/confirmations/cookbook-execution-owner.md` 002).
Wrapper proto ve FÁZI 0.9 zakládá už jen dva konzumenty — `workflow.init` a `<service>.workflow`.

- `_startWorkflowControlListener()` pryč i s voláním ve FÁZI 0.9; `workflowControlConsumerTag`
  pryč; `_cancelConsumers()` ruší jen `workflow.init` a `<service>.workflow`.
- README § Shutting down jmenuje dva konzumenty.
- Testy: nový případ „FÁZE 0.9 nekonzumuje `workflow.control`, `workflow.init` ano“
  (`ServiceWrapper.listenersAfterOrchestrator.test.js`) a „tag `workflowControlConsumerTag`
  se neruší“ (`d998b-consumerCancelWired.test.js`); tabulky listenerů, sady shutdownu a popisky
  fronty v `_processWorkflowMessage(…)` přepsány na dva konzumenty / `<service>.workflow`.

### Tests — `registrationPayloadKeys` čeká `responseQueue` kopie `<service>.registry.<instance_id>` a frontu po sobě maže (d.1023)

- Kontrolní případ tvrdil starý drát `<service>.registry`, registry klient od d.817c jmenuje frontu
  instance: test teď předá klientovi `instanceId` z `_makeInstanceId()` fixturního wrapperu, čeká
  přesně `` `${SERVICE}.registry.${WRAPPER.instanceId}` `` i `instance_id` a teardown maže i tuto
  trvalou frontu, kterou dosud každý běh nechal na brokeru.
- `d998-registryConsumerClosedOnShutdown` měřil konzumenta jen na `<service>.registry`: teď předá
  klientovi `instanceId` fixturního wrapperu a čeká `consumerCount` 1 před a 0 po `shutdown()` na obou
  frontách (`<service>.registry` i `<service>.registry.<instance_id>`); ten i
  `d1019-registrationMetadata` mažou v teardownu i frontu instance.

### Tests — IT sada proti balíčkům vlny 3: registry klient s `instanceId`, fixtura FÁZE 0.2 platná pro validator (d.1015)

- **`registrationPayloadKeys` a `d998-registryConsumerClosedOnShutdown`** stavěly
  `ServiceRegistryClient` bez `instanceId`, který registry klient od d.817 vyžaduje — konstruktor
  hodil `Missing instanceId` a 5 případů padlo dřív, než cokoli změřilo. Testy mu předávají UUID,
  přesně jako `ServiceWrapper._makeInstanceId()`, když nasazení žádné neurčí. Tvrzení beze změny.
- **`registryBootRefusal` (FÁZE 0.2)**: fixtura bez `tests/cookbooks` měla odmítnout validator
  „přesně z jednoho důvodu" (`MISSING_COOKBOOKS`), validator vlny 3 ale hlásil i
  `INVALID_OPERATIONS_DOCUMENT` (chybí `schema_version`) a `INVALID_OPERATIONS_SCHEMA` (chybí
  `mutates`, u čtoucí operace pak `resource_type`). Fixtura (`operations.json` i zrcadlo v
  `src/config.js`) nese `schema_version: '3.0'`, `mutates: false`, `resource_type: null`; test se
  nemění a jediný důvod odmítnutí je zase ten, o kterém mluví.

### Changed — BREAKING: brandovaná chyba s vadným `type` je `MALFORMED_BUSINESS_ERROR`; do obálky se `type: null` nedostane (d.1004)

Tvar obchodní chyby vlastní `@onlineapps/handler-contract` (re-export `ErrorMapper`), a ten
nově měří i `type`, je-li přítomen: neprázdný řetězec. Vlastní brandovaná třída služby
s `type: null`, `''` nebo číslem dřív prošla jako obchodní chyba a `toCallerEnvelope` /
`ErrorMapper.map()` tu hodnotu poslaly volajícímu v `error.type` (proti `ErrorEnvelopeBody`
„absent, never `null`"). Nově: 500 `MALFORMED_BUSINESS_ERROR`, `error` log s vadou
`type is null (object), expected a non-empty string when present` a obálka `type` nenese;
`Fix:` té hlášky nově jmenuje i `type`. Osud doručení je pak tentýž jako u každé jiné
brandované chyby s vadným tvarem: `_classifyDeliveryError` → `transient` (dřív
`permanent`). Nepřítomný `type` a deklarované jméno třídy (`'DECLINED'`) beze změny.
Sada `tests/unit/d1004-declaredTypeShape.test.js`.

### Tests — dvojník `StateConnector` je jeden: `tests/mocks/connectors.js` re-exportuje `MockStateConnector` z `@onlineapps/conn-base-state` (d.976)

- Vlastní kopie dvojníka zanikla; knihovní dvojník nese kontrakt konektoru sám (conn-base-state d.976a) a stráž `tests/unit/stateDoubleOneRail.test.js` hlídá, že druhý nevznikne.

### Fixed — workflow zpráva bez `cookbook` odmítnuta jménem ve wrapperu, ne `TypeError`em v orchestrátoru (d.969)

`_normalizeWorkflowMessage()` odmítá workflow zprávu, jejíž `cookbook` chybí nebo není objekt
(pole povinné podle `api/docs/biz/70-contracts/workflow-message.md` §3.2):
`InvalidEnvelopeError`, 400 `INVALID_ENVELOPE`, `permanent`; orchestrátor se nevolá, handler neběží.
Změřeno před opravou ve službě s orchestrátorem (tak běží každá nasazená služba): zpráva s oběma
id a bez cookbooku prošla do `WorkflowOrchestrator.processWorkflowMessage()` a padla ve validátoru
cookbooku na `TypeError` (`error_type` `UNKNOWN`, `error_code` `null`); orchestrátor ji sám
dead-letteroval a wrapper doručení potvrdil. Sada `ServiceWrapper.envelopeDispatchWithOrchestrator`
to měří proti skutečnému `WorkflowOrchestrator`; hlavičky sad `envelopeDispatchKey` (unit + IT)
říkají, že měří službu bez orchestrátoru.

### Changed — BREAKING: konektor stavu dostane `wrapper.cache.url` celou; `_redisConnection()` pryč; `wrapper.state.{host,port,password,db}` odmítnuty (d.958)

`@onlineapps/conn-base-state` od d.808b (`64a0e537`) bere spojení jedinou hodnotou `url` a `host`,
`port`, `password`, `db` odmítá jménem (`[StateConnector] Option "host" is not accepted`) — tvar,
který už mají cache a secrets. `_initializeState()` dosud URL rozkládal přes `_redisConnection()`
a předával kusy; teď předá `{ url: wrapper.cache.url, serviceName, logger }` a nic nerozkládá.

- `_redisConnection()` zanikl i s docblockem — `_initializeState()` byl jeho poslední čtenář
  (`src/**`: 0 zásahů).
- `wrapper.state.{host,port,password,db}` (dřívější přepis URL) žádná služba nedeklaruje (osm
  `config/service/config.json` na devel: `state` nese `enabled`, meta i `serviceName`); klíč, který
  už nikdo nečte, se odmítne jménem a boot stojí (Permanent).
- Chybějící `wrapper.cache.url` při zapnutém stavu = jmenovaná chyba (Permanent) — jinak by si
  konektor vzal `REDIS_URL` z prostředí sám, druhý zdroj pro fakt, který služba deklaruje.
- Deklarované env wrapperu beze změny (`REDIS_URL`); wrapper `REDIS_HOST`/`REDIS_PORT` nečte.
- Vyžaduje `@onlineapps/conn-base-state` s d.808b (nevydáno; pin přepne vlna). Dvojník stavu
  v `tests/mocks/connectors.js` odmítá kusy stejně jako konektor a nese `deleteMatching()`
  (metoda nevydaného konektoru, d.762).

### Changed — BREAKING: `bootstrap()` odmítá `options.serviceBaseDir`; `service.env` jen z načtené konfigurace (d.953)

Dvě další koleje téhož druhu jako `options.config` (d.948), `architecture-principles.md` §3:

- `options.serviceBaseDir || path.join(serviceRoot, 'src')` — změřeno: volbu nepředává žádná biz
  služba (`index.js` osmi repozitářů na `devel`), šablona ani `scripts/add-service.sh`; jen jedna
  unit sada wrapperu. Volba je vyřazena a odmítnuta jménem (`[bootstrap] options.serviceBaseDir is
  retired - … Fix: …`); handlery se řeší vždy z `<serviceRoot>/src`. Konstruktor
  `new ServiceWrapper({ serviceBaseDir })` beze změny.
- `env: config.service.env || runtimeCfg.get('nodeEnv')` — všech osm služeb staví `src/config`
  přes `ConfigLoader.loadAll()`, které `service.env` plní z téhož `nodeEnv` (NODE_ENV); druhé čtení
  tu bylo mrtvé pro každou živou službu. `bootstrap()` teď bere jen `config.service.env` a
  konfiguraci bez něj odmítne hned: `[ServiceWrapper] <cesta>/src/config exports no service.env -
  … Fix: build src/config with ConfigLoader.loadAll(…)`. Import `./config` z `src/index.js` odpadl.
- Sada `tests/unit/bootstrap.configFromServiceRoot.test.js` rozšířena (dvě odmítnutí + kontrolní
  případ: `service.env` a základ handlerů z kořene i při jiném NODE_ENV).

### Changed — BREAKING: `bootstrap()` čte konfiguraci jen z `<serviceRoot>/src/config`; `options.config` odmítá (d.948)

`bootstrap()` četl `options.config || require(<serviceRoot>/src/config)` — dvě koleje k jednomu
faktu, první tiše vyhrávala (`architecture-principles.md` §3, `change-discipline.md` § One rail
per concern). Změřeno před změnou: žádná biz služba (`index.js` osmi repozitářů na `devel`),
žádná ze dvou šablon (`templates/business-service/index.js`, šablona validatoru) ani
`scripts/add-service.sh` `options.config` nepředávají; předávaly ho jen unit sady wrapperu.

- `options.config` je vyřazen a odmítnut jménem (jako `options.setLogger`): `[bootstrap]
  options.config is retired - Expected: … Fix: drop the option; …`. Žádný tichý ignor.
- Chybějící `<serviceRoot>/src/config` = `[ServiceWrapper] bootstrap() found no service
  configuration at <cesta> - Expected: … Fix: …`; modul, který existuje, ale při načtení padne,
  hlásí SVOU chybu (nepřejmenuje se na „chybí“).
- Unit sady píšou konfiguraci do kořene služby (`tests/helpers/serviceRoot.js`, kopie fixtury
  `tests/fixtures/test-service`); nová sada `tests/unit/bootstrap.configFromServiceRoot.test.js`.
- Konstruktor `new ServiceWrapper({ config, … })` beze změny (injekce závislostí).

### Fixed — žádný hluboký import z `@onlineapps/service-validator-core` (d.948)

`ContextBuilder.js` bral `VALID_BUNDLE_SCOPES` z `…/src/validators/operationsSchemaValidator`
a `ServiceWrapper.js` `FingerprintUtils` z `…/src/utils/FingerprintUtils`; obojí teď ze vstupu
balíčku (tytéž objekty, `===`). `VALID_BUNDLE_SCOPES` vyváží vstup až od d.720 (`7bac4f03`),
který ještě není vydaný — vydání `@onlineapps/service-validator-core` s ním je předpokladem
vydání tohoto balíčku (pin přepne vlna). Hlídá nová sada `tests/unit/noDeepImports.test.js`
(žádné `require('@onlineapps/<pkg>/src/…')` pod `src/`; kontrolní případ ověřuje, že vzor
kousne a že sken vidí importy, které `src/` dělá).

### Changed — BREAKING: fáze 0.7b izoluje selhání per zdroj; hook `scheduler.recurrences` vrací `{ recurrences, failures }` (conf `scheduler` 020, d.943)

Dosud jeden vadný zdroj shodil boot celé služby (conf `scheduler` 019 B). Podle conf 020 (O1 (i)):

- **Kontrakt hooku (BREAKING):** hook vrací `{ recurrences: [...], failures: [...] }`, každé
  selhání `{ source, error: { code?, message, details? } }`. Pole (dosavadní tvar), chybějící
  klíč, selhání bez `source` nebo `error.message` či klíč mimo kontrakt = chyba hooku a boot
  padá hláškou `[ServiceWrapper] scheduler.recurrences must resolve to { recurrences: [...],
  failures: [...] } (service-wrapper 11.0.0) - got … Fix: …`. Žádný fallback na starý tvar.
- **Selhání zdroje se izoluje:** zdroj z `failures`, záznam odmítnutý kontrolou tvaru wrapperu
  a záznam odmítnutý builderem požadavku (`error.code ===
  scheduler.SCHEDULER_REQUEST_INVALID` z `@onlineapps/mq-client-core` — rozlišuje se kódem,
  nikdy textem) se nezaregistrují; ostatní ano. Každý dostane řádek úrovně error
  `[FÁZE 0.7b] source <source> - FAILED: <message>` se strukturovanou kopií
  `PHASE_SOURCE_FAILED` (`source`, `code`, `error`, `details`). Záznam bez `id` se jmenuje
  pozicí `<index N>`.
- **Nový stav fáze `PARTIAL`** (`_logPhase`): `[FÁZE 0.7b] Declared Recurrences - PARTIAL
  (<k> registered, <m> sources failed)` + `PHASE_PARTIAL`, úroveň warn. Nezapisuje
  `bootFailure`, `bootstrap()` je ready; `FAILED` si drží jediný význam „boot stojí“ (d.532).
  Selhaly-li všechny zdroje, je to `PARTIAL (0 registered, N sources failed)`.
- **Selhání služby dál fail-fast:** chybí MQ klient, hook vyhodí nebo poruší kontrakt, připnuté
  jádro nemá `scheduler.createSchedulerPublisher` ani `scheduler.SCHEDULER_REQUEST_INVALID`,
  publikace selže transportem.
- Heartbeat beze změny (conf 020 O3).
- Vyžaduje `@onlineapps/mq-client-core` s `scheduler.SCHEDULER_REQUEST_INVALID` (d.943); pin
  přepne vlna při vydání.
- Sady: nová `tests/unit/ServiceWrapper.declaredRecurrences.partial.test.js`,
  `ServiceWrapper.declaredRecurrences.test.js` srovnána na nový kontrakt.

### Changed — `OperationContext` a rodina chyb handleru definovány v `@onlineapps/handler-contract` (conf handler-contract 001)

`OperationContext` a rodina chyb handleru definovány v `@onlineapps/handler-contract`, wrapper je
re-exportuje (conf handler-contract 001). Týká se tříd `OperationContext`, `BusinessError`,
`ValidationError`, `NotFoundError`, `ConflictError`, `BusinessRuleError`, `AuthorizationError`,
`InvalidEnvelopeError`, brandu `BUSINESS_ERROR_BRAND` a predikátů `isBusinessError`,
`hasBusinessErrorShape`. Hlavní vstup i `./errors` vracejí tytéž objekty, jaké exportuje
`@onlineapps/handler-contract` (`===`), takže `acting-person-capability` 001 i
`wrapper-error-exports` 001 platí beze změny; mění se jen místo definice. Důvod: Tier-1 běžec
(`@onlineapps/conn-orch-validator`, L3) smí stavět týž kontext jako produkce, aniž by importoval
z wrapperu (L4, architecture-principles §7).

- Nová závislost `@onlineapps/handler-contract` `1.0.0` (exact).
- `ErrorMapper` čte brand a tvar přes predikáty balíčku kontraktu (`hasBusinessErrorBrand`,
  `describeBusinessErrorDefects`); co přijímá, se nemění — rozpoznání je brand + tvar, ne identita
  třídy (`business-error-contract` 001). `UnknownOperationError` a `AbortError` zůstávají
  definované zde a brand dostávají přes `brandBusinessErrorClass`.
- `./errors` nyní kromě `src/ErrorMapper.js` načte i `@onlineapps/handler-contract` (bez
  závislostí); `ServiceWrapper` ani jiný soused `@onlineapps` se nenačítá.
- Jednotkové testy tříd (`requireActingPerson`, `normalizeDateRange`) odešly s třídou do
  `@onlineapps/handler-contract`; `packageExports.test.js` tvrdí re-export identitou.

### Fixed — `shutdown()` dokončí všechny kroky a pojmenuje ty, které padly (d.643)

Celý teardown seděl v jediném `try`, jehož `catch` při PRVNÍM selhání rethrownul.
Volající, na kterém záleží, je `_handleInitializationError()` — úklid po neúspěšném
bootu — takže nejčastějším vstupem byl boot padlý ve FÁZI 0.7: `registryClient.unregister()`
odmítl a vzal s sebou cache, state a secrets konektor, `mqClient`, druhé MQ spojení
registru, reset značek konzumentů, `isInitialized` i telemetrický transport. Všechno
zůstalo otevřené v procesu, který vzápětí volá `process.exit(1)`, a jediná stopa byla
`Teardown after a failed boot did not complete: <hláška>` — bez jména kroku a bez výčtu
toho, co zůstalo viset.

Změřeno proti 10.1.1 na živém brokeru (sonda `wrapper-shutdown-probe.js`, chování
`failing-step`): po selhání jednoho kroku `isInitialized: true`, `[Telemetry] Shutdown
complete` chybí a v smyčce zůstává `Socket` na port brokeru (`reffed: true`).

Nově má každý krok vlastní `try`, selhání se sbírají, jeden souhrn řekne, co bylo
uvolněno a co odmítlo, transport se zavírá LAST (a zavře se vždy) a teprve pak
`shutdown()` hodí jedinou chybu.

**Co se mění na cestě selhání** (šťastná cesta ani jeden podpis se nemění):

- hozená chyba je `AggregateError` místo první chyby kroku; hláška zní
  `[ServiceWrapper] Shutdown incomplete - failed steps: <krok> (<hláška>), … Released: … Fix: …`,
  původní chyby jsou v poli `errors` a první i v `cause`;
- `isInitialized`, značky konzumentů a všechny zbylé konektory se uvolní i tehdy,
  když některý krok selže;
- při neúplném teardownu se místo řádku `ServiceWrapper shutdown complete` zapíše
  `logger.error` se souhrnem (`failedSteps`, `releasedSteps`, `notStartedSteps`, `errors`);
  každý padlý krok navíc hlásí `logger.warn` v okamžiku selhání.

**Teardown před FÁZÍ 0.12 už nepadá na `TypeError` (d.643c).** `shutdown()` začínal
`this.logger.info(...)`, ale `this.logger` je `null` z konstruktoru až do FÁZE 0.12 —
a `_handleInitializationError()` volá teardown i pro fázi `INIT`, tedy i pro boot padlý
ve FÁZI 0.1 Configuration Load. Úklid po nejranějším možném neúspěšném bootu tak spadl
dřív než jeho první krok. Nově se rail volí **stavovou podmínkou** (proběhla 0.12?),
nikde ne `logger || console`: bez loggeru jde jeden souhrn na `console.error` — rail,
na který `_handleInitializationError()` píše i dnes — a jmenuje kroky jako `not started`
(žádný konektor v té fázi ještě nevznikl; sekvence se přesto projde, takže cokoli, co by
existovalo, se uvolní a pojmenuje). Souhrn nese `notStartedSteps` i na normální cestě.

### Changed — BREAKING: obálka má jediný dispatch klíč `operation`; `step.operation` v obálce se nečte (d.681, `87942d75`)

`_processWorkflowMessage()` vybírá handler jedinou podmínkou: `message.operation`, který je
v `operations.json` služby. Druhá větev `message.step?.operation` zanikla (rozhodnutí vlastníka
`invocation-envelope-dispatch-key` 001; změřeno nad branou, `WorkflowOrchestrator`,
`conn-orch-validator`, `api_biz/*` a `fe_adminui`: tvar `step: { operation }` do těla zprávy
nepublikuje nikdo). Z logovací věty `[ServiceWrapper] Processing message from <fronta>` zmizelo pole `step`.

- Obálka, jejíž jedinou cestou k operaci je `step.operation`, jde stejnou cestou jako obálka bez
  `operation`: se sestaveným orchestrátorem (služba s MQ klientem) do orchestrátorové větve
  (`_normalizeWorkflowMessage()` + `processWorkflowMessage()`), bez něj do poslední větve —
  `InvalidEnvelopeError`, 400 `INVALID_ENVELOPE`, `Unknown message format or operation: …`,
  klasifikace `permanent`, tedy dead-letter bez opakování.
- Nese-li obálka oba klíče, rozhoduje `operation`. `step.service` dál určuje, že zpráva patří jiné
  službě.
- `step.operation` v RECEPTU se neruší — zaniká jen klíč obálky.
- Sady `tests/unit/ServiceWrapper.envelopeDispatchKey.test.js`,
  `tests/integration/envelopeDispatchKey.integration.test.js` (proti brokeru, bez orchestrátoru).

### Changed — BREAKING: `ctx.stream` je slot `null`, ne tichá no-op funkce (d.802b, `b15030b9`)

`ContextBuilder` dával do `ctx.stream` funkci, která při MQ klientovi bez `publishChunk` vracela
`Promise.resolve()` — handler se dozvěděl, že odeslal, a neodeslalo se nic. Teď `ctx.stream` je
vždy `null` (týž tvar jako slot `db`); volání `ctx.stream(chunk)` v handleru skončí `TypeError`
na místě volání. Počet polí `ctx` se nemění, mění se hodnota jednoho slotu.

- Poskytovatel ani volající neexistují: pinovaný `@onlineapps/conn-infra-mq` `publishChunk`
  nemá a žádný handler platformy `ctx.stream` nevolá (d.776, d.802). Tier-1 běžec
  (`conn-orch-validator` `CookbookTestRunner`) dával `stream: null` už předtím — wrapper se
  s ním srovnává.
- `ContextBuilder` už nečte `this._connectors.mq` do `ctx`; konektor `mq` dál dostává.
- JSDoc `ContextBuilder` a `OperationContext` a vygenerované `API.md` popisují slot `null`.

### Added — identita běžící kopie `instanceId` vzniká jednou za proces a jde registru i monitoringu (d.817, `e855a358`)

Konstruktor `ServiceWrapper` nastaví `this.instanceId = this._makeInstanceId()` — jediné místo,
kde identita kopie vzniká, před oběma konzumenty:

- `_makeInstanceId()`: neprázdná `SERVICE_INSTANCE_ID` z prostředí (táž proměnná, kterou čte
  `monitoring-core`) má přednost a vrací se doslova; samé mezery hodnotou nejsou; jinak
  `crypto.randomUUID()` (UUID v4, řetězec).
- FÁZE 0.7: `new ServiceRegistryClient({ …, instanceId: this.instanceId })`.
- FÁZE 0.3: `MonitoringConnector.init({ …, serviceInstanceId: this.instanceId })` —
  `monitoring-core` hodnotu razítkuje na telemetrii jako `service.instance.id`.
- Wrapper klíč `instanceId` klientovi registru PŘEDÁVÁ; co s ním klient udělá, určuje pinovaná
  verze `@onlineapps/conn-orch-registry`.
- Dvojník `MockServiceRegistryClient` (`tests/mocks/connectors.js`) odmítá chybějící `instanceId`
  jako strom klienta registru. Sada `tests/unit/ServiceWrapper.instanceIdentity.test.js`.

### Removed — BREAKING: veřejná `ServiceWrapper#loadValidationProof()` pryč (d.699, `aaaf22ae`)

Metoda bez volajícího četla `<cwd>/.validation-proof.json` — soubor, který nikdo nezapisuje
(validator píše `conn-runtime/validation-proof.json`); chybějící soubor vracela jako `null`
s varováním v logu. Ve vydané 10.2.1 je v `src/ServiceWrapper.js`, dnes v `src/**` 0 výskytů.

- Migrace: volání odstranit. Proof vzniká ve FÁZI 0.2 a wrapper ho drží v `this.validationProof`
  (viz d.746 níže). Stráž `tests/unit/proofFileLocationOneRail.test.js`.

### Removed — BREAKING: `ConfigLoader.loadValidationProof()`, klíč `validationProof` ve výsledku `loadAll()` a volba konstruktoru `options.validationProof` pryč (d.746, `2f06c94c`)

`ConfigLoader.loadValidationProof()` četla před bootem proof PŘEDCHOZÍHO běhu, a to v tvaru
`{ hash, data }` místo kanonického `{ validationProof, validationData }`; FÁZE 0.2 hodnotu vždy
přepsala dřív, než ji kdo přečetl. Pryč je statická metoda, klíč `validationProof` ve výsledku
`ConfigLoader.loadAll()`, jeho předání v `bootstrap()` i čtení `options.validationProof`
v konstruktoru. `this.validationProof` začíná na `null` a zapisuje ho jen doběhlá validace.

- Chování se mění jen při `validation.enabled: false` se zapnutým registrem: dřív hodnota
  z konstruktoru došla ke klientovi registru (a nad tvarem `{ hash, data }` padla na `TypeError`),
  teď je `null` a klient jde větví bez proofu. Tuto konfiguraci nedeklaruje žádná služba ani
  šablona (měřeno v commitu).
- Migrace: `ConfigLoader.loadValidationProof()` ani `loadAll().validationProof` nečíst,
  `options.validationProof` nepředávat — konstruktor ji nečte.

### Changed — BREAKING: operace bez `bundle_scope` se odmítá jménem místo tichého `'workspace'` (d.701, `af1e11a0`)

`ContextBuilder.build()` četl `operationSpec.bundle_scope || 'workspace'`. Teď chybějící,
`undefined`, `null` nebo prázdný `bundle_scope` = netypovaná chyba (500, handler neběží)
`[ContextBuilder] Operation "<operace>" declares no bundle_scope - Expected one of <VALID_BUNDLE_SCOPES>;
Fix: declare bundle_scope in config/service/operations.json`. Množinu hodnot bere hláška z
`VALID_BUNDLE_SCOPES` v `@onlineapps/service-validator-core`. Klíč je povinný
(`api/docs/biz/30-operations/schema-v3.md` § bundle_scope); jinou deklarovanou hodnotu dál
posuzuje `_validateTenancy` beze změny.

- Migrace: každá operace v `config/service/operations.json` deklaruje `bundle_scope`. Sady
  `tests/unit/d701-bundleScopeRequired.test.js`, `tests/integration/d701-bundleScopeRequired.integration.test.js`.

### Changed — BREAKING: `ConfigLoader.getConfigRegistry()` vrací záznamy bez mrtvých polí a zmrazené hluboko (d.772, `672d5854`; d.772b, `fc27aef5`)

- d.772: pole `required` zmizelo ze všech tří záznamů, `description` z `SERVICE_CONFIG`
  a `OPERATIONS` (záznamy nesou jen `paths`); `VALIDATION_PROOF` si `description` drží s textem
  z d.758. Ve vydané 10.2.1 nesou všechny tři záznamy `required` i `description`. Čtenáře těch
  polí v `api`, `api_biz` ani `fe_adminui` měření v commitu nenašlo.
- d.772b: registr, každý záznam i každé pole `paths` jsou `Object.freeze`; `getConfigRegistry()`
  dál vrací TENTÝŽ objekt, ne kopii. Zápis do něj ve strict módu hodí `TypeError`, jinde se tiše
  neprovede — dřív se propsal dalšímu volajícímu i do `loadServiceConfig()` a `loadOperations()`.
- Migrace: `required`/`description` z registru nečíst a registr nemutovat — cesty konfigurace
  určuje balíček. Sada `tests/unit/configRegistryCarriesOnlyPaths.test.js`.

### Changed — BREAKING: `${VAR}` v konfiguraci konstruktoru rozvíjí jediná implementace — `ConfigLoader` (d.678, `dd837e95`)

`_processConfig` měl vlastní resolver placeholderů: `${VAR:default}` tiše dosadil a pole vnořené
v poli přepsal na objekt (`[["a","b"]]` → `[{"0":"a","1":"b"}]`). Teď volá
`ConfigLoader.resolveEnvPlaceholdersDeep`: `${VAR:default}` hodí `[ConfigLoader] Unsupported
placeholder default - … at $.…` a vnořené pole zůstane polem. Týká se volajícího, který
`new ServiceWrapper({ config })` staví sám; konfigurace z `ConfigLoader.loadAll()` (cesta
`bootstrap()`) placeholdery nenese a projde beze změny.

- Migrace: default do placeholderu nepsat; hodnotu nastavit v `env-active/*.env`, nebo explicitně
  v `config/service/config.json`. Sada `tests/unit/ServiceWrapper.configPlaceholder.test.js`.

### Changed — BREAKING: `new SchemaValidator({ ajv })` odliší nepředaný Ajv od špatně předaného (d.741c, `17c60ee4`)

Dřív `ajv || _buildDefaultAjv()`: `{ ajv: null }` tiše dostal vnitřní Ajv a `{ ajv: {} }` padl až
v `precompile()` na `this._ajv.compile is not a function`. Teď vnitřní Ajv znamená jen vynechaná
volba (`undefined`); cokoli bez `compile()` konstruktor odmítne `[SchemaValidator] Invalid ajv
instance - …` a jmenuje, co dostal (`null`, `object without compile()`, typ). `ServiceWrapper`
volbu nepředává, boot služby se nemění.

- Migrace: `ajv` buď vynechat, nebo předat instanci Ajv. Sada `tests/unit/d741c-ajvInjectionContract.test.js`.

### Changed — BREAKING: `wrapper.heartbeat.dependencyRefreshMs` deklaruje `config/runtime-defaults.json`; neplatná hodnota zastaví FÁZI 0.10 (d.729, `432c7066`)

Dřív `this.config.wrapper?.heartbeat?.dependencyRefreshMs || 10000` — hodnota ke konfiguraci
žila jako literál ve zdroji a explicitní `0` se četla jako „nenastaveno“. Teď 10000 ms deklaruje
`config/runtime-defaults.json`, který `ConfigLoader.loadAll()` merguje pod konfiguraci služby, a čte
se povinnou čtečkou jako `wrapper.mq.*`: chybějící klíč i hodnota, která není kladné celé číslo
milisekund (včetně `0`), zastaví fázi hláškou se jménem klíče (`[ServiceWrapper] Invalid
configuration - wrapper.heartbeat.dependencyRefreshMs must be a positive …`). Výchozí kadence
se nemění; klíč nedeklaruje šablona ani žádná z biz služeb (měřeno v commitu).

- Migrace: konfigurace sestavená mimo `ConfigLoader.loadAll()` klíč nese sama; jinak nic.
  Sady `tests/unit/ServiceWrapper.heartbeatConfig.test.js`,
  `tests/integration/d729-dependencyRefreshDeclaredDefault.integration.test.js`.

### Changed — BREAKING: FÁZE 0.15 (kontrakt integrace a DB handle) už nestojí pod `wrapper.health.enabled`; každé `SKIPPED` jmenuje důvod (d.815c, `f6451fff`)

Dřív celou FÁZI 0.15 podmiňovalo `wrapper.health.enabled !== false`, takže vypnuté zdraví
vypnulo i `_assertDatabaseHandleMatchesContract()` — služba s `requiredConnectors.db: true` bez
handle nabootovala zeleně. Teď fáze běží, kdykoli má wrapper strom služby (`serviceRoot`);
`SKIPPED` jen bez něj, s důvodem `no serviceRoot — …`. Takové službě kniha fází ukáže
`0.15:PASSED`, nebo boot zastaví podle tabulky README § The database handle, místo
`0.15:SKIPPED`. Podmínka 0.15 byla jediné čtení `wrapper.health.enabled` v `src/**` (10.2.1);
od této změny ho tam nečte nic. Žádná biz služba `wrapper.health` nedeklaruje (měřeno v commitu).

- Všech sedm `SKIPPED` v knize fází nese důvod (`_logPhase(…, reason)`); stráž
  `tests/unit/ServiceWrapper.skippedPhasesNameTheirReason.test.js` měří zdroj.
- Migrace: DB handle musí souhlasit s `integration-contract.json` i ve službě s vypnutým zdravím.

### Changed — revalidace má dva stupně; vnitřní stav `slow_backoff` pryč (d.755b, `f6db9c6b`)

Stav `REVALIDATION_STATE.SLOW_BACKOFF` neměl zapisovatele — přechod do něj nikdy neexistoval
a šestihodinová větev byla nedosažitelná. Stav zmizel; stav cyklu mimo oba stupně se místo tichého
šestihodinového časovače odmítne jménem (`[ServiceWrapper] Unexpected re-validation state - …`).
Dosažitelné cesty beze změny: rychlý žebřík → hodinový → po stropu odhlášení a fatální konec.
`REVALIDATION_SLOW_MS` zůstává jako okno restartního cooldownu. `REVALIDATION_STATE` balíček
neexportuje (`module.exports = ServiceWrapper`); mění se jen soukromé `_revalidationState`.
Sada `tests/unit/d755b-revalidationTwoStages.test.js`.

### Changed — vnitřní srovnání bez změny chování (d.713 `33ff5a64`, d.721 `ba830409`, d.733 `97040730`)

- d.713, d.721: startovní smyčka validace i bootovací retry MQ čekají přes seam `_sleep()` místo
  inline `setTimeout`; počet i hodnoty prodlev beze změny.
- d.733: čtyři čtečky povinných `wrapper.*` klíčů sloučeny do `_requiredWrapperSetting(...path)`;
  hlášky operátorovi znak po znaku beze změny, nová hláška `[ServiceWrapper] Invalid config path - …`
  jen pro vadné volání uvnitř balíčku. Sada `tests/unit/d733-oneRequiredSettingReader.test.js`.

### Added — `ConfigLoader.PROOF_RELATIVE_PATH` (d.715, `a00c56d6`)

Cesta k proofu odvozená z `CONFIG_REGISTRY.VALIDATION_PROOF.paths[0]` (týž řetězec, ne druhý
literál); `_deleteProofAndRevalidate()` ji bere odtud místo vlastní kopie.

### Added — přechod zdraví závislosti se loguje s důvodem, jednou za přechod (d.705, `f1b26386`)

`refreshDependencyHealth()` bralo z verdiktu sondy jen `status`. Nově
`_recordDependencyHealth(component, status, reason)` uloží signál pro heartbeat (beze změny)
a na PŘECHOD zaloguje — `warn` při pádu na `unhealthy` s důvodem, `info` při návratu na
`healthy`; setrvalý stav a přechody z/na `unknown` mlčí. Komponenty a jejich pole jmenuje jedno
místo, `DEPENDENCY_HEALTH_FIELD`. Sady `d705-dependencyHealthTransitions` (unit + IT).

### Fixed — zdraví Redis vyžaduje každou spuštěnou sondu, ne aspoň jednu (d.700, `6ef7b170`)

`_checkRedisDependency()` vracelo `healthy`, jakmile prošla jedna ze sond cache/state. Teď jen když
žádná spuštěná sonda neselhala; konektor, který služba neprovozuje, sondou není. `reason` jmenuje
sondu a příčinu (`[Context] Problem - Expected/Fix`). Pro volajícího: služba s mrtvým STATE Redisem
a živou cache hlásí v heartbeatu `redis: 'unhealthy'` (dřív `healthy`). Sady
`d700-redisDependencyEveryProbe` (unit + IT).

### Fixed — sonda cache vrací i důvod (d.714, `0f183cfb`)

`_probeCacheHealth()` vrací `{ status, reason }` jako sondy Redis a DB; důvod `unhealthy` je hláška
konektoru nebo citace odpovědi (`healthCheck returned false`). Stavy `unknown`/`healthy`/`unhealthy`
beze změny. Sada `tests/unit/d714-cacheProbeReportsItsReason.test.js`.

### Fixed — pád bootu má v knize fází jednu řádku — fázi, kde nastal (d.751, `d6b479bd`; d.751b, `c6e32dbe`)

- d.751: fáze, která si FAILED zapíše sama a výjimku pustí dál (0.1b), dostala ve vnějším catchi
  druhou řádku `INIT FAILED`, která přepsala `bootFailure` na `INIT`; souhrn i `bootstrap()` pak
  jmenovaly fázi, kde nic neselhalo. Teď `_handleInitializationError()` druhou řádku nepíše a fázi
  bere z knihy; `INIT FAILED` zůstává pro pád mimo fáze. Tvar `bootFailure` `{ phase, phaseName }`
  beze změny.
- d.751b: pád FÁZE 0.12 boot ukončí (`return` jako u ostatních fází) a větev úklidu se řídí fází
  z knihy, ne argumentem volání.
- Sada `tests/unit/ServiceWrapper.bootFailureAttribution.test.js`; integrační
  `d741-duplicateSchemaId` tvrdí knihu `['0.1b']`.

### Fixed — FÁZE 0.10 zavírá svůj řádek: `SKIPPED` s důvodem (d.649, `ed16bafa`)

Bez spuštěného publisheru tepu chyběla 0.10 v knize fází úplně. Teď `0.10:SKIPPED` s důvodem
`heartbeat disabled`, `no MQ client`, nebo oběma. Kniha fází o řádek vyrostla — sada, která ji
tvrdí celou, řádek uvidí. `wrapper.heartbeat.enabled` beze změny. Sada
`tests/unit/ServiceWrapper.heartbeatSkipReason.test.js`.

### Fixed — poslední pokus startovního žebříku hlásí konec, ne další čekání (d.709, `a79f4281`)

Šestý neúspěšný pokus sliboval `next retry in 1800000ms`, ač další pokus nenastane. Teď
`… - no attempt left: the startup retry budget is spent and the boot ends here. Fix: …`, bez
`delayMs`; hlášky pokusů 1–5 doslova beze změny. Sada `tests/unit/d709-startupLadderLastAttempt.test.js`.

### Fixed — duplicitní kořenový `$id` dvou operací jmenuje obě operace (d.741, `d81285bc`)

`SchemaValidator` si kořenový `$id` nárokuje před `compile()`: `[SchemaValidator] Duplicate schema
$id "<id>" - <první> and <druhá> …` místo věty Ajv `schema with key or id "…" already exists`,
která jmenovala jen druhou operaci. Boot dál končí ve FÁZI 0.1b; vnořené `$id` v `$defs` beze změny.
Sady `d741-duplicateSchemaId` (unit + IT).

### Fixed — citace dokumentů v hláškách nesou předponu `api/` (d.741b, `a450e19e`)

Pět odmítacích hlášek (`ServiceWrapper` `workflow_id`, 2× `current_step`; `ContextBuilder`
`workflow_id`, `correlation_id`) cituje `api/docs/biz/70-contracts/workflow-message.md` místo
`docs/…`; jinak text beze změny. Sada `tests/unit/d741b-docCitationPrefix.test.js`.

### Fixed — hlášky bez ručně psaných verzí a s `Fix:` (d.657, d.658, `266f8433`; d.658b, `e4957a16`)

- d.657: chybějící proměnná placeholderu (`ConfigLoader.resolveEnvPlaceholdersInString`) končí
  `Fix: set <VAR> in env-active/*.env, or replace the placeholder with an explicit value in
  config/service/config.json.`; dosavadní text stojí před ním doslova.
- d.658: hláška o chybějícím delivery kontextu jmenuje schopnost, kterou wrapper hledá (consume()
  předá delivery kontext handleru jako druhý argument), místo nevydaného `mq-client-core >= 8`.
- d.658b: hláška o nekompatibilním `@onlineapps/conn-orch-orchestrator` posílá `Fix:` na
  `api/config/libraries.json` místo ručně psaného pinu; práh nese jediná konstanta
  `REQUIRED_CONN_ORCH_ORCHESTRATOR_MAJOR` (neexportovaná). Kdy se hází a třída chyby beze změny.

### Fixed — odmítnutí `options.scheduler` učí tvar hooku `{ recurrences, failures }` (d.947, `c358887b`)

Expected/Fix v `_validateOptions` radily hook vracející pole, které od d.943 (výše) wrapper
odmítá. Teď jmenují `{ recurrences: [...], failures: [...] }`. Sada `tests/unit/bootstrap.scheduler.test.js`.

### Documentation — komentáře a JSDoc bez změny chování (d.658 `a651fcfa`, d.688 `52f7be0e`, d.688b `c20b34e1`, d.746c `0bd3be9b`, d.746d `a243ef95`, d.758 `a491673d`, d.981 `46e8cc90`)

Komentáře ukazují na uzly místo převyprávění, bez ručně psaných verzí; čísla řádků v komentářích
i testech nahradily symboly (d.981). JSDoc konstruktoru jmenuje povinný `serviceBaseDir` a nejmenuje `app`/`server`
— neznámé klíče konstruktor dál neodmítá, chování beze změny. Hlavička `ConfigLoader` i `description`
záznamu `VALIDATION_PROOF` v registru říkají, že proof je výstup validace, který loader nikdy nečte
(ten řetězec vrací i `getConfigRegistry()`).

### Removed — deklarace `wrapper.health` z `config/runtime-defaults.json` (d.998)

`wrapper.health: { enabled: true, endpoint: "/health" }` neměla v `src/**` čtenáře: `endpoint` od
zrušení HTTP povrchu (ADR 0005), `enabled` od d.815c (výše), jejíž podmínka FÁZE 0.15 byla jeho
posledním čtením. Deklaraci nečte ani žádná biz služba (`config/` a `src/` osmi repozitářů na
`origin/devel`: 0 zásahů), šablona ani `conn-orch-validator`. Konfigurace, která klíč nese, se
neodmítá — jako každý neznámý klíč se nečte; chování boot ani zdraví se nemění.

### Fixed — teardown nečte kanál a tag konzumenta, které klient registru nemá; krok `consumers` nehlásí uvolnění, které neproběhlo (d.998)

`_cancelConsumers()` nesl řádek `registryClient.queueManager.channel` / `.consumerTag`. Pinovaný
`@onlineapps/conn-orch-registry` ani jedno nevystavuje (surový kanál zmizel v d.198b-3, tag nikdy
nebyl), takže řádek se při každém shutdownu přeskočil — a krok `consumers`, spouštěný i jen kvůli
klientovi registru, se přesto zapsal mezi `releasedSteps`. Řádek zanikl a krok `consumers` stojí jen
na MQ klientovi wrapperu; bez něj je `not started`. Konzument registru končí s MQ spojením klienta
v kroku `registry-queue-manager` — to měří nová integrační sada proti brokeru (před shutdownem 1
konzument na `<služba>.registry`, po něm 0). Dvojníky tří unit sad teardownu nesly vyřazený tvar
klienta a srovnaly se na skutečný. Sady `tests/unit/d998-registryConsumerOneRail.test.js`,
`tests/integration/d998-registryConsumerClosedOnShutdown.integration.test.js`.

### Fixed — `shutdown()` ruší vlastní konzumenty přes `cancelConsumer()` dřív, než zavře MQ spojení (d.998b)

`_cancelConsumers()` četl `channel` z MQ klienta a volal `channel.cancel(tag)`. Klient, kterého
wrapper staví (`@onlineapps/conn-infra-mq` `ConnectorMQClient`, potomek `@onlineapps/mq-client-core`
`BaseClient`), vlastnost `channel` nemá — změřeno na připojené instanci proti brokeru — takže se
nerušilo nic a konzumenti `workflow.init`, `workflow.control` a `<služba>.workflow` zmizeli až se
spojením, ne před ním, jak pořadí popisuje README § Shutting down. Teď se ruší
`mqClient.cancelConsumer(<tag>)` tagem, který vrátilo `consume()`; odmítnutí klienta se zaloguje
jménem konzumenta a teardown pokračuje. Pro volajícího: v logu přibude `[ServiceWrapper] Cancelled
<fronta> consumer` a v okamžiku kroku `mq-disconnect` už broker konzumenta nepočítá. Rozpracované
doručení se nečeká — „dokončit rozpracované" koncept nezapisuje. README § Shutting down srovnáno
(konzument registru na seznamu rušených nebyl pravda od d.998). Sady
`tests/unit/d998b-consumerCancelWired.test.js`,
`tests/integration/d998b-consumerCancelledBeforeDisconnect.integration.test.js`; dvojníky MQ klienta
ve třech unit sadách teardownu srovnány na skutečný tvar (`cancelConsumer`, bez `channel`).

### Tests — dvojník MQ klienta nese tvar `consume()` a `cancelConsumer()` skutečného klienta (d.998c)

`MockMQConnector.consume(handler, options)` vracel `undefined` a frontu bral z voleb; skutečný klient
(`@onlineapps/conn-infra-mq`, `consume` zděděné z `@onlineapps/mq-client-core` `BaseClient`) bere
`(queue, handler, options)` a vrací tag konzumenta. Dvojník teď: vrací tag `amq.ctag-…` (tvar
změřený na pinované 4.2.0 proti brokeru), nepřipojený odmítne `consume()` i `cancelConsumer()`
`ConnectionError`, volby politiky doručení (`requeueOnError`, jeho rozpor s `maxAttempts`,
`maxAttempts`, `classify`) odmítne `ValidationError` jako transport, a `cancelConsumer()` bere tag
nebo frontu, neznámý odmítne `ValidationError` a zrušeného konzumenta zahodí. Třídy chyb bere
z pinovaného `@onlineapps/mq-client-core`. Stráž `connectorDoublesCarryTheInterface` nově kryje
i tvar: parametry `consume`, návrat tagu, obě odmítnutí a kontrolní případ platných voleb.

### Tests — rozhraní pinovaných konektorů měří stráž, ne literály; `simulateMessage` pryč (d.998d)

Blok `Connector Requirements` v `ServiceWrapper.component.test.js` tvrdil jen literály, které
o řádek výš napsal sám (`expect(API.methods).toContain('consume')`), a dva z nich byly nepravdivé
(`consume (handler, options) → Promise<void>`, `unregister (serviceName)`). Zanikl. Jeho otázku
teď klade stráž `connectorDoublesCarryTheInterface` pinovaným balíčkům v `node_modules`: každá
nechráněná metoda, kterou wrapper volá na `mqClient`, `registryClient`, `orchestrator`
a `stateConnector` (seznam ze zdroje wrapperu), na skutečném prototypu je; klient registru
`init`/`sendHeartbeat`/`unregister`/`deregister` bez parametrů, `register(serviceInfo)`, a wrapper
volá `unregister()` bez argumentu; `create(config)` a `processWorkflowMessage(message, serviceName)`
orchestrátoru s oběma argumenty z wrapperu; `publish(queue, message, options)` MQ klienta; a co
pinovaný orchestrátor čte z podaného `conn-orch-cookbook` (`createRouter`, `templateHelpers`,
`validateCookbook`, odvozeno z jeho zdroje), pinovaný cookbook nese. Pomocná metoda dvojníka
`MockMQConnector.simulateMessage()` (0 volajících) zanikla.

### Fixed — heartbeat registru volá `sendHeartbeat()` bez argumentu (d.998e)

Smyčka heartbeatu podávala `registryClient.sendHeartbeat(serviceName)`, ale pinovaný
`@onlineapps/conn-orch-registry` (6.0.3) `sendHeartbeat()` žádný parametr nemá — jméno služby zná
z konstruktoru. Argument bez čtenáře zanikl; stráž `connectorDoublesCarryTheInterface` tvrdí volání
bez argumentu jako u `unregister()`. Dvojník registru bere `sendHeartbeat()` také bez argumentu;
jeho záznam `heartbeats` (jediný zapisovatel byla tato metoda, 0 čtenářů) zanikl. Chování beze změny.

### Tests — zbylé dekorativní bloky `ServiceWrapper.component.test.js` pryč (d.998e)

„Workflow Processing Flow", „Error Handling Flow" a „Integration Test Helpers" tvrdily jen literály,
které samy napsaly (a popisovaly zrušené koncepty — ApiMapper, OpenAPI spec). „Missing Connector
Detection" ověřoval `toBeDefined()` balíčky, které `src/ServiceWrapper.js` načítá na prvních řádcích
(každá sada, která wrapper načte, by bez nich padla), a exporty cookbooku ze zdrojového stromu
knihovny místo pinovaného balíčku — tvar pinovaného cookbooku kryje stráž (d.998d), zrušené exporty
vlastní sady `conn-orch-cookbook`.

### Tests — `ServiceWrapper.component.test.js` zanikl (d.998f)

Poslední blok souboru („Basic Integration") tvrdil jediné: wrapper s vypnutým MQ a registrem
`initialize()` dokončí (`isInitialized === true`). Totéž tvrdí `ServiceWrapper.test.js` („should
initialize successfully with MQ/Registry disabled"). Zbytek bylo lešení bez čtenáře: Express server
na portu 3456, který wrapper nepotřebuje (biz kontejner HTTP nemá, ADR 0005) a který dostával jako
`app`/`server` — volby, které konstruktor nečte (d.746c); v configu `service.port` (wrapper ho nečte,
conf `biz-service-port-url` 001) a `wrapper.health` (0 čtenářů od d.998). Po úklidu v souboru
nezbylo žádné vlastní tvrzení.

### Changed — BREAKING: `options.operations` má jediný tvar — dokument `operations.json`; holá mapa operací se odmítne jménem (d.1019)

Wrapper bral `operations` ve dvou tvarech: dokument `{ operations: { <jméno>: {…} } }`, jak ho vrací
`ConfigLoader.loadOperations()` (jediný tvar, který staví produkce — `loadOperations()` dokument bez
objektu `operations` odmítne), a holou mapu operací, kterou pět čtenářů v `src/ServiceWrapper.js`
přijímalo přes `x.operations || x` (dispatch, zarovnání handlerů, kontrola jmen, lookup orchestrátoru,
vyvolání). Registrační payload četl jen dokument (`this.operations?.operations || {}`), takže wrapper
s holou mapou operace dispatchoval, ale do registru poslal `operations: {}` — tiše, ač
`registration-wire.md` §2.1 celou sekci vyžaduje. Konstruktor teď cokoli jiného než dokument odmítne:
`[ServiceWrapper] options.operations must be the operations.json document - got <co>. Expected: { operations:
{ <operation-name>: { … } } }, as ConfigLoader.loadOperations() returns it … Fix: pass the parsed operations.json,
not its inner operations map.`; všichni čtenáři berou `this.operations.operations` bez fallbacku a dvě
nedosažitelné kontroly tvaru zanikly. Chybějící `operations` má dál hlášku `Operations schema is required`.

- Migrace: `new ServiceWrapper({ operations })` dostane parsovaný `operations.json`; `bootstrap()` beze změny.
- Změřeno: holou mapu nepředával nikdo — produkce ani sady wrapperu (všech 5 přímých přiřazení
  `wrapper.operations` v sadách je obalených).
- Sady: nová `tests/unit/d1019-oneOperationsShape.test.js` (4 odmítnutí, dva kontrolní případy, statická sonda).

### Changed — `metadata.description` v registraci je volitelné: nepřítomné, nikdy `''` (d.1019)

`_buildRegistrationPayload()` posílal `description: this.config.service?.description || ''`. Kontrakt
(`registration-wire.md` §2.1, `metadata` = `{ version, description }`) ani řádek uniformy `C-SERVICE`
`service.description` nevyžadují; volitelná hodnota, která chybí, teď v `metadata` chybí (klíč nepřítomný).
Deklarovaný popis jde beze změny. Všech 8 biz služeb popis deklaruje (měřeno na `origin/devel`), registr
`metadata` ukládá, jak přijde. Sady: `d1019-oneOperationsShape` (unit), `d1019-registrationMetadata`
(IT proti brokeru, JSON na drátě), kontrolní případ `registrationPayloadKeys` srovnán.

### Tests — dvojník MQ klienta volá vlastníka pravidel voleb `deliveryPolicy.validateConsumeOptions` místo vlastní kopie (d.1019)

`MockMQConnector.consume()` (d.998c) opisoval čtyři pravidla voleb `consume()`. Od d.1005 má
`@onlineapps/mq-client-core` jejich jediného exportovaného vlastníka, kterého volá i transport; dvojník ho
volá také, takže odmítá totéž a TOUTÉŽ větou (`[RabbitMQClient] Invalid requeueOnError …` atd.). Stráž
`connectorDoublesCarryTheInterface` porovnává hlášku dvojníka s hláškou vlastníka a hlídá, že kopie pravidel
v dvojníkovi nevznikne znovu.

### Changed — tarball už nenese `jest.config.js`, `jest.integration.config.js`, `docs/CONFIGURATION_GUIDE.md`, `build-docs.js` ani `jsdoc.json` (d.1020)

- `.npmignore` je vylučuje. Konfigurace jestu spouštějí jen sady balíčku (`tests/` se nebalí);
  `docs/CONFIGURATION_GUIDE.md` se čte v repozitáři a v nainstalovaném balíčku ho nic nečte;
  `jsdoc.json` čte jen skript `docs:html` a `build-docs.js` nevolá nic.
  `config/runtime-defaults.json` zůstává — čte ho `ConfigLoader`. `npm pack --dry-run` ubral právě tyto
  pět souborů.

## [10.2.1] — 2026-09-26
### Fixed — `bootstrap()` předá wrapperu hook `scheduler.recurrences` (d.935)

`bootstrap()` staví `ServiceWrapper` výslovným výčtem klíčů a klíč `scheduler` na něm nebyl:
`bootstrap(__dirname, { db, scheduler: { recurrences } })` hook zahodil, FÁZE 0.7b skončila
`SKIPPED` bez chyby a služba (ingest, tenant 101) nezaregistrovala žádný rozvrh. `bootstrap()` teď
`options.scheduler` předá beze změny, stejně jako `db`. Konstruktor tvar validuje fail-fast: cokoli
jiného než `{ recurrences: function }` (včetně `null`) zastaví boot hláškou
`[ServiceWrapper] options.scheduler must be { recurrences: function } - got … Fix: …`, místo aby
se tiše četlo jako „hook chybí“. Bez volby beze změny (FÁZE 0.7b `SKIPPED` s důvodem).
Nová sada `tests/unit/bootstrap.scheduler.test.js`.

## [10.2.0] — 2026-09-25

pin: `@onlineapps/conn-infra-error-handler` 3.0.0 → 3.0.1
pin: `@onlineapps/conn-infra-mq` 4.1.1 → 4.1.2
pin: `@onlineapps/conn-orch-orchestrator` 5.2.0 → 5.3.0
pin: `@onlineapps/conn-orch-registry` 6.0.2 → 6.0.3
pin: `@onlineapps/conn-orch-validator` 12.1.0 → 12.1.1
pin: `@onlineapps/infrastructure-tools` 3.1.1 → 3.1.2
pin: `@onlineapps/conn-base-monitoring` 2.2.0 → 2.2.1
pin: `@onlineapps/mq-client-core` 4.2.0 (nová přímá závislost — G6: `src/ContextBuilder.js` a `src/ServiceWrapper.js` ji requirují, dřív nedeklarovaná)

### Fixed — popis `ctx` jmenuje 17 polí včetně `scheduler` (d.900)

JSDoc a hláška konstruktoru `OperationContext`, tabulka polí v `README.md`, vygenerované `API.md`
a název unit testu `ContextBuilder` dosud uváděly 16 polí bez `ctx.scheduler`, který přidal
d.834. Jen text — chování beze změny.

### Fixed — integrační sady `acting-person` a `phase-book` tvrdí kontrakt z d.834 (d.900)

`3470e97c` (d.834) přidal do `ctx` klíč `scheduler` a do knihy fází FÁZI `0.7b Declared
Recurrences`, ale dvě integrační sady, které drží přesný výčet, neaktualizoval — obě byly od té
doby červené. `acting-person` nyní čeká 17 polí včetně `scheduler`, kontrolní případ `phase-book`
řádek `0.7b:SKIPPED` mezi `0.7` a `0.16` (fixtura nedeklaruje hook `scheduler.recurrences`).
Týž drift nesla sada `state-in-context` (výčet klíčů ctx a `ctx_field_count` 16 → 17).
Produkční kód beze změny.

### Fixed — hláška továrny plánovače jmenuje továrnu jejím jmenným prostorem; obě hlášky mají test (d.881)

Hláška pro jádro bez portu plánovače říkala `exports no createSchedulerPublisher()`, továrna ale
v `@onlineapps/mq-client-core` sedí pod jmenným prostorem (`src/index.js`:
`module.exports.scheduler = { …, createSchedulerPublisher }`; na kořeni modulu není). Nově
`exports no scheduler.createSchedulerPublisher()` — operátor hledá to, co hláška jmenuje.

Obě hlášky `requireSchedulerPublisherFactory` („modul se nenačte" i „export chybí") dosud neměly
žádný test. Nová sada `tests/unit/ContextBuilder.schedulerFactoryMessages.test.js` tvrdí každou
CELÝM textem (rovnost `message`, ne podřetězec) a kontrolním případem drží, že s továrnou pod
`scheduler` se port postaví. Větve se spouštějí přes `build()` s jádrem nahrazeným v izolovaném
registru modulů — produkční kód žádný loader navíc kvůli testu nedostal.

### Fixed — deklarovaná třída chyby je neprázdný řetězec, jinak `BusinessError` odmítne (d.876)

Typedef `ErrorEnvelopeBody.type` slibuje „Absent, never `null`", ale konstruktor to nedržel:
`new BusinessError({ …, type: null })` prošlo a obálka nesla `type: null` — třídu, o které tvrdila,
že ji vyhazující zvážil. Nově je PŘÍTOMNÝ klíč `type` deklarace a musí to být neprázdný řetězec;
`null`, `''`, číslo i výslovně předané `undefined` konstruktor odmítne hláškou, která jmenuje
odmítnutou hodnotu a opravu (`architecture-principles.md` §3 No Fallbacks, §4 Fail-Fast).
Vynechaný klíč dál znamená „nedeklaruji" a obálka pak `type` nenese.

Platformní cesty wrapperu `type` nepředávají nikde (šest podtříd posílá jen
`code/status/message/details`), takže je změna nerozbije; zasáhne jen službu, která třídu
předává prázdnou — a ta se opraví u sebe, ne shovívavostí tady.

### Documentation — továrna plánovače popisuje mechanismus, ne číslo verze (d.876)

Docblock `requireSchedulerPublisherFactory` (`src/ContextBuilder.js`) i hláška pro jádro, které se
nenačte, nesly ručně psané `4.0.1` a tvrdily, že verze bez sekce `scheduler` odmítne už při
`require`. Změřeno to neplatí: `require` projde u 4.0.1 i u vydané 4.1.0 a obě skončí stejně —
jmenný prostor `scheduler` s továrnou chybí (druhá větev). `require` selže jen u jádra, které nejde
načíst vůbec. Obě věty teď popisují tyto dvě větve bez čísla verze a hláška radí jádro deklarovat,
pinnout na verzi s portem plánovače a přeinstalovat. `API.md` dogenerován (`npm run docs`).

### Fixed — komentář u `specificationEndpoint` ukazuje na kontrakt, ne na čísla řádků registru (d.873)

Komentář v `_initializeRegistry()` zdůvodňoval povinnost pole tím, co s ním dělá
registr — šesti citacemi čísel řádků do `infra/api_services_registry`. Pět z nich
nesedělo (řádky se posunuly, jedno vedlo na prázdný řádek docblocku), jedno tvrzení
o chování neplatilo (registr prázdné pole neodmítá varováním, ale chybou) a jedna
citace mířila na `heartbeat.listener.js`, který už neexistuje. Komentář teď říká jen
důvod, proč tu není fallback, a ukazuje na kontrakt registrace
(`api/docs/biz/30-operations/registration-wire.md` §2.1), který pole vlastní. Druhý
odkaz do registru (pořadí startu × měření připravenosti) nese místo opsané věty jméno
funkce `_checkService()`. Jen komentáře a hlavičky dvou sad; chování beze změny.

### Changed — text a třída chyby v log datech jako `exception.message` / `exception.type` (d.868b, minor → 10.2.0)

`errorLogFields(err)` vrací `{ err, 'exception.message', 'exception.type' }`
(atributy OpenTelemetry semantic conventions); klíč `message` z log dat zmizel.
Týká se každého řádku `describeFailure` (tedy `ErrorMapper.map()`) a dvou řádků,
které `ServiceWrapper` píše o chybě sám (`Error processing message from …`,
`context release failed`).

Důvod (změřeno d.868 na DEV Loki): obálka `telemetry.logs.*` nese vlastní text
záznamu v `body` a konzument monitoringu ji v `toLokiEntry()` slučuje s `body`
naposledy — `message` z dat tedy text záznamu vždy přepsal a text chyby se do Loki
nedostal u žádného chybového řádku, brandovaného ani nebrandovaného (na stdoutu
zůstával). Pravidlo pro producenty: monitoring-core README § Wire shape — the
telemetry log envelope.

`exception.type` je jméno konstruktoru, ne `err.name`: `BusinessError` si `name`
nastavuje sám, takže podtřída služby bez vlastního `name` by se jménem hlásila
jako `BusinessError`.

Kdo četl text chyby jako `message` v log datech wrapperu (stdout `meta.data.message`,
test konzumenta nad zachyceným loggerem), čte nově `exception.message`. Obálka pro
volajícího se nemění.

### Fixed — přeskočená FÁZE 0.2 jmenuje DŮVOD, místo aby mlčela (d.649)

Řádek `[FÁZE 0.2] Tier 1 Validation - SKIPPED` říkal operátorovi jen to, že se
validace neprovedla — ne proč. Dvě úplně jiné situace vypadaly identicky:
služba má validaci vypnutou konfigurací, nebo jí nikdo nepředal `serviceRoot`
a validovat NEMĚLA ČIM. To druhé je vada zapojení, která se takhle tvářila jako
záměr — a mlčení samo je závada, ne neutrální stav
(`.claude/rules/automation-gates.md` §5, „silence is a defect“).

Fáze teď důvod pojmenuje a nese ho oběma cestami, kterými řádek odchází:
v čitelné hlášce jako `[FÁZE 0.2] Tier 1 Validation - SKIPPED (validation
disabled)` a ve strukturovaném záznamu jako pole `reason`. Když platí obě
příiny najednou, jsou v řádku obě (`validation disabled; no serviceRoot`).
Fáze bez důvodu pole nepřidává vůbec, takže ostatní řádky knihy fází
zůstávají beze změny.

### Fixed — deklarace třídy `error.type` se ztrácela mezi handlerem a klasifikátorem (d.845b)

`DECLINED` je terminální konec, který NENÍ selhání: běh se neopakuje, nejde do DLQ
a nepatří do `workflow.failed` (conf `workflow-failed-status-projection.md` 004).
Vyhazující ji deklaruje výslovně přes `error.type` a klasifikátor ji čte PRVNÍ, před
kódem i statusem — jenže sem se nikdy nedostala:

- `BusinessError` třídu nepřijímal (konstruktor destrukturoval `{ code, status,
  message, details }`), takže ji vyhazující neměl jak deklarovat;
- `buildEnvelope()` ji neznal ani jako parametr a stavěl `{ code, message }`.

Deklarace tedy zanikla o dvě patra dřív, než se na ni klasifikátor zeptal, a běh,
který pravidlo zakázalo, končil jako `failed` — s poplachem nad nĝčím, co se
nerozbilo.

Nově obálka `type` veze, a to PRÁVĚ TAM, kde veže `details`: obojí je hlas
vyhazujícího a maskovaná cesta zahazuje obojí — 5xx branded, `MALFORMED_BUSINESS_ERROR`
i nebrandovaný pád mluví neutrálně a neprozradí ani třídu. Nedeklarovaná třída pole
NEZAKLÁDÁ (`'type' in error === false`), nikdy `null` — totéž, co už platí pro
`details` a `correlation_id`. Kdo třídu nedeklaruje, nepozná rozdíl.

Kontrakt: `api/docs/biz/70-contracts/error-handling.md` § `DECLINED`.

### Added — `ctx.scheduler` a FÁZE 0.7b: jedna kolej pro požadavky plánovači (d.834)

Služba, která si odvozuje rozvrhy ze svých dat nebo ohlašuje fakty, je dosud publikovala
adaptérem nad `wrapper.mqClient` — tolik koleječek, kolik služeb. Wrapper teď nabízí jednu:

- **`ctx.scheduler`** (runtime port pro handlery): `registerRecurring({ id, cookbookRef, cron,
  timezone, input?, catchUp? })`, `publishEvent({ name, idempotencyKey, input? })`,
  `cancel({ id })`. Tenancy doplňuje wrapper z obálky invokace, `requested_by` a
  `event.service` ze jména služby; kdo některý z těch klíčů podá, dostane odmítnutí jménem
  klíče. `idempotencyKey` u události podává VOLAJÍCÍ a wrapper ho neodvozuje — kontrakt ho
  žádá po žadateli, aby opakovaný požadavek nezaložil druhou položku.
- **FÁZE 0.7b `Declared Recurrences`** (volitelný boot hook): `new ServiceWrapper({ scheduler:
  { recurrences: async () => [...] } })`. Běží za registrací v registru a před konzumenty;
  hook chybí → fáze `SKIPPED` s důvodem; neplatný záznam nebo selhaná publikace → boot padá.
  `id` je `idempotency_key`, takže opakovaný boot rozvrh upsertuje.

Tvar požadavku nestaví wrapper: staví ho `@onlineapps/mq-client-core`
(`scheduler.createSchedulerPublisher`), aby kontrakt plánovače měl jednoho stavitele pro
všechny odesílatele — wrapper i operátorská CLI, která wrapper nebootují. Port proto
vyžaduje pin `@onlineapps/mq-client-core` s tou továrnou (d.833/d.840); na starším pinu se
port nepostaví a hláška jmenuje slot i opravu.

Rozvrhy tenanta si služba odvozuje ze svých dat a registruje je sama
(`api/docs/architecture/scheduler.md` §7); callback jako nový druh rozhraní wrapperu
potvrdil vlastník v `api/docs/governance/confirmations/scheduler.md` 010, tamtéž je
i pravidlo „jedna kolej publikace v service-wrapperu nad stavitelemi mq-client-core".

## [10.1.2] — 2026-09-24

pin: `@onlineapps/conn-base-monitoring` 2.1.2 → 2.2.0
pin: `@onlineapps/conn-infra-mq` 4.1.0 → 4.1.1
pin: `@onlineapps/conn-orch-registry` 6.0.1 → 6.0.2
pin: `@onlineapps/infrastructure-tools` 3.1.0 → 3.1.1
pin: `@onlineapps/conn-orch-orchestrator` 5.1.2 → 5.2.0

## [10.1.1] — 2026-09-18

pin: `@onlineapps/conn-base-monitoring` 2.1.1 → 2.1.2
pin: `@onlineapps/conn-orch-validator` 12.0.0 → 12.1.0
pin: `@onlineapps/infrastructure-tools` 3.0.2 → 3.1.0

## [10.1.0] — 2026-09-17

### Changed — chování, které služba po pinu uvidí (dovydání 5)

Vydání nemění žádný exportovaný podpis, ale dvě věci se po pinu chovají jinak, a proto to není patch:

- **Verdikt `components.mq` v heartbeatu** může být nově `unhealthy` i při drženém spojení — když klient sleduje
  konzumenta, který není připojen (d.613, sekce Fixed níže). Registry z mapy komponent odvozuje připravenost služby.
- **Runtime pin `@onlineapps/conn-orch-validator` na nový major:** startovací Tier-1 běh (fáze 0.2) odmítá
  `workspace_id` ≤ 0 z receptu i z `TESTING_WORKSPACE_ID` (d.606; změřeno nad osmi službami: 0 výskytů). Ostatní
  BREAKING změny toho majoru se týkají šablony CI a brány `deploy-contract` (dva refy api, pravidlo R2) — tedy
  repozitáře služby, ne wrapperu; služba je srovná `npx oa-sync-template .gitlab-ci.yml --target .`.


### Fixed — ztráta konzumenta se dostane do `components.mq`, ne jen do logu (d.613)

`_mqHealth()` četlo `isConnected()` a nic jiného, takže služba, které někdo
smazal frontu a jejíž znovupřipojení broker odmítl (406), publikovala
`mq: 'healthy'` tak dlouho, dokud drželo spojení — a registry z toho odvozoval
verdikt připravenosti. Model to nedovoluje:
`docs/architecture/HEALTH_SYSTEM.md` § Two rules every health signal obeys,
pravidlo 2 — „if losing X makes the service unable to serve its purpose, X's
reachability appears somewhere in the health system … otherwise the platform
reports healthy while a whole function is dead" — a § Business Services, kde je
konzument na workflow frontě jedním ze tří signálů připravenosti. Ta samá
koncepce už měří živost konzumenta jako komponentu heartbeatu o třídu vedle:
komponenta `queues` u registry je `mqManager.hasLiveConsumer()`
(`docs/standards/consumer-tag-queue-ready.md`).

`_mqHealth()` proto odpovídá na dvě otázky: spojení (`isConnected()`) a
připojenost konzumentů (`getConsumerState()` — `tracked` je co by klient
obnovil, `active` jmenuje připojené; jedna definice, vlastní ji
`@onlineapps/mq-client-core` d.613). `tracked > active.length` = `unhealthy`.
Nový slovník nevzniká: hodnoty zůstávají `healthy` / `unhealthy` / `unknown`, ty
tři, které validuje `infrastructureHealth.listener.js`
(`docs/standards/health-check-format.md`), a `unhealthy` komponenta se v registry
zapíše s odvozeným `status: DOWN` — přesně ten verdikt, který službě bez
konzumenta patří.

Čtení zůstává **synchronní a levné**: dvě čtení stavu klienta, žádná sonda;
periodická zdravotní kontrola klienta zůstává vypnutá. Klient, kterého se na
konzumenty zeptat nelze, dává `unknown` — půl měření není verdikt, a je to táž
doktrína, kterou tahle metoda říká o klientovi, co neumí odpovědět. Které fronty
o konzumenta přišly, jmenuje na úrovni ERROR sám MQ klient v okamžiku, kdy se to
stane; heartbeat přidává varování s počty, ze kterých verdikt vznikl (payload
heartbeatu žádné pole pro detail nemá — šest polí, `docs/biz/00-model/service-shape.md`).

Do plného účinku to vstoupí po re-pinu na `@onlineapps/mq-client-core` s d.613:
dnes vydané jádro jmenuje v `active` i nepřipojené konzumenty, takže verdikt
zůstane `healthy` — proto je konec konců u jádra, a ne tady.

## [10.0.0] — 2026-09-17

### Fixed — zapnutá cache bez `url` už neprojde jako PASSED (d.596)

`_initializeCache()` odpovídalo na chybějící `wrapper.cache.url` varováním
„Cache enabled but no URL provided. Skipping cache initialization" a `return`.
Metoda se přitom volá z JEDINÉHO důvodu — služba deklarovala
`wrapper.cache.enabled: true` — takže boot napsal `[FÁZE 0.16] Cache Connector -
PASSED` a běžel dál bez konektoru. Týž tichý fallback, jaký d.535 odstranila
o vrstvu výš (`architecture-principles.md` §3): deklarovaný a běžící tvar služby
se tiše lišily a každé čtení, které mělo být teplé, šlo jinam nebo nikam.

Nově FÁZE 0.16 skončí FAILED a boot se zastaví; hláška jmenuje klíč
(`wrapper.cache.url`), proměnnou prostředí, která ho plní (`REDIS_URL`), soubor,
kam patří, i variantu „služba cache nepotřebuje → zahoď `wrapper.cache`".
Dispozice je **trvalá** a čte se z chyby, takže odmítnutí konektoru (Redis, který
neběží) zůstává přechodné jako dosud. Služba bez `wrapper.cache` fázi přeskakuje
beze změny.

### Fixed — boot, který uspět nemůže, se neopakuje 18,5 minuty (d.594)

Startovní smyčka `_ensureValidationProof()` (FÁZE 0.2) spala mezi šesti pokusy
30/60/120/300/600/1800 s, a dělala to pro KAŽDOU chybu Tier-1 validace — i pro
tu, kterou žádný další pokus nezmění. Změřeno dvakrát: biz-converter na
produkční generálce skončil `exit 1` po 18 min 53 s, W591 naměřila totéž na
`MISSING_COOKBOOKS`. Pod `restart: unless-stopped` je to smyčka, ve které
post-deploy brána čeká na službu, která nikdy nenaběhne.

Nově se rozhoduje podle KROKU, který selhal (`result.steps.<krok>.valid` z
`@onlineapps/conn-orch-validator` 10.0.0), ne podle textu hlášky — `result.errors`
je jedno ploché pole, do kterého krok 1 dává objekty a kroky 2/4/5/7 věty:

- **trvalé, konec na první pokus**: kroky 1 `structure`, 2 `config`, 3 `env`,
  4 `operations` a 7 `manifest` — čtou jen soubory na disku, takže další pokus
  čte týž strom a dojde k témuž verdiktu;
- **přechodné, celý rozpočet šesti pokusů beze změny**: krok 5 `cookbooks`
  (recepty proti handlerům a DB) a chyba, kterou vyhodí sám validator (I/O).

Dispozici nese chyba (`error.bootDisposition`), stejně jako FÁZE 0.3 čte
`MQ_CONNECTION_FATAL` z chyby MQ klienta, a FÁZE 0.2 ji z ní přebírá do věty pro
operátora; chyba bez dispozice (guardy identity služby nad smyčkou) zůstává
trvalá jako dosud. Soubor `conn-runtime/validation-failure.json` zapisuje počet
pokusů, které boot SKUTEČNĚ udělal (u trvalého nálezu jeden, ne šest), takže
šestihodinový cooldown `_applyRestartCooldown()` přijde po šesti restartech, ne
po prvním.

Integrační případ FÁZE 0.2 (d.591) tím přestal potřebovat falešné hodiny: běží
na skutečných a měří, že boot do FAILED 0.2 doběhne pod deset vteřin.

### Fixed (testy) — `shutdown-closes-telemetry` je zpátky v regresní smyčce (d.595)

Sada deklaruje a fail-fast ověřuje `LOG_MAX_SIZE_BYTES`/`LOG_MAX_FILES`, které od `@onlineapps/monitoring-core` 3.0.0 nemají default a bez nichž
padala FÁZE 0.12 sondy (6/6 červených od re-pinu); sonda napíše report i při `process.exit(1)` a nese v něm stderr, takže příčina padlého
bootu už není neviditelná. Vada nebyla v `src/` — shutdown telemetrii zavírá (integrační tier 149/149 `--runInBand`).

### Fixed — FAILED 0.2 jmenuje příčinu (d.593)

`results.errors` validatoru je smíšené pole: kroky 2, 4, 5 a 7 do něj dávají
věty, krok 1 (`ServiceStructureValidator`) objekty `{ type, path, message, fix }`.
Wrapper celé pole interpoloval do šablony, takže operátor četl
`Validation failed: [object Object]` — kód nálezu (`MISSING_COOKBOOKS`), cesta
i `fix` se do logu nedostaly (`architecture-principles.md` §5). Změřeno W591 na
bootu služby s prázdným `tests/cookbooks`.

Nově obě místa, která Tier-1 chybu vyhazují — startovní cesta
(`_ensureValidationProof`, FÁZE 0.2) i hodinová revalidace
(`_executeRevalidation`) — skládají hlášku jedním helperem
`describeValidationFailure()`: `[ServiceWrapper] Validation failed - <nález> |
<nález>`, kde objektový nález má tvar `TYPE (cesta): zpráva Fix: …` a nález,
který už větou je, projde doslova.

### Removed — BREAKING: registrační zpráva nenese `deployable` (d.592)

Klíč `deployable` (verdikt uniformy z Tier-1 kroku 7) opouští payload
`_buildRegistrationPayload()` i registrační řádek boot logu. S ním končí pole
`validationDeployable`, metoda `_readDeployableVerdict()` a její kontrola
„Validation result lacks deployable — conn-orch-validator >= 8.0.0 required“:
hlídala kontrakt kvůli payloadu, který zanikl.

Důvod (vlastník 2026-09-17, konfirmace
`docs/governance/confirmations/biz-service-manifest.md` 011): krok 7 měří git
checkout, takže produkční obraz — bez `.git`, bez compose souboru a bez README —
se registroval jako `deployable: false` u commitu, který byl v CI zelený.
Projekce lhala o každé produkční službě a klíč nikdo nečetl (0 výskytů v
`infra/**` i `fe_adminui/src/**`). Nasaditelnost dokazuje CI job
`validate-uniform` z každého commitu (konfirmace 010), ne běžící kontejner.
Slib projekce do `infrastructure:health:<service>` z konfirmace 001 §4 tímto padá.

Pro službu se nemění nic v konfiguraci; registr klíč v příští verzi odmítne
jménem, takže wrapper ho musí přestat posílat dřív nebo současně.

### Fixed — FAILED fáze 0.2/0.3/0.4 boot skutečně zastaví (d.591)

`_handleInitializationError()` je `async` (uvnitř `await this.shutdown()`,
teprve pak `process.exit(1)`), ale volání ve FÁZI 0.2 (Tier-1 validace), 0.3
(připojení k RabbitMQ) a 0.4 (brána infrastruktury) ho nečekala. Nečekané volání
se pozastaví, catch blok se VRÁTÍ a `initialize()` jde dál — službu, kterou
Tier-1 odmítla, stihl boot zaregistrovat u registru (0.7) a rozjet jí heartbeat
(0.10), než se zařazený `process.exit(1)` provedl.

Táž vada a táž oprava jako u FÁZE 0.7 v d.532 (`return await`), jen o pět fází
dřív; 0.15, 0.16 a 0.10 správný tvar měly. Změřeno BIZ-pdfgenem na produkčním
obrazu: `[FÁZE 0.7] FAILED` → `[FÁZE 0.10] PASSED` → `✓ biz-pdfgen ready` →
`Boot stopped … exits 1`.

Dispozice se nemění: 0.2 zůstává trvalá, 0.3 čte marker odmítnutí z chyby, 0.4
zůstává přechodná. Mění se JEN to, že se na teardown a ukončení procesu čeká.

## [9.0.0] — 2026-09-16

### Breaking — heslo a databázi Redisu si konektor tajemství čte sám; `wrapper.secrets.password` a `.db` končí (d.534b, půlka 1)

`@onlineapps/conn-infra-secrets` 3.0.0 — od téhle vlny připnutý — bere celé
`redisUrl` a hostitele, port, ACL uživatele, heslo i index databáze si z něj
přečte sám. Volby `password` a `db` **odmítá jménem**. Wrapper obě předával
(`password: secretsConfig.password || this._redisConnection(redisUrl).password`,
`db: secretsConfig.db`), takže boot každé služby, jejíž Redis má `requirepass`,
umíral na `[conn-infra-secrets] Option "password" is not accepted` — změřeno
integračním tierem proti připnuté verzi a živému Redisu s `requirepass` dřív, než
tahle dávka vznikla. Konstruktor teď dostává přesně `{ redisUrl, masterKeys }`
a sada to hlídá **množinou klíčů**, ne kontrolou pár jmen: odmítnutí je podle
jména a zapomenutý klíč je mrtvý boot.

S předáním končí i konfigurační klíče `wrapper.secrets.password`
a `wrapper.secrets.db` (`change-discipline.md` § Removing something removes its
declaration — čtyři otázky krátce): vznikly 2026-09-11, kdy konektor bral endpoint
po částech a přihlašovací údaj musel jet vedle URL; část koncepce, kterou nesly —
„přihlašovací údaj jede na svém URL" — dnes nese samo URL, což je koncepčnější
kolej; nečte je nic, protože jediný čtenář bylo tohle předání; a nikdo je ve
workspace nedeklaruje (změřeno v `api_biz/*/config/service/config.json`: každá
služba nese jen `secrets.enabled`). `keyPrefix` se nepředává také — výchozí
hodnotu vlastní konektor a klíč, který nikdo nekonfiguruje, není deklarace, kterou
by mělo cenu přidávat.

**Co tahle dávka NEudělala:** cache zůstala na dílčích hodnotách, protože
`@onlineapps/conn-base-cache` 2.0.0 tehdy ještě nebyla připnutá — proti tehdejší
1.0.12 by celé `url` padlo na `[RuntimeConfig] Missing environment variable -
REDIS_HOST is required for config key "host"` (změřeno proti nainstalovanému
balíčku). Uzavírá to druhá půlka d.534b hned níž. `_redisConnection()` zůstává pro
`@onlineapps/conn-base-state`, který vlastní dávku zatím nemá.

### Breaking — hostitele, port i heslo Redisu si konektor cache čte sám z `url`; tři fallbacky wrapperu končí (d.534b, půlka 2)

`@onlineapps/conn-base-cache` 2.0.0 — od téhle vlny připnutá — bere celé `url`
a hostitele, port, ACL uživatele, heslo i index databáze si z něj přečte sama;
volby `host`, `port`, `password` a `db` **odmítá jménem**. Wrapper je do d.534b
předával, takže boot každé služby s nakonfigurovanou cache umíral ve FÁZI 0.16 na
`[CacheConnector] Option "host" is not accepted` — změřeno integračním tierem
proti připnuté 2.0.0 a živému Redisu dřív, než tahle dávka vznikla. Konstruktor
teď dostává přesně `{ url, logger, namespace }` a sada to hlídá **množinou klíčů**,
ne kontrolou pár jmen: odmítnutí je podle jména a zapomenutý klíč je mrtvý boot.

Konektor nově dostává i `logger` — instanci, kterou postavila FÁZE 0.12, ne
znovu sestavenou: `assertLogger` chybějící logger odmítá a nespadne na `console`
(`api/docs/governance/confirmations/connector-logger-contract.md`).

S předáním končí tři fallbacky (`architecture-principles.md` §3): `parsed.host ||
cacheUrl` podstrčil celé URL jako jméno hostitele, kdykoli hodnota nezačínala
`redis://`; `parsed.port || 6379` a `ttl || 300` odpovídaly na chybějící deklaraci
literálem wrapperu. `defaultTTL` se předává **jen když** služba deklaruje
`wrapper.cache.ttl`; jinak klíč v konfiguraci vůbec není — výchozí hodnotu vlastní
konektor (`defaultTTLSeconds`, env `CACHE_TTL`). Není to o tom, co konektor udělá
(`resolve({url})` i `resolve({url, defaultTTL: undefined})` odpovídají shodně
3600 — změřeno proti pinu), ale o tom, co předání **tvrdí**: klíč bez hodnoty
předstírá deklaraci, kterou služba nikdy nenapsala.

`_redisConnection()` zůstává — čte ho `_initializeState()` pro
`@onlineapps/conn-base-state`, který dál bere endpoint po částech a vlastní dávku
zatím nemá. Deklarace `wrapper.cache.ttl` se nikde neruší: všech osm služeb ji
v `api_biz/*/config/service/config.json` nese (`ttl: 300`, změřeno).

### Fixed — dvojník publisheru tepu nepředstírá metodu, kterou vydaná knihovna nemá (d.570)

`tests/unit/HealthStatusPublisher.test.js` stavěl dvojník
`createBaseClientAdapter()` s metodou `isDisabled()`. Vydaný
`@onlineapps/infrastructure-tools` 2.0.3, který tenhle balíček instaluje, ji nemá:
jeho publisher vrací přesně `start`, `stop`, `publishHealthCheck`, `publishNow`
a `isRunning`, a CHANGELOG knihovny zaznamenává proč — „`DISABLE_HEALTH_CHECKS`
a `isDisabled()` pryč" (d.391). Wrapper ji nevolal nikdy
(`grep -rn isDisabled src` → nic).

Čtyři otázky `change-discipline.md` § Removing something removes its declaration:
metoda existovala, dokud šel health check vypnout přes `DISABLE_HEALTH_CHECKS`;
nesla ji provozní úniková klapka; nikdo ji dnes nečte, protože knihovna obě
poloviny zrušila („There is no way to switch a health check off"); a náhrada je
koncepčnější — jedna kolej bez klapky pro pohodlí (`automation-gates.md` §1
požadavek 5), protože „vypnuto" nikdy neznamenalo ticho, ale po grace periodě
tiše hlášené DOWN, k nerozeznání od pádu. Kdo má přestat publikovat, volá `stop()`.

Falešný dvojník není neškodný: každý případ té sady běžel proti tvaru, který
platforma nemá, takže sada mohla být dál zelená o zapojení, které by nefungovalo.
Nově to hlídá kontrolní případ, který povrch dvojníka porovnává se **skutečným**
publisherem z vydaného balíčku (sonda, ne opsaný seznam).

### Removed — registrační payload nese jen to, co registr čte: `name` a `url` pryč (d.569)

`_buildRegistrationPayload()` posílal dva klíče, které nikdo nečte. Změřeno nad
vydaným `@onlineapps/conn-orch-registry` 4.0.3: `register()` sahá na `endpoints`,
`operations`, `metadata`, `health`, `spec`, `validationToken`/`token`,
`tokenSecret`/`secret`, `workspaceScoped`, `deployable` a `timeout` — a na žádné
`serviceInfo.name` ani `serviceInfo.url`. Oba klíče tedy nikdy neopustily proces.

Čtyři otázky `change-discipline.md` § Removing something removes its declaration:

1. **Proč vznikly** — payload byl kdysi sebepopisný záznam služby: kdo jsem
   (`name`) a kde mě zastihneš (`url`), posílaný registru, který URL
   dereferencoval přes HTTP.
2. **Která část koncepce je nesla** — HTTP service discovery; záznam v registru
   byl adresa, na kterou jiné služby volaly.
3. **Proč je dnes nikdo nečte** — ADR 0005 nechává biz kontejner bez HTTP
   listeneru (konfirmace `biz-service-port-url.md` 001), takže žádná URL není;
   a identita se přesunula do konstruktoru klienta, který ji posílá jako
   `serviceName: this.serviceName`.
4. **Je náhrada koncepčnější** — ano, v obou případech. Identita svázaná při
   konstrukci se nemůže sama se sebou rozejít mezi dvěma registračními místy,
   jak se to stalo `url` před d.96a; a dostupnost je dnes fronta, kterou služba
   konzumuje — tu zná broker a žádná zpráva ji nemusí tvrdit.

Hardcodované `url: ''` bylo navíc v JSDoc obhájeno tím, že „keeps pre-3.4.1
Registry consumers that expect the field happy" — tedy shim zpětné kompatibility,
kterou tahle platforma před prvním produkčním nasazením nedluží
(`architecture-principles.md` §11), pro konzumenta, který neexistuje.

Payload nově nese přesně čtyři klíče: `operations`, `workspaceScoped`,
`deployable`, `metadata`. Drátová zpráva se nezměnila — což je právě ten důkaz.

### Fixed — běžící krok má jednu kolej: `current_step`, a chybějící se odmítne jménem (d.561)

`_dispatchViaInvokeOperation()` četl `message.current_step || message.step?.step_id`.
Druhá větev je **mrtvá kolej**: tělo se `step: { step_id }` nepíše nikde — změřeno
2026-09-16 nad `conn-orch-orchestrator/src`, `service-wrapper/src`,
`api/infra/api_gateway/src` a `api/infra/api_monitoring/src` (jediný výskyt
literálu `step: {` je fixture monitoringu s `_execution`, žádné `step_id`).
Orchestrátor publikuje `current_step` (`WorkflowOrchestrator._routeToNextStep`),
a `message.step`, které sem doopravdy chodí, nese `service` a `operation`.

Komentář nad tou větví navíc tvrdil, že se hodnota čte „the same way the
orchestrator branch reads it". Nepravda: orchestrátorská větev je
`_normalizeWorkflowMessage()` a ta sahá na **první krok kuchařky**, ne na
`step.step_id`. Větev, kterou nikdo nekrmí, nemůže selhat nahlas — umí jen
z chybějícího pole udělat jinou hodnotu, a `step_id` pojmenovává úložný prefix
`content/<workflow_id>/<step_id>`: špatná hodnota zapíše soubory běhu pod cizí klíč.

Nově: jeden klíč, a chybějící `current_step` je odmítnut **tady**, jménem drátového
pole, jako `InvalidEnvelopeError` (400 `INVALID_ENVELOPE`) — tedy táž třída, kterou
`_normalizeWorkflowMessage()` pro totéž pole už odmítá v orchestrátorské větvi.
Není to druhá kolej ke `ContextBuilder._validateMqMessage()`: ten hlídá **vnitřní**
`mqMessage.step_id` a jeho `Fix:` říká „pass step_id from the workflow message",
což publikujícímu, jemuž v těle pole chybí, nepomůže.

### Breaking — konfigurovaná cache, která se nepřipojí, boot ukončí: FÁZE 0.16 (d.535)

Selhání `_initializeCache()` se do téhle dávky odchytilo a odpovědělo varováním —
„the service continues without a cache connector" — a boot šel dál. To je fallback
třídy, kterou `architecture-principles.md` §3 zakazuje: deklarovaný tvar služby
(`wrapper.cache.enabled: true`) a běžící tvar se tiše rozcházejí a každé čtení,
které mělo být horké, jde jinam nebo nikam. Změřeno INFRA na devu: invoicing,
pdfgen i property na wrapperu 7.0.0 běžely **bez cache**, po třech řádcích
`Failed to connect`, a boot se tvářil v pořádku.

Nově je to **fáze s vlastním číslem** — `0.16`, protože číslo fáze je její
identita, ne pozice (d.142), a řádek FAILED musí říct, ke které fázi patří:

- `wrapper.cache.enabled: true` a konektor se připojí → `0.16 PASSED`;
- `wrapper.cache.enabled: true` a nepřipojí se → `0.16 FAILED`, exit 1, boot se
  zastaví (přechodné: Redis, který je dole, se restartem vrátí — táž dispozice
  jako FÁZE 0.4);
- služba žádnou `wrapper.cache` nedeklaruje → `0.16 SKIPPED`, jako dosud.

Sada `tests/unit/ServiceWrapper.consoleChannel.test.js` přišla o tři případy,
které popisovaly zrušené chování (varování přes logger, ticho na konzoli, „boot
pokračuje bez cache"); zůstal případ, který se týká jejího vlastního předmětu —
boot bez deklarované cache neřekne nic na žádném kanálu. Nové chování má vlastní
sadu (`ServiceWrapper.cachePhase.test.js` + integrační proti živému Redisu).

### Fixed — FÁZE 0.7 odmítnutá brokerem boot ukončí, a FAILED fáze už nikdy nevytiskne „ready" (d.532)

Dvě vady, obě změřené BIZ-metou na devu (wrapper 8.1.1). Fronta
`biz-meta.registry` existovala se starými argumenty, broker odpověděl
`406 PRECONDITION-FAILED`, FÁZE 0.7 se zapsala FAILED — a boot **šel dál**,
postavil zbytek pipeline, nechal `bootstrap()` vytisknout `✓ biz-meta ready`
a teprve pak umřel na `[StateConnector] Not connected`. Crash loop, jehož log
tvrdí, že služba naběhla.

1. **Boot pokračoval.** `_handleInitializationError()` je `async` a volací místo
   fáze 0.7 ho volalo **bez `await`** — `await this.shutdown()` uvnitř se
   uspalo, `catch` blok se vrátil a `initialize()` doběhlo až na `INIT PASSED`
   dřív, než se zařazený `process.exit(1)` vůbec dostal ke slovu. FÁZE 0.15
   měla správný tvar (`return await`) už dřív; 0.7 ne. Teď ho má.

2. **Odmítnutí bylo hlášeno jako přechodné.** Verdikt se **čte** z chyby, touž
   kolejí jako ve FÁZI 0.3 o obrazovku výš: otázku „odpověděl broker a odmítl?"
   vlastní `@onlineapps/mq-client-core` (`REFUSAL_REPLY_CODES`,
   `_isBrokerRefusal()`) a odpověď píše na chybu jako `code:
   'MQ_CONNECTION_FATAL'` / `reason: 'broker-refused'`;
   `@onlineapps/conn-orch-registry` ji nově přenáší přes svá dvě přebalení
   (d.532 v tom balíčku). Seznam AMQP reply kódů ani čtení textu hlášky v tomhle
   souboru nepřibyly — to by byla druhá kopie cizího faktu.

**„ready" se váže na knihu fází, ne na to, že `initialize()` doběhlo.**
`_logPhase` — jediná kolej, která píše řádek FAILED — si fázi poznamená na
wrapperu (`bootFailure`), a `bootstrap()` na ní odmítne pokračovat: místo tří
`✓` řádků vyhodí výjimku, která jmenuje fázi, službu a kam se podívat.
`isInitialized` by na to nestačilo — boot, který lže, je právě ten, co ho
nenastaví.

**Co tahle dávka NEudělala, změřeno a předáno leadovi:** 406 z cesty
`consume()` **nenese značku odmítnutí**. Sondou proti živému brokeru:
`ConsumeError`, `code === undefined`, `reason === undefined`, 406 přežije jen
jako `cause.cause.code`. `@onlineapps/mq-client-core` sice 406 za odmítnutí
považuje (`REFUSAL_REPLY_CODES = [403, 530, 406]`), ale ptá se na to jen na
cestě connect a reconnect — `_prepareQueueForConsume()` staví `ConsumeError` bez
`code`. Totéž pro 403 na connectu ve **vydané** 3.2.0, kterou tenhle balíček
instaluje (zdroj toho balíčku už `_isBrokerRefusal()` v `connect()` volá, d.295 —
případ se překlopí sám, až verze vyjde). Do té doby se 406 hlásí jako přechodný;
polovina, která zavírá změřený crash loop — boot se zastaví a nikdo neřekne
„ready" — platí od téhle dávky.

Integrační tier k tomu nově potřebuje brokera: `tests/config.js` má zpět
`requireRabbitMq()` (AMQP URL + `RABBITMQ_MGMT_PORT`) a `tests/integration/setup.js`
ho sondou ověří dřív, než Jest postaví první `describe` — nedostupný broker běh
ukončí, nikdy ho tiše nepřeskočí.

### Removed — **BREAKING** `step.step_id` už nezastupuje jméno operace (d.530)

Hook `operationMutatesLookup`, kterým se orchestrátor ptá na specifikaci
operace, četl `step.operation`, a když chybělo, hledal operaci podle id KROKU —
dvě různé skutečnosti na jednom klíči. Id kroku, které se náhodou shodovalo se
jménem operace, tiše zdědilo její `mutates` i `sensitive_input_fields`; id, které
se neshodovalo s ničím, dostalo odpověď „operaci neznám", a ta vypadá přesně jako
deklarovaná čtecí operace bez citlivých vstupů. Obě odpovědi byly vymyšlené — a
`sensitive_input_fields` rozhoduje o scrubu §8 (`docs/architecture/secretbox.md`),
takže vymyšlená odpověď tam znamená tajemství putující celým během, aniž to kdo
ohlásí.

Kolej je tedy jedna: `step.operation`. Krok, který operaci nejmenuje, je odmítnut
výjimkou podle §5 (jmenuje pole i opravu), ne prázdným klíčem.

Odstraněná větev byla navíc nedosažitelná: `@onlineapps/conn-orch-orchestrator`
5.0.0 odmítne krok typu `task` bez `operation` dřív, než se hook zavolá
(`WorkflowOrchestrator._requireStepOperation`, schéma cookbook-core
`TaskStep.required`). Doloženo na skutečném orchestrátoru —
`tests/integration/operationSpecLookup.integration.test.js`.

### Fixed — hláška chyby nepřežila serializaci a trasa jednoho požadavku měla dvě id (d.548)

Dvě vady, každá dělá log prohledatelným v teorii a nepoužitelným v praxi
(změřeno BIZ-converterem na devu, wrapper 8.1.1):

1. `Error.message` je **non-enumerable**. Meta `{ err }` se proto serializuje na
   `{ name, code, status, details }` — věta, která říká, co se stalo, přežila jen
   jako první řádek `stack`, a jen tam, kde se `stack` vůbec předával. Čtyři
   z devíti logovacích řádků `ErrorMapper`u žádný `stack` nepředávají, takže
   jejich hláška se k operátorovi nedostala vůbec.
2. Řádky wrapperu (`Processing message from …`, předání orchestrátoru, zadržená
   RPC odpověď, selhání zapsané error handlerem) nesly jen `workflow_id`, kdežto
   každý řádek `ErrorMapper`u jen `correlation_id`. Trasa JEDNOHO požadavku byla
   rozpůlená mezi dva klíče: kdo hledal podle jednoho, našel polovinu.

Nově nese každý řádek o zprávě **obě id** — `workflow_id` i `correlation_id`,
v pravopisu, který kontrakt zprávy už používá (`docs/biz/70-contracts/workflow-message.md`
§2.2; žádné nové názvy polí) — a každý řádek, kterému se předává chyba, nese
navíc `message` jako vlastní, výčtovou vlastnost. `invokeOperation()` proto
`ErrorMapper.map()` předává i `workflow_id` ze zprávy; kontext zapisovaný přes
`errorHandler.logError()` dostal `correlationId` vedle `workflowId` (camelCase je
pravopis `error-handler-core`, hodnota je snake_case z těla zprávy).

Obálka volajícího se nemění — přibyla pole v LOGU, ne v odpovědi.

### Added — `errorLogFields()` na `@onlineapps/service-wrapper/errors` (d.548)

`{ err, message }` jedním voláním, aby „hláška musí být vlastní vlastnost" byla
jedna kolej pro celý balíček a ne devět kopií téhož výrazu
(`change-discipline.md` § One rail per concern). Používá ho `ErrorMapper` na
všech devíti řádcích a `ServiceWrapper` na dvou, které píše o chybě sám.

### Changed — **BREAKING** deklarovaný business výsledek je `permanent`: zpráva se doručí jednou (d.547)

Změřeno BIZ-converterem na devu (wrapper 8.1.1): handler hodil 409
`DUPLICATE_RESOURCE` (brandovaná `BusinessError`) i 500
`DATA_INTEGRITY_VIOLATION` (uložená vada) — a broker doručil zprávu **třikrát**.
Tři běhy handleru, tři transakce, tři řádky, a teprve pak `<služba>.dlq`.
Příčina: `_classifyDeliveryError()` znal jako `permanent` jen `SyntaxError`,
`code === 'INVALID_ENVELOPE'` a dvojici `type === 'VALIDATION'` + `status === 400`;
všechno ostatní bylo `transient` — „wrapper odmítá hádat".

Nově je `permanent` každý **deklarovaný** výsledek: chyba nese tvar business
chyby podle `docs/biz/70-contracts/error-handling.md` §1 (`code` ve tvaru
slovníku, `status`, hláška) a ten `code` není jedním z
`UNDECLARED_FAILURE_CODES`. Status se nečte samostatně a kód nepřebíjí: 409 říká,
že zdroj už tam je a bude tam i podruhé; 500 `DATA_INTEGRITY_VIOLATION` říká, že
uložený řádek odporuje invariantu, a to žádné doručení nespraví. Přesně to
znamená rozhodnutí vlastníka „validation, business → `.dlq` při prvním selhání"
(`governance/confirmations/mq-consumer-contract.md` 002 bod 3).

Rozpoznání čte **tvar**, ne brand: na cestě v3 se selhání přes `ErrorMapper`
zplošťuje na prostý `Error` s `code` a `status` (`_dispatchViaInvokeOperation`),
takže třída ani brand hop nepřežijí — publikovaný `code` ano. Dvě pravidla proto
nahradila tři: `INVALID_ENVELOPE` i strážci orchestrátoru
`MISSING_CORRELATION_ID` / `MISSING_WORKFLOW_ID` jsou deklarované kódy a čte je
tentýž predikát, proto jejich testy prošly beze změny.

`transient` zůstává tam, kde nikdo nic nedeklaroval: netypovaný `Error`,
programátorská chyba, odmítnuté spojení, timeout — a maskované kódy
`INTERNAL_ERROR` a `MALFORMED_BUSINESS_ERROR`, které jsou po zploštění
k nerozeznání od deklarované 500 a znamenají právě **nepřítomnost** výroku.

**A zůstává i tam, kde opakování deklaruje sám kontrakt (d.559).** Deklarovaná
identita kódu není jen jeho jméno a status: tabulka přijatých doménových kódů
(`docs/biz/70-contracts/error-handling.md` § Accepted domain codes) má sloupec
`Retry` a ten je součástí toho, co kód znamená. Jediný řádek, který dnes říká
„ano", je `PROVIDER_UNAVAILABLE` (503) — třetí strana, která se sama zařadila do
kategorie známé tím, že se zotaví. Takový kód je `transient` a spotřebuje
rozpočet pokusů jako dřív; seznam je `CONTRACT_RETRYABLE_CODES` na `./errors`,
jehož jediným vlastníkem je ta tabulka. Bez toho by d.547 posílal do `.dlq` při
prvním selhání i výpadek poskytovatele, který kontrakt výslovně označuje za
opakovatelný (živě `api_biz/emailer/src/lib/errors.js`).

**Dopad na služby:** handler, který hází `NotFoundError`, `ConflictError`,
`ValidationError` nebo vlastní brandovanou třídu, uvidí zprávu v `.dlq` po
prvním selhání místo po vyčerpání rozpočtu — a jeho vedlejší efekt proběhne
jednou místo třikrát. Kdo chce opakování, nesmí výsledek deklarovat.

### Added — `hasBusinessErrorShape()`, `UNDECLARED_FAILURE_CODES` a `CONTRACT_RETRYABLE_CODES` na `@onlineapps/service-wrapper/errors` (d.547, d.559)

`isBusinessError()` vyžaduje brand, a ten na cestě v3 hop nepřežije. Tvarová
polovina kontraktu (`code` + `status` + hláška) dostala jméno a export, aby
`_classifyDeliveryError()` nemusel mít vlastní kopii pravidel o kódu a statusu
(`change-discipline.md` § One rail per concern). `UNDECLARED_FAILURE_CODES` je
zmrazená množina dvou kódů, které tabulka `describeFailure()` dává obálce, když
selhání nedeklarovalo nic. `CONTRACT_RETRYABLE_CODES` (d.559) je zmrazená množina
kódů, jejichž řádek ve slovníku deklaruje opakování za smysluplné — vlastníkem je
ta tabulka, ne tenhle soubor, a ❌ není to příznak na instanci chyby: co smí být
opakováno, je vlastnost kontraktového slovníku, ne jednoho vyhození. Ani jeden
predikát nerozhoduje o vlastnictví chyby — brandovaná, ale rozbitá chyba zůstává
`MALFORMED_BUSINESS_ERROR`.

### Changed — ztracený souborový log ukončí službu, místo aby běžela dál a mlčela (d.515b)

FÁZE 0.12 zapíná biz službám lokální NDJSON soubor (`file.enabled: true`).
`@onlineapps/monitoring-core` do d.515b odpovídal na selhání zápisu ZA BĚHU —
plný disk, rotace na cestu, která zmizela, odebraná práva — tím, že si kanál sám
vypnul a vypsal jeden řádek; služba pak běžela dál bez souboru, o který si řekla.
Od d.515b to knihovna hlásí volajícímu (`file.onFatal(err)`, povinný klíč) a
ukončení procesu nechává na vlastníkovi životního cyklu
(`mq-client-lifecycle-contract` 001 §1) — tedy na tomhle wrapperu.

Wrapper odpovídá kolejí, kterou už má pro trvale ztracené MQ spojení:
`onFatal` → `_shutdownFatally(err.message, { code, cause })` — jedna §5 hláška
(knihovny, nepřepsaná), řízené vypnutí, exit 1, a restart policy kontejneru
nabootuje službu proti disku, který funguje. Rozhodnutí vlastníka:
`api/docs/governance/confirmations/logger-runtime-write-failure.md` 001.

### Fixed — přepsání jednoho klíče `wrapper.monitoring.file` maže celý blok (d.515b)

Měřeno při zapojování `onFatal`: objekt předávaný `MonitoringConnector.init()`
měl `...this.config.wrapper?.monitoring` jako POSLEDNÍ spread, a ten nese klíč
`file`, kdykoli si ho služba deklaruje. Vnitřní `...(… ?.file || {})`, který měl
dělat sloučení po klíčích, tedy nemohl nikdy vyhrát: služba, která přepsala
jediný klíč (`file.directory`), přišla o `enabled`, `filename`, `maxDays` — a od
d.515b i o vlastníka kanálu — a dostala přesně ten jeden klíč, který jmenovala.
S monitoring-core 3.0.0 by taková služba rovnou nenabootovala.

Deklarace služby jde teď PRVNÍ a blok `file` se na ni skládá po klíčích, jak to
ten vnitřní spread vždycky zamýšlel; ostatní klíče `wrapper.monitoring` procházejí
beze změny.

### Changed — **BREAKING** FÁZE 0.10 je trvale fatální: boot bez tepu se nehlásí jako zdravý (d.513)

Selhání startu publisheru tepu (FÁZE 0.10) bylo označeno za přechodné a boot
pokračoval: `[FÁZE 0.10] Health Heartbeat Publisher - FAILED`, varování
„service continues without heartbeat" a hned za ním `[FÁZE INIT] ServiceWrapper
Initialization - PASSED`.

Premisa té nefatálnosti byla měřitelně nepravdivá. Selhání **publikace** se do
toho catch nikdy nedostane: `runPublish()` drží celé tělo v `try` a vrací
`{status:'failed'}`, takže `publishHealthCheck()` nehodí a `start()` po zapsání
prvního tiku stejně nastaví svůj `setInterval`
(`@onlineapps/infrastructure-tools`, `health/healthPublisher.js`). Tahle cesta
zůstává beze změny — je to „log + continue" z
`docs/biz/00-model/service-shape.md` § Failure semantics a další tik nese týž
verdikt.

Dosažitelné jsou jen chyby **trvalé**: chybějící identita (`service.name`,
`service.version`), nepoužitelný název fronty nebo publish interval, chybějící
knihovna. Nic je neopakuje — `startHealthHeartbeat()` má jediné volací místo —
takže služba běžela s konzumenty nastartovanými v 0.8/0.9 a bez jediného tepu,
tedy trvale `not_ready` podle vlastního verdiktu registry (`businessReadinessChecker`:
fronta + konzumenti + zdravý tep). Změřeno sondou d.513: 0 tepů za 600 s běhu,
`isInitialized = true`, `[FÁZE INIT] … PASSED`.

Nově fáze 0.10 končí boot: `_handleInitializationError('0.10', 'Health Heartbeat
Publisher', error, false)` — teardown, jeden FAILED řádek (kolej d.508b, fázi
píše jen kolej, která uklízí a končí exitem 1), dispozice „Permanent: a restart
will fail the same way until the cause is fixed." a exit 1. Chyba nese §5 tvar
s původní hláškou knihovny (ta jmenuje konkrétní klíč), s Expected (identita +
`wrapper.heartbeat.publishInterval` / `INFRASTRUCTURE_HEALTH_PUBLISH_INTERVAL`,
`wrapper.heartbeat.queueName` / `INFRASTRUCTURE_HEALTH_QUEUE`) a s Fixem.
Rozhodnutí vlastníka 2026-09-16:
`api/docs/governance/confirmations/boot-phase-0-10.md` 001. Táž doktrína jako
d.508 o fázi dřív — boot, který nerozjel svůj mechanismus, se nesmí ohlásit jako
zdravý — a `service-shape.md` má první tep jako MUST.

Spolu s tím: `_heartbeatPublisher` se přiřazuje až po úspěšném `start()`, ne po
konstrukci. Přiřazené dřív tvrdilo pole „publisher existuje", zatímco jeho jediný
čtenář (vstupní guard metody, `isRunning()` v testech) se ptá, jestli služba má
**běžící** tep — postavený a nerozjetý publisher by dalšímu volajícímu odpověděl
„already started" (`architecture-principles.md` §3, táž vada, kterou d.349
odstranila u komponenty `cache`).

Migrace: služba s vadnou konfigurací tepu po nasazení **nenaběhne** — místo aby
tiše běžela trvale červená. Konfigurace se nemění, chování za běhu (kadence,
payload, chování při selhání publikace) také ne.

### Changed — FÁZE 0.8 a 0.9 běží až za orchestrátorem: služba nekonzumuje dřív, než umí obsloužit (d.508)

Workflow fronty (FÁZE 0.8) a tři `consume()` (FÁZE 0.9) běžely uvnitř
`_initializeRegistry()` (FÁZE 0.7), tedy PŘED konektory cache/state/secrets,
před `ContextBuilder`em (`_initializeInvocationContext`) a před
`_initializeOrchestrator()`. Změřeno sondou d.508: v okamžiku prvního
`consume()` byl stav wrapperu
`{"contextBuilder":null,"orchestrator":null,"errorHandler":"undefined","cacheConnector":null}`.

Důsledek na doručené zprávě: s vlastní operací padne `invokeOperation()` na
`this._contextBuilder.build()` nad `null`; bez ní propadne
`_processWorkflowMessage()` větví `else if (this.orchestrator)` do
`InvalidEnvelopeError`, kterou `_classifyDeliveryError()` klasifikuje jako
PERMANENT — platná zpráva jde rovnou do `<služba>.dlq`. Durabilní fronta to
nezachrání, protože zpráva už byla zkonzumována.

Nově `_initializeRegistry()` jen uloží verdikt registrace
(`this._registrationResult`) a `initialize()` spouští `_startWorkflowListenersIfReady()`
až za `_initializeOrchestrator()` a před FÁZE 0.10. Podmínka certifikátu se
nemění a zůstává v témže místě; čísla fází jsou identita těch dvou kroků, ne
jejich pozice — 0.8 a 0.9 se nepřečíslovaly. Selhání zůstává přechodné
(teardown + exit 1), nově pojmenované fází 0.9 místo 0.7, a boot, který nerozjel
konzumenty, už nepokračuje do FÁZE 0.10.

Pořadí odpovídá tomu, čím připravenost měří registry
(`infra/api_services_registry/src/services/businessReadinessChecker.js`:
„Ready = queue exists + has consumers + heartbeat reports healthy") — kde je
konzument, tam je i obsluha. Registry tep (`startHeartbeat`) zůstává, kde byl.

Migrace: služby se netýká, mění se jen pořadí uvnitř bootu. V logu se `[FÁZE 0.7]
Registry Registration - PASSED` nově tiskne BEZ 0.8/0.9 uvnitř; obě fáze se hlásí
až po `Orchestrator initialized`.

Navazující úprava (d.508b): FAILED řádek fází 0.8 a 0.9 píše jen JEDNA kolej.
Obě fáze si ho zapisovaly samy a chybu přeposlaly dál, takže chybová kolej bootu
— ta, která uklízí a končí exitem 1 — zapsala pro tutéž událost FAILED podruhé;
operátor četl `[FÁZE 0.9] Workflow Listener Startup - FAILED` dvakrát na jedno
odmítnutí (`change-discipline.md` § One rail per concern). Fázi nese nově sama
chyba (`_bootPhaseError` → `error.bootPhase`), takže `initialize()` ji ohlásí
jednou a pod číslem té fáze, která skutečně skončila: 0.8 pro odmítnutou
deklaraci front i pro registraci bez certifikátu, 0.9 pro odmítnutého konzumenta.
Do d.508b se v obou případech hlásila 0.9, protože číslo bylo na volajícím
místě natvrdo. Chyba bez značky se nepojmenovává odhadem — jde na cestu pro
neklasifikované chyby (`INIT`, trvalá). Úspěšná cesta píše STARTING/PASSED beze
změny.

### Removed — **BREAKING** `handling.mqClient`: druhá deklarace po témže zrušeném čtenáři (d.502)

Wrapper přikládal na objekt `handling`, který předává
`@onlineapps/conn-infra-error-handler`, ještě vlastní klíč `mqClient`
s komentářem „For DLQ routing". Konektor ten objekt podává beze změny jádru
(`ErrorHandlerCore`), a tam byl jediným čtenářem `DLQRouter` — zrušený v d.473
se všemi klíči, které četl jen on (`handling.dlqEnabled`, `handling.mqClient`,
`dlqNamePattern`, `emitMonitoringEvents`). `dlqEnabled` vyřadila d.493; tohle je
táž deklarace bez čtenáře, jen druhá z dvojice. Čtyři otázky
§ Removing something removes its declaration zodpovídá CHANGELOG jádra
(`shared/error-handler-core/CHANGELOG.md`, sekce `Removed — DLQRouter`),
rozhodnutí je vlastníkovo
(`docs/governance/confirmations/mq-consumer-contract.md` 002 bod 3).

Změřeno 2026-09-15: `mqClient` má nula výskytů v `shared/error-handler-core/src`
i v `shared/connector/conn-infra-error-handler/src`. Objekt `handling` teď nese
přesně to, co služba deklarovala v `wrapper.errorHandling`, a nic navíc.

Migrace: nikoho se netýká — klíč nepsala služba, přikládal ho wrapper.
`this.mqClient` nemizí, orchestrátor ho dostává dál (`OrchestratorConnector.create`).
Jen odmítnut jménem být nemusí, na rozdíl od `dlqEnabled`: nikdy nebyl klíčem
konfigurace, takže ho žádný `config.json` nemůže napsat.

### Removed — **BREAKING** `wrapper.errorHandling.dlqEnabled`: klíč, který po zrušení `DLQRouter` nikdo nečte (d.493)

`dlqEnabled` byl šestý z klíčů, které tenhle balíček předává do
`@onlineapps/conn-infra-error-handler`. Jeho jediným čtenářem byl `DLQRouter`
v `@onlineapps/error-handler-core`, a ten zanikl i s klíčem — čtyři otázky
§ Removing something removes its declaration zodpovídá CHANGELOG toho balíčku
(`shared/error-handler-core/CHANGELOG.md`, sekce
`Removed — DLQRouter`), rozhodnutí je vlastníkovo
(`docs/governance/confirmations/mq-consumer-contract.md` 002 bod 3). Odkládání
mrtvé zprávy má jednu kolej: konzumentskou politiku `@onlineapps/mq-client-core`
(počítadlo pokusů v hlavičce, `nack(requeue=false)` do `<služba>.dlq`, jedna
událost `message_dlq`).

Klíč tedy **není jen vypuštěn ze seznamu, je odmítnut jménem** — stejnou kolejí
jako `wrapper.registry.heartbeatInterval` (`_assertRetiredDlqEnabledKey`).
Konfigurace, která ho dál píše, tvrdí, že dead-lettering zapíná nebo vypíná,
zatímco ho nikdo nečte; to je falešná záruka, kterou `automation-gates.md` §5
označuje za vadu. Není to kompatibilní cesta (`architecture-principles.md` §11):
nic neobnovuje, jen jmenuje jeden řádek ke smazání.

Migrace: smazat `wrapper.errorHandling.dlqEnabled` z `config/service/config.json`.
Změřeno 2026-09-15: klíč nepíše žádná služba (`api_biz/*`, `api/infra`,
`fe_adminui` — nula výskytů), takže migrace se nikoho nedotkne.

### Changed — `API.md` zregenerováno ze zdrojů (d.489)

Generovaná reference chyběla `HandlerContractError` a `WRAPPER_RESERVED_ERROR_CODES`; od d.489 ji hlídá brána `npm run docs:api:check` (`scripts/ci/verify-generated-api-docs.mjs`).

### Changed — boot nečeká na broker, který už odpověděl „ne" (d.295)

Smyčka `_initializeMQ()` opakovala `connect()` podle `wrapper.mq.connectionRetries`
s exponenciálním backoffem u **každé** chyby. To je správně pro broker, který
ještě neběží, a špatně pro **odpověď**: 406 PRECONDITION-FAILED (fronta existuje
s jinými argumenty) i 403 ACCESS-REFUSED vrátí každý další pokus identicky, takže
služba proseděla N × backoff na cestě k selhání, které už měla v ruce
(`architecture-principles.md` §4). Změřeno před opravou: 3 pokusy u 406 i u 403;
proti živému brokeru s nepřijatelnými přihlašovacími údaji 1665 ms a dvě ohlášená
čekání, po opravě 144 ms a žádné.

Smyčka se teď přeruší na `err.code === 'MQ_CONNECTION_FATAL'` a **vyhodí původní
chybu beze změny** — ta nese slova samotného brokera a jmenuje credentials, vhost
a topologii jako fix, kdežto věta o vyčerpaném rozpočtu posílá čtenáře ověřovat
dostupnost `api_services_queuer`, což u odmítnutí není to místo.

Verdikt se **nečte znovu**: otázku „odpověděl broker a odmítl?" vlastní
`@onlineapps/mq-client-core` (`REFUSAL_REPLY_CODES`, `_isBrokerRefusal()`) a od
d.295 ji odpovídá i na cestě prvního `connect()`. Seznam AMQP kódů v tomhle
balíčku by byl druhá kopie téhož seznamu (`change-discipline.md` § One rail per
concern).

Fáze 0.3 navíc hlásí odmítnutí jako **permanentní**: `_handleInitializationError`
dostávalo natvrdo `isTransient = true`, takže by o odpovědi, kterou žádný restart
nezmění, tvrdilo „a restart against a recovered dependency can succeed". Čte se
tatáž značka, ne druhé pravidlo.

## [8.1.1] — 2026-09-15

## [8.1.0] — 2026-09-15

### Added

- **`toCallerEnvelope(err, context)` — obálka chyby pro volajícího jako čistá
  funkce, bez logu** (d.424). `ErrorMapper.map()` obálku staví **a zároveň
  loguje**; to je správně pro cestu, kde chybu zpracovává wrapper. Služba, která
  chybu zpracovává sama — uloží text, kterým odpověděla volajícímu, a sama ji
  loguje — dosud musela kvůli obálce instanciovat `ErrorMapper`, takže každé 5xx
  bylo v logu dvakrát (změřeno BIZ-converter 2026-09-14,
  `api_biz/converter/src/services/conversion.service.js` `storedFailureText`).
  Nová funkce je bez vedlejšího efektu: žádný logger v parametrech, žádný řádek
  do logu. Exportovaná z indexu i z lehkého vstupu
  `@onlineapps/service-wrapper/errors`.

- Mapování **není zdvojené**: `map()` i `toCallerEnvelope()` vedou na jednu
  vnitřní tabulku (`describeFailure`), která vrací obálku i řádek logu; `map()`
  ten řádek zapíše, `toCallerEnvelope()` ho zahodí. Chování `map()` se nemění —
  stejné stavy, stejné maskování 5xx, stejná úroveň i text logu, stále právě
  jeden řádek na volání (`.claude/rules/change-discipline.md` § One rail per
  concern).

## [8.0.0] — 2026-09-14

### Kaskáda vlny 8.0.0 — piny knihoven, které wrapper drží (A3, d.245c, d.406, d.443)

Reakce na Breaking odrážky knihoven vydaných 2026-09-14. Každá položka cituje
odrážku, která ji vynutila.

- **Klíč `queue` v konfiguraci MQ klienta je pryč** — `@onlineapps/mq-client-core`
  3.0.0, „the configuration schema ends with `additionalProperties: false`", plus
  `@onlineapps/conn-infra-mq` 3.0.0, které `queue` nedeklaruje („a key NEITHER
  side declares is now refused at construction"). Wrapper psal
  `queue: '<service>.workflow'` a konstrukce klienta tím končila jménem klíče
  (`[BaseClient] Configuration key not declared by this client: "queue"`, změřeno
  na integračním patře před změnou). Jméno se stejně nikdy nečetlo: každý
  `publish()`/`consume()` wrapperu frontu jmenuje ve volání a deklarované jméno
  skládá `queueConfig.queueName()` z jádra ze `serviceName`, který wrapper předává
  dál.

- **Mapa `INFRA_QUEUE_OWNERS` zrušena.** Čtyři otázky `change-discipline.md`
  § Removing: **(1)** vznikla v `27029c91`/`8c60744e` jako tabulka „kdo vlastní
  kterou platformní frontu" pro hlášky bránění infrastruktury; **(2)** část
  koncepce, kterou nesla — jména platformních front mají jednoho vlastníka —
  **stojí dál**, jen ten vlastník je `queueConfig` v `@onlineapps/mq-client-core`
  (`infrastructurePrefixes()`, `queueName()`), ne lokální kopie; **(3)** nečetl ji
  nikdo: jediný výskyt v celém balíčku byla její vlastní deklarace (změřeno
  2026-09-14 nad `src`, `tests`, `README.md`, `CHANGELOG.md`, `API.md`, `docs`,
  `config`), takže i kdyby zastarala, nikdo by se to nedozvěděl
  (`automation-gates.md` §5); **(4)** náhrada je koncepčnější — jádro jména
  odvozuje z deklarací, takže zná i `workflow.delivery_requested` (d.387), které
  tahle mapa neznala a znát nemohla.

- **Tajemství: `masterKeyBase64` → `masterKeys`, `SECRETS_MASTER_KEY` →
  `SECRETS_MASTER_KEYS`** — `@onlineapps/conn-infra-secrets` 2.0.0,
  „`SecretsConnector` takes `masterKeys` instead of `masterKeyBase64`" (rozhodnutí
  vlastníka `secretbox-master-keys` 001). Klíč prostředí nese deklarace
  `api/config/shared-env.json` a ta říká, že hodnota žije jen v
  `config/env-active/shared.env`, takže volání `requireEnv` ten soubor jmenuje
  (`{ file: 'shared.env' }`, čtený od `@onlineapps/service-common` 3.0.0).
  Konfigurační klíč `wrapper.secrets.masterKey` se jmenuje `masterKeys` — jeden
  klíč a seznam klíčů nejsou totéž.

- **Kadence heartbeatu jde do konstruktoru registru** (d.406) —
  `@onlineapps/conn-orch-registry` 4.0.0, „`heartbeatInterval` and
  `specificationEndpoint` are REQUIRED": knihovna zrušila vlastní default 10000,
  protože default vedle nakonfigurované hodnoty je druhá kadence. Wrapper ji čte
  z platformní proměnné `BIZ_HEARTBEAT_INTERVAL_MS` (potvrzení
  `biz-health-freshness` 001 bod 3 — viz Breaking níž) **před** stavbou klienta
  a tutéž hodnotu dává své smyčce, takže se klient a smyčka nemohou rozejít.
  Smyčka `startHeartbeat()` ve wrapperu **zůstává**: kadenci vlastní prostředí,
  ale zastavit službu po `wrapper.registry.maxHeartbeatFailures` selháních v řadě umí jen ona
  (`_shutdownFatally`), kdežto `registryClient.startHeartbeat()` selhání pouze
  loguje a nikdy se nevzdá. Viz „Co tahle dávka NEudělala".

- **Odmítnutá registrace říká proč** (d.406) — `@onlineapps/conn-orch-registry`
  4.0.0, „`register()` and `deregister()` return what actually happened":
  `validationStatus` a `instructions` knihovna nově propouští doslova. Wrapper je
  čte a píše do hlášky, takže odložená registrace §2.5
  (`awaiting-prevalidation`), která žádný `errorCode` nenese, ukáže operátorovi
  stav i příkaz, který ji rozváže. Za chybějící pole se nedosazuje nic.
  `unregister()` se volá bez argumentu (4.0.0 žádný nebere) a jeho návrat se
  nečte — 4.0.0 zrušilo natvrdo vracené `{ success: true }`, protože registr na
  odregistraci neodpovídá, takže tu žádný výsledek k hlášení není.

- **Lookup operace vrací i `sensitive_input_fields`** (d.443) —
  `@onlineapps/conn-orch-orchestrator`, „`operationMutatesLookup` je povinná
  závislost a nese `sensitive_input_fields`". Pole se čte z téhož záznamu
  `operations.json` jako `mutates` a `resource_type`; bez něj je scrub §8
  nečinný a tajemství projde celým během, aniž to kdokoli ohlásí. Operace, která
  nic nedeklaruje, odpovídá `undefined` — nikdy vymyšleným prázdným seznamem.

- **AMQP URL v hlášce bez credentialu** (d.245c) — `@onlineapps/mq-client-core`
  3.0.0, „the credential redaction removes the WHOLE userinfo, not just the
  password". Jediné místo wrapperu, které URL renderuje (hláška po vyčerpání
  bootovacího rozpočtu), jde přes `redactUrl` z `@onlineapps/logger-contract`
  (vlastník funkce od d.453, odrážka níže), tedy z vlastní deklarované závislosti
  balíčku a se stejnou sémantikou, kterou jádro vystavuje svým závislým. Spojení dál používá původní URL — redakce se
  týká výhradně renderu.

- **Šest defaultů `wrapper.errorHandling` odešlo k vlastníkovi.** `|| 3`,
  `|| 1000`, `|| 2` a tři `!== false` byly druhá kopie hodnot, které
  `@onlineapps/error-handler-core` deklaruje a dokumentuje (`RetryHandler`
  destrukturalizačními defaulty, `ErrorHandlerCore` týmž `!== false`). Klíč, který
  služba nenapíše, se nepředává vůbec; napsaná `0` nebo `false` dojde beze změny
  (`||` explicitní nulu přebíjelo). Klíč mimo těch šest se nepředává také — v
  jádru nic nekonfiguruje a tiché propuštění by překlep schovalo. README
  § Configuration ty klíče jmenuje, hodnoty defaultů neopisuje.

- **`wrapper.timeout` z konfigurace, ne z literálu.** `defaultTimeout` je pro
  `@onlineapps/conn-orch-orchestrator` povinný, takže hodnota patří do
  konfigurace: ships v `config/runtime-defaults.json` téhle knihovny, kterou
  ConfigLoader podkládá pod každý service config — táž kolej, po které už jedou
  `wrapper.mq.*`. Ruční config bez loaderu klíč jmenuje, nebo se boot zastaví
  jménem klíče.

- **Zdravotní kontrola před krokem workflow se ptá klienta, ne jeho transportu.**
  `BaseClient.performHealthCheck()` je od `@onlineapps/mq-client-core` 3.0.0
  veřejné API („`isConnectionFatal()` and `performHealthCheck()` — the last two
  methods a service could not reach"), takže sáhnutí na `this.mqClient._transport`
  odtud zmizelo. `_registerMQHooks()` ho drží dál: `onChannelClose(type, callback)`
  je v 3.0.0 definován jen na transportu a veřejnou náhradu nemá (změřeno nad
  pinovanou instalací).

- **Hláška o chybějícím `onFatal` říká pravdu.** Tvrdila, že metoda „is missing
  from @onlineapps/conn-infra-mq"; vlastníkem je `BaseClient` v
  `@onlineapps/mq-client-core` a `conn-infra-mq` ji dědí. Brána zůstává — je to
  kontrola na švu, ne deklarace — ale jmenuje správného vlastníka a správnou
  nápravu.

- **Redakce přihlašovacích údajů v URL se bere z `@onlineapps/logger-contract`**
  (d.453). `redactUrl` má jednoho vlastníka a je jím balíček, který definuje, co
  je logger — redakce je to, co potřebuje řádek logu. Wrapper ji do teď bral ze
  stejnojmenné kopie v `@onlineapps/service-common`; dvě implementace jednoho
  pojmu se rozejdou na případu, který nikdo netestoval
  (`change-discipline.md` § One rail per concern). Výstup se nemění — co se mění,
  je volaná funkce, a to tvrdí `tests/unit/ServiceWrapper.redactUrlOwner.test.js`
  sentinelem. Závislost na `@onlineapps/service-common` zůstává: wrapper z ní čte
  `requireEnv`, `requireNumberEnv`, `createRedisClient` a `waitForInfrastructureReady`.

- **Sonda zákazu `F002:heartbeat-queue` míří na živou kotvu.**
  `api/config/biz-docs-lint.json` dokládala zákaz literálem
  `infrastructure.health.checks` ve `ServiceWrapper.js`; po zrušení
  `INFRA_QUEUE_OWNERS` wrapper žádné jméno fronty sám nepíše, takže se zákaz
  nahlas vypnul (`doc-code-binding.md` §3) a validátor hlásil `D-RETIRED` jako
  notRun. Sonda teď čte to, co zákaz opravdu tvrdí: volání
  `createBaseClientAdapter` z `@onlineapps/infrastructure-tools` — důkaz, že
  heartbeat zdraví publikuje sdílený publisher, a ne registry klient přes
  `registry.register`.

### Fixed — mez vzdání se heartbeatu je deklarovaná hodnota, ne inline pětka

- **`wrapper.registry.maxHeartbeatFailures || 5` je pryč** (`architecture-principles.md`
  §3). Hodnota je deklarovaná v `config/runtime-defaults.json`, který ConfigLoader
  vmíchá pod každou konfiguraci služby, a wrapper ji čte toutéž kolejí jako své
  sourozence (`_requiredRegistrySetting`, sourozenec `_requiredMqSetting`).
  Chybí-li klíč, boot fail-fastne a jmenuje ho; `0`, záporná a neceločíselná mez
  se odmítají — mez `0` by službu zastavila na prvním zaškobrtnutí, což je opak
  toho, k čemu mez je. Staré `|| 5` navíc explicitní `0` tiše přepsalo na 5.

- **Sada `ServiceWrapper.lifecycleExits.test.js` už nenese vyřazený klíč.**
  Konfigurace v ní psaly `heartbeatInterval: 1000`; klíč je od d.406 odmítaný
  jménem, ale strážce `_assertRetiredHeartbeatIntervalKey()` běží uvnitř
  `_initializeRegistry()` a tahle sada volá `startHeartbeat()` přímo — vyřazený
  klíč tak seděl přesně tam, kam brána nedosáhne, a sada zůstávala zelená.
  Kadence do smyčky přichází argumentem, takže klíč byl i inertní.

### Breaking — kadenci heartbeatu vlastní prostředí, konfigurační klíč vyřazen (d.406, conf `biz-health-freshness` 001)

- **`BIZ_HEARTBEAT_INTERVAL_MS` je povinná proměnná prostředí.** Wrapper ji čte
  ve FÁZI 0.7 toutéž kolejí jako ostatní sdílené klíče
  (`@onlineapps/service-common`, `{ file: 'shared.env' }` — jako `REDIS_URL`
  a `SECRETS_MASTER_KEYS`), takže hláška jmenuje klíč i soubor: `[ServiceConfig]
  Missing environment variable - BIZ_HEARTBEAT_INTERVAL_MS is required. … Fix:
  set BIZ_HEARTBEAT_INTERVAL_MS in config/env-active/shared.env.`. Nulová nebo
  záporná hodnota boot zastaví taky, vlastní hláškou wrapperu. Převod na číslo
  vlastní resolver, ne druhý parser tady (`change-discipline.md` § One rail per
  concern).

- **Konfigurační klíč `wrapper.registry.heartbeatInterval` a runtime default
  30000 jsou pryč**, a konfigurace, která klíč pořád nese, boot **zastaví jménem
  klíče** — ne tiše ignoruje (`architecture-principles.md` §11, čistý řez;
  ignorovat by znamenalo konfiguraci, která tvrdí, že kadenci nastavuje, zatímco
  ji nikdo nečte — `automation-gates.md` §5). Čtyři otázky
  `change-discipline.md` § Removing: **(1)** klíč vznikl jako lokální nastavení
  tempa beatu a default 30000 k němu přibyl v `config/runtime-defaults.json`,
  aby služba bez klíče vůbec nabootovala; **(2)** koncepce, kterou nesl —
  „kadence má jednoho vlastníka" — **stojí dál**, jen ten vlastník je podle
  rozhodnutí vlastníka `biz-health-freshness` 001 bod 3 sdílená proměnná
  prostředí, protože ten, kdo beat vydává, a ten, kdo jeho čerstvost soudí, jsou
  různé procesy; **(3)** dnes ho nečetl nikdo než wrapper sám: v `api_biz/**` ho
  nemá žádná služba (změřeno 2026-09-14, nula výskytů `heartbeatInterval`),
  v šabloně `templates/business-service/**` také ne; **(4)** náhrada je
  koncepčnější — z jedné hodnoty odvozuje registr práh staleness
  (`BIZ_HEARTBEAT_INTERVAL_MS` × `BIZ_HEARTBEAT_STALE_FACTOR`), takže změna
  kadence nemůže dojít jen k jedné straně, kdežto klíč v konfiguraci služby
  druhou stranu nikdy neinformoval.

- **Smyčka `startHeartbeat()` i klíč `maxHeartbeatFailures` zůstávají**: kadenci
  vlastní prostředí, ale zastavit službu po N selháních v řadě
  (`_shutdownFatally`) umí jen tahle smyčka — `registryClient.startHeartbeat()`
  selhání pouze loguje.

- **Příklad `config.json` v README už nenese `registry.url`** — klíč vyřadilo
  potvrzení `biz-discovery-redis` 002 (registrace jde přes MQ, discovery čte
  Redis projekci), takže ho příklad nesmí učit dál. Nová sekce
  README § `wrapper.registry` a řádek v `docs/CONFIGURATION_GUIDE.md` říkají, co
  z registrového bloku zbývá.

### Fixed

- **Červené sady po pinech.** `requireEnvOwningFile` tvrdila PIN na
  `service-common 2.0.1` (arita 2, třetí argument ignorován); 3.0.0 argument čte,
  takže případ měří nové chování hodnotou — obě znění hlášky, s vlastnícím
  souborem i bez něj. `ServiceWrapper.serviceUrl` a
  `orchestrator-no-service-url.integration` dávaly atrapu monitoringu, která neumí
  zapsat řádek; `@onlineapps/conn-infra-error-handler` 2.0.0 takovou instanci
  odmítá v konstruktoru, tak atrapa nově `error()` má (zeslabit assert by schovalo
  právě tu díru, kterou 2.0.0 zavřelo). `ServiceWrapper.heartbeatFamily` ztratila
  `it.failing` u `version`: s pinem `@onlineapps/infrastructure-tools` 2.0.0
  knihovna verzi čte z konfigurace místo razítka `1.0.0`, jak si komentář toho
  případu sám vyžádal.

- **`shutdown-closes-telemetry.integration` tvrdí, co dřív jen zaznamenala.**
  Změřená mez d.398 padla s pinem `@onlineapps/monitoring-core` 2.0.0: smyčka po
  `shutdown()` **doběhne** (`atDrain.handles` je prázdné) a knihovna neinstaluje
  vlastní signal handler (`{ SIGTERM: 0, SIGINT: 0 }`, u běhu bez shutdownu jako
  kontrolní případ). Poslední řádek se čte jako řádek, ne jako konec řetězce —
  2.0.0 píše strukturovanou obálku, takže značka sedí v jejím `message`.

- **Sada `specificationEndpoint` po sobě neuklízela.** Každý případ nechával
  živý 30s interval heartbeatu a běh sady samotné se musel ukončit silou
  (změřeno i nad `HEAD` před touhle dávkou, takže únik je starší); teď se timer
  po každém případu ruší.

### Co tahle dávka NEudělala, a proč

- **Smyčka heartbeatu ve wrapperu nebyla zrušena ve prospěch
  `registryClient.startHeartbeat()`.** Ta metoda selhání jen loguje — nemá
  počítadlo ani mez — kdežto smyčka wrapperu po `wrapper.registry.maxHeartbeatFailures`
  selháních v řadě zastaví službu přes `_shutdownFatally()`. Zrušit ji by tiše
  smazalo chování, které popisuje `docs/standards/SYSTEM_BOOTSTRAP_RESILIENCE.md`,
  `README.md` § Two retry budgets i `docs/CONFIGURATION_GUIDE.md`, a osiřel by
  klíč `maxHeartbeatFailures`. Kadence přesto má jednoho vlastníka: tatáž hodnota
  jde do konstruktoru klienta i do smyčky.

- **Lokální `isPositiveIntegerId` v `ContextBuilder.js` zůstává.**
  `@onlineapps/service-common` 3.0.0 ho neexportuje a v jeho `src` není (změřeno
  nad instalovaným balíčkem i nad stromem), takže „jediná definice ve
  service-common" dnes neexistuje a přesun by byl import neexistujícího symbolu.


### Handler nesmí vyhodit kód, který vyrábí wrapper (d.420)

Kontrakt chyb (`api/docs/biz/70-contracts/error-handling.md` §1 tabulka +
§4, §2) vyhrazuje wrapperu dva kódy: `INVALID_ENVELOPE` („Thrown by the
wrapper — see §4"; §4 dodává „both are raised before the handler runs") a
`UNKNOWN_OPERATION` (§2: „the wrapper raises them about its own dispatch").
`api/docs/biz/10-invocation/handler-dispatch.md` je jmenuje jednou větou:
„Two of those codes are the wrapper's own, not a handler's". Hlídal to jen
review (§8 „Enforced by review only") — a review nevidí handler v cizím repu.

Cena za to není kosmetická. `_classifyDeliveryError()` čte osud zprávy
z `err.code` a `INVALID_ENVELOPE` je ten kód, který dělá doručení
`permanent`: broker odsune zprávu do `<service>.dlq` hned při prvním
selhání a log tvrdí, že volající poslal vadnou obálku. Handler, který ten
kód vyhodí, si tím **sám odešle zprávu do DLQ** a vinu podepíše volajícímu
— vlastní vada zmizí pod cizím jménem a autor cookbooku je poslán hledat
chybu v obálce, která byla v pořádku. (Nález BIZ-emailer 2026-09-08.)

- **`ServiceWrapper._invokeHandlerUnderContract()`** — jedno místo, kudy
  prochází každé volání handleru. Chyba handleru s vyhrazeným kódem se
  nahradí typovanou `HandlerContractError`:
  `[ServiceWrapper] Handler threw a reserved error code - <code> is produced
  by the wrapper only. Fix: …` Původní chyba zůstává v `cause`.
- **Nebrandovaná a bez vlastního `code`** — není to obchodní výsledek, je to
  naše vada, takže ji `ErrorMapper` vede větví neznámé chyby: `error` log
  **se stackem** a maskovaných `500 INTERNAL_ERROR` volajícímu
  (error-handling.md §5). Vlastní kód by byl platformní identita, a ta se
  přidává změnou kontraktového uzlu, ne tím, že si ji knihovna razí sama.
- **Kontroluje se KÓD, ne třída.** Handler si `InvalidEnvelopeError` může
  naimportovat z exportů tohoto balíčku a týž kód unese i holá `Error` —
  klasifikátor čte `err.code` a nic jiného.
- **Kdo chybu vyhodil, říká značka původu** (`src/wrapperRaised.js`,
  `Symbol.for('onlineapps.wrapperRaisedError')`, neenumerovatelná). Nese ji
  ta dvě místa, kde wrapper vyhazuje vyhrazený kód, zatímco handler běží:
  `ctx.secrets.get()` odmítající obálku bez tenancy (`ContextBuilder`) a
  `HandlerRegistry.invoke()` odmítající neregistrovanou operaci. Obě se
  nezměnily — dál `INVALID_ENVELOPE` / `UNKNOWN_OPERATION`, dál `permanent`
  tam, kde to platilo.
- **`CLIENT_CANCELLED` ve vyhrazené množině vědomě NENÍ**: `ErrorMapper` ho
  přiřazuje podle `name` mimo brand kontrakt, protože rozpoznávat má
  `DOMException` z Node `AbortController` — a tu handler, který čeká na
  zrušené volání, propaguje právem.
- Nehnulo se: pravá nevalidní obálka dál `InvalidEnvelopeError` → 400 →
  `permanent` → `.dlq` na první pokus; běžná `BusinessError` handleru dál
  se svým kódem a `transient`.


### Zdraví DB se čte z předané handle, ne z názvu souboru (d.415)

**Breaking.** `_loadDatabaseHealthConnector()` dělal
`require(<serviceRoot>/src/config/database)` a `authenticate()` volal na tom, co
ten soubor vyexportoval. Jenže **týž soubor uniforma ZAKAZUJE**: řádek
`X-DB-CONFIG` manifestu služby (rozhodnutí vlastníka
`api/docs/governance/confirmations/biz-service-manifest.md` 009, severity
`deploy`) ho jmenuje jako druhou kolej jedné koncepce — připojení má jediného
vlastníka, `createSequelize()` z `@onlineapps/conn-base-db`
(`api/docs/biz/70-contracts/database-contract.md` § 10).

Služba, která uniformu splnila, tím pádem hlásila `db: 'unhealthy'` a readiness
`not_ready` nad zdravou databází. Změřeno živě na devu (BIZ-meta), **se všemi
sadami tohoto balíčku zelenými** — žádný test tuhle cestu nikdy nepustil, takže
by to po pinu 8.0.0 potkalo každou službu v kaskádě.

- **Wrapper připojení neotevírá a nehledá.** Instanci, kterou si služba staví
  při bootu, dostane **předanou** pod klíčem `db` — `bootstrap(__dirname,
  { db: sequelize })`, nebo `new ServiceWrapper({ …, db })`. Volá na ní jen
  `authenticate()`, kvůli `components.db` v heartbeatu.
- **Tvar se ověřuje tam, kde handle přichází** (konstruktor): co nemá
  `authenticate()`, je odmítnuto hned, ne až prvním heartbeatem.
- **Deklarace a boot se musí shodnout — obousměrně**, jinak je to trvalé selhání
  FÁZE 0.15 a boot skončí:
  `requiredConnectors.db: true` bez handle → `[ServiceWrapper] Database handle is
  required - …` (dřív tiché `unhealthy` navždy);
  `requiredConnectors.db: false` s předanou handle → `[ServiceWrapper] Database
  handle was injected but not declared - …` (kontrakt lže o službě).
- **Vazba podle jména souboru je pryč** i s `_loadDatabaseHealthConnector()`
  a polem `_dbHealthConnector`. Chování řízené *názvem* souboru zakazují
  principy 9 (Configuration over Convention) a 8 (Explicit over Implicit) —
  stejný vzor jako u loggeru (d.390).
- Nehnulo se: `ctx.db` zůstává `null` (přístup k DB neteče kontextem,
  database-contract § 10); služba bez databáze (`db: false`, nic nepředáno) bootuje
  dál a hlásí `not_required` / `unknown`; pořadí fází beze změny.

**Co to vyžaduje od služeb** (pin 8.x): kdo má `requiredConnectors.db: true`,
předá při bootu `{ db: <instance z createSequelize()> }` — jinak služba
nenaběhne. Je to táž instance, kterou už importují handlery, žádná nová.

### `amqplib` je deklarovaná závislost testovacího tieru (d.412)

Integrační sada sonduje brokera přímo přes `amqplib` — nezávislý pozorovatel
mimo SUT, který rozhoduje o osudu doručení (`correlationIdOneRail`,
`envelopeRejection`) — ale balíček ji od d.349 nedeklaroval. `devDependencies`
ji nově nese s přesným pinem `0.10.9`, tedy toutéž verzí, kterou instaluje
`@onlineapps/mq-client-core`; druhá kopie ve stromu by testu dovolila pozorovat
jiného klienta, než jaký běží pod testem. Navíc `tests/unit/connectorDoubles.test.js`
skládal argument `require.resolve()` za běhu, takže brána G6 nedokázala přečíst
seznam závislostí testovacího tieru vůbec a hlásila UNCERTAIN za celý balíček —
tabulka dvojníků teď nese literální jméno v thunku a `check-package-deps.mjs`
odpovídá PASS. Kontrolní případ (dvě jména nesdílejí jeden soubor dvojníka)
zůstal beze změny.

### `REDIS_URL` říká operátorovi, do kterého souboru ho zapsat (d.399b)

Tři místa, kde wrapper čte `REDIS_URL` sám — infrastrukturní brána
(`_waitForInfrastructureGate`), objevování registru a projekce tajemství —
předávají nově `{ file: 'shared.env' }`. `@onlineapps/service-common` od d.399
skládá z toho větu, kterou `architecture-principles.md` §5 vyžaduje:
`Fix: set REDIS_URL in config/env-active/shared.env.` Do té doby hláška končila
popisem a operátor se nedozvěděl, kam sáhnout.

Jméno souboru není odhad: `api/config/shared-env.json` nese `REDIS_URL` jako
sdílený klíč, a nová sada `tests/unit/requireEnvOwningFile.test.js` to porovnává
proti deklaraci, ne proti paměti.

**Dnes je změna nečinná, a je to změřené, ne předpokládané.** Balíček pinuje
publikovanou `service-common 2.0.1`, jejíž `requireEnv` má aritu 2 a třetí
argument tiše ignoruje — hodnota se vrací stejně, hláška zůstává beze změny.
Věta `Fix:` se objeví, až pin povyskočí na vlnu s d.399. První případ nové sady
to tvrdí hodnotou (`requireEnv.length === 2` a doslovné znění hlášky z 2.0.1),
takže den, kdy se pin pohne, pozná sada, ne provoz.

**`SECRETS_MASTER_KEY` soubor ZÁMĚRNĚ nedostal.** Deklarace `shared-env.json` ho
nenese a nenese ho ani jedna šablona biz služby; stojí jen v
`config/env-templates/gateway.env` a `delivery.env`, tedy u dvou infra služeb,
které wrapper nepoužívají. Napsat sem `shared.env` by znamenalo vymyslet si fakt,
který deklarace netvrdí — a přesně o to jde: soubor je VSTUP z místa volání
ověřitelný proti deklaraci, ne druhá mapa. Bez souboru zůstává obecné znění §5,
což je správně, ne chybějící. Že klíč, který wrapper při bootu vyžaduje, nemá
v žádném souboru biz služby deklaraci, je otevřený nález pro vlastníka šablony.

### `shutdown()` zavírá telemetrický transport — jako POSLEDNÍ krok (d.398)

**Oprava chování, ne breaking change.** `shutdown()` uvolnil heartbeat, konzumery,
discoveryRedis, registr, cache, state, secrets, MQ i druhé MQ spojení registru —
a `this.monitoring` se nedotkl nikdy (změřeno: v celé sekvenci nula volání
`monitoring.shutdown()`). Nevadilo to, dokud si `@onlineapps/monitoring-core`
bral vlastní `SIGTERM`/`SIGINT` handlery a transport zavíral za wrapperem. Dávka
d.395 (`d68555d1`) je odebrala — knihovna si procesní handlery nebere
(`mq-client-lifecycle-contract` 001) — a od té chvíle ho nezavíral nikdo. Proces
sice skončí (`bootstrap()` volá `process.exit(0)`), ale **exit není flush**:
poslední řádky logu a spany odejdou s ním.

- **`await this.monitoring.shutdown()` je poslední krok sekvence**, za odpojením
  MQ i za řádkem `ServiceWrapper shutdown complete`. Pořadí není detail:
  `this.logger` JE `this.monitoring` (FÁZE 0.12), takže každý řádek, který
  teardown píše, jde skrz objekt, který tenhle krok zavírá — zavřít ho dřív
  znamená umlčet záznam o tom, co se právě uklízí.
- **Selhání zavření se zaloguje a sekvenci nezastaví.** Je to poslední krok,
  není co opustit, a volající (`SIGTERM` handler v `src/index.js`) hned nato
  proces ukončí; výjimka odtud by z nedokončeného flushe udělala teardown, který
  vypadá jako selhaný. Hláška má tvar `[Context] Problem - Fix`.
- **Wrapper, jehož boot se k FÁZI 0.12 nedostal, nemá co zavírat** — je to stav,
  ne fallback: `_handleInitializationError` uklízí touž metodou pro každou fázi
  za 0.12.
- Ověřeno, že `@onlineapps/conn-base-monitoring` deleguje na `shutdown()`
  monitoring-core (`src/index.js`: `MonitoringConnector.shutdown()` →
  `this.core.shutdown()` a `api.shutdown = () => connector.core.shutdown()`) —
  jedno jméno po d.395, žádné `close()`. Druhý commit tedy nebyl potřeba.
- Kontrolní případy, které se nehnuly: chyba kroku PŘED transportem se dál
  propaguje (`Error during shutdown` + throw); množina uvolněných konektorů,
  nulování consumer tagů i `isInitialized = false` beze změny.

**Změřená mez, zapsaná místo zamlčení.** Integrační sonda běží proti živému
brokeru, ale tenhle balíček instaluje `@onlineapps/conn-base-monitoring 1.0.17` →
`@onlineapps/monitoring-core 1.1.1`, tedy verzi publikovanou PŘED d.395. V ní
`InfrastructureLogger.shutdown()` testuje `if (this.logger.shutdown)` na třídě,
která má `close()`, takže amqp spojení loggeru se nezavře. Změřeno na sondě: po
`wrapper.shutdown()` zůstává jeden reffed `Socket` na port brokeru, proces
nevyprázdní smyčku a `process.listenerCount('SIGTERM')` je 1. Tvrzení „smyčka se
vyprázdní" proto zdejší sada NEtvrdí — patří monitoring-core a jeho vlastní sadě,
kde je proti zdroji už ověřené; tady bude dosažitelné, až vlna tu opravu
publikuje.

### Sdílený logger nastavuje wrapper sám ve FÁZI 0.12; vazba podle jména souboru pryč (d.390)

**Breaking.** `src/logger.js` je jediný vlastník dvojice `setLogger`/`getLogger`,
ale **uvnitř `ServiceWrapper.js` ji nikdo nevolal** (změřeno: `grep -c setLogger
src/ServiceWrapper.js` → 0). Naplnil ji až `bootstrap()`, a to **po návratu
`wrapper.initialize()`** — tedy o fázi později, než je použitelná: FÁZE 0.2
(Tier-1) pouští vlastní cookbooky služby, takže cokoli, co tam logovalo přes
`getLogger()`, dostalo `[Logger] Logger not initialized` a shodilo boot. Přesně
to si BIZ-converter a BIZ-pdfgen 2026-08-29 zapsaly jako důvod, proč nemohou
smazat svou kopii `src/lib/logger.js`: kopie odpovídají konzolovým fallbackem,
takže mezeru zakrývaly místo zavření.

- **FÁZE 0.12 předá `this.logger` sdílené proxy hned po jejím vzniku**, uvnitř
  téhož `try` — proxy, která instanci odmítne, je selhání 0.12, trvalé, stejně
  jako monitoring, který nenastartoval (konfirmace `wrapper-boot-logger` 001).
  Logger je tak živý dřív, než poběží kterákoli pozdější fáze.
- **`options.setLogger` je zrušen** a `bootstrap()` ho **odmítne jménem**
  (`[bootstrap] options.setLogger is retired - …`), ne že by ho tiše ignoroval:
  volající, který funkci předává schválně, nesmí zůstat v domnění, že něco
  zapojil. Jediný volající (`api_biz/converter/index.js:21`) předával
  `setLogger` **z tohoto balíčku** — tedy prosil balíček, ať zavolá vlastní
  funkci; to je druhá kolej téhož (`change-discipline.md` § One rail per concern).
- **Vazba podle jména souboru `<serviceRoot>/src/lib/logger.js` je pryč**
  i s `resolveModuleLoggerSetter()`. Chování řízené *názvem* souboru je to, co
  principy 9 (Configuration over Convention) a 8 (Explicit over Implicit)
  zakazují, a kopie proxy v každé službě byla druhá kolej jednoho konceptu.
- **Explicitní cesta**: uvnitř handleru `ctx.logger` (scoped, staví ho
  `ContextBuilder`), mimo handler `require('@onlineapps/service-wrapper').getLogger()`
  v čase volání. `setLogger` zůstává exportován pro testy a pro FÁZI 0.12.
- Kontrolní případy, které se nehnuly: `getLogger()` před bootem dál odmítá
  hláškou `[Context] Problem - Fix`; boot, jehož FÁZE 0.12 selhala, nechá proxy
  prázdnou místo polovičního zapojení; pořadí fází v knize
  (`0.1 → 0.12 → 0.1b → 0.15 → 0.2 → 0.3 → 0.4 → 0.7 → INIT`) i `options.config`
  a `options.serviceBaseDir` beze změny.

**Co to vyžaduje od služeb** (pin 8.x): smazat `src/lib/logger.js` a přepojit
konzumenty na `getLogger()` / `ctx.logger`; converter navíc zahodit
`{ setLogger }` z `bootstrap(__dirname, …)`. Kopii dnes drží emailer,
hello-service, ingest, invoicing, meta, pdfgen.

### Neopravitelná obálka se odmítá jednou — jedna kolej, permanentní klasifikace (d.383)

**Oprava chování na kontrakt, ne breaking change.** Zpráva bez `workflow_id`
skončila v `_normalizeWorkflowMessage()` **netypovaným `Error`** (bez `code`,
`status` i `type`), ještě než na ni sáhl `orchestrator.processWorkflowMessage()`.
Politika doručení `consume()` z `@onlineapps/mq-client-core` (d.198b-1) se ptá
klasifikátoru tohoto wrapperu, co takové selhání stojí — a ten znal jedinou
permanentní chybu, `SyntaxError`. Vadná obálka tedy byla `transient`: projela
celý rozpočet `RABBITMQ_MAX_DELIVERY_ATTEMPTS`, na každý pokus řádek logu, a do
`<služba>.dlq` dorazila až potom. Na publikované 7.0.0, která rozpočet nemá
vůbec, dala jedna taková zpráva ~628 000 redeliveries za tři minuty (měřeno
INFRA 2026-09-14 na devu, `biz-hello.workflow`).

- **Jedna kolej odmítnutí obálky.** Všechny čtyři odmítnutí
  v `_normalizeWorkflowMessage()` (chybějící `workflow_id`, krok s vyřazeným
  klíčem `id`, krok bez `step_id`, chybějící `current_step`) i poslední větev
  dispatche `Unknown message format or operation` házejí `InvalidEnvelopeError`
  — 400 `INVALID_ENVELOPE`, tutéž třídu, kterou o hop dál hází
  `ContextBuilder._validateMqMessage()`
  (`docs/biz/70-contracts/workflow-message.md` §3.3). Třída se importuje
  z `ErrorMapper`, druhá definice nevznikla. **Hlášky se nezměnily**, jen typ;
  RPC odpověď volajícímu proto nese `INVALID_ENVELOPE` místo dosavadního
  `PROCESSING_ERROR`.
- **`_classifyDeliveryError()` vrací `permanent`** pro `SyntaxError`, pro
  `code === 'INVALID_ENVELOPE'` a pro strážce orchestrátoru z §3.3
  (`type === 'VALIDATION'` **a zároveň** `status === 400`, tj.
  `MISSING_CORRELATION_ID` / `MISSING_WORKFLOW_ID`). Jinak `transient`. Čte se
  deklarovaná identita chyby — třída, `code`, dvojice `type`/`status` — nikdy
  text hlášky. `code` místo `instanceof` je záměr a je širší: na v3 dispatch
  cestě projde odmítnutí `ErrorMapperem` jako obálka a
  `_dispatchViaInvokeOperation()` z něj staví prostý `Error`, takže třída hop
  nepřežije — publikovaný kód kontraktu ano.
- Kontrolní případy, které se nehnuly: obyčejné selhání handleru, `TypeError`,
  samotný status 400 bez `type`, samotný `type === 'VALIDATION'` bez 400
  a `VALIDATION_FAILED` vlastního schématu operace zůstávají `transient`.
- Testy: `tests/unit/ServiceWrapper.envelopeRejection.test.js` (20) a
  `tests/integration/envelopeRejection.integration.test.js` (4) — druhý proti
  živému brokeru: zpráva bez `workflow_id` je odmítnuta před orchestrátorem,
  klasifikována `permanent`, v `x-death` má `reason: 'rejected'`, `count: 1`
  a fronta služby zůstává prázdná; kontrola s tranzientním selháním se na frontu
  vrací a do DLQ nejde. Smyčku samotné knihovny drží její vlastní repozitář —
  instalovaný `@onlineapps/mq-client-core` 2.0.1 volbu `classify` ještě nemá,
  takže osud doručení řídí sada přes `amqplib` stejně jako
  `correlationIdOneRail.integration.test.js`.

### `getStatus()` zrušeno — stav služby má jednu kolej, heartbeat (d.380)

**Breaking (veřejné API).** `ServiceWrapper.getStatus()` byl posledním
in-process čtenářem HTTP endpointu `GET /status`, který vyřadilo ADR 0005
(`api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md` § MQ status
contract: „Belongs to heartbeat `components` map"). Jedinou kolejí, na které
biz služba vypovídá o svém zdraví, je heartbeatová mapa
`_collectHeartbeatComponents()` — `mq`, `redis`, `db`, `cache` — validovaná
registrem (`infrastructureHealth.listener`) a projektovaná do Redisu
(`api/docs/biz/00-model/service-shape.md` § Heartbeat).

Produkčního volajícího metoda neměla: měřeno přes `shared/`, `infra/`,
`api_biz/*` a `fe_adminui/src` — jediný zásah mimo tenhle balíček byl
`monitoring-core/tests/test-service.js`, od ADR 0005 nespustitelný (mazán
v téže vlně, d.370). Registrové `checker.getStatus()`, `rateLimiter.getStatus()`
a `SyncEngine.getStatus()` jsou jiné třídy.

Fakta, která metoda držela, platí dál a drží je testy přepsané na heartbeat:
po fatálním pádu MQ je `mq` nezdravé; `cache` je poslední **naměřené** zdraví,
ne existence objektu (nález INFRA ㊷); `registry` ani `monitoring` nejsou
komponenty, protože ani jedno nelze změřit. `API.md` přegenerováno deklarovaným
mechanismem (`npm run docs`), README už neslibuje dvě plochy téhož faktu.

### Čtyři `|| null` no-opy v `_initializeInvocationContext()` pryč (d.380)

`cache`, `secrets`, `mq` a `state` se předávaly do `ContextBuilder` jako
`this.<pole> || null`. Všechna čtyři pole konstruktor inicializuje `null`
(`ServiceWrapper.js:156-161`) a jinde jim přiřazuje výhradně instanci
konektoru (`:1294`, `:1731`, `:1757`, `:1790`); `= undefined` ani
`delete this.<pole>` v balíčku neexistuje. Strážce tedy neměnil výsledek
v žádném dosažitelném stavu. Komentář nad `monitoring:` už netvrdí, že se
sousedy liší — liší se tím, že `monitoring` povinné je, ne přítomností
strážce.

### `bootstrap()` má svůj jsdoc zase na sobě; API.md přegenerováno (d.368)

Blok jsdoc popisující `bootstrap()` stál v `src/index.js` **nad jiným blokem
jsdoc**, ne nad funkcí — mezi ním a `async function bootstrap(...)` ležel celý
komentář a celé tělo `resolveModuleLoggerSetter`. jsdoc váže komentář na
následující konstrukci, takže tenhle se nevázal na nic a generátor ho zahazoval:
hlavní vstupní bod balíčku v `API.md` existoval jen proto, že ten soubor byl
starší než ta přestavba. Blok přesunut na funkci; nic jiného se v `src/index.js`
nezměnilo (23 řádků ven, 23 zpět, samý komentář).

`API.md` přegenerováno deklarovaným mechanismem (`npm run docs`). Vedle
vráceného `bootstrap()` dohnalo zdroj i jinde — a obojí je oprava, ne ztráta:

- přibylo `OperationContext.requireActingPerson()` (d.196), `isPositiveIntegerId`,
  `describeRejectedId` a `isMissingId` (d.363),
- ubylo `REQUIRED_METHODS` a typedef `DbAcquisition` — změřeno, ani jeden už
  ve zdroji neexistuje (`grep -rn` přes `src/`, exit 1, s pozitivní kontrolou
  na témže souboru).

Výstup ověřen jako deterministický: dva po sobě jdoucí běhy generátoru dají
bajtově týž soubor.


### `getStatus().components` jmenuje jen to, co umí změřit (d.363)

Mapa četla `{ mq: _mqHealth(), registry: !!registryClient, cache: !!cacheConnector,
monitoring: !!monitoring }` — jeden skutečný verdikt a tři booleany o existenci
objektu pod klíčem `components`, který všude jinde znamená zdraví komponenty.

- `cache` se bere z `_lastCacheHealth`, tedy z hodnoty, kterou publikuje
  heartbeat. Řádek `mq` nad ním to v komentáři sliboval už dřív („the same value
  the heartbeat publishes, from the same owner"); teď to slib plní i cache místo
  druhého výpočtu téhož faktu. Do d.363 tu stálo `!!this.cacheConnector` — táž
  vada, kterou heartbeat odložil v d.349 (nález INFRA ㊷).
- `registry` **pryč**. `ServiceRegistryClient` z `@onlineapps/conn-orch-registry`
  3.0.1 nemá `isConnected()` ani `healthCheck()` (změřeno: init, register,
  deregister, unregister, sendHeartbeat, startHeartbeat, stopHeartbeat,
  subscribeToChanges, getServiceSpec, getService, isServiceActive,
  getActiveServices, close), takže se není koho zeptat — a klíč, který umí říct
  jen „konstruktor proběhl", nepatří do mapy, kde ostatní položky tvrdí zdraví.
  `_collectHeartbeatComponents()` staví čtyři komponenty (mq, redis, db, cache)
  a registry mezi nimi nikdy nebyla.

`monitoring` beze změny; kontrolní případ to drží.

### Tři id obálky odpovídají jednomu predikátu (d.363)

`ContextBuilder._validateMqMessage` kontrolovala `workflow_id` a `correlation_id`
přes `.length === 0`, ale `step_id` přes `.trim() === ''` — id ze samých mezer
tedy prošlo dvěma strážci ze tří, uvnitř jedné metody, na třech polích téhož
druhu. Hodnota, která prošla, neškodná není: `workflow_id` a `step_id` pojmenují
úložný prefix `content/<workflow_id>/<step_id>` a `correlation_id` je to, na čem
se spojuje trasa.

- Nový sdílený predikát `isMissingId()` pro všechna tři pole. Odmítá prázdnotu
  a nic jiného — id se skutečným obsahem a mezerami kolem je hodnota a předá se
  přesně jak přišlo; tichý `trim()` by byl výmysl druhého zápisu cizího id.
- Hlášky `workflow_id` a `correlation_id` doplněny do tvaru §5 s `Fix:`
  a odkazem na `docs/biz/70-contracts/workflow-message.md` §3.2. Dosavadní
  „Expected uuid string" bylo navíc nepravdivé: gateway vydává `wf-<...>`,
  ne uuid.

RED (unit, na zdrojáku před opravou): `ContextBuilder.test.js` 8 failed /
6 passed — zelené právě `step_id` (ten `trim()` i `Fix:` už měl) a oba kontrolní
případy; `ServiceWrapper.statusComponents.test.js` 7 failed / 2 passed — zelené
`mq` a `monitoring`.
GREEN: unit 51 sad / 632 testů, integrace 13 sad / 76 testů.


### Komponenta `cache` v heartbeatu se měří, ne předpokládá (d.349)

`_collectHeartbeatComponents()` počítala
`components.cache = this.cacheConnector ? 'healthy' : 'unknown'` — existence
objektu hlášená jako zdraví spojení. Změřená cena (nález INFRA ㊷): biz-pdfgen
publikoval 27 hodin `cache: healthy` nad mrtvým spojením do Redisu, protože
objekt tam pořád byl a na objekt se ptalo.

Tři sousedé v téže mapě jsou přitom skuteční: `mq` se ptá `isConnected()`
(`_mqHealth()`), `db` pouští `authenticate()` a `redis` pouští `healthCheck()`
konektorů cache i state (`_checkDatabaseDependency` / `_checkRedisDependency`,
vzorkované přes `refreshDependencyHealth()`). Cache se přidává na tutéž kolej:
asynchronní sonda `_probeCacheHealth()` patří k periodickému refreshi,
fire-and-forget heartbeat vzorkuje, co po ní zbylo (`_lastCacheHealth`).

Tři stavy, žádný čtvrtý — doktrína, kterou `_mqHealth()` už říká: `unknown`
(konektor není, nebo neumí odpovědět), `healthy` (řekl ano), `unhealthy` (řekl
ne, nebo se otázka nepovedla). `unknown` u konektoru bez `healthCheck()` je ten
rozdíl oproti smazanému řádku: nevědět není totéž co být zdravý.

Ostatní komponenty projekce prověřeny, beze změny: `mq`, `db` a `redis` už
volají reálnou sondu; `state` vlastní sondu v projekci nemá a nepotřebuje —
vstupuje do `redis` přes `_checkRedisDependency`; `registry` a `secrets`
v heartbeatové mapě nejsou vůbec, takže o nich nic netvrdí.

RED (unit, na zdrojáku před opravou): 6 failed / 3 passed — kontrolní případy
(živý konektor, žádný konektor, tři sousední komponenty) zelené.
RED (integrace proti živému Redisu 127.0.0.1:33030 a publikovanému
`@onlineapps/conn-base-cache` 1.0.10, tentýž zdroják): 1 failed / 4 passed —
padl přesně scénář ㊷ (spojení mrtvé, objekt na místě).
GREEN: unit 9/9, integrace 5/5.


### `correlation_id` jedna kolej — vyřazený alias i výmysl z `workflow_id` pryč (d.349)

`_dispatchViaInvokeOperation` četla
`message.correlation_id || message.correlationId || workflow_id`. Obě náhrady
byly vada, každá jiného druhu:

- `message.correlationId` je camelCase tvar, který vlastník vyřadil 2026-09-03
  (`docs/biz/70-contracts/workflow-message.md` §2.2, konfirmace
  `workflow-message-aliases` 001). Přeměřeno pro tuhle dávku: žádný producent
  do `workflow.init`, `workflow.control` ani `<služba>.queue` ho do TĚLA nepíše
  — gateway `routes/workflow.js` a `routes/webhooks.js`, monitoring
  `supervisionWorkflow.js` a `watchers/canaryWatchdog.js` i orchestrátorovo
  `_routeToNextStep` publikují `correlation_id`; camelCase žije jen v AMQP
  hlavičkách, které žádný konzument neparsuje. Větev četla, co nikdo nepíše.
- `|| workflow_id` bylo horší než alias: hodnotu VYMÝŠLELO. Kontrakt §3.2 říká,
  že producent bez `correlation_id` nepublikuje, a §3.3, že strážce wrapperu
  znamená, že k handleru bez obou id nedojde žádná obálka. S touhle náhradou se
  chyba producenta tiše zaplátovala o skok dál a handler běžel pod id, které
  nikdo nevydal — přesně ta lež v trasování, kvůli které korelační id existuje.

Odmítnutí NENÍ nová kontrola ani nová třída chyby: hodnota se předá dál a
`ContextBuilder._validateMqMessage` ji odmítne — `InvalidEnvelopeError`,
400 `INVALID_ENVELOPE` — toutéž kolejí, kterou v téže metodě odmítá chybějící
`workflow_id` a `step_id` (`change-discipline.md` § One rail per concern).

BREAKING: zpráva bez `correlation_id` v těle se od teď nezpracuje. Nic ji dnes
neposílá (změřeno u všech producentů výše), takže jde o utažení odmítnutí, ne
o změnu chování živé cesty.

RED (unit, na zdrojáku před opravou): 3 failed / 6 passed — kontrolní případy
(kanonická hodnota, chybějící `workflow_id`, chybějící `step_id`) zelené.
RED (integrace proti brokeru 127.0.0.1:33023, tentýž zdroják): 2 failed /
2 passed — kontrolní běh s polem v těle prošel k handleru.
GREEN: unit 9/9, integrace 4/4.


### Zbylá dvě místa, která hedžovala o zaručeném loggeru (d.313)

Dokončení d.183 na dvou místech, která tam zůstala stát:

- `_initializeMQ()` → `healthReportCallback` otevíral log podmínkou
  `if (this.monitoring && this.monitoring.info)`. Týž argument jako u
  `healthCriticalCallback`: callback se staví ve FÁZI 0.3, tedy až za FÁZÍ 0.12,
  takže podmínka nemá co chytit — a jediný stav, který by ji zneplatnil (logger
  bez kanálu `info`), měnila na ticho. Pryč; zpráva o zdraví MQ teď dojde vždy,
  nebo selže nahlas.
- `_initializeInvocationContext()` předával ContextBuilderu
  `monitoring: this.monitoring || null`. Vedle stojí čtyři konektory psané
  stejně — a u nich ta pojistka říká pravdu: cache, secrets, mq a state jsou
  volitelné, nabootovaná služba nemusí držet ani jeden. U monitoringu tvrdila
  možnost, která neexistuje. Pryč; sousední čtyři slots zůstávají beze změny.

Poctivě k důkazu: druhá změna **nemění žádnou hodnotu** — `this.monitoring` je
buď objekt, nebo `null` (placeholder z konstruktoru), a `null || null` je `null`,
takže operátor byl v každém dosažitelném stavu no-op. Mění se, co ten řádek
tvrdí. RED→GREEN proto nese jen první změna
(`tests/unit/ServiceWrapper.mqLogGuard.test.js`); pravidlo o předání objektu
říká `tests/unit/ServiceWrapper.monitoringHandoff.test.js` včetně kontrolního
případu na ty čtyři sloty, které prázdné být SMÍ.

### Jedno jméno konektoru, jeden soubor dvojníka (d.190)

`jest.config.js` mapoval všech pět zdvojených jmen konektorů na JEDEN soubor,
`tests/mocks/connectors.js`. Jest si ale registr dvojníků klíčuje podle
**rozřešené cesty**, takže dvě `jest.mock` továrny pro dvě různá jména balíčků
se zapsaly pod jeden klíč a druhá tu první tiše PŘEPSALA — co sada dostala zpět,
záviselo na pořadí deklarací, ne na jménu, o které si řekla. Navíc každé jméno
vracelo *pytel* dvojníků, ne tvar balíčku, za který stojí.

- Pět nových souborů `tests/mocks/<pkg>.js`, každý s exportním tvarem svého
  balíčku (`conn-infra-mq` a `conn-base-state` konstruktor, `conn-orch-registry`
  jmenný prostor s `ServiceRegistryClient`, `conn-orch-orchestrator` s `create`,
  `conn-orch-cookbook` jmenný prostor funkcí). Chování dvojníků zůstává na
  jednom místě, v `tests/mocks/connectors.js` — nové soubory říkají jen tvar
  (`change-discipline.md` § One rail per concern).
- `moduleNameMapper` míří každým klíčem na vlastní soubor; kotvení klíčů
  (`^…$`) a samostatná mapa pro `conn-orch-orchestrator/package.json` zůstávají
  beze změny.
- Obcházky, které kolize vynutila, pryč: `ServiceWrapper.bootLogger` už neslepuje
  MQ dvojníka s orchestrátorovým `create` do jedné podtřídy (a nepotřebuje
  lokální `jest.mock` vůbec — mapper podá správný tvar sám);
  `ServiceWrapper.serviceUrl` a `ServiceWrapper.errorHandlerLogger` už nenosí
  pole `version` uvnitř továrny na modul, které od zavedení kotvených klíčů
  neodpovídalo nikomu.
- Pravidlo drží `tests/unit/connectorDoubles.test.js` (dvě továrny v jednom
  souboru jsou dvě továrny; každé jméno má vlastní soubor; každý dvojník nese
  tvar svého balíčku). Sousední pravidlo o kotvení zůstává ve své sadě,
  `tests/unit/jestModuleMapper.test.js`, srovnané na nové cíle.

### Hlášení o MQ jde do loggeru bez podmínky (d.183)

Dvě místa v `src/ServiceWrapper.js` otevírala log příkazem
`if (this.monitoring && this.monitoring.error)`: hook zavřeného kanálu
v `_registerMQHooks()` a `healthCriticalCallback` v `_initializeMQ()`. Od
rozhodnutí vlastníka `api/docs/governance/confirmations/wrapper-boot-logger.md`
001 ta podmínka nemůže být nepravdivá — FÁZE 0.12 přiřazuje týž objekt do
`this.monitoring` i `this.logger` a `initialize()` ji pouští před oběma
fázemi, které na tato místa vedou (0.3 a fáze za 0.7); služba, která si logger
nepostaví, nenabootuje vůbec. Guard tedy hlídal stav, který boot nevyrobí — a
jediný stav, který by ho vyrobil (logger bez kanálu `error`), měnil na ticho:
kanál se zavřel, kritická zpráva o zdraví přišla, a nikde nic
(`architecture-principles.md` §3 No Fallbacks, §4 Fail-Fast).

- Obě podmínky pryč; log se volá přímo. Chybějící kanál `error` teď selže
  nahlas místo toho, aby hlášení zmizelo.
- Chování na dosažitelné cestě se nemění: `@onlineapps/conn-base-monitoring`
  vrací všechny čtyři kanály, což měří
  `tests/integration/mq-log-guard.integration.test.js` proti skutečnému
  konektoru — a tamtéž skutečný broker zavře kanál (404 NOT-FOUND na
  `checkQueue`) a hlášení dorazí do skutečného loggeru.
- Pravidlo („hlášení o MQ vždy dojde, jinak selže nahlas") drží
  `tests/unit/ServiceWrapper.mqLogGuard.test.js`, včetně kontrolních případů
  na stavové podmínky, které pravdivostní hodnotu měnit MOHOU (chybějící
  `mqClient`, transport bez `onChannelClose`) — ty zůstávají.

### Deklarovaný veřejný povrch: pole `exports` a lehké vstupy (d.258)

Balíček dosud neměl `exports`, takže jeho veřejným API byl **celý strom
souborů** — kdo chtěl jen základní třídu chyby, buď platil načtení celé
infrastruktury, nebo sahal dovnitř na `…/src/ErrorMapper.js`. Změřeno v
kontejnerovém běžci converteru: `require('@onlineapps/service-wrapper')`
**5164 ms**, samotný `src/ErrorMapper.js` **5 ms**. Rozhodnutí vlastníka
`api/docs/governance/confirmations/wrapper-error-exports.md` 001, varianta A.

- `package.json` deklaruje `exports` s pěti vstupy: `.` (beze změny —
  `src/index.js`), `./errors` → `src/ErrorMapper.js`, `./logger` →
  `src/logger.js`, `./schema-validator` → `src/SchemaValidator.js`
  a `./package.json`.
- **BREAKING:** jakákoli jiná cesta do balíčku už neexistuje. Import tvaru
  `@onlineapps/service-wrapper/src/<cokoli>` padá na
  `ERR_PACKAGE_PATH_NOT_EXPORTED`. Změřeno v celém workspace (mimo
  `node_modules`): tři takové řádky, obě jejich potřeby mají vstup v mapě —
  `api_biz/converter/tests/unit/logger-proxy.test.js:50` (`…/src/logger` →
  `…/logger`) a `api_biz/invoicing/tests/{unit/variable-symbol-contract.test.js:23,
  db/integration/document.service.test.js:148}` (`…/src/SchemaValidator` →
  `…/schema-validator`). Přepis je jednořádkový a patří do kaskády 8.0.0.
- Kořenový vstup ani jeho obsah se nemění: `require('@onlineapps/service-wrapper')`
  vrací totéž co dnes.
- Nové vstupy nejsou druhá kolej k rootu — root tytéž symboly stále vyváží.
  Vstup navíc přináší jen **cenu**: načte se jeden modul, ne celá vrstva.

### Chybová RPC odpověď jen jednou, při posledním pokusu (d.198c)

Dosud wrapper publikoval chybovou odpověď do `replyTo` volajícího při **každém**
doručení zprávy — a protože transport vadnou zprávu bezpodmínečně vracel do
fronty, byl to nekonečný proud odpovědí pod jedním `correlationId`. Konfirmace
`api/docs/governance/confirmations/mq-consumer-contract.md` 002 bod 3.

- `_processWorkflowMessage(rawMessage, queueName, delivery)` bere třetí argument —
  kontext doručení `{ attempt, maxAttempts, isFinalAttempt }`, který předává
  `consume()` z `@onlineapps/mq-client-core`.
- Chybová odpověď odchází, jen když je to **poslední pokus**, nebo když je chyba
  **permanentní** (knihovna ji dead-letteruje hned při prvním selhání). Úspěšná
  odpověď se nemění: odchází hned, při kterémkoli pokusu. Zpráva bez `replyTo`
  se neodpovídá nikdy.
- `_classifyDeliveryError()` je klasifikátor wrapperu: tělo, které není JSON, je
  `permanent` (stejné bajty se rozparsují stejně při každém pokusu), všechno
  ostatní `transient` — wrapper si netroufá tvrdit víc, zpráva si nechává pokusy
  a do `<služba>.dlq` dojde tak jako tak, jen později. Táž funkce jde injekcí do
  `consume()` jako `options.classify` i do brány odpovědi, aby knihovna a wrapper
  nemohly o osudu zprávy rozhodnout rozdílně.
- `maxAttempts` se **nepředává**: rozpočet řeší knihovna z
  `RABBITMQ_MAX_DELIVERY_ATTEMPTS`, resp. ze svého modulového defaultu, takže má
  jednoho vlastníka a jednu dokumentovanou cestu přepisu.

**Chybějící kontext doručení je chyba zapojení, ne případ k odpovědi.** Wrapper
8.0.0 se publikuje v jedné kaskádě s `@onlineapps/mq-client-core` 8.x, která
kontext předává vždy, takže kombinace „nový wrapper + knihovna bez politiky"
nevznikne; větev, která by přesto odpověděla, by byla přechodový shim pro stav,
který neexistuje (`architecture-principles.md` §11), a v den vydání mrtvý kód.
Bez kontextu proto `_shouldAnswerRpcNow()` hází
`[ServiceWrapper] consume() handed no delivery context - Expected:
@onlineapps/mq-client-core >= 8 (dead-letter policy). Fix: pin the platform
version from config/libraries.json` a nic se neodpovídá.

**Integrace zatím neběží** — cílové chování není v pinnuté 2.0.1 a `file:` odkaz
je zakázán (`architecture-principles.md` § Shared Packages). Po pinu se změří
proti živému brokeru: workflow zpráva s `replyTo`, handler hází vždy, `N=2` →
v `replyTo` frontě právě **jedna** odpověď se `status: 'error'` a zpráva
v `<služba>.dlq`; kontrolní případ: handler projde → právě jedna odpověď
se `status: 'ok'` a `.dlq` prázdná.

### `REGISTRY_URL` odchází z biz strany (d.244)

`_initializeRegistry()` už `wrapper.registry.url` **nevyžaduje ani nepředává**
klientovi. Klíč vstoupil do `conn-orch-registry` 2025-12-30 jako adresa pro
`getService()` (discovery přes `GET /services`); registrace ho nikdy nepoužila —
jede frontou `registry.register`. Konfirmace `biz-discovery-redis` 001 přesunula
discovery na Redis projekci a `725b25c2` HTTP volání smazal, ale parametr nechal.
Wrapper tak odmítal nabootovat bez adresy, kterou ADR 0005 zakazuje volat.
Konfirmace `biz-discovery-redis` 002 to uzavírá.

- Konfigurace **bez bloku `registry`** projde inicializací registru.
  `registry.heartbeatInterval` má i nadále hodnotu — z `config/runtime-defaults.json`,
  které ConfigLoader domerguje; v šabloně nikdy nebyl.
- `ConfigLoader` `registry.url` nikdy nedeklaroval ani nevyžadoval (změřeno:
  0 výskytů), takže tam nebylo co rušit.

### Heslo z `REDIS_URL` dojde do cache, state i secrets (d.245)

`REDIS_URL` nese credential k Redisu — jedna kolej, rozhodnutí leada INFRA
2026-09-10; `REDIS_PASSWORD` se do sdílené sady nepřidává. Wrapper z té URL do
té doby bral jen `hostname` a `port` a `parsed.password` zahodil, takže proti
Redisu s `requirepass` odpověděl `NOAUTH` každý konektor, který wrapper z té URL
staví.

Nový privátní pomocník `_redisConnection(url)` → `{ host, port, password }` je
**jediné** místo, kde se ta URL rozebírá; používají ho `_initializeCache`,
`_initializeState` i `_initializeSecrets` (ten heslo předává jako `password`,
protože `conn-infra-secrets` ho jako součást URL nečte). Explicitní
`state.password` / `secrets.password` dál vyhrávají nad URL.

- Prázdné userinfo = `password: undefined`, nikdy `''` — dev Redis bez hesla
  žádné `AUTH` nečeká.
- Heslo se percent-dekóduje (`p%40ss` → `p@ss`), protože znaky `@ / :` se
  v userinfo jinak zapsat nedají.
- Discovery klient (`_initializeRegistry`) nic nepotřebuje: dostává celou URL
  a node-redis si z ní heslo vezme sám — změřeno proti živému Redisu
  s `requirepass` (`tests/integration/redisPassword.integration.test.js`).

### Registrace nese `deployable` — verdikt shody s manifestem (d.221)

Registrační payload (`_buildRegistrationPayload()`) má nový klíč `deployable`
s hodnotou, kterou vrátil krok 7/7 validace: `true`, `false`, nebo `null`, když
běh skončil dřív, než konformitu kdokoli změřil. Registry ho promítá do
`infrastructure:health:<service>` vedle `status` (potvrzení vlastníka
`docs/governance/confirmations/biz-service-manifest.md` 001 §4). Tři stavy se
nikde neslučují: `null` je tvrzení „nikdo neměřil", ne chybějící hodnota.

- `deployFindings` do payload **nejde** — tabulka patří do bootovacího logu
  a do `ci/deployability.json`; registru stačí signál, ne report.
- Výsledek validace bez klíče `deployable` je porušení pinu, ne přechodná
  chyba: `[ServiceWrapper] Validation result lacks "deployable" - conn-orch-validator
  >= 8.0.0 required. Fix: pin the validator the wrapper declares.` Vyhazuje se
  **mimo** startovní retry smyčku — uvnitř by boot prospal šest backoffů
  (30 s … 30 min), než by pojmenoval špatný pin (architecture-principles §4).
- Registrace hlásí **jednou** řádkou, co posílá a že volá registry:
  `[ServiceWrapper] [REGISTRATION] Registering as <service> (deployable: <v>) — calling
  registryClient.register()`. Do 7.0.0 to byly dvě řádky o jedné události — marker
  `About to call registryClient.register()` a vedle něj věta s verdiktem; sloučeny
  v d.224 (`.claude/rules/change-discipline.md` § One rail per concern).
- Vyžaduje `@onlineapps/conn-orch-registry` s klíčem `deployable` na drátě
  (jinak ho `register()` tiše zahodí — staví zprávu z pevného seznamu klíčů).

### Testy: fixture `tests/fixtures/test-service` je trackovaná, ne vyráběná za běhu

Třináct unit sad předávalo `tests/fixtures/test-service/src` jako
`serviceBaseDir`/`serviceRoot`, ale adresář v repozitáři nebyl: `git ls-files
tests/fixtures` nevracelo nic a složka byla prázdná. Vyráběla ho jediná sada —
`ServiceWrapper.validation.component.test.js` v `beforeAll` — a v `afterAll` ho
zase `rmSync`-ovala, takže existoval jen po dobu běhu toho jednoho souboru.

Co to zakrývalo (změřeno 2026-09-08 obalením `_handleInitializationError` a
sledováním `fs` volání):

- `ServiceWrapper.bootLogger.test.js` bootoval proti neexistující cestě.
  `initialize()` končilo `[HandlerLoader] baseDir does not exist - Got:
  …/tests/fixtures/test-service/src` ve fázi `INIT` a `process.exit(1)`;
  všech šest testů hlásilo zeleno, protože `_logPhase` je zamockovaný
  a `process.exit` odchycený.
- `ServiceWrapper.noHttpListener.test.js` tvrdil, že `bootstrap()` neotevře TCP
  port, zatímco bootstrap padal o dva řádky dřív na `Cannot find module
  …/tests/fixtures/test-service/src/config`. Test tu chybu propouštěl, protože
  hlídá jen tvar hlášky (`EADDRINUSE`, „no app instance").

Nově je fixture v gitu: `config/service/config.json`,
`config/service/operations.json` (živý layout deklarací — `conn-config/` je
retirovaný, `src/ConfigLoader.js` ho nezná) a `src/config/index.js`, který je
skládá tak, jak to dělá `src/config/index.js` každé biz služby, aby
`bootstrap()` měl co načíst. `ServiceWrapper.validation.component.test.js`
fixture negeneruje ani nemaže; uklízí výhradně `conn-runtime/`, což je jediné,
co za běhu vzniká.

Rail proti návratu: `tests/unit/fixturePaths.test.js` čte zdrojáky sad v
`tests/unit/**`, vytáhne z nich každý literál `path.resolve|join(__dirname,
'../…')` a ověří, že cesta existuje; kontrolní případy hlídají, že scan něco
našel a že fixture je trackovaná v gitu. Falzifikováno přejmenováním fixture —
sada zčervená se seznamem 14 deklarací.

### Testy: `bootLogger` už nespolkne selhaný boot — a boot v ní poprvé doběhne

Sada `ServiceWrapper.bootLogger.test.js` hlásila 6/6 zeleno nad bootem, který
padal a volal `process.exit(1)` ve čtyřech z šesti testů: `_logPhase` je v ní
zamockovaný, takže FÁZE se `status: 'FAILED'` skončila v poli, které nikdo
nečetl, a `process.exit` je odchycený, takže běh pokračoval za bod, kde by
produkce zemřela. Harness, který spolkne přesně to, co třída při selhání dělá,
nemůže o bootu tvrdit nic (`automation-gates.md` §5).

`bootAndRecord()` nově **hází**, když boot skončí s FAILED fází nebo se zapsaným
`exit`, a test, který je o selhání, si to musí vyžádat explicitně
(`{ expectBootFailure: true }` — `architecture-principles.md` §8). Po zatvrzení
vyplavaly tři vrstvy, všechny opravené u zdroje, ne obejité:

1. konfigurace sady neměla `service.env`, které čte `_initializeOrchestrator`
   (boot umíral na `Missing configuration - service.env (NODE_ENV) is required`);
2. dvojník `_initializeMonitoring` nastavoval jen `this.logger`, zatímco skutečná
   FÁZE 0.12 přiřazuje jeden a týž objekt do `this.monitoring` i `this.logger` —
   proto `conn-infra-error-handler` házel `monitoring instance is required`;
3. dvojník orchestrátoru chyběl. Nesmí být ve druhém `jest.mock`: `jest.config.js`
   mapuje `conn-infra-mq` i `conn-orch-orchestrator` na týž soubor a registr
   mocků je klíčovaný resolvovanou cestou, takže druhá továrna první **nahradí**
   (změřeno: `MQConnector is not a constructor`). Jedna továrna proto vrací
   lokální podtřídu MQ dvojníka s připojeným `create`.

Kontrolní test „phases run in ascending order" je přeformulovaný na to, co kód
deklaruje. Číslo fáze je **identita, ne pozice** — dávka d.142 to změřila a
zapsala (`_logPhase` neřadí, číslování už dnes chronologické není) a `0.1b` si
jméno drží proto, že ho citují dokumenty. Alignment se do knihy zapisuje tam,
kde skutečně běží, tedy za 0.7. Test teď porovnává celou knihu hodnotou:

```
["0.1:PASSED","0.12:PASSED","0.15:SKIPPED","0.2:SKIPPED","0.3:PASSED",
 "0.4:PASSED","0.7:SKIPPED","0.1b:PASSED","INIT:PASSED"]   exits=[]  isInitialized=true
```

Aby boot doběhl a nezůstal viset, sada vypíná FÁZI 0.10 (heartbeat má vlastní
sadu a arms reálný interval) a v `afterEach` každý nabootovaný wrapper skutečně
vypíná — dokud boot umíral, nebylo co zavírat.

`tests/unit/fixturePaths.test.js` skenuje nově i `tests/integration/**`: obě
integrační sady jmenují tutéž fixture a běží pod jiným runnerem, takže by je
unit rail jinak nikdy neviděl. Falzifikováno přejmenováním cesty v
`orchestrator-integration.test.js` — sada zčervená a jmenuje ji.

### Odstraněno: `ContextBuilder._acquireDb()` — mechanismus bez konzumenta

`ctx.db` je **slot, ne mechanismus**. Že je vždy `null` a že přístup k databázi
nevede přes kontext, je kanonické od 2026-04-21
(`api/docs/biz/10-invocation/operation-context.md`, řádek `db`;
`api/docs/biz/80-decisions/0001-http-loopback-removal.md`;
`api/docs/biz/70-contracts/database-contract.md` §10). Builder přesto nesl
`_acquireDb()`, který rozpoznával tři tvary db handle
(`storage.acquireClient()`, `storage.client`, konektor sám jako klient) a k nim
hák `releaseDb` v `release()`.

Změřeno před smazáním: jediné místo, které `ContextBuilder` staví, je
`ServiceWrapper._initializeInvocationPipeline()` a předávalo `storage: null`;
jinde v celém workspace žádné `new ContextBuilder` není; `acquireClient` ani
`releaseClient` se nevyskytují v žádném konektoru (`@onlineapps/conn-base-db`
exportuje pouze `createSequelize`) — jen v samotném `_acquireDb()` a v jeho
testech. Žádná ze tří větví tedy neměla konzumenta
(`change-discipline.md` § Removing something removes its declaration).

Zaniká: `_acquireDb()`, typedef `DbAcquisition`, větev `releaseDb` v `release()`
(zbývá flush loggeru), slot `storage` v konstruktoru `ContextBuilder` i v jeho
chybové hlášce, a `storage: null` v `ServiceWrapper`. **Kontrakt `ctx` se
nemění**: pole `db` zůstává mezi 16 poli a je `null`, jak dokumentace popisuje;
`OperationContext` už ho nepopisuje jako „PgClient / pool handle".

### Logování: osm `console.*` volání za FÁZÍ 0.12 zaniká, hlášky jdou jedním kanálem

`console` je ambientní kanál — žádná služba ho nekonfiguruje a kolektor
monitoringu řádek na něm nedokáže přiřadit. Vlastník ho pro konektory a jejich
wrappery zavřel (`connector-logger-contract.md` 001/003) a pro pořadí bootu
tohoto balíčku konkrétně (`wrapper-boot-logger.md` 001): monitoring běží ve
FÁZI 0.12, tedy PŘED infrastrukturní bránou, takže od té fáze dál je
`this.logger` zaručený. Osm volání za tou hranicí přesto zůstávalo.

- **Selhání inicializace cache** (`src/ServiceWrapper.js`, blok
  `wrapper.cache.enabled`) hlásilo `console.warn('Cache initialization failed
  (non-critical):', …)` — jediný záznam o službě, která nastartovala bez cache,
  a ten mimo logger. Nově jde přes `this.logger.warn` se strukturovaným
  `{ error, stack }` a hláškou ve tvaru `[Context] Problem - Fix`. **Chování se
  nemění**: boot pokračuje dál, jak popisuje README § Boot order („cache is
  non-critical; state and secrets throw").
- **Registrace u Registry** (FÁZE 0.7) psala šest řádků dvakrát: jednou přes
  `this.logger.*`, hned pod tím `console.log`/`console.warn` s komentářem
  „Fallback if logger not initialized". Ten fallback nemohl nikdy zafungovat —
  kdyby logger nebyl, spadlo by už volání o řádek výš. Konzolové kopie
  smazány, hlášky beze změny.
- **Dva `console.error` u MQ** (hook `onChannelClose`, `healthCriticalCallback`)
  duplikovaly záznam, který už šel do monitoringu. Smazány.

Nedotčena zůstává **kolej viditelnosti bootu**: `_logPhase` a `_logStartup`
píší řádky `[FÁZE …]` a startovní hlášky na oba kanály záměrně a s odůvodněním
na místě (`automation-gates.md` §5 — ticho je vada). Nová sada
`tests/unit/ServiceWrapper.consoleChannel.test.js` to drží kontrolním případem:
`_logPhase` musí dál psát i na stdout.


### Testy a runner: brána §20 umí rozhodnout, mrtvý práh pokrytí a mrtvý harness pryč

Tři deklarace, které nikdo nečetl, a jedna brána, která kvůli testu nemohla
odpovědět.

**Brána §20 hlásila UNCERTAIN.** `scripts/ci/check-package-deps.mjs` čte
`require()` staticky. `tests/unit/jestModuleMapper.test.js` stavěl argument
`require()` ze smyčkové proměnné (`test.each` přes názvy balíčků), takže brána
nedokázala prokázat úplnost seznamu a celý balíček dostal trvalé UNCERTAIN —
mechanismus, který nemůže rozhodnout, je tichá brána
(`automation-gates.md` §5). Případy jsou nově tabulka **literálů a thunků**;
každý `require`/`require.resolve` má statický řetězec. Brána nezměkčena, jen
čitelný vstup: `check-package-deps.mjs --all` → `28/28 packages declare what
they use.` bez UNCERTAIN.

**`coverageThresholds` v `jest.config.js` byl překlep**, ne volba Jestu —
každý běh tiskl `Unknown option "coverageThresholds" … Did you mean
"coverageThreshold"?` a čtyři osmdesátky nevynucovaly nic. Změřeno na plné unit
sadě: statements 75.98 / branches 70.84 / functions 78.75 / lines 76.34; dolů to
táhne `ServiceWrapper.js` (68.12 / 61.21 / 70 / 68.40) a větve v
`ConfigLoader.js` (62.88). Se správně psaným klíčem na 80 % běh padá na všech
čtyřech číslech. Práh se proto **nezapíná** — brána přichází až s compliance,
ne s backlogem (`automation-gates.md` §3) — a mrtvý klíč je smazán i s
odůvodněním v komentáři, aby se překlep nevrátil.

**`tests/run-tests.js` smazán.** Samostatný harness, který přepisoval
`Module.prototype.require` a mapoval osm konektorů — druhá kolej k
`moduleNameMapper` (`change-discipline.md` § One rail per concern). Nulový
konzument: `run-tests` se nevyskytuje v `package.json`, v žádném jest configu,
v `.gitlab-ci.yml` ani ve `scripts/`; jediné zásahy v celém workspace jsou
`run-tests-in-throwaway-container.js` v converteru (jiný soubor), poznámky
v cizích `TODO.md` a datovaný záznam v tomto CHANGELOGu u 6.0.0, který
zůstává tak, jak byl napsán — je to zápis o tehdejším stavu, ne živý odkaz.


### Testy: klíče `moduleNameMapper` ukotvené — podcesta ani absolutní cesta už neskončí na dvojníkovi

Klíče `moduleNameMapper` jsou neukotvené regulární výrazy, takže holé
`@onlineapps/conn-infra-mq` chytalo každý požadavek, který ten název jen
*obsahuje* — podcestu `@onlineapps/conn-infra-mq/package.json` i absolutní
cestu `…/node_modules/@onlineapps/conn-infra-mq/src/index.js`. Ukotven byl
jediný klíč (`conn-base-state`); ostatní pět ne.

Změřené následky:

- **Explicitní mapování bylo mrtvé.** Klíč
  `@onlineapps/conn-orch-orchestrator/package.json` →
  `tests/mocks/orchestrator-package-v2.js` se nikdy neuplatnil, protože širší
  klíč o řádek výš už zabral. `readConnOrchOrchestratorContractVersion()`
  v `src/ServiceWrapper.js` tak četl `.version` z `tests/mocks/connectors.js`
  a dostával `undefined`.
- **Skutečný konektor byl z testů nedosažitelný** i po absolutní cestě.
- Kolize jmen v jednom cíli je i příčina nálezu d.119b: několik továren
  `jest.mock` se přepsalo, fáze 0.3 spadla a `process.exit(1)` zabil jest
  worker bez jediného výpisu.

Všech pět klíčů je nově ukotveno (`^…$`), společný komentář nese jedno
zdůvodnění místo kopií. Mrtvý klíč `@onlineapps/conn-base-logger` smazán —
balíček není závislostí tohoto balíčku, není v `node_modules` a nic v `src/`
ani v jest testech ho nevyžaduje (`change-discipline.md`
§ Removing something removes its declaration).

Pravidlo drží `tests/unit/jestModuleMapper.test.js` (22 testů): každý klíč
ukotvený, každý klíč pojmenovává deklarovanou závislost, holý název dál míří
na dvojníka (kontrolní případ), podcesta míří na skutečný `package.json`
s napinovanou verzí, a absolutní cesta do `node_modules` vrací skutečný balíček.


### Testy: rodina heartbeat a „po initialize() nedrží port" — jeden důkaz za všech osm biz služeb

`docs/biz/00-model/service-shape.md` § Testing requirements žádá po každé biz
službě čtyři tvrzení o heartbeatu (interval, povinná pole payloadu, synchronní
první tep ve fázi 0.10, odpojené MQ = log + pokračuj) a jedno „no-HTTP" runtime
tvrzení. Ani jedno není o kódu biz služby — publisher i socket vlastní tento
balíček, takže osm kopií téhož testu by byla druhá kolej
(`change-discipline.md` § One rail per concern). Změřeno 2026-09-06 vláknem
BIZ-pdfgen: heartbeat netestovala **žádná** z osmi služeb.

Nově `tests/unit/ServiceWrapper.heartbeatFamily.test.js` (14 testů) jede proti
skutečnému `@onlineapps/infrastructure-tools` — dvojníkem je jen MQ klient a
monitoring, ne SUT:

- **Interval** — počítá tepy přes falešné časovače: 250 ms → 1 + 3 tepy za
  750 ms; kontrolní případy drží hodnotu i pořadí zdrojů (jiná hodnota → jiný
  počet, `INFRASTRUCTURE_HEALTH_PUBLISH_INTERVAL` když konfigurace mlčí,
  explicitní konfigurace přebíjí proměnnou prostředí).
- **Payload** — každé pole, které `validateHealthCheckFormat` v Registry
  přijímá, na konkrétní hodnotu: `serviceName` = `config.service.name`,
  `status` = `UP`, ISO 8601 `timestamp`, `correlationId` jako uuid,
  `components` právě `mq`/`redis`/`db`/`cache` s tri-state hodnotami
  (`mq` = `healthy` u připojeného klienta), a šest polí celkem — nic navíc.
- **První tep synchronně** — po `initialize()` je ve frontě
  `infrastructure.health.checks` právě jedna zpráva, ještě před uplynutím
  prvního intervalu.
- **Odpojené MQ** — publish selže, `initialize()` doběhne
  (`isInitialized === true`), selhání je zalogováno, publisher běží dál
  a `_mqHealth()` hlásí `unhealthy`; kontrolní případ: u připojeného MQ žádný
  chybový záznam.

`tests/unit/ServiceWrapper.noHttpListener.test.js` dostal runtime důkaz k těm
statickým: špeh na `net.Server.prototype.listen`, `http.createServer`
a `https.createServer` po celou dobu `initialize()` → 0 volání, plus kontrolní
případ, který na týchž špehách skutečný port otevře a zavře — nula nahoře je
tedy měření, ne ticho (`automation-gates.md` §5).

`tests/unit/__mqDouble.js` (sdílený dvojník MQ klienta) umí nově `publish()`
se záznamem a nese `DisconnectedMQClient` pro cestu selhání.

### Heartbeat nese verzi služby

`startHealthHeartbeat()` verzi zahazovalo (`void serviceVersion`), protože
`createBaseClientAdapter` parametr `version` neměl a `createHealthPublisher`
razítkoval vlastní default `1.0.0` — každý biz heartbeat tedy hlásil verzi,
kterou služba neběží (norma `docs/biz/00-model/service-shape.md`
§ Heartbeat payload to vedla jako defekt wrapperu). Opraveno u zdroje:
`@onlineapps/infrastructure-tools` má `version` povinný a adaptéry ho předávají
(BREAKING, tamní commit d.119b-1); wrapper ho posílá jako šestý argument.

Dokud je zde připnutá `@onlineapps/infrastructure-tools` 1.2.10, která šestý
argument nezná, zůstává tvrzení o `version` v sadě jako `it.failing` — po pinu
2.0.0 ten běh zčervená (což je u `it.failing` signál „tělo prošlo") a `.failing`
se odstraní.

### FÁZE 0.1b closes where the check runs — the phase book no longer adjourns without a verdict

`initialize()` announced `[FÁZE 0.1b] Operations-Handler Alignment - DEFERRED`
next to FÁZE 0.1 and never came back to it. The check itself,
`_validateOperationsHandlerAlignment()`, runs unconditionally from
`_initializeInvocationPipeline()` once the connectors exist, and reported its
result as a plain `logger.info('operations-handler alignment verified')` — not
on the `init:PHASE_*` rail. Anyone reading only the phase book saw a phase the
boot opened and never closed, and a boot stopped by a misaligned handler closed
`FÁZE INIT` as FAILED without ever naming 0.1b.

The `DEFERRED` line is gone (nothing read that status: no consumer in
`api/shared`, `api/infra`, `api/scripts`, `api_biz/*` or `fe_adminui` parses
`PHASE_*` at all — it is an operator-facing record). The phase is now written
by the method that IS the phase:

- alignment holds → `[FÁZE 0.1b] Operations-Handler Alignment - PASSED`, with
  the real duration;
- a declared operation without a handler, or a handler without a declaration →
  `FAILED` carrying the error, then the throw travels to `initialize()`'s outer
  catch, which closes `FÁZE INIT` — the boot reporting its own outcome, not a
  second report of 0.1b.

Phase numbering was never chronological (0.12 precedes 0.15 and 0.2; 0.10 follows
0.7) and nothing in the workspace parses or orders these records, so 0.1b keeps
the identifier every document cites while closing after 0.12.

`operations-handler alignment verified` with its `count` field is unchanged: it
is the operator's smoke match against `[HandlerRegistry] validated N handler(s)`
per `docs/biz/90-migration/v2-to-v3.md` §3.9, a different fact from the verdict.

Evidence: `tests/integration/phase-book.integration.test.js` boots a real
broker-free fixture service and asserts the whole book (0.1b closed exactly once
as PASSED, no `DEFERRED` record anywhere, every other phase in its place with its
verdict); `tests/unit/ServiceWrapper.alignmentPhase.test.js` forces the
divergence a real boot cannot produce and asserts the FAILED record for both
directions, plus the unchanged count line.

### BREAKING — `workflow_id` is the only spelling, and it is never invented

`docs/biz/70-contracts/workflow-message.md` §2.2 retired the camelCase aliases
on 2026-09-03 (owner decision, `governance/confirmations/workflow-message-aliases.md`
001) and names this file's reads among the removals handed to their owners.
Re-measured 2026-09-07: `api/infra/api_gateway/routes/workflow.js` builds its MQ
body with `workflow_id`, and `workflowId` appears only in the AMQP headers —
beside the gateway's own note that consumers do not parse headers. The comment
this wrapper carried, "Gateway sends workflowId, not workflow_id", was false,
and every `|| message.workflowId` behind it read a spelling nothing writes.

Seven such reads are gone. Two behaviour changes follow:

- a workflow message without `workflow_id` is refused with a message naming the
  field, the retirement date and the contract, instead of being accepted under
  its camelCase name;
- `_dispatchViaInvokeOperation` no longer ends `|| operationName`. That fallback
  handed the operation name to anything reading `ctx.workflow_id` — including
  `content-resolver`, which builds `content/<workflow_id>/<step_id>` from it, so
  every invocation of one operation wrote into the same "workflow" directory.
  Absent now, `ContextBuilder` refuses the invocation and names the field.

`current_step` keeps its fallback to the cookbook's first step: that is the
cookbook speaking, not a default invented here. The normalisation moved into
`_normalizeWorkflowMessage()` so the refusal has a seam a test can drive.

One `workflowId` remains, at the error-handler call: it is
`@onlineapps/error-handler-core`'s own field name (`UnifiedLogSchema` reads
`context.workflowId`), carrying the snake_case value. Commented as such.

**Boot-visible message change**: services that publish a malformed workflow
message now see `[ServiceWrapper] Missing required field - workflow_id is
required on a workflow message. …` instead of `Workflow message must have
workflow_id or workflowId field`.

### BREAKING — `OperationContext` carries `step_id` (16 fields, was 15)

`@onlineapps/content-resolver` stores every file under
`content/<workflow_id>/<step_id>` and refuses to invent either half
(`storagePathPrefix()`, `CONTENT_CONTEXT_STEP_ID_MISSING`). A handler that
stores anything therefore needs the step id — and the context did not carry one.

The value was never missing upstream: `WorkflowOrchestrator` passes
`step_id: stepId` into `invokeOperation` and its constructor contract has named
that field all along. The wrapper dropped it: `ContextBuilder` never read it and
`OperationContext` had no field for it. So the only file-descriptor producer in
the workspace, `api_biz/pdfgen/src/handlers/pdf.js`, passes
`step_id: ctx.operation_name` — an operation name standing in for a step id,
which collides whenever one cookbook calls the same operation twice: two steps,
one storage prefix, the second overwriting the first.

`ContextBuilder` now validates `mqMessage.step_id` beside `workflow_id` and
`correlation_id` — non-empty string, `InvalidEnvelopeError` (400, the caller's
fault) naming the field and the fix — and passes it into the context and into
the scoped logger's bindings. `_dispatchViaInvokeOperation` reads it off the
workflow message (`current_step`, or `step.step_id`) the same way the
orchestrator branch does. It is deliberately NOT defaulted to the operation
name: that is the stand-in this change exists to remove.

**BREAKING** for anything that builds an `mqMessage` or an `OperationContext` by
hand — a message without `step_id` is now refused. Every in-tree caller is
updated; `api_biz/pdfgen` should switch `step_id: ctx.operation_name` to
`step_id: ctx.step_id` when it picks up the new wrapper.

### A permanently lost MQ connection now stops the service

`@onlineapps/mq-client-core` spends a finite reconnect budget and then declares
the connection permanently lost — it stops trying, reports `isConnected() ===
false`, and calls `onFatal`. It deliberately does not end the process; the owner
of the lifecycle does. That owner is this wrapper.

Phase 0.3 now registers that handler on the client it builds. The handler logs
the reason with `code` and `attempts`, tears down through the **same**
`shutdown()` SIGTERM uses, and exits `1` so the restart policy boots the service
against a re-verified environment. A teardown that throws is logged and the exit
still happens — a stuck cleanup must not keep a dead process alive.

Every runtime exit now shares that implementation (`_shutdownFatally`), per
`change-discipline.md` § One rail per concern: the fatal MQ loss, the registry
heartbeat giving up after `registry.maxHeartbeatFailures`, and re-validation
exhausting every attempt. Each used to exit on its own terms — the heartbeat
left MQ, cache, state and secrets open and the service still registered, and
blamed "the MQ channel" for a failure of `registryClient.sendHeartbeat`, which
can fail with MQ perfectly healthy.

`_deregisterAndShutdown()` is gone: it was a second teardown that deregistered
and then exited with every other connector open. `shutdown()` already
deregisters — `unregister()` is an alias of `deregister()` in
`@onlineapps/conn-orch-registry`.

### One teardown, and it is `shutdown()`

`_cleanupBeforeRestart()` is gone. The measurement that decided it: the two
teardowns were not nested in either direction.

| | old `_cleanupBeforeRestart()` | old `shutdown()` |
|---|---|---|
| cancel registry + 3 workflow consumers | ✅ | ❌ |
| close the registry's own MQ connection | ✅ | ❌ |
| null the consumer tags | ✅ | ❌ |
| clear the heartbeat timer | ❌ | ✅ |
| quit discoveryRedis | ❌ | ✅ |
| deregister from the Registry | ❌ | ✅ |
| MQ / cache / state / secrets disconnect | ✅ | ✅ |

So a service that failed to boot left a heartbeat timer running and stayed
registered, and a SIGTERM left consumers attached and the registry's MQ
connection open. `shutdown()` now does the union, every step tolerant of a
resource that was never created, and the boot path calls it.

One documented exception: a failure of **phase 0.12** tears nothing down. That
phase builds the logger and runs before any connector exists — there is nothing
to release, and no logger to release it through. Keyed on the phase, a fact, not
on whether some object happens to be truthy.

**BREAKING for hand-built configs**: a boot failure now deregisters from the
Registry, which the old boot cleanup did not do.

### The pre-step health check is configuration, not environment

`if (runtimeCfg.get('debug') || runtimeCfg.get('nodeEnv') === 'development')`
decided whether to health-check the MQ transport before each workflow step —
behaviour steered by ambient environment, which `architecture-principles.md` §8
names as the anti-pattern. It ran on every workflow message in dev and never in
production, with nobody having chosen either.

It is now `wrapper.mq.healthCheckBeforeWorkflowStep`, default `false` in
`config/runtime-defaults.json`, and the method owns the decision so there is one
place to read. Kept rather than deleted because its result has real consumers
(two log lines, plus the transport's health report/critical callbacks and
events) and because it is the only thing that surfaces what `onFatal` cannot:
closed channels, and consumers whose `consumerTag` is gone. Off by default
because it runs on the hot path and queries the broker per tracked queue.

The `debug` key went with the branch: `src/config.js` and `src/defaults.js` no
longer declare it, and `DEBUG` is no longer read anywhere in this package.

### `prefetch` comes from the config file too

`prefetch || 10` was the last inline copy of a default that already ships in
`config/runtime-defaults.json`. It also silently rewrote an explicit `0` —
unlimited, in AMQP terms — to `10`. It now reads through `_requiredMqSetting()`
like the boot-retry budget: `0` is honoured, and a config assembled by hand that
skips the loader fails with the key and the file named.

### Removed — the MQ `health:shutdown` policy that could never apply

The wrapper passed `mq.healthCheckInterval`, `mq.criticalHealthShutdown` and
`mq.criticalHealthShutdownDelay` to the MQ client and listened for the
`health:shutdown` those keys arm — while also passing `healthCheckEnabled:
false`. The transport emits that event from `_performHealthCheck()` alone, which
under this configuration runs only from `_runHealthCheckBeforeWorkflowStep()`:
gated on `DEBUG || NODE_ENV === 'development'` **and** on a workflow message
arriving, which is precisely what stops arriving when MQ is unwell. In
production the three keys and the listener were inert — a shutdown policy that
read as a guarantee and was none (`change-discipline.md` § Removing something
removes its declaration). `healthCheckInterval` was already dead by its own
comment: "Not used when healthCheckEnabled=false". They are removed from the
code and from `docs/CONFIGURATION_GUIDE.md`; no service config in the workspace
set any of them.

This is not a claim that the check is worthless. `_performHealthCheck()` also
measures closed channels and consumers whose `consumerTag` is gone — a service
that is connected and silently receiving nothing, which `onFatal` does not
cover. Arming it is a gate, and a gate lands after evidence that every service
passes it (`automation-gates.md` §3), not as a side effect of this cleanup.

### Boot retry budget comes from the config file, not from an inline copy

`_initializeMQ()` read `connectionRetries ?? 10`, `connectionRetryDelayMs ??
2000` and `connectionRetryMaxDelayMs ?? 30000` — a second copy of three values
that already ship in this package's `config/runtime-defaults.json`, which
`ConfigLoader` deep-merges under every service config. The inline copies were
therefore never reached through the loader, looked authoritative, and were free
to drift from the file that actually decides. They are gone; a config assembled
by hand that skips the merge now fails with the key named and the file named,
rather than silently running someone's guess.

The boot budget and the transport's `RABBITMQ_MAX_RECONNECT_ATTEMPTS` stay two
separate settings, documented as such in the README: nothing is lost during
boot, and nothing is being waited for at runtime.

Why it matters, measured 2026-09-07 against an isolated broker: before that
contract existed, a client whose broker went away retried, gave up after 10
attempts, and then ran forever — `isConnected()` still `true`, no recovery when
the broker came back 40 s later, no exit. A green healthcheck over a process
that could not do any work.

**Requires an `@onlineapps/mq-client-core` that provides `onFatal(callback)`.**
Phase 0.3 refuses to boot against one that does not, naming the missing method
and the fix, rather than failing later with a `TypeError` inside a callback. The
version currently pinned here (2.0.1) predates it; the pin bump lands with this
change.

### BREAKING — `getStatus().components.mq` is a state, not a boolean

`getStatus()` reported `this.mqClient?.isConnected() || false` while the
heartbeat reported `'healthy'|'unhealthy'|'unknown'`: two answers to one
question, disagreeing exactly where it matters. `|| false` turned "no client
built yet" into "the connection is DOWN", and the heartbeat branch reported a
client without `isConnected()` as *healthy* — an assumption dressed as a
measurement.

Both now read `_mqHealth()`, the single owner, which returns
`'healthy' | 'unhealthy' | 'unknown'`. Not-initialised is `'unknown'`, never
`'unhealthy'`: a service still booting has not failed. No production consumer of
`getStatus()` exists in the workspace (only `tests/run-tests.js` prints it), so
the shape change costs nothing today — but it IS a shape change.

**BREAKING.** The boot order changed and monitoring became mandatory (below). No
exported API moves: `BUSINESS_ERROR_BRAND`, `isBusinessError`, the error classes,
`ServiceWrapper` and `bootstrap` are untouched. What changes is startup
behaviour, one field inside `conn-runtime/validation-failure.json` — a runtime
file this package writes and reads alone — and one field that disappears from the
loaded config (below).

### BREAKING — `HandlerLoader` and `HandlerRegistry` hold callers to the platform logger contract

Both classes are exported from this package, and both used to declare a narrower
logger than the platform's — `HandlerLoader` said `debug/info/warn`,
`HandlerRegistry` said `debug/info` — while checking only that the value was
truthy. So the message stated a rule nothing enforced, and a two-method stub
reached `resolve()` and `validate()` unnoticed. Both now call `assertLogger()`
from `@onlineapps/logger-contract`: four callable methods, refused in the
constructor.

Marked breaking because a caller passing a logger with fewer than four methods
now fails where it used to construct. The one known external consumer,
`api_biz/converter`'s `tests/integration/error-envelope.test.js:51`, passes five
and is unaffected; inside this package `ServiceWrapper.js:2274` passes
`this.logger`, which phase 0.12 guarantees.

`HandlerRegistry.trace()` is untouched: it stays optional and is still probed at
the call site, because it is not part of the contract.

Both messages change to the contract's shape — `[HandlerLoader] logger is
required - Expected: a logger with info/warn/error/debug, so … Fix: pass
options.logger …` — which also gives `HandlerRegistry` the Expected/Fix half its
old one-line message never had.

### Changed — the four-method list comes from `@onlineapps/logger-contract`

`src/logger.js` declared its own `REQUIRED_METHODS`, the fourth copy of a
constant three other packages also carried. It now reads `LOGGER_METHODS` from
`@onlineapps/logger-contract` (exact pin `1.0.0`), per owner confirmation
`api/docs/governance/confirmations/connector-logger-contract.md` 004.

`setLogger()` keeps its own check and its own messages: it refuses a non-object,
an array or `null` by name before asking about methods, which `assertLogger()`
does not do. Only the list is shared, so no message and no accepted value
changes — the two `join('/')` renderings are identical strings.

### BREAKING — the logger exists before the boot can block (DÁVKA 101a)

Owner decision, variant A:
`api/docs/governance/confirmations/wrapper-boot-logger.md` 001.

**New phase 0.12 — Monitoring & Logger**, placed right after configuration and
therefore before the infrastructure gate in phase 0.4. Monitoring used to be
initialised in an unnumbered block after phase 0.7.

Why it mattered: the gate blocks the boot for up to
`wrapper.infrastructureGate.maxWaitMs` (300 000 ms in the platform's config), and
its progress messages are the only thing that tells an operator WHY a service is
not starting. Those messages went to the bare console, because `this.logger` did
not exist yet. `api_biz/meta/index.js:20-34` had already refused to accept the
shape and stopped its own boot instead, calling it "silent degradation sold as
normal operation".

- **A failing monitoring init is now a permanent boot failure**, reported as
  `[FÁZE 0.12] Monitoring & Logger - FAILED` and exiting. It used to be caught and
  logged as `Monitoring initialization failed (non-critical)`, after which the
  service ran its whole life with no central logging.
- **`wrapper.monitoring.enabled = false` is refused** with a message naming the
  supported alternative: `monitoring.mode = 'off'` runs without telemetry export
  and still provides the logger. No `api_biz/*` service used the flag; it lived
  only in this package's own tests.
- **`bootstrap()` no longer guards the logger injection.** `setLogger(wrapper.logger)`
  used to sit behind `if (setLogger && wrapper.logger)`, so a service whose
  monitoring had failed skipped it silently and every `getLogger()` consumer threw
  later, far from the cause.
- **The gate's console fallback is gone**, together with 5 × `this.logger || console`
  — four of which handed `console` to a connector as its logger — and 87 of the
  90 `this.logger?.`. The three that remain are in `_logPhase`, which reports
  phases 0.1/0.1b and a failed 0.12, i.e. the only points where the logger can
  legitimately be absent.

No circle was created, measured before the change: `MonitoringConnector.init()`
resolves in 23 ms against an unreachable broker, because the logger's RabbitMQ
connect is fire-and-forget and the exporter connects lazily. It needs
configuration and a writable `logs/` — not the infrastructure the gate waits for.

Migration for a service: nothing, unless it set `wrapper.monitoring.enabled = false`
(no biz service did) — replace it with `wrapper.monitoring.mode = 'off'`.

### Fixed — the error handler is handed the wrapper's logger

`_initializeOrchestrator` built `ErrorHandlerConnector` without a `logger`.
`@onlineapps/conn-infra-error-handler` now requires one and validates it in the
constructor, so the first pin of that version would have turned every biz
service's boot into a throw. The wrapper already holds a contract logger there —
phase 0.12 assigns it long before this runs — and now passes that one on rather
than building a second.

The `!this.logger` guard moved ahead of both constructions. Left where it was,
between the error handler and the orchestrator, the connector's own constructor
would throw first and the wrapper's message would be unreachable.

Not a breaking change for consumers: no signature, config key or exported API
moves. Owner confirmation:
`api/docs/governance/confirmations/connector-logger-contract.md` 001–004.

### Removed — the no-op module-logger setter in `bootstrap()` (DÁVKA 101a-bis)

`bootstrap()` used to end its logger resolution in `catch (e) { setLogger = () => {} }`.
That no-op is a fallback with no consumer (architecture-principles.md §3): it made
"this service has no module logger" and "this service's logger module is broken"
the same outcome, and after phase 0.12 it was the one remaining way to reach a
running service whose `getLogger()` throws.

Resolution is now three explicit branches:

1. `options.setLogger` — validated as callable, refused with `[bootstrap]
   options.setLogger is not callable - …` before the wrapper is built (it used to
   fail at the call, after `initialize()`, as a bare TypeError).
2. otherwise `<serviceRoot>/src/lib/logger.js` **if it exists** — `fs.existsSync`,
   not a `try/catch` around the `require`, so a module that throws surfaces ITS
   error. A module without a `setLogger` export is refused with `[bootstrap]
   Module logger without a setter - Expected: <path> to export setLogger(logger) …`.
3. otherwise **no module logger** — the service uses `ctx.logger`, and the boot
   records it once: `[bootstrap] no module logger — service uses ctx.logger`.

The setter stays optional, because measurement says it must: of the eight biz
services, six provide one through `src/lib/logger.js`, converter passes
`options.setLogger`, and `biz-property` has neither by design — it logs through
`ctx.logger` from the OperationContext, with no `getLogger()` consumer.
§3 forbids a fallback, not an option.

Migration for a service: none for the seven; `biz-property` gains one info line at boot.

### Removed — the `|| ''` fallback on `specificationEndpoint` (DÁVKA 79 B)

`_initializeRegistry` read `this.config.service?.specificationEndpoint || ''`.
The branch could never fire: `ConfigLoader.js:360-365` refuses a config that does
not declare the field, and all eight biz services build their config through
`ConfigLoader.loadAll` (`src/config/index.js` in converter, emailer,
hello-service, ingest, invoicing, meta, pdfgen, property), with `bootstrap()`
passing it straight through (`src/index.js:81`).

A fallback that cannot fire is not harmless — it tells the reader that an empty
endpoint is an expected state, while the registry warns about exactly that case
(`api/infra/api_services_registry/src/listeners/registry.listener.js:474-477`).
`architecture-principles.md` §3.

Behaviour is unchanged, which is the point: the new control suite
`tests/unit/ServiceWrapper.specificationEndpoint.test.js` is green before and
after, and asserts the value reaching the registry client verbatim.

### Removed — `config.service.port` is no longer parsed (DÁVKA 77 B)

ADR 0005 removed the HTTP surface from biz containers. `ConfigLoader` parsed
`service.port` into the loaded config anyway, and nothing read it back: measured
2026-08-30, the only `.port` reads in `src/` are the Redis/state URL ports at
`ServiceWrapper.js:1495`, `:1525`, `:1530`. A declaration nothing reads tells the
reader something depends on it — `change-discipline.md` § "Removing something
removes its declaration".

- `cfg.service.port` is gone. A config that still declares one loads fine; the
  field is simply not carried forward. `conn-orch-validator` is the component
  that tells the author to delete it (`ORPHANED_CONFIG_FIELD`, warning).
- `service.specificationEndpoint` stays REQUIRED and is UNCHANGED. It looked like
  the same class of orphan — `ServiceWrapper.js:1295` even says ADR 0005 left it
  without a consumer — but it has one: `api_services_registry` reads it off the
  register payload (`listeners/registry.listener.js:354`, `:474-477`, `:542`,
  `:626`, `:730`, `:813`, `:1108`) and stores it in the Redis summary entry
  (`models/microservice.model.js:166-177`); the heartbeat listener reads it too
  (`listeners/heartbeat.listener.js:30`, `:49`). The comment at `:1295` is wrong
  about that, and the requirement stands.

### Fixed — the restart cooldown is keyed on the CAUSE, not on time alone

Measured on `api_service_emailer`, 2026-08-29: the cookbooks were repaired and
the container restarted, but the failure file still said `attemptCount: 240`, so
`_ensureValidationProof` slept `min(REVALIDATION_SLOW_MS - since, 300000)` — five
minutes — before FÁZE 0.2. The boot looked wedged at 0.15, because the only
announcement went to `this.logger?.info` and stdout said nothing.

- `_writeValidationFailureFile` records `causeFingerprint`: the platform hash
  (`FingerprintUtils.generateContentFingerprint`, never a new one) over every
  `.json` under `config/service/` and `tests/cookbooks/` — what Tier-1
  validation actually judges.
- `_applyRestartCooldown` compares it at startup. Cause changed → the failure
  file is deleted, no wait, and the attempt count starts from zero. Same cause →
  the previous behaviour, unchanged.
- **A file written before this version has no fingerprint, which is not the
  current one, so it takes the "cause changed" branch.** There is no
  compatibility branch and no migration: a missing fingerprint is a changed
  cause (`architecture-principles.md` §11).
- Both branches now print through `_logStartup`, which writes to the structured
  log AND to stdout — the same two channels `_logPhase` uses for every
  `[FÁZE …]` line. A five-minute wait that announces itself on a channel the
  container does not show is not an announcement (`automation-gates.md` §5).
- The failure file is re-read AFTER the cooldown. Reading it before carried the
  old count into the write that follows, so a reset lifted the cooldown for
  exactly one boot and then reinstated it.

### Changed — one orchestrator factory, two callers

`_executeRevalidation` built a `ValidationOrchestrator` per attempt;
`_ensureValidationProof` built one and reused it across all six startup
attempts. Both arrived in the same commit (`869a346a`, 2026-03-26) — the
difference was a local convenience, not a design.

The reuse was not free: dávka 49 had to add `resetResults()` to
`CookbookTestRunner` because the shared runner accumulated failed cookbooks
across those six attempts, so the verdict could never come back true inside one
process. Nothing needs state between attempts, so `_createValidationOrchestrator()`
is now the single construction site and every attempt gets a fresh instance. The
injected orchestrator (`options._validationOrchestrator`) is still returned as-is
— it is the test seam.

### Added — `@onlineapps/service-validator-core` 1.0.14 as a declared dependency

`FingerprintUtils` is required directly now. It was already installed
transitively through `@onlineapps/conn-orch-validator`; requiring it without
declaring it is the §20 defect the new G6 gate rejects.

### Fixed — the heartbeat no longer invents an identity

`startHealthHeartbeat()` defaulted the two facts that identify the service:
`service.name || 'unnamed-service'` and `service.version || '1.0.0'`. Both are
fallbacks (`architecture-principles.md` §3), and of the costly kind: a heartbeat
published as `unnamed-service@1.0.0` is not a missing heartbeat but a WRONG one —
the monitoring stack records a service that does not exist while the real one
looks silent.

Not dead branches either. `_ensureValidationProof` fail-fasts on both keys, but
it runs only when `serviceRoot` is set and `wrapper.validation.enabled !== false`
(`ServiceWrapper.js:831`), so a service with validation off arrived here with
neither key ever checked. Both now fail fast with
`[ServiceWrapper] Missing configuration - service.<key> is required`, in the
shape `_registerService`already uses one method away.

### Fixed — the guard test failed by hanging instead of failing

The `_executeRevalidation` guard added above did catch a deleted local, but its
`catch` scheduled the next attempt through a REAL `setTimeout` (30 s … 30 min).
Jest then waited on the open handle, so the run froze instead of reporting the
failure — measured by the lead: five minutes, no output. The test now records
the scheduling instead of arming it, and asserts the success path never reaches
the scheduler at all. Under the mutation it fails in 2 ms.

### Fixed — one owner for "who am I"

`this.config.service?.name || 'unnamed-service'` stood in **thirteen** places.
Each looked harmless; together they were a policy nobody decided — a wrapper with
no configured name kept running and told the registry, the MQ layer, the
heartbeat and every log line that it was `unnamed-service`. Not a missing name: a
WRONG one, and the same defect the heartbeat fix above removed, twelve more
times (`architecture-principles.md` §3).

`_serviceName()` and `_serviceVersion()` are now the only readers and they fail
fast with `[ServiceWrapper] Missing configuration - service.<key> is required` —
a message that is defined exactly once. Three duplicate guards that said the same
thing in their own words (`_initializeOrchestrator`, `startHealthHeartbeat`,
`_ensureValidationProof`'s "Service name is required for validation") now defer
to it.

Two readers stay direct, and each is named in a test:

- `_logPhase` — **the only path that may not throw.**
  `_handleInitializationError` reports a failed phase through it, and a phase can
  fail precisely because the configuration never loaded; throwing there would
  replace the error being reported with a second one and the operator would never
  see the first. It uses `this.config?.service?.name ?? '<config not loaded>'` —
  not an invented identity but a statement ABOUT the configuration, in a message
  whose subject is the configuration failing.
- `_initializeState` — `state.serviceName || service.name` is a different
  contract; its error names both keys.

`service.url || ''` is unchanged: ADR 0005 says a biz service has no URL.

### Tests

`tests/unit/ServiceWrapper.serviceIdentity.test.js` — 14 cases: the getters
return and throw by value; the initialization-error path keeps BOTH facts (the
original cause and that the name was unknown); `unnamed-service` appears nowhere
in the code; exactly three direct readers, each identified; each message defined
once; and a control that the ADR 0005 url default survives.

`tests/unit/ServiceWrapper.validationFailureFile.test.js` — 18 cases: the
fingerprint moves for a cookbook, for `operations.json` and for an added
cookbook and NOT for an unrelated file; the three cooldown branches; the
pre-fingerprint file; the reset that really starts from zero; no failure file;
fewer failures than the fast backoff; an elapsed cooldown; the factory returning
a new instance per call and the injected one unchanged; a single construction
site in the source. Plus a guard for the class of defect 3.4.2 hot-fixed — a
dangling local left by a cleanup pass — by driving `_executeRevalidation` far
enough to register the service.

`tests/unit/ServiceWrapper.heartbeatConfig.test.js` — 7 cases: each missing key
by name, an absent `service` section, the publisher never constructed under a
made-up identity, a complete identity starting it under its real name, the older
`mqClient` guard still firing first, and the retired default absent from the
source.

Suite: 17 suites, 228 passed (was 189).

## [3.4.3] — 2026-08-17

**Ajv config: `allowUnionTypes: true`.**

Every biz `operations.json` in `api_biz/*` uses `type: [X, "null"]` union types for nullable fields — a widely-accepted JSON Schema idiom. Ajv `strict: true` without `allowUnionTypes` rejects them and blocks service boot at `SchemaValidator.precompile` with:

    strict mode: use allowUnionTypes to allow union type keyword ...

3.4.3 keeps strict mode (still rejects unknown keywords etc.) but explicitly allows union types. No other behaviour change.

Discovered during Fáze F5 live-rollout: property, converter, ingest, meta all have nullable output-schema fields using unions. Blocked their boot after F3 bump. Fix here is server-side (validator config).

## [3.4.2] — 2026-08-17

**Hot-fix: dangling `serviceUrl` reference at line 2649** left by 3.4.1's guard-removal pass. `_ensureValidationProof` still passed `serviceUrl` to `ValidationOrchestrator` after the `const serviceUrl = ...` declaration was deleted. Result: every biz that force-recreated to 3.4.1 hit `Phase 0.2 Tier 1 Validation - FAILED: serviceUrl is not defined` and stalled.

Fix: reintroduce `const serviceUrl = this.config.service?.url || ''` — kept as local so downstream still gets the value (empty string when config omits URL; used only for telemetry/log annotation by ValidationOrchestrator).

Verified: property/meta/converter recreated post-3.4.2 pass Phase 0.2.

## [3.4.1] — 2026-08-17

**Patch: drop stale port/url/specificationEndpoint guards left over from A2.2.**

Under 3.4.0 the wrapper still refused to start when `config.service.port`, `config.service.url`, or `config.service.specificationEndpoint` were absent — legacy checks from `_ensureValidationProof` and `_initializeRegistry` that predated ADR 0005 and were not removed together with the Express init in A2.2. Live rollout hit the failure on first biz restart (biz-property Phase 0.2: "Service port is required for validation").

Removed:
- Port + URL guard in `_ensureValidationProof` (before Tier-1 validation runs).
- Port + URL guard in `_initializeRegistry` (before Registry registration).
- specificationEndpoint guard in `_initializeRegistry`.

Behaviour changes:
- `serviceInfo.url` sent to Registry is now the empty string (was: required non-empty).
- `specificationEndpoint` sent to RegistryClient is `''` when absent (was: throw).
- Consumers: none — Registry stores the ops spec directly from the register payload; the URL and endpoint fields have had no live reader since 3.4.0 A2.

Fully compatible with 3.4.0 tests (98/98 GREEN) and with 3.4.0 biz configs that still declare `port`/`url`/`specificationEndpoint` in `config/service/config.json` — they're just no longer required.

## [3.4.0] — 2026-08-17

**Zero-HTTP shape for biz services** (ADR 0005 landed). See `api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md`.

### BREAKING

- **Constructor:** `options.app` and `options.server` no longer required. Passed values are accepted for transitional callers but IGNORED — the wrapper never binds an Express app or HTTP server.
- **`bootstrap()`:** signature `bootstrap(serviceRoot, options)` no longer reads `options.app`, no longer resolves `require('./src/app')`, no longer calls `app.listen(PORT)`. Return value shape changed from `{wrapper, server}` to `{wrapper}` (no server exists).
- **Removed HTTP endpoints:** `GET /`, `GET /health`, `GET /info`, `GET /status`, `GET /specification`. These no longer mount on any Express app. Consumers relying on HTTP-polling `/health` (e.g. Registry `businessReadinessChecker` pre-3.4.0) MUST switch to reading `infrastructureHealthTracker` state populated from the MQ heartbeat.
- **Removed methods** (were private, listed for grep-completeness): `_setupHealthChecks`, `_setupInfoEndpoint`, `_getValidationInfo`, `_getStandardLevel`, `_getInfraFingerprint`, `_extractExpressRoutes`.
- **Removed test suites**: `tests/unit/ServiceWrapper.healthDependencies.test.js` (whole file tested the retired HTTP `/health` handler). `tests/unit/ServiceWrapper.test.js` describes `/info endpoint` and `Operations-Routes Alignment` deleted (tested removed methods).

### Added

- **`wrapper.startHealthHeartbeat()`** — new public method. Wires the biz service into the existing MQ heartbeat mechanism (`createBaseClientAdapter` from `@onlineapps/infrastructure-tools`). First heartbeat is emitted synchronously by publisher `start()`.
- **`wrapper.stopHealthHeartbeat()`** — cleanup counterpart; called from `_cleanupBeforeRestart`.
- **`wrapper.refreshDependencyHealth()`** — periodic refresh of cached `_lastDbHealth` / `_lastRedisHealth` (was a side effect of `_setupHealthChecks`; now standalone).
- **`wrapper._collectHeartbeatComponents()`** — private helper returning `{mq, redis, db, cache} → 'healthy'|'unhealthy'|'unknown'` map, matching the shape `infrastructureHealth.listener` validates.
- **Bootstrap phase 0.10** — Health Heartbeat Publisher startup (documented in `api/docs/biz/50-lifecycle/tier1-validation.md`).
- **New standard level v1.3** — Zero-HTTP Shape (documented in `api/docs/biz/00-model/service-shape.md`).

### Dependencies

- New: `@onlineapps/infrastructure-tools@1.2.4` (exact, provides the `createHealthPublisher` factory this version wires into).

### Migration for biz services

Every biz service under `api_biz/*/` has also been updated in the same session:
- `docker-compose.yml` / `docker-compose.production.yml`: removed `ports:` section
- `Dockerfile`: removed `EXPOSE` and `HEALTHCHECK`
- No changes needed to handlers (they never touched Express directly)
- `src/app.js` remains in each biz repo for now (bootstrap doesn't read it; deletion is a follow-up cleanup)

Rollout order:
1. `npm publish @onlineapps/service-wrapper@3.4.0`
2. In each `api_biz/*/package.json`: exact `"@onlineapps/service-wrapper": "3.4.0"` + `npm install`
3. Restart containers; verify `docker port <biz>` empty and `redis-cli KEYS 'infrastructure:health:*'` shows entries.

### Rationale

- Removes the last HTTP surface in biz containers. `_probeServiceHealth` (Registry) was the sole automated consumer; replaced by tracker reads.
- Reduces service-wrapper code by ~950 lines net.
- Unifies biz + infra service liveness path (same queue, same tracker, same Redis projection).

## [3.3.5] — Pre-3.4.0 baseline

Last version with Express-mounted health/info/status endpoints. No changelog for versions prior to 3.4.0.
