From d4b0982f624e4c8838ac16eef6d71f1288b2add5 Mon Sep 17 00:00:00 2001 From: Selim Date: Sat, 11 Jul 2026 10:47:19 +0300 Subject: [PATCH] =?UTF-8?q?feat(capability):=20provider=20adapter=20founda?= =?UTF-8?q?tion=20=E2=80=94=20runtime=20kan=C4=B1t=20=E2=86=92=20Registry?= =?UTF-8?q?=20k=C3=B6pr=C3=BCs=C3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gerçek runtime/cihaz/platform kanıtlarını salt-okunur biçimde Capability Registry kayıtlarına aktaran provider adapter. Yalnız provider foundation — gerçek provider implementasyonları/Event Bus/SystemBoot wiring/persistence ayrı PR'lar; native tarama başlatmaz, Registry karar kurallarını değiştirmez. Salt-okunur analiz: doğrulanabilir kaynaklar sınırlı (deviceCapabilities browser probe, vehicleHal signal support, browser API gps/mic/bt/storage, config/lisans); runtime kaynağı hiç yok: vehicle.ignition/ai.grok/navigation.offline_map → unknown. - Provider sözleşmesi {id, domain, source, authoritative?, refreshPolicy?, read()}; read sync/async, kaynak yoksa null. Foundation'da yerleşik gerçek provider yok (DI). - Provider sonucu → Registry evidence → resolveCapability (status Registry'de çözülür). - Yalnız değişeni günceller (imza dedup, duplicate update yok, O(1)). - API: start/refresh/refreshProvider/stop/getStatus/getLastRefreshAt/getProviderResults/ reset/dispose. Steady-state timer/polling yok; paralel allSettled (biri diğerini bloke etmez); opt-in DI timeout (settle'da temizlenir); timeout/hata/null → unknown. - DeviceTier varlık uydurmaz (Registry tier engeli); kaynaksız capability açılmaz. - Fail-soft (provider/Registry hatası izole), privacy (sanitize), immutable çıktı. - Registry'yi doğrudan import etmez (yapısal DI + type-only → import yan etkisiz); Registry'yi dispose etmez; bounded 128 provider; yeni bağımlılık yok. - 41 birim testi (45 senaryo). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PaohAWKE2s1FhB6Hmmcj2p --- docs/DEVICE_VALIDATION_LEDGER.md | 1 + .../capabilityProviderAdapter.test.ts | 368 ++++++++++++++++++ .../capability/capabilityProviderAdapter.ts | 324 +++++++++++++++ src/platform/capability/index.ts | 11 + 4 files changed, 704 insertions(+) create mode 100644 src/__tests__/capabilityProviderAdapter.test.ts create mode 100644 src/platform/capability/capabilityProviderAdapter.ts diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index 7501c649..d56b0f2b 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 | |---|---------|--------|------------------------------------------|---------| +| 37 | **🔴 Device Validation Pending — Capability Provider Adapter Foundation** — Gerçek runtime/cihaz/platform kanıtlarını SALT-OKUNUR biçimde Capability Registry kayıtlarına aktaran provider adapter köprüsü (gerçek kanıt → adapter → Registry). ⚠️ **KOD GERÇEĞİ (salt-okunur analiz):** doğrulanabilir kaynaklar sınırlı — `deviceCapabilities`(WebGL/CSS/SAB/cores browser probe→device/platform) · `vehicleHal.getCapability/hasSignal`(araç sinyali, foundation'da hepsi false) · browser API(gps/mic/bluetooth/storage → yalnız "API var", donanım erişimini KESİNLEŞTİRMEZ) · config/lisans(oem.*/ai-provider BYOK anahtarı). **Runtime kaynağı HİÇ YOK:** `vehicle.ignition`(kontak kaynağı yok)·`ai.grok`(xAI yok)·`navigation.offline_map`(tile paketi yoksa) → unknown/unavailable KALIR. UI/dosya/modül varlığı kanıt DEĞİL. **Adapter:** donanımı DOĞRUDAN taramaz — yalnız DI `providers`'ı okur (foundation'da YERLEŞİK gerçek provider YOK; gerçek prob'lar ayrı wiring PR'ı). **Provider sözleşmesi:** `{id, domain, source, authoritative?, refreshPolicy?, read()}` (read sync/async, kaynak yoksa null). Provider sonucu→Registry evidence'ına normalize→`resolveCapability(id, evidence, domain)` (status'ü REGISTRY çözer — karar kuralları DEĞİŞMEZ). **YALNIZ değişeni günceller** (imza-tabanlı dedup→duplicate Registry update yok, O(1)). **API:** start/refresh/refreshProvider/stop/getStatus/getLastRefreshAt/getProviderResults/reset/dispose. **Refresh:** start'ta ilk refresh + açık çağrı; **steady-state timer/polling YOK**; provider'lar PARALEL (`Promise.allSettled` → biri diğerini bloke etmez); opt-in DI timeout (settle'da temizlenir→leak yok), timeout/hata/null→güvenli **unknown**. **DeviceTier:** varlık uydurmaz — Registry tier engelini uygular (low'da ai.local_model restricted); high tier kanıtsız açmaz. **Fail-soft:** provider throw/Registry update hatası izole, public API throw etmez, dispose sonrası no-op. **Gizlilik:** provider/details/reason sanitize (VIN/MAC/koordinat/secret/API-key sızmaz). **Immutable:** getProviderResults dondurulmuş kopya (nested dahil), girdi mutate edilmez. **YALIN:** Registry'yi DOĞRUDAN import ETMEZ (yapısal DI + type-only import→import yan etkisiz); Event Bus'a yayınlamaz; SystemBoot'a bağlanmaz; persistence/cloud yok; native tarama/OBD-CAN sorgusu yok; yeni bağımlılık yok; provider bounded 128. Registry'nin SAHİBİ DEĞİL→dispose Registry'yi dispose etmez. | `platform/capability/capabilityProviderAdapter.ts` (yeni), `platform/capability/index.ts` (export), `__tests__/capabilityProviderAdapter.test.ts` (yeni, 41 test) | Gerçek cihazda (gerçek provider wiring PR'ı bağlandıktan SONRA): (1) GPS/Bluetooth/Wi-Fi/kamera capability'leri doğru doğrulanıyor (native kanıtla available); (2) donanımı olmayan özellik available GÖRÜNMÜYOR; (3) gerçek kontak kaynağı olmayan cihazda `vehicle.ignition` unknown kalıyor; (4) TPMS kaynağı olmayan araçta available görünmüyor; (5) provider hatası Registry'yi/diğer provider'ları çökertmiyor; (6) aynı kanıt tekrarında duplicate capability update yok; (7) BASIC_JS/Mali-400'de CPU/RAM regresyonu yok (O(1) dedup, timer yok, paralel bounded read). ⚠️ **Gerçek provider implementasyonları · Event Bus · SystemBoot wiring · persistence AYRI PR'lar.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 36 | **🔴 Device Validation Pending — Vehicle HAL Provider Adapter Foundation** — Mevcut normalize araç durumunu (UnifiedVehicleStore fused worker/CAN/OBD/GPS) Vehicle HAL sinyal modeline aktaran ilk resmi SALT-OKUNUR provider adapter köprüsü. ⚠️ **KOD GERÇEĞİ (salt-okunur analiz):** UnifiedVehicleStore şu alanları taşır — `speed`(km/h,null=yok)·`rpm`(fused/`canRpm`)·`fuel`(%)·`odometer`·`reverse` + CAN extras `canCoolantTemp`/`canOilTemp`/`canThrottle`/`canBatteryVolt`/`canGearPos`/`canAmbientTemp`/`canTpmsKpa`/`canDoorOpen`/`canParkingBrake`; **per-signal source/quality/confidence metadata YOK** (fused nativeSource bile store'da yok); **gerçek ignition kaynağı YOK.** Store zaten dirty-check ile dedupe eder. **Aktarılan sinyaller:** speed/rpm/fuel_level/odometer(>0)/coolant_temp/oil_temp/throttle/battery_voltage/gear/ambient_temp/tpms + reverse/door_state/parking_brake (yalnız aktif araç varken=en az bir numerik sinyal mevcut). **supported=false KALAN:** `vehicle.ignition` (kaynak yok, ASLA ingest edilmez), TPMS (canTpmsKpa null iken). **Metadata yok → dürüst eşleme:** CAN-adlı store alanları `source=can`(quality high, confidence 0.9, VAL CAN base ~0.92 gerekçeli), füzyon alanları `source=inferred`(quality medium, 0.6); **magic confidence uydurulmadı** (VAL SIGNAL_SOURCE_DEFAULTS taban gerekçesi). Nullable numerik null→ingest YOK; boolean sinyaller default false'u kaynak sanmamak için aktif-araç kapısına tabi. **Değişiklik modeli:** adapter kendi son-değer haritasıyla YALNIZ değişeni ingest eder (duplicate yok, O(1), tam snapshot clone YOK, hot-path allocation yok). **Lifecycle:** start/stop/refresh/dispose İDEMPOTENT; duplicate subscription yok; store aboneliğini kullanır (yeni timer/polling YOK); dispose sonrası callback no-op; **HAL'in SAHİBİ DEĞİL → dispose HAL'i dispose ETMEZ.** **Fail-soft:** store okunamazsa/HAL ingest hatasında/tek sinyal bozukken diğerleri sürer, public API throw etmez. **Gizlilik:** yalnız numerik/boolean/array değer aktarılır (VIN/MAC/koordinat/ham/kimlik YOK). **YALIN:** store'u DOĞRUDAN import ETMEZ (yapısal `source` DI → import yan etkisiz); Event Bus'a yayınlamaz; Capability Registry'yi güncellemez; SystemBoot'a bağlanmaz; native/OBD/CAN sorgusu başlatmaz; poll frekansı değiştirmez; yeni bağımlılık yok. | `platform/vehicleHal/vehicleHalProviderAdapter.ts` (yeni), `platform/vehicleHal/index.ts` (export), `__tests__/vehicleHalProviderAdapter.test.ts` (yeni, 32 test) | Gerçek araçta (wiring PR'ı `useUnifiedVehicleStore.getState/subscribe`'ı bağladıktan SONRA): (1) hız/RPM/soğutma/yakıt/voltaj HAL'de kaynakla uyumlu (`getSpeed()` vb. gerçek değer); (2) aynı sinyal tekrarında duplicate update yok; (3) kaynağı olmayan ignition ve (verisiz) TPMS destekleniyor GÖRÜNMEZ; (4) reconnect sonrası abonelik duplicate olmuyor (start idempotent); (5) adapter stop/dispose sonrası callback kalmıyor; (6) BASIC_JS/Mali-400'de CPU/FPS regresyonu yok (O(1) değişiklik kontrolü, timer yok, mevcut subscribe akışı). ⚠️ **Event Bus · Capability Registry · SystemBoot wiring AYRI PR'lar.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 35 | **🔴 Device Validation Pending — Platform Event Bus Foundation** — Platform modüllerinin birbirini DOĞRUDAN çağırması yerine tip-güvenli, bounded, fail-soft, izole bir olay omurgası üzerinden haberleşmesini sağlayan foundation. Bugün olay/abonelik ≥5 desende parçalı (≥40 servis kendi `_listeners=new Set`'ini tutuyor; DOM CustomEvent; Zustand subscribe; mini-bus errorBus/VehicleEventHub; native listener), ORTAK event-adı standardı YOK. `PlatformEventBus` (createPlatformEventBus DI) bunu birleştirir. **Model:** 6 priority (safety/critical/high/normal/low/background) · 16 domain · 15 source · PlatformEvent(id/name/domain/source/priority/timestamp/**sequence**/payload/correlation/causation/vehicleFingerprintHash/replayable/retained/experimental). **İsim standardı** `domain.entity.action` (regex zorunlu). **~43 event katalog** (platform/vehicle/capability/deep_scan/health/navigation/eagle_eye/assistant/remote/fleet) — GERÇEK modüle BAĞLI DEĞİL (yalnız isim+contract). **API:** publish/publishName/subscribe/subscribeDomain/once/unsubscribe(idempotent)/hasSubscribers/listenerCount/getStats/getRecentEvents/clearRecentEvents/reset/dispose. **Dispatch:** listener'lar subscription-priority (yüksek→düşük) sonra kayıt sırası; **safety event re-entrant kuyrukta ÖNCE** dispatch (öncelikli drain); listener exception İZOLE (safety dahil engellenmez). **Sequence** monotonik (saat-geri'de bile artar). **Retained** (vehicle.connection/ignition/health.thermal/capability.snapshot) son değer tutar, yeni abone `replayLast` OPT-IN ile alır (varsayılan replay YOK), bounded. **History** bounded (256, BASIC_JS düşük config), **transient** (vehicle.signal.changed 3Hz) history DIŞI, priority-aware eviction (background önce, **safety KORUNUR**), yalnız sanitize özet. **Recursion** bounded depth (16) → aşım drop+recursionDropCount. **Gizlilik:** VIN/MAC/koordinat/ham hex/secret event'e giremez (vehicleFingerprintHash yalnız anonim 8-64 hex, VIN reddedilir; history payload özeti sanitize). **YALIN:** güvenlik KARARI ÜRETMEZ (yalnız taşır); SystemBoot'a BAĞLANMAZ; GLOBAL SINGLETON YOK (yalnız factory/class); **HİÇ import YOK** (tamamen bağımsız); timer YOK; polling YOK; import yan etkisiz; Map/Set O(1); hot-path'te JSON stringify YOK; yeni bağımlılık yok. Bounded (event-adı 256/listener 512/event-başına 64/domain 32/history 256/retained 32); immutable (event+payload deep-freeze, stats/history frozen); fail-soft (bozuk event reddedilir, dispose sonrası no-op, listener içinde unsubscribe/dispose güvenli). Gerçek modül davranışı DEĞİŞMEZ. | `platform/eventBus/platformEventBus.ts` (yeni), `platform/eventBus/index.ts` (yeni), `__tests__/platformEventBus.test.ts` (yeni, 46 test) | Gerçek cihazda (modül wiring PR'ları bağlandıktan SONRA): (1) event sırası deterministik; (2) duplicate listener oluşmuyor; (3) bir listener exception diğer modülleri etkilemiyor; (4) safety event normal event'ten önce teslim; (5) yüksek frekanslı transient event (vehicle.signal) history/RAM şişirmiyor; (6) dispose sonrası listener/timer sızıntısı yok; (7) BASIC_JS/Mali-400'de publish CPU/FPS regresyonu yok (O(1), timer yok, hot-path'te stringify yok). ⚠️ **Capability Registry/Vehicle HAL/Deep Scan wiring + event migration + persistence AYRI PR'lar.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 34 | **🔴 Device Validation Pending — Vehicle HAL Foundation** — CarOS Pro'daki TÜM modüllerin araçla YALNIZ bu katman üzerinden konuştuğu tek resmi Hardware Abstraction Layer. Bugün araç verisi Native VHAL/CAN(Hiworld/K24/NWD)/OBD/GPS → SignalNormalizer → VAL → VehicleCompute.worker → VehicleState → `useUnifiedVehicleStore` (**~40 dosyada** okunuyor) + Assistant Context + Battery Service + gauge SAB ile AYRI okunuyor; per-signal quality/confidence/source/stale/supported/unit veren ORTAK API yoktu. `VehicleHal` bu boşluğu doldurur. **15 sinyal kataloğu** (speed/rpm/coolant_temp/oil_temp/fuel_level/battery_voltage/throttle/ambient_temp/odometer/gear/reverse/ignition/door_state/parking_brake/tpms), her biri `supported=false` tohumlu (kaynak yok). **Signal modeli:** id·value·quality(low/medium/high/unknown)·confidence(0-1)·source(native/can/obd/deep_scan/inferred/none)·timestamp·stale·unit·supported. **Snapshot:** revision·updatedAt·signals(deep-freeze, id-sıralı). **API:** getSnapshot/getSignal(id)/hasSignal/subscribe/unsubscribe/refresh/reset/dispose + tipli erişimciler (getSpeed/getRPM/getCoolantTemp/getFuelLevel/getBatteryVoltage/getIgnition/getGear/getDoorState/getReverse/getThrottle/getOdometer/getTpms) + getVehicleIdentity + getCapability(HAL-yerel). **HAL ilkesi:** donanımdan bağımsız, KAYNAĞI GİZLER, aynı API her araçta çalışır, veri yoksa null/supported=false (fail-closed), hiçbir tüketici CAN/ELM327/Hiworld bilmez. **Veri ÜRETMEZ/YORUMLAMAZ** — yalnız tip doğrulama (number sinyale boolean/NaN→null), magic sanity eşiği YOK. **Gizlilik:** HAM VIN reddedilir (getVehicleIdentity yalnız fingerprint hash 8-64 hex, 17-karakter=VIN reddedilir), protokol sanitize, ham CAN frame/OBD response SAKLANMAZ. **YALIN:** SystemBoot/native/VAL/UnifiedVehicleStore'a BAĞLANMAZ (import YOK — tamamen bağımsız foundation); timer YOK; polling YOK; import yan etkisiz; yeni bağımlılık yok; Map O(1). Bounded (katalog sabit 15 · listener 32 · tpms 6); immutable (snapshot/signal deep-freeze); fail-soft (provider/listener izole, throw yok); duplicate event önleme (değişmezse emit yok); stale (yaş>5000ms veya saat-geri). ⚠️ **Gerçek kaynak wiring (VAL/UnifiedVehicleStore → HAL.ingest) + modüllerin HAL'e taşınması AYRI PR'lar.** | `platform/vehicleHal/vehicleHal.ts` (yeni), `platform/vehicleHal/index.ts` (yeni), `__tests__/vehicleHal.test.ts` (yeni, 35 test) | Gerçek araçta (kaynak wiring PR'ı bağlandıktan SONRA): (1) `getSpeed()/getRPM()/getBatteryVoltage()` gerçek değer döner (CAN/OBD/HAL kaynağından, tüketici kaynağı BİLMEDEN); (2) kaynağı olmayan sinyal (ör. `getIgnition()` şu an) null/supported=false; (3) sinyal eskirse stale=true (taze gösterilmez); (4) bir provider düşse HAL çökmez, diğer sinyaller okunur; (5) HAM VIN hiçbir yere sızmaz (yalnız fingerprint hash); (6) BASIC_JS/Mali-400'de HAL CPU/RAM regresyonu yok (O(1) lookup, timer yok, pasifken sıfır yük); (7) tüm modüller CAN/OBD/Hiworld yerine yalnız HAL kullanır. ⚠️ **Gerçek kaynak wiring + modül taşıma SONRAKİ PR KAPSAMI.** Test edilmeden 🟢 yapma. | 2026-07-11 | diff --git a/src/__tests__/capabilityProviderAdapter.test.ts b/src/__tests__/capabilityProviderAdapter.test.ts new file mode 100644 index 00000000..015b4448 --- /dev/null +++ b/src/__tests__/capabilityProviderAdapter.test.ts @@ -0,0 +1,368 @@ +/** + * capabilityProviderAdapter.test.ts — Capability Provider Adapter Foundation testleri. + * + * Kapsam: provider yok · start/stop idempotent · Registry'ye aktarım · available/ + * unavailable/unknown/degraded/restricted · confidence clamp · source/quality · native + * authoritative · config donanımı açmaz · UI/modül kanıt değil · stale · duplicate/değişen + * update · provider throw/timeout/izolasyon · ignition/TPMS/grok/offline-map kaynaksız → + * unknown · DeviceTier · immutability · privacy · dispose zero-leak · Registry dispose + * edilmiyor · import yan etkisi · Event Bus/SystemBoot/persistence wiring yok. + */ + +import { describe, it, expect } from 'vitest'; +import { + createCapabilityProviderAdapter, + createCapabilityRegistry, + DEFAULT_CAPABILITY_CATALOG, + MAX_CAPABILITY_PROVIDERS, + type CapabilityProvider, + type CapabilityProviderResult, + type CapabilityRegistryTarget, + type CapabilityDomain, +} from '../platform/capability'; +import adapterSource from '../platform/capability/capabilityProviderAdapter.ts?raw'; + +const NOW = 7_000_000; +const reg = (deps = {}) => + createCapabilityRegistry({ now: () => NOW, deviceTier: 'high', seedCatalog: DEFAULT_CAPABILITY_CATALOG, ...deps }); + +function prov(id: string, domain: CapabilityDomain, read: CapabilityProvider['read'], extra: Partial = {}): CapabilityProvider { + return { id, domain, source: 'native', read, ...extra }; +} +function adapter(registry: CapabilityRegistryTarget, providers: CapabilityProvider[], deps = {}) { + return createCapabilityProviderAdapter({ registry, providers, now: () => NOW, ...deps }); +} + +function spyRegistry() { + const calls: { id: string }[] = []; + const target: CapabilityRegistryTarget = { + resolveCapability: (id) => { calls.push({ id }); return null; }, + getCapability: () => null, + }; + return { target, calls }; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * 1–9 · Temel + status + * ════════════════════════════════════════════════════════════════════════ */ + +describe('temel ve status', () => { + it('1) provider yok — refresh no-op', async () => { + const r = reg(); const a = adapter(r, []); + await a.refresh(); + expect(a.getStatus().providerCount).toBe(0); + expect(a.getStatus().resolvedCount).toBe(0); + }); + + it('2) start idempotent', () => { + const a = adapter(reg(), [prov('device.wifi', 'device', () => ({ available: true }))]); + a.start(); a.start(); + expect(a.getStatus().started).toBe(true); + expect(a.getStatus().providerCount).toBe(1); + }); + + it('3) stop idempotent', () => { + const a = adapter(reg(), []); + a.start(); + expect(() => { a.stop(); a.stop(); }).not.toThrow(); + }); + + it('4-5) available — provider sonucu Registry\'ye aktarılıyor', async () => { + const r = reg(); + const a = adapter(r, [prov('device.wifi', 'device', () => ({ available: true, source: 'native', confidence: 0.95 }))]); + await a.refresh(); + expect(r.getStatus('device.wifi')).toBe('available'); + expect(r.isAvailable('device.wifi')).toBe(true); + }); + + it('6) unavailable', async () => { + const r = reg(); + await adapter(r, [prov('device.cellular', 'device', () => ({ available: false, source: 'native' }))]).refresh(); + expect(r.getStatus('device.cellular')).toBe('unavailable'); + }); + + it('7) unknown — provider null sonuç', async () => { + const r = reg(); + await adapter(r, [prov('device.wifi', 'device', () => null)]).refresh(); + expect(r.getStatus('device.wifi')).toBe('unknown'); + }); + + it('8) degraded — düşük confidence/kalite', async () => { + const r = reg(); + await adapter(r, [prov('device.gps', 'device', () => ({ available: true, source: 'native', confidence: 0.9, quality: 'low' }))]).refresh(); + expect(r.getStatus('device.gps')).toBe('degraded'); + }); + + it('9) restricted — safety-critical writable + non-authoritative', async () => { + const r = reg(); + await adapter(r, [prov('vehicle.coding', 'vehicle', () => ({ available: true, source: 'user', confidence: 0.9 }), { source: 'user', authoritative: false })]).refresh(); + expect(r.getStatus('vehicle.coding')).toBe('restricted'); + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * 10–16 · Metadata / karar + * ════════════════════════════════════════════════════════════════════════ */ + +describe('metadata ve karar', () => { + it('10) confidence clamp', async () => { + const r = reg(); + await adapter(r, [prov('device.wifi', 'device', () => ({ available: true, source: 'native', confidence: 5 }))]).refresh(); + expect(r.getConfidence('device.wifi')).toBe(1); + }); + + it('11) source korunuyor (can)', async () => { + const r = reg(); + await adapter(r, [prov('vehicle.obd', 'vehicle', () => ({ available: true, source: 'can', confidence: 0.9 }))]).refresh(); + expect(r.getCapability('vehicle.obd')?.source).toBe('can'); + }); + + it('12) native authoritative → available', async () => { + const r = reg(); + await adapter(r, [prov('device.bluetooth', 'device', () => ({ available: true, source: 'native' }))]).refresh(); + expect(r.getStatus('device.bluetooth')).toBe('available'); + }); + + it('13) config tek başına donanımı açmıyor → unknown', async () => { + const r = reg(); + await adapter(r, [prov('device.camera.rear', 'device', () => ({ available: true, source: 'config', confidence: 0.9 }), { source: 'config' })]).refresh(); + expect(r.getStatus('device.camera.rear')).toBe('unknown'); + }); + + it('14) UI/modül varlığı kanıt değil — null → unknown', async () => { + const r = reg(); + await adapter(r, [prov('vehicle.live_pid', 'vehicle', () => null)]).refresh(); + expect(r.getStatus('vehicle.live_pid')).toBe('unknown'); + }); + + it('15) stale sonuç available değil → unknown', async () => { + const r = reg(); + await adapter(r, [prov('device.wifi', 'device', () => ({ available: true, source: 'native', observedAt: NOW - 70_000 }))]).refresh(); + expect(r.getStatus('device.wifi')).toBe('unknown'); + }); + + it('16) çelişkili/değişen sonuç deterministik — son kanıt kazanır', async () => { + const r = reg(); + let avail = true; + const a = adapter(r, [prov('device.wifi', 'device', () => ({ available: avail, source: 'native' }))]); + await a.refresh(); + expect(r.getStatus('device.wifi')).toBe('available'); + avail = false; + await a.refresh(); + expect(r.getStatus('device.wifi')).toBe('unavailable'); + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * 17–23 · Dedup / fail-soft + * ════════════════════════════════════════════════════════════════════════ */ + +describe('dedup ve fail-soft', () => { + it('17) aynı sonuç duplicate Registry update üretmiyor', async () => { + const s = spyRegistry(); + const a = adapter(s.target, [prov('device.wifi', 'device', () => ({ available: true, source: 'native' }))]); + await a.refresh(); await a.refresh(); await a.refresh(); + expect(s.calls.length).toBe(1); // yalnız ilk sonuç + }); + + it('18) değişen sonuç update üretir', async () => { + const s = spyRegistry(); + let v = 1; + const a = adapter(s.target, [prov('device.wifi', 'device', () => ({ available: true, source: 'native', confidence: v / 10 }))]); + await a.refresh(); v = 2; await a.refresh(); + expect(s.calls.length).toBe(2); + }); + + it('19-20) provider throw fail-soft — diğerleri sürer', async () => { + const r = reg(); + await adapter(r, [ + prov('device.wifi', 'device', () => { throw new Error('boom'); }), + prov('device.bluetooth', 'device', () => ({ available: true, source: 'native' })), + ]).refresh(); + expect(r.getStatus('device.wifi')).toBe('unknown'); // hata → güvenli unknown + expect(r.getStatus('device.bluetooth')).toBe('available'); // izole, diğeri sürdü + }); + + it('21) Registry update hata izolasyonu', async () => { + let good = 0; + const target: CapabilityRegistryTarget = { + resolveCapability: (id) => { if (id === 'device.wifi') throw new Error('reg boom'); good++; return null; }, + getCapability: () => null, + }; + const a = adapter(target, [ + prov('device.wifi', 'device', () => ({ available: true, source: 'native' })), + prov('device.bluetooth', 'device', () => ({ available: true, source: 'native' })), + ]); + await expect(a.refresh()).resolves.toBeUndefined(); + expect(good).toBe(1); // bluetooth çözüldü + }); + + it('22) bozuk provider sonucu → unknown', async () => { + const r = reg(); + await adapter(r, [prov('device.wifi', 'device', () => 42 as unknown as CapabilityProviderResult)]).refresh(); + expect(r.getStatus('device.wifi')).toBe('unknown'); + }); + + it('23) provider timeout fail-soft → unknown', async () => { + const r = reg(); + const a = adapter(r, [prov('device.wifi', 'device', () => new Promise(() => { /* asla resolve olmaz */ }))], { timeoutMs: 15 }); + await a.refresh(); + expect(r.getStatus('device.wifi')).toBe('unknown'); + expect(a.getProviderResults()['device.wifi']?.reason).toBe('provider_timeout'); + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * 24–30 · Kaynaksız capability + DeviceTier + * ════════════════════════════════════════════════════════════════════════ */ + +describe('kaynaksız capability ve DeviceTier', () => { + it('24) gerçek ignition yok → unknown', async () => { + const r = reg(); + await adapter(r, [prov('device.wifi', 'device', () => ({ available: true, source: 'native' }))]).refresh(); + expect(r.getStatus('vehicle.ignition')).toBe('unknown'); // provider yok → unknown kalır + }); + + it('25) gerçek TPMS yok → unknown', async () => { + const r = reg(); + await adapter(r, []).refresh(); + expect(r.getStatus('vehicle.tpms')).toBe('unknown'); + }); + + it('26) Grok entegrasyonu yok → unavailable/unknown', async () => { + const r = reg(); + await adapter(r, [prov('ai.grok', 'ai', () => null)]).refresh(); // gerçek entegrasyon yok + expect(r.isAvailable('ai.grok')).toBe(false); + }); + + it('27) offline map paketi yok → unavailable', async () => { + const r = reg(); + await adapter(r, [prov('navigation.offline_map', 'navigation', () => ({ available: false, source: 'runtime', reason: 'no_tile_pack' }))]).refresh(); + expect(r.getStatus('navigation.offline_map')).toBe('unavailable'); + }); + + it('28-29) Mali-400 (low) — local model restricted (kanıt olsa bile)', async () => { + const r = reg({ deviceTier: 'low' }); + await adapter(r, [prov('ai.local_model', 'ai', () => ({ available: true, source: 'native' }))]).refresh(); + expect(r.getStatus('ai.local_model')).toBe('restricted'); // tier engeli + }); + + it('30) high tier — kanıtsız capability AÇILMIYOR', async () => { + const r = reg({ deviceTier: 'high' }); + await adapter(r, []).refresh(); // device.npu için provider yok + expect(r.getStatus('device.npu')).toBe('unknown'); + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * 31–40 · Bounded / immutability / yaşam döngüsü + * ════════════════════════════════════════════════════════════════════════ */ + +describe('bounded, immutability, yaşam döngüsü', () => { + it('31) provider sayısı bounded', () => { + const many = Array.from({ length: MAX_CAPABILITY_PROVIDERS + 20 }, (_, i) => prov(`plugin.p${i}.x`, 'plugin', () => null)); + const a = adapter(reg(), many); + expect(a.getStatus().providerCount).toBe(MAX_CAPABILITY_PROVIDERS); + }); + + it('32) duplicate provider (aynı id) engelleniyor', () => { + const a = adapter(reg(), [ + prov('device.wifi', 'device', () => ({ available: true })), + prov('device.wifi', 'device', () => ({ available: false })), + ]); + expect(a.getStatus().providerCount).toBe(1); + }); + + it('33) refreshProvider — tek provider', async () => { + const r = reg(); + const a = adapter(r, [prov('device.wifi', 'device', () => ({ available: true, source: 'native' }))]); + await a.refreshProvider('device.wifi'); + expect(r.getStatus('device.wifi')).toBe('available'); + }); + + it('34-35) getProviderResults immutable (nested dahil)', async () => { + const a = adapter(reg(), [prov('device.wifi', 'device', () => ({ available: true, source: 'native', details: { note: 'ok' } }))]); + await a.refresh(); + const res = a.getProviderResults(); + expect(Object.isFrozen(res['device.wifi'])).toBe(true); + expect(Object.isFrozen(res['device.wifi'].details)).toBe(true); + }); + + it('36) girdi provider sonucu mutate edilmiyor', async () => { + const result = { available: true, source: 'native' as const, details: { a: '1' } }; + await adapter(reg(), [prov('device.wifi', 'device', () => result)]).refresh(); + expect(result.details).toEqual({ a: '1' }); + }); + + it('37) privacy — VIN/koordinat sanitize', async () => { + const r = reg(); + const a = adapter(r, [prov('vehicle.obd', 'vehicle', () => ({ available: true, source: 'can', provider: 'ecu 1HGCM82633A004352', details: { loc: '41.0082,28.9784' } }))]); + await a.refresh(); + const serialized = JSON.stringify(a.getProviderResults()) + JSON.stringify(r.getCapability('vehicle.obd')); + expect(serialized).not.toContain('1HGCM82633A004352'); + expect(serialized).not.toContain('41.0082,28.9784'); + }); + + it('38) dispose zero-leak', async () => { + const a = adapter(reg(), [prov('device.wifi', 'device', () => ({ available: true }))]); + await a.refresh(); a.dispose(); + expect(a.isDisposed).toBe(true); + expect(a.getProviderResults()).toEqual({}); + }); + + it('39) dispose sonrası callback no-op', async () => { + const s = spyRegistry(); + const a = adapter(s.target, [prov('device.wifi', 'device', () => ({ available: true, source: 'native' }))]); + await a.refresh(); + const before = s.calls.length; + a.dispose(); + await a.refresh(); // dispose sonrası + expect(s.calls.length).toBe(before); + }); + + it('40) Registry adapter tarafından dispose EDİLMİYOR', async () => { + const r = reg(); + const a = adapter(r, [prov('device.wifi', 'device', () => ({ available: true, source: 'native' }))]); + await a.refresh(); a.dispose(); + expect(r.isDisposed).toBe(false); // Registry çağıranındır + expect(r.getStatus('device.wifi')).toBe('available'); // Registry hâlâ kullanılabilir + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * 41–45 · Yalıtım + * ════════════════════════════════════════════════════════════════════════ */ + +describe('yalıtım', () => { + it('41) import yan etkisiz — steady-state timer/native YOK, yalnız type import', () => { + // setInterval yok (steady-state polling yok); setTimeout yalnız opt-in timeout için (settle'da temizlenir). + expect(/setInterval/.test(adapterSource)).toBe(false); + // Yalnız TYPE import → değer importu yok (native/store/registry çekilmez, import yan etkisiz). + expect(/^\s*import\s+type\s/m.test(adapterSource)).toBe(true); + expect(/^\s*import\s+(?!type)[\w{]/m.test(adapterSource)).toBe(false); + }); + + it('42) SystemBoot wiring yok (import edilmiyor)', () => { + expect(/from\s+['"][^'"]*SystemBoot['"]/.test(adapterSource)).toBe(false); + }); + + it('43) Event Bus wiring yok', () => { + expect(/from\s+['"][^'"]*eventBus[^'"]*['"]/.test(adapterSource)).toBe(false); + }); + + it('44) persistence yok', () => { + expect(/safeStorage|from\s+['"][^'"]*safeStorage/.test(adapterSource)).toBe(false); + }); + + it('45) BASIC_JS — değişmeyen sonuç yeniden Registry update üretmez', async () => { + const s = spyRegistry(); + const a = adapter(s.target, [ + prov('device.wifi', 'device', () => ({ available: true, source: 'native' })), + prov('device.gps', 'device', () => ({ available: true, source: 'native' })), + ]); + await a.refresh(); + const after = s.calls.length; // 2 + await a.refresh(); await a.refresh(); + expect(s.calls.length).toBe(after); // ek update yok (dedup) + }); +}); diff --git a/src/platform/capability/capabilityProviderAdapter.ts b/src/platform/capability/capabilityProviderAdapter.ts new file mode 100644 index 00000000..37bd0ca1 --- /dev/null +++ b/src/platform/capability/capabilityProviderAdapter.ts @@ -0,0 +1,324 @@ +/** + * capabilityProviderAdapter — Gerçek runtime/cihaz/platform kanıtlarını Capability + * Registry kayıtlarına aktaran SALT-OKUNUR provider adapter — FOUNDATION. + * + * AKIŞ: (gerçek runtime kanıtları) → Capability Provider Adapter → Capability Registry. + * + * ⚠️ KOD GERÇEĞİ (salt-okunur analiz): Doğrulanabilir gerçek kaynaklar sınırlı — + * - `deviceCapabilities` (WebGL/CSS/SAB/cores/memory browser probe'ları) → device/platform. + * - `vehicleHal.getCapability/hasSignal` → araç sinyali desteği (foundation'da hepsi false). + * - Browser/native API: gps(navigator.geolocation)/microphone/bluetooth/storage → yalnız + * "API var" der, gerçek donanım erişimini KESİNLEŞTİRMEZ (→ düşük confidence/degraded). + * - Config/lisans: oem.* · ai.gemini/groq/claude (BYOK anahtarı) → source=config/license. + * - **Runtime kaynağı HİÇ YOK:** `vehicle.ignition` (kontak kaynağı yok), `ai.grok` (xAI + * entegrasyonu yok), `navigation.offline_map` (tile paketi yoksa) → unknown/unavailable KALIR. + * UI/dosya/modül varlığı capability kanıtı DEĞİLDİR. + * + * NE YAPMAZ (bilinçli — bu PR yalnız provider FOUNDATION'ıdır): + * - Donanımı DOĞRUDAN TARAMAZ: yalnız DI ile verilen `providers`'ı okur. Foundation'da + * YERLEŞİK gerçek provider YOK (gerçek prob'lar ayrı wiring PR'ında). Import YAN ETKİSİZDİR + * (provider yalnız `start()`/`refresh()`'te çalışır; yapıcı hiçbir provider çağırmaz). + * - Event Bus'a YAYINLAMAZ · SystemBoot'a BAĞLANMAZ · UI/SQL/persistence/cloud YOK · + * native tarama/OBD-CAN sorgusu BAŞLATMAZ. Capability Registry'nin KARAR KURALLARINI + * değiştirmez (yalnız kanıt besler; status'ü Registry çözer). Registry'nin SAHİBİ DEĞİL: + * `dispose()` Registry'yi dispose ETMEZ. + * + * KARAR: Kaynağı olmayan capability available YAPILMAZ (fail-closed). Stale/timeout/hata → + * güvenli unknown. Çelişki Registry'de unknown/degraded olur. ZERO-LEAK: `dispose()` durumu + * kilitler (steady-state timer YOK; timeout yalnız opt-in ve settle'da temizlenir). FAIL-SOFT: + * bir provider hatası diğerlerini durdurmaz; public API throw ETMEZ. + */ + +import type { + CapabilityDomain, CapabilityStatus, CapabilitySource, CapabilityQuality, + CapabilityEvidence, CapabilityRecord, +} from './capabilityRegistry'; + +/* ══════════════════════════════════════════════════════════════════════════ + * Provider sözleşmesi + DI hedefleri + * ════════════════════════════════════════════════════════════════════════ */ + +export type CapabilityRefreshPolicy = 'once' | 'on_demand' | 'on_refresh'; + +/** Provider'ın döndürdüğü ham sonuç (Registry evidence'ına normalize edilir). */ +export interface CapabilityProviderResult { + readonly status?: CapabilityStatus; + readonly available?: boolean; + readonly quality?: CapabilityQuality; + readonly confidence?: number; + readonly source?: CapabilitySource; + readonly provider?: string; + readonly version?: string; + readonly details?: Record; + readonly limitations?: readonly string[]; + readonly observedAt?: number; + readonly reason?: string; +} + +export interface CapabilityProvider { + readonly id: string; + readonly domain: CapabilityDomain; + readonly source: CapabilitySource; + readonly authoritative?: boolean; + readonly refreshPolicy?: CapabilityRefreshPolicy; + /** Gerçek kanıt okuma (sync veya async). Kaynak yoksa null döner. */ + readonly read: () => CapabilityProviderResult | null | Promise; +} + +/** Registry hedefi (DI) — gerçek CapabilityRegistry yapısal olarak uyar. */ +export interface CapabilityRegistryTarget { + resolveCapability( + id: string, + evidence: CapabilityEvidence | readonly CapabilityEvidence[], + domain?: CapabilityDomain, + ): CapabilityRecord | null; + getCapability(id: string): CapabilityRecord | null; +} + +export interface CapabilityProviderAdapterDeps { + readonly registry: CapabilityRegistryTarget; + readonly providers: readonly CapabilityProvider[]; + readonly now?: () => number; + /** Opt-in provider okuma timeout'u (ms). 0/undefined → timeout yok (senkron beklenir). */ + readonly timeoutMs?: number; +} + +export interface CapabilityProviderAdapterStatus { + readonly started: boolean; + readonly disposed: boolean; + readonly lastRefreshAt: number | null; + readonly providerCount: number; + readonly resolvedCount: number; + readonly refreshCount: number; +} + +export const MAX_CAPABILITY_PROVIDERS = 128; +const MAX_TEXT_CHARS = 96; + +/* ══════════════════════════════════════════════════════════════════════════ + * Saf yardımcılar + * ════════════════════════════════════════════════════════════════════════ */ + +function _clamp01(v: unknown): number { + if (typeof v !== 'number' || !Number.isFinite(v)) return 0; + return v < 0 ? 0 : v > 1 ? 1 : v; +} + +function _text(v: unknown): string | null { + if (typeof v !== 'string') return null; + const s = v + .replace(/\b(?:sk|pk|api|key|token|bearer)[-_]?[A-Za-z0-9_-]{12,}\b/gi, '[redacted]') + .replace(/\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g, '[redacted]') + .replace(/\b[A-HJ-NPR-Z0-9]{17}\b/g, '[redacted]') + .replace(/-?\d{1,3}\.\d{4,}\s*[,;]\s*-?\d{1,3}\.\d{4,}/g, '[redacted]') + .trim(); + const c = s.length > MAX_TEXT_CHARS ? s.slice(0, MAX_TEXT_CHARS) : s; + return c || null; +} + +/** Dedup imzası — sonuç değişmediyse Registry update yapılmaz. */ +function _signature(r: CapabilityProviderResult): string { + return `${r.status ?? ''}|${r.available ?? ''}|${r.source ?? ''}|${r.quality ?? ''}|${_clamp01(r.confidence)}|${r.reason ?? ''}`; +} + +/** Sonucu güvenli/dondurulmuş dış-referanssız kopyaya çevirir (getProviderResults için). */ +function _freezeResult(r: CapabilityProviderResult): CapabilityProviderResult { + const details: Record = {}; + if (r.details && typeof r.details === 'object') { + let n = 0; + for (const [k, v] of Object.entries(r.details)) { + if (n >= 12) break; + const val = _text(typeof v === 'string' ? v : String(v)); + if (val) { details[k] = val; n++; } + } + } + return Object.freeze({ + status: r.status, + available: r.available, + quality: r.quality, + confidence: r.confidence !== undefined ? _clamp01(r.confidence) : undefined, + source: r.source, + provider: _text(r.provider) ?? undefined, + version: _text(r.version) ?? undefined, + details: Object.freeze(details), + limitations: Object.freeze((Array.isArray(r.limitations) ? r.limitations.map((l) => _text(l) ?? '').filter(Boolean) : []).slice(0, 8)), + observedAt: r.observedAt, + reason: _text(r.reason) ?? undefined, + }); +} + +/** Provider sonucunu Registry evidence'ına çevirir (status → available hint). */ +function _toEvidence(prov: CapabilityProvider, r: CapabilityProviderResult, now: number): CapabilityEvidence { + const available = r.available !== undefined ? r.available + : r.status === 'available' ? true + : (r.status === 'unavailable' || r.status === 'unsupported') ? false + : undefined; + return { + source: r.source ?? prov.source, + available, + quality: r.quality, + confidence: r.confidence, + observedAt: r.observedAt ?? now, + provider: r.provider ?? prov.id, + version: r.version, + details: r.details, + limitations: r.limitations, + reason: r.reason, + authoritative: prov.authoritative, + }; +} + +function _withTimeout(p: Promise, ms: number, onTimeout: () => T): Promise { + if (!(ms > 0)) return p; + return new Promise((resolve) => { + let done = false; + const timer = setTimeout(() => { if (!done) { done = true; resolve(onTimeout()); } }, ms); + p.then( + (v) => { if (!done) { done = true; clearTimeout(timer); resolve(v); } }, + () => { if (!done) { done = true; clearTimeout(timer); resolve(onTimeout()); } }, + ); + }); +} + +const TIMEOUT_MARKER = Symbol('capability_provider_timeout'); + +/* ══════════════════════════════════════════════════════════════════════════ + * Adapter + * ════════════════════════════════════════════════════════════════════════ */ + +export class CapabilityProviderAdapter { + private readonly _registry: CapabilityRegistryTarget; + private readonly _providers: CapabilityProvider[] = []; + private readonly _now: () => number; + private readonly _timeoutMs: number; + + private readonly _last = new Map(); // id → son sonuç imzası (dedup) + private readonly _results = new Map(); // id → son dondurulmuş sonuç + private _started = false; + private _disposed = false; + private _lastRefreshAt: number | null = null; + private _refreshCount = 0; + + constructor(deps: CapabilityProviderAdapterDeps) { + this._registry = deps.registry; + this._now = typeof deps.now === 'function' ? deps.now : () => Date.now(); + this._timeoutMs = typeof deps.timeoutMs === 'number' && deps.timeoutMs > 0 ? deps.timeoutMs : 0; + // Duplicate id engelle + bounded. + const seen = new Set(); + for (const p of Array.isArray(deps.providers) ? deps.providers : []) { + if (!p || typeof p.id !== 'string' || !p.id || typeof p.read !== 'function') continue; + if (seen.has(p.id) || this._providers.length >= MAX_CAPABILITY_PROVIDERS) continue; + seen.add(p.id); + this._providers.push(p); + } + } + + private _nowSafe(): number { + try { const n = this._now(); return Number.isFinite(n) ? n : 0; } catch { return 0; } + } + + /** Store aboneliği YOK — start yalnız ilk refresh'i tetikler. İDEMPOTENT. */ + start(): void { + if (this._disposed || this._started) return; + this._started = true; + void this.refresh(); + } + + /** Tüm provider'ları okuyup (paralel, biri diğerini bloke etmez) Registry'ye çözer. */ + async refresh(): Promise { + if (this._disposed) return; + this._refreshCount++; + this._lastRefreshAt = this._nowSafe(); + await Promise.allSettled(this._providers.map((p) => this._runProvider(p))); + } + + async refreshProvider(id: string): Promise { + if (this._disposed) return; + const p = this._providers.find((x) => x.id === id); + if (p) await this._runProvider(p); + } + + private async _runProvider(prov: CapabilityProvider): Promise { + if (this._disposed) return; + let result: CapabilityProviderResult | null; + try { + const raw = this._timeoutMs > 0 + ? await _withTimeout(Promise.resolve().then(() => prov.read()), this._timeoutMs, () => TIMEOUT_MARKER as never) + : await prov.read(); + if ((raw as unknown) === TIMEOUT_MARKER) { + result = { source: 'none', reason: 'provider_timeout' }; // timeout → güvenli unknown + } else { + result = raw && typeof raw === 'object' ? raw : { source: 'none', reason: 'provider_no_result' }; + } + } catch { + result = { source: 'none', reason: 'provider_error' }; // hata → güvenli unknown, izole + } + if (this._disposed) return; + + // Dedup: sonuç değişmediyse Registry update YOK. + const sig = _signature(result); + if (this._last.get(prov.id) === sig) return; + this._last.set(prov.id, sig); + this._results.set(prov.id, _freezeResult(result)); + + // Registry'ye çöz (karar kuralları Registry'de — status'ü Registry belirler). Hata izole. + try { + this._registry.resolveCapability(prov.id, _toEvidence(prov, result, this._nowSafe()), prov.domain); + } catch { /* Registry update hatası diğer provider'ları engellemez */ } + } + + getStatus(): CapabilityProviderAdapterStatus { + return Object.freeze({ + started: this._started, + disposed: this._disposed, + lastRefreshAt: this._lastRefreshAt, + providerCount: this._providers.length, + resolvedCount: this._results.size, + refreshCount: this._refreshCount, + }); + } + + getLastRefreshAt(): number | null { + return this._lastRefreshAt; + } + + /** Son provider sonuçları (dondurulmuş kopyalar — iç referans vermez). */ + getProviderResults(): Record { + const out: Record = {}; + for (const [id, r] of this._results) out[id] = r; // öğeler zaten frozen + return out; + } + + /** Dedup/sonuç durumunu sıfırlar (provider listesi korunur). */ + reset(): void { + if (this._disposed) return; + this._last.clear(); + this._results.clear(); + } + + /** Aboneliği bırakır (İDEMPOTENT). */ + stop(): void { + this._started = false; + } + + /** Zero-leak: durumu kilitler. Registry çağıranındır → dispose EDİLMEZ. */ + dispose(): void { + if (this._disposed) return; + this._started = false; + this._last.clear(); + this._results.clear(); + this._disposed = true; + } + + get isDisposed(): boolean { + return this._disposed; + } +} + +/** + * Fabrika — DI ile örnek üretir. Yapıcı YAN ETKİSİZ: provider ÇAĞRILMAZ, native/registry + * dokunulmaz (yalnız `start()`/`refresh()`'te çalışır) → import edilmesi davranış değiştirmez. + */ +export function createCapabilityProviderAdapter(deps: CapabilityProviderAdapterDeps): CapabilityProviderAdapter { + return new CapabilityProviderAdapter(deps); +} diff --git a/src/platform/capability/index.ts b/src/platform/capability/index.ts index 7388d359..f027329c 100644 --- a/src/platform/capability/index.ts +++ b/src/platform/capability/index.ts @@ -33,3 +33,14 @@ export { type CapabilityListener, type CapabilityRegistryDeps, } from './capabilityRegistry'; +export { + CapabilityProviderAdapter, + createCapabilityProviderAdapter, + MAX_CAPABILITY_PROVIDERS, + type CapabilityRefreshPolicy, + type CapabilityProviderResult, + type CapabilityProvider, + type CapabilityRegistryTarget, + type CapabilityProviderAdapterDeps, + type CapabilityProviderAdapterStatus, +} from './capabilityProviderAdapter';