# Kế hoạch xử lý triệt để drift schema Postgres trong DSCons

## 1. Phạm vi và mục tiêu

Tài liệu này tập trung vào vấn đề lệch schema giữa:

- `app/core/postgres.py` như là "schema contract ngầm" đang được code runtime sử dụng
- `docker/postgres/init/01_init.sql` như là schema bootstrap hiện đang dùng để khởi tạo môi trường mới
- các test persistence/routes hiện có, chủ yếu xác nhận behavior ở service layer nhưng chưa khóa contract DB thật

Mục tiêu không phải chỉ "sửa vài cột lệch", mà là thiết lập mô hình quản trị schema để:

- ngăn drift tái diễn
- rollout thay đổi không phá dữ liệu đang có
- hỗ trợ các hướng hardening khác như workflow transaction, shared projection và integration verification
- tạo được CI guardrail phát hiện drift sớm

## 2. Root cause sâu

Drift hiện tại không đơn thuần là lỗi đặt tên cột. Nó đến từ 4 nguyên nhân cấu trúc:

1. **Code-first nhưng không có source-of-truth chính thức**
   - `app/core/postgres.py` encode trực tiếp tên bảng/cột/check-value trong raw SQL.
   - `docker/postgres/init/01_init.sql` lại được cập nhật theo hướng infra/bootstrap.
   - Không có lớp migration/versioning nối hai phía.

2. **Raw SQL ở application đang là contract thật**
   - Các câu `INSERT`, `SELECT`, `ON CONFLICT`, `UPDATE`, trigger assumption và enum-like value trong `PostgresClient` chính là nơi hệ thống fail nếu schema lệch.
   - Nhưng contract này không được trích xuất, version hóa hay kiểm tra tự động.

3. **Init SQL đang gánh đồng thời 3 vai trò**
   - tạo schema
   - bootstrap dữ liệu mẫu
   - định nghĩa compatibility/constraint
   Khi một file làm quá nhiều việc, việc chỉnh nhanh để local chạy được dễ làm lệch runtime expectation.

4. **Thiếu test drift ở tầng DB thật**
   - Unit test hiện dùng fake client, rất tốt để test orchestration nhưng không bảo vệ được against:
     - tên cột lệch
     - kiểu dữ liệu lệch
     - unique/index/constraint lệch
     - check constraint value set lệch
     - bootstrapped database khác runtime database

## 3. Taxonomy drift

Dưới đây là taxonomy drift nên dùng cho DSCons. Nó quan trọng vì mỗi loại drift có chiến lược fix/rollback khác nhau.

### 3.1. Structural drift
Lệch về cấu trúc vật lý:
- thiếu bảng
- thiếu cột
- cột đổi tên
- cột nullable khác kỳ vọng
- PK/FK khác
- index/unique constraint khác

### 3.2. Type drift
Lệch về kiểu dữ liệu hoặc casting expectation:
- code expect `jsonb` nhưng schema là `text`
- code insert `dict/list -> ::jsonb` nhưng cột không phải `jsonb`
- code expect `uuid/date/timestamptz` cast được nhưng schema khác
- precision numeric không đủ

### 3.3. Semantic-value drift
Lệch về bộ giá trị hợp lệ trong check constraint hoặc normalization:
- code map status sang `planned`, `in_progress`, `blocked`, nhưng DB check constraint chỉ cho `draft/active/...`
- code ghi priority `'high'`/`'medium'` trong vài luồng, nhưng API/seed có thể dùng tiếng Việt hoặc value khác
- workflow action type do service chuẩn hóa nhưng DB check constraint chưa cho phép

### 3.4. Naming drift
Lệch giữa tên cột thực tế và tên code dùng:
- `metadata` trong code vs `session_metadata` / `event_metadata` trong SQL
- `review_session_id` được insert ở code nhưng không tồn tại ở SQL table tương ứng
- `dossier_scope` được code coi là JSON payload nhưng SQL đang để `VARCHAR(100)`

### 3.5. Lifecycle drift
Schema bootstrap khác schema sau khi hệ thống chạy lâu:
- init SQL tạo DB mới một kiểu
- local/prod đã bị patch tay hoặc bằng script tmp một kiểu khác
- code đang vô tình tương thích với "schema đã sửa tay", không phải schema từ init

