# Dấu vân tay kho — và cách dựng lại

Mọi con số trong gói này đo trên **một kho cụ thể**. Kho khác thì số khác, và
bảng không tự nói ra điều đó. Nên trước khi đọc bất cứ bảng nào: kiểm kho có
khớp dấu vân tay dưới đây không.

```bash
python scripts/kiem_kho.py        # hoac: make kiem-kho
```

---

## Dấu vân tay chuẩn *(đo 27/08/2026)*

| | số |
|---|---|
| `Document` | **24.025** |
| `Provision` | **94.923** |
| `HAS_PROVISION` | 94.923 |
| `CITES` | **46.968** |
| `ABOLISHED_BY` | **467** |
| `REPLACED_BY` | 844 |
| `AMENDED_BY` | 673 |
| `SUPPLEMENTED_BY` | 62 |
| cạnh mang `pham_vi_boi` | **126** |
| cạnh mang `suy_ra` | **386** |
| `scope_text` khác rỗng trên cạnh giết | **136** |
| `is_partial = true` trên cạnh giết | 148 |
| `Provision.cap` khác NULL | **0** |
| `nguon_ghi = corpus_quan_ly` | **19.575** |
| `nguon_ghi = nguoi_dung_tai_len` | 0 |
| `nguon_ghi = khong_ro` | **4.450** |
| `Document` chưa gắn `nguon_ghi` | **0** |

### Ba con số dễ đọc nhầm

**`Document 24.025` KHÔNG phải 24.025 văn bản thật.** **4.450 trong đó là stub** —
chỉ có `id` + `so_hieu`, không có điều khoản nào, sinh ra vì một văn bản khác dẫn
chiếu tới chúng mà chưa ai cào về. Số văn bản **thật** là **19.575**. Đếm
`Document` trần rồi công bố là **nói quá kho lên 23%**.

Trùng khớp đáng chú ý: `khong_ro = 4.450` đúng bằng số stub, và
`corpus_quan_ly = 19.575` đúng bằng số văn bản thật — vì phép gán `nguon_ghi` chỉ
đánh `corpus_quan_ly` cho `doc_id` có mặt trong `chunks.jsonl`, mà stub thì không
có mặt ở đó.

**`Provision.cap = 0` là ĐÚNG, không phải thiếu sót.** Thuộc tính `cap` chưa được
gán trên toàn kho. Mọi vị ngữ Cypher đọc `p.cap` phải dùng `coalesce(p.cap,
'dieu')` — viết thẳng `p.cap = 'dieu'` sẽ làm **mọi tra cứu theo số Điều im lặng
trả rỗng**, vì trong Cypher `NULL = 'dieu'` cho ra NULL chứ không phải FALSE. Xem
[`truy-hoi-va-dinh-vi.md`](truy-hoi-va-dinh-vi.md) §2.

**`scope_text 136` và `pham_vi_boi 126` KHÔNG có trong `data/raw`.** Xem mục kế.

---

## Trạng thái SUY RA — thứ không nằm trong nguồn

Kho này có hai loại dữ liệu, và lượt nạp đối xử với chúng khác nhau:

| | ở đâu ra | có trong `data/raw`? |
|---|---|---|
| văn bản, điều khoản, quan hệ | cào về rồi nạp | có |
| `scope_text`, `is_partial`, `pham_vi_boi`, `suy_ra` | **máy suy ra sau khi nạp** | **không** |

Đo 26/08 trên cạnh giết (`REPLACED_BY` + `ABOLISHED_BY`):

```
scope_text khac rong    graph 136    data/raw  11
is_partial = true       graph 148    data/raw  32
```

**126/136 chú giải phạm vi là do `scripts/doi_chieu_quan_he.py` gán thẳng lên
cạnh**, đánh dấu bằng `rel.pham_vi_boi`. Chúng chưa bao giờ nằm trong `data/raw`.

Đây là một **pipeline bất đối xứng**: bên bồi đắp chỉ ghi vào graph, bên nạp chỉ
đọc từ nguồn rồi ghi đè. Nên bản `ingest_relations` cũ, vốn `SET rel.scope_text =
r.scope_text` **vô điều kiện**, kéo `136 → 11` và `148 → 32` mỗi lần chạy —
im lặng.

Và hệ quả tệ hơn mất cờ: `pham_vi_boi` **không** nằm trong danh sách `SET` nên nó
**sống sót** trên chính cạnh vừa bị xoá sạch scope. Chạy khô đo được **126 cạnh
tự mâu thuẫn** — mang lời khai *"phạm vi này do máy gán"* mà scope thì trống.

Luật giữ (PA-1) nay là:

```cypher
rel.scope_text = CASE
    WHEN rel.pham_vi_boi IS NOT NULL
         AND trim(coalesce(r.scope_text, '')) = ''
    THEN rel.scope_text ELSE r.scope_text END
```

