Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/DEVICE_VALIDATION_LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

| # | Özellik | Nerede | Kabul ölçütü (cihazda ne gözlemlenmeli) | Eklendi |
|---|---------|--------|------------------------------------------|---------|
| 26 | **🔴 Device Validation Pending — Evidence Write Bridge (P2-6)** — P2-1→P2-5 öğrenme katmanları kuruldu ama Evidence Store'a runtime YAZICI yoktu (Integration Service yalnız `list()` okuyordu) → learning rozetleri / Expert özeti / diagnostic learning context BOŞ kalıyordu. Bu köprü eksik write-path'i bağlar: VKB güncellenince (discovery gözlemi + VID kimlik değişimi abonelikleri, debounce'lu cold-path) `vehicleLearningEngine.computeEvidence()` çağrılır → üretilen kanıtlar Evidence Store'a `save()` (idempotent reproject — createdAt korunur, observationCount ŞİŞMEZ, vehicleCount distinct) ile yazılır → Pattern/Integration Service `list()` ile okuyabilir. 3Hz OBD hot-path'e GİRMEZ; yalnız anlamlı kimlik/discovery değişiminde tetiklenir; BALANCED/HIGH 8sn, BASIC_JS(low) 20sn debounce; kapanışta flush + dispose; fail-soft (köprü hatası OBD/Discovery'ye SIZMAZ); bounded(store 512 LRU) + throttle'lı yazma + zero-leak. Confidence/decay/pattern-promotion FORMÜLLERİ · Discovery capture/queue · Fingerprint · Auto Learning · VKB · Manufacturer Intelligence · Diagnostic safety/severity/driveSafe · PID/DID registry DEĞİŞMEZ. Cloud/SQL/LLM/Native YOK. | `vehicleLearningEvidenceBridge.ts` (yeni), `system/SystemBoot.ts` (VKB'den SONRA cold-path wiring, `_reg` cleanup), `vehicleLearningEvidenceBridge.test.ts` (yeni, 12 test) | Gerçek araçta: (1) discovery sonrası Evidence Store DOLUYOR (list() boş değil); (2) uygulama restart sonrası evidence geri geliyor (kalıcı); (3) Dashboard learning rozeti görünüyor; (4) Expert "Araç Öğrenme" sayıları >0; (5) Diagnostic learning context doluyor; (6) duplicate gözlem vehicleCount'i ŞİŞİRMİYOR (distinct fingerprint); (7) BASIC_JS tier'da FPS/CPU regresyonu YOK (seyrek debounce + cold-path). ⚠️ Test edilmeden 🟢'a çekme. | 2026-07-10 |
| 25 | **🔴 Device Validation Pending — Learning Integration Layer (P2-5)** — P2-1→P2-4 öğrenme katmanları (Evidence Store · Decay · Pattern Engine) uygulamanın mevcut ekran/tanı akışlarına SALT-OKUNUR bağlandı. `vehicleLearningIntegrationService` Evidence Store'dan okur → decay uygular → pattern üretir → (a) Discovery Dashboard'a WEAK/CANDIDATE/STRONG/MANUAL_REVIEW/STALE/CONFLICT rozetleri + confidence/decay/araç/gözlem/ECU alanları + Strong/Candidate/Manual/Conflict filtreleri ekler, (b) Expert Mode'a "Araç Öğrenme" özet bölümü (toplam/weak/candidate/strong/manual/stale/prune + güçlü PID/DID adayları + marka cluster + son öğrenme) ekler, (c) Diagnostic Insight için `enrichInsight` ile öğrenme bağlamı üretir. Hiçbir registry/profile/store'a OTOMATİK YAZMAZ; safety/severity/driveSafe DEĞİŞMEZ; memoized + bounded(512) + fail-soft + zero-leak. BASIC_JS(low) tier yalnız özet istatistik (ağır pattern/conflict detayı kapalı). | `vehicleLearningIntegrationService.ts` (yeni), `discoveryDashboardModel.ts` (öğrenme filtreleri + AnnotationResolver), `DiscoveryDashboard.tsx` (rozet/alan/filtre), `settings/expert/VehicleLearningInspector.tsx` (yeni), `ExpertModePanel.tsx` (bölüm wiring) | Gerçek araçta: (1) yeni PID/DID discovery oluşur → Evidence Store kaydı oluşur → Dashboard'da doğru WEAK/CANDIDATE/STRONG rozeti görünür; (2) duplicate gözlem vehicleCount'i şişirmez (distinct fingerprint); (3) Expert "Araç Öğrenme" özeti doğru sayıları gösterir; (4) Diagnostic Insight öğrenme bağlamını (learnedEvidenceCount/relatedStrongPids/requiresManualReview) gösterir; (5) düşük cihaz tier'ında (BASIC_JS) FPS/CPU regresyonu oluşmaz (ağır pattern detayı kapalı). ⚠️ Salt-okunur/additive → alt katman davranışı değişmez (tsc+test+build yeşil). | 2026-07-10 |
| 24 | **🔴 Device Validation Pending — P0-Güvenlik #4: production log hijyeni** (commit 049e290). terser `drop_console:true` (error+warn dahil TÜM console'u siliyor → üretimde hata gözlemlenebilirliği yoktu + logGate ölü kod) yerine dev-aware kesim: release'te yalnız `log/info/debug/trace` çıkarılır, `error`+`warn` KORUNUR (logGate runtime'da adaptif susturur). `NODE_ENV=development` build hiçbir console'ı çıkarmaz. Build-analiz kanıtı: release bundle'da `console.error(`/`warn(` var, `console.log(`/`info(`/`debug(` **çağrı = 0**; dev bundle'da log/info çağrıları korunur. | `vite.config.ts` (`_isDevBuild` + `drop_console: _isDevBuild ? false : ['log','info','debug','trace']`) | **Release APK** cihaz-üstü (debug build ile Chrome inspector): bir hata durumunda `console.error` GÖRÜNÜR (üretim gözlemlenebilirliği geri geldi); debug seviye log GÖRÜNMEZ. Debug APK'da tüm console akar (on-device debug korundu). Hot-path'te debug log kaynaklı ek CPU/IO gözlenmez. | 2026-07-08 |
| 23 | **🔴 Device Validation Pending — P0-Güvenlik #2: cleartext trafik kısıtlama** (commit 77e5292). Global `android:usesCleartextTraffic="true"` kaldırıldı; `network_security_config` base-config ile tüm cleartext reddedilir, yalnız loopback (`localhost`/`127.0.0.1`) whitelist'te. Ölü dev IP `192.168.1.12` çıkarıldı. Ham `ServerSocket`'ler (passenger 8765 / diag 8899) bu politikadan etkilenmez. | `AndroidManifest.xml`, `res/xml/network_security_config.xml` | **Release APK**'da dış **https** API'ler (hava/Nominatim/Supabase/AI) çalışmaya devam eder; kasıtlı bir `http://` dış çağrı NSC ile REDDEDİLİR; cihaz-içi OSRM (`http://localhost:5000`) offline routing + yolcu paneli (ayrı telefonun tarayıcısı) çalışır. | 2026-07-08 |
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/cleanup.remote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ vi.mock('../utils/safeStorage', () => ({
safeFlushKey: () => {},
safeGetRaw: () => null,
safeSetRaw: () => {},
safeRemoveRaw: () => {},
}));