### 3.6. Seed drift
Seed data không còn phù hợp với constraint/logic mới:
- bootstrap record có status/priority hợp lệ ở thời điểm cũ nhưng không khớp validation hiện tại
- dữ liệu mẫu che mất bug do không đi qua full workflow path

### 3.7. Observability drift
Hệ thống không biết DB đang ở schema version nào:
- không có `schema_migrations`
- không có schema fingerprint
- readiness/health không báo schema mismatch

## 4. Các drift cụ thể hiện thấy từ codebase

## 4.1. `employee_work_log_sessions.metadata` vs `session_metadata`
Trong `app/core/postgres.py`:
- `upsert_employee_work_log_session()` insert vào cột `metadata`
- `ON CONFLICT` cũng merge `employee_work_log_sessions.metadata`

Trong `docker/postgres/init/01_init.sql`:
- bảng `employee_work_log_sessions` dùng cột `session_metadata`

Tác động:
- môi trường bootstrap từ `01_init.sql` sẽ fail ngay khi code gọi upsert session
- đây là drift structural + naming + lifecycle

## 4.2. `employee_work_log_actions.metadata` vs `event_metadata`
Trong code:
- `append_employee_work_log_action()` insert/merge vào `metadata`

Trong SQL:
- bảng `employee_work_log_actions` dùng `event_metadata`

Tác động:
- append action sẽ fail ở DB mới khởi tạo từ init
- CI/unit test fake hiện không chặn được

## 4.3. `employee_work_log_actions.shift_date` được code insert nhưng SQL không có
Trong code:
- `append_employee_work_log_action()` insert trường `shift_date`

Trong SQL:
- bảng `employee_work_log_actions` không có cột `shift_date`

Tác động:
- lỗi insert cứng, không phải degraded behavior
- đây là drift structural nghiêm trọng vì route/service path ghi log action sẽ hỏng toàn phần trên schema bootstrap mới

## 4.4. `dossier_review_submissions.review_session_id` được code insert nhưng SQL không có
Trong code:
- `submit_dossier_review_supplement()` insert vào `dossier_review_submissions(review_session_id, ...)`

Trong SQL:
- bảng `dossier_review_submissions` không có `review_session_id`

Tác động:
- workflow persisted không thể submit supplement trên DB bootstrap
- đây là drift trực tiếp trong workflow lifecycle

## 4.5. `dossier_review_sessions.dossier_scope` type drift
Trong code:
- `_build_create_session_params()` serialize `dossier_scope` bằng `json.dumps(...)`
- `create_dossier_review_session()` insert `%(... )s` vào `dossier_scope`

Trong SQL:
- `dossier_scope VARCHAR(100) NOT NULL DEFAULT 'project_dossier'`

Tác động:
- code đang xem đây là object-like contract, SQL lại coi là enum/string
- dù value có thể vô tình stringify được, semantics đã lệch
- đây là type drift + semantic drift

## 4.6. `ON CONFLICT ... COALESCE(external_ref, '')` khả năng không hợp lệ như index target
Trong code:
- `append_employee_work_log_action()` dùng `ON CONFLICT (work_log_session_id, action_timestamp, action, COALESCE(external_ref, ''))`

Trong SQL:
- tạo unique constraint với expression `COALESCE(external_ref, '')`

Rủi ro:
- expression index/constraint target trong `ON CONFLICT` phải khớp rất chặt; pattern này khó bảo trì, dễ fail khi migrate hoặc khi PostgreSQL version/DDL khác chút
- không hẳn drift tuyệt đối, nhưng là schema contract mong manh

## 4.7. Status/value-set drift ở employee work log
Trong service test:
- `WorkflowPersistenceService._normalize_work_log_status()` map ra các giá trị như `planned`, `in_progress`, `blocked`, `completed`

Trong SQL:
- `employee_work_log_sessions.status` chỉ cho:
  - `draft`, `active`, `in_progress`, `completed`, `blocked`, `on_hold`, `cancelled`

Tác động:
- nếu bất kỳ code path nào persist `planned`, DB sẽ reject
- hiện test đã xác nhận normalization ra `planned`, nên drift semantic-value là có thật ở contract tổng thể giữa service và DB

## 4.8. Action type drift ở employee work log actions
Trong test workflow persistence:
- expected action types: `system_sync`, `issue_recorded`, `progress_update`, `task_update`

