@onlineapps/conn-orch-registry 6.0.2 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,538 @@
1
+ # Changelog — @onlineapps/conn-orch-registry
2
+
3
+ All notable changes to this package. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [7.0.0] — 2026-09-27
8
+
9
+ pin: `@onlineapps/mq-client-core` 4.2.0 → 5.0.0
10
+
11
+ ### Documentation — `API.md` je součástí tarballu (d.1039)
12
+
13
+ `files` teď jmenuje `API.md`; dokument ležel v balíčku, ale do vydaného artefaktu se nedostal.
14
+
15
+ ### Changed — BREAKING — odpověď na registraci chodí na frontu instance `<service>.registry.<instance_id>` (d.817c)
16
+
17
+ Konfirmace `registry-instance-identity` 001 (c): odpověď na registraci jde na frontu TÉ kopie, která
18
+ se registrovala, přes existující `responseQueue`. Sdílenou `<service>.registry` dostávají od registru
19
+ jen podněty. Klient teď:
20
+ - ve zprávě `register` posílá `responseQueue: "<service>.registry.<instance_id>"` (dřív
21
+ `"<service>.registry"`); nová vlastnost `instanceResponseQueue`;
22
+ - v `init()` konzumuje DVĚ fronty, obě s `requeueOnError: false`: sdílenou `<service>.registry`
23
+ (podněty registru, např. `revalidate`, které `api_services_registry` posílá na
24
+ `<serviceName>.registry`) a frontu instance (odpovědi);
25
+ - hlášku o vypršení registrace jmenuje frontou instance a log `Initialized` nese `promptQueue` a
26
+ `responseQueue`.
27
+
28
+ Heartbeat a `deregister` se nemění. Pořadí z konfirmace 002 platí: šablona `registry.{instance_id}`
29
+ s dead-letter cestou musí být v pinovaném `@onlineapps/mq-client-core` (d.817b), jinak `consume()`
30
+ frontu instance odmítne. Tato změna proto vyžaduje mq-client-core s d.817b. Server registru
31
+ (`registry.listener.js`, `validationResponse.listener.js`) odpovídá na `msg.responseQueue` ze
32
+ zprávy a změnu nepotřebuje.
33
+
34
+ Proč BREAKING: mění se veřejný tvar zprávy `register` (`responseQueue`), topologie, kterou klient
35
+ deklaruje (nová trvalá fronta na instanci), a minimální `@onlineapps/mq-client-core` (musí nést d.817b).
36
+
37
+ Životnost fronty instance (`auto-delete`, `x-expires`, trvalá) rozhodnutá není (konfirmace 002,
38
+ Conditions; šablona d.817b ji záměrně nenese). Fronta je trvalá a po skončení kopie zůstává.
39
+
40
+ ### Documentation — README, průvodce a komentáře testů ukazují na vlastníka drátu jeho jménem (d.999b, d.999d)
41
+
42
+ Balíček citoval kontrakt registrace starým jménem dokumentu, který v `docs/biz/30-operations/` není.
43
+ Odkazy přitom vedly na `registration-wire.md`. Všechna místa teď jmenují existující uzel a jeho
44
+ sekci:
45
+ - úvod README: `registration-wire.md §2` (MQ wire format) (d.999b);
46
+ - komentář v příkladu `register()` v README a parametr `serviceInfo.operations` v
47
+ `docs/REGISTRY_CLIENT_GUIDE.md`: `§3` (Operation schema);
48
+ - úvod průvodce a komentáře ve třech unit testech: `§2.1` (`register`) (d.999d).
49
+
50
+ ### Changed — BREAKING — konstruktor `ServiceRegistryClient` vyžaduje `instanceId`; `instance_id` jde v `register`, `heartbeat` i `deregister` (d.817)
51
+
52
+ Každá zpráva na drát teď říká nejen, KTERÁ služba, ale i KTERÁ běžící kopie (konfirmace
53
+ `registry-instance-identity` 001): `register`, `heartbeat` a `deregister` nesou `instance_id`.
54
+ Registr podle něj maže jen při shodě, takže krátce žijící cizí kopie už neodhlásí tu živou.
55
+ Konstruktor bez `instanceId` (nebo s prázdným řetězcem) skončí chybou:
56
+ `[RegistryClient] <service>: Missing instanceId - got undefined. Expected: a non-empty string naming
57
+ THIS running copy of the service, the same value for the whole process; … Fix: pass the instance id
58
+ the wrapper made once at boot (SERVICE_INSTANCE_ID when the deployment gives one, otherwise a UUID v4)
59
+ - this client never makes one, because the same value also reaches monitoring-core as serviceInstanceId.`
60
+
61
+ Migrace volajícího: předat `instanceId`, jednu hodnotu na proces. `@onlineapps/service-wrapper` ji
62
+ předává (`this.instanceId`, stejná hodnota jde do monitoringu jako `serviceInstanceId`). Kdo staví
63
+ klienta sám, musí ji doplnit. Registrační odpověď dál chodí do sdílené fronty `<service>.registry`,
64
+ viz komentář u `serviceRegistryQueue` v `src/registryClient.js`.
65
+
66
+ ### Changed — BREAKING — `register()` přijímá jen klíče registračního kontraktu (d.702)
67
+
68
+ `register(serviceInfo)` odmítne klíč, který kontrakt nezná (`registration-wire.md` §2.1). Dřív ho
69
+ tiše zahodil. Povolené klíče: `endpoints`, `operations`, `metadata`, `health`, `workspaceScoped`,
70
+ `timeout`. Hláška:
71
+ `[RegistryClient] <service>: serviceInfo key "foo" is not part of the registration contract - Expected
72
+ one of: endpoints, operations, metadata, health, workspaceScoped, timeout (api/docs/biz/30-operations/
73
+ registration-wire.md §2.1). Fix: delete "foo" from the object passed to register(), or send the value
74
+ under the key the contract names for it.`
75
+ Zaniklé klíče si dál nesou vlastní hlášku (např. `token`: `serviceInfo carries the retired key "token" …`).
76
+ Migrace: z objektu pro `register()` odstranit vše mimo šest klíčů výše.
77
+
78
+ ### Changed — druhý souběžný `register()` pod týmž request id a druhé `startHeartbeat()` se odmítnou, místo tichého přepisu (d.760b, d.760c)
79
+
80
+ - `register()`, dokud čeká registrace se stejným `id`, skončí chybou `[RegistryClient] <service>: Duplicate
81
+ request id "<id>" - a registration with this id is still pending. … Fix: await the register() already in
82
+ flight before starting another one …`. Dřív druhé volání přepsalo záznamy prvního a handle jeho timeru
83
+ zmizel, takže starší timer doběhl celý timeout bez možnosti ho zrušit. S `uuid.v4` se id neopakují,
84
+ takže běžný volající to nepozná.
85
+ - `startHeartbeat()` nad klientem, jehož smyčka běží, skončí chybou `[RegistryClient] <service>: A heartbeat
86
+ loop is already running - … Fix: call client.stopHeartbeat() before starting a new loop, or keep the loop
87
+ that is already running.`. Dřív přepsal handle a první smyčka tepala dál bez možnosti ji zastavit.
88
+
89
+ ### Fixed — `RegistryEventConsumer` bere jméno výměny `registry.changes` z `queueConfig` (d.809); `stopHeartbeat()` ruší jen `setTimeout` (d.752c)
90
+
91
+ - Jméno výměny, na kterou se odběratel změn váže, vlastní `@onlineapps/mq-client-core`
92
+ (`queueConfig.queueName('registry', 'changes')`), ne literál v balíčku. Hodnota se nemění:
93
+ `registry.changes`.
94
+ - `stopHeartbeat()` volal nad týmž handlem `clearTimeout` i `clearInterval`, přestože smyčka je
95
+ rekurzivní `setTimeout`. Druhé volání je pryč, chování se nemění.
96
+
97
+ ### Changed — `CHANGELOG.md` je součástí tarballu (d.997)
98
+
99
+ `package.json` `files` nese `CHANGELOG.md`. Vydaná 6.0.3 ho nenesla.
100
+
101
+ ## [6.0.3] — 2026-09-25
102
+
103
+ pin: `@onlineapps/mq-client-core` 4.1.0 → 4.2.0
104
+
105
+ ## [6.0.2] — 2026-09-24
106
+
107
+ pin: `@onlineapps/mq-client-core` 4.0.1 → 4.1.0
108
+
109
+ ## [6.0.1] — 2026-09-17
110
+
111
+ pin: `@onlineapps/mq-client-core` 4.0.0 → 4.0.1
112
+
113
+ ## [6.0.0] — 2026-09-17
114
+
115
+ ### Changed (testy) — odmítnutá deklarace se tvrdí hodnotou značky (d.579b)
116
+
117
+ `tests/integration/ensureQueuesRefusal.integration.test.js` čekal `cause.code === 406` z doby pinu jádra 3.2.0; pinované
118
+ `@onlineapps/mq-client-core` 4.x značí odmítnutou deklaraci u zdroje (d.582), takže případ teď tvrdí `code: MQ_CONNECTION_FATAL`,
119
+ `reason: broker-refused` a 406 brokeru čte o patro níž (`cause.cause.code`). Sada byla od re-pinu červená (W592 měření).
120
+
121
+ ### Removed — BREAKING: `deployable` odchází z registrační zprávy (d.592)
122
+
123
+ Verdikt shody s uniformou jezdil na registraci od d.221 a **nikdo ho nečetl**:
124
+ `processRegistration` (`api/infra/api_services_registry/src/listeners/registry.listener.js`)
125
+ zprávu destrukturuje bez něj a jiný čtenář na platformě není (`grep -rn deployable`
126
+ nad `api/infra/**` a `fe_adminui/src` vrací prázdno). Slib konfirmace
127
+ `biz-service-manifest` 001 §4 — „registr ho promítne do
128
+ `infrastructure:health:<service>` vedle `status`" — zůstal bez implementace.
129
+
130
+ Čtyři otázky `change-discipline.md` § Removing something removes its declaration:
131
+
132
+ - **Vznik.** d.221 klíč zavedla, aby nasaditelnost dojela z běhu služby k registru.
133
+ - **Role v koncepci.** 001 §4: dvě závažnosti, dva důsledky — `boot` shodí start,
134
+ `deploy` nechá službu běžet a označí ji jako nenasaditelnou.
135
+ - **Proč nikdo nečte.** Projekce nikdy nevznikla, a mezitím se ukázalo, že
136
+ verdikt z běžícího kontejneru nevypovídá o repozitáři: produkční obraz není git
137
+ checkout a nenese compose ani README, takže krok 7 nad ním hlásí 12 nálezů
138
+ nepřítomnosti — na drát by u každé zdravé služby jel `false`.
139
+ - **Náhrada je koncepčnější.** Nasaditelnost dokládá z každého commitu CI job
140
+ `validate-uniform` nad checkoutem (konfirmace `biz-service-manifest` 010), tedy
141
+ jediné místo, které vidí, co dostane klon. Rozhodnutí vlastníka 2026-09-17,
142
+ konfirmace `biz-service-manifest` 011.
143
+
144
+ S klíčem odešel i jeho `@param` v JSDoc a blok `if ('deployable' in serviceInfo)`;
145
+ `API.md` je zregenerovaná ze zdrojů (`npm run docs`).
146
+
147
+ ### Changed — BREAKING: `register()` odmítá zaniklý klíč jménem (d.592)
148
+
149
+ `deployable` je šestý řádek tabulky `RETIRED_SERVICE_INFO_KEYS` (vedle `spec`,
150
+ `token`, `validationToken`, `secret`, `tokenSecret` z d.562). Volání, které ho
151
+ ještě pošle, **skončí výjimkou před publikací**, ne tiše bez účinku:
152
+
153
+ [RegistryClient] biz-invoicing: serviceInfo carries the retired key
154
+ "deployable" - the manifest-conformance verdict of a boot-time validation run
155
+ (owner decision 2026-09-17, confirmation
156
+ `api/docs/governance/confirmations/biz-service-manifest.md` 011). … Fix:
157
+ delete "deployable" from the object passed to register().
158
+
159
+ Odmítnutí se ptá na **přítomnost klíče, ne na jeho hodnotu** — `false` i `null`
160
+ byly stavy, které wrapper posílal nejčastěji, a kontrola podle pravdivosti by je
161
+ propustila.
162
+
163
+ Dopad: `ServiceWrapper._buildRegistrationPayload()` klíč dnes posílá, takže
164
+ wrapperová půlka změny (d.591) musí přistát ve stejné vlně; bez ní boot skončí
165
+ touto výjimkou. Pro ostatní přímé volající `register()` platí `Fix:` z hlášky.
166
+
167
+ Sady: unit 19 suites / 205 testů (o jednu sadu méně — `registryClient.deployable`
168
+ i `deployable-on-the-wire` odešly s klíčem, který tvrdily); integrace proti
169
+ živému brokeru včetně případu `deployable: false` odmítnutého dřív, než něco
170
+ odjede.
171
+
172
+ ## [5.0.0] — 2026-09-16
173
+
174
+ ### Fixed — klasifikace chyby přežije i rewrap ve FÁZI 0.8 (d.579)
175
+
176
+ `ensureQueues()` byla **třetí** cesta, která obalovala pád do
177
+ `new Error(msg, { cause })` — a ta konstrukce nenese žádnou vlastní vlastnost,
178
+ takže `code` i `reason`, které na chybu napsal
179
+ `@onlineapps/mq-client-core`, tady umíraly. d.532 opravila dvě cesty FÁZE 0.7
180
+ (`QueueManager.init()`, `RegistryClient.init()`); deklarace business front ve
181
+ FÁZI 0.8 zůstala. `@onlineapps/service-wrapper` rozhoduje podle `error.code`,
182
+ takže odmítnutí z této cesty dostal jako neklasifikované a ukončil ho jako
183
+ přechodné.
184
+
185
+ Teď se klasifikace přenáší týmž vzorem jako v `init()`: `refusal.code =
186
+ assertErr.code`, `refusal.reason = assertErr.reason`, původní chyba zůstává
187
+ v `cause` a věta, kterou tato vrstva přidává (která fronta, co to spraví), je
188
+ beze změny.
189
+
190
+ **Nic se tu neklasifikuje**: co zdroj nechal neoznačené, zůstává neoznačené.
191
+ Tenhle balíček nečte AMQP reply kódy ani nematchuje text hlášky — otázku „odpověděl
192
+ broker a odmítl?" vlastní `@onlineapps/mq-client-core` (`REFUSAL_REPLY_CODES`,
193
+ `_isBrokerRefusal()`) a druhá kopie toho seznamu by se od něj mohla rozejít.
194
+
195
+ Změřeno a nezamlčeno: proti pinu `@onlineapps/mq-client-core` 3.2.0 předává
196
+ `assertQueue()` syrovou chybu z amqplib, takže dnes touto cestou cestuje **číslo
197
+ 406**, ne značka `code: 'MQ_CONNECTION_FATAL'` / `reason: 'broker-refused'`,
198
+ podle které wrapper končí boot natrvalo. d.577 dala značku rail `consume()`
199
+ v jádře; deklarační rail ji zatím nemá. Tento přenos je tedy nutný, ne
200
+ dostačující — a integrační test to říká nahlas místo toho, aby tvrdil značku,
201
+ kterou žádný zdroj nepíše.
202
+
203
+ ### Documentation — komentář v `registryClient.js` ukazoval na zrušený dokument (d.574)
204
+
205
+ Komentář nad propagací `workspaceScoped` posílal čtenáře na
206
+ `biz-service-onboarding.md` §4 — dokument, který 2026-08-16 přešel na
207
+ `api/docs/biz/60-templates/onboarding-checklist.md` (čísla sekcí se přenesla,
208
+ `api/docs/biz/RETIRED-VOCABULARY.md` §4). Nová podoba je `@see` s živou cestou.
209
+ Ostatních 16 citací `api/docs/…` v tomto souboru bylo změřeno (`ls`) a všechny
210
+ cíle existují. Chování ani API se nemění.
211
+
212
+ ### Removed — BREAKING: `spec`, `validationToken` a `tokenSecret` odcházejí z registrační zprávy (d.562)
213
+
214
+ Tři klíče jezdily na každé registraci a nikdo je nečetl. Změřeno 2026-09-16 nad
215
+ `api/infra/api_services_registry/src/**`: `processRegistration` destrukturuje
216
+ `id, serviceName, version, endpoints, operations, metadata, health,
217
+ specificationEndpoint, responseQueue, timestamp, validationProof,
218
+ validationData, workspaceScoped` — a `grep -rn 'validationToken|tokenSecret'`
219
+ nad tím stromem nevrací nic. Kontrakt to už popisoval slovy „emitted, unread"
220
+ (`api/docs/biz/30-operations/registration-wire.md` §2.1).
221
+
222
+ Čtyři otázky `change-discipline.md` § Removing something removes its declaration:
223
+
224
+ - `spec` nesl OpenAPI dokument z doby, kdy registr dereferencoval HTTP cestu
225
+ `/specification`. ADR 0005 vzalo biz kontejnerům HTTP; co registr dnes
226
+ **validuje**, je `operations` podle schema-v3 (§3 téhož kontraktu). Schéma,
227
+ které protistrana kontroluje klíč po klíči, je koncepčnější než dokument,
228
+ který se jen uložil.
229
+ - `validationToken` a `tokenSecret` patřily k JWT token API, které odplulo
230
+ z `@onlineapps/service-validator-core` v d.344; poslední čtenář (HTTP
231
+ `POST /validate`) byl smazán 2026-08-22 a validator si zrušení zapsal do
232
+ vlastní konfigurace (`api/infra/api_services_validator/src/config/config.js`
233
+ § security). Běh dnes dokládá `validationProof` / `validationData` a podepsaný
234
+ certifikát — důkaz, který si příjemce ověří, místo sdíleného tajemství, které
235
+ tahle zpráva vozila po brokeru v čistém tvaru.
236
+
237
+ S klíči padly i dvě jejich záložky: `spec: serviceInfo.spec || null`
238
+ (výchozí hodnota pod `architecture-principles.md` §3) a dva pravopisy jednoho
239
+ pole — `serviceInfo.token || serviceInfo.validationToken`, `serviceInfo.secret
240
+ || serviceInfo.tokenSecret`.
241
+
242
+ ### Changed — BREAKING: `register()` odmítá zaniklý klíč jménem (d.562)
243
+
244
+ Volání, které některý z pěti zaniklých klíčů (`spec`, `token`,
245
+ `validationToken`, `secret`, `tokenSecret`) ještě posílá, **skončí výjimkou
246
+ ještě před publikací** — nikoli tiše bez účinku, protože tiše zahozený klíč
247
+ nechává volajícího věřit, že něco odjelo:
248
+
249
+ [RegistryClient] biz-invoicing: serviceInfo carries the retired key
250
+ "tokenSecret" - the shared secret of that same token API, whose last reader
251
+ (HTTP POST /validate) was deleted 2026-08-22; nothing verifies a token with
252
+ it, and a broker message is no place for a secret. Fix: delete "tokenSecret"
253
+ from the object passed to register().
254
+
255
+ Hláška **nikdy neopakuje předanou hodnotu** — dva z těch klíčů nesly tajemství.
256
+
257
+ `ServiceWrapper._buildRegistrationPayload()` žádný z nich neodesílá, takže pro
258
+ běžící biz služby se nemění nic; dopad je na přímého volajícího `register()`.
259
+
260
+ Sady: unit 20 suites / 199 testů; integrace 6 suites / 38 testů proti živému
261
+ brokeru, včetně nové `registrationPayloadKeys.integration.test.js`, která čte
262
+ zprávu zpět z fronty — `undefined` klíč zmizí až v serializaci, takže to, co
263
+ brokerem opravdu jede, neukáže žádný unit tier.
264
+
265
+ ### Fixed — značka odmítnutí brokerem přežije přebalení chyby (d.532)
266
+
267
+ Obě selhávající cesty tohohle balíčku chybu z jádra **přebalují**, aby dodaly
268
+ fakt, který jádru chybí: který broker (`QueueManager.init()`) a která fronta
269
+ které služby (`RegistryClient.init()`). Dělaly to holým `new Error(msg, { cause })`,
270
+ které nenese žádnou vlastní vlastnost — takže `code: 'MQ_CONNECTION_FATAL'`
271
+ a `reason: 'broker-refused'`, které na chybu napsal její vlastník
272
+ (`@onlineapps/mq-client-core`, `REFUSAL_REPLY_CODES` + `_isBrokerRefusal()`),
273
+ tady umřely.
274
+
275
+ Volající, který podle nich jedná, je `@onlineapps/service-wrapper`: FÁZE 0.3
276
+ ukončí boot natrvalo právě podle `error.code`, a od d.532 to dělá i FÁZE 0.7.
277
+ Bez značky viděl neklasifikované selhání a odmítnutí ohlásil jako přechodné —
278
+ změřeno na biz-meta (dev, wrapper 8.1.1): FÁZE 0.7 FAILED pro 406 na
279
+ `biz-meta.registry`, hlášeno jako „a restart against a recovered dependency can
280
+ succeed", a služba pak v crash loopu.
281
+
282
+ `code` i `reason` se teď přenášejí **bezpodmínečně** — táž kolej, jakou drží
283
+ `@onlineapps/conn-infra-mq` v `ConnectorMQClient` (d.498b). Nic se tu
284
+ neklasifikuje: co zdroj neoznačil, zůstává neoznačené, takže se do balíčku
285
+ nedostane druhá kopie seznamu AMQP reply kódů ani čtení textu hlášky.
286
+
287
+ Sonda `tests/unit/error-message-contract.test.js` navíc umí tvar
288
+ `const <jméno> = new …Error(…); … throw <jméno>;` — chyba, která nese vlastní
289
+ vlastnosti, se jinak postavit nedá. Bez toho by obě hlášky tiše vypadly z jejího
290
+ dosahu (změřeno: kontrolní počet spadl 28 → 26), což je přesně ta falešná záruka,
291
+ kvůli které kontrolní případ existuje (`automation-gates.md` §5).
292
+
293
+ ### Breaking — `startHeartbeat({ maxFailures, onFatal })`: smyčka heartbeatu má mez selhání (d.496)
294
+
295
+ Do téhle dávky `startHeartbeat()` selhání pouze logovala a plánovala další beat
296
+ donekonečna (`src/registryClient.js`, větev `heartbeatLoop`/`catch`). Služba, jejíž
297
+ beaty registr nedostával, tedy běžela dál a nikdo v tomhle balíčku neuměl říct, že
298
+ je „živá" jen naoko. Mez měl jen `@onlineapps/service-wrapper`, ve **vlastní druhé
299
+ smyčce** kolem `sendHeartbeat()` — dvě smyčky nad jednou kadencí, což
300
+ `.claude/rules/change-discipline.md` § One rail per concern označuje za vadu
301
+ a co si CHANGELOG wrapperu sám poznamenal do „Co tahle dávka NEudělala"
302
+ (*„Ta metoda selhání jen loguje — nemá počítadlo ani mez"*).
303
+
304
+ `startHeartbeat()` nově **vyžaduje objekt se dvěma volbami** a ani jednu si
305
+ nedosazuje (`architecture-principles` §3 a §4; chybějící nebo nesmyslná hodnota je
306
+ výjimka §5 dřív, než vznikne timer):
307
+
308
+ - `maxFailures` — kladné celé číslo beatů **v řadě**, které služba smí minout.
309
+ Jeden úspěch počítadlo nuluje.
310
+ - `onFatal(err)` — zavolá se **právě jednou**, ve chvíli, kdy je rozpočet vyčerpán.
311
+ Dostane chybu, která jmenuje počet, poslední příčinu (i jako `cause`) a Fix.
312
+ Smyčka je v tu chvíli už zastavená, takže do zavírající se služby nepřiletí
313
+ další beat.
314
+
315
+ **Kadence parametrem metody NENÍ** a záměrně: má jednoho vlastníka, a tím je
316
+ volba konstruktoru `heartbeatInterval` (rozhodnutí vlastníka
317
+ `api/docs/governance/confirmations/biz-health-freshness.md` 001 bod 3). Přijímat
318
+ ji ještě jednou při volání by bylo přesně to druhé nezávisle nastavené číslo pro
319
+ jednu věc, které to potvrzení odmítá.
320
+
321
+ Co s tím musí udělat volající: doplnit obě volby. Volání bez nich je od téhle
322
+ verze výjimka, ne tichý default. `@onlineapps/service-wrapper` pak svou vlastní
323
+ smyčku ruší ve prospěch téhle — samostatná dávka, po pinu.
324
+
325
+ Dvě menší změny chování ve stejné smyčce:
326
+
327
+ - **První (okamžitý) beat se počítá do téhož rozpočtu.** Dřív měl vlastní
328
+ `catch` a vlastní hlášku `[RegistryClient] Initial heartbeat failed`; ta
329
+ zaniká. Beat započtený mimo rozpočet by z `maxFailures` udělal „N + 1 pro
330
+ první selhání" — číslo, které volající nenastavil.
331
+ - **Každé selhání se loguje s počtem, kterého dosáhlo, a s rozpočtem, proti
332
+ kterému se počítá** (`consecutiveFailures`, `maxFailures` v meta), takže log
333
+ říká, jak blízko ke konci smyčka je, ne jen že jeden beat neprošel.
334
+
335
+ Testy: nová sada `tests/unit/registryClient.heartbeatBudget.test.js` (13 případů —
336
+ odmítnutí obou voleb hodnotou, vyčerpaný rozpočet, a kontrolní případy, které se
337
+ změnit nesmí: `maxFailures − 1` selhání a úspěch, zdravá smyčka bez `onFatal`,
338
+ idempotentní `stopHeartbeat`) a `tests/integration/heartbeatBudget.integration.test.js`
339
+ (3 případy proti **živému** RabbitMQ: beaty na drátě, skutečné odmítnutí po ztrátě
340
+ spojení, a reset počítadla beatem, který prošel). Selhání se v obou tierech vyrábí
341
+ skutečnou cestou kódu — nikdy atrapou `sendHeartbeat`, která by dokázala jen to, že
342
+ počítadlo počítá volání.
343
+
344
+ Kontrolní počet chyb v `tests/unit/error-message-contract.test.js` je 28 (bylo 25):
345
+ tři nové jsou odmítnutí voleb. Chyba předaná do `onFatal` mezi nimi **není** a hledat
346
+ se tam nemá — nevzniká `throw` ani `reject`, takže ji ta sonda nevidí; její znění
347
+ tvrdí hodnotou nová sada výše.
348
+
349
+ `README.md`, `docs/REGISTRY_CLIENT_GUIDE.md` a oba příklady učí novou signaturu;
350
+ `API.md` je přegenerované (`npm run docs`).
351
+
352
+ ### Fixed
353
+
354
+ - `API.md` regenerated from the sources (`npm run docs`). The committed file still
355
+ described the connector as asserting `workflow` and `<serviceName>.registry` on
356
+ startup and as handing out its channel for further operations — a surface replaced
357
+ when the connector moved onto a single `@onlineapps/mq-client-core` client, declared
358
+ its queues through `queueConfig` and stopped asserting infrastructure queues it does
359
+ not own. 167 lines changed, generated content only: the file is written by `jsdoc2md`,
360
+ never by hand (`.claude/rules/doc-code-binding.md` §1). What now keeps it true is
361
+ `scripts/ci/verify-generated-api-docs.mjs` (`npm run docs:api:check`).
362
+
363
+ ## [4.0.3] — 2026-09-15
364
+
365
+ ## [4.0.2] — 2026-09-15
366
+
367
+ ### Fixed — `redactUrl` se bere z `@onlineapps/logger-contract`, ne přes `mq-client-core` (d.455)
368
+
369
+ `src/queueManager.js` si redaktor bral z `@onlineapps/mq-client-core` (d.446b),
370
+ protože tehdy jediný deklaroval platformní sémantiku. Od
371
+ `@onlineapps/logger-contract` 1.2.0 (d.453) ji vlastní core vrstva, kterou
372
+ pinuje každá kategorie, takže import míří tam — vedle `assertLogger`, které si
373
+ tenhle soubor z téhož balíčku bere už dřív. Chování se nemění: pro `amqp` /
374
+ `amqps` vrací obě implementace shodný řetězec (změřeno).
375
+
376
+ Tady se nic nemaže — smazaná deklarace byla `src/redactUrl.js` a odešla už
377
+ v d.446b. Mění se jen to, **odkud** se tatáž funkce bere; `mq-client-core`
378
+ zůstává závislostí kvůli `BaseClient` a `queueConfig`.
379
+
380
+ Sady `queueManager.test.js`, `queueManager.spec.test.js`
381
+ a `queueManager.credentialRedaction.test.js`: dvojník `@onlineapps/mq-client-core`
382
+ už **záměrně nenabízí** `redactUrl` ani `UNPARSEABLE_PLACEHOLDER` — kdyby je
383
+ nabízel, sada by prošla i pro import ze špatného balíčku (to byla červená, která
384
+ tuhle změnu vynutila: `TypeError: redactUrl is not a function`,
385
+ `src/queueManager.js:69`). Přibyl kontrolní případ, že dvojník redaktor nemá,
386
+ a cesta selhání (neparsovatelná adresa → placeholder).
387
+
388
+ ## [4.0.1] — 2026-09-14
389
+
390
+ ## [4.0.0] — 2026-09-14
391
+
392
+ ### Breaking
393
+
394
+ #### Delivery of a registry message belongs to the core, not to this package (d.198b-3)
395
+
396
+ Both consumers here held a RAW amqplib channel (`queueManager.channel`): they
397
+ asserted their own queue, called `channel.consume(…, { noAck: false })` and settled
398
+ every delivery by hand — `channel.ack(msg)` on success,
399
+ `channel.nack(msg, false, false)` on failure. That is a second implementation of the
400
+ delivery contract (`.claude/rules/change-discipline.md` § One rail per concern), and
401
+ it is the half that cannot count: a hand-written nack knows nothing about the attempt
402
+ budget, the `x-oa-delivery-attempts` header or the `message_dlq` event
403
+ `@onlineapps/mq-client-core` publishes. Worse, `nack(requeue=false)` asks the broker
404
+ to DROP the message — and it did, because `<service>.registry` declared no
405
+ dead-letter route at all. A registry reply this service could not read simply ceased
406
+ to exist.
407
+
408
+ Both now run on `consume(queue, handler, { requeueOnError: false })`: one attempt,
409
+ and a failure is rejected into `<service>.dlq` over `dlx`, the route `queueConfig`
410
+ declares for these queues. A handler reports failure by THROWING and settles nothing
411
+ itself. Measured on the live broker (`tests/integration/deliveryPolicy.integration`):
412
+ the poison message arrives in the DLQ verbatim with `x-death[0].reason: 'rejected'`,
413
+ the handler ran exactly once, and the control case leaves both queues empty.
414
+
415
+ `QueueManager` is now the package's single MQ surface, built on
416
+ `@onlineapps/mq-client-core`: `init/isConnected/ensureQueues/checkQueue/
417
+ assertExchange/bindQueue/publish/consume/close`. **It hands out no `channel` any
418
+ more** — a caller reaching for `client.queueManager.channel` gets `undefined`.
419
+ Queue names are this package's; the DECLARATION behind them (durability, TTL, cap,
420
+ dead-letter route) is `queueConfig`'s, so `<service>.registry` and
421
+ `<service>.registry.events` are asserted with no options of our own. The 406 branch
422
+ in `subscribeToChanges()` — "the queue exists with different arguments, use it
423
+ as-is" — went with them: that is the fallback `architecture-principles.md` §3
424
+ forbids, and the reason a queue keeps whatever arguments its first declarer happened
425
+ to pass. `register()` publishes through the core (`persistent: true`), so a failed
426
+ publish THROWS instead of returning `false`, and the infrastructure-queue check runs
427
+ on the core's queue channel rather than a private one.
428
+
429
+ #### `heartbeatInterval` and `specificationEndpoint` are REQUIRED (d.278c)
430
+
431
+ Both were module defaults (`10000`, `'/api/v1/specification'`) and both were
432
+ statements this package had no standing to make.
433
+
434
+ The heartbeat cadence has ONE owner — `wrapper.registry.heartbeatInterval`, fed from
435
+ `BIZ_HEARTBEAT_INTERVAL_MS` (owner confirmation `biz-health-freshness` 001 point 3),
436
+ and the freshness window monitoring computes is a multiple of it. A library default
437
+ beside it is a second cadence that fires whenever the caller forgets the first. The
438
+ specification path is REQUIRED on the wire (`registration-wire.md` §2.1) and the
439
+ registry stores what it is sent, so a substituted path lands in `registry:services`
440
+ for a service that never declared it. Both are refused at construction, by value —
441
+ a string `'10000'` is not parsed into a cadence — naming the fix.
442
+
443
+ **Callers must now pass both.** `@onlineapps/service-wrapper` passes
444
+ `specificationEndpoint` already and does NOT pass `heartbeatInterval` (it runs its
445
+ own heartbeat loop from `wrapper.registry.heartbeatInterval` and calls
446
+ `sendHeartbeat()`); that value has to reach the constructor.
447
+
448
+ #### `register()` and `deregister()` return what actually happened (d.278c)
449
+
450
+ `register()` read the registry's reply and dropped two of its fields on the floor:
451
+ `validationStatus` and `instructions` (`registration-wire.md` §2.5 — the parked
452
+ registration, "you have not pre-validated yet" plus what to run about it). A caller
453
+ saw `success: false` and nothing that explained it. Both are now passed through
454
+ verbatim, like `errorCode`: a confirmed registration sends neither and both stay
455
+ `undefined` — nothing is substituted.
456
+
457
+ `deregister()` answered a hardcoded `{ success: true, message: 'Service deregistered
458
+ successfully' }` whatever happened. The registry sends NO reply to a `deregister`
459
+ (`registration-wire.md` §2.3), so there was no success to report — a claim about the
460
+ registry made by the one party that never heard from it. It now returns what this
461
+ call knows: `{ requestId, serviceName, version, publishedTo, heartbeatStopped,
462
+ publishedAt }`. A publish that fails REJECTS; reaching the return means the broker
463
+ took the message. There is no `success` key.
464
+
465
+ ### Fixed
466
+
467
+ #### The integration tier connects with a credential (d.446b)
468
+
469
+ `tests/config.js` composed the AMQP URI as `amqp://${host}:${port}` from
470
+ `RABBITMQ_HOST` + `RABBITMQ_PORT` — a URI with no account, which the broker answers
471
+ `403 ACCESS-REFUSED` to. 21 of the tier's 24 tests failed on it, on every run, for as
472
+ long as the broker has required authentication. The tier now takes `RABBITMQ_URL`
473
+ whole (credential included) and DERIVES host and port from it, so the address it
474
+ probes in `integration/setup.js` is the address it then connects to. The banner it
475
+ prints names host and port only.
476
+
477
+ Two more reds went with them, both stale rather than broken: `readRegistrySpec()`
478
+ asked `GET /services/<name>/spec`, a route `api_services_registry` stopped mounting
479
+ when the `/services` alias went in batch 263c — so `status === 404` passed for the
480
+ WRONG reason (a missing route, not a missing record) while the body assertion failed.
481
+ It reads `/microservices/<name>/spec` now, the route `src/app.js` mounts. And the
482
+ "nobody consumes the registry queue" failure path pointed at a queue that did not
483
+ exist; since the publish path asks the broker whether the target exists, that is now
484
+ a different (and faster) failure, so the harness declares the queue and leaves it
485
+ unconsumed — the situation the timeout exists for.
486
+
487
+
488
+ #### AMQP URL v hlášce i logu bez credentialu (d.446)
489
+
490
+ `AMQP_URL` je jediná kolej pro heslo k brokeru, takže obě chybové cesty
491
+ `QueueManager.init()` — timeout spojení i selhání `amqp.connect()` — vypisovaly to
492
+ heslo doslova: URL šla do hlášky nezredigovaná. Chyba konektoru jde rovnou do logu
493
+ služby a odtud do Loki, kde je trvalá a vyhledatelná. Táž třída úniku, kterou pro
494
+ Redis zavřel `shared/service-common/src/redisClient.js`.
495
+
496
+ Nově jde URL do hlášky přes `src/redactUrl.js`: userinfo (`uživatel:heslo@`) pryč,
497
+ schéma, host, port i vhost zůstávají — to je, co čtenář logu potřebuje. Hodnota,
498
+ kterou nelze přečíst jako URL, se nevypisuje vůbec, nahradí ji `<unparseable-url>`
499
+ (neparsovatelný řetězec je právě ten případ, kdy nikdo neumí říct, jestli
500
+ credential nese). Připojení samo dál používá původní URL — redakce se týká výhradně
501
+ renderu do textu.
502
+
503
+ Helper byl zprvu lokální (`src/redactUrl.js`): pinované závislosti balíčku tehdy
504
+ žádnou redakci nevystavovaly. **Od d.446b je pryč** — `@onlineapps/mq-client-core`
505
+ 3.0.0 exportuje `redactUrl` i `UNPARSEABLE_PLACEHOLDER` s totožnou sémantikou
506
+ (d.448, celé userinfo pryč), takže lokální kopie byla druhá kolej pro jeden koncept
507
+ (`change-discipline.md` § One rail per concern). `QueueManager` ho importuje z jádra;
508
+ identitu toho exportu drží test, a `tests/unit/redactUrl.test.js` (chování lokální
509
+ kopie) odešel s ní.
510
+
511
+ ### Konstruktor klienta už nenese `registryUrl` (d.244)
512
+
513
+ Parametr `registryUrl` i pole `this.registryUrl` zrušeny. Od konfirmace
514
+ `biz-discovery-redis` 001 a commitu `725b25c2` se jen ukládaly a nikdo je nečetl
515
+ (v `src/` žádný `axios`/`fetch`/`http.request`). Uložený nečtený parametr není
516
+ neškodný: říkal každému volajícímu, že na té adrese něco závisí — a to drželo
517
+ `REGISTRY_URL` jako povinný klíč v osmi biz konfiguracích. Konfirmace
518
+ `biz-discovery-redis` 002.
519
+
520
+ Volající, který `registryUrl` stále předá, ho dostane ignorovaný, ne uložený.
521
+ JSDoc konstruktoru ho nikdy nepopisoval (změřeno), takže se tam nic nemazalo.
522
+ Integrační tier balíčku `REGISTRY_URL` dál vyžaduje — čte jím HTTP API registru,
523
+ aby ověřil, co registr uložil; to je testovací harness, ne klient.
524
+
525
+ ### `register()` nese `deployable` na drát (d.221)
526
+
527
+ `register()` staví zprávu z pevného seznamu klíčů, takže klíč, který nejmenuje,
528
+ se na drát nedostane — tiše. Nově jmenuje `deployable`: verdikt shody s manifestem
529
+ (krok 7/7 validace), který registry promítá do `infrastructure:health:<service>`
530
+ vedle `status` (potvrzení vlastníka
531
+ `api/docs/governance/confirmations/biz-service-manifest.md` 001 §4). Přenáší se
532
+ doslova ve všech třech stavech — `true`, `false`, `null` (běh konformitu nezměřil) —
533
+ proto test přítomnosti klíče, ne pravdivosti hodnoty. Volající, který verdikt
534
+ neposílá, nemá klíč ve zprávě; nic se za něj nedosazuje.
535
+
536
+ ## [3.0.1] — 2026-09-04
537
+
538
+ - Changelog started with this release; the history before it is in git (`git log -- shared/connector/conn-orch-registry`).
package/README.md CHANGED
@@ -25,11 +25,11 @@ Duty sections that apply:
25
25
 
