# /validate-traces — Traceability Coverage Matrix

Check read-only độ phủ giữa spec, code, và test — gồm cả PRD version drift.

## Gate
{{include:steps/gate.md}}

*Lưu ý: Với lệnh này, target ở Bước 1 là một tên domain hoặc UC-ID cụ thể từ `$ARGUMENTS`. Không có một file đơn để phân giải — lệnh quét nhiều thư mục.*

## Context
{{include:steps/context-loader.md}}

---

## Process

### Step 0 — Umbrella Mode Detection

Kiểm tra mảng `services` có tồn tại trong `project-context.yaml` không.

**Nếu `services` tồn tại (umbrella mode):**
- Phân giải trace dir:
  - **Nếu `setup.spec_source` được set (consolidated trace):** `all_trace_dirs = [ {spec_source}/.trace ]` — một vị trí authoritative **duy nhất** trong spec repo. Scenario mang service sở hữu qua field `@trace.service`, nên không cần tách theo service. Đây là trường hợp phổ biến.
  - **Else (không có spec_source — legacy per-service trace):** một dir mỗi service — `services[N].trace_dir` nếu set, else `{services[N].path}/.trace`; `all_trace_dirs = [ dir1, dir2, … ]`, gắn tag tên service khi đọc.
- Step 1 đọc TSV từ `all_trace_dirs`.
- **Phân giải Living Docs home (vị trí report sinh ra):**
  - Nếu `setup.spec_source` được set → `living_docs_dir = {spec_source}/.living-docs`
    *(specs module dùng chung — mount trong mọi service/umbrella workspace, nên panel phân giải nó bất kể dev đứng ở submodule nào)*
  - Else (umbrella không có spec repo riêng) → `living_docs_dir = .living-docs` ở umbrella root
- **Phân giải panel mirror:** `panel_mirror = ./.trace` ở **gốc workspace hiện tại** (nơi lệnh chạy). Panel VS Code đọc `.trace/trace-report.json` từ workspace đang mở — ghi report ở đây là cái làm view không rỗng khi dev mở một service submodule trực tiếp.

**Nếu không có key `services` (single-service mode):**
- Set `all_trace_dirs = [ {paths.trace_dir} ]`
- Không cần umbrella sync

---

### Step 1 — Nạp dữ liệu TSV

**Umbrella mode:** đọc tất cả file `{trace_dir}/**/*.tsv` từ mọi dir trong `all_trace_dirs`. Với mỗi TSV, gắn tag row với tên service gốc.

**Single-service mode:** đọc tất cả file `{paths.trace_dir}/{domain}/**/*.tsv` khớp domain target (hoặc `{paths.trace_dir}/**/*.tsv` cho mọi domain nếu không có domain filter).

Mỗi file cho trace state đã lưu của UC đó.

**Nếu không tìm thấy file `.tsv` nào** trong bất kỳ trace dir nào:
- Quét tất cả file `{paths.specs_dir}/**/*.feature` trong domain target để dựng list in-memory mọi scenario.
- Coi mọi scenario là `UNTRACKED` (chưa sinh code).
- In: "⚠️ No trace files found. All {N} scenarios across {M} UCs are UNTRACKED."
- Đề xuất: "Run `/generate-bdd {prd-file}` to initialize trace state, or `/generate-code {feature-file}` to generate code."
- **Bỏ qua hoàn toàn Step 2–6.** Đi thẳng tới Step 7 dùng state in-memory này — ĐỪNG abort.

### Step 2 — Reconcile với file `.feature` hiện tại

Mỗi file trace là `{UC-ID}-{platform}.tsv` (một sổ / UC × platform). Với mỗi row, đọc file `.feature` **của đúng platform đó** (`{specs_dir}/{domain}/{prd-slug}/bdd/{platform}/{UC-ID}*.feature` — platform lấy từ tên file TSV) và lấy `@trace.sc_version` **hiện tại** cho SC đó.
Nếu version SC trong `.feature` khác `spec_ver` của `.tsv` → cập nhật `spec_ver` trong memory (sẽ ghi lại).

