# sub-impl-M3-signal — M3 `scripts/h24-signal-write.ps1` (K3)

> Vai: implementer (hmw) · Task-2 · S149 hội-tụ bookend · spec §M3 + FIX-1 · checklist K3
> 🔴 Path note: task-body chỉ đích-danh file này (`sub-impl-M3-signal.md`); footer generic ghi `sub-task-2.md`. Theo lệnh-specific task-body (fix-mới thắng footer-cũ, khai ở đây — khuôn `feedback_edit_must_predate_review`).

## Trạng thái
- [x] Đọc spec M3 + FIX-1 + K3 + enum sống + invariant 2f39a7e
- [x] Viết `scripts/h24-signal-write.ps1` (280 dòng, pure-ASCII 0 byte >0x7F)
- [x] Fault-inject K3 (a)-(e) + boundary (f) trên cây tạm → 10/10 PASS
- [x] Verify isolation (real counter 0 hit test-artifact) + parse-OK

## ① Spec đọc được (evidence)
- **Enum ĐÓNG đọc sống:** `memory-budget.json:156-168` → `lead_self_audit.flag_classes` = 11 class (5 `view-*` + 6 `gap-*`). Script đọc RUNTIME, KHÔNG hardcode (`h24-signal-write.ps1:143` — `$enum = @($budget.lead_self_audit.flag_classes | ...)`). Enum missing ⇒ fail-loud exit 3 (`:137-141`), CẤM tự chế default.
- **FIX-1 hoà-giải 3-vế** (spec:38-41): (1) đơn-vị tally = phiên-LOGIC, 2 audit bookend gộp 1 quyết-định/class/phiên; (2) reset tối-đa 1 lần/phiên procedural; (3) freeze dải treo qua `_frozen_until_owner` cạnh map.
- **Invariant map-số** (spec:44 + commit 2f39a7e): `counts` CHỈ chứa SỐ; note để cấp cha `class_repeat`. Bug gốc: lead nhét string `_backfill_S148` vào counts → consumer đọc map số vấp.
- **Consumer check:** grep toàn `scripts/*.ps1` → **0 script đọc `class_repeat.counts`** hiện tại (`-ge 3` @governance-detectors:1012 là closeout-scorer khác lớp; "filter -ge 3 của lead" @2f39a7e là filter tương-tác ad-hoc, không phải script). ⇒ nghĩa vụ writer: giữ file JSON hợp-lệ + counts int-only. governance-detectors:1426 + nhip-no-probe:79 đều `ConvertFrom-Json` cả file ⇒ phải giữ parse-được.

## ② Thiết kế thuật toán

**Input:** `-Session S<nn>` · `-Flags 'c1,c1,c2'` (class FLAG vừa fire, có thể trùng, có thể rỗng) · `-AuditKind light|deep` · `-RepoRoot` (mặc định repo; đổi = fault-inject cây tạm, khuôn governance-detectors:38).

**Ledger phiên-LOGIC (mấu chốt FIX-1.1):** 2 audit bookend @start/@end là 2 LẦN GỌI script RIÊNG (process riêng) ⇒ để gộp thành 1 quyết-định/class/phiên, ledger PHẢI persist trong file. Lưu dưới `last_audit` (NGOÀI map counts):
- `signal_session` — nhãn phiên ledger áp cho. `!= -Session` ⇒ phiên MỚI, ledger tươi (`decided=[]`, `reset_done=false`).
- `signal_decided` — mảng class ĐÃ FIRE (đếm) phiên này. Chặn re-fire (fault-inject b).
- `signal_reset_done` — bool, gate cả PASS reset (chạy tối-đa 1 lần/phiên, khớp FIX-1.2 + lịch-sử S137 "3 class reset" 1 audit).

**Luồng:**
1. Đọc enum sống → validate MỌI class ∈ `-Flags`. Lạ → exit 2, in class sai + escalate-note.
2. Dedup `-Flags` (fault-inject a: `X,X` → 1 fire).
3. **FIRE pass:** mỗi unique fired class: ∈ `decided` → NO-OP + in khai; else `counts[c]+=1`, `decided+=c`.
4. **RESET pass** (chỉ khi `reset_done==false`): candidate = enum-class KHÔNG fire lượt này ∧ `counts≥1` ∧ ∉ decided.
   - `_frozen_until_owner` hiện diện → in `(frozen - cho owner xu dai JUMP)`, KHÔNG reset, `reset_done` giữ false, counts nguyên.
   - else → reset mọi candidate về 0, `reset_done=true`.
5. Stamp `last_audit.{kind}_at_counter = counter`.
6. Persist ledger dưới `last_audit`. Assert counts int-only (Test-IntLike loại string/bool/double/object) → vi phạm exit 3.
7. Atomic: `ConvertTo-Json -Depth 20 | Out-File -Encoding utf8` → temp → `Move-Item -Force`. (BOM PS5.1; 2 reader đều strip.)

**Quyết-định fire-vs-reset (vì sao decided chỉ chứa FIRE, không chứa reset):** để "fire nếu fire ở ≥1 đầu" (FIX-1.1) thắng: class clean@start→reset0, fire@end→∉decided→fire lại thành 1 (đăng-ký fire, KHÔNG bị NO-OP). Class fire@start→decided; clean@end→reset-pass đã đóng (reset_done) HOẶC ∈decided ⇒ KHÔNG reset → giữ fire. Cả 2 sub-case honored.