26
26
  > A lightweight client for microservice registration and heartbeat over RabbitMQ.
27
27
  > Sends the full service specification (including `operations`) in the `register`
28
- > message — see [operations-registry-contract.md §2](../../../docs/biz/30-operations/registration-wire.md).
28
+ > message — see [registration-wire.md §2](../../../docs/biz/30-operations/registration-wire.md).
29
29
 
30
30
  ## 🚀 Features
31
31
 
32
- * Automatic queue management (`workflow`, `<serviceName>.registry`, `api_services_queuer`, `registry.register`)
32
+ * Automatic queue management (`workflow`, `<serviceName>.registry`, `<serviceName>.registry.<instanceId>`, `api_services_queuer`, `registry.register`)
33
33
  * Periodic heartbeat messages with metadata
34
34
  * Full specification (endpoints + operations) published inside `register`
35
35
  * Event-driven API using `EventEmitter`
@@ -63,15 +63,18 @@ const client = new ServiceRegistryClient({
63
63
  version: '1.0.0',
64
64
  specificationEndpoint: '/api/v1/specification', // required — see § Configuration
65
65
  heartbeatInterval: 30000, // required — see § Configuration
66
+ instanceId: process.env.SERVICE_INSTANCE_ID, // required — THIS running copy; see § Configuration
66
67
  logger // required — see § Logger contract below
67
68
  });
