Skip to content

Decisions

The design document is design/spec.md; decisions taken while implementing it live in design/decisions.md, and where they conflict, that file wins. The design-phase decision log (appendix A, A53–A76) and the scenario matrix (appendix B) are in the original design document; every scenario is a running test on the Recipes page.

The table below is read from design/decisions.md at build time, so every decision added to the file shows up here. The log is kept in Turkish, as it was written during implementation.

#KararGerekçe
A77Monorepo: bun workspaces, docs: Astro Starlight → GitHub Pages, paket: npm. §10'daki pnpm / Cloudflare Pages yerine D7 geçerliD7 kapandı
A78package.json exports'a ./calendar eklendiA72 modülü getiriyor, §10 listesi eksikti
A79graduated için "artmayan fiyat" kuralı kaldırıldıB3 ([0: 0, 500: p]) artan fiyatlı; kural B3'ü imkânsız kılıyordu
A80Hold üst sınırı her modelde: en pahalı kademe (price/per) × adet (package: ceil(q/size) blok) + tüm flatMicroUsd + (volume on_crossing) usedSoFar × (pmax − pmin); perObservation.max ile kırpılırA79 sonrası "ilk kademe" garantisi yok; en kötü durum her konumda geçerli (özellik testi)
A81Rated.totalMicroUsd hacim düzeltmesini içermez; düzeltme adjustmentMicroUsd, ayrı satır …:adjUsage satırı = gözlemin kendi tutarı; düzeltme ayrı idempotent kalem
A82usedSoFar zorunluluğu: çok kademe, volume, package, herhangi bir flatMicroUsd, ya da cumulative (carry yoksa). Tek kademeli sabit ücret de ilk girişi bilmek zorunda"Sessiz 0 yok" (A58)
A83cumulative: usedSoFar ile floor(T(u+q)) − floor(T(u)) (tam telescoping); carry verilirse floor(carry + Δ). Hacim none modelinde T tanımsız → carry gerekirSapmasızlık özelliği T üzerinden kanıtlanıyor
A84Aynı çağrıda aynı (meter, dims) satırları birbirinin usedSoFar'ını ilerletir; aynı sayaç tekrar ederse refId #2, #3 eki alırÇok satırlı gözlem tek tutarlı gözlem; refId çakışması veri kaybı olurdu
A85Sıfır tutarlı satır usage'a yazılır, ledger'a yazılmaz"sayılır, 0 yazılır" + boş ledger kalemi yok
A86Yeni hata nedenleri: invalid_ref, hold_exceeded, hold_closedRef şeması doğrulanıyor; hold değer nesnesi olduğundan kapanmış/aşılmış hold yakalanmalı
A87Hold değer nesnesi her çağrıda güncellenmiş hâliyle döner (result.hold); capture sıra numarası refId'ye girer (…:capture:n)Değer nesnesi + idempotensi: aynı eski hold ile retry aynı refId'yi üretir
A88Kısmi capture'lar teleskopik: R(u, c+q) − R(u, c); hold tek gözlem gibi yuvarlanırAksi hâlde per_event_up parçalı capture'da üst sınırı aşar
A89Havuz sayaçları boyutsuzdur; havuz satırı dims: {}Besleyen boyutlarından havuz boyutu türetilemez
A90Havuz miktarı ceil(q × w) float gürültüsünden arındırılır (12 hane)10 × 1.1 12 değil 11 olmalı
A91plan.observe sonucu lines (fiyatlanmış satırlar, havuzlar dahil) döner; metering.price(lines, …, { pools }) bunları tekrar fiyatlarDO içinde gömülü price() (§9) için satırların tarifesi gerekir
A92metering.commit(fn) adapter bağlar, metering.commit(plan) plan yazar (argüman türüne göre)§7 metering.commit(plan) kullanıyor
A93Tip düzeyinde durum: sayaçlar MeterEntry interface birliği; meter() dönüş tipi alias'sız yazılırAlias önceki durumu alias argümanı olarak taşıyor, ~100 sayaçta TS2589. 500 sayaç: ~0,6 sn tip kontrolü (havuzlar artımlı tutulur)
A94getRate callback'i dört parametreyi de yazmalı (_ctx, _at)TS birlik-tuple rest parametresinde eksik parametreli callback'i reddediyor
A95typed<T>() işaretçisi: .meter("x", typed<{…}>()), .context(typed<C>()) ya da .context<C>()Kısmi generic çıkarımı yok; düz tip için değer düzeyi işaretçi
A96Pratik zincir sınırı ~800 .meter(): TS parser'ı 1000 zincirli çağrıda yığını taşırıyorKatalog bölünebilir; hedef 500
A97B22 (işlem başı sabit): her işlem kendi dönemidir → getRate usedSoFar: 0 döner (tierPeriod: "observation")flatMicroUsd "kademeye ilk giriş"; işlem başı ücret için her gözlem yeni dönem
A98Dönem sonu gözlemi at: window.to − 1 ile yapılır; tarifesi döneme bağlı (per = ay saniyesi) getRate at'e bakar ve window.to bir sonraki dönemi seçerPencereler yarı açık [from, to) (/calendar, /gauge)
A99Aynı (refType, refId) ile tekrar edilen çağrı ok ve yeniden hesaplanan charged döner; commit hiçbir şey yazmaz. Sonuç "bu çağrının fiyatı"dır, "yazılan tutar" değilcommit imzası void; yazıldı/yazılmadı bilgisini taşımak adapter kontratını büyütür. Gerekirse ileride commit { duplicate: true } dönebilir
A100Sayaç (usedSoFar) kapsamı uygulamanındır: bellek adapter'ı ve referans adapter'lar sayaç başına (boyutsuz) sayar; boyut başına kademe isteyen getRate'te dims ile sayar (sqliteAdapter.usedSoFar({ dims }))"Bu dönemde bu sayaçta" tanımı (§2) boyutları kapsamaz; boyut başına kademe bir tarif seçimidir
A101Bir hold'da her (meter, dims) için tek satır vardır: tekrar eden satırlar birleşir, havuz satırı besleyicilerini feeders[] olarak taşır; extend mevcut satırı büyütür (tarifesi ve konumu sabit). Hold her anahtarı bir kez yuvarlar; cumulative'de ayrı satırlı observe ile birebir aynıdır, per_event_up'ta yalnızca yuvarlama kadar (satır başına en fazla ücret + düzeltme = 2 mikro birim) farklıdır, yönü garanti değildirAynı anahtarlı ayrı satırlar extend'de kademe konumlarını çakıştırıyordu (inceleme #1) ve capture edilemiyordu (#6)
A102Adapter'lar hold başına gerçekten ayrılan tutarı tutar: release onu serbest bırakır (plan tutarı değil), capture rezervasyonu en fazla onu kadar düşürürFarklı tutarla tekrarlanan hold no-op yazılır ama yeniden hesaplanmış upperBound döner; plan tutarına güvenen release rezervasyonu kalıcı kaydırıyordu (#5)
A103minimumCommit refId'si hesabı içerir: minimum_commit:{account}:{period}Ledger tekilliği (type, refType, refId) hesaplar arası geneldir; ikinci hesabın kalemi sessizce düşüyordu (#8)
A104Float gürültüsü yalnızca tamsayıya göreli 1e-9 yakınsa temizlenir (ceilClean); carry toplamı sonraki tamsayıya 1e-9 yakınsa yuvarlanır; aynı anahtarlı satırlar carry'yi zincirler12 hanelik kesme büyük tam değerleri bozuyordu; ⅓+⅓+⅓ 1 etmiyordu; ikinci satır ilk satırın carry girdisini tekrar kullanıyordu (#3, #10, #11)
A105SQLite ve Cloudflare referans adapter'ları ayrı paket değil, pricemeter/sqlite ve pricemeter/cloudflare alt yollarıdır (§10'daki packages/sqlite, packages/cloudflare yerine)İkisinin de bağımlılığı yok (sürücü ve Workers tipleri yapısal); tek paket, tek sürüm, tree-shake
A106volume + cumulative + on_crossing olmayan tarife carry olmadan usage_required döner (requiresCarry)Tanımlı bir koşu toplamı yok; carrysız her alt-mikro tutar kayboluyor ve sonsuza dek 0 kesiliyordu (A58 "sessiz 0 yok")
A107Kota: getRate { rate, usedSoFar, limit? } döner; aşan çağrı quota_exceeded (cause: { meter, limit, used, requested }), hiçbir şey yazılmaz. limit: 0 = planda yok. Limit sayaç başınadır (boyutlar toplanır), dönemi usedSoFar'ınkidir. Limit usage satırına yazılır; adapter commit içinde atomik kontrol eder ve QuotaExceeded fırlatır (SQLite: windowOf, DO: periodOf). Hold/extend ayrılan miktarı kontrol eder; capture asla reddedilmez"Ne kadar"ın sınırı: fiyatı olan ama hakkı olmayan kullanım. Bakiye kapısıyla aynı yarış, aynı çözüm (atomik sınır). Rate'e girmedi: ücretsiz sayaçlarda da gerekir ve fiyat modeline bir şey eklemez
A108Koltuk gibi inip çıkan seviyelerin limiti uygulamadadır (examples/saas-seats/src/seats.ts); limit yalnızca artan olay sayaçları içindirSayaçlar yalnızca artar; koltuk silinip yeniden eklenebilir
# Question Decision
D7 Docs domain and hosting bun workspaces monorepo (package + docs site); docs site on Astro, GitHub Pages; package on npm
D16 Calendar helpers the /calendar module (A72)
D17 Context validation on every call (A73)
D18 Should a feeds function see ctx yes (A74)
  • No silent zero (A58, A82): a tariff that needs usedSoFar returns usage_required when the counter is missing; the library never assumes 0.
  • Value objects (A87, A101): a hold comes back updated from every call and carries one line per (meter, dims).
  • One package (A105): the SQLite and Cloudflare adapters are the pricemeter/sqlite and pricemeter/cloudflare subpaths; they have no dependencies.
  • Result = the price of this call (A99): a repeated call returns the recomputed charged; whether anything was written is commit’s knowledge.
  • Counter scope belongs to the app (A100): the reference adapters count per meter; tiers per dimension value are a getRate choice.