From 86261d1d57027f131d1af3897ca9000c9e9d4d4b Mon Sep 17 00:00:00 2001 From: Selim Date: Mon, 13 Jul 2026 11:50:35 +0300 Subject: [PATCH] =?UTF-8?q?feat(deep-scan):=20W5-3b=20=E2=80=94=20Offline?= =?UTF-8?q?=20Pass=20Trigger=20+=20Runtime=20Wiring=20+=20Minimal=20Diagno?= =?UTF-8?q?stics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W5-3a'daki runOfflinePass() İLK kez üretim wiring'ine bağlandı. Tek deterministik giriş noktası triggerDeepScanOfflinePass() (hash/dedup guard + single-flight) + cancelDeepScanOfflinePass() + bounded getDeepScanOfflinePassStatus() eklendi; SystemBoot Wave 3'te (Wave 2 ownership KURULDUKTAN SONRA) fire-and-forget çağrılır. Davranış DEĞİŞMEZ (yalnız çağrılabilir altyapı): - 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 (aktif-kayıt API'leri hiç çağrılmaz — gerçek zincir testiyle kanıtlı). runtime idle → running → idle (pass sonunda reset). 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); restart'ta guard doğal sıfırlanır (durum wiring örneğine bağlı). Ownership korunur: OwnedOrchestrator offline runOfflinePass/cancelOfflinePass'i gösterir ama AKTİF tarama API'lerini (start/run/runNextPhase) ASLA göstermez (compile-time). W5-3a'nın 2 kapsam kilidi bu yeni davranışa GÜNCELLENDİ (kaldırılmadı). Diagnostics (bounded + immutable): present/started/running/active/cancelled/ triggerCount/lastRun/lastDuration/lastResult/lastReason/lastError/phaseCount/ summaryAvailable — hepsi primitive/enum; VIN/GPS/secret/ham CAN-OBD/guard-anahtarı YOK. DEĞİŞMEDİ: Event Bus / Capability Registry / HAL / Assistant / Driver DNA / Prediction / SQL / Supabase / Native / CAN / OBD (wiring bunları import etmez). Doğrulama: yeni trigger-wiring testleri (25+ senaryo); tam suite 3820 yeşil (210 dosya); tsc -b --noEmit temiz; lint 0 error; production build OK. Ledger: W5-3b 🔴 (gerçek handler yok · gerçek discovery yok · W5-3c değer üreten PR · aktif ECU 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 + src/__tests__/deepScanOfflinePass.test.ts | 18 +- .../deepScanOfflineTriggerWiring.test.ts | 478 ++++++++++++++++++ .../platformCoreDeepScanWiring.test.ts | 13 +- src/platform/system/SystemBoot.ts | 10 +- .../system/platformCoreDeepScanWiring.ts | 219 +++++++- 6 files changed, 726 insertions(+), 13 deletions(-) create mode 100644 src/__tests__/deepScanOfflineTriggerWiring.test.ts diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index c13d763..bd05ee0 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 | |---|---------|--------|------------------------------------------|---------| +| 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 | | 62 | **🔴 Device Validation Pending — Tanı Raporu DTC Derinliği + Triyaj Null-Guard (Diagnostics PR-1)** — Denetimin (2026-07-12) KANITLI iki P0'ı kapatıldı. **(1) DTC kodları kabloda `null` gidiyordu:** `remoteLogService._deepSanitize` derinlik ≥ `MAX_DEPTH` olan KABI düşürür; gövde `depth=0` ile çağrıldığı için zincir `root(0)→obdDeep(1)→dtc(2)→codes[](3)→kod nesnesi(4)` **derinlik 4'te ölüyordu** → `codes:[null,null]`. Aynı derinlikteki `obdDeep.extended.samples[]` (katalog-dışı PID keşfi), `uiActivity.recent[].reasons[]`, `inspector.timeline[].signals/env` ve `inspector.runtime.workers[]` de düşüyordu. **`MAX_DEPTH` 4 → 6 (ÖLÇÜLMÜŞ, keyfi değil):** payload gövdesinin tüm bölümleri tarandı, en derin **kap** derinlik 4'te → 5 = minimum yeterli; 6 seçildi çünkü `sanitizeForRemote` ve inspector girişleri `depth=1`'den başlıyor (bir seviye pay) ve ölçülen payload'da derinlik ≥5'te KAP OLMADIĞI için **bugün 0 ek bayt** akıtır. Sanitize dizi dalı artık düşen elemanı **ELER** (`.map` ile yerinde bırakılan `undefined` JSON'da `null` oluyordu — sessiz "kod var ama boş" yalanı). **(2) DTC varken TRİYAJ TAMAMEN kayboluyordu:** `ruleObdDtc` null-guard'sız `c.code` okuyordu → TypeError → `buildTriageSnapshot` döngüsü kırılıyor → `_attachTriage`'ın yutucu catch'i `payload.triage`'ı HİÇ yazmıyor → admin **arızanın olduğu tam anda** "kritik bulgu yok" görüyordu (13 kuralın HEPSİ birden susuyordu: termal/bellek/güç/GPS/self-test dahil). Kural null-guard'landı (`dtc.count` "arıza var" kanıtıdır; kod METNİ yalnız zenginleştirme → liste boş/bozuk olsa da bulgu ÜRETİLİR) **+ motor seviyesinde KURAL İZOLASYONU** (bir kural patlarsa yalnız o kural düşer, diğerleri çalışır) **+ `ruleErrors` sayacı** (izolasyon sessiz olmasın — triyaj eksik çalıştıysa raporda görünür). **DEĞİŞMEYEN (bilinçli):** redaction/allowlist/deny-list/maskeler · `MAX_ARRAY_LEN`/`MAX_STR_LEN` · UI · RPC · DB/migration · rıza/önizleme · teslimat modeli (`'sent'` yalanı **PR-2'ye** bırakıldı). **HOST-DOĞRULANDI:** 13 yeni kilit yeşil (9 davranış + 4 kaynak-metin regresyon kasası), tam suite **3690 yeşil (205 dosya)**, tsc temiz, lint 0 error, build OK. **BYTE ÖLÇÜMÜ (en zengin fixture, aynı test, önce/sonra):** `support_snapshot` **3163 B → 4045 B (+882 B, +%27,9)** · `dev_inspector` **3747 B → 5480 B (+1733 B, +%46,3)** → ikisi de canlı sunucu tavanının (**64 KB**, migration 027) çok altında. | `platform/remoteLogService.ts` (MAX_DEPTH 4→6 + dizi dalı undefined filtresi), `platform/diagnosticTriage.ts` (ruleObdDtc null-guard + kural izolasyonu + `ruleErrors`), `__tests__/diagnosticDtcDepthTriage.test.ts` (yeni, 9 test), `__tests__/regression.guards.test.ts` (4 kaynak-metin kilidi) | **Gerçek araçta, DTC VARKEN** (arıza lambası yanan / kasıtlı DTC üretilmiş araç) gönderilen `support_snapshot` gövdesinde: (1) `obdDeep.dtc.codes[0].code` **dolu string** (`null` DEĞİL) ve `severity`/`system` dolu; (2) `codes` dizisinde **hiç `null` eleman yok**; (3) `payload.triage` **MEVCUT** ve `findings` içinde **`OBD_DTC_PRESENT`** bulgusu var (kritik DTC varsa `severity:'critical'`); (4) `triage.ruleErrors = 0`; (5) DTC varken diğer triyaj bulguları (termal/güç/GPS) **da** üretilmeye devam ediyor; (6) `obdDeep.extended.samples[0].pid` dolu (katalog-dışı PID keşfi artık sunucuya ulaşıyor); (7) Dev Inspector raporunda `inspector.timeline[0].signals.spd/rpm` **dolu**; (8) raporda VIN/MAC/koordinat/secret **YOK** (maskeler delinmedi); (9) gerçek rapor gövdesi **`truncated` alanı taşımıyor** ve `octet_length` < 45 KB (64 KB tavanı aşılırsa sunucu gövdeyi `{truncated:true}` kabuğuyla DEĞİŞTİRİR = %100 sessiz kayıp, üstelik UI yine "gönderildi" der — PR-2 konusu); (10) low-tier cihazda (K24/Mali-400 veya T507) rapor gönderimi başarılı, ANR/donma yok. **Araç + DTC olmadan 🟢 YAPMA** — host testleri kodu kanıtlar, gerçek DTC akışını kanıtlamaz. | 2026-07-13 | | 61 | **🔴 Device Validation Pending — Deep Scan Offline Run Surface + Guard Band (W5-3a)** — Deep Scan Orchestrator'a tip-seviyesinde ayrılmış, SONLANDIRMAYAN offline yürütme yüzeyi `runOfflinePass()` eklendi. YALNIZ `OFFLINE_PHASE_SEQUENCE` fazlarını (capability/fingerprint/knowledge/evidence/change_detection/report) deterministik sırayla, kontak BİLİNMİYORKEN (`ignitionConfirmed=null`) yürütür — çünkü offline fazlar araca sorgu göndermez. **KORUMA BANDI (tip + çalışma-zamanı):** handler haritası `Partial>` → aktif faz (`vehicle_identity`/`protocol_detection`/`ecu_discovery`/`standard_pid_discovery`/`manufacturer_did_discovery`/`firmware_inventory`) anahtarı DERLEME HATASIDIR; `OfflinePhase = Exclude` türetildiği için yeni faz sessizce sınıflandırılamaz. **YAPMAZ:** `_finalize()`/`completeScan()`/`persistence.completeScan()` ÇAĞIRMAZ → `hasCompletedFullScan`/`completedScanCount`/`changeCheckCount` DEĞİŞMEZ; persistence'a HİÇ YAZMAZ (yalnız mod kararı için `hasCompletedFullScan()` OKUR); `_applyResult()` (aktif-kayıt yolu) ÇAĞIRMAZ → keşif payload'ı (`ecus/pids/dids/firmware`) runtime aktif-kayıt API'lerine (`recordEcuDiscovery` vb.) GİTMEZ (o API'ler kontak yokken `waiting_for_ignition`'a düşürürdü); kontak-serbest tek yazma yolu `recordChangeDetection` (yalnız `changedFirmware/changedEcu`). **YAŞAM DÖNGÜSÜ:** pass sonunda `runtime.reset()` ZORUNLU (`try/finally`) → runtime `idle`, gerçek tarama sonradan başlayabilir, ikinci pass tekrar çalışır. **FAIL-SOFT:** handler throw/timeout(5sn varsayılan)/cancel tek fazı düşürür, KALAN offline fazlar DEVAM EDER; runtime `failed`'a düşmez; timeout timer'ı `finally`'de temizlenir (zero-leak). Bounded+immutable özet (sayımlar + faz sonuçları + sanitize uyarılar; VIN/koordinat/ham hex/secret/parmak-izi hash TAŞIMAZ). Ownership wiring yüzeyi (`OwnedOrchestrator`) DEĞİŞMEDİ → hâlâ yalnız `getSnapshot/dispose/isDisposed`; `start()/run()/runNextPhase()/runOfflinePass()` GÖSTERMEZ. SystemBoot/Event Bus/Capability/Native/OBD/CAN/SQL/Supabase DEĞİŞMEDİ. **HOST-DOĞRULANDI:** 32 yeni test yeşil; deep scan suite 177 yeşil; tam suite 3709 yeşil (205 dosya); tsc `-b --noEmit` temiz (aktif-handler `@ts-expect-error` compile-lock doğrulandı); lint 0 error (W5-3a dosyaları uyarısız); production build OK. **AÇIKÇA:** (a) Bu PR hiçbir yerden çağrılmıyor — üretim runtime davranışı DEĞİŞMİYOR. (b) W5-3b Trigger AYRI PR. (c) W5-3c gerçek offline Handler'lar AYRI PR. (d) Aktif ECU/PID/DID/Firmware sorgusunun gitmemesi tip (`OfflinePhase`) + faz sınırı (`OFFLINE_PHASE_SEQUENCE`) + aktif-kayıt kapısı ile GARANTİ ediliyor. | `platform/deepScan/deepScanOrchestrator.ts` (+304: `runOfflinePass`/`cancelOfflinePass`/guard band), `platform/deepScan/deepScanModel.ts` (`ActivePhase`/`OfflinePhase`/`OFFLINE_PHASE_SEQUENCE`/`isOfflinePhase`), `platform/deepScan/index.ts` (tip export), `__tests__/deepScanOfflinePass.test.ts` (yeni, 32 test) | Gerçek cihazda (Xiaomi 23090RA98I + en az bir low-tier K24/Mali-400/T507) — W5-3b Trigger bağlandıktan SONRA: (1) `runOfflinePass()` kontak `null` iken offline fazları yürütür, araç OBD/CAN trafiğinde deep-scan kaynaklı ECU/PID/DID/Firmware isteği YOK (aktif sorgu üretilmiyor); (2) pass sonrası runtime `idle`, `hasCompletedFullScan` false, `completedScanCount`/`changeCheckCount` artmadı; (3) gerçek tarama pass'ten sonra normal başlayabiliyor (idle guard'ına takılmıyor); (4) handler throw/timeout/cancel'da çökme/ANR yok, tek faz düşse kalanlar sürüyor; (5) **Mali-400/K24'te steady-state ek yük ölçüm gürültüsü seviyesinde** (timer/polling yok → ≈0; pass tek-atış); (6) background/foreground geçişinde leak/çift-pass yok. Cihaz kanıtı olmadan 🟢 YAPMA. ⚠️ **Mali-400 doğrulaması bekliyor.** ⚠️ Not: gözlemlenebilirlik dönen özet üzerinden; teşhis raporuna alan EKLENMEDİ (ayrı küçük PR). | 2026-07-13 | diff --git a/src/__tests__/deepScanOfflinePass.test.ts b/src/__tests__/deepScanOfflinePass.test.ts index 2a4fb13..59a03ff 100644 --- a/src/__tests__/deepScanOfflinePass.test.ts +++ b/src/__tests__/deepScanOfflinePass.test.ts @@ -381,25 +381,29 @@ describe('W5-3a — fail-soft', () => { * ═════════════════════════════════════════════════════════════════════ */ describe('W5-3a — kapsam kilitleri (bu PR çağrılmıyor, davranış değişmiyor)', () => { - it('SystemBoot offline pass ÇAĞIRMAZ (trigger/wiring W5-3b’de)', () => { - expect(systemBootSource).not.toContain('runOfflinePass'); + it('SystemBoot offline pass’i TEK giriş noktasından (trigger wrapper) tetikler; raw scan API’si YOK (W5-3b GÜNCELLENDİ)', () => { + // W5-3a→W5-3b (regresyon kasası GÜNCELLEMESİ): W5-3a’da SystemBoot pass’i HİÇ tetiklemiyordu; + // W5-3b’de deterministik trigger wrapper ile tetikler — AMA raw pass/scan API’lerini DOĞRUDAN çağırmaz. + expect(systemBootSource).toContain('triggerDeepScanOfflinePass'); + expect(systemBootSource).not.toContain('.runOfflinePass('); // pass’ı DOĞRUDAN koşmaz (wrapper üzerinden) expect(systemBootSource).not.toContain('cancelOfflinePass'); - // W5-1 kilidi korunur: SystemBoot gerçek tarama da başlatmaz. + // W5-1 kilidi korunur: SystemBoot gerçek/aktif tarama başlatmaz. expect(systemBootSource).not.toContain('startScan'); expect(systemBootSource).not.toContain('runNextPhase'); expect(systemBootSource).toContain('startPlatformCoreDeepScanWiring'); }); - it('ownership wiring tipi start/run/runNextPhase/runOfflinePass GÖSTERMEZ', () => { - // OwnedOrchestrator yüzeyi W5-3a’da DEĞİŞMEDİ → wiring pass koşamaz (compile-time). + it('ownership wiring tipi start/run/runNextPhase GÖSTERMEZ; offline yüzey (runOfflinePass) W5-3b’de eklendi', () => { + // W5-3a→W5-3b (regresyon kasası GÜNCELLEMESİ): OwnedOrchestrator artık offline (non-active) + // yüzeyi gösterir (trigger onu çağırır); AKTİF tarama API’leri ASLA görünmez (compile-time garanti). const ifaceStart = wiringSource.indexOf('export interface OwnedOrchestrator'); expect(ifaceStart).toBeGreaterThan(-1); const iface = wiringSource.slice(ifaceStart, wiringSource.indexOf('}', ifaceStart)); expect(iface).not.toContain('start('); expect(iface).not.toContain('run('); expect(iface).not.toContain('runNextPhase'); - expect(iface).not.toContain('runOfflinePass'); - expect(wiringSource).not.toContain('runOfflinePass'); + expect(iface).toContain('runOfflinePass'); // W5-3b: offline yüzey görünür + expect(wiringSource).toContain('triggerDeepScanOfflinePass'); }); it('orchestrator Event Bus’a publish ETMEZ (köprü bu PR’da değişmedi)', () => { diff --git a/src/__tests__/deepScanOfflineTriggerWiring.test.ts b/src/__tests__/deepScanOfflineTriggerWiring.test.ts new file mode 100644 index 0000000..6b1ef8a --- /dev/null +++ b/src/__tests__/deepScanOfflineTriggerWiring.test.ts @@ -0,0 +1,478 @@ +/** + * deepScanOfflineTriggerWiring.test.ts — W5-3b "Offline Pass Trigger + Runtime Wiring + + * Minimal Diagnostics" kilitleri. + * + * KAPSAM: `runOfflinePass()` İLK kez üretim wiring'ine bağlanır — tek deterministik giriş + * noktası (`triggerDeepScanOfflinePass`) hash/dedup guard'ıyla EN FAZLA bir kez pass başlatır. + * HANDLER YOK → gerçek iş yapılmaz → üretim davranışı DEĞİŞMEZ; runtime idle→running→idle. + * + * Test izolasyonu: DI ile fake orchestrator (dedup/duplicate/cancel/timeout/diagnostics) VEYA + * gerçek runtime+persistence+orchestrator (idle→running→idle, reset, aktif-kayıt yok). Her test + * kendi wiring'ini kurar ve afterEach ile söker (modül singleton temizliği). + */ + +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { + startPlatformCoreDeepScanWiring, + triggerDeepScanOfflinePass, + cancelDeepScanOfflinePass, + getDeepScanOfflinePassStatus, + DEEP_SCAN_OFFLINE_TRIGGER_KEY, + type DeepScanWiringDeps, + type OwnedOrchestrator, +} from '../platform/system/platformCoreDeepScanWiring'; +import { + DeepScanRuntimeService, + DeepScanPersistenceStore, + type OrchestratorSnapshot, + type OfflinePassInput, + type OfflinePassSummary, + type DeepScanOrchestratorDeps, + type DeepScanStoreIO, +} from '../platform/deepScan'; + +const SRC_DIR = join(process.cwd(), 'src'); +const WIRING_SRC = readFileSync(join(SRC_DIR, 'platform', 'system', 'platformCoreDeepScanWiring.ts'), 'utf8'); +const SYSTEMBOOT_SRC = readFileSync(join(SRC_DIR, 'platform', 'system', 'SystemBoot.ts'), 'utf8'); + +/* ── Fake orchestrator (offline yüzey) ───────────────────────────────────────── */ + +function snap(over: Partial = {}): OrchestratorSnapshot { + return { + status: 'idle', mode: null, currentPhase: null, currentPhaseIndex: 0, totalPhases: 12, + progressPercent: 0, runtimeStatus: 'idle', reportSummary: null, warnings: [], ...over, + }; +} + +function makeSummary(over: Partial = {}): OfflinePassSummary { + return Object.freeze({ + ran: true, blockedReason: null, mode: null, phaseCount: 6, + successCount: 0, skippedCount: 6, errorCount: 0, cancelled: false, + outcomes: Object.freeze([]), changedFirmware: false, changedEcu: false, + warnings: Object.freeze([]), startedAt: 0, completedAt: 0, durationMs: 0, ...over, + }); +} + +function fakeOrch(opts: { + runImpl?: (i?: OfflinePassInput) => Promise; + snapshot?: OrchestratorSnapshot; +} = {}) { + let disposed = false; + let disposeCalls = 0; + const runOfflinePass = vi.fn((i?: OfflinePassInput) => + opts.runImpl ? opts.runImpl(i) : Promise.resolve(makeSummary())); + const cancelOfflinePass = vi.fn(() => { /* no-op */ }); + return { + getSnapshot: () => opts.snapshot ?? snap(), + runOfflinePass, + cancelOfflinePass, + dispose: () => { disposed = true; disposeCalls++; }, + get isDisposed() { return disposed; }, + get disposeCalls() { return disposeCalls; }, + }; +} + +function fakeShared() { return { dispose: vi.fn() }; } +function fakeIgnition() { return { dispose: vi.fn(), getConfirmedValue: () => null }; } + +function mkDeps(over: { + orchestrator?: ReturnType; + factory?: (d: DeepScanOrchestratorDeps) => OwnedOrchestrator; + now?: () => number; +} = {}): DeepScanWiringDeps { + const orch = over.orchestrator ?? fakeOrch(); + return { + runtime: fakeShared() as unknown as DeepScanWiringDeps['runtime'], + persistence: fakeShared() as unknown as DeepScanWiringDeps['persistence'], + ignitionSource: fakeIgnition() as unknown as DeepScanWiringDeps['ignitionSource'], + createOrchestrator: over.factory ?? ((_d: DeepScanOrchestratorDeps) => orch), + now: over.now ?? (() => 1000), + }; +} + +/* ── Gerçek zincir (runtime davranışı) ──────────────────────────────────────── */ + +function memIO() { + const map = new Map(); + const io: DeepScanStoreIO = { + read: (k) => map.get(k) ?? null, + write: (k, v) => { map.set(k, v); }, + remove: (k) => { map.delete(k); }, + }; + return { io, map }; +} + +function realDeps(now: () => number): { deps: DeepScanWiringDeps; runtime: DeepScanRuntimeService } { + const runtime = new DeepScanRuntimeService({ now }); + const persistence = new DeepScanPersistenceStore('k-w53b', 16, 5000, memIO().io, now); + // createOrchestrator verilmez → gerçek createDeepScanOrchestrator kullanılır. + return { deps: { runtime, persistence, now }, runtime }; +} + +const _open: Array<() => void> = []; +function start(deps: DeepScanWiringDeps) { + const c = startPlatformCoreDeepScanWiring(deps); + _open.push(c); + return c; +} +afterEach(() => { while (_open.length) { try { _open.pop()!(); } catch { /* */ } } }); + +/* ═══════════════════════════════════════════════════════════════════════ + * 1-4) Trigger · duplicate · hash guard · tek pass + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3b — trigger + dedup guard', () => { + it('1) trigger runOfflinePass’i çağırır ve özet döner', async () => { + const orch = fakeOrch(); + start(mkDeps({ orchestrator: orch })); + + const summary = await triggerDeepScanOfflinePass(); + + expect(orch.runOfflinePass).toHaveBeenCalledTimes(1); + expect(summary?.ran).toBe(true); + const st = getDeepScanOfflinePassStatus(); + expect(st.started).toBe(true); + expect(st.triggerCount).toBe(1); + }); + + it('2) DUPLICATE yok: aynı guard ikinci tetik pass BAŞLATMAZ (no-op null)', async () => { + const orch = fakeOrch(); + start(mkDeps({ orchestrator: orch })); + + const first = await triggerDeepScanOfflinePass(); + const second = await triggerDeepScanOfflinePass(); + + expect(orch.runOfflinePass).toHaveBeenCalledTimes(1); // tek pass + expect(first?.ran).toBe(true); + expect(second).toBeNull(); // dedup → no-op + expect(getDeepScanOfflinePassStatus().triggerCount).toBe(1); + }); + + it('3) HASH GUARD: aynı anahtar dedup edilir; anahtar değişirse tekrar tetiklenebilir', async () => { + const orch = fakeOrch(); + start(mkDeps({ orchestrator: orch })); + + await triggerDeepScanOfflinePass({ triggerKey: 'k1' }); + const dup = await triggerDeepScanOfflinePass({ triggerKey: 'k1' }); // aynı → dedup + const other = await triggerDeepScanOfflinePass({ triggerKey: 'k2' }); // farklı → tetiklenir + + expect(dup).toBeNull(); + expect(other?.ran).toBe(true); + expect(orch.runOfflinePass).toHaveBeenCalledTimes(2); + }); + + it('4) TEK PASS: varsayılan boot anahtarı ile tekrar tekrar tetik → tek çalışma', async () => { + const orch = fakeOrch(); + start(mkDeps({ orchestrator: orch })); + + await triggerDeepScanOfflinePass(); + await triggerDeepScanOfflinePass(); + await triggerDeepScanOfflinePass({ triggerKey: DEEP_SCAN_OFFLINE_TRIGGER_KEY }); + + expect(orch.runOfflinePass).toHaveBeenCalledTimes(1); + }); + + it('4b) SINGLE-FLIGHT: yürüyen pass varken ikinci tetik reddedilir', async () => { + let release!: (s: OfflinePassSummary) => void; + const gate = new Promise((r) => { release = r; }); + const orch = fakeOrch({ runImpl: () => gate }); + start(mkDeps({ orchestrator: orch })); + + const p1 = triggerDeepScanOfflinePass({ triggerKey: 'x' }); + const p2 = await triggerDeepScanOfflinePass({ triggerKey: 'y' }); // yürüyor → null (farklı key olsa bile) + expect(p2).toBeNull(); + release(makeSummary()); + await p1; + expect(orch.runOfflinePass).toHaveBeenCalledTimes(1); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 5, 18) Runtime yaşam döngüsü — idle→running→idle · reset + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3b — runtime yaşam döngüsü', () => { + it('5) idle → running → idle (gerçek runtime; pass sonunda reset)', async () => { + const { deps, runtime } = realDeps(() => 5000); + start(deps); + + expect(runtime.getSnapshot().status).toBe('idle'); + const summary = await triggerDeepScanOfflinePass(); + expect(summary?.ran).toBe(true); + // Pass yürüdü (running) ve bitince reset ile idle'a döndü. + expect(runtime.getSnapshot().status).toBe('idle'); + }); + + it('5b) running bayrağı pass boyunca true, bitince false (wiring bookkeeping)', async () => { + let release!: (s: OfflinePassSummary) => void; + const gate = new Promise((r) => { release = r; }); + const orch = fakeOrch({ runImpl: () => gate }); + start(mkDeps({ orchestrator: orch })); + + const p = triggerDeepScanOfflinePass(); + expect(getDeepScanOfflinePassStatus().running).toBe(true); + release(makeSummary()); + await p; + expect(getDeepScanOfflinePassStatus().running).toBe(false); + }); + + it('18) runtime RESET: pass sonrası scanId null, progress 0 (gerçek runtime)', async () => { + const { deps, runtime } = realDeps(() => 7000); + start(deps); + await triggerDeepScanOfflinePass(); + + const s = runtime.getSnapshot(); + expect(s.status).toBe('idle'); + expect(s.scanId).toBeNull(); + expect(s.progressPercent).toBe(0); + }); + + it('10) RESTART: dispose → yeniden start → trigger yine çalışır (guard sıfırlanır)', async () => { + const orch1 = fakeOrch(); + const c1 = start(mkDeps({ orchestrator: orch1 })); + await triggerDeepScanOfflinePass(); + expect(orch1.runOfflinePass).toHaveBeenCalledTimes(1); + c1(); // shutdown + + const orch2 = fakeOrch(); + start(mkDeps({ orchestrator: orch2 })); + const s = await triggerDeepScanOfflinePass(); // yeni boot → guard taze + expect(s?.ran).toBe(true); + expect(orch2.runOfflinePass).toHaveBeenCalledTimes(1); + expect(getDeepScanOfflinePassStatus().triggerCount).toBe(1); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 6-8) Diagnostics — summary · immutable · bounded + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3b — diagnostics', () => { + it('6) SUMMARY oluşuyor: summaryAvailable, lastResult, phaseCount, lastDuration dolar', async () => { + let clock = 1000; + const orch = fakeOrch({ runImpl: async () => { clock += 42; return makeSummary({ phaseCount: 6 }); } }); + start(mkDeps({ orchestrator: orch, now: () => clock })); + + await triggerDeepScanOfflinePass(); + + const st = getDeepScanOfflinePassStatus(); + expect(st.summaryAvailable).toBe(true); + expect(st.lastResult).toBe('ran'); + expect(st.phaseCount).toBe(6); + expect(st.lastReason).toBeNull(); + expect(st.lastDuration).toBe(42); + expect(typeof st.lastRun).toBe('number'); + }); + + it('6b) blocked pass → lastResult=blocked + lastReason taşınır', async () => { + const orch = fakeOrch({ runImpl: async () => makeSummary({ ran: false, blockedReason: 'runtime_not_idle', phaseCount: 0 }) }); + start(mkDeps({ orchestrator: orch })); + + await triggerDeepScanOfflinePass(); + const st = getDeepScanOfflinePassStatus(); + expect(st.lastResult).toBe('blocked'); + expect(st.lastReason).toBe('runtime_not_idle'); + }); + + it('7) diagnostics IMMUTABLE (frozen)', async () => { + start(mkDeps()); + await triggerDeepScanOfflinePass(); + expect(Object.isFrozen(getDeepScanOfflinePassStatus())).toBe(true); + // Kapalıyken de frozen. + _open.pop()!(); + expect(Object.isFrozen(getDeepScanOfflinePassStatus())).toBe(true); + }); + + it('8) diagnostics BOUNDED: yalnız sabit, primitive alanlar (dizi/nesne payload YOK)', async () => { + start(mkDeps()); + await triggerDeepScanOfflinePass(); + const st = getDeepScanOfflinePassStatus(); + const keys = Object.keys(st).sort(); + expect(keys).toEqual([ + 'active', 'cancelled', 'lastDuration', 'lastError', 'lastReason', 'lastResult', + 'lastRun', 'phaseCount', 'present', 'running', 'started', 'summaryAvailable', 'triggerCount', + ]); + for (const v of Object.values(st)) { + const ok = v === null || ['number', 'boolean', 'string'].includes(typeof v); + expect(ok).toBe(true); // hiçbir alan dizi/nesne DEĞİL + } + }); + + it('23) import yan etkisiz: hiç start edilmeden status IDLE (present=false)', () => { + const st = getDeepScanOfflinePassStatus(); + expect(st.present).toBe(false); + expect(st.started).toBe(false); + expect(st.running).toBe(false); + expect(st.triggerCount).toBe(0); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 9) Wiring dispose · ownership + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3b — dispose & ownership', () => { + it('9) cleanup owned orchestrator’ı dispose eder; status present=false', async () => { + const orch = fakeOrch(); + const c = start(mkDeps({ orchestrator: orch })); + await triggerDeepScanOfflinePass(); + c(); + expect(orch.disposeCalls).toBe(1); + expect(getDeepScanOfflinePassStatus().present).toBe(false); + }); + + it('17) OwnedOrchestrator yüzeyi start/run/runNextPhase GÖSTERMEZ (ownership korunur)', () => { + const i = WIRING_SRC.indexOf('export interface OwnedOrchestrator'); + const iface = WIRING_SRC.slice(i, WIRING_SRC.indexOf('}', i)); + expect(iface).not.toContain('start('); + expect(iface).not.toContain('run('); + expect(iface).not.toContain('runNextPhase'); + expect(iface).toContain('runOfflinePass'); // offline yüzey görünür (aktif değil) + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 19-20) cancel · timeout iletimi + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3b — cancel & timeout', () => { + it('19) cancelDeepScanOfflinePass yürüyen pass’i iptal eder (orchestrator.cancelOfflinePass + status.cancelled)', async () => { + let release!: (s: OfflinePassSummary) => void; + const gate = new Promise((r) => { release = r; }); + const orch = fakeOrch({ runImpl: () => gate }); + start(mkDeps({ orchestrator: orch })); + + const p = triggerDeepScanOfflinePass(); + cancelDeepScanOfflinePass(); + expect(orch.cancelOfflinePass).toHaveBeenCalledTimes(1); + expect(getDeepScanOfflinePassStatus().cancelled).toBe(true); + + release(makeSummary({ cancelled: true })); + await p; + expect(getDeepScanOfflinePassStatus().cancelled).toBe(true); + }); + + it('19b) cancel wiring/pass yokken güvenli no-op', () => { + expect(() => cancelDeepScanOfflinePass()).not.toThrow(); + }); + + it('20) phaseTimeoutMs pass’a AYNEN iletilir (timeout üst sınırı runOfflinePass’a düşer)', async () => { + const orch = fakeOrch(); + start(mkDeps({ orchestrator: orch })); + + await triggerDeepScanOfflinePass({ phaseTimeoutMs: 1234 }); + expect(orch.runOfflinePass).toHaveBeenCalledTimes(1); + const arg = orch.runOfflinePass.mock.calls[0][0] as OfflinePassInput | undefined; + expect(arg?.phaseTimeoutMs).toBe(1234); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 12-13) Handler yok · aktif discovery yok + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3b — handler yok / aktif discovery yok', () => { + it('12) trigger runOfflinePass’a HANDLER geçmez', async () => { + const orch = fakeOrch(); + start(mkDeps({ orchestrator: orch })); + + await triggerDeepScanOfflinePass(); + const arg = orch.runOfflinePass.mock.calls[0][0] as OfflinePassInput | undefined; + expect(arg?.handlers).toBeUndefined(); + }); + + it('13) gerçek zincirde aktif-kayıt API’leri HİÇ çağrılmaz (aktif discovery yok)', async () => { + const { deps, runtime } = realDeps(() => 9000); + const ecu = vi.spyOn(runtime, 'recordEcuDiscovery'); + const pid = vi.spyOn(runtime, 'recordPidDiscovery'); + const did = vi.spyOn(runtime, 'recordDidDiscovery'); + const fw = vi.spyOn(runtime, 'recordFirmwareResult'); + start(deps); + + await triggerDeepScanOfflinePass(); + + expect(ecu).not.toHaveBeenCalled(); + expect(pid).not.toHaveBeenCalled(); + expect(did).not.toHaveBeenCalled(); + expect(fw).not.toHaveBeenCalled(); + // Keşif sayımları sıfır kalır. + expect(runtime.getSnapshot().discoveredEcuCount).toBe(0); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 21-22) Privacy · input immutable + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3b — privacy & input immutability', () => { + it('21) status VIN/GPS/secret/ham CAN-OBD SIZDIRMAZ (yalnız enum/sayım/bayrak)', async () => { + const orch = fakeOrch({ runImpl: async () => makeSummary() }); + start(mkDeps({ orchestrator: orch })); + await triggerDeepScanOfflinePass({ triggerKey: 'WDD12345678901234' }); // guard anahtarı bile taşınmamalı + + const blob = JSON.stringify(getDeepScanOfflinePassStatus()); + expect(blob).not.toContain('WDD12345678901234'); // guard anahtarı/VIN benzeri + expect(blob).not.toMatch(/\d{2}\.\d{4},\s?\d{2}\.\d{4}/); // koordinat + expect(blob).not.toContain('sk-'); // secret öneki + }); + + it('22) trigger GİRDİSİ mutate edilmez', async () => { + start(mkDeps()); + const input = Object.freeze({ triggerKey: 'k9', phaseTimeoutMs: 77 }); + const keys = Object.keys(input); + + await triggerDeepScanOfflinePass(input); + + expect(input.triggerKey).toBe('k9'); + expect(input.phaseTimeoutMs).toBe(77); + expect(Object.keys(input)).toEqual(keys); + }); +}); + +/* ═══════════════════════════════════════════════════════════════════════ + * 11, 14-16) SystemBoot entegrasyonu · kapsam kilitleri (kaynak) + * ═════════════════════════════════════════════════════════════════════ */ + +describe('W5-3b — SystemBoot entegrasyonu & kapsam (kaynak kilitleri)', () => { + it('11) SystemBoot trigger’ı fire-and-forget çağırır (boot’u BLOKLAMAZ, fail-soft)', () => { + // void + .catch → boot await etmez, throw kaçmaz (boot regresyonu yok). + expect(SYSTEMBOOT_SRC).toContain('triggerDeepScanOfflinePass'); + expect(SYSTEMBOOT_SRC).toMatch(/void\s+triggerDeepScanOfflinePass\(\)/); + expect(SYSTEMBOOT_SRC).toContain('SystemBoot:deepScanOfflineTrigger'); + }); + + it('16) WAVE SIRASI: trigger, ownership wiring’den SONRA ve Wave 3 içinde', () => { + const iWiring = SYSTEMBOOT_SRC.indexOf('startPlatformCoreDeepScanWiring('); + const iTrigger = SYSTEMBOOT_SRC.indexOf('triggerDeepScanOfflinePass('); + const iWave3 = SYSTEMBOOT_SRC.indexOf('Starting Wave 3'); + const iWave3Ready = SYSTEMBOOT_SRC.indexOf('Wave 3 ready'); + expect(iWiring).toBeGreaterThan(0); + expect(iTrigger).toBeGreaterThan(iWiring); // ownership ÖNCE (Wave 2), trigger SONRA + expect(iTrigger).toBeGreaterThan(iWave3); // Wave 3 içinde + expect(iTrigger).toBeLessThan(iWave3Ready); + }); + + it('14) EventBus mimarisi değişmedi: wiring Event Bus import ETMEZ / publish YOK', () => { + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*eventBus/i); + expect(WIRING_SRC).not.toContain('appEventBus'); + expect(WIRING_SRC).not.toContain('.publish('); + }); + + it('15) Capability/HAL/Assistant/native-OBD-CAN import EDİLMEZ', () => { + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*capability/i); + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*assistant/i); + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*vehicleHal/i); + expect(WIRING_SRC).not.toMatch(/from\s+['"][^'"]*(obdService|canbus|nativePlugin|supabase)/i); + }); + + it('kapsam: aktif tarama API’leri (start/run/runNextPhase/startScan) wiring KAYNAĞINDA çağrılmaz', () => { + expect(WIRING_SRC).not.toMatch(/\.start\s*\(/); + expect(WIRING_SRC).not.toMatch(/\.run\s*\(/); + expect(WIRING_SRC).not.toMatch(/\.runNextPhase\s*\(/); + expect(WIRING_SRC).not.toContain('.startScan('); + // Yeni timer/polling YOK (offline pass timeout'u orchestrator'ın içinde, wiring'de değil). + expect(WIRING_SRC).not.toMatch(/setInterval|setTimeout|requestAnimationFrame/); + }); +}); diff --git a/src/__tests__/platformCoreDeepScanWiring.test.ts b/src/__tests__/platformCoreDeepScanWiring.test.ts index 6e0262b..6c17595 100644 --- a/src/__tests__/platformCoreDeepScanWiring.test.ts +++ b/src/__tests__/platformCoreDeepScanWiring.test.ts @@ -15,7 +15,15 @@ import { type DeepScanWiringDeps, type OwnedOrchestrator, } from '../platform/system/platformCoreDeepScanWiring'; -import type { OrchestratorSnapshot, DeepScanOrchestratorDeps } from '../platform/deepScan'; +import type { OrchestratorSnapshot, DeepScanOrchestratorDeps, OfflinePassSummary } from '../platform/deepScan'; + +/** W5-3b: OwnedOrchestrator artık offline yüzeyi de içerir → fake minimal özet döndürür. */ +const FAKE_OFFLINE_SUMMARY: OfflinePassSummary = Object.freeze({ + ran: true, blockedReason: null, mode: null, phaseCount: 0, + successCount: 0, skippedCount: 0, errorCount: 0, cancelled: false, + outcomes: Object.freeze([]), changedFirmware: false, changedEcu: false, + warnings: Object.freeze([]), startedAt: 0, completedAt: 0, durationMs: 0, +}); /* ── Fake'ler ──────────────────────────────────────────────────────────────── */ @@ -43,6 +51,9 @@ function createFakeOrchestrator(opts?: { snapshot?: OrchestratorSnapshot; getSna start: vi.fn(), run: vi.fn(), runNextPhase: vi.fn(), + // W5-3b: OwnedOrchestrator offline yüzeyi (W5-1 wiring bunları ÇAĞIRMAZ — trigger ayrı). + runOfflinePass: async () => FAKE_OFFLINE_SUMMARY, + cancelOfflinePass: () => { /* no-op */ }, }; } diff --git a/src/platform/system/SystemBoot.ts b/src/platform/system/SystemBoot.ts index 555e5e1..b0486be 100644 --- a/src/platform/system/SystemBoot.ts +++ b/src/platform/system/SystemBoot.ts @@ -39,7 +39,7 @@ import { startPlatformCoreVehicleHalWiring } from './platformCoreVehicleHalWirin import { startPlatformCoreVehicleHalBridgeWiring } from './platformCoreVehicleHalBridgeWiring'; import { startPlatformCoreCapabilityWiring } from './platformCoreCapabilityWiring'; import { startPlatformCoreCapabilityBridgeWiring } from './platformCoreCapabilityBridgeWiring'; -import { startPlatformCoreDeepScanWiring } from './platformCoreDeepScanWiring'; +import { startPlatformCoreDeepScanWiring, triggerDeepScanOfflinePass } from './platformCoreDeepScanWiring'; import { startPlatformCoreEventBusWiring, publishRuntimeStarted, @@ -658,6 +658,14 @@ class SystemBoot { startCognitiveEngine(); this._startLimpMonitor(); + // Platform Core: Deep Scan offline pass TRIGGER (W5-3b). Wave 2'deki ownership wiring + // KURULDUKTAN SONRA, tek deterministik giriş noktasından offline pass'i EN FAZLA bir kez + // tetikler (hash/dedup guard). HANDLER YOK → gerçek iş yapılmaz, aktif ECU/PID/DID sorgusu + // YOK, üretim davranışı değişmez (runtime idle→running→idle). Fire-and-forget: boot'u + // BLOKLAMAZ; trigger fail-soft (dışarı throw kaçırmaz), yine de savunmacı .catch. + _log(' › Deep Scan offline pass trigger (Platform Core)'); + void triggerDeepScanOfflinePass().catch((e: unknown) => logError('SystemBoot:deepScanOfflineTrigger', e)); + _log('Wave 3 ready ✓'); } diff --git a/src/platform/system/platformCoreDeepScanWiring.ts b/src/platform/system/platformCoreDeepScanWiring.ts index fd5db10..48ecc4d 100644 --- a/src/platform/system/platformCoreDeepScanWiring.ts +++ b/src/platform/system/platformCoreDeepScanWiring.ts @@ -1,10 +1,18 @@ /** - * platformCoreDeepScanWiring — Deep Scan RUNTIME OWNERSHIP WIRING (W5-1). + * platformCoreDeepScanWiring — Deep Scan RUNTIME OWNERSHIP WIRING (W5-1) + * + OFFLINE PASS TRIGGER (W5-3b). * * AMAÇ: Daha önce foundation olarak hazırlanmış Deep Scan singleton'larını (runtime / * persistence / ignition source) üretim yaşam döngüsüne bağlar ve bunları besleyen bir * `DeepScanOrchestrator` örneğini DI fabrikasıyla OLUŞTURUP SAHİPLENİR. Yalnız SAHİPLİK + - * erişim + cleanup + bounded status. Zincir yalnız KURULUR — ÇALIŞTIRILMAZ. + * erişim + cleanup + bounded status. Aktif tarama zinciri yalnız KURULUR — ÇALIŞTIRILMAZ. + * + * W5-3b (offline trigger): `runOfflinePass()` İLK kez üretim wiring'ine bağlanır — + * `triggerDeepScanOfflinePass()` tek deterministik giriş noktasıdır (hash/dedup guard, + * single-flight). HANDLER YOK (W5-3c ayrı PR) → tüm offline fazlar `skipped` → GERÇEK + * İŞ YAPILMAZ, üretim davranışı DEĞİŞMEZ. Aktif ECU/PID/DID/Firmware sorgusu YOK; change + * detection / knowledge / fingerprint / evidence / report ÇALIŞMAZ; runtime + * `idle → running → idle` döner (pass sonunda `runtime.reset()` W5-3a garantisi). * * NE YAPMAZ (bilinçli — W5-1 yalnız ownership'tir): * - `start()`/`run()`/`runNextPhase()` ÇAĞIRMAZ → hiçbir tarama başlamaz, hiçbir faz yürümez. @@ -48,11 +56,25 @@ import { type DeepScanStatus, type DeepScanMode, type DeepScanPhase, + type OfflinePassInput, + type OfflinePassSummary, + type OfflinePassBlockedReason, } from '../deepScan'; -/** Wiring'in SAHİPLENDİĞİ orchestrator — yalnız gereken salt-okunur yüzey (yapısal). */ +/** + * Wiring'in SAHİPLENDİĞİ orchestrator — yalnız gereken salt-okunur yüzey (yapısal). + * + * ⚠️ Aktif tarama API'leri (`start`/`run`/`runNextPhase`) BİLİNÇLİ OLARAK YOK → + * compile-time garanti: bu wiring hiçbir AKTİF tarama (araca sorgu) başlatamaz. + * W5-3b'de yalnız `runOfflinePass` (araca sorgu GÖNDERMEYEN offline yüzey) + iptal + * eklendi; trigger onu çağırır. Offline yüzey aktif tarama DEĞİLDİR. + */ export interface OwnedOrchestrator { getSnapshot(): OrchestratorSnapshot; + /** W5-3b: offline (non-active) pass yüzeyi — deterministik trigger tarafından çağrılır. */ + runOfflinePass(input?: OfflinePassInput): Promise; + /** Yürüyen offline pass'i iptal eder (fail-soft; pass yoksa no-op). */ + cancelOfflinePass(): void; dispose(): void; readonly isDisposed: boolean; } @@ -93,12 +115,102 @@ export interface DeepScanWiringStatus { readonly lastTransitionAt: number | null; } +/* ══════════════════════════════════════════════════════════════════════════ + * W5-3b — OFFLINE PASS TRIGGER + BOUNDED DIAGNOSTICS + * + * `runOfflinePass()` İLK kez üretim wiring'ine bağlanır: tek deterministik giriş + * noktası (`triggerDeepScanOfflinePass`) hash/dedup guard'ıyla pass'i EN FAZLA bir + * kez başlatır. HANDLER YOK (W5-3c) → tüm fazlar `skipped` → GERÇEK İŞ YAPILMAZ → + * üretim davranışı DEĞİŞMEZ. Aktif ECU/PID/DID/Firmware sorgusu YOK (offline yüzey). + * ════════════════════════════════════════════════════════════════════════ */ + +/** Offline pass tetikleme sonucu (bounded — VIN/GPS/secret/ham CAN-OBD TAŞIMAZ). */ +export type OfflinePassResult = 'ran' | 'blocked'; + +/** Bounded teşhis görünümü — offline pass tetikleme durumu. Ham telemetri TAŞIMAZ. */ +export interface DeepScanOfflinePassStatus { + /** Wiring aktif ve trigger çağrılabilir mi. */ + readonly present: boolean; + /** En az bir kez tetiklenmeye çalışıldı mı. */ + readonly started: boolean; + /** Şu an bir pass yürüyor mu (wiring bookkeeping). */ + readonly running: boolean; + /** Orchestrator canlı + çağrılabilir mi (dispose edilmemiş). */ + readonly active: boolean; + /** Son pass iptal edildi mi. */ + readonly cancelled: boolean; + /** Kaç kez tetiklendi (dedup kanıtı — guard çalışıyorsa ≤1 kalır). */ + readonly triggerCount: number; + /** Son pass başlangıç zamanı (monotonik değil; yalnız teşhis). */ + readonly lastRun: number | null; + readonly lastDuration: number | null; + readonly lastResult: OfflinePassResult | null; + /** `blocked` ise sebep (offline pass yüzeyinden). */ + readonly lastReason: OfflinePassBlockedReason | null; + /** Temizlenmiş hata kodu (yoksa null). Ham veri TAŞIMAZ. */ + readonly lastError: 'trigger_failed' | null; + readonly phaseCount: number; + readonly summaryAvailable: boolean; +} + +/** Deterministik guard varsayılan anahtarı — boot'ta pass EN FAZLA bir kez tetiklenir. */ +export const DEEP_SCAN_OFFLINE_TRIGGER_KEY = 'deepscan.offline.boot'; + +/** Wiring örneğine bağlı (restart'ta doğal sıfırlanan) mutable pass durumu. */ +interface OfflinePassWiringState { + triggeredKey: string | null; + triggerCount: number; + running: boolean; + cancelled: boolean; + lastRun: number | null; + lastDuration: number | null; + lastResult: OfflinePassResult | null; + lastReason: OfflinePassBlockedReason | null; + lastError: 'trigger_failed' | null; + lastPhaseCount: number; + lastSummaryAvailable: boolean; +} + +function _freshOfflineState(): OfflinePassWiringState { + return { + triggeredKey: null, + triggerCount: 0, + running: false, + cancelled: false, + lastRun: null, + lastDuration: null, + lastResult: null, + lastReason: null, + lastError: null, + lastPhaseCount: 0, + lastSummaryAvailable: false, + }; +} + +const IDLE_OFFLINE_STATUS: DeepScanOfflinePassStatus = Object.freeze({ + present: false, + started: false, + running: false, + active: false, + cancelled: false, + triggerCount: 0, + lastRun: null, + lastDuration: null, + lastResult: null, + lastReason: null, + lastError: null, + phaseCount: 0, + summaryAvailable: false, +}); + const NOOP_CLEANUP: DeepScanWiringCleanup = () => { /* no-op */ }; interface ActiveWiring { readonly orchestrator: OwnedOrchestrator; readonly ignition: DeepScanIgnitionSource; readonly startedAt: number; + readonly now: () => number; + readonly offline: OfflinePassWiringState; } let _active: ActiveWiring | null = null; @@ -149,7 +261,7 @@ export function startPlatformCoreDeepScanWiring(deps: DeepScanWiringDeps = {}): let startedAt = 0; try { const n = now(); startedAt = Number.isFinite(n) ? n : 0; } catch { startedAt = 0; } - active = { orchestrator, ignition, startedAt }; + active = { orchestrator, ignition, startedAt, now, offline: _freshOfflineState() }; _active = active; _lastErrorCode = null; @@ -204,3 +316,102 @@ export function getDeepScanWiringStatus(): DeepScanWiringStatus { return IDLE_STATUS; // teşhis yolu asla çökmez } } + +/* ══════════════════════════════════════════════════════════════════════════ + * W5-3b — Offline pass trigger (tek deterministik giriş noktası) + teşhis + * ════════════════════════════════════════════════════════════════════════ */ + +/** + * Deep Scan offline pass'i TEK giriş noktasından, deterministik dedup guard'ıyla + * tetikler. Boot'ta yalnız uygun koşul (wiring aktif) varsa çağrılır ve aynı guard + * anahtarı için EN FAZLA BİR KEZ çalışır (tekrar/duplicate YOK). Handler bağlanmadığı + * için (W5-3c) pass GERÇEK İŞ YAPMAZ → üretim davranışı değişmez; runtime + * `idle → running → idle` döner (pass sonunda `runtime.reset()` W5-3a garantisi). + * + * Fail-soft: wiring yoksa / zaten yürüyorsa / aynı guard tekrar gelirse → `null` (no-op). + * Dışarı EXCEPTION KAÇIRMAZ. Boot'u BLOKLAMAZ (çağıran await etmeyebilir). + * + * @returns pass özeti; tetiklenmediyse (guard/no-op) `null`. + */ +export async function triggerDeepScanOfflinePass( + opts: { readonly triggerKey?: string; readonly phaseTimeoutMs?: number } = {}, +): Promise { + _pruneStale(); + const a = _active; + if (!a) return null; // wiring yok → tetiklenemez + const st = a.offline; + if (st.running) return null; // SINGLE-FLIGHT: yürüyen pass varken tetiklenmez + + // DETERMİNİSTİK DEDUP GUARD: aynı anahtar ikinci kez pass BAŞLATMAZ. + const key = typeof opts.triggerKey === 'string' && opts.triggerKey.length > 0 + ? opts.triggerKey + : DEEP_SCAN_OFFLINE_TRIGGER_KEY; + if (st.triggeredKey === key) return null; // aynı guard → tekrar YOK + + st.triggeredKey = key; + st.triggerCount += 1; + st.running = true; + st.cancelled = false; + + let startedAt = 0; + try { const n = a.now(); startedAt = Number.isFinite(n) ? n : 0; } catch { startedAt = 0; } + st.lastRun = startedAt; + + try { + // Handler GEÇİLMEZ → tüm offline fazlar `skipped`; aktif-kayıt yolu tetiklenmez. + // Vehicle fingerprint verilmez (HAL/fingerprint bu PR'da bağlanmaz). + const summary = await a.orchestrator.runOfflinePass({ phaseTimeoutMs: opts.phaseTimeoutMs }); + st.lastResult = summary.ran ? 'ran' : 'blocked'; + st.lastReason = summary.blockedReason; + st.lastPhaseCount = summary.phaseCount; + st.cancelled = summary.cancelled; + st.lastSummaryAvailable = true; + st.lastError = null; + let doneAt = startedAt; + try { const n = a.now(); doneAt = Number.isFinite(n) ? n : startedAt; } catch { doneAt = startedAt; } + st.lastDuration = Math.max(0, doneAt - startedAt); + return summary; + } catch (e) { + st.lastResult = 'blocked'; + st.lastError = 'trigger_failed'; + st.lastSummaryAvailable = false; + logError('deepScanWiring:offlineTrigger', e); // ham telemetri/VIN LOGLANMAZ + return null; + } finally { + st.running = false; // ZERO-LEAK: her yolda bookkeeping temizlenir + } +} + +/** Yürüyen offline pass'i iptal eder (fail-soft; wiring/pass yoksa no-op). */ +export function cancelDeepScanOfflinePass(): void { + _pruneStale(); + const a = _active; + if (!a) return; + a.offline.cancelled = true; + try { a.orchestrator.cancelOfflinePass(); } catch (e) { logError('deepScanWiring:offlineCancel', e); } +} + +/** Bounded teşhis görünümü — offline pass tetikleme durumu. Throw ETMEZ; payload TAŞIMAZ. */ +export function getDeepScanOfflinePassStatus(): DeepScanOfflinePassStatus { + _pruneStale(); + const a = _active; + if (!a) return IDLE_OFFLINE_STATUS; + const st = a.offline; + let active = false; + try { active = !a.orchestrator.isDisposed; } catch { active = false; } + return Object.freeze({ + present: true, + started: st.triggerCount > 0, + running: st.running, + active, + cancelled: st.cancelled, + triggerCount: st.triggerCount, + lastRun: st.lastRun, + lastDuration: st.lastDuration, + lastResult: st.lastResult, + lastReason: st.lastReason, + lastError: st.lastError, + phaseCount: st.lastPhaseCount, + summaryAvailable: st.lastSummaryAvailable, + }); +}