From 2f46aae573f6791bf1337529cdc5f275069670eb Mon Sep 17 00:00:00 2001 From: Selim Date: Mon, 13 Jul 2026 13:14:45 +0300 Subject: [PATCH] =?UTF-8?q?feat(deep-scan):=20W5-3c-2=20=E2=80=94=20Change?= =?UTF-8?q?=20Baseline=20Adapter=20(salt-okunur,=20DI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Offline change-detection handler'a (W5-3c-1) GERÇEK baseline sağlayan salt-okunur adapter: createChangeBaselineAdapter({persistence?, knowledgeBase?}) — factory + DI. İki mevcut pasif kaynağı ChangeBaseline'a (yalnız normalize ECU kümesi) çevirir: - PRIOR = deepScanPersistence.load(hash).discoveredEcus (son tam taramanın çıktısı) - CURRENT = vehicleKnowledgeBaseStore.get(hash).discoveredEcus (AutoLearning pasif ECU seti) Aktif sorgu YOK: iki kaynak da salt-okunur; hiçbir save/completeScan/active-record/ discovery/ECU-PID-DID isteği/CAN frame/OBD isteği yok. - Normalize + bounded + immutable çıktı {ecus} (trim+upper+0x-at+dedup, frozen). - Privacy: yalnız normalize ECU; VIN/firmware/PID/DID/supportedModes TAŞIMAZ. - DI + factory korunur: handler adapter'ı/persistence'ı/knowledge base'i DOĞRUDAN import ETMEZ (saf kalır); adapter çıktısı OfflineChangeDetectionDeps olarak handler'a bağlanabilir (W5-3c-3 wiring ayrı PR). - Firmware eklenmedi → handler changedFirmware=false üretmeye devam eder. - Fail-soft: kaynak throw / kayıt yok / hash geçersiz → null. - Barrel korundu: adapter deepScan index.ts'ten İHRAÇ EDİLMEZ (barrel'ın "import yan etkisiz + self-contained" invaryantı korunur). Handler DEĞİŞMEDİ; trigger/wiring/runtime/persistence-yazımı/Event Bus/Capability/ Assistant/HAL/CAN/OBD/SQL/Native'e dokunulmadı. Doğrulama: 23 yeni test; tam suite 3864 yeşil (212 dosya); tsc -b --noEmit temiz; lint 0 error; production build OK. Ledger: W5-3c-2 🔴 (yalnız adapter · handler/trigger/wiring değişmedi · aktif discovery yok · ECU/PID/DID sorgusu yok · Mali-400 doğrulaması bekliyor). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014VRXRfa6ZD3qJ74ozfzugm --- docs/DEVICE_VALIDATION_LEDGER.md | 1 + .../deepScanChangeBaselineAdapter.test.ts | 258 ++++++++++++++++++ .../deepScan/changeBaselineAdapter.ts | 100 +++++++ 3 files changed, 359 insertions(+) create mode 100644 src/__tests__/deepScanChangeBaselineAdapter.test.ts create mode 100644 src/platform/deepScan/changeBaselineAdapter.ts diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index cafe636..7c0e7a9 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 | |---|---------|--------|------------------------------------------|---------| +| 66 | **🔴 Device Validation Pending — Change Baseline Adapter (W5-3c-2)** — Offline change-detection handler'a (W5-3c-1) GERÇEK baseline sağlayan SALT-OKUNUR adapter: `createChangeBaselineAdapter({persistence?, knowledgeBase?})` (factory + DI). İki mevcut, pasif kaynağı `ChangeBaseline`'a (yalnız normalize ECU kümesi) çevirir: PRIOR = `deepScanPersistence.load(hash).discoveredEcus` (son TAM taramanın diske yazılı çıktısı); CURRENT = `vehicleKnowledgeBaseStore.get(hash).discoveredEcus` (AutoLearning'in PASİF öğrendiği ECU seti — read-only store; facade `vehicleKnowledgeBase` DEĞİL, o start/stop'lu servis). **AKTİF SORGU YOK:** iki kaynak da salt-okunur; hiçbir save/completeScan/active-record/discovery/ECU-PID-DID isteği/CAN frame/OBD isteği YOK. **NORMALİZE + BOUNDED + IMMUTABLE:** ECU'lar trim+upper+`0x`-at+dedup; çıktı dondurulmuş `{ecus}`. **GİZLİLİK:** yalnız normalize ECU kimlikleri — VIN, firmware sürümü, PID/DID sinyalleri, supportedModes, koordinat, secret TAŞIMAZ (kayıtta bu alanlar OLSA BİLE baseline'a girmez). **DI + FACTORY KORUNUR:** handler bu adapter'ı / persistence'ı / knowledge base'i DOĞRUDAN import ETMEZ (saf kalır; bağımlılık yönü adapter→kaynaklar); adapter çıktısı `OfflineChangeDetectionDeps` şeklinde doğrudan handler'a bağlanabilir (W5-3c-3 wiring ayrı PR). **FIRMWARE EKLENMEDİ:** handler `changedFirmware=false` üretmeye DEVAM eder. **FAIL-SOFT:** kaynak throw / kayıt yok / hash geçersiz → `null` (değişim iddiası handler tarafında fail-closed). **BARREL KORUNDU:** adapter deepScan barrel'ından (`index.ts`) İHRAÇ EDİLMEZ → barrel'ın "import yan etkisiz + self-contained" invaryantı korunur (adapter vehicleKnowledgeBase graph'ına dokunur; barrel'a sokulmaz). Trigger/wiring/runtime/persistence-yazımı/Event Bus/Capability/Assistant/Driver DNA/Prediction/Diagnostic section/SQL/Native/HAL/CAN/OBD DEĞİŞMEDİ. **HOST-DOĞRULANDI:** 23 yeni test (kaynak kombinasyonları [boş/yalnız-biri/ikisi], normalize, duplicate, null-güvenli, geçersiz hash, immutable, kaynak mutate edilmiyor, privacy, fail-soft, adapter→handler entegrasyonu [changedEcu doğru + handler yalnız {ecus} görür], cancel, kaynak kilitleri); tam suite **3864 yeşil (212 dosya)**, tsc `-b --noEmit` temiz, lint 0 error, production build OK. **AÇIKÇA:** (a) bu PR yalnız adapter ekler; (b) handler değiştirilmedi; (c) trigger değiştirilmedi; (d) wiring değiştirilmedi; (e) aktif discovery yapılmıyor; (f) hiçbir ECU/PID/DID sorgusu gönderilmiyor; (g) adapter+handler henüz pass'e/production'a BAĞLI DEĞİL (W5-3c-3 ayrı PR). | `platform/deepScan/changeBaselineAdapter.ts` (yeni — `createChangeBaselineAdapter` + `ChangeBaselineAdapterDeps`/`ChangeBaselineAdapter`), `__tests__/deepScanChangeBaselineAdapter.test.ts` (yeni, 23 test) | Gerçek cihazda (Xiaomi 23090RA98I + en az bir low-tier K24/Mali-400/T507) — adapter+handler pass'e bağlandıktan SONRA (W5-3c-3): (1) gerçek araçta persistence (son tam tarama) ECU seti ile knowledge base (pasif öğrenilmiş) ECU seti farklı iken `changedEcu=true`, aynı iken `false`; (2) bu adapter/handler kaynaklı OBD/CAN trafiğinde ECU/PID/DID/Firmware isteği YOK (yalnız disk+bellek okur); (3) persistence/counter/`hasCompletedFullScan` değişmedi (adapter salt-okunur); (4) baseline'da VIN/firmware/PID sızıntısı YOK (yalnız ECU); (5) low-tier'da ek CPU/RAM yükü ölçüm gürültüsü seviyesinde (saf normalize + set okuma, O(n)). Cihaz kanıtı olmadan 🟢 YAPMA. ⚠️ **Mali-400 cihaz doğrulaması bekliyor.** | 2026-07-13 | | 65 | **🔴 Device Validation Pending — Safe Offline Change Detection Handler (W5-3c-1)** — Deep Scan offline pass için İLK gerçek değer üreten handler eklendi: `createOfflineChangeDetectionHandler()` (fabrika; bağımlılıklar closure ile ENJEKTE). Handler ÖNCEDEN-VAROLAN iki baseline'ın ECU kümesi FARKINI hesaplar → `changedEcu`. PRIOR baseline = son TAM taramanın diske yazılı çıktısı (`deepScanPersistence.load` ile beslenecek — AYRI PR); CURRENT baseline = pasif öğrenilmiş gözlem (`vehicleKnowledgeBase.get` ile beslenecek — AYRI PR). **AKTİF SORGU YOK:** iki küme de zaten elde olan veri; araca tek CAN frame / OBD isteği / ECU-PID-DID sorgusu GÖNDERİLMEZ. **SAF + İZOLE:** handler `deepScanPersistence`/`vehicleKnowledgeBase`/runtime/native/HAL/EventBus'i DOĞRUDAN import ETMEZ (yalnız `PhaseContext`/`PhaseResult` tip import'u); `PhaseHandler` sözleşmesi ve `PhaseContext` DEĞİŞMEZ. **FAIL-CLOSED:** parmak izi yok / baseline eksik (null) / baseline BOŞ → değişim İDDİA EDİLMEZ (`changedEcu=false`, kategori reason). Persistence ECU listesi opsiyonel → boş baseline OLAĞAN; bunu "değişti" saymak sahte pozitif olurdu, o yüzden `baseline_incomplete` döner. **FIRMWARE DEĞERLENDİRİLMEZ:** `changedFirmware` DAİMA `false` (dürüst — pasif firmware kaynağı yok; ayrı PR). **BOUNDED + IMMUTABLE ÇIKTI:** yalnız `{status, changedEcu, changedFirmware, reason(kategori)}` — ham ECU/PID/DID listesi, firmware sürümü, VIN, GPS, secret, parmak izi hash'i TAŞIMAZ. **KORUMA BANDI (uçtan uca kanıtlı):** `completeScan`/`_finalize`/`_applyResult` aktif-kayıt yolu / runtime aktif-kayıt API'leri (`recordEcuDiscovery` vb.) / `saveSnapshot` ÇAĞRILMAZ → `hasCompletedFullScan`/`completedScanCount`/`changeCheckCount` DEĞİŞMEZ; `runtime.reset()` davranışı korunur (gerçek `runOfflinePass` ile koşturulan uçtan uca testte doğrulandı). Trigger/wiring DEĞİŞMEDİ; Event Bus/Capability/Assistant/Driver DNA/Prediction/Diagnostic section/SQL/Native/OBD/CAN/HAL'e DOKUNULMADI. **HOST-DOĞRULANDI:** 21 yeni test (changedEcu diff/normalizasyon, boş+null baseline fail-closed, firmware daima false, immutable+privacy, input immutable, fail-soft, cancel, kaynak-kilidi, uçtan uca guard band); tam suite **3841 yeşil (211 dosya)**, tsc `-b --noEmit` temiz, lint 0 error, production build OK. **AÇIKÇA:** (a) bu PR yalnız handler ekler; (b) trigger değiştirilmedi; (c) wiring değiştirilmedi; (d) aktif discovery yapılmıyor; (e) hiçbir ECU/PID/DID sorgusu gönderilmiyor; (f) handler henüz pass'e BAĞLI DEĞİL (W5-3c-2 adapter + W5-3c-3 bağlama ayrı PR). | `platform/deepScan/offlineChangeDetectionHandler.ts` (yeni — saf handler + `ChangeBaseline`/`ChangeDetectionReason`/`OfflineChangeDetectionDeps`), `platform/deepScan/index.ts` (barrel export), `__tests__/deepScanOfflineChangeDetection.test.ts` (yeni, 21 test) | Gerçek cihazda (Xiaomi 23090RA98I + en az bir low-tier K24/Mali-400/T507) — handler pass'e bağlandıktan SONRA (W5-3c-3): (1) gerçek araçta prior (son tam tarama) + current (pasif öğrenilmiş) baseline'lar dolu iken ECU seti değiştiğinde `changedEcu=true`, değişmediğinde `false`; (2) araç OBD/CAN trafiğinde bu handler kaynaklı ECU/PID/DID/Firmware isteği YOK (aktif sorgu üretmiyor); (3) pass sonrası `hasCompletedFullScan`/counter'lar değişmedi, runtime `idle`; (4) boş/eksik baseline'da sahte `changedEcu=true` ÜRETİLMİYOR (fail-closed); (5) low-tier'da ek CPU/RAM yükü ölçüm gürültüsü seviyesinde (saf set-diff, O(n)). Cihaz kanıtı olmadan 🟢 YAPMA. ⚠️ **Mali-400 cihaz doğrulaması bekliyor.** | 2026-07-13 | | 64 | **🔴 Device Validation Pending — Deep Scan Offline Pass Trigger + Runtime Wiring + Minimal Diagnostics (W5-3b)** — W5-3a'daki `runOfflinePass()` İLK kez üretim wiring'ine bağlandı. `platformCoreDeepScanWiring`'e tek deterministik giriş noktası `triggerDeepScanOfflinePass()` (hash/dedup guard + single-flight) + iptal (`cancelDeepScanOfflinePass`) + bounded teşhis (`getDeepScanOfflinePassStatus`) eklendi; SystemBoot **Wave 3**'te (Wave 2 ownership KURULDUKTAN SONRA) fire-and-forget çağrılır (`void ... .catch`; boot BLOKLANMAZ). **GUARD:** aynı guard anahtarı (varsayılan `deepscan.offline.boot`) EN FAZLA bir kez pass başlatır (duplicate YOK); yürüyen pass varken ikinci tetik reddedilir (single-flight); wiring restart'ında guard doğal sıfırlanır (durum wiring örneğine bağlı). **DAVRANIŞ DEĞİŞMEZ:** handler GEÇİLMEZ → tüm offline fazlar `skipped` → GERÇEK İŞ YAPILMAZ; change detection / knowledge / fingerprint / evidence / report ÇALIŞMAZ; aktif ECU/PID/DID/Firmware sorgusu YOK (runtime aktif-kayıt API'leri hiç çağrılmaz — gerçek zincir testiyle kanıtlı); runtime `idle → running → idle` döner (pass sonunda `runtime.reset()` W5-3a garantisi). **OWNERSHIP KORUNUR:** `OwnedOrchestrator` yüzeyi offline `runOfflinePass`/`cancelOfflinePass`'i gösterir ama AKTİF tarama API'lerini (`start`/`run`/`runNextPhase`) ASLA göstermez (compile-time; W5-3a kilidi bu davranışa GÜNCELLENDİ, kaldırılmadı). **TEŞHİS (bounded + immutable):** present/started/running/active/cancelled/triggerCount/lastRun/lastDuration/lastResult/lastReason/lastError/phaseCount/summaryAvailable — hepsi primitive/enum/sayı; VIN/GPS/secret/ham CAN-OBD/guard-anahtarı TAŞIMAZ. Event Bus / Capability Registry / HAL / Assistant / Driver DNA / Prediction / SQL / Supabase / Native / CAN / OBD DEĞİŞMEDİ (wiring bunları import ETMEZ). **HOST-DOĞRULANDI:** yeni trigger-wiring testleri (25+ senaryo: trigger/duplicate/hash-guard/tek-pass/single-flight/idle→running→idle/reset/summary/immutable/bounded/dispose/restart/cancel/timeout-iletimi/handler-yok/aktif-discovery-yok/privacy/input-immutable/Wave-sırası/EventBus-Capability değişmedi); W5-1 wiring fake'i + W5-3a 2 kilit yeni davranışa güncellendi; tam suite **3820 yeşil (210 dosya)**, tsc `-b --noEmit` temiz, lint 0 error, production build OK. **AÇIKÇA:** (a) gerçek handler YOK; (b) gerçek discovery YOK; (c) **W5-3c değer üreten (offline handler) PR'dır**; (d) aktif ECU sorgusu YOK (tip + faz + aktif-kayıt kapısı + handler-yok ile garanti); (e) Mali-400 doğrulaması bekliyor. | `platform/system/platformCoreDeepScanWiring.ts` (OwnedOrchestrator offline yüzey + `triggerDeepScanOfflinePass`/`cancelDeepScanOfflinePass`/`getDeepScanOfflinePassStatus` + bounded status), `platform/system/SystemBoot.ts` (Wave 3 trigger, fire-and-forget), `__tests__/deepScanOfflineTriggerWiring.test.ts` (yeni), `__tests__/platformCoreDeepScanWiring.test.ts` (fake offline yüzey), `__tests__/deepScanOfflinePass.test.ts` (2 kapsam kilidi güncellendi) | Gerçek cihazda (Xiaomi 23090RA98I + en az bir low-tier K24/Mali-400/T507): (1) boot'ta trigger TEK kez çalışır (`getDeepScanOfflinePassStatus().triggerCount=1`, duplicate boot/HMR çoğaltmıyor); (2) `runOfflinePass` çağrıldı ama araç OBD/CAN trafiğinde deep-scan kaynaklı ECU/PID/DID/Firmware isteği YOK (handler yok → aktif sorgu üretilmiyor); (3) pass sonrası runtime `idle` (`summaryAvailable=true`, `lastResult='ran'`); gerçek tarama sonradan başlayabiliyor; (4) boot süresi regresyonu yok (fire-and-forget → boot bloklanmıyor; hedef boot delta <10ms); (5) **Mali-400/K24 steady-state ek yük ölçüm gürültüsü seviyesinde** (pass tek-atış; timer/polling yok → ≈0); (6) background/foreground / restart'ta çift-pass veya leak yok; (7) teşhis raporunda offline pass status okunabilir (ADB'siz) — ⚠️ Not: `getDeepScanOfflinePassStatus` henüz `diagnosticSections`'a EKLENMEDİ (ayrı küçük teşhis PR'ı gerekir). Cihaz kanıtı olmadan 🟢 YAPMA. ⚠️ **Mali-400 doğrulaması bekliyor.** | 2026-07-13 | | 63 | **🔴 Device Validation Pending — Tanı Raporu Sanitize Sertleştirme (Diagnostics PR-2)** — PR-1'in (#80) açtığı denetimde ortaya çıkan ÜÇ sanitize boşluğu kapatıldı. **(1) `selfTest` bölümü sanitize'ı TAMAMEN atlıyordu:** `reportSelfTestSnapshot` raporu ham spread ediyordu (`{...base, selfTest}`) → deny-list, VIN/MAC/koordinat/api_key maskeleri ve derinlik tavanı bu bölüme UYGULANMIYORDU. Prob `detail` alanları SERBEST METİN taşır: ham `Error.message` (`selfTestEngine` runProbe catch), ham fetch hata metni (probeBackend) ve stack karesi/dosya yolu (probeIdleRenderLoop) → **maskesiz kanal**. Artık inspector ile AYNI kapıdan geçer: yeni `_sanitizeSection()` (tek kapı — yeni bölüm eklenince sanitize'ı atlamak imkânsız). Teşhis değeri KAYBOLMAZ: stack karesi (`FullMapView.tsx:814`) okunur kalır, yalnız VIN/MAC/koordinat/anahtar maskelenir. **(2) `DENY_KEYS` camelCase'e KÖRDÜ:** eşleşme `key.toLowerCase()` idi, set'te `'api_key'` vardı → `apiKey`.toLowerCase() = `'apikey'` ≠ `'api_key'` → **SIZIYORDU**. Anahtar artık NORMALİZE edilir (küçült + alfanümerik olmayanı at) ve **TAM EŞLEŞME** yapılır. Liste genişledi: `apikey · accesstoken · refreshtoken · authorization · bearer · secret · password · jwt · token · email` (+ mevcut konum/VIN/MAC/telefon). **⚠️ TAM eşleşme ZORUNLU — substring/prefix YASAK:** `lat` substring olsaydı `platform` (W4E runtime teşhis bölümünün TAMAMI) ve `detectedPlatform` düşerdi; `vin` prefix olsaydı `vinMasked` (VIN sızıntısını ÖNLEMEK için üretilen maskeli türev) silinirdi → koruma raporu boşaltırdı. False-positive taraması: 14 üretici dosyada 326 benzersiz anahtar tarandı, **meşru alan düşmüyor**. **(3) Cycle guard YOKTU:** özyinelemeyi sınırlayan tek şey MAX_DEPTH'ti. Eklenen guard **ANCESTOR-PATH** desenidir (girerken ekle, `finally` ile çıkarken sil) — naif "görülen nesneler" seti PAYLAŞILAN REFERANSI (aynı nesne iki KARDEŞ dalda = DAG) cycle sanıp ikinci dalı sessizce boşaltırdı (DTC `[null,null]` ile aynı sınıf sessiz kayıp). Cycle → `[CYCLE]` işareti (sessiz kayıp DEĞİL, görünür); dizi elemanıysa ELENMEZ (uzunluk korunur). **Public API artık THROW ETMEZ:** getter/Proxy patlarsa yalnız o DÜĞÜM `[UNREADABLE]` olur, kardeş alanlar akar (`_safeSection`'ın düğüm-bazlı karşılığı) → tek zehirli alan TÜM tanı raporunu öldüremez. **DEĞİŞMEYEN (bilinçli):** MAX_DEPTH=6 · MAX_ARRAY_LEN=20 · MAX_STR_LEN=2048 · mevcut MASKS (VIN/MAC/COORD/api_key=/token=) · UI · RPC · DB/migration · rıza · teslimat modeli (`'sent'` yalanı AYRI PR). **HOST-DOĞRULANDI:** 17 yeni davranış kilidi + 4 kaynak-metin kilidi (regresyon kasası; PR-1'in dizi kilidi kaldırılmadı, yeni doğru davranışa GÜNCELLENDİ), tam suite **3711 yeşil (206 dosya)**, tsc temiz, lint 0 error, build OK. **BYTE ÖLÇÜMÜ (en zengin fixture, aynı test, önce/sonra):** `self_test` **5157 B → 5119 B (−38 B)** — sanitize maskeleme sonrası hafif KÜÇÜLDÜ, şişme YOK; `dev_inspector` **5479 B → 5479 B (0)** — inspector zaten aynı kapıdan geçiyordu. | `platform/remoteLogService.ts` (`_normalizeKey` + genişletilmiş DENY_KEYS + ancestor-path cycle guard + düğüm-bazlı fail-soft + `_sanitizeSection` tek kapı; `reportSelfTestSnapshot` + `reportDiagnosticSnapshot` onu kullanır), `__tests__/diagnosticSanitizeHardening.test.ts` (yeni, 17 test), `__tests__/regression.guards.test.ts` (4 kilit; dizi kilidi güncellendi) | Gerçek cihazda: (1) **"Tanı Robotu" (self-test) raporu gönderiliyor** ve gövdesinde `selfTest.results[].detail` alanları MEVCUT (bölüm kaybolmadı, sanitize onu boşaltmadı); (2) `selfTest.summary`/`worst`/`env` dolu ve `results.length` = summary toplamı (MAX_ARRAY_LEN=20 kırpması YOK — bugün 14 prob, 6 pay var); (3) gerçek self-test raporunda **ham `Error.message`/fetch hata metni içinde VIN/MAC/koordinat/anahtar İZİ YOK**, maskeler (`[VIN]`/`[MAC]`/`[COORD]`/`api_key=[MASKED]`) görünüyorsa doğru çalışıyor; (4) `platform` (Event Bus/HAL sayaçları) ve `vidMirror.vehicle.vinMasked` bölümleri raporda **HÂLÂ VAR** (yeni deny listesi meşru alanları düşürmedi — TAM eşleşme kanıtı); (5) `triage` mevcut ve `ruleErrors=0`; (6) DTC varken `obdDeep.dtc.codes[0].code` dolu (PR-1 kazanımı bozulmadı); (7) raporda `[CYCLE]`/`[UNREADABLE]` işareti **görünmüyor** (üretimde dairesel/zehirli düğüm yok — görünürse üretici tarafta bug var demektir, teşhis edilir); (8) rapor gövdesinde `truncated` alanı YOK ve `octet_length` < 45 KB; (9) low-tier cihazda (K24/Mali-400 veya T507) self-test raporu gönderimi başarılı, ANR/donma yok; (10) Xiaomi'de CPU/RAM/boot regresyonu yok. **Gerçek cihazda self-test raporu görülmeden 🟢 YAPMA** — host testleri kodu kanıtlar, sahadaki gerçek `detail` metinlerini kanıtlamaz. | 2026-07-13 | diff --git a/src/__tests__/deepScanChangeBaselineAdapter.test.ts b/src/__tests__/deepScanChangeBaselineAdapter.test.ts new file mode 100644 index 0000000..ebf6e0a --- /dev/null +++ b/src/__tests__/deepScanChangeBaselineAdapter.test.ts @@ -0,0 +1,258 @@ +/** + * deepScanChangeBaselineAdapter.test.ts — W5-3c-2 "Change Baseline Adapter" kilitleri. + * + * KAPSAM: adapter iki SALT-OKUNUR kaynağı (persistence.load + knowledgeBase.get) normalize + * `ChangeBaseline`'a çevirir. Hiçbir save/active-discovery/ECU-PID-DID isteği YOK. Çıktı + * bounded+immutable, yalnız normalize ECU kümesi (firmware/VIN/PID/DID TAŞIMAZ). Handler + * DEĞİŞMEZ; adapter'ın ürettiği veri doğrudan handler'a bağlanabilir. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { createChangeBaselineAdapter } from '../platform/deepScan/changeBaselineAdapter'; +import { + createOfflineChangeDetectionHandler, + type PhaseContext, + type PhaseResult, + type DeepScanSnapshot, +} from '../platform/deepScan'; + +const HASH = 'a1b2c3d4e5f60718'; +const ADAPTER_SRC = readFileSync( + join(process.cwd(), 'src', 'platform', 'deepScan', 'changeBaselineAdapter.ts'), 'utf8'); +const HANDLER_SRC = readFileSync( + join(process.cwd(), 'src', 'platform', 'deepScan', 'offlineChangeDetectionHandler.ts'), 'utf8'); + +/* ── Fake kaynaklar ─────────────────────────────────────────────────────────── */ + +function fakePersistence(rec: { discoveredEcus?: readonly string[] } | null) { + return { load: vi.fn((_h: string) => rec) }; +} +function fakeKnowledge(rec: { discoveredEcus?: readonly string[] } | null) { + return { get: vi.fn((_h: string) => rec) }; +} + +function mkSnap(hash: string | null): DeepScanSnapshot { + return { + scanId: 'scan-1', vehicleFingerprintHash: hash, status: 'analyzing', mode: 'CHANGE_CHECK', + phase: 'change_detection', progressPercent: 0, startedAt: 1, updatedAt: 1, completedAt: null, + isFirstScan: false, ignitionRequired: true, ignitionConfirmed: null, + discoveredEcuCount: 0, discoveredPidCount: 0, discoveredDidCount: 0, newDiscoveriesCount: 0, + changedFirmware: false, changedEcu: false, warnings: [], errorCode: null, reportSummary: null, + }; +} +function mkCtx(hash: string | null, cancelled = false): PhaseContext { + return Object.freeze({ + phase: 'change_detection', mode: 'CHANGE_CHECK', snapshot: mkSnap(hash), + isCancelled: () => cancelled, + }); +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 1) Kaynak kombinasyonları + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3c-2 — kaynak kombinasyonları', () => { + it('persistence BOŞ (load→null) → prior baseline null', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence(null), knowledgeBase: fakeKnowledge({ discoveredEcus: ['7E0'] }) }); + expect(a.loadPriorBaseline(HASH)).toBeNull(); + expect(a.loadCurrentBaseline(HASH)).toEqual({ ecus: ['7E0'] }); + }); + + it('knowledge BOŞ (get→null) → current baseline null', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: ['7E0'] }), knowledgeBase: fakeKnowledge(null) }); + expect(a.loadCurrentBaseline(HASH)).toBeNull(); + expect(a.loadPriorBaseline(HASH)).toEqual({ ecus: ['7E0'] }); + }); + + it('YALNIZ persistence → prior dolu, current null', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: ['7E0', '7E8'] }), knowledgeBase: fakeKnowledge(null) }); + expect(a.loadPriorBaseline(HASH)?.ecus).toEqual(['7E0', '7E8']); + expect(a.loadCurrentBaseline(HASH)).toBeNull(); + }); + + it('YALNIZ knowledge → prior null, current dolu', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence(null), knowledgeBase: fakeKnowledge({ discoveredEcus: ['7E0'] }) }); + expect(a.loadPriorBaseline(HASH)).toBeNull(); + expect(a.loadCurrentBaseline(HASH)?.ecus).toEqual(['7E0']); + }); + + it('İKİSİ dolu → iki baseline da normalize ECU taşır', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: ['7E0'] }), knowledgeBase: fakeKnowledge({ discoveredEcus: ['7E0', '7E8'] }) }); + expect(a.loadPriorBaseline(HASH)?.ecus).toEqual(['7E0']); + expect(a.loadCurrentBaseline(HASH)?.ecus).toEqual(['7E0', '7E8']); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 2) Normalize · duplicate · null-güvenli + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3c-2 — normalize & güvenlik', () => { + it('NORMALİZE: trim/upper/0x kaldırılır', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: [' 0x7e0 ', '7E8'] }), knowledgeBase: fakeKnowledge(null) }); + expect(a.loadPriorBaseline(HASH)?.ecus).toEqual(['7E0', '7E8']); + }); + + it('DUPLICATE kaldırılır (normalize sonrası aynı olanlar)', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: ['7E0', '0x7E0', '7e0', '7E8'] }), knowledgeBase: fakeKnowledge(null) }); + expect(a.loadPriorBaseline(HASH)?.ecus).toEqual(['7E0', '7E8']); + }); + + it('NULL-GÜVENLİ: discoveredEcus undefined / dizi değil → boş ecus', () => { + const a1 = createChangeBaselineAdapter({ persistence: fakePersistence({}), knowledgeBase: fakeKnowledge(null) }); + expect(a1.loadPriorBaseline(HASH)).toEqual({ ecus: [] }); + const a2 = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: 'x' as unknown as string[] }), knowledgeBase: fakeKnowledge(null) }); + expect(a2.loadPriorBaseline(HASH)).toEqual({ ecus: [] }); + }); + + it('geçersiz eleman (non-string / boş) atılır', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: ['7E0', '', ' ', 123 as unknown as string, '7E8'] }), knowledgeBase: fakeKnowledge(null) }); + expect(a.loadPriorBaseline(HASH)?.ecus).toEqual(['7E0', '7E8']); + }); + + it('GEÇERSİZ HASH (boş) → null, kaynak ÇAĞRILMAZ', () => { + const p = fakePersistence({ discoveredEcus: ['7E0'] }); + const k = fakeKnowledge({ discoveredEcus: ['7E0'] }); + const a = createChangeBaselineAdapter({ persistence: p, knowledgeBase: k }); + expect(a.loadPriorBaseline('')).toBeNull(); + expect(a.loadCurrentBaseline('')).toBeNull(); + expect(p.load).not.toHaveBeenCalled(); + expect(k.get).not.toHaveBeenCalled(); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 3) Immutable · input mutate edilmiyor · privacy + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3c-2 — immutability & privacy', () => { + it('çıktı IMMUTABLE (baseline + ecus dizisi frozen)', () => { + const a = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: ['7E0'] }), knowledgeBase: fakeKnowledge(null) }); + const bl = a.loadPriorBaseline(HASH)!; + expect(Object.isFrozen(bl)).toBe(true); + expect(Object.isFrozen(bl.ecus)).toBe(true); + }); + + it('KAYNAK dizisi MUTATE EDİLMEZ', () => { + const src = Object.freeze(['7E0', '7E8']); + const a = createChangeBaselineAdapter({ persistence: fakePersistence({ discoveredEcus: src }), knowledgeBase: fakeKnowledge(null) }); + a.loadPriorBaseline(HASH); + expect(src).toEqual(['7E0', '7E8']); // değişmedi + }); + + it('PRIVACY: baseline YALNIZ {ecus} — VIN/firmware/PID/DID/signals TAŞIMAZ', () => { + const rec = { discoveredEcus: ['7E0'], vin: 'WDD12345678901234', firmwareVersions: ['v1'], discoveredPids: { '0C': {} }, supportedModes: ['01'] }; + const a = createChangeBaselineAdapter({ persistence: fakePersistence(rec as unknown as { discoveredEcus?: readonly string[] }), knowledgeBase: fakeKnowledge(null) }); + const bl = a.loadPriorBaseline(HASH)!; + expect(Object.keys(bl)).toEqual(['ecus']); + const blob = JSON.stringify(bl); + expect(blob).not.toContain('WDD12345678901234'); + expect(blob).not.toContain('v1'); + expect(blob).not.toContain('0C'); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 4) Fail-soft + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3c-2 — fail-soft', () => { + it('persistence.load THROW → null (throw kaçmaz)', () => { + const a = createChangeBaselineAdapter({ + persistence: { load: () => { throw new Error('disk boom'); } }, + knowledgeBase: fakeKnowledge({ discoveredEcus: ['7E0'] }), + }); + expect(a.loadPriorBaseline(HASH)).toBeNull(); + expect(a.loadCurrentBaseline(HASH)?.ecus).toEqual(['7E0']); + }); + + it('knowledge.get THROW → null', () => { + const a = createChangeBaselineAdapter({ + persistence: fakePersistence({ discoveredEcus: ['7E0'] }), + knowledgeBase: { get: () => { throw new Error('kb boom'); } }, + }); + expect(a.loadCurrentBaseline(HASH)).toBeNull(); + }); + + it('varsayılan deps (gerçek boş singleton\'lar) → throw etmeden null döner', () => { + const a = createChangeBaselineAdapter(); + expect(() => a.loadPriorBaseline(HASH)).not.toThrow(); + expect(() => a.loadCurrentBaseline(HASH)).not.toThrow(); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 5) Handler entegrasyonu — handler DEĞİŞMEDEN yalnız ChangeBaseline görür + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3c-2 — handler entegrasyonu', () => { + it('adapter çıktısı handler’a bağlanır → changedEcu doğru; handler yalnız {ecus} görür', async () => { + const a = createChangeBaselineAdapter({ + persistence: fakePersistence({ discoveredEcus: ['7E0'] }), // prior + knowledgeBase: fakeKnowledge({ discoveredEcus: ['7E0', '7E8'] }), // current + }); + // Handler'ın gördüğü baseline yalnız {ecus} olmalı. + expect(Object.keys(a.loadPriorBaseline(HASH)!)).toEqual(['ecus']); + + const handler = createOfflineChangeDetectionHandler(a); + const r = await handler(mkCtx(HASH)) as PhaseResult; + expect(r.changedEcu).toBe(true); + expect(r.changedFirmware).toBe(false); // firmware hâlâ üretilmiyor + expect(r.reason).toBe('ecu_set_changed'); + }); + + it('adapter+handler: aynı ECU seti → değişim yok', async () => { + const a = createChangeBaselineAdapter({ + persistence: fakePersistence({ discoveredEcus: ['7E0', '7E8'] }), + knowledgeBase: fakeKnowledge({ discoveredEcus: ['0x7E8', '7e0'] }), // normalize sonrası aynı + }); + const r = await createOfflineChangeDetectionHandler(a)(mkCtx(HASH)) as PhaseResult; + expect(r.changedEcu).toBe(false); + }); + + it('adapter+handler: CANCEL → status cancelled (kaynak okunmaz)', async () => { + const p = fakePersistence({ discoveredEcus: ['7E0'] }); + const a = createChangeBaselineAdapter({ persistence: p, knowledgeBase: fakeKnowledge({ discoveredEcus: ['7E8'] }) }); + const r = await createOfflineChangeDetectionHandler(a)(mkCtx(HASH, true)) as PhaseResult; + expect(r.status).toBe('cancelled'); + expect(p.load).not.toHaveBeenCalled(); // iptalde baseline okunmaz + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 6) Kaynak kilitleri — salt-okunur, active discovery/write YOK, handler saf kalır + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3c-2 — kaynak kilitleri', () => { + it('adapter YALNIZ .load()/.get() okur; save/active-record/discovery çağırmaz', () => { + expect(ADAPTER_SRC).toMatch(/\.load\s*\(/); + expect(ADAPTER_SRC).toMatch(/\.get\s*\(/); + expect(ADAPTER_SRC).not.toMatch(/\.saveSnapshot\s*\(/); + expect(ADAPTER_SRC).not.toMatch(/\.completeScan\s*\(/); + expect(ADAPTER_SRC).not.toMatch(/\.recordEcuDiscovery\s*\(|\.recordPidDiscovery\s*\(|\.recordDidDiscovery\s*\(|\.recordFirmwareResult\s*\(/); + expect(ADAPTER_SRC).not.toMatch(/\.recordChangeDetection\s*\(|\.startScan\s*\(|\.runNextPhase\s*\(|_applyResult\s*\(|_finalize\s*\(/); + }); + + it('adapter Event Bus/Capability/HAL/Native/OBD/CAN import ETMEZ', () => { + const imports = ADAPTER_SRC.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n'); + expect(imports).not.toMatch(/eventBus|capabilityRegistry|vehicleHal|obdService|nativePlugin|canbus|Capacitor|supabase|assistant|driverDna|prediction/i); + // Yalnız izinli kaynaklar + tip. + expect(imports).toMatch(/deepScanPersistence/); + expect(imports).toMatch(/vehicleKnowledgeBase/); + }); + + it('HANDLER hâlâ persistence/knowledge-base/adapter import ETMEZ (saf kalır, DI korunur)', () => { + const imports = HANDLER_SRC.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n'); + expect(imports).not.toMatch(/deepScanPersistence|vehicleKnowledgeBase|changeBaselineAdapter/i); + expect(imports).toMatch(/import type/); // yalnız tip + }); + + it('adapter trigger/wiring/SystemBoot’a DOKUNMAZ', () => { + expect(ADAPTER_SRC).not.toContain('triggerDeepScanOfflinePass'); + expect(ADAPTER_SRC).not.toContain('platformCoreDeepScanWiring'); + expect(ADAPTER_SRC).not.toContain('SystemBoot'); + expect(ADAPTER_SRC).not.toMatch(/setInterval|setTimeout|requestAnimationFrame/); + }); +}); diff --git a/src/platform/deepScan/changeBaselineAdapter.ts b/src/platform/deepScan/changeBaselineAdapter.ts new file mode 100644 index 0000000..50840dd --- /dev/null +++ b/src/platform/deepScan/changeBaselineAdapter.ts @@ -0,0 +1,100 @@ +/** + * changeBaselineAdapter — W5-3c-2. Offline change-detection handler'a GERÇEK baseline + * sağlayan SALT-OKUNUR adapter katmanı. + * + * NE YAPAR: iki mevcut, pasif veri kaynağını `ChangeBaseline`'a (yalnız normalize ECU + * kümesi) çevirir: + * - PRIOR → `deepScanPersistence.load(hash).discoveredEcus` (son TAM taramanın diske + * yazılı çıktısı) + * - CURRENT → `vehicleKnowledgeBase.get(hash).discoveredEcus` (AutoLearning'in PASİF + * öğrendiği, araç zaten HAL/VDL üzerinden akarken toplanan gözlem — read-only store) + * İkisi de SALT-OKUNUR: hiçbir `save`/`completeScan`/active-record/discovery/CAN/OBD YOK. + * Deep-scan araca TEK sorgu bile göndermez — iki küme de zaten elde olan veridir. + * + * TASARIM: + * - Factory + Dependency Injection: `createChangeBaselineAdapter({persistence?, knowledgeBase?})`. + * Üretim varsayılanları paylaşılan singleton'lar; test için fake enjekte edilir. + * - Handler'a `OfflineChangeDetectionDeps` şeklinde bağlanır (loadPriorBaseline/ + * loadCurrentBaseline) — ADAPTER handler'ı ÇAĞIRMAZ, yalnız onun tükettiği veriyi üretir. + * - Handler bu modülü import ETMEZ (bağımlılık yönü: adapter → kaynaklar; handler saf kalır). + * + * NE YAPMAZ: firmware EKLEMEZ (handler `changedFirmware=false` üretmeye devam eder); + * VIN/firmware sürümü/PID-DID/koordinat/secret TAŞIMAZ (yalnız normalize ECU kimlikleri); + * trigger/wiring/runtime/persistence-yazımı/Event Bus/Capability/HAL DEĞİŞTİRMEZ. + * + * FAIL-SOFT: kaynak throw ederse / kayıt yoksa / hash geçersizse → `null` (değişim iddiası + * handler tarafında fail-closed). Çıktı IMMUTABLE (dondurulmuş). + */ + +import { deepScanPersistenceStore } from './deepScanPersistence'; +import { vehicleKnowledgeBaseStore } from '../vehicleKnowledgeBase'; +import type { ChangeBaseline, OfflineChangeDetectionDeps } from './offlineChangeDetectionHandler'; + +/** Yalnız `discoveredEcus` okunan minimal, yapısal kayıt arayüzü (kaynak-agnostik). */ +interface EcuRecordLike { + readonly discoveredEcus?: readonly string[]; +} +interface PersistenceLike { + load(vehicleFingerprintHash: string): EcuRecordLike | null; +} +interface KnowledgeLike { + get(vehicleFingerprintHash: string): EcuRecordLike | null; +} + +/** Enjekte edilebilir kaynaklar (varsayılan: paylaşılan salt-okunur singleton'lar). */ +export interface ChangeBaselineAdapterDeps { + readonly persistence?: PersistenceLike; + readonly knowledgeBase?: KnowledgeLike; +} + +/** Adapter yüzeyi = handler'ın tükettiği bağımlılık şekli (DI ile bağlanır). */ +export type ChangeBaselineAdapter = OfflineChangeDetectionDeps; + +/** Normalize ECU listesi: trim + upper + `0x` at + dedup + boş at (sıra korunur). */ +function _normEcus(list: readonly string[] | undefined): string[] { + const seen = new Set(); + const out: string[] = []; + if (!Array.isArray(list)) return out; + for (const raw of list) { + if (typeof raw !== 'string') continue; + const key = raw.trim().toUpperCase().replace(/\s+/g, '').replace(/^0X/, ''); + if (key && !seen.has(key)) { seen.add(key); out.push(key); } + } + return out; +} + +/** Kayıt → bounded + immutable `ChangeBaseline` (yalnız ECU). Kayıt yoksa null. */ +function _toBaseline(rec: EcuRecordLike | null): ChangeBaseline | null { + if (!rec || typeof rec !== 'object') return null; + return Object.freeze({ ecus: Object.freeze(_normEcus(rec.discoveredEcus)) }); +} + +function _validHash(hash: string): boolean { + return typeof hash === 'string' && hash.length > 0; +} + +/** + * Offline change-detection için baseline sağlayıcılarını üretir. Dönen nesne DOĞRUDAN + * `createOfflineChangeDetectionHandler(...)`'a geçirilebilir (W5-3c-3 wiring — AYRI PR). + */ +export function createChangeBaselineAdapter( + deps: ChangeBaselineAdapterDeps = {}, +): ChangeBaselineAdapter { + const persistence = deps.persistence ?? deepScanPersistenceStore; + const knowledgeBase = deps.knowledgeBase ?? vehicleKnowledgeBaseStore; + + return { + loadPriorBaseline: (hash: string): ChangeBaseline | null => { + if (!_validHash(hash)) return null; + let rec: EcuRecordLike | null = null; + try { rec = persistence.load(hash); } catch { rec = null; } // salt-okunur, fail-soft + return _toBaseline(rec); + }, + loadCurrentBaseline: (hash: string): ChangeBaseline | null => { + if (!_validHash(hash)) return null; + let rec: EcuRecordLike | null = null; + try { rec = knowledgeBase.get(hash); } catch { rec = null; } // salt-okunur, fail-soft + return _toBaseline(rec); + }, + }; +}