68
69
 
69
70
  await client.init();
70
71
 
71
72
  // Publish full service spec in the register message (single round-trip).
73
+ // Only the keys of the registration contract are accepted — endpoints, operations,
74
+ // metadata, health, workspaceScoped, timeout; any other key is refused by name.
72
75
  await client.register({
73
76
  endpoints: [/* ... */],
74
- operations: await loadOperationsJson(), // see operations-registry-contract.md §3
77
+ operations: await loadOperationsJson(), // see registration-wire.md §3
75
78
  metadata: { /* ... */ },
76
79
  health: '/health'
77
80
  });
@@ -104,6 +107,7 @@ Configuration can be provided via environment variables or constructor options:
104
107
  | `version` | Service version in SemVer format (required) | — |
105
108
  | `specificationEndpoint` | Path the `register`/`heartbeat` messages carry (**required**) | — |
106
109
  | `heartbeatInterval` | Milliseconds between heartbeats (**required**) | — |
110
+ | `instanceId` | Non-empty string naming THIS running copy, one value per process (**required**) | — |
107
111
  | `apiQueue` | Queue name for heartbeat/API messages | `api_services_queuer` |
108
112
  | `registryQueue` | Queue name for registry requests | `registry.register` |
109
113
 
@@ -122,6 +126,14 @@ whenever the caller forgets the first (architecture-principles §3):
122
126
 