## ③ Code
`scripts/h24-signal-write.ps1` (280 dòng, cite đã verify bằng grep). Điểm chốt:
- `:78-88` `Read-JsonFile` — UTF-8 byte-read + `TrimStart(BOM)` (khuôn governance-detectors:1424, né E-010 + BOM-primitive).
- `:132` `$enum` đọc sống từ `flag_classes`; `:127-129` fail-loud nếu thiếu (no-hardcode-default).
- `:143` in class sai + `:147` exit 2 khi ∉ enum.
- `:200-213` FIRE pass (dedup `:150` + decided NO-OP; `:209` `counts[c]+=1`).
- `:215-234` RESET pass (`reset_done` gate + frozen block + `:230` reset→0 khi có răng).
- `:239-247` stamp `last_audit.{kind}_at_counter` + persist ledger `signal_*` NGOÀI counts.
- `:251-259` invariant assert `Test-IntLike` counts int-only → exit 3.
- `:262-272` atomic `Out-File utf8` temp → `Move-Item -Force`, rollback temp nếu lỗi.

## ④ Fault-inject K3 (cây tạm — `$env:TEMP\k3-faultinject-repo`, harness `scratchpad/faultinject-k3.ps1`)
Mỗi ca chạy child `powershell.exe` riêng (để `exit N` không giết harness) + đọc `$LASTEXITCODE`. Cây tạm = copy real budget + real counter (ledger clear). **10/10 PASS:**

| Ca | Kỳ vọng | Kết quả |
|---|---|---|
| (a) `X,X` cùng call | +1 đúng 1 | view-stale-count 1→2, exit 0 ✅ |
| (b) call lần 2 cùng phiên cùng class | NO-OP | giữ 2, in "NO-OP", exit 0 ✅ |
| (c) class `tu-che-xyz` | exit≠0 + in class + no-write | exit **2**, in `tu-che-xyz`, file byte-identical ✅ |
| (d) `_frozen_until_owner` + class-vắng | KHÔNG reset + in frozen | gap-owner-specifics 4→4, in "frozen", exit 0 ✅ |
| (d-bis) fire dưới freeze | fire vẫn chạy | view-stale-header →1 ✅ |
| **(f) boundary** no-freeze + class-vắng ≥1 | reset→0 (CÓ RĂNG) | gap-owner-specifics 4→**0**, in "RESET" ✅ |
| (e) counts all-int | mọi value integral | all-int, badKey='' ✅ |
| (e) note-fidelity | round-trip giữ emoji-note | `_view_star_repair_S149` sống, len=1342 ✅ |
| (e) ledger-outside-counts | signal_* dưới last_audit, KHÔNG trong counts | True ✅ |
| (e) stamp | deep_at_counter==counter | 23==23 ✅ |

**Anti-Goodhart:** ca (d) vs (f) là cùng input (class-vắng ≥1) khác DUY-NHẤT ở `_frozen_until_owner` → (f) reset THẬT 4→0, (d) chặn 4→4. Chứng frozen-guard KHÔNG phải no-op luôn-skip (khuôn `feedback_faultinjection_proves_teeth`). Ca (b) PASS ⇒ ledger 1-phần-tử round-trip đúng qua process riêng.

## ④b Isolation (B6)
- Real `.session-counter.json`: grep `S150|S151|S152|S153|view-stale-header|M6 landed S149|signal_session` = **0 hit** + grep `signal_` = **0** ⇒ test KHÔNG rò vào real file.
- git diff real counter = phần S149/S149-W3-kiem là việc PARENT (có trước tôi), KHÔNG phải tôi (signal_* = 0 hit trong real).
- Chỉ ghi 2 file được phép: `scripts/h24-signal-write.ps1` (code) + sub-MD này. Cây tạm đã dọn.
- Parse-check: `PSParser::Tokenize` 0 lỗi. ASCII: 0 byte >0x7F.

## ⑤ Verdict
**M3 K3 DONE — 10/10 fault-inject PASS.** `scripts/h24-signal-write.ps1` mechanises tally-sau-audit: enum-đóng-đọc-sống (exit 2 nếu lạ) · max-1-quyết-định/class/phiên procedural qua ledger persist · fire dedup · reset consecutive tối-đa-1-pass/phiên bị `_frozen_until_owner` chặn (có răng: boundary f chứng reset THẬT khi không freeze) · stamp last_audit · atomic temp+Move · invariant counts-int-only. Pure-ASCII.

**Residual khai thẳng (KHÔNG overclaim):**
1. **Streak-loss edge (chưa test, freeze che ngắn-hạn):** class clean@start→reset0 rồi fire@end kết ở counts=1 thay vì (giá-cũ)+1 — greedy per-call không biết tương-lai. Undercount streak = hướng NGUY-HIỂM (có thể miss JUMP như bug S146). Fix đúng-hoàn-toàn cần defer reset tới call-@end tường-minh (call-site bookend hiện chưa truyền tín-hiệu "đây là @end"). Freeze `_frozen_until_owner` đang chặn MỌI reset ⇒ edge chưa phát-sinh tới khi owner gỡ freeze; nếu lúc đó còn quan-trọng = follow-up.
2. **`_frozen_until_owner` do AI KHÔNG viết:** M3 chỉ ĐỌC key này làm guard. Ghi freeze khi M6 land là bước riêng (M2/em-main @M6-land per FIX-1.3) — đúng phân-tách quyền.
3. **Ai wire call-site (M6/session-start·end) gọi script này** phải truyền class-FLAG từ 2 vai làm `-Flags` (input M3 cần vai đưa FLAG-class — chính lý-do M4 land TRƯỚC M6, spec:64).
