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.
Implementation decisions (A77–)
Section titled “Implementation decisions (A77–)”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.
| # | Karar | Gerekçe |
|---|---|---|
| A77 | Monorepo: bun workspaces, docs: Astro Starlight → GitHub Pages, paket: npm. §10'daki pnpm / Cloudflare Pages yerine D7 geçerli | D7 kapandı |
| A78 | package.json exports'a ./calendar eklendi | A72 modülü getiriyor, §10 listesi eksikti |
| A79 | graduated için "artmayan fiyat" kuralı kaldırıldı | B3 ([0: 0, 500: p]) artan fiyatlı; kural B3'ü imkânsız kılıyordu |
| A80 | Hold ü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ır | A79 sonrası "ilk kademe" garantisi yok; en kötü durum her konumda geçerli (özellik testi) |
| A81 | Rated.totalMicroUsd hacim düzeltmesini içermez; düzeltme adjustmentMicroUsd, ayrı satır …:adj | Usage satırı = gözlemin kendi tutarı; düzeltme ayrı idempotent kalem |
| A82 | usedSoFar 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) |
| A83 | cumulative: usedSoFar ile floor(T(u+q)) − floor(T(u)) (tam telescoping); carry verilirse floor(carry + Δ). Hacim none modelinde T tanımsız → carry gerekir | Sapmasızlık özelliği T üzerinden kanıtlanıyor |
| A84 | Aynı ç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 |
| A85 | Sı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 |
| A86 | Yeni hata nedenleri: invalid_ref, hold_exceeded, hold_closed | Ref şeması doğrulanıyor; hold değer nesnesi olduğundan kapanmış/aşılmış hold yakalanmalı |
| A87 | Hold 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 |
| A88 | Kısmi capture'lar teleskopik: R(u, c+q) − R(u, c); hold tek gözlem gibi yuvarlanır | Aksi hâlde per_event_up parçalı capture'da üst sınırı aşar |
| A89 | Havuz sayaçları boyutsuzdur; havuz satırı dims: {} | Besleyen boyutlarından havuz boyutu türetilemez |
| A90 | Havuz 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ı |
| A91 | plan.observe sonucu lines (fiyatlanmış satırlar, havuzlar dahil) döner; metering.price(lines, …, { pools }) bunları tekrar fiyatlar | DO içinde gömülü price() (§9) için satırların tarifesi gerekir |
| A92 | metering.commit(fn) adapter bağlar, metering.commit(plan) plan yazar (argüman türüne göre) | §7 metering.commit(plan) kullanıyor |
| A93 | Tip düzeyinde durum: sayaçlar MeterEntry interface birliği; meter() dönüş tipi alias'sız yazılır | Alias önceki durumu alias argümanı olarak taşıyor, ~100 sayaçta TS2589. 500 sayaç: ~0,6 sn tip kontrolü (havuzlar artımlı tutulur) |
| A94 | getRate callback'i dört parametreyi de yazmalı (_ctx, _at) | TS birlik-tuple rest parametresinde eksik parametreli callback'i reddediyor |
| A95 | typed<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 |
| A96 | Pratik zincir sınırı ~800 .meter(): TS parser'ı 1000 zincirli çağrıda yığını taşırıyor | Katalog bölünebilir; hedef 500 |
| A97 | B22 (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 |
| A98 | Dö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çer | Pencereler yarı açık [from, to) (/calendar, /gauge) |
| A99 | Aynı (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ğil | commit imzası void; yazıldı/yazılmadı bilgisini taşımak adapter kontratını büyütür. Gerekirse ileride commit { duplicate: true } dönebilir |
| A100 | Sayaç (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 |
| A101 | Bir 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ğildir | Aynı anahtarlı ayrı satırlar extend'de kademe konumlarını çakıştırıyordu (inceleme #1) ve capture edilemiyordu (#6) |
| A102 | Adapter'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ür | Farklı 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) |
| A103 | minimumCommit 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) |
| A104 | Float 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 zincirler | 12 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) |
| A105 | SQLite 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 |
| A106 | volume + 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") |
| A107 | Kota: 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 |
| A108 | Koltuk gibi inip çıkan seviyelerin limiti uygulamadadır (examples/saas-seats/src/seats.ts); limit yalnızca artan olay sayaçları içindir | Sayaçlar yalnızca artar; koltuk silinip yeniden eklenebilir |
Closed open questions
Section titled “Closed open questions”| # | 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) |
A few reading notes
Section titled “A few reading notes”- No silent zero (A58, A82): a tariff that needs
usedSoFarreturnsusage_requiredwhen 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/sqliteandpricemeter/cloudflaresubpaths; they have no dependencies. - Result = the price of this call (A99): a repeated call returns the recomputed
charged; whether anything was written iscommit’s knowledge. - Counter scope belongs to the app (A100): the reference adapters count per meter; tiers per dimension value are a
getRatechoice.