Cũng phát hiện SC có trong `.feature` (platform đó) nhưng thiếu trong `.tsv` → thêm row mới với `status: UNTRACKED`. *(sc_id trùng số giữa các platform là 2 scenario khác nhau → mỗi sổ platform giữ tập SC riêng, không dedupe chéo platform.)*

### Step 3 — Tính `status` theo từng scenario

Áp dụng quy tắc theo thứ tự ưu tiên (first-match-wins):

| Rule | Status | Điều kiện |
|------|--------|-----------|
| 1 | `UNTRACKED` | `implemented_by == —` (chưa sinh code) |
| 2 | `DRIFT` | `implemented_by != —` AND `spec_ver != gen_ver` (spec đã đổi sau lần codegen — code cũ, **ưu tiên regen trước khi test**) |
| 3 | `GAP` | `implemented_by != —` AND (`test_count == —` OR `test_count == 0`) |
| 4 | `OK` | tất cả: `spec_ver == gen_ver`, `implemented_by != —`, `test_count > 0` |

> **Vì sao DRIFT xét trước GAP:** một scenario đã có code, chưa test, **và** spec vừa drift phải hiện `DRIFT` (không phải `GAP`) — vì `generate-code` xử `GAP` = "skip codegen, chạy /dev-gen-test" còn `DRIFT` = "regenerate". Nếu GAP thắng, code lỗi thời bị bỏ qua và test lại sinh trên code cũ. UNTRACKED vẫn phải là Rule 1 để scenario chưa code (gen_ver `—`) không lọt vào DRIFT.

### Step 4 — PRD version drift check

Với mỗi UC, so:
- PRD `| **Version** |` hiện tại từ `{paths.specs_dir}/{domain}/{prd-slug}/{TICKET-ID}-{prd-slug}.md`
- `prd_version` lưu trong `.tsv` (version tại thời điểm sinh BDD)
- `@trace.prd_version` trong các file code implement UC đó

Nếu layer nào sau version PRD hiện tại → gắn cờ `PRD_DRIFT` và trích các changelog entry kể từ version đó.

### Step 5 — Tech-doc revision drift check

Mỗi PRD có **một** tech-doc gộp `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md` với một `@trace.revision` chung. So revision đó vs hai cột đã lưu:

- **Backend drift:** `@trace.revision` của doc vs `tech_doc_revision` trong `.tsv`.
  Code sinh từ revision cũ hơn → gắn cờ `TECHDOC_DRIFT`.
- **FE integration drift:** `@trace.revision` của doc vs `fe_tech_doc_revision` trong `.tsv` (row FE `--phase=integration`, theo từng platform).
  Code FE wire theo revision cũ hơn (§4.5.4 đã đổi) → gắn cờ `FE_TECHDOC_DRIFT`.

Skip cột nào chưa có revision đã lưu (`—`), hoặc cả UC chưa có tech-doc.

### Step 5b — Seam & Stub Audit (mồ côi khi ghép luồng)

*Bắt lỗi "gen từng BDD thì đúng, ghép cả luồng thì hỏng": chỗ giả lập còn rỗng trong khi hàng thật đã tồn tại ở nơi khác — luồng chạy vào no-op / hàm thật không ai gọi. Build vẫn xanh, test từng-UC vẫn xanh, nên không cổng nào khác bắt được. Hai loại: `seam` (port cross-UC chưa nối) và `stub` (method trắng nội-feature chưa lấp).*

**Nguồn (dùng cái nào có, hợp nhất):**
1. Sổ chung `{trace_dir}/{domain}/{prd-slug}/_seams.tsv` — do `/generate-code` ghi (cột `kind` phân biệt `seam`/`stub`).
2. Quét code dưới `{code_base_package}` (từ CLAUDE.md §2 đã nạp) — bắt cả chỗ sinh **trước khi** có sổ:
   - **seam:** class mang `@trace.seam_pending` HOẶC tên khớp `*Stub*Adapter`.
   - **stub:** method mang `@trace.stub` (thân còn placeholder — `throw UnsupportedOperationException` / TODO / rỗng).