Trong SQL:
- check constraint cho action_type gồm:
  - `manual_update`, `ai_analysis`, `task_update`, `issue_recorded`, `handover_note`, `progress_update`, `checklist_update`, `system_sync`

Phần này hiện **không drift**, nhưng là contract cần khóa trong governance vì rất dễ lệch khi service thêm mapping mới.

## 4.9. Seed/bootstrap coupling drift
`01_init.sql` vừa:
- tạo schema
- tạo seed company/project/employee
- tạo cả pilot logs mẫu

Rủi ro:
- khi schema đổi, seed có thể còn insert được nhưng không còn phản ánh workflow thật
- người dev dễ sửa seed để "database khởi động được" mà không sửa application contract

## 5. Đề xuất source-of-truth

## 5.1. Source-of-truth nên là migration history, không phải file init đơn lẻ
Đề xuất chính thức:

- **Source-of-truth chuẩn:** thư mục migration versioned trong repo
- **Derived artifact:** `docker/postgres/init/01_init.sql` chỉ là snapshot bootstrap được sinh hoặc đồng bộ từ migration baseline
- **Runtime contract reference:** tài liệu schema contract cho các bảng mà `app/core/postgres.py` chạm tới

Lý do:
- code raw SQL cần schema ổn định theo thời gian, không chỉ ở trạng thái "mới init"
- migration history cho phép upgrade dần trên DB có dữ liệu
- init SQL chỉ phù hợp cho DB trắng, không đủ làm chuẩn duy nhất cho hệ đã chạy

## 5.2. Nếu chưa đưa migration framework ngay, vẫn phải chọn chuẩn tạm thời
Trong giai đoạn chuyển tiếp:
- coi `app/core/postgres.py` là **runtime compatibility source**
- coi `01_init.sql` là **bootstrap artifact phải tương thích 100% với runtime compatibility source**
- mọi thay đổi schema phải được review theo tiêu chí: "code hiện tại có chạy được trên DB mới init không?"

## 6. Target governance model

## 6.1. Artifact model
Đề xuất mô hình artifact theo thứ tự ưu tiên:

1. `db/migrations/`  
   - mỗi thay đổi schema là một migration bất biến, đánh số version
2. `db/contracts/postgres_runtime_contract.md`  
   - mô tả bảng/cột/index/check-value mà application phụ thuộc
3. `docker/postgres/init/01_init.sql`  
   - bootstrap baseline cho local/dev, được sync từ migration baseline
4. drift checker script trong `tools/` hoặc `tests/`

Không cần thay framework ORM; vẫn giữ raw SQL, nhưng schema phải được quản trị như product artifact.

## 6.2. Ownership model
- `app/core/postgres.py`: owner phía application/runtime
- `docker/postgres/init/01_init.sql`: owner phía bootstrap/infra
- migration + contract doc: shared ownership, review bắt buộc từ cả hai phía

Rule:
- PR thay raw SQL phải đồng thời đánh giá impact lên migration/init
- PR thay init/migration phải chứng minh runtime SQL không drift

## 6.3. Schema version visibility
Cần có bảng chuẩn:
- `schema_migrations(version, description, applied_at, checksum)`

Và health/readiness tương lai nên expose:
- current schema version
- expected app schema version
- mismatch flag

