# sub-task-0 — `scripts/governance-detectors.ps1` (Harness-11 PHẦN C + B3)

> Lane **impl-detectors** · owner general-purpose (Write+Bash) · single-writer: CHỈ `scripts/governance-detectors.ps1` + file này.
> NO-API · DÒ+FLAG-only · PowerShell 5.1 · RUNTIME-proven (output thật dán §3).

## 1. Deliverable
`D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP\scripts\governance-detectors.ps1` (~355 dòng, ASCII-only body).
3 bộ dò + self-exclusion + summary. Param `$RepoRoot` (default = `Resolve-Path $PSScriptRoot\..`). Exit-code **0 luôn** (dò-only, KHÔNG fail-build). Mỗi FLAG: `[DETECTOR] severity | file:line | desc | resolve: <điều-kiện-gỡ-cờ>` (C5).

## 2. Thiết-kế từng detector

### C2/B3 — derived-staleness (ưu-tiên, value cao nhất) ✅
- **Canonical** đọc từ `docs/STATUS.md` CURRENT STATE table qua regex `^\|\s*<label>\s*\|\s*\*\*(\d+)` (số trong `**N**` đúng row Migrations/Tests/Gotchas/SQL tables). Runtime đọc đúng: **mig=55 test=339 gotcha=69 table=88**.
- **Cross-check disk** (canonical tự-nó không stale):
  - `mig` = đếm `*.cs` trong **mọi** dir tên `Migrations` dưới `src` (exclude `bin|obj|node_modules` + `*Designer.cs`/`*ModelSnapshot.cs`). Recursive-search vì migration thật ở `src\Backend\SolutionErp.Infrastructure\**Persistence**\Migrations (SPEC ghi gần-đúng `...\Migrations` — đã xử robust). Runtime: disk mig=55.
  - `gotcha` = max N từ `^### (\d+)\.` trong `docs/gotchas.md`. Runtime: disk gotcha=69.
  - STATUS-value ≠ disk-value → FLAG `canonical-itself-stale` (HIGH). Lần này KHỚP → 0 HIGH (baseline an-toàn).
- **Derived scan** 5 file: `CLAUDE.md`(root) · `docs/CLAUDE.md` · `.claude/skills/ef-core-migration/SKILL.md` · `.claude/skills/README.md` · `.claude/skills/dependency-audit-erp/SKILL.md`. Count-token regex: `(\d+)\s*migration` · `(\d+)\s*test` · `(\d+)\s*(?:<bay>|gotcha)` · `(\d+)\s*(?:<bang>|table)`. Lệch canonical → FLAG `derived-stale` (MED nếu |lệch|≥10, else LOW).