Đọc là: *chỉ giữ khi cạnh mang dấu máy-gán **và** nguồn không có gì để ghi đè.*
Nguồn có scope thật thì nguồn vẫn thắng — PA-1 không khoá cứng.
Bộ khoá: `tests/test_scripts/test_pa1_khong_de_chu_giai.py`.

---

## Quyền sở hữu — vì sao lượt dọn không xoá tài liệu người dùng

`nguon_ghi` phân biệt tài liệu do corpus quản lý với tài liệu người dùng tải lên.
Nó tồn tại vì một lỗi mất dữ liệu **của sản phẩm**, không phải của benchmark.

Vị ngữ dọn dẹp cũ hỏi *"tài liệu này có trong `chunks.jsonl` không"* — tức hỏi về
**ĐƯỜNG NẠP**, trong khi việc cần hỏi là **AI SỞ HỮU**. Mọi tài liệu khách hàng
tải lên qua API đều không có trong `chunks.jsonl`, nên **lượt nạp kế tiếp sẽ xoá
sạch chúng**.

Không dùng được `corpus` để phân biệt: đường tải lên đặt
`corpus = first.get("corpus") or "thue"` — **giống hệt** corpus quản lý. `is_stub`
cũng không, vì đường ấy đặt `is_stub = false`.

`src/core/nguon_ghi.py` là nguồn duy nhất của ngữ nghĩa này, và nó **an toàn theo
hướng không xoá**:

```python
DUOC_PHEP_DON = frozenset({CORPUS_QUAN_LY})

def duoc_phep_don(nguon: str | None) -> bool:
    return nguon in DUOC_PHEP_DON        # None -> False
```

`nguoi_dung_tai_len` và `khong_ro` **không bao giờ** bị dọn. Cả ba vị ngữ dọn dẹp
trong `scripts/ingest_db.py` đều mang thêm `d.nguon_ghi = $nguon` — **cộng thêm**
vào phép so tập chuẩn cũ, không thay thế nó.

Bộ khoá: `tests/test_scripts/test_cleanup_ton_trong_so_huu.py` — chạy **Cypher
thật** trên Neo4j sống, trong transaction luôn rollback. Nó phải chạm kho thật vì
thứ nó kiểm là *"câu tuyển chọn có chọn đúng node không"*, mà điều đó không so
chuỗi được: `AND d.nguon_ghi = $nguon` đặt nhầm chỗ trong `WHERE` vẫn giữ nguyên
mọi chuỗi.

---

## Dựng lại kho

⚠️ **`data/raw/` và `chunks.jsonl` KHÔNG nằm trong Git** (gitignore) — dung lượng
lớn và có ràng buộc giấy phép. Xem `docs/nguon-du-lieu-va-giay-phep.md`. Mục này
mô tả cách dựng lại, không phải cách tải về.

### Thứ tự — không đảo được

```bash
# 1. Nap van ban + dieu khoan tu nguon
python scripts/ingest_db.py

# 2. Dung chuoi thay the (sinh canh `suy_ra` — 386)
python scripts/dung_chuoi_thay_the.py

# 3. Doi chieu quan he (sinh `scope_text`/`pham_vi_boi` — 126)
python scripts/doi_chieu_quan_he.py

# 4. Gan quyen so huu (CHAY KHO truoc, mac dinh la dry-run)
python scripts/danh_dau_nguon_ghi.py            # xem truoc (mac dinh chay kho)
python scripts/danh_dau_nguon_ghi.py --ap-dung  # ghi that

# 5. Kiem — phai khop bang dau trang nay
python scripts/kiem_kho.py
```

**Bước 2 và 3 phải chạy SAU bước 1 và không được bỏ.** Chúng sinh ra 386 + 126
cạnh/chú giải mà nguồn không có. Bỏ chúng thì kho trông vẫn "đủ văn bản" nhưng
tầng hiệu lực mất phần lớn dữ kiện, và không bảng nào báo động.

**Chạy lại bước 1 sau đó là an toàn** nhờ PA-1 — nhưng chỉ vì PA-1 có ở đó. Đừng
gỡ nó ra để "cho lượt nạp sạch hơn".

### Ước lượng thời gian

Dựng từ đầu kể cả nhúng: **~7,5 giờ**. Đây là lý do một số lỗi được chữa **trên
đường ĐỌC** thay vì nạp lại kho — ví dụ `nhan_dieu_gon` trong `src/api/safety.py`
chuẩn hoá nhãn điều lúc đọc, vì **26,4% provision trong kho đã nạp mang nhãn là
thân điều bị cắt cụt**.

### Kho demo KHÁC kho benchmark

Máy demo Mac mini có **8.019 file raw**; kho benchmark có **19.627**. Hai kho
khác nhau, và **không được đồng bộ tuỳ tiện** — sửa kho bên này không cải thiện
bên kia. Mọi số trong gói này đo trên kho benchmark.
