# `sha256-utc-iso-v2` — yaprak kuralının tam tanımı

Bu belge, bir denetim satırının **tek bir metne** nasıl çevrildiğini tarif eder. Zincirin tamamı bu metnin üzerine kuruludur: metin değişirse özet değişir, özet değişirse kök değişir, kök değişirse o mühürden sonraki bütün halkalar değişir.

⛔ **Kural veritabanındadır, bu belge onun tarifidir.** İkisi çelişirse veritabanındaki tanım esastır ve okunabilir:

```
\df+ app.audit_leaf_line
\df+ app.audit_leaf_part
\df+ app.audit_leaf_time
```

Her mühür, hangi kuralla alındığını `hash_algorithm` alanında taşır. Bu belge yalnız `sha256-utc-iso-v2` kuralını anlatır; başka bir ad taşıyan mühür bu kurala göre hesaplanamaz ve "doğrulanamadı" olarak raporlanmalıdır — "kırık" olarak değil.

---

## 1 · Alan kodlaması — uzunluk öneki

Her alan şu biçimde yazılır:

```
<bayt uzunluğu>:<değer>;
```

Değer boşsa (NULL):

```
-;
```

**Neden ayırıcı değil de uzunluk?** Denetim satırları serbest metin taşır — bir adres, bir tarayıcı kimliği, bir etiket. Hangi karakteri ayırıcı seçerseniz seçin, o karakter değerlerin içinde geçebilir: `a|bc` ile `ab|c` aynı metni üretir ve iki farklı satır aynı yaprağa düşer. Uzunluk, değerin nerede bittiğini söyler ve tartışma bırakmaz.

**Neden bayt?** Bayt kesindir. Karakterin ne olduğu — birleşik harfler, emoji, yön işaretleri — yoruma bağlıdır. PostgreSQL tarafında `octet_length` kullanılır.

**NULL neden kendi işaretini taşır?** Boş metin ile "değer yok" farklı şeylerdir ve `0:;` ile `-;` ayrımı bunu korur.

---

## 2 · Zaman biçimi

Zaman damgaları **her zaman UTC'ye çevrilir** ve şu kalıpla yazılır:

```
YYYY-MM-DD"T"HH24:MI:SS.US
```

Örnek: `2026-08-10T06:00:00.123456`

⚠ **Bu, kuralın en kolay gözden kaçan yeridir.** Bir zaman damgasını metne çevirmek, oturumun `TimeZone` ve `DateStyle` ayarlarına bağlıdır: aynı satır `Europe/Istanbul` altında ve `German, DMY` altında üç farklı metin üretir. Ayar sabitlenmemiş olsaydı, dışarıdan doğrulayan kişi farklı bir kök hesaplar ve zinciri "bozuk" okurdu — sistem sağlamken.

Kendi oturumunuzda sınayabilirsiniz:

```sql
SET TimeZone = 'Europe/Istanbul';
SELECT app.audit_leaf_time(created_at) FROM audit_writes ORDER BY id LIMIT 1;

SET TimeZone = 'UTC';
SELECT app.audit_leaf_time(created_at) FROM audit_writes ORDER BY id LIMIT 1;
```

İki sonuç **aynı** olmalıdır.

---

## 3 · Hangi kolonlar, hangi sırayla

⛔ **Satırın HER kolonu**, kimliği değil. Kuralın ilk sürümü yalnız `(id, created_at, tenant_id)` üçlüsünü kapsıyordu; bu, "kümedeki bir satırı değiştirin, kök değişir" iddiasını zarf için doğru, içerik için yanlış yapıyordu — bir onay kaydının `new_values` veya `user_id` alanını yeniden yazmak kökü, zinciri, tanığı ve doğrulama işini temiz bırakıyordu. `v2` tam olarak bunu kapatır.

### `audit_writes`

Sırasıyla:

1. `id`
2. `created_at` (UTC kalıbıyla)
3. `updated_at` (UTC kalıbıyla)
4. `tenant_id`
5. `actor_type`
6. `user_type`
7. `user_id`
8. `event`
9. `auditable_type`
10. `auditable_id`
11. `old_values` (metin hâli)
12. `new_values` (metin hâli)
13. `url`
14. `ip_address`
15. `user_agent`
16. `tags`

### `audit_access`

Sırasıyla:

1. `id`
2. `created_at` (UTC kalıbıyla)
3. `updated_at` (UTC kalıbıyla)
4. `tenant_id`
5. `actor_type`
6. `user_type`
7. `user_id`
8. `auditable_type`
9. `auditable_id`
10. `field_class`
11. `context` (metin hâli)
12. `url`
13. `ip_address`
14. `session_id`

Her alan §1'deki kodlamayla yazılır ve hepsi **birleştirilir**. Yaprak metni budur.

---

## 4 · Yapraktan köke

1. Her yaprağın `sha256` özeti alınır ve **onaltılık** (hex, küçük harf) yazılır.
2. Özetler **`id` sırasına göre** dizilir.
3. İkişer ikişer birleştirilip (`sol || sağ`, iki hex metnin yan yana yazılması) her çiftin `sha256`'sı alınır.
4. Bir katta tek düğüm kalırsa **olduğu gibi bir üst kata taşınır**.
5. Tek özet kalana kadar sürdürülür. Kalan özet **köktür**.

⚠ **Dördüncü madde önemlidir.** Yaygın bir hata, tek kalan düğümü kendisiyle eşlemektir; bu, farklı iki kümenin aynı kökü üretebilmesine yol açar (Merkle "malleability" hatası). Burada eşleme yapılmaz.

⚠ Bir kümede hiç satır yoksa kök yoktur (NULL). Tek satır varsa o satırın özeti köktür.

---

## 5 · Kökten zincire

Her mühür, kendinden öncekine şöyle bağlanır:

```
chain_hash = sha256(previous_chain_hash || merkle_root)
```

`previous_chain_hash` bir önceki mührün `chain_hash` değeridir. İlk mühürde önceki yoktur ve **boş metin** olarak alınır.

Bu yüzden bir satırı değiştirmek yerel kalamaz: o satırın kümesinin kökü değişir, o mührün zincir özeti değişir, ve ondan sonraki her mührün zincir özeti değişir.

---

## 6 · Mühürün aralığı

⚠ `from_id` **dışlayıcı**, `to_id` **kapsayıcıdır**:

```
WHERE id > from_id AND id <= to_id
```

Bir mühür, bir önceki mührün bittiği yerden başlar; `from_id` o önceki mührün son `id`'sidir ve bu kümeye ait değildir. Aralığı kapsayıcı okumak her mühre fazladan bir satır katar ve hiçbir kök tutmaz.

---

## 7 · Doğrulamanın kendisi

Bir mühür için:

1. Kümenin satırlarını oku (§6'daki aralıkla).
2. Her satırı §1–§3'e göre metne çevir.
3. §4'e göre kökü hesapla.
4. Bulduğun kökü mührün `merkle_root` alanıyla karşılaştır.
5. §5'e göre zincir özetini hesapla, `chain_hash` ile karşılaştır.
6. Mührün `previous_chain_hash` alanının, kendinden önceki mührün `chain_hash` alanına eşit olduğunu doğrula.

Üçü de tutuyorsa o mühür sağlamdır. **Bunların hiçbiri 3E'nin uygulama kodunu gerektirmez** — pakettteki `dogrula.py` bu altı adımı yapar ve içinde `hashlib` dışında bir şey yoktur.

---

## 8 · Zincirin kanıtlamadığı şey

⚠ Buradaki hiçbir şey zincirin **ne zaman** kurulduğunu söylemez. Geçen mart mühürlenmiş bir zincirle bu sabah baştan üretilmiş birini ayırt edemezsiniz. Bunu söyleyen şey **dış tanıktır**: mühürler yayımlandığı depoda, yayımlandıkları tarihle durur ve o depo silinmeye ve geçmişi yeniden yazmaya kapalıdır. `tanik.txt` adresleri verir.