/* ── Imports (mock'lardan sonra) ── */
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/cleanup.services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ vi.mock('../utils/safeStorage', () => ({
safeFlushKey: () => {},
safeGetRaw: () => null,
safeSetRaw: () => {},
safeRemoveRaw: () => {},
}));

/* ── Imports (mock'lardan sonra) ── */
Expand Down
294 changes: 294 additions & 0 deletions src/__tests__/vehicleLearningEvidenceBridge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/**
* vehicleLearningEvidenceBridge.test.ts — Evidence Write Bridge (P2-6).
*
* Kilitler: değişimde computeEvidence çağrılıyor · store'a yazılıyor · duplicate kayıt yok ·
* aynı araç vehicleCount şişmiyor · observationCount doğru · debounce disk yazımını sınırlıyor ·
* BASIC_JS seyrek / BALANCED normal · boş+bozuk fail-soft · hata upstream'e sızmıyor ·
* stop/dispose cleanup · flush kapanışta · start idempotent (duplicate subscription yok).
*/
import { describe, it, expect } from 'vitest';
import {
VehicleLearningEvidenceBridge,
BRIDGE_DEBOUNCE_MS_NORMAL,
BRIDGE_DEBOUNCE_MS_LOW,
type BridgeTimer,
} from '../platform/vehicleLearningEvidenceBridge';
import {
VehicleLearningEvidenceStore,
type EvidenceStoreIO,
} from '../platform/vehicleLearningEvidenceStore';
import { type LearningEvidence } from '../platform/vehicleLearningEngine';

