From 643ff407a6367c010ed95194cd1900f32cfc15d7 Mon Sep 17 00:00:00 2001 From: Selim Date: Thu, 9 Jul 2026 21:16:31 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(vehicle):=20otomatik=20=C3=B6=C4=9Fren?= =?UTF-8?q?me=20motoru=20temeli=20(PR-27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vehicle Fingerprint (PR-25/26) ile Discovery'yi (PR-DISC) birleştirir: sistem, bir araçta tekrar görülen katalog-dışı PID/DID'leri o aracın parmak izine BAĞLAYARAK öğrenmeye başlar → kendi kendini geliştiren YEREL bilgi tabanının çekirdeği. - autoLearningEngine.ts (yeni): * VehicleKnowledge = fingerprint ÜSTÜNE discoveredPids/discoveredDids (her sinyal: firstSeen/lastSeen/seenCount/confidence) * applyObservationsToKnowledge: discovery gözlemlerini bilgiye işler (idempotent mirror — firstSeen korunur, lastSeen/seenCount/confidence güncellenir) * isLikelySameVehicle + mergeKnowledge: PR-26 "staged VIN" çözümü — VIN'siz kayıt VIN gelince aynı araç anlaşılırsa OTOMATİK merge, duplicate bırakmaz * AutoLearningEngine: discovery + useVidStore aboneliği, FAIL-SOFT - SystemBoot Wave-3: startAutoLearningEngine() additive wiring + _reg cleanup Yalnız YEREL bilgi (cloud/AI YOK). Dokunulmadı: native OBD · poll · Discovery Queue/Capture · PID/DID Registry · SQL/Supabase · cloud sync. Foundation/builder tipleri DEĞİŞMEDİ (bilgi katmanı depoda JSON olarak şeffaf saklanır). 17 yeni test (seenCount/confidence↑ · VIN merge · duplicate yok · firstSeen korunur · lastSeen güncellenir · farklı araçlar birleşmez · fail-soft · OBD/Discovery salt-okunur). tsc temiz · eslint temiz · 2437 test yeşil (yalnız 23 bilinen env-gated, ilgisiz) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Fauew13VP3vbfKqnocPdfX --- src/__tests__/autoLearningEngine.test.ts | 259 ++++++++++++++++++++ src/platform/autoLearningEngine.ts | 293 +++++++++++++++++++++++ src/platform/system/SystemBoot.ts | 6 + 3 files changed, 558 insertions(+) create mode 100644 src/__tests__/autoLearningEngine.test.ts create mode 100644 src/platform/autoLearningEngine.ts diff --git a/src/__tests__/autoLearningEngine.test.ts b/src/__tests__/autoLearningEngine.test.ts new file mode 100644 index 00000000..f14daf98 --- /dev/null +++ b/src/__tests__/autoLearningEngine.test.ts @@ -0,0 +1,259 @@ +/** + * autoLearningEngine.test.ts — Otomatik Öğrenme Motoru TEMELİ (PR-27). + * + * Kilitlenen davranışlar: aynı PID/DID tekrar → seenCount↑ · confidence↑ · firstSeen korunur · + * lastSeen güncellenir · staged VIN merge (duplicate kalmaz) · farklı araçlar birleşmez · + * fail-soft · OBD/Discovery akışı etkilenmez (SALT-OKUNUR). + */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + deriveConfidence, + initKnowledge, + applyObservationsToKnowledge, + isLikelySameVehicle, + mergeKnowledge, + AutoLearningEngine, + type VehicleKnowledge, +} from '../platform/autoLearningEngine'; +import { ingestVehicleFingerprint } from '../platform/vehicleFingerprintBuilder'; +import { VehicleFingerprintStore, normalizeVin } from '../platform/vehicleFingerprintService'; +import { useVidStore } from '../store/useVidStore'; +import { + createDiscoveryRecord, + DiscoveryCaptureService, + DiscoveryQueue, + DiscoveryCache, + type DiscoveryObservation, + type DiscoverySource, +} from '../platform/obd/discovery'; + +let _k = 0; +function fpStore() { return new VehicleFingerprintStore(`ale-test-${_k++}`); } + +function obs(source: DiscoverySource, id: string, seenCount = 1, firstAt = 1000, lastAt = 1000, ecu = '7E8'): DiscoveryObservation { + return { + record: createDiscoveryRecord({ pidOrDid: id, discoverySource: source, ecuAddress: ecu, mode: source === 'DID' ? '22' : '01' }), + status: 'new', + seenCount, + firstAt, + lastAt, + }; +} + +/** Öğrenilebilir boş bilgi (VIN'li/VIN'siz) üretir. */ +function knowledge(over: { vin?: string; protocol?: string; ecu?: string[]; mac?: string } = {}): VehicleKnowledge { + return initKnowledge(ingestVehicleFingerprint({ + vin: over.vin ?? '', + protocol: over.protocol ?? '6', + ecuAddresses: over.ecu ?? ['7E8'], + metadata: over.mac ? { adapterMac: over.mac } : undefined, + }, fpStore(), 1000)); +} + +beforeEach(() => { + try { localStorage.clear(); } catch { /* jsdom */ } + useVidStore.getState().resetStore(); +}); + +/* ── deriveConfidence ─────────────────────────────────────────────────────── */ +describe('deriveConfidence', () => { + it('görülme arttıkça artar, tavan 1.0', () => { + expect(deriveConfidence(1)).toBeCloseTo(0.5); + expect(deriveConfidence(2)).toBeCloseTo(0.6); + expect(deriveConfidence(3)).toBeCloseTo(0.7); + expect(deriveConfidence(20)).toBe(1.0); + expect(deriveConfidence(0)).toBeCloseTo(0.5); // güvenli taban + }); +}); + +/* ── applyObservationsToKnowledge (SAF öğrenme) ───────────────────────────── */ +describe('applyObservationsToKnowledge', () => { + it('yeni PID → discoveredPids kaydı (firstSeen/lastSeen/seenCount/confidence)', () => { + const k = applyObservationsToKnowledge(knowledge(), [obs('PID', 'A5', 1, 1000, 1000)], 1000); + expect(k.discoveredPids['A5']).toEqual({ firstSeen: 1000, lastSeen: 1000, seenCount: 1, confidence: 0.5 }); + expect(k.discoveredDids['A5']).toBeUndefined(); + }); + + it('yeni DID → discoveredDids kaydı', () => { + const k = applyObservationsToKnowledge(knowledge(), [obs('DID', 'F190', 1, 1000, 1000)], 1000); + expect(k.discoveredDids['F190'].seenCount).toBe(1); + expect(k.discoveredPids['F190']).toBeUndefined(); + }); + + it('aynı PID tekrar görülünce seenCount↑, confidence↑, firstSeen korunur, lastSeen güncellenir', () => { + let k = applyObservationsToKnowledge(knowledge(), [obs('PID', 'A5', 1, 1000, 1000)], 1000); + k = applyObservationsToKnowledge(k, [obs('PID', 'A5', 2, 1000, 5000)], 5000); + expect(k.discoveredPids['A5'].seenCount).toBe(2); + expect(k.discoveredPids['A5'].confidence).toBeCloseTo(0.6); + expect(k.discoveredPids['A5'].firstSeen).toBe(1000); // korundu + expect(k.discoveredPids['A5'].lastSeen).toBe(5000); // güncellendi + expect(k.lastSeen).toBe(5000); + }); +}); + +/* ── isLikelySameVehicle ──────────────────────────────────────────────────── */ +describe('isLikelySameVehicle', () => { + it('VIN\'siz + VIN\'li, aynı protocol+ECU → aynı araç', () => { + const a = knowledge({ vin: 'VF1BM0A0H12345678', ecu: ['7E8'] }); + const b = knowledge({ vin: '', ecu: ['7E8'] }); + expect(isLikelySameVehicle(a, b)).toBe(true); + }); + it('farklı ECU → farklı araç', () => { + expect(isLikelySameVehicle(knowledge({ vin: 'X', ecu: ['7E8'] }), knowledge({ vin: '', ecu: ['7E0'] }))).toBe(false); + }); + it('farklı protocol → farklı araç', () => { + expect(isLikelySameVehicle(knowledge({ vin: 'X', protocol: '6' }), knowledge({ vin: '', protocol: '7' }))).toBe(false); + }); + it('iki farklı VIN → birleşmez', () => { + expect(isLikelySameVehicle(knowledge({ vin: 'AAA' }), knowledge({ vin: 'BBB' }))).toBe(false); + }); + it('adaptör MAC\'leri çelişirse → farklı araç', () => { + const a = knowledge({ vin: 'X', ecu: ['7E8'], mac: 'MAC1' }); + const b = knowledge({ vin: '', ecu: ['7E8'], mac: 'MAC2' }); + expect(isLikelySameVehicle(a, b)).toBe(false); + }); +}); + +/* ── mergeKnowledge ───────────────────────────────────────────────────────── */ +describe('mergeKnowledge', () => { + it('sinyalleri birleştirir (toplam seenCount, min firstSeen, max lastSeen), VIN korunur', () => { + const vinFul = knowledge({ vin: 'VF1BM0A0H12345678' }); + vinFul.discoveredPids['A5'] = { firstSeen: 3000, lastSeen: 4000, seenCount: 1, confidence: 0.5 }; + const vinLess = knowledge({ vin: '' }); + vinLess.firstSeen = 1000; + vinLess.discoveredPids['A5'] = { firstSeen: 1000, lastSeen: 2000, seenCount: 2, confidence: 0.6 }; + vinLess.discoveredDids['F190'] = { firstSeen: 1500, lastSeen: 1500, seenCount: 1, confidence: 0.5 }; + + const m = mergeKnowledge(vinFul, vinLess); + expect(normalizeVin(m.vin)).toBe('VF1BM0A0H12345678'); // survivor VIN'li + expect(m.firstSeen).toBe(1000); // min + expect(m.discoveredPids['A5'].seenCount).toBe(3); // 1 + 2 + expect(m.discoveredPids['A5'].firstSeen).toBe(1000); // min + expect(m.discoveredPids['A5'].lastSeen).toBe(4000); // max + expect(m.discoveredDids['F190'].seenCount).toBe(1); // yalnız vinLess'te + }); +}); + +/* ── AutoLearningEngine (canlı) ───────────────────────────────────────────── */ +describe('AutoLearningEngine', () => { + function connect(vin = '') { + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + if (vin) useVidStore.getState().updateVehicleInfo({ vin }); + } + function poke() { useVidStore.getState().updateTelemetryInfo({ trustScore: Math.random() }); } + + it('aynı PID tekrar görülünce seenCount + confidence artıyor', () => { + const s = fpStore(); + let observations: DiscoveryObservation[] = []; + const eng = new AutoLearningEngine(s, () => useVidStore.getState(), () => observations, () => 5000); + const stop = eng.start(); + observations = [obs('PID', 'A5', 1, 1000, 1000)]; + connect('VF1BM0A0H12345678'); // bağlantı tamam → tick + let fp = s.list()[0] as VehicleKnowledge; + expect(fp.discoveredPids['A5'].seenCount).toBe(1); + // aynı PID ikinci kez (discovery seenCount 2) + observations = [obs('PID', 'A5', 2, 1000, 6000)]; + poke(); + fp = s.list()[0] as VehicleKnowledge; + expect(fp.discoveredPids['A5'].seenCount).toBe(2); + expect(fp.discoveredPids['A5'].confidence).toBeCloseTo(0.6); + expect(fp.discoveredPids['A5'].firstSeen).toBe(1000); // korundu + stop(); + }); + + it('aynı DID tekrar görülünce seenCount artıyor', () => { + const s = fpStore(); + let observations: DiscoveryObservation[] = []; + const eng = new AutoLearningEngine(s, () => useVidStore.getState(), () => observations, () => 5000); + const stop = eng.start(); + observations = [obs('DID', '242E', 1, 1000, 1000, '7E0')]; + connect('VF1BM0A0H12345678'); + expect((s.list()[0] as VehicleKnowledge).discoveredDids['242E'].seenCount).toBe(1); + observations = [obs('DID', '242E', 3, 1000, 6000, '7E0')]; + poke(); + expect((s.list()[0] as VehicleKnowledge).discoveredDids['242E'].seenCount).toBe(3); + stop(); + }); + + it('VIN merge: staged VIN\'siz kayıt VIN gelince birleşir, duplicate kalmaz, firstSeen korunur', () => { + const s = fpStore(); + // Önce VIN'siz fingerprint (bağlantı VIN'den önce tamamlanmış) — öğrenilmiş bir PID ile. + const vinLess = ingestVehicleFingerprint({ vin: '', protocol: '6', ecuAddresses: ['7E8'] }, s, 1000); + const k = initKnowledge(s.load(vinLess.hash)!); + k.discoveredPids['A5'] = { firstSeen: 1000, lastSeen: 1000, seenCount: 2, confidence: 0.6 }; + s.save(k); + expect(s.size).toBe(1); + + const observations: DiscoveryObservation[] = [obs('PID', 'A5', 2, 1000, 5000, '7E8')]; + const eng = new AutoLearningEngine(s, () => useVidStore.getState(), () => observations, () => 5000); + const stop = eng.start(); + // ECU obs'tan (7E8) → aynı imza; VIN geldi → merge + useVidStore.getState().updateVehicleInfo({ vin: 'VF1BM0A0H12345678', make: 'Renault' }); + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + + expect(s.size).toBe(1); // DUPLICATE YOK + const survivor = s.list()[0] as VehicleKnowledge; + expect(normalizeVin(survivor.vin)).toBe('VF1BM0A0H12345678'); // VIN'li kalan + expect(survivor.firstSeen).toBe(1000); // firstSeen korundu + expect(survivor.discoveredPids['A5']).toBeDefined(); // öğrenilen bilgi taşındı + stop(); + }); + + it('farklı araçlar birleşmiyor (farklı ECU)', () => { + const s = fpStore(); + ingestVehicleFingerprint({ vin: '', protocol: '6', ecuAddresses: ['7E0'] }, s, 1000); // farklı ECU + const observations: DiscoveryObservation[] = [obs('PID', 'A5', 1, 1000, 5000, '7E8')]; + const eng = new AutoLearningEngine(s, () => useVidStore.getState(), () => observations, () => 5000); + const stop = eng.start(); + useVidStore.getState().updateVehicleInfo({ vin: 'VF1BM0A0H12345678' }); + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + expect(s.size).toBe(2); // birleşmedi — iki ayrı araç + stop(); + }); + + it('FAIL-SOFT: depo hata fırlatsa da akış çökmez', () => { + const throwing = { + load: () => { throw new Error('disk fail'); }, + save: () => { throw new Error('disk fail'); }, + list: () => { throw new Error('disk fail'); }, + remove: () => { throw new Error('disk fail'); }, + } as unknown as VehicleFingerprintStore; + const eng = new AutoLearningEngine(throwing, () => useVidStore.getState(), () => [obs('PID', 'A5')], () => 5000); + const stop = eng.start(); + expect(() => { + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + useVidStore.getState().updateVehicleInfo({ vin: 'VF1BM0A0H12345678' }); + }).not.toThrow(); + stop(); + }); + + it('OBD/Discovery akışı etkilenmez (öğrenme SALT-OKUNUR)', () => { + // Gerçek discovery servisi — capture/queue davranışı öğrenmeden bağımsız kalmalı. + const disc = new DiscoveryCaptureService({ cache: new DiscoveryCache(), queue: new DiscoveryQueue(`ale-disc-${_k++}`), emitDiagnostic: vi.fn() }); + disc.capture({ pidOrDid: '242E', discoverySource: 'DID', mode: '22', ecuAddress: '7E0' }); + const capturedBefore = disc.getCaptured().length; + const obsBefore = disc.getObservations().length; + + const s = fpStore(); + const eng = new AutoLearningEngine(s, () => useVidStore.getState(), () => disc.getObservations(), () => 5000); + const stop = eng.start(); + useVidStore.getState().updateVehicleInfo({ vin: 'VF1BM0A0H12345678' }); + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + + expect(s.size).toBe(1); // öğrenildi + expect(disc.getCaptured().length).toBe(capturedBefore); // queue DEĞİŞMEDİ + expect(disc.getObservations().length).toBe(obsBefore); // capture DEĞİŞMEDİ + stop(); + }); + + it('stop() sonrası öğrenme durur (zero-leak)', () => { + const s = fpStore(); + const observations: DiscoveryObservation[] = [obs('PID', 'A5')]; + const eng = new AutoLearningEngine(s, () => useVidStore.getState(), () => observations, () => 5000); + const stop = eng.start(); + stop(); + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + useVidStore.getState().updateVehicleInfo({ vin: 'VF1BM0A0H12345678' }); + expect(s.size).toBe(0); + }); +}); diff --git a/src/platform/autoLearningEngine.ts b/src/platform/autoLearningEngine.ts new file mode 100644 index 00000000..37e1422b --- /dev/null +++ b/src/platform/autoLearningEngine.ts @@ -0,0 +1,293 @@ +/** + * autoLearningEngine — Otomatik Öğrenme Motoru TEMELİ (PR-27, foundation-only). + * + * AMAÇ: Vehicle Fingerprint (PR-25/26) ile Discovery (PR-DISC) sistemini birleştirir. + * Sistem, bir araçta tekrar görülen katalog-dışı PID/DID'leri o aracın parmak izine + * BAĞLAYARAK öğrenmeye başlar → kendi kendini geliştiren YEREL bilgi tabanının çekirdeği. + * + * BU PR (yalnız altyapı): + * - Discovery gözlemlerini DİNLE (SALT-OKUNUR: getObservations/subscribe). + * - Gözlemleri, o an bağlı aracın fingerprint'ine EŞLE + PID/DID bilgisini işle + * (firstSeen / lastSeen / seenCount / confidence). + * - PR-26'daki "staged VIN" durumunu ÇÖZ: VIN'siz oluşmuş fingerprint sonradan VIN + * gelince aynı araç anlaşılırsa OTOMATİK merge → duplicate bırakma. + * + * KESİN SINIRLAR (CLAUDE.md): Native OBD / poll / Discovery Queue / Discovery Capture / + * PID Registry / DID Registry / SQL-Supabase / cloud sync DEĞİŞMEZ. Cloud/AI YOK — yalnız + * yerel bilgi. TAMAMEN ADDITIVE + FAIL-SOFT: öğrenme çökse bile OBD/Discovery/Fingerprint + * akışları aynen sürer (hata sızmaz). + * + * KATMAN (Clean Architecture): saf öğrenme mantığı (apply/merge/deriveConfidence) React'sız + * test edilebilir; motor sınıfı yalnız ince abonelik sarmalayıcısıdır. + */ + +import { useVidStore, type VidStore } from '../store/useVidStore'; +import { + discoveryCaptureService, + type DiscoveryObservation, +} from './obd/discovery'; +import { + buildFingerprint, + normalizeVin, + vehicleFingerprintStore, + VehicleFingerprintStore, +} from './vehicleFingerprintService'; +import { + ingestVehicleFingerprint, + isConnectionComplete, + assembleFingerprintInput, + type LearnedVehicleFingerprint, +} from './vehicleFingerprintBuilder'; + +/* ══════════════════════════════════════════════════════════════════════════ + * Bilgi modeli — fingerprint ÜSTÜNE öğrenilmiş PID/DID kataloğu + * ════════════════════════════════════════════════════════════════════════ */ + +/** Öğrenilmiş tek bir sinyalin (PID/DID) yaşam-döngüsü kaydı. */ +export interface DiscoveredSignal { + firstSeen: number; + lastSeen: number; + seenCount: number; + confidence: number; +} + +/** + * Öğrenme bilgisiyle genişletilmiş fingerprint. discoveredPids/Dids depoda JSON olarak + * şeffaf saklanır (foundation/builder tipleri DEĞİŞMEDEN — Clean Arch). + */ +export interface VehicleKnowledge extends LearnedVehicleFingerprint { + /** PID (Mode 01…) → öğrenilmiş sinyal kaydı. */ + discoveredPids: Record; + /** DID (Mode 22 / UDS…) → öğrenilmiş sinyal kaydı. */ + discoveredDids: Record; +} + +/* ── Güven ──────────────────────────────────────────────────────────────── */ +const INITIAL_CONFIDENCE = 0.5; +const CONFIDENCE_STEP = 0.1; +const MAX_CONFIDENCE = 1.0; + +function clamp01(v: number): number { + return Math.max(0, Math.min(MAX_CONFIDENCE, v)); +} + +/** Görülme sayısından güven türetir (deterministik, tavanı 1.0). */ +export function deriveConfidence(seenCount: number): number { + const n = Math.max(1, seenCount || 1); + return clamp01(INITIAL_CONFIDENCE + CONFIDENCE_STEP * (n - 1)); +} + +/** Bir fingerprint'i bilgi katmanıyla (boş PID/DID haritaları) TAM-şekilli hale getirir. */ +export function initKnowledge(fp: LearnedVehicleFingerprint | VehicleKnowledge): VehicleKnowledge { + const k = fp as Partial; + return { + ...(fp as LearnedVehicleFingerprint), + discoveredPids: { ...(k.discoveredPids ?? {}) }, + discoveredDids: { ...(k.discoveredDids ?? {}) }, + }; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * SAF öğrenme — Discovery gözlemlerini bilgiye yansıt (idempotent mirror) + * ════════════════════════════════════════════════════════════════════════ */ + +function _normId(s: string): string { + return (s ?? '').replace(/\s+/g, '').toUpperCase(); +} + +/** + * Discovery gözlemlerini fingerprint bilgisine İŞLER (SALT-OKUNUR kaynak → idempotent): + * her sinyal için firstSeen KORUNUR, lastSeen/seenCount/confidence GÜNCELLENİR. seenCount + * discovery gözleminin kendi sayısını yansıtır (tekrar görülünce artar) → deterministik. + * Yeni (immutable) bilgi döndürür; girdiyi mutasyona uğratmaz. + */ +export function applyObservationsToKnowledge( + fp: VehicleKnowledge, + observations: readonly DiscoveryObservation[], + now: number = Date.now(), +): VehicleKnowledge { + const pids = { ...fp.discoveredPids }; + const dids = { ...fp.discoveredDids }; + let latest = fp.lastSeen; + + for (const o of observations) { + const rec = o?.record; + if (!rec) continue; + const id = _normId(rec.pidOrDid); + if (!id) continue; + const map = rec.discoverySource === 'DID' ? dids : pids; + const seenCount = Math.max(1, o.seenCount || 1); + const firstAt = o.firstAt || now; + const lastAt = o.lastAt || now; + const existing = map[id]; + map[id] = { + firstSeen: existing?.firstSeen ?? firstAt, // İLK görülme KORUNUR + lastSeen: Math.max(existing?.lastSeen ?? 0, lastAt), + seenCount, + confidence: deriveConfidence(seenCount), + }; + if (lastAt > latest) latest = lastAt; + } + + return { ...fp, discoveredPids: pids, discoveredDids: dids, lastSeen: latest }; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * VIN merge — staged (VIN'siz → VIN'li) çözümü + * ════════════════════════════════════════════════════════════════════════ */ + +/** İki dizinin (normalize + sıralı ECU) eşit olup olmadığı. */ +function _ecuEqual(a: readonly string[], b: readonly string[]): boolean { + return a.length === b.length && a.every((e, i) => e === b[i]); +} + +/** + * İki fingerprint AYNI ARACA mı ait? Kurallar: + * - İkisi de VIN'li → yalnız VIN eşitse aynı (farklı VIN = farklı araç → BİRLEŞTİRME). + * - Biri VIN'siz (staged) → protocol + ECU seti eşit VE adaptör MAC'i çelişmiyorsa aynı. + * ECU seti boşsa güvenli tarafta kal (aynı sayma → birleştirme). + */ +export function isLikelySameVehicle(a: VehicleKnowledge, b: VehicleKnowledge): boolean { + const aVin = normalizeVin(a.vin); + const bVin = normalizeVin(b.vin); + if (aVin && bVin) return aVin === bVin; + + if (a.protocol !== b.protocol) return false; + if (a.ecuAddresses.length === 0 || !_ecuEqual(a.ecuAddresses, b.ecuAddresses)) return false; + + const macA = a.metadata.adapterMac; + const macB = b.metadata.adapterMac; + if (macA && macB && macA !== macB) return false; // farklı adaptör → farklı araç + + return true; +} + +/** İki sinyal haritasını birleştirir (sinyal başına: min firstSeen, max lastSeen, toplam seenCount). */ +function _mergeSignalMaps( + a: Record, + b: Record, +): Record { + const out: Record = { ...a }; + for (const [id, sig] of Object.entries(b)) { + const cur = out[id]; + if (!cur) { + out[id] = { ...sig }; + } else { + const seenCount = cur.seenCount + sig.seenCount; + out[id] = { + firstSeen: Math.min(cur.firstSeen, sig.firstSeen), + lastSeen: Math.max(cur.lastSeen, sig.lastSeen), + seenCount, + confidence: deriveConfidence(seenCount), + }; + } + } + return out; +} + +/** + * İki aracı (primary=SAĞ KALAN, tercihen VIN'li) tek kayıtta birleştirir. Kimlik primary'nin + * (hash/VIN); yaşam-döngüsü ve öğrenilmiş bilgi İKİSİNİN toplamıdır. Duplicate kalmaz. + */ +export function mergeKnowledge(primary: VehicleKnowledge, secondary: VehicleKnowledge): VehicleKnowledge { + const ecuUnion = [...new Set([...primary.ecuAddresses, ...secondary.ecuAddresses])].sort(); + return { + ...primary, + createdAt: Math.min(primary.createdAt ?? primary.firstSeen, secondary.createdAt ?? secondary.firstSeen), + firstSeen: Math.min(primary.firstSeen, secondary.firstSeen), // İLK görülme KORUNUR + lastSeen: Math.max(primary.lastSeen, secondary.lastSeen), + sourceCount: (primary.sourceCount ?? 1) + (secondary.sourceCount ?? 1), + confidence: Math.max(primary.confidence ?? 0, secondary.confidence ?? 0), + ecuAddresses: ecuUnion, + metadata: { + adapterMac: primary.metadata.adapterMac ?? secondary.metadata.adapterMac, + firmwareVersion: primary.metadata.firmwareVersion ?? secondary.metadata.firmwareVersion, + label: primary.metadata.label ?? secondary.metadata.label, + }, + profileHint: primary.profileHint || secondary.profileHint, + discoveredPids: _mergeSignalMaps(primary.discoveredPids, secondary.discoveredPids), + discoveredDids: _mergeSignalMaps(primary.discoveredDids, secondary.discoveredDids), + }; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Otomatik Öğrenme Motoru — Discovery + VID aboneliği (ince sarmalayıcı) + * ════════════════════════════════════════════════════════════════════════ */ + +export class AutoLearningEngine { + private _unsubDisc: (() => void) | null = null; + private _unsubVid: (() => void) | null = null; + + constructor( + private readonly _store: VehicleFingerprintStore = vehicleFingerprintStore, + private readonly _readVid: () => VidStore = () => useVidStore.getState(), + private readonly _readObs: () => DiscoveryObservation[] = () => discoveryCaptureService.getObservations(), + private readonly _now: () => number = () => Date.now(), + ) {} + + /** Discovery + VID aboneliklerini başlatır (idempotent). Döndürülen fonksiyon durdurur (zero-leak). */ + start(): () => void { + if (this._unsubDisc || this._unsubVid) return () => this.stop(); + this._unsubDisc = discoveryCaptureService.subscribe(() => this._tick()); + this._unsubVid = useVidStore.subscribe(() => this._tick()); + this._tick(); // mevcut durumu bir kez değerlendir + return () => this.stop(); + } + + stop(): void { + this._unsubDisc?.(); this._unsubDisc = null; + this._unsubVid?.(); this._unsubVid = null; + } + + /** + * Tek öğrenme adımı: bağlı aracın fingerprint'ini bul (yoksa oluştur) → VIN merge → + * discovery gözlemlerini bilgiye işle → kaydet. FAIL-SOFT: hata OBD/Discovery/Fingerprint + * akışını ASLA etkilemez. + */ + private _tick(): void { + try { + const vid = this._readVid(); + if (!isConnectionComplete(vid)) return; + + const input = assembleFingerprintInput(vid, this._readObs()); + const base = buildFingerprint(input); + const loaded = this._store.load(base.hash); + // Fingerprint yoksa builder henüz oluşturmamış olabilir → burada oluştur (idempotent). + let fp: VehicleKnowledge = initKnowledge(loaded ?? ingestVehicleFingerprint(input, this._store)); + + // Staged VIN çözümü — bu araç VIN'liyse, aynı aracın VIN'siz kaydını birleştir. + fp = this._mergeStagedVin(fp); + + // Öğren: discovery gözlemlerini (SALT-OKUNUR) bilgiye işle. + const learned = applyObservationsToKnowledge(fp, this._readObs(), this._now()); + this._store.save(learned); + } catch { + /* FAIL-SOFT: öğrenme hatası mevcut akışları ASLA bozmaz */ + } + } + + /** current VIN'liyse: aynı araca ait VIN'siz kaydı bul → merge → duplicate'i kaldır. */ + private _mergeStagedVin(current: VehicleKnowledge): VehicleKnowledge { + if (!normalizeVin(current.vin)) return current; // VIN yok → birleştirilecek bir şey yok + for (const other of this._store.list()) { + if (other.hash === current.hash) continue; + const sibling = initKnowledge(other as LearnedVehicleFingerprint); + if (!normalizeVin(sibling.vin) && isLikelySameVehicle(current, sibling)) { + const merged = mergeKnowledge(current, sibling); + this._store.remove(sibling.hash); + this._store.remove(current.hash); // firstSeen korunacak şekilde taze yaz + this._store.save(merged); + return merged; + } + } + return current; + } +} + +/** Uygulama geneli tekil öğrenme motoru (SystemBoot Wave-3'ten başlatılır). */ +export const autoLearningEngine = new AutoLearningEngine(); + +/** SystemBoot wiring yardımcı — başlatır, cleanup fonksiyonu döndürür. */ +export function startAutoLearningEngine(): () => void { + return autoLearningEngine.start(); +} diff --git a/src/platform/system/SystemBoot.ts b/src/platform/system/SystemBoot.ts index d57b4cf0..558b0c17 100644 --- a/src/platform/system/SystemBoot.ts +++ b/src/platform/system/SystemBoot.ts @@ -60,6 +60,7 @@ import { initPushService } from '../pushService'; import { startBatteryProtection } from '../power/BatteryProtectionService'; import { startVehicleIntelligenceService } from '../vehicleIntelligenceService'; import { startAutomaticVehicleFingerprint } from '../vehicleFingerprintBuilder'; +import { startAutoLearningEngine } from '../autoLearningEngine'; import { logError } from '../crashLogger'; import { showToast, dismissToast } from '../errorBus'; import { healthMonitor } from './SystemHealthMonitor'; @@ -530,6 +531,11 @@ class SystemBoot { _log(' › AutomaticVehicleFingerprint'); this._reg(startAutomaticVehicleFingerprint()); + // AutoLearningEngine (PR-27): discovery gözlemlerini fingerprint'e bağlayıp öğren + // (PID/DID seenCount/confidence) + staged VIN merge. Additive + fail-soft; cleanup _reg'le. + _log(' › AutoLearningEngine'); + this._reg(startAutoLearningEngine()); + // GeofenceService: async (Supabase zona sorgusu) _log(' › GeofenceService (async)'); const geofenceCleanup = await startGeofenceService().catch((e: unknown) => { From 046746032f7e5b91359d076acf1fea0edc7b2f21 Mon Sep 17 00:00:00 2001 From: Selim Date: Thu, 9 Jul 2026 23:58:20 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(vehicle):=20TS1294=20parametre=20=C3=B6?= =?UTF-8?q?zelli=C4=9Fi=20+=20latent=20TS2345=20tip=20(PR-27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AutoLearningEngine constructor'ı açık field+atama (erasableSyntaxOnly). Ayrıca tsc -b ile açığa çıkan latent TS2345: store.load()'un döndürdüğü kayıt VehicleKnowledge olarak işaretlenir (depoda öyle saklanıyor) — davranış AYNI. (#26 fix merge-forward ile devralındı.) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Fauew13VP3vbfKqnocPdfX --- src/platform/autoLearningEngine.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/platform/autoLearningEngine.ts b/src/platform/autoLearningEngine.ts index 37e1422b..09a76475 100644 --- a/src/platform/autoLearningEngine.ts +++ b/src/platform/autoLearningEngine.ts @@ -217,13 +217,22 @@ export function mergeKnowledge(primary: VehicleKnowledge, secondary: VehicleKnow export class AutoLearningEngine { private _unsubDisc: (() => void) | null = null; private _unsubVid: (() => void) | null = null; + private readonly _store: VehicleFingerprintStore; + private readonly _readVid: () => VidStore; + private readonly _readObs: () => DiscoveryObservation[]; + private readonly _now: () => number; constructor( - private readonly _store: VehicleFingerprintStore = vehicleFingerprintStore, - private readonly _readVid: () => VidStore = () => useVidStore.getState(), - private readonly _readObs: () => DiscoveryObservation[] = () => discoveryCaptureService.getObservations(), - private readonly _now: () => number = () => Date.now(), - ) {} + store: VehicleFingerprintStore = vehicleFingerprintStore, + readVid: () => VidStore = () => useVidStore.getState(), + readObs: () => DiscoveryObservation[] = () => discoveryCaptureService.getObservations(), + now: () => number = () => Date.now(), + ) { + this._store = store; + this._readVid = readVid; + this._readObs = readObs; + this._now = now; + } /** Discovery + VID aboneliklerini başlatır (idempotent). Döndürülen fonksiyon durdurur (zero-leak). */ start(): () => void { @@ -251,7 +260,7 @@ export class AutoLearningEngine { const input = assembleFingerprintInput(vid, this._readObs()); const base = buildFingerprint(input); - const loaded = this._store.load(base.hash); + const loaded = this._store.load(base.hash) as VehicleKnowledge | null; // Fingerprint yoksa builder henüz oluşturmamış olabilir → burada oluştur (idempotent). let fp: VehicleKnowledge = initKnowledge(loaded ?? ingestVehicleFingerprint(input, this._store));