**Audit A — Seam (port cross-UC).** Với mỗi seam:
- Tìm **hàng thật**: class KHÁC (không `*Stub*`/`*Mock*`) implement cùng port/interface dưới `{code_base_package}`.
- Tìm **binding hiện tại**: stub có còn được tham chiếu như adapter đang dùng không (được inject / `@Primary` / đăng ký trong config) — hàng thật thì KHÔNG có đường vào?

| Điều kiện | Cờ | Ý nghĩa |
|---|---|---|
| Hàng thật CHƯA tồn tại | `SEAM_PENDING` | Bình thường — owner UC chưa gen. Chỉ nhắc. |
| Hàng thật ĐÃ tồn tại **và** stub vẫn là binding đang dùng | `SEAM_UNWIRED` 🔴 | **Lỗi thật** — hàng thật mồ côi, luồng chạy vào stub rỗng. |
| Binding đã trỏ hàng thật (stub chỉ còn cho test) | *(sạch)* | Đã nối xong. |

**Audit B — Stub (method trắng nội-feature).** Với mỗi method còn tag `@trace.stub`:
- Đọc `@trace.stub_owner` (UC lẽ ra phải lấp) + `@trace.stub_for` (trách nhiệm).
- **Owner đã gen chưa?** = có bất kỳ `@trace.implements={stub_owner}-*` nào trong codebase không.
- **Có hàng thật song song không?** = method KHÁC mang `@trace.implements` với thân thật cùng trách nhiệm `stub_for` (dấu hiệu BDD owner đã đẻ hàm song song thay vì lấp).

| Điều kiện | Cờ | Ý nghĩa |
|---|---|---|
| `stub_owner` chưa gen **và** không có hàng song song | `STUB_PENDING` | Bình thường — owner BDD chưa chạy. Chỉ nhắc. |
| `stub_owner` ĐÃ gen (hoặc có method song song cùng `stub_for`) mà method vẫn còn `@trace.stub` rỗng | `STUB_UNRESOLVED` 🔴 | **Lỗi thật** — hàm trắng mồ côi (caller chạy vào rỗng) và/hoặc hàm thật mồ côi (không ai gọi). Fill-before-create đã trượt. |
| Không còn method nào mang `@trace.stub` cho trách nhiệm đó | *(sạch)* | Đã lấp xong. |

- **Cập nhật sổ `_seams.tsv`** (nếu tồn tại): theo `kind`,
  - `seam`: `PENDING`→`READY` khi hàng thật xuất hiện mà chưa nối; `→RESOLVED` khi binding đã trỏ hàng thật.
  - `stub`: `PENDING`→`READY` khi phát hiện `STUB_UNRESOLVED`; `→RESOLVED` khi method đã được lấp (hết `@trace.stub`).
  `READY` = đồng nghĩa cờ 🔴 tương ứng (`SEAM_UNWIRED` / `STUB_UNRESOLVED`).

Không tìm thấy seam/stub nào → bỏ qua im lặng.

### Step 6 — Ghi status lại vào TSV

*Bỏ qua step này nếu không có file TSV nào (đã xử lý bởi path no-TSV của Step 1).*

Với mỗi file `.tsv` đã xử lý: ghi `spec_ver`, `status`, `last_updated` đã cập nhật lại disk.
Đồng thời **đồng bộ `uc_status` ← `@trace.status`** của file `.feature` tương ứng (header `.feature` là nguồn-sự-thật về duyệt BDD — người đặt `approved` sau khi review sạch, giống PO đặt PRD Metadata `Status`). Nhờ vậy `approved_ucs` trên dashboard phản ánh đúng thay vì luôn = 0.
**Đừng** sửa `dev_selftest`/`dev_selftest_at` (do `/dev-run-test` sở hữu) hay `qc_status`/`qc_run_at`/`qc_owner`/`qc_blocked_by` (do `/qc-run-test` + `/report-bug` sở hữu); lệnh này chỉ đọc chúng cho report.