/* ── Yardımcılar ──────────────────────────────────────────────────────────── */

/** Enjekte edilebilir manuel zamanlayıcı — test fire()'ı çağırınca callback çalışır. */
function manualTimer() {
let scheduled: { cb: () => void; ms: number } | null = null;
let setCount = 0, clearCount = 0;
const timer: BridgeTimer = {
set: (cb, ms) => { scheduled = { cb, ms }; setCount++; return { id: setCount }; },
clear: () => { scheduled = null; clearCount++; },
};
return {
timer,
fire: () => { const s = scheduled; scheduled = null; s?.cb(); },
pending: () => scheduled,
get setCount() { return setCount; },
get clearCount() { return clearCount; },
};
}

/** Kaynak yakalayıcı — abone callback'lerini toplar, unsub sayar. */
function captureSource() {
const cbs: Array<() => void> = [];
let subCount = 0, unsubCount = 0;
const source = (cb: () => void) => { cbs.push(cb); subCount++; return () => { unsubCount++; }; };
return {
source,
trigger: () => { for (const cb of cbs) cb(); },
get subCount() { return subCount; },
get unsubCount() { return unsubCount; },
};
}

function ev(over: Partial<LearningEvidence> = {}): LearningEvidence {
return {
evidenceId: over.evidenceId ?? 'Renault|6|PID|A5|01',
manufacturer: 'Renault', profileHint: 'Renault', protocol: '6',
discoverySource: 'PID', pidOrDid: 'A5', mode: '01',
ecuAddresses: over.ecuAddresses ?? ['7E8'],
supportingVehicleHashes: over.supportingVehicleHashes ?? ['v1'],
vehicleCount: over.vehicleCount ?? 1,
observationCount: over.observationCount ?? 5,
firstSeen: over.firstSeen ?? 1000,
lastSeen: over.lastSeen ?? 2000,
confidence: over.confidence ?? 0.4,
status: over.status ?? 'weak',
createdAt: over.createdAt ?? 1000,
updatedAt: over.updatedAt ?? 2000,
};
}

/** Bellek-içi IO ile gerçek Evidence Store (debounce 0 → anında yazım testte kolay). */
function memStore(): VehicleLearningEvidenceStore {
const mem = new Map<string, string>();
const io: EvidenceStoreIO = {
read: (k) => mem.get(k) ?? null,
write: (k, v) => { mem.set(k, v); },
remove: (k) => { mem.delete(k); },
};
return new VehicleLearningEvidenceStore('test-evidence', 512, 0, io, () => 5000);
}

/* ── Tetikleme & yazım ────────────────────────────────────────────────────── */
describe('trigger + write', () => {
it('değişim → computeEvidence çağrılıyor + store’a yazılıyor', () => {
const t = manualTimer();
const src = captureSource();
let computeCalls = 0;
const written: LearningEvidence[] = [];
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => { computeCalls++; return [ev()]; },
writeEvidence: (e) => { written.push(e); },
subscribeSources: [src.source],
timer: t.timer,
});
b.start();
expect(computeCalls).toBe(0); // boot'ta debounce'lu (henüz çalışmadı)
src.trigger(); // VKB/discovery değişimi
t.fire(); // debounce doldu
expect(computeCalls).toBe(1);
expect(written).toHaveLength(1);
expect(written[0].evidenceId).toBe('Renault|6|PID|A5|01');
b.stop();
});

