diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index c7d30ec5..c67f1c01 100644 --- a/docs/DEVICE_VALIDATION_LEDGER.md +++ b/docs/DEVICE_VALIDATION_LEDGER.md @@ -17,6 +17,7 @@ | # | Özellik | Nerede | Kabul ölçütü (cihazda ne gözlemlenmeli) | Eklendi | |---|---------|--------|------------------------------------------|---------| +| 54 | **🔴 Device Validation Pending — Vehicle HAL → Event Bus Bridge Runtime Wiring (W4C)** — Zincir TAMAMLANDI: `UnifiedVehicleStore → VehicleHalProviderAdapter → vehicleHal → VehicleHalEventBridge → appEventBus`. Bridge foundation'ı (PR #54) gerçek `vehicleHal` singleton'ına ve W3'ün TEK aktif bus'ına bağlandı. **SAHİPLİK:** bridge bu wiring'in → cleanup YALNIZ bridge'i dispose eder; **HAL ve Bus DISPOSE EDİLMEZ**. **TEK INSTANCE:** aktif bridge varken ikinci start YENİ HAL aboneliği AÇMAZ; bayat kayıt serbest; eski cleanup yeni bridge'i düşüremez. **SystemBoot:** Wave 2'de HAL wiring'den SONRA, SystemOrchestrator'dan ÖNCE `_reg` ile → LIFO shutdown: bridge → HAL wiring → VDL → … → Event Bus (en son) ⇒ bridge kapanırken HAL ve Bus HÂLÂ AYAKTA. Bus yoksa wiring sessizce no-op (fail-soft, boot sürer). **EVENT:** `vehicle.signal.changed` (transient, supported sinyal değişimi; W4A sayesinde tek HAL snapshot → değişen sinyal başına 1 event), `vehicle.connection.changed` (retained; W4B unsupported geçişi TÜM destek kalkınca false üretir, kısmi kayıpta üretmez, reconnect'te true), `vehicle.identity.changed` (retained, ham VIN YOK — yalnız fingerprint). **`vehicle.ignition.changed` ÜRETİLMEZ** (kaynak yok → supported=false). TPMS payload bounded (≤8). Duplicate değer → duplicate event YOK (bridge imza-dedupe). **TEŞHİS:** support_snapshot'a bounded `halBridge{present,started,disposed,publishedCount,droppedCount,lastPublishAt}` eklendi — "bridge yok" (present:false, null) ile "0 event" AYRI. **BİLİNÇLİ KARARLAR:** throttle/debounce/coalescing/async-queue **YOK** → publish hızı = gerçek sinyal değişim hızı; **W4D kararı bu PR'ın CİHAZ ÖLÇÜMÜNDEN sonra** verilecek. **RİSK:** `PlatformEventBus.publish()` SENKRON dispatch eder → ileride eklenecek consumer'lar HAL/store hot-path stack'inde çalışır (bu PR'da ABONE YOK → yalnız publish maliyeti). **TİP NOTU:** bridge'in publish sözleşmesi geniş (`string`) domain/source alır, Bus dar birlik tipleri bekler → foundation'a dokunmadan wiring'de daraltıcı publish hedefi kullanıldı (doğrulama zayıflamaz; Bus zaten beyaz liste uygular). **KAPSAM DIŞI:** consumer/abone · Capability bridge/Registry · Deep Scan · Kernel servis kaydı · yeni catalog girdisi · HAL stale sweeper · **native `canStatus` wiring / worker source-health** · throttle (W4D). | `platform/system/platformCoreVehicleHalBridgeWiring.ts` (yeni), `platform/system/SystemBoot.ts` (additive Wave 2), `platform/diagnosticSections.ts` (halBridge bölümü), `__tests__/platformCoreVehicleHalBridgeWiring.test.ts` (yeni, 35 test), `__tests__/platformRuntimeDiagnostics.test.ts` (kilit güncellendi) | Gerçek cihazda/araçta: (1) TEK aktif bridge ve DOĞRU app Event Bus (Tanı raporu: `halBridge.present=true`, `started=true`); (2) HAL sinyal değişimleri doğru topic'lere publish (araçta speed/rpm/coolant/gear/reverse); (3) **publish/sn ölçülür** (park + sürüş); (4) duplicate sinyal duplicate event ÜRETMİYOR; (5) cleanup sonrası publish YOK; (6) bridge HAL/Bus'ı dispose ETMİYOR (restart sonrası bus sayaçları sıfırlanmıyor/çift bus yok); (7) `vehicle.ignition.changed` HİÇ yok; (8) TPMS payload bounded; (9) raporda VIN/ham CAN/koordinat/secret YOK; (10) `droppedCount=0` (veya gerekçeli); (11) CAN/OBD disconnect sonrası mümkün olan sinyaller unsupported + `connection.changed=false` YALNIZ tüm destek kalkınca; (12) reconnect sonrası `connection.changed=true`; (13) Xiaomi'de boot/idle CPU/RSS/FPS regresyonu YOK; (14) **en az bir low-tier cihazda (K24/Mali-400 veya T507)** sayaçlar raporda görünüyor, ANR/donma yok, publish yükü CPU/FPS bütçesini bozmuyor. ⚠️ **AÇIK BLOKAJ (ayrı iş):** ECU sessizce frame kesip store değerlerini null'a çekmezse disconnect TESPİT EDİLEMEZ (native `canStatus` hiçbir store'a yazılmıyor) → `connection.changed=false` o senaryoda üretilemez. ⚠️ **throttle/coalescing W4D'ye bırakıldı** (ölçüm sonrası). Masaüstü/araçsız Xiaomi testleriyle 🟢 YAPMA. ⚠️ **KISMİ CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — ARAÇ/OBD YOK; ölçüm PR #68 teşhis yüzeyinden, uygulama koduna HİÇ enstrümantasyon eklenmeden):** 🟢 Bridge CANLI: gerçek "Tanı Gönder" raporunda `halBridge{present:true, started:true, droppedCount:0}`. 🟢 **PUBLISH/SN ÖLÇÜLDÜ (park, yalnız GPS):** iki rapor arası 440 sn — bridge `publishedCount` 33→197 (Δ=164) ⇒ **≈0,37 publish/sn**; bus `publishedCount` 34→198 (aynı Δ ⇒ yalnız bridge yayınlıyor), `droppedCount=0`, **`deliveredCount=0` ve `activeListenerCount=0`** (tasarım gereği abone YOK). 🟢 `halWiring.refreshCount` 56→487 (≈1 store update/sn) ve **`activeSubscriptionCount=1`** (tek HAL aboneliği). 🟢 `historyCount` 6'da SABİT → `vehicle.signal.changed` transient olduğu için history/RAM ŞİŞMİYOR; `retainedEventCount=2` (runtime.started + connection.changed). 🟢 Teşhis bölümü 612-615 bayt, **sızıntı izi YOK**. 🟢 Boot crash/ANR YOK (3 cold start: 1474/1112/1077 ms), idle CPU 17-27% / RSS 545-576 MB (önceki build'lerle aynı bantta → regresyon gözlenmedi). **HÂLÂ DOĞRULANMADI (bu yüzden 🔴): gerçek araç/OBD** — speed/rpm/coolant/gear/reverse event'leri, **sürüşteki publish/sn** (CAN frame hızı kodda yok → W4D throttle kararının girdisi), CAN/OBD disconnect sonrası `connection.changed=false`, reconnect, CPU/FPS bütçesi; ve **low-tier cihaz (K24/Mali-400 veya T507) doğrulaması YAPILMADI.** | 2026-07-12 | | 53 | **🔴 Device Validation Pending — Vehicle HAL Source Disconnect / Fail-Closed (W4B)** — Kaynak kopunca HAL'in eski değerleri sonsuza kadar `supported=true`+`stale=false` tutması KAPATILDI. **KOD GERÇEĞİ:** `VehicleHal.refresh()` (tek stale süpürücü) üretimde HİÇ çağrılmıyor; `canStatus` native olayı hiçbir store'a yazılmıyor (yalnız `console.info` — CanAdapter.ts:57-59); `canPhase` yalnız İLK-FRAME timeout'unu temsil ediyor. **Gerçek kopma kanıtı:** worker kaynak-timeout'unda `null` yayar (SRC_TIMEOUT_CAN/OBD/GPS) ve `resetCanData()` CAN alanlarını null'lar. **SEÇİLEN MODEL (A-refined, timersız, yeni abonelik/DI YOK):** adapter, DAHA ÖNCE aktarılmış bir sinyalin değeri kaybolunca `source:'none'` ingest eder → HAL `supported=false` + `value=null` (eski değer taşınmaz; quality `unknown`, confidence 0 — uydurma YOK; timestamp = GEÇİŞ anı). **unknown ≠ disconnected:** hiç aktarılmamış sinyal için SAHTE disconnect üretilmez. **Toptan kapatma YOK:** yalnız kaynağı kaybolan sinyal düşer; füzyon `speed` GPS/OBD ile canlıysa supported KALIR. **CAN-özel boolean'lar** (door_state/parking_brake) artık CAN canlılığına bağlı → `resetCanData()`'nın `false` artığı "gerçek veri" sayılmaz (reverse füzyon olduğu için genel canlılıkta kalır). **Batch korunur (W4A):** bir geçişte düşen tüm sinyaller TEK batch → TEK HAL emit; aynı disconnected durum TEKRAR ingest edilmez (duplicate emit yok); kaynak dönünce değer normal yoldan yeniden ingest edilir (supported=true + gerçek source). **ignition:** kaynak yok → hâlâ ASLA üretilmez. **KAPSAM DIŞI:** zaman-tabanlı stale sweeper / yeni timer · Event Bus/bridge (W4C) · throttle (W4D) · native CAN state machine refactor. | `platform/vehicleHal/vehicleHalProviderAdapter.ts`, `__tests__/vehicleHalSourceDisconnect.test.ts` (yeni, 25 test) | Gerçek araçta: (1) CAN/OBD bağlıyken ilgili sinyaller supported; (2) adaptör/CAN kesilince **yalnız o kaynağın sinyalleri** `source=none`/`supported=false` oluyor; (3) diğer canlı kaynakların sinyalleri (GPS füzyon hızı) supported KALIYOR; (4) bir kopma geçişi TEK batch/TEK HAL emit üretiyor; (5) aynı disconnected durum duplicate emit ÜRETMİYOR; (6) yeniden bağlanınca supported/source/değer doğru geri dönüyor; (7) `vehicle.ignition` unsupported KALIYOR; (8) yeni timer/polling YOK ve tek subscription korunuyor (Tanı raporu: `halWiring.activeSubscriptionCount=1`, kopma/geri gelme sırasında `ingestedSignalCount` düşüp yükseliyor); (9) Xiaomi'de CPU/RAM/boot regresyonu YOK; (10) **en az bir low-tier cihazda (K24/Mali-400 veya T507)** doğrulama. ⚠️ **BİLİNEN SINIRLAMA (kapsam dışı):** CAN kablosu takılıyken ECU sessizce frame göndermeyi keserse store'daki CAN değerleri null'a DÖNMEZ (native `canStatus` hiçbir store'a yazılmıyor) → bu senaryoda kopma HÂLÂ tespit edilemez; ayrı iş (native canStatus wiring veya worker CAN-sağlık expose). Test edilmeden 🟢 yapma. ⚠️ **KISMİ CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — ARAÇ/OBD YOK; ölçüm W4E teşhis yüzeyiyle, uygulama koduna HİÇ enstrümantasyon eklenmeden):** 🟢 Cihazın gerçek "Tanı Gönder" raporunda `halWiring{started:true, **activeSubscriptionCount:1**, refreshCount:169, ingestedSignalCount:3, lastErrorCode:null}` → tek abonelik korunuyor, hata yok. 🟢 **CAN-boolean kapısı cihazda GÖZLEMLENDİ:** aynı cihazda W4B ÖNCESİ build `ingestedSignalCount=5` veriyordu, W4B ile **3** → CAN yokken `door_state`+`parking_brake` artık HAL'e aktarılmıyor (resetCanData'nın `false` artığı "gerçek veri" sayılmıyor). 🟢 Boot/CPU/RAM regresyonu YOK (aynı cihaz durumunda A/B): baseline (main eşdeğeri) cold start 3026/3014/3014 ms · CPU 0-26% · RSS 360-365 MB → W4B 3017/3012/3010 ms · CPU 3-28% · RSS ~332 MB. 🟢 Crash/ANR yok (3 cold start + background/foreground + rapor gönderimi). **HÂLÂ DOĞRULANMADI (bu yüzden 🔴): gerçek CAN/OBD kopar-bağla senaryosu** (supported→unsupported geçişi, fallback, reconnect, tek-emit) — **araç/OBD gerektirir**; ve **low-tier cihaz (K24/Mali-400 veya T507) doğrulaması yapılmadı.** | 2026-07-12 | | 52 | **🔴 Device Validation Pending — Platform Runtime Diagnostics (W4E, kalıcı bounded teşhis yüzeyi)** — Event Bus + Vehicle HAL wiring'in BOUNDED durum sayaçları mevcut kullanıcı-tetikli teşhis raporuna (`triggerSupportSnapshot` → `reportSupportSnapshot` → `_buildSupportSnapshotPayload` → `_deepSanitize` → `pushVehicleEvent('support_snapshot')` → /admin/tani) **kalıcı** olarak eklendi. **AMAÇ:** "tek instance / tek abonelik / event sayacı" invaryantları cihazda **ADB, CDP veya geçici debug expose OLMADAN** okunabilsin (T507 gibi PC-adb'siz cihazların TEK ölçüm kanalı). **KULLANILAN API'ler (yalnız mevcut, salt-okunur):** `getEventBusStatus()` ve `getVehicleHalWiringStatus()`. **WHITELIST — Event Bus:** present · disposed · publishedCount · deliveredCount · droppedCount · listenerErrorCount · duplicateSubscriptionCount · recursionDropCount · activeListenerCount · retainedEventCount · historyCount · lastEventAt · runtimeStartedPublished · runtimeStoppedPublished. **HAL wiring:** started · lastRefreshAt · refreshCount · ingestedSignalCount · activeSubscriptionCount · lastErrorCode (SABİT kod: init_failed/cleanup_failed; serbest metin/stack REDDEDİLİR). **`disposed` HAL wiring status'ünde YOK → uydurulmadı.** **Bridge bölümü ÜRETİLMEZ** (üretimde bağlı değil, status accessor'ı yok → sahte present/started yerine W4C'ye bırakıldı). **PRIVACY:** runtime status nesnesi ASLA spread edilmez (alan-alan whitelist); event payload · history içeriği · topic detayı · correlation/causation · araç sinyal DEĞERLERİ (hız/RPM) · VIN · fingerprint · koordinat · MAC · CAN frame · ham hex · secret · stack trace **GİRMEZ**; çıktı yalnız sayı/bool/null. **"0" ≠ "ölçülemiyor":** wiring yoksa sayaçlar **null** (0 değil). **FAIL-SOFT:** accessor throw ederse bölüm boş döner ve raporun DİĞER bölümleri üretilmeye devam eder (`_safeSection`); NaN/Infinity → null. **YALIN:** yeni timer/polling YOK · yeni publisher/subscriber/bridge/wiring YOK · runtime instance YARATMAZ (teşhis okuma bus/adapter oluşturmaz) · yeni otomatik/yüksek frekanslı gönderim YOK (kullanıcı tetikli akış korunur) · yeni bağımlılık YOK. | `platform/diagnosticSections.ts` (+`buildPlatformRuntimeSnapshot`), `platform/remoteLogService.ts` (payload'a `platform: _safeSection(...)`), `__tests__/platformRuntimeDiagnostics.test.ts` (yeni, 17 test) | Gerçek cihazda: (1) "Tanı Gönder" raporunda **bounded Event Bus sayaçları** görünüyor; (2) raporda **bounded Vehicle HAL wiring sayaçları** görünüyor (`activeSubscriptionCount=1` → tek abonelik ADB'siz doğrulanabiliyor); (3) raporda **hiçbir event payload'ı veya araç sinyal değeri YOK**; (4) VIN/koordinat/CAN/ham hex/secret YOK; (5) wiring yokken rapor fail-soft (sayaçlar null, rapor yine gönderiliyor); (6) teşhis üretimi yeni runtime instance YARATMIYOR; (7) yeni timer/polling YOK; (8) rapor boyutunda anlamlı şişme YOK (önce/sonra ölçülür); (9) Xiaomi 23090RA98I'de ADB/global expose olmadan sayaçlar okunabiliyor; (10) **T507 veya K24/Mali-400'de mevcut teşhis kanalıyla sayaçlar okunabiliyor** ve rapor gönderimi başarılı, UI donması/ANR yok; (11) CPU/RAM/boot regresyonu YOK. Masaüstü testleriyle 🟢 YAPMA; low-tier cihazda gerçek rapor doğrulanmadan 🔴 kalır. ⚠️ **CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — UYGULAMA KODUNA HİÇ ENSTRÜMANTASYON EKLENMEDEN):** 🟢 Cihazda **gerçek "Tanı Gönder" butonu** ile rapor tetiklendi; giden `push_vehicle_event` (support_snapshot) isteğinin GÖVDESİ (CDP Network ile, yalnız gözlem) `platform` bölümünü İÇERİYOR: `eventBus{present:true, publishedCount:1, deliveredCount:0, droppedCount:0, listenerErrorCount:0, duplicateSubscriptionCount:0, recursionDropCount:0, activeListenerCount:0, retainedEventCount:1, historyCount:1, runtimeStartedPublished:true, runtimeStoppedPublished:false}` + `halWiring{started:true, **activeSubscriptionCount:1**, refreshCount:289, ingestedSignalCount:5, lastErrorCode:null}` → **tek bus + tek HAL aboneliği + runtime.started bir kez, ADB/global expose OLMADAN raporda okunabiliyor.** 🟢 Gizlilik: platform bölümünde VIN/koordinat/sinyal değeri/correlation/payload/history izi **YOK** (otomatik tarama: sızıntı izi yok). 🟢 Boyut: platform bölümü **484 bayt**, toplam rapor ~14,2 KB (≈%3,4 → anlamlı şişme yok). 🟢 Kararlılık: 3 cold start (1059/1085/1325 ms) + background/foreground + rapor gönderimi sırasında **crash/ANR YOK**, UI donmadı; idle CPU 25-39% / RSS 577-600 MB (önceki build ile aynı bantta → regresyon yok). **HÂLÂ DOĞRULANMADI (bu yüzden 🔴): low-tier cihaz (T507 veya K24/Mali-400) üzerinde rapor gönderimi ve /admin/tani'de platform sayaçlarının görünmesi DOĞRULANMADI** (T507'de PC-adb yok — bu PR'ın asıl hedef senaryosu); ayrıca alınan raporun admin panelinde görüntülenmesi kullanıcı tarafından teyit edilmeli. | 2026-07-12 | | 51 | **🔴 Device Validation Pending — Vehicle HAL Provider Adapter Batch Ingest (W4A, perf)** — Adapter tek `refresh()` içinde değişen TÜM sinyalleri artık **tek `hal.ingest(batch)`** ile veriyor. **KÖK PROBLEM:** eskiden sinyal başına `hal.ingestSignal()` çağrılıyordu; `VehicleHal.ingestSignal` her değişimde revision artırıp **15 sinyallik tam snapshot** üretiyor (kopya + sort + freeze) ve abonelere emit ediyordu → tek store tick'inde **N değişim = N emit + N snapshot**; CAN yolu tek `updateCanExtras` çağrısında 10'a kadar HAL sinyalini birlikte değiştirebiliyor (worker yoluyla toplam 14). **YENİ:** N değişim = **1 ingest = 1 emit**. **KORUNAN:** 14 eşleme ve SIGNAL_MAP sırası · ignition eşlenmez · TPMS yalnız gerçek 4-tuple · source/quality/confidence · refresh başına ORTAK timestamp · değer normalizasyonu · duplicate ingest yok · start/stop/dispose idempotency · abonelik sayısı · cleanup · fail-soft · bounded `_last` · timer/polling YOK · HAL sahipliği adapter'a GEÇMEZ. **DEDUPE ATOMİKLİĞİ:** `_last` YALNIZ başarılı `ingest()` sonrası güncellenir — `VehicleHal.ingest()` sinyal-bazlı catch İÇERMEZ (tümden throw edebilir), bu yüzden batch düşerse sinyaller "aktarılmış" sayılmaz ve sonraki refresh yeniden dener (HAL idempotent: aynı değer → değişim yok → emit yok). **BİLİNÇLİ DAVRANIŞ FARKLARI:** (1) HAL `revision` sinyal başına değil **batch başına 1** artar; (2) HAL aboneleri **ara/kısmi snapshot görmez**, yalnız nihai tutarlı snapshot'ı görür; (3) yayınlanan nihai sinyal kümesi + değerler + metadata DEĞİŞMEZ; (4) üretimde HAL snapshot/revision tüketicisi olmadığı için **kullanıcı-gözlemlenebilir davranış değişmez**. **KAPSAM DIŞI:** Event Bus bridge wiring (W4C) · stale/ disconnect semantiği (W4B) · throttle/coalescing (W4D) · kalıcı teşhis yüzeyi (W4E). Bu PR bridge'i BAĞLAMAZ — W4C'nin performans ÖN KOŞULUDUR. | `platform/vehicleHal/vehicleHalProviderAdapter.ts` (refresh + `VehicleHalIngestTarget` sözleşmesi `ingest`'e geçti), `__tests__/vehicleHalBatchIngest.test.ts` (yeni, 24 test), `__tests__/vehicleHalProviderAdapter.test.ts` · `platformCoreVehicleHalWiring.test.ts` · `unifiedVehicleStoreProvider.test.ts` (fake HAL'ler yeni sözleşmeye güncellendi) | Gerçek cihazda: (1) tek refresh içindeki N değişiklik **yalnız 1 HAL emit** üretir (deterministik sayaçla; testte kilitli, cihazda gerçek CAN tick'i ile doğrulanmalı); (2) nihai HAL snapshot sinyal kümesi ve değerleri değişmedi; (3) source/quality/confidence/timestamp korunuyor; (4) duplicate ingest davranışı korunuyor; (5) **gerçek araçta emit amplifikasyonunun azaldığı sayaçla ölçülür** (çok-sinyalli CAN tick'i araç/OBD olmadan üretilemez); (6) Xiaomi 23090RA98I'de CPU/RAM/FPS regresyonu YOK; (7) **en az bir low-tier cihazda (tercihen K24/Mali-400) CPU/RAM/FPS regresyonu YOK**; (8) boot süresi regresyonu YOK; (9) Event Bus bridge hâlâ BAĞLI DEĞİL. Test edilmeden 🟢 yapma. ⚠️ **KISMİ CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — ARAÇ/OBD YOK; geçici, commit-EDİLMEYEN bounded sayaç + CDP-over-adb, ölçüm sonrası kaldırıldı ve temiz ağaçtan yeniden build/kurulum yapıldı):** 🟢 boot crash/ANR YOK (3 cold start + background/foreground); cold start WaitTime baseline (main, W4A yok) 1218/1060/1041 ms → W4A 1082/1035/1138 ms (**boot regresyonu YOK**); idle CPU baseline 109/62/58% → W4A 23/30/26% ve RSS 572-622 MB → 559-570 MB (⚠️ **her iki ölçüm de gürültülü; W4A lehine kesin iyileşme İDDİA EDİLMİYOR**, yalnız regresyon yok). 🟢 Adapter/HAL sayaçları (30 sn, park, yalnız GPS akışı): `subs=1 · refreshCount≈56 (~1.9 store update/sn) · emits=23 (~0.77 HAL emit/sn) · ingestedSignals=5 · maxBatchDelta=5` → **emit sayısı refresh sayısından KÜÇÜK** (değişiklik yoksa ingest/emit yok) ve tek abonelik korunuyor. **HÂLÂ DOĞRULANMADI (bu yüzden 🔴):** park hâlinde tick başına genelde TEK sinyal değiştiği için **N→1 emit azalması cihazda GÖSTERİLEMEDİ** — çok-sinyalli gerçek CAN tick'i **araç/OBD gerektirir**; ayrıca **low-tier cihaz (K24/Mali-400 veya T507) ölçümü YAPILMADI** ve uzun-süreli CPU/termal ölçülmedi. | 2026-07-12 | diff --git a/src/__tests__/platformCoreVehicleHalBridgeWiring.test.ts b/src/__tests__/platformCoreVehicleHalBridgeWiring.test.ts new file mode 100644 index 00000000..83d1e936 --- /dev/null +++ b/src/__tests__/platformCoreVehicleHalBridgeWiring.test.ts @@ -0,0 +1,411 @@ +/** + * platformCoreVehicleHalBridgeWiring.test.ts — W4C: Vehicle HAL → Event Bus bridge wiring. + * + * Zincir: UnifiedVehicleStore → adapter → vehicleHal → VehicleHalEventBridge → appEventBus. + * Bu PR yalnız WIRING'dir: throttle/coalescing YOK (W4D), consumer/abone YOK, Capability/ + * Deep Scan/Kernel YOK, yeni catalog girdisi YOK, stale sweeper YOK. + * + * Kritik invaryantlar: tek bridge · doğru (TEK) bus · bridge HAL/Bus'ı DISPOSE ETMEZ · + * ignition event YOK · duplicate event YOK · cleanup sonrası publish YOK · payload bounded/PII'siz. + */ + +import { describe, it, expect, afterEach } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { + startPlatformCoreVehicleHalBridgeWiring, + getVehicleHalBridgeStatus, +} from '../platform/system/platformCoreVehicleHalBridgeWiring'; +import { + startPlatformCoreEventBusWiring, + getAppEventBus, +} from '../platform/system/platformCoreEventBusWiring'; +import { createVehicleHal, type VehicleSignalId, type VehicleSignalInput } from '../platform/vehicleHal'; +import { createPlatformEventBus, type PlatformEvent } from '../platform/eventBus'; +import { buildPlatformRuntimeSnapshot } from '../platform/diagnosticSections'; + +const SRC = join(process.cwd(), 'src', 'platform'); +const WIRING_SRC = readFileSync(join(SRC, 'system', 'platformCoreVehicleHalBridgeWiring.ts'), 'utf8'); +const SYSTEMBOOT_SRC = readFileSync(join(SRC, 'system', 'SystemBoot.ts'), 'utf8'); + +/** + * Kapsam kilitleri YORUMLARA takılmamalı: docstring kapsam-dışı kavramları (throttle, + * canStatus, stale sweeper…) AÇIKLAR. Bu yüzden kilitler yalnız KOD üzerinde çalışır. + */ +const WIRING_CODE = WIRING_SRC + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|[^:])\/\/.*$/gm, '$1'); + +const NOW = 9_000_000; + +/** Gerçek HAL + gerçek Bus ile izole zincir (üretim singleton'larına DOKUNMAZ). */ +function chain() { + const hal = createVehicleHal({ now: () => NOW }); + const bus = createPlatformEventBus({ now: () => NOW }); + const events: PlatformEvent[] = []; + bus.subscribeDomain('vehicle', (e) => events.push(e)); // yalnız TEST gözlemcisi + return { hal, bus, events }; +} + +const ingest = (hal: ReturnType, id: VehicleSignalId, input: VehicleSignalInput) => + hal.ingestSignal(id, input); + +const CAN = (value: number | boolean | number[]): VehicleSignalInput => + ({ value, source: 'can', quality: 'high', confidence: 0.9, timestamp: NOW }); + +const _open: Array<() => void> = []; +function start(deps: Parameters[0] = {}) { + const c = startPlatformCoreVehicleHalBridgeWiring(deps); + _open.push(c); + return c; +} +afterEach(() => { + while (_open.length) { try { _open.pop()!(); } catch { /* */ } } +}); + +/* ── Wiring / tek instance ────────────────────────────────────────────────── */ + +describe('W4C — wiring & tek instance', () => { + it('import yan etkisiz: wiring çağrılmadan bridge YOK', () => { + expect(getVehicleHalBridgeStatus().present).toBe(false); + }); + + it('bus YOKSA fail-soft no-op cleanup (boot çökmez, bridge kurulmaz)', () => { + expect(getAppEventBus()).toBeNull(); // W3 wiring çalışmadı + const { hal } = chain(); + let cleanup: (() => void) | null = null; + expect(() => { cleanup = start({ hal }); }).not.toThrow(); + expect(typeof cleanup).toBe('function'); + expect(getVehicleHalBridgeStatus().present).toBe(false); + }); + + it('start sonrası TEK bridge (present/started)', () => { + const { hal, bus } = chain(); + start({ hal, bus }); + const s = getVehicleHalBridgeStatus(); + expect(s.present).toBe(true); + expect(s.started).toBe(true); + expect(s.disposed).toBe(false); + }); + + it('İKİNCİ start yeni HAL aboneliği AÇMAZ (aynı olay iki kez yayınlanmaz)', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + start({ hal, bus }); // duplicate wiring + ingest(hal, 'vehicle.coolant_temp', CAN(90)); + const changed = events.filter((e) => e.name === 'vehicle.signal.changed'); + expect(changed.length).toBe(1); // TEK event (çift abonelik yok) + }); + + it('cleanup İDEMPOTENT ve sonrasında accessor present=false', () => { + const { hal, bus } = chain(); + const cleanup = start({ hal, bus }); + cleanup(); + expect(() => cleanup()).not.toThrow(); + expect(getVehicleHalBridgeStatus().present).toBe(false); + }); + + it('ESKİ cleanup YENİ aktif bridge kaydını silemez', () => { + const c1 = chain(); + const cleanup1 = start({ hal: c1.hal, bus: c1.bus }); + cleanup1(); + const c2 = chain(); + start({ hal: c2.hal, bus: c2.bus }); + cleanup1(); // bayat cleanup + expect(getVehicleHalBridgeStatus().present).toBe(true); + ingest(c2.hal, 'vehicle.coolant_temp', CAN(80)); + expect(c2.events.some((e) => e.name === 'vehicle.signal.changed')).toBe(true); + }); + + it('boot → shutdown → boot: yeni bridge kurulur ve çalışır', () => { + const c1 = chain(); + const cl = start({ hal: c1.hal, bus: c1.bus }); + cl(); + const c2 = chain(); + start({ hal: c2.hal, bus: c2.bus }); + ingest(c2.hal, 'vehicle.speed', CAN(50)); + expect(c2.events.some((e) => e.name === 'vehicle.signal.changed')).toBe(true); + }); + + it('cleanup bridge\'i dispose eder; HAL ve BUS\'ı DISPOSE ETMEZ', () => { + const { hal, bus } = chain(); + const cleanup = start({ hal, bus }); + cleanup(); + expect(hal.isDisposed).toBe(false); + expect(bus.isDisposed).toBe(false); + expect(bus.publishName('platform.runtime.started')).not.toBeNull(); // bus hâlâ çalışıyor + }); + + it('gerçek W3 bus\'ı ile bağlanır (getAppEventBus) — DOĞRU bus instance', () => { + _open.push(startPlatformCoreEventBusWiring()); + const appBus = getAppEventBus()!; + const seen: string[] = []; + appBus.subscribeDomain('vehicle', (e) => seen.push(e.name)); + const { hal } = chain(); + start({ hal }); // bus DI YOK → getAppEventBus() + ingest(hal, 'vehicle.speed', CAN(60)); + expect(seen).toContain('vehicle.signal.changed'); // app bus'a aktı + }); +}); + +/* ── Event mapping ────────────────────────────────────────────────────────── */ + +describe('W4C — event mapping', () => { + it('supported sinyal değişimi → vehicle.signal.changed (transient)', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + ingest(hal, 'vehicle.speed', CAN(50)); + const e = events.find((x) => x.name === 'vehicle.signal.changed')!; + expect(e).toBeTruthy(); + expect(e.domain).toBe('vehicle'); + expect(e.source).toBe('vehicle_hal'); + expect(bus.getRecentEvents({ name: 'vehicle.signal.changed' }).length).toBe(0); // transient → history yok + }); + + it('aynı değer DUPLICATE event üretmez', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + ingest(hal, 'vehicle.speed', CAN(50)); + ingest(hal, 'vehicle.speed', CAN(50)); // aynı değer → HAL emit yok + expect(events.filter((e) => e.name === 'vehicle.signal.changed').length).toBe(1); + }); + + it('TEK HAL snapshot (batch) → değişen sinyal BAŞINA bir event', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + hal.ingest({ // W4A: tek emit + 'vehicle.speed': CAN(50), + 'vehicle.rpm': CAN(2000), + 'vehicle.coolant_temp': CAN(90), + }); + const changed = events.filter((e) => e.name === 'vehicle.signal.changed'); + expect(changed.length).toBe(3); // 3 sinyal → 3 event, 1 snapshot işlendi + }); + + it('ignition kaynağı yok → ignition event ÜRETİLMEZ', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + hal.ingest({ 'vehicle.speed': CAN(50), 'vehicle.rpm': CAN(1500) }); + expect(events.some((e) => e.name === 'vehicle.ignition.changed')).toBe(false); + expect(hal.hasSignal('vehicle.ignition')).toBe(false); + }); + + it('connection.changed=true ilk supported sinyalde (retained)', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + ingest(hal, 'vehicle.speed', CAN(50)); + const conn = events.filter((e) => e.name === 'vehicle.connection.changed'); + expect(conn.length).toBe(1); + expect((conn[0]!.payload as { connected: boolean }).connected).toBe(true); + expect(conn[0]!.retained).toBe(true); + }); + + it('W4B unsupported geçişi TÜM destek kalkınca connection.changed=false üretir', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + ingest(hal, 'vehicle.speed', CAN(50)); + // W4B: kaynak kaybı → source:'none' → supported=false + ingest(hal, 'vehicle.speed', { value: null, source: 'none', quality: 'unknown', confidence: 0, timestamp: NOW }); + const conn = events.filter((e) => e.name === 'vehicle.connection.changed'); + expect(conn.length).toBe(2); + expect((conn[1]!.payload as { connected: boolean }).connected).toBe(false); + }); + + it('kısmi kayıpta connection=false ÜRETİLMEZ (bir sinyal hâlâ supported)', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + hal.ingest({ 'vehicle.speed': CAN(50), 'vehicle.coolant_temp': CAN(90) }); + ingest(hal, 'vehicle.coolant_temp', { value: null, source: 'none', quality: 'unknown', confidence: 0, timestamp: NOW }); + const conn = events.filter((e) => e.name === 'vehicle.connection.changed'); + expect(conn.length).toBe(1); // yalnız ilk true + }); + + it('reconnect: destek geri gelince connection.changed=true tekrar üretilir', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + ingest(hal, 'vehicle.speed', CAN(50)); + ingest(hal, 'vehicle.speed', { value: null, source: 'none', quality: 'unknown', confidence: 0, timestamp: NOW }); + ingest(hal, 'vehicle.speed', CAN(55)); // reconnect + const conn = events.filter((e) => e.name === 'vehicle.connection.changed') + .map((e) => (e.payload as { connected: boolean }).connected); + expect(conn).toEqual([true, false, true]); + }); + + it('identity: fingerprint değişimi retained event; HAM VIN payload\'a GİRMEZ', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + hal.ingestIdentity({ fingerprintHash: 'a1b2c3d4e5f6', protocol: 'can' }); + ingest(hal, 'vehicle.speed', CAN(50)); // emit tetikle + const idev = events.find((e) => e.name === 'vehicle.identity.changed'); + expect(idev).toBeTruthy(); + expect(idev!.retained).toBe(true); + expect(JSON.stringify(idev!.payload)).not.toMatch(/[A-HJ-NPR-Z0-9]{17}/); // ham VIN yok + }); + + it('TPMS payload bounded (dizi kırpılır, ham veri taşınmaz)', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + ingest(hal, 'vehicle.tpms', CAN([220, 225, 230, 228])); + const e = events.find((x) => x.name === 'vehicle.signal.changed' + && (x.payload as { signalId: string }).signalId === 'vehicle.tpms')!; + const v = (e.payload as { value: number[] }).value; + expect(Array.isArray(v)).toBe(true); + expect(v.length).toBeLessThanOrEqual(8); + }); +}); + +/* ── Lifecycle / fail-soft / privacy ──────────────────────────────────────── */ + +describe('W4C — lifecycle, fail-soft, privacy', () => { + it('cleanup sonrası HAL değişimi publish ÜRETMEZ', () => { + const { hal, bus, events } = chain(); + const cleanup = start({ hal, bus }); + ingest(hal, 'vehicle.speed', CAN(50)); + const before = events.length; + cleanup(); + ingest(hal, 'vehicle.speed', CAN(99)); + expect(events.length).toBe(before); + }); + + it('bridge YALNIZ verilen bus\'a publish eder — ikinci bus\'a event SIZMAZ', () => { + const { hal, bus, events } = chain(); + const other = createPlatformEventBus({ now: () => NOW }); + const otherEvents: PlatformEvent[] = []; + other.subscribeDomain('vehicle', (e) => otherEvents.push(e)); + start({ hal, bus }); + ingest(hal, 'vehicle.speed', CAN(50)); + expect(events.length).toBeGreaterThan(0); + expect(otherEvents.length).toBe(0); + }); + + it('bus publish null dönerse droppedCount artar (kaynak servis etkilenmez)', () => { + const { hal } = chain(); + const nullBus = { publish: () => null }; + start({ hal, bus: nullBus }); + ingest(hal, 'vehicle.speed', CAN(50)); + const s = getVehicleHalBridgeStatus(); + expect(s.droppedCount).toBeGreaterThan(0); + expect(s.publishedCount).toBe(0); + }); + + it('bus publish throw ederse fail-soft (HAL etkilenmez)', () => { + const { hal } = chain(); + const badBus = { publish: () => { throw new Error('bus boom'); } }; + start({ hal, bus: badBus }); + expect(() => ingest(hal, 'vehicle.speed', CAN(50))).not.toThrow(); + expect(hal.getSpeed()).toBe(50); + expect(getVehicleHalBridgeStatus().droppedCount).toBeGreaterThan(0); + }); + + it('HAL subscribe throw ederse wiring throw ETMEZ (boot sürer)', () => { + const badHal = { + subscribe: () => { throw new Error('hal boom'); }, + getSnapshot: () => ({ revision: 0, updatedAt: NOW, signals: [] }), + getVehicleIdentity: () => ({ fingerprintHash: null, protocol: null, supported: false }), + }; + const bus = createPlatformEventBus({ now: () => NOW }); + expect(() => start({ hal: badHal, bus })).not.toThrow(); + }); + + it('listener hatası bridge\'i ÇÖKERTMEZ (bus izole eder)', () => { + const { hal, bus } = chain(); + bus.subscribe('vehicle.signal.changed', () => { throw new Error('listener boom'); }); + start({ hal, bus }); + expect(() => ingest(hal, 'vehicle.speed', CAN(50))).not.toThrow(); + expect(getVehicleHalBridgeStatus().publishedCount).toBeGreaterThan(0); + expect(bus.getStats().listenerErrorCount).toBe(1); + }); + + it('event payload\'ında koordinat/MAC/secret YOK (yalnız sinyal özeti)', () => { + const { hal, bus, events } = chain(); + start({ hal, bus }); + hal.ingest({ 'vehicle.speed': CAN(50), 'vehicle.coolant_temp': CAN(90) }); + const json = JSON.stringify(events.map((e) => e.payload)); + expect(json).not.toMatch(/lat|lon|[0-9A-F]{2}(:[0-9A-F]{2}){5}|token|api_key/i); + }); +}); + +/* ── Diagnostic ───────────────────────────────────────────────────────────── */ + +describe('W4C — diagnostic (support snapshot)', () => { + it('bridge YOKKEN present=false ve sayaçlar NULL ("0 event" ile karışmaz)', () => { + const d = buildPlatformRuntimeSnapshot(); + expect(d.halBridge.present).toBe(false); + expect(d.halBridge.publishedCount).toBeNull(); + expect(d.halBridge.droppedCount).toBeNull(); + }); + + it('bridge VARKEN present=true ve sayaçlar okunur; whitelist DIŞI alan yok', () => { + const { hal, bus } = chain(); + start({ hal, bus }); + ingest(hal, 'vehicle.speed', CAN(50)); + const d = buildPlatformRuntimeSnapshot(); + expect(d.halBridge.present).toBe(true); + expect(d.halBridge.started).toBe(true); + expect(d.halBridge.publishedCount).toBeGreaterThan(0); + expect(d.halBridge.droppedCount).toBe(0); + expect(new Set(Object.keys(d.halBridge))).toEqual(new Set([ + 'present', 'started', 'disposed', 'publishedCount', 'droppedCount', 'lastPublishAt', + ])); + }); + + it('teşhis çıktısı yalnız sayaç/bayrak — event payload / sinyal değeri / topic YOK', () => { + const { hal, bus } = chain(); + start({ hal, bus }); + hal.ingest({ 'vehicle.speed': CAN(137), 'vehicle.coolant_temp': CAN(91) }); + // lastPublishAt (epoch ms) rakamları sinyal değerlerini alt-dize olarak içerebilir → çıkarılır. + const { lastPublishAt: _ts, ...rest } = buildPlatformRuntimeSnapshot().halBridge; + void _ts; + const json = JSON.stringify(rest); + expect(json).not.toMatch(/137|91|signalId|vehicle\.signal|payload/); + for (const v of Object.values(buildPlatformRuntimeSnapshot().halBridge)) { + expect(['number', 'boolean', 'object']).toContain(typeof v); + } + }); +}); + +/* ── Kapsam sınırı & SystemBoot sırası ────────────────────────────────────── */ + +describe('W4C — kapsam sınırı', () => { + it('throttle/debounce/coalescing/async-queue EKLENMEDİ (W4D)', () => { + expect(WIRING_CODE).not.toMatch(/THROTTLE_MS|debounce|coalesce|queueMicrotask/i); + expect(WIRING_CODE).not.toMatch(/setInterval|setTimeout|requestAnimationFrame/); + }); + + it('consumer/abone EKLENMEDİ (bus.subscribe yok)', () => { + expect(WIRING_CODE).not.toMatch(/\.subscribe\(|subscribeDomain/); + }); + + it('Capability / Deep Scan / Kernel / native canStatus / stale sweeper YOK', () => { + expect(WIRING_CODE).not.toMatch(/from\s+['"][^'"]*capability/i); + expect(WIRING_CODE).not.toMatch(/from\s+['"][^'"]*deepScan/i); + expect(WIRING_CODE).not.toMatch(/from\s+['"][^'"]*kernel/i); + expect(WIRING_CODE).not.toMatch(/canStatus|halStatusStore|\.refresh\s*\(/); + }); + + it('kalıcı global debug expose YOK', () => { + expect(WIRING_CODE).not.toMatch(/window\.__|globalThis\./); + }); + + it('SystemBoot: bridge HAL wiring\'den SONRA, SystemOrchestrator\'dan ÖNCE (_reg ile)', () => { + const iBus = SYSTEMBOOT_SRC.indexOf('this._reg(startPlatformCoreEventBusWiring())'); + const iHal = SYSTEMBOOT_SRC.indexOf('this._reg(startPlatformCoreVehicleHalWiring('); + const iBridge = SYSTEMBOOT_SRC.indexOf('this._reg(startPlatformCoreVehicleHalBridgeWiring())'); + const iOrch = SYSTEMBOOT_SRC.indexOf('this._reg(startSystemOrchestrator())'); + expect(iBus).toBeGreaterThan(0); + expect(iHal).toBeGreaterThan(iBus); // Event Bus (Wave 1) EN ÖNCE + expect(iBridge).toBeGreaterThan(iHal); // bridge, HAL wiring'den SONRA + expect(iOrch).toBeGreaterThan(iBridge); // SystemOrchestrator'dan ÖNCE + expect(SYSTEMBOOT_SRC).not.toMatch(/_regNamed\([^)]*BridgeWiring/); + }); + + it('mevcut Wave sırası (1→2→3→4) DEĞİŞMEDİ', () => { + const i1 = SYSTEMBOOT_SRC.indexOf('await this._wave1()'); + const i2 = SYSTEMBOOT_SRC.indexOf('await this._wave2()'); + const i3 = SYSTEMBOOT_SRC.indexOf('await this._wave3()'); + const i4 = SYSTEMBOOT_SRC.indexOf('await this._wave4()'); + expect(i2).toBeGreaterThan(i1); + expect(i3).toBeGreaterThan(i2); + expect(i4).toBeGreaterThan(i3); + }); +}); diff --git a/src/__tests__/platformRuntimeDiagnostics.test.ts b/src/__tests__/platformRuntimeDiagnostics.test.ts index 567d0211..25763fc3 100644 --- a/src/__tests__/platformRuntimeDiagnostics.test.ts +++ b/src/__tests__/platformRuntimeDiagnostics.test.ts @@ -66,9 +66,13 @@ describe('W4E — teşhis üretimi runtime wiring BAŞLATMAZ', () => { expect(platformBlock).not.toMatch(/setInterval|setTimeout|requestAnimationFrame/); }); - it('teşhis bridge/consumer wiring EKLEMEZ (bridge bölümü yok — W4C)', () => { + it('teşhis bridge/consumer wiring KURMAZ (W4C ile bridge bölümü OKUNUR, ama yaratılmaz)', () => { + // KİLİT GÜNCELLENDİ (W4C): bridge artık üretimde bağlı → teşhis onun BOUNDED sayaçlarını + // OKUR. Değişmeyen invaryant: teşhis kendisi bridge/consumer YARATMAZ ve publish/subscribe ETMEZ. const snap = buildPlatformRuntimeSnapshot(); - expect(Object.keys(snap)).toEqual(['eventBus', 'halWiring']); // bridge bölümü YOK + expect(Object.keys(snap)).toEqual(['eventBus', 'halWiring', 'halBridge']); + expect(snap.halBridge.present).toBe(false); // wiring çalışmadı → "ölçülemiyor" (0 değil) + expect(snap.halBridge.publishedCount).toBeNull(); const platformBlock = SECTIONS_SRC.slice(SECTIONS_SRC.indexOf('PLATFORM RUNTIME')); expect(platformBlock).not.toMatch(/createVehicleHalEventBridge|\.subscribe\(|\.publish\(/); }); @@ -174,7 +178,19 @@ describe('W4E — whitelist ve privacy sınırı', () => { store: fakeStore({ speed: 137, canCoolantTemp: 91, canBatteryVolt: 14.4 }), hal: fakeHal, })); publishRuntimeStarted(); - const json = JSON.stringify(buildPlatformRuntimeSnapshot()); + // Zaman damgaları (epoch ms) rakam dizisi olarak sinyal değerlerini "içerebilir" — alt-dize + // taraması onlara takılmasın diye ölçüm-zamanı alanları çıkarılır (kilit KIRILGAN değil). + const snap = buildPlatformRuntimeSnapshot(); + const scrub = (o: Record) => { + const c = { ...o }; + delete c.lastEventAt; delete c.lastRefreshAt; delete c.lastPublishAt; + return c; + }; + const json = JSON.stringify({ + eventBus: scrub(snap.eventBus as unknown as Record), + halWiring: scrub(snap.halWiring as unknown as Record), + halBridge: scrub(snap.halBridge as unknown as Record), + }); expect(json).not.toMatch(/137|91|14\.4/); // sinyal değerleri yok (yalnız sayaçlar) expect(json).not.toMatch(/speed|rpm|coolant|voltage|lat|lon/i); }); diff --git a/src/platform/diagnosticSections.ts b/src/platform/diagnosticSections.ts index f2de2c0e..228350a5 100644 --- a/src/platform/diagnosticSections.ts +++ b/src/platform/diagnosticSections.ts @@ -33,6 +33,7 @@ import { // bu modüller çağrılmadan bus/adapter YARATMAZ; teşhis okuma runtime instance oluşturmaz). import { getEventBusStatus } from './system/platformCoreEventBusWiring'; import { getVehicleHalWiringStatus } from './system/platformCoreVehicleHalWiring'; +import { getVehicleHalBridgeStatus } from './system/platformCoreVehicleHalBridgeWiring'; /* ── OBD DERİN ───────────────────────────────────────────────── */ @@ -469,9 +470,24 @@ export interface PlatformHalWiringDiag { lastErrorCode: string | null; } +/** + * Vehicle HAL → Event Bus bridge (W4C). `present:false` = bridge RUNTIME'DA YOK + * ("ölçülemiyor"); `present:true` + `publishedCount:0` = bridge var ama henüz event yok. + * Bu ikisi KARIŞTIRILMAZ. Event payload'ı / signal id-değer listesi / topic detayı GİRMEZ. + */ +export interface PlatformHalBridgeDiag { + present: boolean; + started: boolean; + disposed: boolean; + publishedCount: number | null; + droppedCount: number | null; + lastPublishAt: number | null; +} + export interface PlatformRuntimeSnapshot { eventBus: PlatformEventBusDiag; halWiring: PlatformHalWiringDiag; + halBridge: PlatformHalBridgeDiag; } const _EVENT_BUS_ABSENT: PlatformEventBusDiag = { @@ -482,6 +498,11 @@ const _EVENT_BUS_ABSENT: PlatformEventBusDiag = { runtimeStartedPublished: false, runtimeStoppedPublished: false, }; +const _HAL_BRIDGE_ABSENT: PlatformHalBridgeDiag = { + present: false, started: false, disposed: false, + publishedCount: null, droppedCount: null, lastPublishAt: null, +}; + const _HAL_WIRING_ABSENT: PlatformHalWiringDiag = { started: false, lastRefreshAt: null, refreshCount: null, ingestedSignalCount: null, activeSubscriptionCount: null, lastErrorCode: null, @@ -537,7 +558,20 @@ export function buildPlatformRuntimeSnapshot(): PlatformRuntimeSnapshot { }; }, _HAL_WIRING_ABSENT); - return { eventBus: bus, halWiring }; + const halBridge = _safe(() => { + const s = getVehicleHalBridgeStatus(); + if (!s || s.present !== true) return _HAL_BRIDGE_ABSENT; // bridge yok → sayaç YOK (0 değil) + return { + present: true, + started: s.started === true, + disposed: s.disposed === true, + publishedCount: _count(s.publishedCount), + droppedCount: _count(s.droppedCount), + lastPublishAt: _ts(s.lastPublishAt), + }; + }, _HAL_BRIDGE_ABSENT); + + return { eventBus: bus, halWiring, halBridge }; } /* ── util ────────────────────────────────────────────────────── */ diff --git a/src/platform/system/SystemBoot.ts b/src/platform/system/SystemBoot.ts index ce28e762..062f8e57 100644 --- a/src/platform/system/SystemBoot.ts +++ b/src/platform/system/SystemBoot.ts @@ -36,6 +36,7 @@ import { } from '../vehicleDataLayer'; import { startSystemOrchestrator } from './SystemOrchestrator'; import { startPlatformCoreVehicleHalWiring } from './platformCoreVehicleHalWiring'; +import { startPlatformCoreVehicleHalBridgeWiring } from './platformCoreVehicleHalBridgeWiring'; import { startPlatformCoreEventBusWiring, publishRuntimeStarted, @@ -536,6 +537,17 @@ class SystemBoot { logError('SystemBoot:vehicleHalWiring', e); } + // Platform Core: Vehicle HAL → Event Bus bridge (W4C). HAL wiring'den SONRA kaydedilir → + // LIFO shutdown'da bridge, HAL wiring'den ve (Wave 1'deki) Event Bus'tan ÖNCE dispose olur, + // yani bridge kapanırken HAL ve Bus hâlâ AYAKTA. Bus yoksa wiring sessizce no-op döner + // (fail-soft). Bu PR'da bus'a ABONE YOK (consumer migration ayrı PR); throttle/coalescing W4D. + _log(' › Vehicle HAL → Event Bus bridge (Platform Core)'); + try { + this._reg(startPlatformCoreVehicleHalBridgeWiring()); + } catch (e) { + logError('SystemBoot:vehicleHalBridgeWiring', e); // wiring zaten fail-soft; sözleşme ihlali koruması + } + // SystemOrchestrator: VDL event'lerini UI sinyallerine dönüştürür _log(' › SystemOrchestrator'); this._reg(startSystemOrchestrator()); diff --git a/src/platform/system/platformCoreVehicleHalBridgeWiring.ts b/src/platform/system/platformCoreVehicleHalBridgeWiring.ts new file mode 100644 index 00000000..2ae28646 --- /dev/null +++ b/src/platform/system/platformCoreVehicleHalBridgeWiring.ts @@ -0,0 +1,170 @@ +/** + * platformCoreVehicleHalBridgeWiring — Vehicle HAL → Event Bus BRIDGE RUNTIME WIRING (W4C). + * + * AMAÇ: Mevcut `VehicleHalEventBridge` foundation'ını (PR #54) gerçek `vehicleHal` singleton'ına + * ve W3'ün TEK aktif `appEventBus` örneğine bağlar. Zincir tamamlanır: + * + * UnifiedVehicleStore → VehicleHalProviderAdapter → vehicleHal + * → VehicleHalEventBridge → appEventBus (abone YOK — consumer migration AYRI PR) + * + * ⚠️ KOD GERÇEĞİ (salt-okunur analiz): + * - `createVehicleHalEventBridge({hal, bus})` fabrikadır; GLOBAL SINGLETON YOK; yapıcı YAN + * ETKİSİZ (abonelik yalnız `start()`'ta) — `vehicleHalEventBridge.ts:283-289`. + * - Bridge kaynakların SAHİBİ DEĞİL: `dispose()` HAL'i/Bus'ı dispose ETMEZ (`:272-278`). + * - Bridge'te timer/polling YOK; throttle/debounce YOK — yalnız imza-tabanlı DIFF/dedupe + * (`:123-126, 184`). Publish hızı = gerçek sinyal değişim hızı. Ölçüm W4D kararına girdi. + * - `PlatformEventBus.publish()` SENKRON dispatch eder (`platformEventBus.ts:425-426`) → + * ileride eklenecek consumer'lar HAL/store hot-path stack'inde çalışır. Bu PR'da abone YOK. + * - `vehicleHal` yapısal olarak `VehicleHalSource`'a uyar (subscribe/getSnapshot/ + * getVehicleIdentity — `vehicleHal.ts:344, 391`); `PlatformEventBus` `EventBusPublishTarget`'a. + * + * NE YAPMAZ (bilinçli — W4C yalnız WIRING'dir): + * - throttle · debounce · microtask/frame coalescing · DeviceTier frekans politikası · + * Event Bus async queue → **W4D** (ölçümden SONRA). + * - consumer/abone EKLEMEZ · Capability bridge/Registry · Deep Scan · Kernel servis kaydı · + * yeni event catalog girdisi · HAL stale sweeper · native `canStatus` wiring · worker + * source-health expose · UI/SQL değişikliği — HİÇBİRİ. + * - Foundation davranışını (bridge/bus/HAL/adapter/provider) DEĞİŞTİRMEZ. + * + * SAHİPLİK: bridge bu modülündür → cleanup YALNIZ bridge'i dispose eder. `vehicleHal` (Wave 2 + * wiring'in beslediği singleton) ve `appEventBus` (W3'ün sahibi olduğu bus) DISPOSE EDİLMEZ. + * TEK INSTANCE: aktif bridge varken ikinci `start` YENİ HAL aboneliği açmaz; bayat (disposed) + * kayıt otomatik serbest bırakılır; cleanup yalnız KENDİ kaydını siler → boot→shutdown→boot güvenli. + * + * FAIL-SOFT: bus YOKSA (W3 wiring çalışmadıysa) sessizce no-op cleanup döner — boot sürer. + * Public API dışarı EXCEPTION KAÇIRMAZ; hata bir kez `logError` ile kaydedilir (ham event/araç + * sinyali/VIN/koordinat LOGLANMAZ). ZERO-LEAK: cleanup bridge aboneliğini bırakır; timer yok. + */ + +import { logError } from '../crashLogger'; +import { + createVehicleHalEventBridge, + type VehicleHalEventBridge, + type VehicleHalEventBridgeDeps, + type PlatformEventBus, + type PlatformEventDomain, + type PlatformEventSource, +} from '../eventBus'; +import { vehicleHal } from '../vehicleHal'; +import { getAppEventBus } from './platformCoreEventBusWiring'; + +type BridgeHalSource = VehicleHalEventBridgeDeps['hal']; +type BridgePublishTarget = VehicleHalEventBridgeDeps['bus']; + +/** Test için opsiyonel DI; üretimde `vehicleHal` singleton + `getAppEventBus()`. */ +export interface VehicleHalBridgeWiringDeps { + readonly hal?: BridgeHalSource; + readonly bus?: BridgePublishTarget; +} + +/** + * Bridge'in publish sözleşmesi `domain`/`source`'u GENİŞ (`string`) tiple alır; `PlatformEventBus` + * ise DAR birlik tipleri (`PlatformEventDomain`/`PlatformEventSource`) bekler → bus doğrudan hedef + * olarak GEÇEMEZ (property-syntax → kontravaryans). Bu ince sarmalayıcı, foundation'a HİÇ DOKUNMADAN + * daraltmayı wiring katmanında yapar. Doğrulama ZAYIFLAMAZ: Bus geçersiz domain/source değerlerini + * zaten kendi beyaz listesiyle reddeder/`unknown`'a düşürür. + */ +function _toPublishTarget(bus: PlatformEventBus): BridgePublishTarget { + return { + publish: (input) => bus.publish({ + name: input.name, + payload: input.payload, + domain: input.domain as PlatformEventDomain | undefined, + source: input.source as PlatformEventSource | undefined, + transient: input.transient, + retained: input.retained, + vehicleFingerprintHash: input.vehicleFingerprintHash, + }), + }; +} + +/** Tek cleanup thunk — İDEMPOTENT. YALNIZ bridge'i dispose eder (HAL/Bus'a DOKUNMAZ). */ +export type VehicleHalBridgeWiringCleanup = () => void; + +/** Bounded teşhis görünümü — event payload'ı / sinyal değeri / topic detayı TAŞIMAZ. */ +export interface VehicleHalBridgeWiringStatus { + /** Bridge runtime'da kurulu mu (false → "ölçülemiyor", 0 event ile KARIŞTIRILMAZ). */ + readonly present: boolean; + readonly started: boolean; + readonly disposed: boolean; + readonly publishedCount: number | null; + readonly droppedCount: number | null; + readonly lastPublishAt: number | null; +} + +const NOOP_CLEANUP: VehicleHalBridgeWiringCleanup = () => { /* no-op */ }; + +const ABSENT_STATUS: VehicleHalBridgeWiringStatus = Object.freeze({ + present: false, started: false, disposed: false, + publishedCount: null, droppedCount: null, lastPublishAt: null, +}); + +let _active: VehicleHalEventBridge | null = null; + +/** Bayat kayıt (HMR/restart artığı: dispose edilmiş bridge) → serbest bırak. */ +function _pruneStale(): void { + if (_active && _active.isDisposed) _active = null; +} + +/** + * Bridge'i oluşturur, HAL'e abone eder ve TEK aktif bus'a publish etmesini sağlar. + * YALNIZ cleanup thunk döner. Dışarı exception KAÇIRMAZ. İDEMPOTENT (ikinci çağrı yeni + * HAL aboneliği AÇMAZ). Bus yoksa fail-soft no-op. + */ +export function startPlatformCoreVehicleHalBridgeWiring( + deps: VehicleHalBridgeWiringDeps = {}, +): VehicleHalBridgeWiringCleanup { + let bridge: VehicleHalEventBridge | null = null; + try { + _pruneStale(); + if (_active) return NOOP_CLEANUP; // zaten aktif → ikinci abonelik YOK + + const appBus = getAppEventBus(); // W3'ün TEK aktif bus'ı + const bus: BridgePublishTarget | null = deps.bus ?? (appBus ? _toPublishTarget(appBus) : null); + if (!bus) return NOOP_CLEANUP; // bus yok → sessiz no-op (boot sürer) + const hal = deps.hal ?? vehicleHal; // Wave 2 wiring'in beslediği singleton + + bridge = createVehicleHalEventBridge({ hal, bus }); + const own = bridge; + _active = own; + own.start(); // HAL aboneliği + ilk snapshot (bridge içi fail-soft) + + let disposed = false; + return () => { + if (disposed) return; // İDEMPOTENT + disposed = true; + try { + own.dispose(); // YALNIZ bridge — HAL/Bus DISPOSE EDİLMEZ + } catch (e) { + logError('vehicleHalBridgeWiring:cleanup', e); // cleanup hatası shutdown'ı engellemez + } + if (_active === own) _active = null; // yalnız KENDİ kaydını siler + }; + } catch (e) { + if (bridge && _active === bridge) _active = null; // yarım kayıt bırakma + logError('vehicleHalBridgeWiring:init', e); // ham event/sinyal/VIN LOGLANMAZ + return NOOP_CLEANUP; // boot devam eder (fail-soft) + } +} + +/** Bounded teşhis görünümü. Bridge yoksa `present:false` + null sayaçlar. Throw ETMEZ. */ +export function getVehicleHalBridgeStatus(): VehicleHalBridgeWiringStatus { + _pruneStale(); + const b = _active; + if (!b) return ABSENT_STATUS; + try { + const s = b.getStatus(); + return Object.freeze({ + present: true, + started: s.started === true, + disposed: s.disposed === true, + publishedCount: Number.isFinite(s.publishedCount) ? s.publishedCount : null, + droppedCount: Number.isFinite(s.droppedCount) ? s.droppedCount : null, + lastPublishAt: typeof s.lastPublishAt === 'number' && Number.isFinite(s.lastPublishAt) + ? s.lastPublishAt + : null, + }); + } catch { + return ABSENT_STATUS; // teşhis yolu asla çökmez + } +}