### Step 7 — Tính aggregate cho dashboard

```
total_prds       = count distinct PRD files in {paths.specs_dir}/{domain}/*/*.md  (file .md ở gốc mỗi feature folder = PRD)
approved_prds    = PRDs with | Status | approved
total_ucs        = count distinct UC-IDs across all .tsv files (strip the -{platform} suffix from the filename)
approved_ucs     = UCs with uc_status == approved
draft_ucs        = UCs with uc_status == draft
total_scs        = total rows across all .tsv files (a UC's SCs are counted per platform — no cross-platform dedupe by sc_id)
code_coverage    = rows where implemented_by != — / total_scs
test_coverage    = rows where test_count > 0 / total_scs
drift_count      = rows where status == DRIFT
untracked_count  = rows where status == UNTRACKED
gap_count        = rows where status == GAP
seam_unwired_count = số seam bị cờ SEAM_UNWIRED (hàng thật đã có nhưng consumer còn wire vào stub — Step 5b)
stub_unresolved_count = số stub bị cờ STUB_UNRESOLVED (method còn trắng dù owner đã gen / có hàm song song — Step 5b)
dev_selftest_passing = rows where dev_selftest == pass
dev_selftest_failing = rows where dev_selftest == fail
dev_selftest_not_run = rows where dev_selftest in (not_run, —)
# NOTE: dev_selftest is the DEV self-check signal (did the dev run their own smoke tests),
# NOT official coverage — keep it labeled as such on the dashboard.
qc_passing       = rows where qc_status == pass
qc_failing       = rows where qc_status == fail
qc_skipped       = rows where qc_status == skip
qc_not_run       = rows where qc_status in (not_run, —)
# qc_status is the OFFICIAL QC automation result (set by /qc-run-test),
# shown alongside — never merged with — dev_selftest.
waiting_dev      = rows where qc_owner == dev      # PM view: QC-found, waiting on dev to fix
waiting_po       = rows where qc_owner == po       # PM view: blocked, waiting on PO to confirm/clarify
# qc_owner + qc_blocked_by trả lời "case nào đang chờ ai" — surface as a "Waiting on" column.
tech_docs_count  = count .md files in {paths.tech_docs_dir}/{domain}/*/tech-docs/
```

### Step 8 — Ghi JSON report

Ghi `{paths.trace_dir}/trace-report.json` (ghi đè nếu tồn tại). File này là source of truth duy nhất cho web dashboard — nó chứa snapshot đầy đủ tại thời điểm `/validate-traces` chạy lần cuối.

Schema:

```json
{
  "generated_at": "<ISO-8601 timestamp>",
  "domain": "<domain argument, or 'all' if no filter>",
  "summary": {
    "total_prds": 0,
    "approved_prds": 0,
    "total_ucs": 0,
    "approved_ucs": 0,
    "draft_ucs": 0,
    "total_scs": 0,
    "coded_scs": 0,
    "tested_scs": 0,
    "code_coverage_pct": 0,
    "test_coverage_pct": 0,
    "drift_count": 0,
    "gap_count": 0,
    "untracked_count": 0,
    "seam_unwired_count": 0,
    "stub_unresolved_count": 0,
    "dev_selftest_passing": 0,
    "dev_selftest_failing": 0,
    "dev_selftest_not_run": 0,
    "qc_passing": 0,
    "qc_failing": 0,
    "qc_skipped": 0,
    "qc_not_run": 0,
    "waiting_dev": 0,
    "waiting_po": 0,
    "tech_docs_count": 0
  },
  "prds": [
    {
      "prd_id": "<e.g. PAY>",
      "prd_status": "approved | draft | other",
      "total_scs": 0,
      "coded_scs": 0,
      "tested_scs": 0,
      "drift_count": 0,
      "gap_count": 0,
      "untracked_count": 0,
      "ucs": [
        {
          "uc_id": "<e.g. PAY-UC01>",
          "uc_status": "approved | draft | other",
          "scenarios": [
            {
              "sc_id": "<e.g. PAY-UC01-SC1>",
              "sc_title": "<title>",
              "spec_ver": "<current version from .feature>",
              "gen_ver": "<version at codegen time>",
              "implemented_by": "<ClassName.method or null>",
              "test_count": 0,
              "test_classes": ["<TestClass1>", "<TestClass2>"],
              "dev_selftest": "pass | fail | not_run",
              "dev_selftest_at": "<YYYY-MM-DD or null>",
              "qc_status": "pass | fail | skip | not_run",
              "qc_run_at": "<YYYY-MM-DD or null>",
              "qc_owner": "dev | po | null",
              "qc_blocked_by": "<BUG-id / GAP-id or null>",
              "prd_version": "<prd version when BDD was generated>",
              "bdd_version": "<bdd version when code was generated>",
              "tech_doc_revision": 0,
              "fe_tech_doc_revision": 0,
              "status": "OK | DRIFT | GAP | UNTRACKED",
              "last_updated": "<YYYY-MM-DD>"
            }
          ]
        }
      ]
    }
  ],
  "issues": {
    "drift": [
      {
        "sc_id": "<SC-ID>",
        "sc_title": "<title>",
        "spec_ver": "<current>",
        "gen_ver": "<at codegen>",
        "fix": "/generate-code <UC-ID>"
      }
    ],
    "gap": [
      {
        "sc_id": "<SC-ID>",
        "sc_title": "<title>",
        "implemented_by": "<method>",
        "fix": "/dev-gen-test <UC-ID>"
      }
    ],
    "untracked": [
      {
        "sc_id": "<SC-ID>",
        "sc_title": "<title>",
        "fix": "/generate-code <UC-ID>"
      }
    ],
    "prd_version_drift": [
      {
        "uc_id": "<UC-ID>",
        "code_prd_version": "<version in code tag>",
        "current_prd_version": "<version in PRD file>",
        "changelog_since": ["<v1.1: ...>", "<v1.2: ...>"],
        "fix": "/generate-bdd <prd-file> then /generate-code <UC-ID>"
      }
    ],
    "techdoc_drift": [
      {
        "uc_id": "<UC-ID>",
        "code_revision": 0,
        "current_revision": 0,
        "fix": "/generate-code <UC-ID>"
      }
    ],
    "fe_techdoc_drift": [
      {
        "uc_id": "<UC-ID>",
        "platform": "web | app",
        "code_revision": 0,
        "current_revision": 0,
        "fix": "/generate-code <UC-ID> --phase=integration"
      }
    ],
    "seam_unwired": [
      {
        "port": "<PortName>",
        "consumer_uc": "<UC còn gọi stub>",
        "owner_uc": "<UC sở hữu hàng thật>",
        "stub_class": "<StubClass>",
        "real_class": "<RealClass>",
        "fix": "Trỏ binding của <consumer_uc> sang <RealClass> (xoá/thay stub), build lại"
      }
    ],
    "stub_unresolved": [
      {
        "artifact": "<ClassName#method>",
        "stub_for": "<trách nhiệm>",
        "consumer_uc": "<UC để trắng>",
        "owner_uc": "<UC lẽ ra phải lấp>",
        "parallel_impl": "<ClassName#method hàm song song, hoặc null>",
        "fix": "/generate-code <owner_uc> — lấp logic vào <ClassName#method> tại chỗ (Fill-before-create), xoá hàm song song nếu có, build lại"
      }
    ]
  }
}
```