Điểm này liên kết trực tiếp với sub-plan observability (#3).

## 6.4. Contract tiers
Nên chia contract DB thành 3 tier:

### Tier A - hard runtime contract
Nếu lệch là app fail ngay:
- tên bảng/cột
- data type
- nullability quan trọng
- FK cần cho join chính
- unique/index target dùng trong `ON CONFLICT`
- check constraint value set mà service thường ghi

### Tier B - performance contract
Lệch chưa fail logic ngay nhưng gây thoái hóa:
- index hỗ trợ query
- GIN index cho jsonb
- sort-support index

### Tier C - bootstrap/seed contract
- dữ liệu mẫu
- seed workflows
- demo projects

CI phải fail cứng ở Tier A, cảnh báo ở Tier B, optional ở Tier C.

## 7. Chiến lược migration/versioning/schema contract

## 7.1. Phase 0: Baseline và freeze
Trước khi đổi gì:
- chụp schema thực tế của DB đang dùng trong local/staging nếu có
- so sánh với `01_init.sql`
- lập baseline drift report
- freeze thay đổi schema ad-hoc trong code không qua kế hoạch

Output:
- danh sách drift canonical
- quyết định canonical names/types

## 7.2. Phase 1: Thiết lập migration backbone
Thêm cơ chế migration versioned, có thể dùng SQL thuần trong repo nếu chưa muốn thêm dependency mới.

Đề xuất cấu trúc:
- `db/migrations/0001_baseline.sql`
- `db/migrations/0002_runtime_compat_worklog_and_review.sql`
- `db/migrations/0003_drop_legacy_aliases.sql`

Nguyên tắc:
- migration chỉ append, không sửa file cũ
- init SQL được rebuild/sync từ baseline + migrations đã squash khi cần

## 7.3. Phase 2: Compatibility-first migration
Với các drift đang có, ưu tiên migration additive trước destructive:
- thêm cột alias mới hoặc cột canonical mới
- backfill dữ liệu
- cập nhật trigger/view nếu cần
- chỉ drop cột cũ sau khi app đã chạy ổn qua ít nhất một chu kỳ release

Ví dụ chiến lược:

### Work log sessions
- canonical cần chọn giữa `metadata` và `session_metadata`
- khuyến nghị chọn **`metadata`** nếu muốn thống nhất naming với các bảng khác (`dossier_review_*` đang dùng `metadata`)
- migration:
  1. thêm `metadata JSONB` nếu chưa có
  2. backfill `metadata = session_metadata` cho row cũ
  3. tạo trigger đồng bộ hai chiều tạm thời hoặc update code sau đó
  4. sau rollout ổn định mới drop `session_metadata`

### Work log actions
- canonical chọn `metadata` thay vì `event_metadata` để đồng nhất model
- thêm `shift_date DATE` nếu code thực sự cần và nghiệp vụ chấp nhận
- nếu không cần nghiệp vụ, phase sau mới xóa reference trong code; nhưng vì task này là planning, rollout an toàn nhất hiện tại là **thêm cột để tương thích trước**

### Dossier review submissions
- thêm `review_session_id UUID` nếu workflow/query cần join trực tiếp theo session
- backfill từ `assignment_id -> finding_id -> review_session_id`
- thêm FK/index tương ứng

### Dossier review sessions.dossier_scope
Có 2 lựa chọn:

**Lựa chọn A: canonical là `JSONB`**
- phù hợp với code hiện serialize object
- linh hoạt cho scope tương lai

**Lựa chọn B: canonical là `VARCHAR`**
- đơn giản hơn
- ép service chỉ truyền enum/string

Khuyến nghị cho DSCons: **A - chuyển sang `JSONB`**, vì workflow docs và request schema cho thấy `dossier_scope` có xu hướng là cấu trúc mở rộng, không chỉ một enum string.

## 7.4. Phase 3: Contract lock
Sau khi migration compatibility đã xong:
- cập nhật init SQL để phản ánh canonical schema
- thêm drift checker CI
- thêm integration test against real Postgres bootstrap từ init + migrate
- đánh dấu cột/tên legacy là deprecated

## 7.5. Phase 4: Legacy cleanup
Chỉ thực hiện sau khi:
- app đã chạy qua staging/production cycle ổn định
- không còn query nào chạm cột legacy
- drift checker xác nhận bootstrap và migrated DB ra cùng fingerprint

Khi đó mới:
- drop cột alias cũ
- drop trigger đồng bộ tạm
- drop compatibility views nếu có

## 8. Kế hoạch migration chi tiết

## 8.1. Quyết định canonical schema cần chốt

### Nhóm employee work log
- `employee_work_log_sessions.metadata` là canonical
- `employee_work_log_actions.metadata` là canonical
- `employee_work_log_actions.shift_date` cần tồn tại nếu application còn insert và nghiệp vụ session/action theo ngày vẫn hữu ích

### Nhóm dossier review
- `dossier_review_submissions.review_session_id` cần tồn tại vì code đang dùng và truy xuất lifecycle theo review
- `dossier_review_sessions.dossier_scope` canonical là `JSONB`

### Constraint/value-set
- chuẩn hóa `employee_work_log_sessions.status` để bao phủ giá trị runtime thực dùng, hoặc ép service không bao giờ persist giá trị ngoài set
- vì test/service đang map ra `planned`, canonical tốt hơn là:
  - hoặc thêm `planned`
  - hoặc đổi normalization layer để không bao giờ ghi `planned`
  
Vì đây là kế hoạch schema, khuyến nghị:
- **không mở rộng check constraint vội** nếu `planned` chỉ là abstraction ở service
- thay vào đó, trong contract doc phải ghi rõ persisted status allowed set
- integration test phải chứng minh service thực tế không insert `planned`

## 8.2. Migration batch đề xuất

### Migration 0001 - baseline snapshot
- snapshot schema hiện đang chọn làm điểm khởi đầu
- chưa sửa logic, chỉ ghi version

### Migration 0002 - compatibility columns
- add `employee_work_log_sessions.metadata JSONB DEFAULT '{}'::jsonb`
- backfill từ `session_metadata`
- add `employee_work_log_actions.metadata JSONB DEFAULT '{}'::jsonb`
- backfill từ `event_metadata`
- add `employee_work_log_actions.shift_date DATE`
- backfill từ parent session `shift_date`
- add `dossier_review_submissions.review_session_id UUID`
- backfill từ join assignment/finding/review
- add FK/index cho `review_session_id`
- add `dossier_scope_json JSONB` hoặc trực tiếp convert `dossier_scope` sang JSONB tùy chiến lược cutover

### Migration 0003 - canonicalization
Nếu chọn in-place:
- alter `dossier_review_sessions.dossier_scope` sang JSONB using conversion rule  
Nếu chọn safer:
- add `dossier_scope_json JSONB`
- backfill from string/object
- application cutover
- rename sau

Khuyến nghị safer:
- add `dossier_scope_json`
- populate:
  - nếu string kiểu `project_dossier` -> `{"scope": "project_dossier"}`
  - nếu đã là JSON string hợp lệ -> parse object
- app cutover sau đó rename ở migration sau

### Migration 0004 - sync bootstrap
- cập nhật `docker/postgres/init/01_init.sql` về canonical schema cuối phase compatibility
- loại bỏ tên cũ khỏi init mới
- giữ seed data nhưng tách logic seed khỏi schema sections rõ ràng

### Migration 0005 - cleanup legacy
- drop `session_metadata` nếu không còn dùng
- drop `event_metadata`
- drop legacy `dossier_scope` varchar nếu đã chuyển sang jsonb canonical
- finalize indexes/constraints

## 8.3. Backfill strategy

### Nguyên tắc
- backfill phải idempotent
- chạy theo transaction từng migration
- ghi log số row bị ảnh hưởng
- có validation query sau backfill

### Backfill cụ thể

#### `session_metadata -> metadata`
```sql
UPDATE employee_work_log_sessions
SET metadata = COALESCE(metadata, '{}'::jsonb) || COALESCE(session_metadata, '{}'::jsonb)
WHERE session_metadata IS NOT NULL;
```

#### `event_metadata -> metadata`
```sql
UPDATE employee_work_log_actions
SET metadata = COALESCE(metadata, '{}'::jsonb) || COALESCE(event_metadata, '{}'::jsonb)
WHERE event_metadata IS NOT NULL;
```

#### `shift_date` cho action
```sql
UPDATE employee_work_log_actions a
SET shift_date = s.shift_date
FROM employee_work_log_sessions s
WHERE a.work_log_session_id = s.id
  AND a.shift_date IS NULL;
```

#### `review_session_id` cho submissions
```sql
UPDATE dossier_review_submissions s
SET review_session_id = a.review_session_id
FROM dossier_review_assignments a
WHERE s.assignment_id = a.id
  AND s.review_session_id IS NULL;
```

#### `dossier_scope varchar -> jsonb`
Rule chuyển đổi:
- null/blank -> `{"scope": "project_dossier"}`
- string thường -> `{"scope": <value>}`
- nếu đã lưu JSON string hợp lệ -> parse và giữ object

## 8.4. Compatibility window
Khuyến nghị ít nhất 2 release windows:

- **Window 1:** DB additive migration trước, app cũ vẫn chạy
- **Window 2:** app chuyển sang canonical names/types
- **Window 3:** cleanup legacy

Điều này đặc biệt quan trọng nếu parent plan triển khai integration hardening song song.

## 9. CI chống drift

## 9.1. Drift checks bắt buộc
CI nên có 3 lớp kiểm tra:

### Check A - Static contract scan
Script parse `app/core/postgres.py` để liệt kê:
- bảng được dùng
- cột được insert/update/select
- enum-like literals trong queries quan trọng

So sánh với contract manifest đã lưu.

Mục tiêu:
- fail nhanh khi code thêm cột mới mà migration/init chưa cập nhật

### Check B - Bootstrap DB verification
Spin up Postgres sạch bằng `docker/postgres/init/01_init.sql`, sau đó chạy:
- smoke query cho các path chính trong `PostgresClient`
- ít nhất prepare/execute các statement critical

Mục tiêu:
- chứng minh DB mới init tương thích runtime ngay

### Check C - Migration equivalence verification
Tạo 2 DB:
- DB A: bootstrap trực tiếp bằng `01_init.sql`
- DB B: apply full migration chain từ baseline

So sánh schema fingerprint:
- tables
- columns
- types
- nullability
- defaults
- constraints/index names quan trọng

Hai DB phải tương đương ở Tier A.

## 9.2. Schema fingerprint đề xuất
Fingerprint nên bao gồm:
- `information_schema.columns`
- `pg_constraint`
- `pg_indexes`
- optional: trigger/function names quan trọng

Không cần byte-perfect; cần normalized canonical compare.

## 9.3. Test placement
- drift static checker: `tools/` hoặc `tests/contract/`
- bootstrap compatibility: `tests/integration/`
- migration equivalence: `tests/integration/` hoặc CI-only script

Điểm này nên tích hợp với sub-plan verification (#5).

## 10. Rollout plan không phá dữ liệu

## Phase 1 - Audit và chuẩn hóa quyết định
Thời lượng: 2-3 ngày

Việc làm:
- chốt canonical schema names/types
- lập drift inventory chính thức
- thống nhất source-of-truth model
- freeze hotfix schema ad-hoc

Acceptance:
- có bảng mapping legacy -> canonical
- có danh sách migration cần tạo

## Phase 2 - Additive compatibility migration
Thời lượng: 3-5 ngày

Việc làm:
- thêm cột mới/alias cần thiết
- backfill
- thêm FK/index cho cột mới
- thêm validation query

Acceptance:
- app hiện tại chạy được trên DB migrated
- không mất dữ liệu cũ
- bootstrap mới vẫn chưa cần cutover ngay

## Phase 3 - Bootstrap sync + CI guard
Thời lượng: 2-4 ngày

Việc làm:
- cập nhật `01_init.sql` theo canonical schema
- tách schema section với seed section rõ ràng
- thêm drift checker vào CI
- thêm integration smoke với Postgres thật

Acceptance:
- DB mới init chạy được các path chính của `PostgresClient`
- drift checker fail nếu init và runtime lệch

## Phase 4 - App cutover to canonical contract
Thời lượng: phụ thuộc đội code thực thi

Việc làm:
- chuyển code đọc/ghi chỉ dùng canonical names/types
- bỏ dependency vào legacy names

Acceptance:
- log/telemetry xác nhận không còn truy cập legacy columns
- integration tests pass trên canonical-only DB

## Phase 5 - Cleanup legacy
Thời lượng: sau ít nhất 1 chu kỳ ổn định

Việc làm:
- drop cột cũ
- drop compatibility triggers/views
- compact docs

Acceptance:
- migrated DB và fresh-init DB có fingerprint giống nhau
- không còn legacy alias

## 11. Rollback plan

Rollback phải theo từng phase, không rollback chung chung.

## 11.1. Rollback Phase 2
Nếu additive migration gây lỗi:
- rollback transaction migration nếu chưa commit
- nếu đã commit:
  - giữ cột mới nhưng disable app cutover
  - không xóa cột ngay nếu đã backfill
- vì additive migration thường an toàn, rollback ưu tiên là **app rollback**, không phải destructive DB rollback

## 11.2. Rollback Phase 3
Nếu `01_init.sql` mới có lỗi:
- revert file init về snapshot trước
- giữ migration history nguyên
- CI phải tiếp tục test migrated DB để không chặn vận hành môi trường cũ

## 11.3. Rollback Phase 4
Nếu app cutover canonical lỗi:
- deploy lại app version cũ
- nhờ compatibility columns/aliases, DB vẫn chấp nhận
- đây là lý do cleanup legacy phải trì hoãn

## 11.4. Rollback Phase 5
Cleanup legacy là pha khó rollback nhất.
Chỉ được làm khi:
- có backup schema/data
- đã có evidence không còn query vào cột cũ
- có script recreate legacy columns nếu cần emergency restore

Nguyên tắc:
- destructive migration phải có precomputed rollback SQL
- nếu không chuẩn bị được rollback script, chưa được cleanup

## 12. Acceptance criteria

## 12.1. Functional acceptance
- mọi method ghi dữ liệu chính trong `PostgresClient` chạy được trên DB khởi tạo mới:
  - `create_dossier_review_session`
  - `create_dossier_review_finding`
  - `assign_dossier_review_finding`
  - `submit_dossier_review_supplement`
  - `verify_dossier_review_finding`
  - `close_dossier_review_session`
  - `upsert_employee_work_log_session`
  - `append_employee_work_log_action`

## 12.2. Schema acceptance
- không còn mismatch Tier A giữa `app/core/postgres.py` và `docker/postgres/init/01_init.sql`
- migrated DB và fresh-init DB cho cùng canonical schema fingerprint
- có `schema_migrations` hoặc cơ chế version visibility tương đương

## 12.3. Data safety acceptance
- backfill không làm mất `session_metadata/event_metadata` cũ
- dữ liệu review/submission hiện hữu join được đầy đủ sau migration
- có validation query chứng minh số row null bất thường = 0 cho các cột mới bắt buộc

## 12.4. Process acceptance
- mọi thay đổi schema mới phải có:
  - migration
  - init sync hoặc explicit justification
  - contract test / drift check update
- không còn merge PR schema/runtime raw SQL mà thiếu verification tương ứng

## 13. Trade-off và quyết định thiết kế

## 13.1. Additive-first vs big-bang rename
- **Additive-first** an toàn dữ liệu hơn, rollback dễ hơn, hợp DSCons hiện tại
- **Big-bang rename** gọn nhưng rủi ro cao vì raw SQL nhiều và test DB thật còn yếu

Chọn: **Additive-first**

## 13.2. JSONB hóa `dossier_scope` vs giữ VARCHAR
- `VARCHAR` đơn giản nhưng kém mở rộng và đã lệch code hiện tại
- `JSONB` phù hợp workflow scope evolving

Chọn: **JSONB canonical**

## 13.3. Canonical `metadata` vs giữ `session_metadata/event_metadata`
- giữ tên riêng theo bảng giúp self-descriptive hơn
- nhưng DSCons đã dùng `metadata` rộng rãi ở review tables; canonical thống nhất giúp raw SQL/service đơn giản hơn

Chọn: **`metadata` canonical**

## 13.4. Dùng migration framework mới vs SQL thuần
- framework chuyên dụng tiện hơn nhưng tăng thay đổi công cụ
- SQL thuần đủ cho hiện trạng nếu governance nghiêm

Chọn giai đoạn đầu: **SQL migration thuần**, sau này mới nâng cấp nếu cần

## 14. Phụ thuộc với 4 hướng còn lại

- **#1 transaction hardening:** cần schema ổn định trước khi thiết kế boundary/compensation bền vững
- **#3 degraded observability:** nên dùng schema version mismatch làm source health signal
- **#4 shared review projection:** cần canonical review tables rõ ràng để tránh semantic drift tăng thêm
- **#5 integration verification:** là nơi thực thi CI drift checks và DB smoke tests

## 15. Thứ tự triển khai tối ưu đề xuất cho parent

1. Audit drift + chốt canonical schema
2. Additive migration compatibility cho các drift chặn runtime
3. Thiết lập integration verification tối thiểu cho Postgres schema
4. Đồng bộ init SQL với canonical schema
5. Sau đó mới rollout các refactor transaction/projection lớn hơn
6. Cuối cùng cleanup legacy schema

## 16. Kết luận ngắn

Vấn đề #2 ở DSCons không phải chỉ là "SQL init cũ hơn code". Đây là thiếu cơ chế quản trị schema khiến raw SQL runtime, bootstrap SQL và dữ liệu sống tiến hóa độc lập. Hướng xử lý tối ưu là:

- chọn migration history làm source-of-truth
- dùng additive compatibility migration trước
- đồng bộ `01_init.sql` thành bootstrap artifact
- thêm CI drift checks bằng Postgres thật
- trì hoãn cleanup legacy đến khi app đã chạy ổn trên canonical contract

Cách này giảm rủi ro mất dữ liệu, hỗ trợ các refactor lớn khác, và chặn drift tái diễn về sau.