it('gerçek store: duplicate kayıt YOK + aynı araç vehicleCount ŞİŞMİYOR + observationCount doğru', () => {
const t = manualTimer();
const store = memStore();
let obs = 5;
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => [ev({ observationCount: obs, supportingVehicleHashes: ['v1'] })],
writeEvidence: (e) => { store.save(e); },
subscribeSources: [captureSource().source],
timer: t.timer,
});
b.start();
b.flush(); // 1. projeksiyon (obs=5)
b.flush(); // 2. projeksiyon AYNI veri (idempotent — şişmemeli)
let list = store.list();
expect(list).toHaveLength(1); // duplicate kayıt yok
expect(list[0].vehicleCount).toBe(1); // aynı araç → şişmedi
expect(list[0].observationCount).toBe(5); // tekrar yazımda ŞİŞMEDİ (save reproject)

obs = 9; // VKB büyüdü (computeEvidence daha yüksek obs verir)
b.flush();
list = store.list();
expect(list).toHaveLength(1);
expect(list[0].observationCount).toBe(9); // doğru arttı
b.dispose();
});

it('farklı araç → vehicleCount computeEvidence’ten yansıyor', () => {
const t = manualTimer();
const store = memStore();
let hashes = ['v1'];
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => [ev({ supportingVehicleHashes: hashes, vehicleCount: hashes.length })],
writeEvidence: (e) => { store.save(e); },
subscribeSources: [captureSource().source],
timer: t.timer,
});
b.start(); b.flush();
expect(store.list()[0].vehicleCount).toBe(1);
hashes = ['v1', 'v2']; // 2. farklı araç öğrenildi
b.flush();
expect(store.list()[0].vehicleCount).toBe(2); // farklı araçta arttı
b.dispose();
});
});

/* ── Throttle / debounce ──────────────────────────────────────────────────── */
describe('throttle / debounce', () => {
it('art arda sinyaller TEK projeksiyona indirgeniyor (disk yazımı sınırlı)', () => {
const t = manualTimer();
const src = captureSource();
let computeCalls = 0;
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => { computeCalls++; return [ev()]; },
writeEvidence: () => {},
subscribeSources: [src.source],
timer: t.timer,
});
b.start(); // 1 schedule
src.trigger(); // +1
src.trigger(); // +1
src.trigger(); // +1
expect(t.setCount).toBe(4); // her sinyal yeniden planladı
expect(t.clearCount).toBe(3); // öncekiler iptal (coalesce)
expect(computeCalls).toBe(0); // henüz çalışmadı
t.fire();
expect(computeCalls).toBe(1); // 4 sinyal → 1 projeksiyon
b.stop();
});

it('BASIC_JS(low) DAHA SEYREK, BALANCED/HIGH normal', () => {
const low = manualTimer();
const high = manualTimer();
const bLow = new VehicleLearningEvidenceBridge({ computeEvidence: () => [], timer: low.timer, tier: () => 'low', subscribeSources: [] });
const bHigh = new VehicleLearningEvidenceBridge({ computeEvidence: () => [], timer: high.timer, tier: () => 'high', subscribeSources: [] });
bLow.start();
bHigh.start();
expect(low.pending()!.ms).toBe(BRIDGE_DEBOUNCE_MS_LOW);
expect(high.pending()!.ms).toBe(BRIDGE_DEBOUNCE_MS_NORMAL);
expect(BRIDGE_DEBOUNCE_MS_LOW).toBeGreaterThan(BRIDGE_DEBOUNCE_MS_NORMAL);
bLow.stop(); bHigh.stop();
});
});

/* ── Fail-soft ────────────────────────────────────────────────────────────── */
describe('fail-soft', () => {
it('boş knowledge → fail-soft (yazım yok, çökme yok)', () => {
const t = manualTimer();
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => [], writeEvidence: () => { throw new Error('yazılmamalı'); },
subscribeSources: [captureSource().source], timer: t.timer,
});
b.start();
expect(() => b.flush()).not.toThrow();
expect(b.lastWriteCount).toBe(0);
b.stop();
});

it('bozuk evidence → yalnız geçerli yazılır (fail-soft)', () => {
const t = manualTimer();
const written: LearningEvidence[] = [];
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => ([null, { evidenceId: '' }, ev({ evidenceId: 'ok' })] as unknown as LearningEvidence[]),
writeEvidence: (e) => { written.push(e); },
subscribeSources: [captureSource().source], timer: t.timer,
});
b.start(); b.flush();
expect(written).toHaveLength(1);
expect(written[0].evidenceId).toBe('ok');
b.stop();
});