123
127
  Both are refused at construction, by value, naming the fix.
124
128
 
129
+ **`instanceId` carries no default either** (d.817, owner confirmation
130
+ `registry-instance-identity` 001). The service name says which service, the instance id
131
+ says which copy; `register`, `heartbeat` and `deregister` carry it as `instance_id`, and
132
+ the registry deletes a registration only when it matches. The client never makes one:
133
+ `@onlineapps/service-wrapper` makes it once at boot (`SERVICE_INSTANCE_ID` when the
134
+ deployment gives one, otherwise a UUID v4) and hands the same value to monitoring as
135
+ `serviceInstanceId`.
136
+
125
137
  `amqpUrl` carries the broker credential, so no error message and no log line of this
126
138
  package renders it as given: the URL passes through `redactUrl` from
127
139
  `@onlineapps/logger-contract`, which drops the userinfo (`user:password@`) and keeps
@@ -132,8 +144,11 @@ implementace přestěhovala do core vrstvy; one concern, one rail.)
132
144
 
133
145
  ## 📬 Delivery of registry messages
134
146
 
135
- Both consumers in this package — the service's own `<service>.registry` response queue
136
- and the `<service>.registry.events` fanout copy — run on the delivery contract of
147
+ The consumers in this package — the shared `<service>.registry` (the registry's own
148
+ prompts, such as `revalidate`, heard by every copy), the answer queue of THIS copy
149
+ `<service>.registry.<instanceId>` (named as `responseQueue` in `register`, so a copy
150
+ never reads the answer meant for another — owner confirmation `registry-instance-identity`
151
+ 001 (c), d.817c) and the `<service>.registry.events` fanout copy — run on the delivery contract of
137
152
  [`@onlineapps/mq-client-core`](https://www.npmjs.com/package/@onlineapps/mq-client-core)
138
153
  (§ `consume()` — the delivery contract), registered with `requeueOnError: false`.
139
154
 
@@ -191,15 +206,25 @@ demands is a logger that component writes to). Standard:
191
206
  ### Register Request