**Rules:**
- `implemented_by`: dùng `null` (không phải `"—"`) trong JSON khi không có giá trị
- `test_count`: dùng integer `0` (không phải `"—"`) khi không có test
- `test_classes`: dùng `[]` (không phải `"—"`) khi không có test class
- `tech_doc_revision` / `fe_tech_doc_revision`: dùng integer; `0` nếu chưa sinh
- `code_coverage_pct` / `test_coverage_pct`: làm tròn về integer gần nhất (0–100)
- Luôn ghi vào `{paths.trace_dir}/trace-report.json` bất kể domain filter — nếu có domain filter, chỉ gồm các PRD đó trong `prds[]` nhưng ghi domain vào field `domain`
- **TSV `"—"` mapping**: khi đọc file TSV, map giá trị dash sang kiểu JSON: `implemented_by: "—"` → `null`; `test_count: "—"` → `0`; `test_classes: "—"` → `[]`; `tech_doc_revision: "—"` → `0`; `fe_tech_doc_revision: "—"` → `0`; `dev_selftest: "—"` → `"not_run"`; `dev_selftest_at: "—"` → `null`; `qc_status: "—"` → `"not_run"`; `qc_run_at: "—"` → `null`; `qc_owner: "—"` → `null`; `qc_blocked_by: "—"` → `null`
- **Backward-compat:** TSV cũ có thể thiếu cột mới hơn trong header — coi cột vắng nào là giá trị rỗng của nó (đừng báo lỗi): `qc_owner`/`qc_blocked_by` (pre-19-col) → `null`; `fe_tech_doc_revision` (pre-22-col) → `0`. Lần `/generate-bdd` gen lại tiếp theo nâng header lên layout 22 cột hiện tại.

### Step 8b — Living Docs Sync *(chỉ umbrella mode)*

*Bỏ qua step này ở single-service mode.*

**Với `spec_source` được set,** các trace TSV authoritative đã sống ở **một** chỗ —
`{spec_source}/.trace/` (committed trong spec repo). **Không có merge theo service**:
mỗi scenario row mang service sở hữu qua `@trace.service`. Step này chỉ
(re)generate report và làm mới panel local.

1. **Ghi report** vào `{living_docs_dir}/trace-report.json` (`mkdir -p` trước) — dựng
   trực tiếp từ `{spec_source}/.trace/*.tsv`, với field `"service"` mỗi scenario row và
   các summary aggregate. *(Umbrella legacy không-`spec_source` vẫn merge mọi `trace-report.json`
   theo service thành một document, namespace theo service.)*

2. **Mirror tới panel location** `{panel_mirror}` (`./.trace` ở gốc workspace hiện tại)
   để dev mở *repo này* thấy data ngay: copy
   `{living_docs_dir}/trace-report.json` (+ các file `{UC-ID}-{platform}.tsv`) → `{panel_mirror}/`.
   Nếu `panel_mirror` đã phân giải về `{spec_source}/.trace`, skip.

3. **In sync summary:**
   ```
   Living Docs → {living_docs_dir}/trace-report.json  ({total} scenarios across {S} services)
   Trace (authoritative) → {spec_source}/.trace/   (committed in spec repo)
   Panel mirror → {panel_mirror}/trace-report.json  (current workspace)
   ```

> **Lưu ý:** trace state committed, authoritative là `{spec_source}/.trace/*.tsv` (trong
> spec repo — một chỗ cho PM). Report (`.living-docs/`) và panel mirror
> (`./.trace` ở workspace không phải spec repo) là **sinh ra** — gitignore chúng;
> chúng được regenerate bởi `/validate-traces` hoặc `/sync`.

## Output

{{include:steps/report-footer.md}}