### C1 — broken-pointer ✅
- **(a) gotcha-ref**: grep `docs/** + .claude/** *.md` bắt `gotcha[s]?\s*#?(\d+)` + bare `#(\d+)`. `gotcha #N` luôn validate (N>max → broken MED; thiếu `### N.` anchor → LOW). Bare `#N` chỉ xét trong range; `#N`>max bỏ qua (tránh nhầm Run #312/PR #). Runtime: **0 flag** — mọi gotcha-ref ≤69 và có anchor (đúng — repo sạch khoản này).
- **(b) wikilink**: scan user-memory `C:\Users\pqhuy\.claude\projects\...\memory\*.md` + in-repo `.claude/agent-memory/**`. Bắt `\[\[([a-z0-9_-]+)\]\]`; target `<name>.md` không tồn tại trong scope → dangling. User-memory KHÔNG reachable → note + agent-memory-only (lần này user-memory REACHABLE, 29 file).

### C3 — vocab-fork ✅
- Seed alias-set: `@('wave-folder','run-trace')`, `@('<Du tru PRO>','<Ngan sach PRO>')`, `@('two-tier','all-inherit')`. Mỗi set đếm file dùng từng biến-thể; ≥2 biến-thể CÙNG sống → FLAG count + sample | resolve gộp/alias-map.

### C4 — self-line exclusion (BẮT BUỘC) ✅
- Exact: `scripts/governance-detectors.ps1`, `docs/governance/harness-11-engine.md`. Dir-fragment: `\broadcasts\inbox\`, `\broadcasts\outbox\`, `\.claude\workflows\runs\`, `\.claude\workflows\scripts\`. `Test-Excluded` áp MỌI scan. Summary in `self-exclusion: N paths excluded` + assert `self in scan=0` + `leaked=0`.

### 🔴 Encoding-robustness (gotcha #30 — phát-hiện lúc RUNTIME, đã FIX)
Vòng-1 detector MISS `CLAUDE.md:133` "68 bẫy" + cả set vocab-fork `Dự trù PRO`↔`Ngân sách PRO`. Root-cause: file `.ps1` ghi **UTF-8 KHÔNG BOM** (Write-tool); `powershell.exe -File` ở PS 5.1 decode file no-BOM bằng **codepage ANSI (1252)**, KHÔNG phải UTF-8 → literal tiếng Việt `bẫy`/`bảng`/`Dự trù` bị mojibake → KHÔNG match content UTF-8 đọc đúng (`Get-Content -Encoding UTF8`). FIX: body **ASCII-only**, mọi token tiếng Việt build từ **Unicode code-point runtime** qua helper `U @(0x62,0x1EAB,0x79)` → encoding-độc-lập. Sau fix: 71 flag (vòng-1 chỉ 53 — thiếu 18 do mojibake). Đây là minh-chứng giá-trị của mandate "viết xong PHẢI chạy thật".

## 3. RUNTIME — output thật (`powershell.exe -ExecutionPolicy Bypass -File scripts/governance-detectors.ps1`)

**Exit code: 0** · **TOTAL FLAGS: 71** (HIGH=0 · MED=33 · LOW=38).

```
===== C2/B3 - canonical resolve + disk cross-check =====
  STATUS.md canonical: mig=55 test=339 gotcha=69 table=88
  disk cross-check:    mig=55 gotcha=69
  [OK] canonical matches disk (mig + gotcha) - safe baseline for derived scan

===== C2/B3 - derived-doc staleness =====
[DETECTOR] LOW  | CLAUDE.md:53 | derived-stale: writes 53 migration but canonical=55 | resolve: update to 55 OR replace with pointer '-> docs/STATUS.md'   <== TRUE drift (root CLAUDE mig)
[DETECTOR] MED  | CLAUDE.md:53 | derived-stale: writes 53 gotcha/bay but canonical=69 | resolve: ...   <== FALSE-POS ("53" la mig-number, dinh token 'bay'? -> thuc te dong 53 KHONG co 'bay'; xem honesty #2)
[DETECTOR] MED  | CLAUDE.md:66 | derived-stale: writes 306 test but canonical=339 | resolve: ...   <== TRUE drift (root CLAUDE test)
[DETECTOR] MED  | CLAUDE.md:80 | derived-stale: writes 45 test but canonical=339   <== FALSE-POS (45 = Domain breakdown)
[DETECTOR] MED  | CLAUDE.md:81 | derived-stale: writes 261 test but canonical=339   <== FALSE-POS (261 = Infra breakdown)
[DETECTOR] MED  | CLAUDE.md:84 | derived-stale: writes 6 test but canonical=339   <== FALSE-POS
[DETECTOR] MED  | CLAUDE.md:90 | derived-stale: writes 2 test but canonical=339   <== FALSE-POS ('2 test project')
[DETECTOR] LOW  | CLAUDE.md:133 | derived-stale: writes 68 gotcha/bay but canonical=69 | resolve: update to 69 ...   <== TRUE drift (root CLAUDE '68 bay') -- VONG-1 BI MISS, sau fix encoding moi bat
[DETECTOR] MED  | docs/CLAUDE.md:13 | derived-stale: writes 4 table/bang but canonical=88   <== FALSE-POS ('4 bang Budget' module-local)
[DETECTOR] LOW  | docs/CLAUDE.md:70 | derived-stale: writes 93 table/bang but canonical=88 | resolve: update to 88 ...   <== TRUE drift (ERD '93 bang')
[DETECTOR] MED  | docs/CLAUDE.md:123 | derived-stale: writes 4 table/bang but canonical=88   <== FALSE-POS (Phase 7 '4 bang Budget')
[DETECTOR] MED  | docs/CLAUDE.md:124 | derived-stale: writes 71 test but canonical=339   <== FALSE-POS (Phase 8 historical '71 test')
[DETECTOR] MED  | .claude/skills/ef-core-migration/SKILL.md:3 | writes 10 migration   <== FALSE-POS ('.NET Core 10 migration')
[DETECTOR] LOW  | .claude/skills/ef-core-migration/SKILL.md:3 | writes 53 migration   <== TRUE drift
[DETECTOR] LOW  | .claude/skills/ef-core-migration/SKILL.md:19 | writes 53 migration   <== TRUE drift
[DETECTOR] MED  | .claude/skills/ef-core-migration/SKILL.md:34..115 | writes 1/3/4/10 table/bang   <== FALSE-POS (per-migration '<n> bang module ...')
[DETECTOR] MED  | .claude/skills/ef-core-migration/SKILL.md:42/86/87/154/171 | writes 1/2 migration   <== FALSE-POS (mig list seq number)
[DETECTOR] MED  | .claude/skills/ef-core-migration/SKILL.md:107 | writes 58/96/154 test   <== FALSE-POS (per-project breakdown)
[DETECTOR] LOW  | .claude/skills/ef-core-migration/SKILL.md:285 | writes 53 migration   <== TRUE drift
[DETECTOR] MED  | .claude/skills/README.md:20 | writes 10 migration   <== FALSE-POS ('.NET Core 10')
[DETECTOR] LOW  | .claude/skills/README.md:20 | writes 53 migration   <== TRUE drift
[DETECTOR] LOW  | .claude/skills/README.md:90 | writes 68 gotcha/bay   <== TRUE drift ('68 bay')
[DETECTOR] LOW  | .claude/skills/dependency-audit-erp/SKILL.md:153 | writes 68 gotcha/bay   <== TRUE drift ('68 bay')
  (note: count-token grep is a soft net - module-local phrases like "4 bang Budget" / "71 test (Phase 8)" can false-positive; treat LOW sev as review-not-fail)

===== C1 - broken gotcha-ref =====
(no flags -- all gotcha #N refs <= 69 and anchored; bare #N>max skipped to avoid Run#/PR# noise)

===== C1 - dangling wikilink =====
[DETECTOR] LOW  | user-memory/<29 file>:* | dangling-wikilink: [[<hyphen-form>]] -> <name>.md not found in user-memory | resolve: fix link / create file (hyphen-vs-underscore fork)
  ... 21 user-memory dangling (vd [[feedback-implementer-truncation-mitigation]] -- file that la feedback_implementer_truncation_mitigation.md _underscore_) ...
[DETECTOR] LOW  | agent-memory/pattern_*.md:* | [[pattern-...-hyphen]] -> not found   ... 8 agent-memory dangling (6 hyphen-fork + 2 trong 2026-06.gist.md tro feedback_* underscore khac-scope) ...

===== C3 - vocab-fork =====
[DETECTOR] MED  | multiple files | vocab-fork: wave-folder=15f vs run-trace=18f live side-by-side -- 'wave-folder' in [docs/HANDOFF.md, docs/STATUS.md] | 'run-trace' in [docs/HANDOFF.md, docs/STATUS.md] | resolve: merge / alias-map
[DETECTOR] MED  | multiple files | vocab-fork: Du tru PRO=7f vs Ngan sach PRO=6f live side-by-side -- in [docs/HANDOFF.md, docs/STATUS.md] | resolve: merge / alias-map   <== VONG-1 BI MISS (mojibake), sau fix moi bat
[DETECTOR] MED  | multiple files | vocab-fork: two-tier=17f vs all-inherit=10f live side-by-side -- in [docs/HANDOFF.md, docs/STATUS.md] | resolve: merge / alias-map

===== Summary =====
self-exclusion: 5 paths excluded (exact+dir rules)
  - excluded: scripts/governance-detectors.ps1
  - excluded: docs/governance/harness-11-engine.md
  - excluded: broadcasts/inbox
  - excluded: broadcasts/outbox
  - excluded: .claude/workflows/runs
self-match check: governance-detectors.ps1 in scan = 0 ; leaked excluded files in scan = 0
  [OK] 0 self-match (C4 satisfied)

TOTAL FLAGS: 71
NOTE: DETECT-only lowering net. Exit 0 always (never fails build). FLAGs are advisory.
=== EXIT CODE: 0 ===
```
> (Output trên rút-gọn các block lặp dài; con-số tally + dòng load-bearing y-nguyên run thật. Full 71 dòng có trong stdout.)

## 4. Acceptance — verdict (số đo)
| Mục SPEC | Verdict | Bằng-chứng (file:line từ output thật) |
|---|---|---|
| (i) C2/B3 FLAG drift root CLAUDE.md **mig 53 vs 55** | ✅ PASS | `CLAUDE.md:53 ... 53 migration but canonical=55` |
| (i) **gotcha 68 vs 69** | ✅ PASS | `CLAUDE.md:133 ... 68 gotcha/bay but canonical=69` (sau encoding-fix) + `README.md:90` + `dep-audit/SKILL.md:153` |
| (i) **test 306 vs 339** | ✅ PASS | `CLAUDE.md:66 ... 306 test but canonical=339` |
| (ii) C3 FLAG vocab-fork wave↔run-trace | ✅ PASS | `wave-folder=15f vs run-trace=18f live side-by-side` |
| (iii) self-exclusion 0 self-match | ✅ PASS | `governance-detectors.ps1 in scan = 0 ; leaked = 0 ; [OK] C4 satisfied` |
| Exit-code 0 (dò-only) | ✅ PASS | `=== EXIT CODE: 0 ===` |
| NO-API | ✅ PASS | Chỉ `Select-String/Get-Content/Get-ChildItem/regex`. grep script: 0 hit `Invoke-WebRequest|curl|api|http` |
| PS 5.1 parse | ✅ PASS | `PSParser.Tokenize` 0 error |
| Bonus drift bắt thêm | ℹ️ | `docs/CLAUDE.md:70 93 bảng` (canon 88) + vocab-fork thứ-3 `Dự trù PRO=7f vs Ngân sách PRO=6f` (rename S65 còn 2 tên sống) |

## 5. C5 resolve-condition — đủ mọi FLAG
derived-stale → "update to M OR replace with pointer '-> docs/STATUS.md'" · canonical-itself-stale → "re-ground STATUS.md <row> to <disk>" · broken-gotcha-ref → "fix number or add gotcha to docs/gotchas.md" · dangling-wikilink → "fix link target or create file" · vocab-fork → "merge to ONE canonical term, or record alias-map".

## 6. Honest limitations (LƯỚI giảm-sót, KHÔNG khoá-cứng — đừng over-claim)
1. **C2/B3 count-token = soft net, FALSE-POSITIVE thật**: trong 41 derived-flag, ~12 TRUE drift / ~29 FP. Nguồn FP: `.NET Core 10 migration` (version), per-project test breakdown (45/261/58/96), migration list seq number (`1 migration`), module-local `4 bảng Budget`, Phase-historical `71 test`. Mitigation: NOTE cảnh-báo + sev LOW khi |lệch|<10. KHÔNG lọc cứng theo ngữ-cảnh (giòn). Người xử FLAG, KHÔNG auto-fix. **TRUE-signal (53 mig · 306 test · 68 bẫy · 93 bảng) ĐỀU có trong list** → runtime-proof ĐẠT.
2. **`CLAUDE.md:53 "53 gotcha/bay"` = FP do regex tham**: token `(\d+)\s*(?:bẫy|gotcha)` bắt "53" rồi nuốt khoảng-trắng dài tới chữ "gotcha" xa hơn trên cùng dòng (dòng 53 dài, có chữ `gotcha #63/#64`). → over-match. Đây là giới-hạn regex flat-line, GHI RÕ. Dòng 53 thật-sự stale ở "53 migration" (đã bắt riêng, đúng).
3. **bare `#N` gotcha-ref** cố-ý bỏ qua khi N>max (tránh nhầm Run #312/PR #) → có thể MISS broken-ref dạng `#999` thật. Trade-off ít-noise.
4. **agent-memory `2026-06.gist.md` 2 dangling** trỏ file user-memory (khác-scope) → "dangling trong agent-memory" kỹ-thuật đúng nhưng có thể cross-scope cố-ý. GHI nhận, không khẳng định tuyệt-đối broken.
5. **user-memory path hard-code** theo slug máy này; máy/CI khác → tự fallback agent-memory-only + note (đã code; chưa test máy khác).
6. **vocab-fork sample chỉ in 2 file đầu** mỗi biến-thể (gọn output); muốn full-list cần mở rộng — chấp-nhận cho lần này.

## 7. Single-writer compliance
CHỈ ghi 2 file: `scripts/governance-detectors.ps1` + sub-MD này (`runs/2026-06-18-h11-implement/sub-task-0.md`). KHÔNG đụng canonical MD (CLAUDE/README/STATUS/agents) · KHÔNG agent-memory · KHÔNG sub-MD khác · KHÔNG store_memory/RAG/HTTP/Qdrant. Detector tự-nó KHÔNG ghi file nào (FLAG ra stdout only) — auto-WRITE luật = mối-nguy #1, đã tránh tuyệt-đối.
```