192
207
  ```javascript
193
208
  {
209
+ id: "request-uuid", // which request
210
+ instance_id: "copy-id", // which running copy (the constructor's instanceId)
194
211
  type: "register",
195
212
  serviceName: "hello-service",
196
213
  version: "1.0.0",
197
- token: "pre-validation-token",
214
+ specificationEndpoint: "/api/v1/specification",
198
215
  endpoints: [...],
216
+ operations: { ... },
217
+ metadata: { ... },
218
+ health: "/health",
219
+ responseQueue: "hello-service.registry.copy-id", // THIS copy's answer queue (d.817c)
199
220
  timestamp: "ISO-8601"
221
+ // + workspaceScoped when the service declares it,
222
+ // + validationProof / validationData when a proof was injected
200
223
  }
201
224
  ```
202
225
 
226
+ The wire format has one owner: `api/docs/biz/30-operations/registration-wire.md` §2.
227
+
203
228
  ### Register Confirmed
204
229
  ```javascript
205
230
  {
@@ -225,9 +250,12 @@ fixes it: `api/docs/biz/30-operations/registration-wire.md` §8.
225
250
  ### Heartbeat
226
251
  ```javascript
227
252
  {
253
+ id: "message-uuid",
228
254
  type: "heartbeat",
229
255
  serviceName: "hello-service",
230
- status: "UP",
256
+ instance_id: "copy-id",
257
+ version: "1.0.0",
258
+ specificationEndpoint: "/api/v1/specification",
231
259
  timestamp: "ISO-8601"
232
260
  }
233
261
  ```