diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index fe22cc25..8b1988b9 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 | |---|---------|--------|------------------------------------------|---------| +| 49 | **🔴 Device Validation Pending — Vehicle HAL Runtime Wiring** — Mevcut `useUnifiedVehicleStore` (fused worker/CAN/OBD/GPS) veri akışını, hazır SALT-OKUNUR provider + adapter foundation üzerinden gerçek `vehicleHal` singleton'ına AYNA modunda bağlar. Zincir: `useUnifiedVehicleStore` → `createUnifiedVehicleStoreProvider` → `VehicleHalProviderAdapter` → `vehicleHal`. **Yalnız HAL veri girişi aktive edilir** — Event Bus wiring YOK · Capability Registry wiring YOK · Platform Kernel servis kaydı YOK · Deep Scan wiring YOK · tüketici migrasyonu YOK · UI/native/OBD/CAN/poll frekansı DEĞİŞMEZ. **SystemBoot:** Wave 2'de VehicleDataLayer'dan SONRA, SystemOrchestrator'dan ÖNCE `_reg` ile kaydedilir → LIFO shutdown'da store hâlâ ayaktayken adapter/provider dispose olur; Wave sırası (1→2→3→4) DEĞİŞMEZ. **Tek instance:** modül-düzeyi aktif-kayıt guard → duplicate `start` ikinci provider/adapter/subscription AÇMAZ (no-op cleanup); HMR/restart bayat kaydı otomatik serbest bırakılır; cleanup yalnız KENDİ kaydını siler → boot→shutdown→boot güvenli. **Sahiplik:** provider+adapter wiring'e ait (cleanup dispose eder); `vehicleHal` PAYLAŞILAN singleton → cleanup HAL'i DISPOSE ETMEZ (çift-dispose yok). **Sinyaller (store'da gerçekten var):** speed · rpm · fuel_level · odometer · reverse · coolant_temp · oil_temp · throttle · battery_voltage · gear · ambient_temp · door_state · parking_brake; **TPMS yalnız gerçek 4-tuple veri varsa**; **ignition kaynağı YOK → ingest EDİLMEZ (supported=false, fail-closed)**; metadata UYDURULMAZ. **Duplicate ingest yok** (adapter O(1) son-değer dedupe); yeni timer/polling YOK (mevcut store subscribe akışı); hot-path'te JSON stringify/deep clone YOK. **Fail-soft:** store yoksa/bozuksa boot ÇÖKMEZ (tek `logError` + no-op cleanup, wiring kilitlenmez); provider/adapter/HAL-ingest hatası izole (tek sinyal bozuksa diğerleri sürer); cleanup hatası shutdown'ı engellemez; hata hâlinde legacy store tüketicileri AYNEN çalışır. **Gözlemlenebilirlik:** bounded `getVehicleHalWiringStatus()` (started/lastRefreshAt/ingestedSignalCount/refreshCount/activeSubscriptionCount/lastErrorCode) — ham telemetri/log spam YOK. | `platform/system/platformCoreVehicleHalWiring.ts` (yeni), `platform/system/SystemBoot.ts` (+13 satır, additive), `__tests__/platformCoreVehicleHalWiring.test.ts` (yeni, 43 test) | Gerçek cihazda: (1) boot'ta crash/ANR YOK; (2) TEK HAL/provider/adapter instance ve TEK store aboneliği (`getVehicleHalWiringStatus().activeSubscriptionCount === 1`); (3) araç bağlıyken hız/RPM/soğutma suyu/akü voltajı/vites-reverse HAL'e DOĞRU ulaşıyor; (4) aynı veri duplicate ingest ÜRETMİYOR; (5) OBD reconnect/uygulama restart sonrası subscription ÇOĞALMIYOR (hâlâ 1); (6) kaynağı olmayan `vehicle.ignition` supported=false KALIYOR; (7) TPMS kaynağı olmayan araçta supported GÖRÜNMÜYOR; (8) boot süresi regresyonu <50 ms (öncesi/sonrası ölçülür); (9) Xiaomi'de steady-state idle CPU regresyonu YOK; (10) Mali-400/K24'te CPU/RAM/FPS ayrıca ölçülür; (11) stale davranışı: veri kesilince HAL sinyalleri stale'e düşüyor. ⚠️ **Event Bus · Capability Registry · Platform Kernel servis kaydı · tüketici migrasyonu AYRI PR'lar.** Test edilmeden 🟢 yapma. ⚠️ **KISMİ CİHAZ KANITI (2026-07-12, Xiaomi 23090RA98I — ARAÇ/OBD YOK):** boot crash/ANR YOK (4 cold start + background/foreground + restart, logcat FATAL/ANR/vehicleHalWiring temiz); cold start WaitTime baseline (wiring'siz main) 3013 ms → wiring 3011/3015/3046/3078 ms (ort. ~3037, **delta ~+24 ms < 50 ms hedefi**); idle CPU: her iki build de 3–30% arası dalgalı, ayırt edici regresyon gözlenmedi. **HÂLÂ DOĞRULANMADI** (bu yüzden 🔴): tek subscription/instance sayacı (in-page teşhis kancası yok), HAL sinyal akışı (hız/RPM/soğutma/voltaj), ignition & TPMS fail-closed, reconnect/stale davranışı — GERÇEK ARAÇ/OBD gerektirir. Mali-400/K24 ölçülmedi. | 2026-07-12 | | 48 | **🔴 Device Validation Pending — Platform Kernel / Service Lifecycle Foundation** — Platform servislerinin (Vehicle HAL · Capability Registry · Event Bus · Deep Scan · Navigation · AI · Fleet · Remote · Health · Vision) yaşam döngüsünü, bağımlılıklarını, başlatma/durdurma SIRASINI, sağlık durumunu ve HATA İZOLASYONUNU yöneten merkezi çekirdek. ⚠️ **KOD GERÇEĞİ (salt-okunur analiz):** lifecycle ≥3 desende parçalı — SystemBoot "Wave 1-4"+LIFO cleanup+named restart (MAX_RESTARTS=2, BACKOFF_BASE 5s, COOLOFF 5dk); modül-düzeyi startX/stopX; servis-içi _listeners/_reg. ORTAK descriptor/bağımlılık/health/circuit modeli YOK → Kernel doldurur (gerçek servisleri BAĞLAMADAN, yalnız DI). **Model:** 11 state (registered→initializing→ready→starting→running/degraded/paused→stopping→stopped/failed/disposed) · 6 criticality · 6 startPolicy · 4 restartPolicy · 4 health. **Descriptor:** id/version/criticality/dependencies/optionalDependencies/requiredCapabilities/minimumDeviceTier/startPolicy/restartPolicy/init·start·stopTimeoutMs/owner/experimental. **Service contract:** init/start/pause/resume/stop/health/dispose HEPSİ opsiyonel (eksik→no-op). **API:** register/unregister/has/getServiceStatus/list/resolveStartOrder/initialize/start/startAll/pause/resume/stop/stopAll/restart/markDegraded/runHealthCheck/getKernelSnapshot/subscribe/reset/dispose. **Dependency:** directed-acyclic; **cycle kayıt anında reddedilir**; topolojik başlatma (tie-break=id), **ters topolojik durdurma**; eksik required→başlamaz; eksik optional→degraded başlar; dependency failed→dependent bloke. **Gate (DI):** Capability Registry DOĞRUDAN import EDİLMEZ (isAvailable/getStatus/subscribe contract); required unknown/unavailable→başlamaz; degraded→degraded başlar; **minimumDeviceTier sağlanmazsa başlamaz** (Mali-400/low'da optional ağır servis GATE). **Timeout:** proje 5s konvansiyonundan türetildi (magic yok); timer YALNIZ aktif lifecycle çağrısı boyunca (settle'da temizlenir); **steady-state timer/polling YOK**. **Restart/circuit:** never/manual/on_failure/bounded_auto; bounded (safety=1, diğer=2); max aşımında **circuit-open**; **retry timer döngüsü YOK** (backoff yalnız saf `computeKernelBackoffMs` contract). **Health:** service.health() varsa çağrılır (fail-soft), yoksa state'ten türetilir; result state'i MUTATE ETMEZ. **Event (DI opsiyonel):** publishName ile platform.runtime.started/stopped/degraded · platform.service.started/stopped/failed; **Event Bus yoksa lifecycle sürer**, publish hatası izole. **Fail-soft:** servis exception izole, bir servis düşse bağımsızlar sürer, public API throw etmez, unknown servis güvenli, dispose sonrası no-op, partial-start+stopAll güvenli. **Immutable** snapshot/status (nested dahil), descriptor girdi mutate edilmez, iç Map/graph dışa verilmez. **Privacy:** degradedReasons/lastErrorCode sanitize (VIN/MAC/koordinat/secret sızmaz). **YALIN:** SystemBoot'a BAĞLANMAZ · gerçek servisleri auto-start ETMEZ · HAL/Registry/Bus'ı KENDİSİ YARATMAZ (DI) · persistence/SQL/cloud YOK · global singleton YOK · yeni bağımlılık YOK · import yan etkisiz (yalnız DeviceTier TYPE). Kernel servis iş mantığını BİLMEZ. Bounded (servis 128 · listener 32). **Gerçek servis wiring · SystemBoot wiring AYRI PR.** | `platform/kernel/platformKernel.ts` (yeni), `platform/kernel/index.ts` (yeni), `__tests__/platformKernel.test.ts` (yeni, ~53 test) | Gerçek cihazda (gerçek servis wiring PR'ı bağlandıktan SONRA): (1) servisler dependency SIRASIYLA başlıyor; (2) stop sırasında TERS sırayla kapanıyor; (3) bir servis çökünce bağımsız servisler çalışmaya devam ediyor; (4) capability/DeviceTier olmayan servis başlatılmıyor; (5) Mali-400 cihazda ağır optional servisler gate ediliyor; (6) lifecycle timeout ve cleanup sonrası timer/listener sızıntısı yok; (7) kernel snapshot servis durumlarını doğru gösteriyor; (8) BASIC_JS/Mali-400'de sürekli CPU/RAM yükü yok (steady-state timer yok, pasifken sıfır yük). ⚠️ **Gerçek servis + SystemBoot wiring AYRI PR.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 45 | **🟢 FullMapView Idle Render Optimization (device-verified 2026-07-12, +WAKE-tarafı park tutucusu)** — Cihaz QA'sının FAIL sebebini kapatır: tam ekran harita **boşta, kullanıcı dokunmadan** 1.5–2.1 CPU çekirdeği yakıyordu (+30sn %64 · +60sn %96 · +2dk %106 · **+3dk %210** · +6dk %167; harita kapanınca 45sn'de %20). **KÖK NEDEN (iki katman):** (1) idle kapısı **GPS gürültüsünü hareket sanıyordu** — park hâlinde ±3 m doğrulukla gelen fix'ler "3 m yer değiştirme ≈ 10 km/h" üretip `isIdleNow`'u sürekli false yapıyor, rAF döngüsü **hiç uyumuyordu**; (2) döngü uyanıkken tick **değişiklik olmasa da** `updateUserMarker()` çağırıyor, o da **koşulsuz `source.setData()`** yapıyordu (`MapLayerManager.ts:371`) → her 60 ms'de bir MapLibre repaint + kamera 150–500 ms'de yeniden hesap → sürekli GPU/compositor yükü (`mali-event-hand` %24 + `Chrome_InProcGp` %17). **ÇÖZÜM (yalnız FullMapView.tsx):** (a) **yapılan-iş kapısı** — marker/kamera `NO_WORK_IDLE_MS=2500` boyunca hiç güncellenmediyse harita gerçekten sabittir → döngü uyur (gürültüye dayalı hız/yer-değiştirme sezgilerinin yerine **ground truth**); (b) **marker + kamera dedup** (eşikler: 0.3 m / 0.5° / 0.5 km/h; kamera 0.5 m / 0.5°) → değişmeyen kare repaint üretmez; (c) **park gürültü tutucusu** — hız < 1.5 km/h ve nav/sürüş yokken fix'in kendi doğruluk yarıçapı (≥5 m) altındaki kayma hareket sayılmaz; (d) **force kapıları** — kullanıcı pan'inden sonra kamera MUTLAKA geri merkezler, stil/katman düşerse marker MUTLAKA yeniden çizilir (self-healing korunur); (e) DR yolu ve tek-nokta dalı (her karede setData ediyordu) da dedup'a bağlandı. **Wake tetikleyicileri ve hareket semantiği DEĞİŞMEDİ** → nav/sürüş/takip davranışı aynı. | `components/map/FullMapView.tsx` (tek dosya), `__tests__/fullMapViewIdleRender.test.ts` (yeni, 25 test) | **🔴 Xiaomi zircon (bekliyor):** (1) tam ekran harita açık + dokunulmadan 3 dk → process CPU **%210 → tek haneli/düşük onlu** seviyeye insin (önceki QA baseline: `docs-local/qa-runs/2026-07-11T15-52-52Z-deep-device-analysis/`); (2) `dumpsys gfxinfo` boşta kare üretimi ~56 fps'ten **~0–2 fps**'e insin; (3) harita **kapatılınca** timer/rAF tamamen dursun; (4) 30 dk soak: PSS/Graphics düz, crash/ANR 0; (5) **navigasyon davranışı DEĞİŞMESİN** — rota başlat/reroute/takip/marker/pan-sonrası-recenter aynı; (6) pan/zoom/rotate akıcılığı bozulmasın. **🔴 Mali-400 (K24) — bekliyor:** aynı ölçümler; asıl kazanç orada kanıtlanmalı. Cihazda görülmeden 🟢 YAPILMAZ. **✅ DOĞRULAMA (2026-07-12, gerçek Xiaomi 23090RA98I, main+#64+#61, tam ekran harita idle/park, dokunma yok, 12s settle, CDP rAF-attribution + `/proc` jiffies — screencap YOK):** İLK ÖLÇÜM #61'i **DÜŞÜRDÜ** — idle CPU 108% (main+#64 106% ile aynı), rAF 137/sn, idle-gate döngüsü `L` 278/3sn (HİÇ uyumuyor). **KÖK NEDEN (cihaz CDP tanısı, geçici `window.__ib`/`__wc` instrumentation):** `isIdleNow` %100 `hysteresis(wake <2500ms)` bloğunda takılıyordu; wake **2.8/sn spam** — `onGPSLocation` displacement-wake (satır ~318) **accuracy-BLIND**'dı → park GPS gürültüsü (accuracy 3m, ~3m zıplama) "≥3m/≥5km/h" eşiğini geçip wake tetikliyordu. #61'in park tutucusu yalnız `isIdleNow` (döngü ÇIKIŞI) tarafındaydı, wake GİRİŞİ korunmamıştı → **yarım fix**. **DÜZELTME (wake-tarafı park tutucusu, `isIdleNow` ile tutarlı semantik):** park (nav/sürüş yok + hız; +} +function createFakeHal(): FakeHal { + const ingests: Array<{ id: string; value: unknown }> = []; + return { + ingestSignal(id, input) { ingests.push({ id: String(id), value: input.value }); return null; }, + ingests, + }; +} + +interface FakeStore extends UnifiedVehicleStoreLike { + set(next: Partial): void; + readonly listenerCount: number; +} +function createFakeStore(initial: UnifiedVehicleStateReadable): FakeStore { + let state: UnifiedVehicleStateReadable = { ...initial }; + const listeners = new Set<(s: UnifiedVehicleStateReadable) => void>(); + return { + getState: () => state, + subscribe: (l) => { listeners.add(l); return () => { listeners.delete(l); }; }, + set: (next) => { state = { ...state, ...next }; listeners.forEach((l) => l(state)); }, + get listenerCount() { return listeners.size; }, + }; +} + +const idsOf = (hal: FakeHal) => hal.ingests.map((x) => x.id); +const valuesOf = (hal: FakeHal, id: string) => hal.ingests.filter((x) => x.id === id).map((x) => x.value); + +const SRC_DIR = join(process.cwd(), 'src'); +const WIRING_SRC = readFileSync(join(SRC_DIR, 'platform', 'system', 'platformCoreVehicleHalWiring.ts'), 'utf8'); +const SYSTEMBOOT_SRC = readFileSync(join(SRC_DIR, 'platform', 'system', 'SystemBoot.ts'), 'utf8'); + +/** Aktif wiring kaydı testler arası SIZMASIN (tek-instance guard modül düzeyinde). */ +const _open: Array<() => void> = []; +function start(deps: { store: UnifiedVehicleStoreLike | null; hal?: VehicleHalIngestTarget }) { + const c = startPlatformCoreVehicleHalWiring(deps); + _open.push(c); + return c; +} +afterEach(() => { + while (_open.length) { try { _open.pop()!(); } catch { /* */ } } +}); + +/* ── Yaşam döngüsü + veri akışı ────────────────────────────────────────────── */ + +describe('PR-W2 Vehicle HAL wiring — yaşam döngüsü & veri akışı', () => { + it('1) import side-effect yok: wiring çağrılmadan store aboneliği kurulmaz', () => { + const store = createFakeStore({ speed: 50 }); + expect(store.listenerCount).toBe(0); + }); + + it('2) wiring oluşturulur ve cleanup thunk döner', () => { + const store = createFakeStore({ speed: 50 }); + const cleanup = start({ store, hal: createFakeHal() }); + expect(typeof cleanup).toBe('function'); + }); + + it('3) provider gerçek store API\'sine (getState + subscribe) bağlanır', () => { + const store = createFakeStore({ speed: 50 }); + const getState = vi.spyOn(store, 'getState'); + const subscribe = vi.spyOn(store, 'subscribe'); + start({ store, hal: createFakeHal() }); + expect(subscribe).toHaveBeenCalledTimes(1); + expect(getState).toHaveBeenCalled(); // adapter.start() → ilk refresh + }); + + it('4) wiring sonrası TEK subscription', () => { + const store = createFakeStore({ speed: 50 }); + start({ store, hal: createFakeHal() }); + expect(store.listenerCount).toBe(1); + }); + + it('5) speed HAL\'e akar', () => { + const store = createFakeStore({ speed: 50 }); + const hal = createFakeHal(); + start({ store, hal }); + expect(valuesOf(hal, 'vehicle.speed')).toContain(50); + }); + + it('6) rpm HAL\'e akar', () => { + const store = createFakeStore({ speed: 10, rpm: 2400 }); + const hal = createFakeHal(); + start({ store, hal }); + expect(valuesOf(hal, 'vehicle.rpm')).toContain(2400); + }); + + it('7) CAN extras (coolant/oil/throttle/voltaj/gear/ambient) HAL\'e akar', () => { + const store = createFakeStore({ + speed: 40, canCoolantTemp: 90, canOilTemp: 105, canThrottle: 32, + canBatteryVolt: 14.1, canGearPos: 3, canAmbientTemp: 21, + }); + const hal = createFakeHal(); + start({ store, hal }); + const ids = idsOf(hal); + for (const id of [ + 'vehicle.coolant_temp', 'vehicle.oil_temp', 'vehicle.throttle', + 'vehicle.battery_voltage', 'vehicle.gear', 'vehicle.ambient_temp', + ]) expect(ids).toContain(id); + }); + + it('8) store güncellemesi HAL\'e aktarılır', () => { + const store = createFakeStore({ speed: 50 }); + const hal = createFakeHal(); + start({ store, hal }); + store.set({ speed: 60 }); + expect(valuesOf(hal, 'vehicle.speed')).toEqual([50, 60]); + }); + + it('9) aynı değer DUPLICATE ingest üretmez', () => { + const store = createFakeStore({ speed: 50 }); + const hal = createFakeHal(); + start({ store, hal }); + store.set({ speed: 50 }); + store.set({ speed: 50 }); + expect(valuesOf(hal, 'vehicle.speed')).toEqual([50]); + }); + + it('10) değişen değer ingest EDİLİR (dedupe yalnız aynı değere)', () => { + const store = createFakeStore({ speed: 50 }); + const hal = createFakeHal(); + start({ store, hal }); + store.set({ speed: 60 }); + store.set({ speed: 60 }); + store.set({ speed: 70 }); + expect(valuesOf(hal, 'vehicle.speed')).toEqual([50, 60, 70]); + }); +}); + +/* ── Duplicate / tek-instance / lifecycle ─────────────────────────────────── */ + +describe('PR-W2 wiring — duplicate & lifecycle', () => { + it('11) start İDEMPOTENT: aktifken ikinci wiring İKİNCİ subscription açmaz', () => { + const store = createFakeStore({ speed: 50 }); + start({ store, hal: createFakeHal() }); + start({ store, hal: createFakeHal() }); // duplicate çağrı + expect(store.listenerCount).toBe(1); // TEK abonelik + }); + + it('12) duplicate wiring İKİNCİ adapter\'ı beslemez (yalnız ilk zincir canlı)', () => { + const store = createFakeStore({ speed: 50 }); + const hal1 = createFakeHal(); + const hal2 = createFakeHal(); + start({ store, hal: hal1 }); + start({ store, hal: hal2 }); + store.set({ speed: 60 }); + expect(valuesOf(hal1, 'vehicle.speed')).toEqual([50, 60]); + expect(hal2.ingests.length).toBe(0); // ikinci HAL hiç beslenmez + }); + + it('13) cleanup adapter+provider\'ı durdurur: sonraki veri HAL\'e gitmez', () => { + const store = createFakeStore({ speed: 50 }); + const hal = createFakeHal(); + const cleanup = start({ store, hal }); + cleanup(); + store.set({ speed: 999 }); + expect(valuesOf(hal, 'vehicle.speed')).toEqual([50]); + expect(store.listenerCount).toBe(0); // abonelik bırakıldı (zero-leak) + }); + + it('14) cleanup İDEMPOTENT (ikinci çağrı no-op, çökmesiz)', () => { + const store = createFakeStore({ speed: 50 }); + const cleanup = start({ store, hal: createFakeHal() }); + cleanup(); + expect(() => cleanup()).not.toThrow(); + expect(store.listenerCount).toBe(0); + }); + + it('15) boot → shutdown → boot güvenli, subscription ÇOĞALTMAZ', () => { + const store = createFakeStore({ speed: 50 }); + const hal = createFakeHal(); + const c1 = start({ store, hal }); + c1(); + const c2 = start({ store, hal }); + expect(store.listenerCount).toBe(1); + store.set({ speed: 80 }); + expect(valuesOf(hal, 'vehicle.speed')).toContain(80); // ikinci boot canlı + c2(); + expect(store.listenerCount).toBe(0); + }); + + it('16) dispose sonrası store callback\'i no-op (HAL beslenmez)', () => { + const store = createFakeStore({ speed: 50 }); + const hal = createFakeHal(); + const cleanup = start({ store, hal }); + const before = hal.ingests.length; + cleanup(); + store.set({ speed: 51 }); + store.set({ canCoolantTemp: 95 }); + expect(hal.ingests.length).toBe(before); + }); + + it('17) cleanup HAL\'i DISPOSE ETMEZ (paylaşılan singleton — sahiplik provider/adapter\'da)', () => { + const dispose = vi.fn(); + const store = createFakeStore({ speed: 50 }); + const halWithDispose = { ...createFakeHal(), dispose } as unknown as VehicleHalIngestTarget; + const cleanup = start({ store, hal: halWithDispose }); + cleanup(); + expect(dispose).not.toHaveBeenCalled(); + }); + + it('18) eski wiring temizlenince yeni wiring bloke OLMAZ (HMR/restart)', () => { + const s1 = createFakeStore({ speed: 10 }); + const c1 = start({ store: s1, hal: createFakeHal() }); + c1(); + const s2 = createFakeStore({ speed: 20 }); + const hal2 = createFakeHal(); + start({ store: s2, hal: hal2 }); + expect(s2.listenerCount).toBe(1); + expect(valuesOf(hal2, 'vehicle.speed')).toContain(20); + }); +}); + +/* ── Fail-soft ────────────────────────────────────────────────────────────── */ + +describe('PR-W2 wiring — fail-soft', () => { + it('19) store hazır değilse (null) wiring throw ETMEZ, cleanup thunk döner', () => { + let cleanup: (() => void) | null = null; + expect(() => { cleanup = start({ store: null, hal: createFakeHal() }); }).not.toThrow(); + expect(typeof cleanup).toBe('function'); + expect(() => cleanup!()).not.toThrow(); + }); + + it('20) store null iken wiring KİLİTLENMEZ — sonraki gerçek wiring çalışır', () => { + start({ store: null, hal: createFakeHal() }); + const store = createFakeStore({ speed: 33 }); + const hal = createFakeHal(); + start({ store, hal }); + expect(store.listenerCount).toBe(1); + expect(valuesOf(hal, 'vehicle.speed')).toContain(33); + }); + + it('21) getState throw eden bozuk store\'da wiring throw ETMEZ (provider hatası izole)', () => { + const badStore = { + getState: () => { throw new Error('boom'); }, + subscribe: (_l: (s: UnifiedVehicleStateReadable) => void) => () => { /* */ }, + } as unknown as UnifiedVehicleStoreLike; + const hal = createFakeHal(); + expect(() => { start({ store: badStore, hal }); }).not.toThrow(); + expect(hal.ingests.length).toBe(0); // veri okunamadı ama çökmedi + }); + + it('22) subscribe throw eden store\'da wiring throw ETMEZ (adapter start hatası izole)', () => { + const badStore = { + getState: () => ({ speed: 42 }), + subscribe: () => { throw new Error('subscribe fail'); }, + } as unknown as UnifiedVehicleStoreLike; + const hal = createFakeHal(); + expect(() => { start({ store: badStore, hal }); }).not.toThrow(); + expect(valuesOf(hal, 'vehicle.speed')).toContain(42); // ilk refresh yine de aktı + }); + + it('23) HAL ingest hatası diğer sinyalleri engellemez (tek sinyal bozuksa diğerleri sürer)', () => { + let calls = 0; + const flakyHal: VehicleHalIngestTarget = { + ingestSignal(id) { calls++; if (String(id) === 'vehicle.speed') throw new Error('hal fail'); return null; }, + }; + const store = createFakeStore({ speed: 50, canCoolantTemp: 90 }); + expect(() => { start({ store, hal: flakyHal }); }).not.toThrow(); + expect(calls).toBeGreaterThan(1); + }); + + it('24) cleanup hatası shutdown\'ı ENGELLEMEZ (throw etmez)', () => { + const store = createFakeStore({ speed: 50 }); + const unsubThrows = { + getState: () => ({ speed: 50 }), + subscribe: () => () => { throw new Error('unsub fail'); }, + } as unknown as UnifiedVehicleStoreLike; + void store; + const cleanup = start({ store: unsubThrows, hal: createFakeHal() }); + expect(() => cleanup()).not.toThrow(); + }); + + it('25) public wiring API throw ETMEZ (deps bozuk olsa da)', () => { + expect(() => { + const c = startPlatformCoreVehicleHalWiring({ store: undefined }); + c(); + }).not.toThrow(); + expect(() => getVehicleHalWiringStatus()).not.toThrow(); + }); +}); + +/* ── Sinyal davranışı ──────────────────────────────────────────────────────── */ + +describe('PR-W2 wiring — sinyal davranışı', () => { + it('26) ignition kaynağı YOK → HAL\'e HİÇ ingest edilmez (supported=false kalır)', () => { + const store = createFakeStore({ speed: 50, rpm: 2000, canCoolantTemp: 90 }); + const hal = createFakeHal(); + start({ store, hal }); + expect(idsOf(hal)).not.toContain('vehicle.ignition'); + }); + + it('27) TPMS yalnız gerçek 4-tuple veri varsa akar', () => { + const s1 = createFakeStore({ speed: 50 }); + const h1 = createFakeHal(); + const c1 = start({ store: s1, hal: h1 }); + expect(idsOf(h1)).not.toContain('vehicle.tpms'); + c1(); + + const s2 = createFakeStore({ speed: 50, canTpmsKpa: [220, 225, 230, 228] }); + const h2 = createFakeHal(); + start({ store: s2, hal: h2 }); + expect(idsOf(h2)).toContain('vehicle.tpms'); + }); + + it('28) reverse yalnız aktif araç verisi varken akar (park default\'u kaynak sanmaz)', () => { + const s1 = createFakeStore({ reverse: false }); // hiçbir numerik sinyal yok + const h1 = createFakeHal(); + const c1 = start({ store: s1, hal: h1 }); + expect(idsOf(h1)).not.toContain('vehicle.reverse'); + c1(); + + const s2 = createFakeStore({ speed: 0, rpm: 800, reverse: true }); + const h2 = createFakeHal(); + start({ store: s2, hal: h2 }); + expect(valuesOf(h2, 'vehicle.reverse')).toContain(true); + }); + + it('29) gizlilik: GPS konum/heading gibi alanlar HAL\'e SIZMAZ (yalnız katalog sinyalleri)', () => { + const ALLOWED = new Set([ + 'vehicle.speed', 'vehicle.rpm', 'vehicle.fuel_level', 'vehicle.odometer', 'vehicle.reverse', + 'vehicle.coolant_temp', 'vehicle.oil_temp', 'vehicle.throttle', 'vehicle.battery_voltage', + 'vehicle.gear', 'vehicle.ambient_temp', 'vehicle.tpms', 'vehicle.door_state', 'vehicle.parking_brake', + ]); + const store = createFakeStore({ + speed: 50, rpm: 2000, + // Store'da GERÇEKTEN bulunan ama HAL kapsamı DIŞINDA olan alanlar: + ...({ location: { lat: 41.0, lon: 29.0 }, heading: 180, gpsSource: 'native' } as object), + } as UnifiedVehicleStateReadable); + const hal = createFakeHal(); + start({ store, hal }); + for (const id of idsOf(hal)) expect(ALLOWED.has(id)).toBe(true); + const payload = JSON.stringify(hal.ingests); + expect(payload).not.toMatch(/41\.0|29\.0|native/); // koordinat/kaynak sızmadı + }); + + it('30) store state MUTATE EDİLMEZ (dondurulmuş state ile de çalışır)', () => { + const frozen = Object.freeze({ speed: 55, canCoolantTemp: 88 }) as UnifiedVehicleStateReadable; + const store: UnifiedVehicleStoreLike = { getState: () => frozen, subscribe: () => () => { /* */ } }; + const hal = createFakeHal(); + expect(() => { start({ store, hal }); }).not.toThrow(); + expect(frozen).toEqual({ speed: 55, canCoolantTemp: 88 }); + expect(valuesOf(hal, 'vehicle.speed')).toContain(55); + }); +}); + +/* ── Gözlemlenebilirlik (bounded) ─────────────────────────────────────────── */ + +describe('PR-W2 wiring — bounded status', () => { + it('31) wiring kapalıyken status started=false / subscription=0', () => { + const s = getVehicleHalWiringStatus(); + expect(s.started).toBe(false); + expect(s.activeSubscriptionCount).toBe(0); + }); + + it('32) aktif wiring status: started=true, TEK subscription, ingest sayacı artar', () => { + const store = createFakeStore({ speed: 50, canCoolantTemp: 90 }); + start({ store, hal: createFakeHal() }); + const s = getVehicleHalWiringStatus(); + expect(s.started).toBe(true); + expect(s.activeSubscriptionCount).toBe(1); + expect(s.ingestedSignalCount).toBeGreaterThanOrEqual(2); + expect(s.refreshCount).toBeGreaterThanOrEqual(1); + expect(Object.isFrozen(s)).toBe(true); // immutable görünüm + }); + + it('33) cleanup sonrası status started=false\'a döner', () => { + const store = createFakeStore({ speed: 50 }); + const cleanup = start({ store, hal: createFakeHal() }); + cleanup(); + expect(getVehicleHalWiringStatus().started).toBe(false); + }); +}); + +/* ── Kapsam sınırı (kaynak-kilidi — kırılgan regex yalnız burada) ──────────── */ + +describe('PR-W2 wiring — kapsam sınırı', () => { + it('34) Event Bus / Capability / Deep Scan import EDİLMEZ', () => { + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*eventBus/); + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*capability/i); + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*deepScan/i); + }); + + it('35) Platform Kernel import EDİLMEZ (legacy servisler Kernel\'e TAŞINMAZ)', () => { + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*kernel/i); + expect(SYSTEMBOOT_SRC).not.toMatch(/from\s+['"][^'"]*kernel/i); + }); + + it('36) native / OBD / CAN katmanı import EDİLMEZ (poll frekansı değişmez)', () => { + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*(obdService|canbus|nativePlugin|bridge)['"]/i); + }); + + it('37) yeni timer/polling/rAF AÇILMAZ (BASIC_JS/Mali-400 sürekli yük yok)', () => { + expect(WIRING_SRC).not.toMatch(/setInterval|setTimeout|requestAnimationFrame/); + }); + + it('38) hot-path\'te ağır serileştirme YOK (JSON.stringify / structuredClone / deep clone)', () => { + expect(WIRING_SRC).not.toMatch(/JSON\.stringify|structuredClone/); + }); + + it('39) İKİNCİ HAL instance ÜRETİLMEZ (paylaşılan `vehicleHal` singleton kullanılır)', () => { + expect(WIRING_SRC).not.toMatch(/createVehicleHal\s*\(/); + expect(WIRING_SRC).toMatch(/vehicleHal/); + }); +}); + +/* ── SystemBoot entegrasyon sıra kilidi ────────────────────────────────────── */ + +describe('PR-W2 — SystemBoot startup sırası korunur', () => { + it('40) wiring VehicleDataLayer\'dan SONRA, SystemOrchestrator\'dan ÖNCE kaydedilir', () => { + const iVDL = SYSTEMBOOT_SRC.indexOf('startVehicleDataLayer('); + const iWiring = SYSTEMBOOT_SRC.indexOf('startPlatformCoreVehicleHalWiring('); + const iOrch = SYSTEMBOOT_SRC.indexOf('startSystemOrchestrator('); + expect(iVDL).toBeGreaterThan(0); + expect(iWiring).toBeGreaterThan(iVDL); + expect(iOrch).toBeGreaterThan(iWiring); + }); + + it('41) wiring çağrısı savunmacı try/catch ile sarılır (boot fail-soft)', () => { + const iWiring = SYSTEMBOOT_SRC.indexOf('startPlatformCoreVehicleHalWiring('); + const iOrch = SYSTEMBOOT_SRC.indexOf('startSystemOrchestrator('); + const between = SYSTEMBOOT_SRC.slice(iWiring, iOrch); + expect(between).toMatch(/logError\(['"]SystemBoot:vehicleHalWiring/); + }); + + it('42) wiring `_reg` cleanup modeliyle kaydedilir (LIFO shutdown)', () => { + expect(SYSTEMBOOT_SRC).toMatch(/this\._reg\(startPlatformCoreVehicleHalWiring\(/); + }); + + it('43) 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(i1).toBeGreaterThan(0); + expect(i2).toBeGreaterThan(i1); + expect(i3).toBeGreaterThan(i2); + expect(i4).toBeGreaterThan(i3); + }); +}); diff --git a/src/platform/system/SystemBoot.ts b/src/platform/system/SystemBoot.ts index 3ec6f6ba..79e40310 100644 --- a/src/platform/system/SystemBoot.ts +++ b/src/platform/system/SystemBoot.ts @@ -35,6 +35,7 @@ import { restoreOdometer, } from '../vehicleDataLayer'; import { startSystemOrchestrator } from './SystemOrchestrator'; +import { startPlatformCoreVehicleHalWiring } from './platformCoreVehicleHalWiring'; import { startMaintenanceBrain } from '../diagnostic/maintenanceBrain'; import { startFuelAdvisor } from '../diagnostic/fuelAdvisorService'; import { startBlackBox } from '../security/blackBoxService'; @@ -499,6 +500,18 @@ class SystemBoot { alertMsg: 'Konum verisi alınamıyor — tünel veya sinyal kesintisi.', }); + // Platform Core: Vehicle HAL runtime wiring (PR-W2) — store→provider→adapter→HAL AYNA modu. + // Additive; Wave sırası bozulmaz. VehicleDataLayer'dan SONRA kaydedilir → LIFO shutdown'da + // VDL'den ÖNCE kapanır (store hâlâ ayaktayken adapter dispose olur). Wiring fonksiyonu init + // hatasını kendi içinde yutar (fail-soft); buradaki savunmacı catch YALNIZ sözleşme ihlali + // (dışarı exception) için — çift-log YOK. HAL beslenir ama okuyan yok (tüketici migrasyonu W7). + _log(' › Vehicle HAL wiring (Platform Core)'); + try { + this._reg(startPlatformCoreVehicleHalWiring({ store: useVehicleStore })); + } catch (e) { + logError('SystemBoot:vehicleHalWiring', e); + } + // SystemOrchestrator: VDL event'lerini UI sinyallerine dönüştürür _log(' › SystemOrchestrator'); this._reg(startSystemOrchestrator()); diff --git a/src/platform/system/platformCoreVehicleHalWiring.ts b/src/platform/system/platformCoreVehicleHalWiring.ts new file mode 100644 index 00000000..aeb80768 --- /dev/null +++ b/src/platform/system/platformCoreVehicleHalWiring.ts @@ -0,0 +1,162 @@ +/** + * platformCoreVehicleHalWiring — Vehicle HAL RUNTIME WIRING (PR-W2). + * + * AMAÇ: Mevcut `useUnifiedVehicleStore` (fused worker/CAN/OBD/GPS) veri akışını, daha önce + * hazırlanmış SALT-OKUNUR provider + adapter foundation'ı üzerinden gerçek `vehicleHal` + * singleton'ına — AYNA MODUNDA — bağlar. Zincir: + * + * useUnifiedVehicleStore + * → createUnifiedVehicleStoreProvider (store → VehicleStoreSource) + * → createVehicleHalProviderAdapter (source → HAL ingestSignal, yalnız değişen sinyal) + * → vehicleHal (singleton; kimse okumaz — tüketici migrasyonu AYRI PR) + * + * NE YAPMAZ (bilinçli — PR-W2 yalnız HAL veri girişini aktive eder): + * - Event Bus'a YAYINLAMAZ · Capability Registry'yi GÜNCELLEMEZ · Platform Kernel'e servis + * KAYDETMEZ · Deep Scan'i BAĞLAMAZ · tüketicileri HAL'e TAŞIMAZ · UI/SQL/native/OBD/CAN'e + * DOKUNMAZ · poll frekansı DEĞİŞTİRMEZ · yeni timer/polling AÇMAZ · sinyal eşlemesini + * (provider/adapter) DEĞİŞTİRMEZ. + * - Store'u DOĞRUDAN import ETMEZ (`store` DI ile gelir); `hal` opsiyonel DI (test), üretimde + * paylaşılan `vehicleHal` singleton'ı. Modül import'u YAN ETKİSİZDİR — `start...()` çağrılana + * dek store'a abone olunmaz, HAL beslenmez. + * + * TEK INSTANCE (duplicate koruması): modül-düzeyi `_active` kaydı → wiring zaten AKTİFKEN ikinci + * `start...()` çağrısı YENİ provider/adapter/subscription OLUŞTURMAZ (no-op cleanup döner). + * HMR/restart'ta bayat kayıt (adapter disposed) otomatik temizlenir. Cleanup yalnız KENDİ + * kaydını siler (yabancı wiring'i düşürmez) → boot→shutdown→boot güvenli. + * + * SAHİPLİK: provider + adapter bu wiring'e aittir (cleanup'ta dispose edilir). `vehicleHal` + * PAYLAŞILAN modül singleton'ıdır → cleanup HAL'i DISPOSE ETMEZ (çift-dispose yok). + * + * FAIL-SOFT: başlatma fonksiyonu dışarı EXCEPTION KAÇIRMAZ — init hatası bir kez `logError` ile + * kaydedilir ve güvenli no-op cleanup döner (ham sinyal/VIN/CAN payload'ı LOGLANMAZ). + * ZERO-LEAK: cleanup adapter → provider sırasıyla dispose eder; İDEMPOTENT (ikinci çağrı no-op). + */ + +import { logError } from '../crashLogger'; +import { + createVehicleHalProviderAdapter, + vehicleHal, + type VehicleHalIngestTarget, + type VehicleHalProviderAdapter, +} from '../vehicleHal'; +import { + createUnifiedVehicleStoreProvider, + type UnifiedVehicleStoreLike, + type UnifiedVehicleStoreProvider, +} from '../vehicleHal/providers'; + +/** Wiring bağımlılıkları — store DI zorunlu; hal opsiyonel (test), varsayılan singleton. */ +export interface VehicleHalWiringDeps { + /** `useUnifiedVehicleStore` (zustand — getState/subscribe). Yapısal DI; doğrudan import yok. */ + readonly store: UnifiedVehicleStoreLike | null | undefined; + /** Test için HAL enjeksiyonu; verilmezse üretim `vehicleHal` singleton'ı kullanılır. */ + readonly hal?: VehicleHalIngestTarget; +} + +/** Tek cleanup thunk — İDEMPOTENT + fail-soft. HAL'i dispose ETMEZ. */ +export type VehicleHalWiringCleanup = () => void; + +/** Bounded teşhis görünümü — ham telemetri YOK (yalnız sayaç/bayrak). */ +export interface VehicleHalWiringStatus { + readonly started: boolean; + readonly lastRefreshAt: number | null; + /** HAL'e en az bir kez aktarılmış AYRI sinyal sayısı (adapter'ın son-değer haritası). */ + readonly ingestedSignalCount: number; + /** Store değişikliğiyle tetiklenen refresh sayısı (ingest sayısı DEĞİL). */ + readonly refreshCount: number; + /** Aktif store aboneliği sayısı — sağlıklı çalışmada 1 (wiring kapalıyken 0). */ + readonly activeSubscriptionCount: number; + /** Sabit, sanitize hata kodu (ham hata/telemetri değil). */ + readonly lastErrorCode: 'init_failed' | 'cleanup_failed' | null; +} + +const NOOP_CLEANUP: VehicleHalWiringCleanup = () => { /* no-op */ }; + +interface ActiveWiring { + readonly adapter: VehicleHalProviderAdapter; + readonly provider: UnifiedVehicleStoreProvider; +} + +/** Tek runtime wiring kaydı (duplicate/HMR koruması). */ +let _active: ActiveWiring | null = null; +let _lastErrorCode: VehicleHalWiringStatus['lastErrorCode'] = null; + +const IDLE_STATUS: VehicleHalWiringStatus = Object.freeze({ + started: false, + lastRefreshAt: null, + ingestedSignalCount: 0, + refreshCount: 0, + activeSubscriptionCount: 0, + lastErrorCode: null, +}); + +/** + * HAL veri girişini kurar ve adapter'ı başlatır. YALNIZ cleanup thunk döner. + * Dışarı exception KAÇIRMAZ (init hatası → tek `logError` + no-op cleanup). + * İDEMPOTENT: aktif wiring varken ikinci çağrı YENİ abonelik açmaz. + */ +export function startPlatformCoreVehicleHalWiring(deps: VehicleHalWiringDeps): VehicleHalWiringCleanup { + let active: ActiveWiring | null = null; + try { + // HMR/restart artığı: cleanup'sız kalan bayat kayıt → serbest bırak. + if (_active && _active.adapter.isDisposed) _active = null; + // Zaten aktif → ikinci provider/adapter/subscription OLUŞTURULMAZ. + if (_active) return NOOP_CLEANUP; + + const store = deps && deps.store ? deps.store : null; + const hal: VehicleHalIngestTarget = deps && deps.hal ? deps.hal : vehicleHal; + + const provider = createUnifiedVehicleStoreProvider({ store }); + const adapter = createVehicleHalProviderAdapter({ hal, source: provider }); + + // Store yoksa wiring inert kalır (abonelik açılmaz) → aktif kayıt TUTULMAZ ki + // store hazır olduğunda sonraki wiring bloke olmasın. + if (store) { + active = { adapter, provider }; + _active = active; + } + adapter.start(); // adapter içi fail-soft (subscribe hatası yutulur) + _lastErrorCode = null; + + let disposed = false; + return () => { + if (disposed) return; // İDEMPOTENT + disposed = true; + try { + adapter.dispose(); // önce adapter (store hâlâ ayaktayken aboneliği bırakır) + provider.dispose(); + } catch (e) { + _lastErrorCode = 'cleanup_failed'; // cleanup hatası shutdown'ı ENGELLEMEZ + logError('vehicleHalWiring:cleanup', e); + } + if (active && _active === active) _active = null; // yalnız KENDİ kaydını siler + }; + } catch (e) { + // Init hatası: wiring sınırında BİR KEZ kaydet (ham veri/VIN/payload YOK), boot'u çökertme. + if (active && _active === active) _active = null; // yarım kayıt bırakma + _lastErrorCode = 'init_failed'; + logError('vehicleHalWiring:init', e); + return NOOP_CLEANUP; + } +} + +/** Bounded teşhis görünümü (prod log spam YOK). Throw ETMEZ. */ +export function getVehicleHalWiringStatus(): VehicleHalWiringStatus { + const a = _active; + if (!a || a.adapter.isDisposed) { + return _lastErrorCode ? Object.freeze({ ...IDLE_STATUS, lastErrorCode: _lastErrorCode }) : IDLE_STATUS; + } + try { + const s = a.adapter.getStatus(); + return Object.freeze({ + started: s.started, + lastRefreshAt: s.lastRefreshAt, + ingestedSignalCount: s.ingestedSignalCount, + refreshCount: s.refreshCount, + activeSubscriptionCount: a.provider.activeSubscriptionCount, + lastErrorCode: _lastErrorCode, + }); + } catch { + return IDLE_STATUS; // teşhis yolu asla çökmez + } +}