it('köprü hatası OBD/Discovery akışına SIZMIYOR (computeEvidence throw)', () => {
const t = manualTimer();
const src = captureSource();
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => { throw new Error('patladı'); },
writeEvidence: () => {},
subscribeSources: [src.source], timer: t.timer,
});
b.start();
expect(() => src.trigger()).not.toThrow(); // sinyal callback'i çökmez
expect(() => t.fire()).not.toThrow(); // projeksiyon fail-soft
b.stop();
});

it('bir kaynak subscribe atarsa diğerleri çalışır', () => {
const t = manualTimer();
const good = captureSource();
const bad = () => { throw new Error('subscribe hata'); };
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => [], writeEvidence: () => {},
subscribeSources: [bad, good.source], timer: t.timer,
});
expect(() => b.start()).not.toThrow();
expect(good.subCount).toBe(1); // iyi kaynak yine abone oldu
b.stop();
});
});

/* ── Lifecycle ────────────────────────────────────────────────────────────── */
describe('lifecycle', () => {
it('start İDEMPOTENT — ikinci start duplicate subscription oluşturmuyor', () => {
const t = manualTimer();
const src = captureSource();
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => [], writeEvidence: () => {},
subscribeSources: [src.source], timer: t.timer,
});
b.start();
b.start(); // ikinci — abone açmamalı
expect(src.subCount).toBe(1);
expect(b.isRunning).toBe(true);
b.stop();
});

it('stop cleanup: unsubscribe + timer temizler', () => {
const t = manualTimer();
const src = captureSource();
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => [], writeEvidence: () => {},
subscribeSources: [src.source], timer: t.timer,
});
b.start();
b.stop();
expect(src.unsubCount).toBe(1);
expect(b.isRunning).toBe(false);
expect(t.pending()).toBeNull();
});

it('dispose kapanışta FLUSH ediyor + unsubscribe', () => {
const t = manualTimer();
const src = captureSource();
let computeCalls = 0;
const written: LearningEvidence[] = [];
const b = new VehicleLearningEvidenceBridge({
computeEvidence: () => { computeCalls++; return [ev()]; },
writeEvidence: (e) => { written.push(e); },
subscribeSources: [src.source], timer: t.timer,
});
b.start();
b.dispose();
expect(computeCalls).toBe(1); // kapanışta final flush projeksiyonu
expect(written).toHaveLength(1);
expect(src.unsubCount).toBe(1); // abonelik bırakıldı
// dispose sonrası schedule no-op (yeni timer planlanmaz)
src.trigger();
expect(t.pending()).toBeNull();
});
});
7 changes: 7 additions & 0 deletions src/platform/system/SystemBoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { startVehicleIntelligenceService } from '../vehicleIntelligenceService';
import { startAutomaticVehicleFingerprint } from '../vehicleFingerprintBuilder';
import { startAutoLearningEngine } from '../autoLearningEngine';
import { startVehicleKnowledgeBase } from '../vehicleKnowledgeBase';
import { startVehicleLearningEvidenceBridge } from '../vehicleLearningEvidenceBridge';
import { logError } from '../crashLogger';
import { showToast, dismissToast } from '../errorBus';
import { healthMonitor } from './SystemHealthMonitor';
Expand Down Expand Up @@ -542,6 +543,12 @@ class SystemBoot {
_log(' › VehicleKnowledgeBase');
this._reg(startVehicleKnowledgeBase());

// VehicleLearningEvidenceBridge (P2-6): VKB güncellenince computeEvidence() → Evidence
// Store'a idempotent yazar (debounce'lu cold-path). VKB'DEN SONRA başlar (ona bağlı).
// 3Hz hot-path'e girmez; fail-soft + zero-leak (cleanup _reg'le).
_log(' › VehicleLearningEvidenceBridge');
this._reg(startVehicleLearningEvidenceBridge());

// GeofenceService: async (Supabase zona sorgusu)
_log(' › GeofenceService (async)');
const geofenceCleanup = await startGeofenceService().catch((e: unknown) => {
Expand Down
Loading
Loading