```
/validate-traces — {domain}

📄 {paths.trace_dir}/trace-report.json  ← updated

┌─────────────────────────────────────────────────────────────────────────────────────┐
│  PRDs      Use Cases     Scenarios   Code Cov.  Test Cov.  Drift  Untracked  Gap   │
│  {N}       {N}           {N}         {N}%       {N}%       {N}    {N}        {N}   │
│  {A} appr  {A} appr                 {X}/{T} SCs {X}/{T} SCs                        │
└─────────────────────────────────────────────────────────────────────────────────────┘
{nếu seam_unwired_count > 0 hoặc stub_unresolved_count > 0, in dòng GATE — ngược lại bỏ}
🔴 GATE — luồng ghép có MỒ CÔI: {seam_unwired_count} SEAM_UNWIRED + {stub_unresolved_count} STUB_UNRESOLVED.
   Build/test từng-UC vẫn xanh nhưng luồng to chạy vào no-op / hàm thật không ai gọi.
   KHÔNG coi là pass tới khi cả hai = 0 (xem Seam & Stub Audit bên dưới).

| UC-ID       | SC   | Title (truncated)            | Spec  | Gen   | Code                 | Tests          | Status   |
|-------------|------|------------------------------|-------|-------|----------------------|----------------|----------|
| {UC}-UC1    | SC1  | {title...}                   | v1.0  | v1.0  | ✅ {Controller.fn}   | ✅ 10 tests    | OK       |
| {UC}-UC1    | SC2  | {title...}                   | v1.1  | v1.0  | ✅ {Controller.fn}   | ✅ 3 tests     | DRIFT    |
| {UC}-UC1    | SC6  | {title...}                   | v1.0  | —     | —                    | —              | UNTRACKED|
| {UC}-UC2    | SC1  | {title...}                   | v1.0  | v1.0  | ✅ {Controller.fn}   | —              | GAP      |

Drift Detail:
  {UC}-UC1-SC2 — spec v1.1 nhưng code sinh từ v1.0
    → Chạy lại: /generate-code {UC-ID}

PRD Version Drift:
  {UC}-UC2 — code ở PRD v1.0, PRD giờ ở v1.2
    Thay đổi kể từ v1.0:
      v1.1: {changelog entry}
      v1.2: {changelog entry}
    → /generate-bdd {prd-file} then /generate-code {UC-ID}

Tech-Doc Revision Drift:
  {UC}-UC3 — code sinh từ tech-doc revision 2, giờ ở revision 4
    → Review thay đổi tech-doc rồi /generate-code {UC-ID}

Seam & Stub Audit (mồ côi khi ghép luồng):
  🔴 SEAM_UNWIRED — {port}: {consumer_uc} còn gọi {stub_class} rỗng,
     hàng thật {real_class} ({owner_uc}) chưa được nối → luồng ghép chạy vào no-op
       → Trỏ binding {consumer_uc} sang {real_class}, xoá/thay stub, build lại
  ⓘ SEAM_PENDING — {port}: {consumer_uc} đang dùng stub, owner {owner_uc} chưa gen (chưa phải lỗi)
  🔴 STUB_UNRESOLVED — {ClassName#method} ({stub_for}): {consumer_uc} để trắng, owner {owner_uc} đã gen
     {parallel_impl → "đẻ hàm song song " + parallel_impl | ""}→ hàm trắng mồ côi / hàm thật không ai gọi
       → /generate-code {owner_uc} lấp logic vào {ClassName#method} tại chỗ, xoá hàm song song, build lại
  ⓘ STUB_PENDING — {ClassName#method} ({stub_for}): owner {owner_uc} chưa gen (chưa phải lỗi)

Recommendations:
  - /generate-code {UC-ID}      cho scenario DRIFT và UNTRACKED
  - /dev-gen-test {UC-ID}     cho GAP (thiếu test)
  - /generate-bdd {prd-file}    cho PRD version drift
  - Nối binding thủ công         cho mỗi SEAM_UNWIRED 🔴 (hàng thật đã có, còn kẹt stub)
  - /generate-code {owner_uc}    cho mỗi STUB_UNRESOLVED 🔴 (lấp method trắng tại chỗ + xoá hàm song song)

[Chỉ umbrella mode]
Living Docs canonical → {living_docs_dir}/  (specs module — shared, gitignored)
Panel mirror          → {panel_mirror}/trace-report.json  (current workspace)
  Tip: chạy /validate-traces (hoặc /sync) sau mỗi phiên codegen để làm mới panel.
  Cả hai là mirror sinh ra — đừng commit (.living-docs/ + .trace/ trong .gitignore).
```
