# Investigator-Codebase Agent — Persistent Memory

> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
> Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
> Full verbatim history pre-S40 → git `d2f52ba` + `archive/2026-05-q1..q4.md` + `archive/2026-06.md`. 🗺️ **Lookup map (Harness-9 S70): `archive/_INDEX.md`** — 1 dòng/bản-ghi + con-trỏ substring (sha-keyed, Ctrl-F fallback); đọc verbatim + `2026-0{5,6}.gist.md` (nén 4-field) theo nhu cầu.
> **Renamed S39:** investigator → investigator-codebase (internal half; external → investigator-api).

---

## 🎯 Role baseline

Read-only INTERNAL audit SOLUTION_ERP codebase. Tools: Read, Grep, Glob, Bash + 5 RAG MCP. Output: concise findings <500 words + file:line refs. Skills: `contract-workflow` + `permission-matrix` + `ef-core-migration`.

## 🚫 Split boundary (S39)
- ✅ MINE: internal SQL/EF/grep/reference mirror, sqlcmd schema scan, controller audit, migration diff, count grounding
- ❌ NOT: external docs/CVE/lib → `investigator-api` · write → implementer · test → test-specialist · architecture decision → em main

---

## 📋 Patterns proven (apply confidently)

### Schema scan via sqlcmd
```bash
sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -Q "..."     # runtime API (primary)
sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Design -Q "..."  # ef tooling
ssh vietreport-vps "sqlcmd -S .\SQLEXPRESS -d SolutionErp -U vrapp -P '...' -Q '...'"  # prod
```
Queries: `sys.columns`, `sys.triggers`, `__EFMigrationsHistory`, `COUNT(*)`, `sys.indexes`.
**Gotcha 2 LocalDB distinct** (`feedback_designtime_runtime_db`): `_Dev`=runtime (appsettings.Development), `_Design`=`dotnet ef` default. Prod password fallback `C:\inetpub\solution-erp\api\appsettings.Production.json` khi `$env:PROD_DB_PASSWORD` empty.

### Controller / wire-claim audit
- Grep `\[Route\("api/[a-z]+"\)\]` enumerate controllers · `\[Authorize(Policy = "..."` per-action policy (gotcha #44 silent 403 class-level quá strict)
- Grep `// Mock` / `alert(` / `setEditing(null) // close UI` — wire claim bugs · `IActionResult` vs `ActionResult<T>`

### Smoke verify catalog
Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expected vs actual + file:line evidence.

### Memory cross-reference
27 user-memory tại `C:\Users\pqhuy\.claude\projects\D--...\memory\MEMORY.md` (index). Key: per_chunk_commit · uat_skip_verify · audit_reuse_before_clone · designtime_runtime_db · per_nv_permission_scope · ef_migration_backfill_reorder · status_handoff_tiering (S40) · 7agent_split_upgrade (S39).

### External research → DEFER `investigator-api` (split S39)

---

## ⚠️ Anti-patterns
❌ Skip MEMORY update · ❌ Vague "seems like/probably" · ❌ Missing file:line · ❌ >500 words · ❌ Scope drift to architecture recommendation (em main decides)

---

## 🧠 SOLUTION_ERP context essentials (S40 verified — re-grounded)

- **DB:** Dev `SolutionErp_Dev` · Design `SolutionErp_Design` (distinct) · Prod `.\SQLEXPRESS`/`SolutionErp`/`vrapp` via SSH `vietreport-vps`
- **Migration path:** `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/*.cs` (⚠️ NOT root `/Migrations/`). **40 mig**, last `20260528090839_AddAttendances`.
- **Counts S40:** 40 mig · **84 SQL tables** (77 DbSet + 7 Identity, count `.ToTable()` ModelSnapshot NOT DbSet) · ~211 endpoints · 65 FE pages (36 admin + 29 user `*Page.tsx`) · ~53 menu keys (BE `MenuKeys` const) · **130 test** (58 Domain + 72 Infra) · **55 gotchas** (format `### N.` highest #55) · 27 user-memory · 6 skills · 7 sub-agents
- **Tech:** .NET 10 Clean Arch (Api→Application←Domain + Infra) + CQRS MediatR + EF Core 10 + 2 React 19 Vite 8 TS 6 (fe-admin :8082 + fe-user :8080) + SQL Server + Gitea CI + IIS
- **Prod:** api/admin/eoffice.solutions.com.vn · Gitea `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` (Actions API `/api/v1/repos/.../actions/tasks` NOT `/runs` 404, cache stale ~2min gotcha #46 — cross-check VPS mtime)
- **Auth:** `admin@solutions.com.vn/Admin@123456` (full) / `nv.test@solutions.com.vn/TestUser@123456` (Drafter). Response `accessToken`+`refreshToken`+`user`. Password ≥12 chars.

---

## 🔄 Active workflow schemas (V1 + V2 coexist post-S17)

- **V1 Mig 21 flat** — `WorkflowDefinition` pin PE/Contract cũ. Match Dept+PositionLevel.
- **V2 Mig 22-31** — `ApprovalWorkflow` pin PE/Contract mới, match `ApproverUserId` 1-1 OR-of-N cùng Cấp. Steps (Phòng) > Levels (Cấp). PE + Contract đã wire V2 (Mig 32+33 S29). Proposal V2 (Mig 38 S37 inline ApproveV2Async). WorkflowApps skeleton (Mig 39 S38 — ApproveV2 advance DEFER Phase 11).
  - Mig 25 IsUserSelectable · Mig 26 PE LevelOpinions UPSERT · Mig 29 Allow* per-NV (F1/F3 per Approver slot + F2 `Users.AllowDrafterSkipToFinal`) · Mig 30 F4 AllowApproverEditBudget · Mig 31 SkipToFinal→ApproverLevel
- **State machine PE 5 trạng thái:** Nháp / Đã gửi duyệt / **Trả lại (TraLai=98)** / Từ chối / Đã duyệt
- **Mode Trả lại 4 option per-Level:** OneLevel (lùi 1 Cấp) / OneStep (lùi Bước trước) / Assignee (pick NV đã ký) / Drafter (Phase=TraLai). 3 mode đầu giữ ChoDuyet lùi pointer. Admin bypass `level.Allow*`.

---

## 📅 Recent activity (FIFO — older → archive/git)

- **2026-06-19 (S76 P2+P3 — budget-edit-role BADGE insert-point map, designer+fe-user-flow, on-disk):** ⭐ **Display-only "✎ NS PRO/CCM" badge per approver — BE change = SMALL both DTOs.** **(A) Designer fe-admin `ApprovalWorkflowsV2Page.tsx`:** read-only render `DefinitionCard:446-454` (level group → approver `{approverUserName}` + `({approverEmail})`); DTO `LevelDto:37-54` (approverUserId/userName/email + 7 Allow* flag, **NO role/dept field**). Feed = `GetAwAdminOverview` (`/approval-workflows-v2`). **Insert badge → `:447-452`** cạnh `approverUserName`. **(B) fe-user `PeDetailTabs.tsx`:** approvalFlow render `LevelOpinionsSectionV2:588` (signed-only) — но live flow tree = `currentApproval.approvers` :131 + Panel3 separate. `PeApprovalFlow` DTO `purchaseEvaluation.ts` + BE `PurchaseEvaluationApprovalLevelApproverDto` (`PurchaseEvaluationDtos.cs:129-132` = UserId/FullName/Email, **NO role**). **(C) Role-resolve for LIST userId:** codebase uses `userManager.GetRolesAsync(u)` (per-user, N+1 risk) OR `GetUsersInRoleAsync(role)` (reverse, `PeUrgentFeatures.cs:74`). `IApplicationDbContext` exposes `DbSet<Role> Roles` :29 but **NO UserRoles join-table DbSet** → efficient batch = either (a) `userManager.GetUsersInRoleAsync(Procurement/CostControl)`→2 HashSet<Guid>, mark approver if id∈set (NO N+1, 2 queries total); or (b) add `DbSet<IdentityUserRole<Guid>>` to interface for join. **BE build site `PurchaseEvaluationFeatures.cs:964-972`** already batches `approverInfos` via `userManager.Users.Where(Contains(allApproverIds))` — extend SELECT or post-join 2 role-sets here; handler has both `db`+`userManager` :750-751. **(D) Change size = SMALL:** +2 bool field (canEditProBudget/canEditCcmBudget) per approver DTO + 2 GetUsersInRoleAsync calls. Designer side: `GetAwAdminOverview` query needs same 2-set lookup (admin-only, cheap). Gate semantics ALREADY proven `:800-801` (canEditPro=Admin||Procurement, canEditCcm=Admin||CostControl). **(E) REC:** minimal = compute 2 HashSet once (proFans/ccmFans via GetUsersInRoleAsync), pass into approver-DTO map both sites; badge = pure display `id∈proFans→"✎ NS PRO"` `id∈ccmFans→"✎ NS CCM"`. RISK low (display-only, no authz touch) — only watch: a user can hold BOTH roles → show both badges; Admin holds neither role explicitly unless seeded → may need OR Admin note. Tag `[s76, budget-role-badge, designer+pe-flow, getusersinrole-batch-no-n1, approver-dto-add-2bool, display-only]`.

- **2026-06-19 (PE Block-A budget editable-gate audit — submission-count lock NEXISTS, on-disk):** ⭐ **Gate = PURE ROLE, KHÔNG phase, KHÔNG số-lần-trình.** BE `PurchaseEvaluationFeatures.cs:800-801` `canEditPro=isAdmin||Procurement` · `canEditCcm=isAdmin||CostControl` (DTO arg :856). Handler `PeWorkItemBudgetFeatures.cs`: PRO `:86-91` CCM `:152-157` fail-closed ForbiddenException role-only TRƯỚC side-effect; comment `:18-20` ghi RÕ "KHÔNG ràng Phase (bảng NS = tài-liệu-sống chỉnh bất-kỳ-lúc-nào như Excel)". Validator chỉ `>=0` (Initial :136, Adjustment cho-ÂM :138), absolute-set null=clear. **FE `PeDetailTabs.tsx:1060 PeBudgetSummaryTable`:** ô "Ban hành lần đầu" :1173 + ô "hiệu chỉnh V0" :1188 dùng **CÙNG biến `bs.canEditCcm`** — ZERO phân-biệt 2 ô, ZERO lock-after-first. `drafterEditable:1066`=`!readOnly&&isEditablePhase` chỉ áp row3/row8 (drafter NS-kỳ-này), KHÔNG áp Block-A. **(b) submission-count lock = KHÔNG TỒN TẠI:** grep `submitCount|lanTrinh|firstSubmit|lockInitial|hasSubmitted|soLanTrinh` toàn `src/Backend`=0 + FE=0. Entity `PeWorkItemBudget.cs` 6 field plain, KHÔNG cờ `IsInitialLocked`/`SubmitCount`; record per-cặp(Project×WorkItem) share mọi phiếu KHÔNG track lần-trình. **Kết luận: yêu-cầu chị Trà/anh Kiệt (khóa Initial sau lần-trình-đầu, mở Adjustment) = FEATURE MỚI — cần field track first-submit-done + TÁCH gate 2 ô (Initial vs Adjustment), HIỆN cùng `canEditCcm` không tách được.** Tag `[pe-block-a-gate, role-only-no-phase, submission-count-lock-NEXISTS, initial-vs-adjustment-same-gate, fdc-feature-new]`.

- **2026-06-18 (PE price-model recon FDC "Giá chào thầu" PRO-Min/Max + CCM-proposed, on-disk):** ⭐ **"Giá chào thầu" mục c = DERIVED, KHÔNG stored column** = `WinnerQuoteTotal` = SUM(Quote.ThanhTien WHERE supplierRows==SelectedSupplierId). Computed 3 nơi đồng-predicate: submit-guard `PurchaseEvaluationWorkflowService.cs:188-192` · detail-GET `PurchaseEvaluationFeatures.cs:818-826`(→`CurrentProposalTotal`) · CEO-threshold `:833`. DTO `WinnerQuoteTotal` `PurchaseEvaluationDtos.cs:244`. **ALL money fields:** Quote(NCC) `BgVat/ChuaVat/ThanhTien` decimal non-null `PurchaseEvaluationQuote.cs:12-14` · PE-header `BudgetPeriodAmount`(row3 drafter)/`ExpectedRemainingAmount`(row8) decimal? `PurchaseEvaluation.cs:40-41` · PeWorkItemBudget(per cặp Project×WorkItem) PRO `ProEstimateAmount:27` + CCM `InitialAmount`/`AdjustmentAmount`(ÂM-OK) `:29-30` decimal? · Detail dự-toán `KhoiLuong/DonGia/ThanhTienNganSach` `PurchaseEvaluationDetail.cs:15-18`. **PRO-min/max + CCM-proposed = KHÔNG tồn-tại** (grep Min|Max|Proposed|Suggest|BidPrice|GiaChaoThau PE-entities=0) → field MỚI. **Role-gate mirror-được (`PeWorkItemBudgetFeatures.cs`):** 2 cmd tách `UpdatePeBudgetProCommand:61`+`UpdatePeBudgetCcmCommand:126`; handler fail-closed `ForbiddenException` TRƯỚC side-effect — PRO `:86-91`(`Admin||Procurement`) CCM `:150-155`(`Admin||CostControl`); capability-flag BE-computed `canEditPro/canEditCcm` `PurchaseEvaluationFeatures.cs:783-784`→DTO `PeBudgetSummaryDto:290-291`; auto-create race-safe `PeWorkItemBudgetEnsurer.EnsureTrackedAsync:34`; KHÔNG ràng Phase. NO AutoMapper (DTO project tay). **FE (fe-user `src/`; fe-admin PeDetailTabs.tsx = SHA-identical `diff -q`):** mục-c `components/pe/PeDetailTabs.tsx:1406-1417`(helper `computeGiaChaoThau` def:71 call:1393) · budget-table `PeBudgetSummaryTable:1062-`(rows:1110-1128, host `ChonNccSection:1383`) · giá-gói+CEO-threshold `:311-313` · create `PeWorkspaceCreateView.tsx` · header `PeHeaderForm.tsx`. FE type `types/purchaseEvaluation.ts` `PeBudgetSummary:292-307`+`winnerQuoteTotal:445`. ⚠️ fe-admin types DIFFER (sync cả 2). **Surprise:** PRO-Min/Max-chốt + CCM-proposed = semantic MỚI (giá-người-duyệt ≠ giá-NCC-báo); gắn PeWorkItemBudget(per-cặp role-gate-sẵn) vs column-PE(per-phiếu) = em-main quyết. Mig 53 CeoApprovalThreshold+cờ-gấp đã có khung CEO-duyệt-theo-ngưỡng. Tag `[pe-price-model, gia-chao-thau-derived, pro-minmax-ccm-proposed-NEW, role-gate-mirror, fdc]`.

- **2026-06-18 (S71 PART-C audit — run-trace vs checklist-v2 FLAT + detector-refine, on-disk):** ⭐ **2 GAP THẬT (trung-thực, không inflate):** (1) **C1/C2/C8 = SUBFOLDER, canonical-v2 = FLAT → migration NEEDED, chưa làm.** `find runs/` cho thấy MỖI run-folder có `sub-md/`+`harvest/` SUBDIR (5 run: h10-{invest,implement,review}+h910-{finalize,curate}) — đúng cấu-trúc CŨ broadcast-delta phát-bỏ. ZERO flat-awareness: grep `phẳng|flat|cùng cấp` trong `.claude/workflows/`+`.claude/commands/`=0 hit. SE-adoption-commit `8c47bd0`(06-18) TRƯỚC broadcast-flat cùng-ngày → SE chưa biết. README/hmw.js/session-end đều mô-tả subfolder. C8 dual-form-acceptance close-gate cũng chưa. (2) **REFINE(b) detector = MISSING HOÀN-TOÀN.** `find .claude -name *.js/*.ps1`=CHỈ `hmw.js`(=engine ≠ detector). `.claude/hooks`+`.claude/scripts` KHÔNG tồn-tại. Repo-wide grep `bypass|scan.*runs` script=0. SE KHÔNG có bộ-dò chống-lách-engine → 3-function (whitelist/path-variants/launch-key-anchor)+relation-acceptance = n-a. **MET (đừng nhạ oan):** C3 committed THẬT — `git check-ignore runs`=exit1(NOT-ignored)+`git ls-files runs`=22 file (cả hai nấc). C4 per-turn real (`invest-synthesis.md` 43-dòng). C5 3-layer wired: L1 README:51(convention em-main) · L2 `session-start.md:71` orphan-scan `runs/*/` closed=⏳+harvest-rỗng · L3 `session-end.md:51` close-gate idempotent 5-trục. C6 ledger 2-beat (`_ledger.md:7`, 5 run đều CLOSE-beat+wf_). C7 caveat present (README §69-73 no-overclaim/fragile/G-015 TRACKED≠enforced). ⚠️ sub-md/ chỉ `.gitkeep` (read-only sub→em-main scribe, design KHÔNG phải miss). Tag `[s71, part-c-audit, subfolder-not-flat, detector-MISSING, c3-committed-real]`.

- **2026-06-18 (S71 Harness-10 ref-sweep — wave-*/agent-teams/harvest migration map, on-disk):** ⭐ **2 RULE-MECHANISM (cơ-chế, sửa CẨN THẬN ≠ text-swap):** (1) **`.gitignore:93-94`** `.claude/workflows/wave-*/` + `.claude/agent-teams/` AFTER `!.claude/**:83` (last-match-wins) — Harness-10 LẬT containment: runs/ TRACKED nên KHÔNG gitignore (đã có `runs/_ledger.md:4` "tracked-change NGOÀI run-folder = vi-phạm" thay B6 "mọi tracked = vi-phạm"). agent-teams = n-a Windows in-process → giữ-hay-bỏ tùy. (2) **`hmw.js`** wave-mechanism: meta.description:9 (2-MODE) · args:19 `wave:{name,dir}` · SCHEMA subMdPath:52 · WAVE-MODE block:87-91 (`const wave = A.wave&&A.wave.dir`) · log:94 · subMd path:102 · writeGuard TOOL-AWARE 2-nhánh:106-120 (wave→sub-MD-isolated / default→return-delta) · prompt subMdPath:131. → run-trace = đổi `wave`→`run`, dir `wave-<tên>`→`runs/<run-id>`, +harvest/ path. **TEXT-ONLY (đổi chữ, KHÔNG cơ-chế):** `.claude/workflows/README.md` TOÀN BỘ (48 dòng, đầu-đề + table 2-MODE + structure + B1-B6 + agent-team §) · `session-end.md:32,49,51` (§L.b(d)(f) GATE + B5 wave-gom) · `session-start.md:71` (H2 báo wave-folder tồn-đọng) · `agents/README.md:111` (decision-tree wave-gom B5) + :22-28,52 harvest-curator.md (B5 scan path `wave-<tên>/sub-*.md` + agent-team) · `harvest-curator/MEMORY.md:20` (wave-folder gitignored). **DOC/HISTORY (immutable evidence — KHÔNG sửa):** `broadcasts/**` (handshake:17 + inbox/README:15 "khác wave-folder gitignored") · `docs/governance/adap-reports/2026-06-07-Agent-harness-2.md` (toàn bộ B1-B6 spec) · `docs/changelog/sessions/*` · `error-ledger.md:86` (wave-folder-leak=0 evidence). **ĐÃ SCAFFOLD Harness-10 (S71 đang chạy):** `runs/_ledger.md` (2-beat OPEN/CLOSE) + `runs/2026-06-18-h10-invest/run.md` + `harvest/`. ⚠️ **Note .gitignore:92 comment** `git check-ignore -v .claude/workflows/wave-x/wave.md` = verify-cmd cũ, đổi theo. Tag `[s71, harness-10-refsweep, wave-to-runtrace-migration, gitignore-mechanism, hmw-wave-mechanism]`.

- **2026-06-18 (S71 Harness-10 STAGE-C harvest-flow recon — per-turn + 3-layer wire points, on-disk):** ⭐ **CURRENT harvest = SINGLE-POINT @session-end (B5), KHÔNG per-turn.** Driver = `harvest-curator` H2 (`agents/harvest-curator.md:22` "sau workflow-dài/cuối-session quét `wave-<tên>/sub-*.md`→gom→APPEND agent-memory/<role>"). Wired ONLY `session-end.md §L.b(f):51` (5-trục GATE + wave-folder gom B5). `session-start.md:71` = REPORT-only (báo wave tồn-đọng, KHÔNG gom). ZERO per-turn hook — `hmw.js` JS-sandbox no-fs (`hmw.js:5`), harvest deferred-to-close. **C4 per-turn-primary wire (3 chỗ):** (a) `hmw.js:122-134` prompt-builder — sub return findings; (b) NEW em-main step: ghi `runs/<id>/harvest/` SAU MỖI fan-out turn (KHÔNG đợi close); (c) `session-end.md §L.b(f):51` đổi "gom @end" → "VERIFY per-turn harvest đủ". **C5 3-layer anti-miss wire:** L1 in-run-reminder = `hmw.js` prompt + `run.md` checklist (run trước chưa-harvest → flag); L2 post-exec-rescan = `session-start.md:71` (mở rộng orphan-scan `runs/*/` tìm ledger-OPEN-no-harvest, hiện chỉ báo wave); L3 close-gate = `session-end.md §L.b(f):51` (GATE đã có, repoint wave→runs). **EVIDENCE tracked:** `git check-ignore runs/.../run.md`→matched `!.claude/**` (.gitignore:83 negation)=NOT-ignored ✓ vs `wave-*/` still gitignored (:93). Run-folder ĐÃ scaffold S71: `runs/2026-06-18-h10-invest/`{run.md·sub-md/.gitkeep·harvest/.gitkeep}+`runs/_ledger.md` (2-beat OPEN/CLOSE `:3`, orphan=OPEN-no-CLOSE). **G-015 shift:** Harness-2 "mọi tracked=vi-phạm" (wave gitignored→diff mù) → Harness-10 "tracked NGOÀI run-folder+code-disjoint=vi-phạm" (`_ledger.md:4`) → containment MẠNH hơn (run-folder in git-diff thấy sub-MD writes). Tag `[s71, h10-harvest-flow, per-turn-C4, 3-layer-C5, single-point-end-current]`.

- **2026-06-18 (S71 Harness-10 task-A — hmw.js EXACT edit-list wave→run-trace, on-disk):** ⭐ Refines sibling ref-sweep with precise diffs. **3 LOGIC edits:** (1) `:90` `const wave=(A.wave&&A.wave.dir)?A.wave:null` → `run=(A.run&&A.run.dir)?A.run:null`; (2) `:102` subMd `\`${wave.dir}/sub-${role||'task'}-${i}.md\`` → `\`${run.dir}/sub-md/sub-${role||'task'}-${i}.md\`` (⚠️ +`/sub-md/` SUBDIR — matches scaffolded `runs/<id>/sub-md/`, today FLAT); (3) `:106-120` writeGuard 2-branch keep TOOL-AWARE, reword. **CONTAINMENT-FLIP 2 strings:** `:112` "wave-folder gitignored nên KHÔNG hiện trong diff = sạch" → "run-folder TRACKED; tracked-change NGOÀI run-folder(+code-disjoint)=vi-phạm" (model = `runs/_ledger.md:4`); `:114` "file NGOÀI repo/wave-folder"→run-folder. **TEXT reword:** `:5,9(+drop stale two-tier H4.5→H8),19(args wave→run),52,55,88-91,94,108,113,131`. **VERDICT: pure mechanical** — fan-out/SCHEMA/resolveModel/parallel/checkpoint-gate ALL unchanged; only rename + path-subdir + 2 string-flips. **Read-only sub flow same** (`:111` subMdPath→em-main-scribe @P3, no Write tool). **C2/C4 stay em-main** (hmw.js no-fs `:1-5`). Tag `[s71, h10-task-a, hmw-exact-difflist, subdir-sub-md, mechanical-rename]`.

- **2026-06-17 (PE-workflow recon for FDC feature-plan — urgent flag + value-threshold routing, on-disk):** ⭐ **PE VALUE: NO stored "giá trị gói thầu" column.** Best-fit = winner-quote-total `SUM(Quote.ThanhTien WHERE supplier==SelectedSupplierId)` — COMPUTED (submit-guard `PurchaseEvaluationWorkflowService.cs:188-190` + `CurrentProposalTotal` in `PeBudgetSummaryDto`). Other amounts: `PE.BudgetPeriodAmount`(:40 drafter NS kỳ này)/`ExpectedRemainingAmount`(:41)/`PeWorkItemBudget.FullAmount`=(Initial??0)+(Adjustment??0) (`PeWorkItemBudget.cs:29-30`) — all budgets, not deal-value. **ROLES PRO/CCM/CEO = domain shorthand NOT constants** (`AppRoles.cs` has Procurement/CostControl/Director; PRO=Procurement CCM=CostControl CEO=Director). **V2 routing IGNORES roles** — approvers = specific `ApproverUserId` (`ApprovalWorkflow.cs:80`), OR-of-N = N Level rows same `Order` (GroupBy :687). "Phòng CCM" = seed Step NAME + non-strict DeptId hint only (`:67`). **CEO = positional (last level/last step), NOT conditional.** **ROUTING 100% LINEAR** (level→step, `DaDuyet` when `nextIdx>=steps.Count`). ZERO value/threshold/conditional config anywhere (grep 0 on AW/Step/Level/PEType). ⭐ **HOOK B (value-threshold) = `ApproveV2Async` advance block lines 816-845** (`:817` levelOrder++ / `:828-837` terminal DaDuyet / `:838-845` next step). Precedent: `skipToFinal :773-814` already "jump pointer to last step+level" — reuse mechanic conditioned on value. **HOOK A (urgent):** add `IsUrgent bit`/`PePriority` enum (mirror `ItTicketPriority{Low,Medium,High,Urgent}` `Office/Enums.cs:48-54`) AddColumn no-new-table; notify `INotificationService.NotifyAsync(userId,type,title,desc?,href?,refId?)` (`INotificationService.cs:10`)+SignalR interceptor; LogTransition notifies DRAFTER-only on terminal (`:960-980`), NO approver-notify yet. Badge DTOs: `PurchaseEvaluationListItemDto`(`PurchaseEvaluationDtos.cs:6`)+`DetailBundleDto`(:201). Type A/B (`PurchaseEvaluationType.cs:6-10`) constrains pinnable ApplicableType only — ZERO type-conditional routing. ⚠️ "Từ chối" REMOVED S60 hard-guard `:80-85` (throws even Admin; only Duyệt/Trả lại). ⚠️ drafter-in-chain bypass `:543` auto-approves drafter's own step-1 levels on submit (interacts w/ value-finalize). Tag `[pe-workflow-recon, value-threshold-hook, urgent-flag, fdc-feature-plan]`.

- **2026-06-17 (S69 recon — Office-module inventory + Hồ sơ-NS CSS-contract, on-disk):** ⭐ **PART A Office:** 21 `Off_*` keys (`MenuKeys.cs:99-121`): root `Off` + DanhBa(card-grid), `Off_PhongHop`{View=cal/Manage=room-CRUD-admin/Book}, `Off_DeXuat`{List/Create/Inbox=Proposal-V2}, `Off_DonTu`{Leave/Ot/Travel}, `Off_DatXe`, `Off_ItTicket`, `Off_ChamCong`(re-parent→Personal S57), `Off_AttendanceReport`(admin). 10 office pages `{fe-admin,fe-user}/src/pages/office/` ALL SHA256-MIRROR except **MyAttendancePage DIFFERS** + AttendanceReportPage ADMIN-ONLY. Routes `App.tsx` user:70-80/admin:88-100; staticMap `Layout.tsx:87-103` (workflow-apps :kind `/workflow-apps/{leave,ot,travel,vehicle}`); menuKeys.ts:45-63. **HIDE-FLAG** `RevokeTemporarilyHiddenModulesAsync` (`DbInitializer.cs:2157-2190` called :2040 LAST) wipes CRUD on `MenuKey.StartsWith("Off")||"Hrm"||==Personal` non-Admin, idempotent. **Golive flip:** remove :2040 call (+ re-add prefix InReviewScope grant). Office already S55-shell polished NOT bare. **PART B Hồ sơ-NS CSS:** layout=3-col flex (`EmployeesListPage.tsx` SHA256-identical x2, 1597 LOC): cây-tổ-chức TRÁI(:178) + NV-list MID(:244) + detail PHẢI = avatar-header `app-gradient-brand`(:643)+`text-white!`(:653)+initials chip bg-white/15 → 5-TAB(:507 Tổng quan/Thân nhân/Trình độ/Kinh nghiệm/Hợp đồng) → `Card`(:1526 left-rail+icon-chip) w/ `Field`(:1572 label uppercase accent-tint + value `font-medium text-brand-800`, empty=`text-slate-300 —`). `ACCENT` map :497-503 Record<5,{chipBg/chipFg/head/rail/labelText}> accent∈{brand,teal,violet,amberx,greenx}, palettes stops 50/100/500/600/700 only no-800→headings -700 (brand -800 OK). Tokens `index.css`: brand-600=#1f7dc1 brand-800=#175685 @theme:5-55, font Be-Vietnam-Pro:53; classes `.app-gradient-brand`(:105 120deg b600→700→800),`.card-accent`(:112),`.icon-chip`(:128 --chip-bg/--chip-fg),`.stat-value`(:140),`.label-eyebrow`(:89). ⚠️ **GOTCHA #66 = `index.css:79-83` `h1,h2,h3,h4{color:#0b1220;font-weight:700}` OUTSIDE @layer** → TW-v4 unlayered wins → heading-tag inside gradient MUST `text-white!`. ⚠️ **CROSS-APP DRIFT:** fe-user=S68 (h1-4 #0b1220/700, label-eyebrow brand-600, 175L); **fe-admin STILL OLD** (h1-4 #0f172a/600, label-eyebrow #64748b slate, 167L) — fe-admin NOT synced S66-68 heading bump → mirror Office to fe-admin needs index.css sync. Tag `[s69, office-inventory, hoso-css-contract, gotcha66, fe-admin-css-drift]`.

- **2026-06-18 (S71 Harness-8/hmw/pending/sleep audit — fidelity ground-truth, on-disk):** ⭐ **H8 all-inherit = FULLY ADOPTED both layers.** (1) Frontmatter: ALL 12 `.claude/agents/*.md` = `model: inherit` (11 sub + README; grep-count 12/12, zero demoted). (2) `hmw.js resolveModel:36-44` = all-inherit (`:43` role-with-frontmatter→`undefined`=inherit; `:41` invalid-role→fail-UP inherit; `:42` role-less→inherit). **Per-task escape-hatch PRESENT** `:37` `if(tier==='fable'||'opus')return tier` (H8.1 "ngoại lệ per-task" satisfied). **hmw run-trace = args.run + legacy args.wave alias** `:91` `(A.run&&A.run.dir)?A.run:((A.wave&&A.wave.dir)?A.wave:null)` ✓. ⚠️ **GAP-1 (the one real gap): hmw FLAT-vs-SUBFOLDER.** hmw STILL emits SUBFOLDER: `:103` `subMd=${dir}/sub-md/${role}-${i}.md` + desc/`:113` reference `run.md+sub-md/+harvest/`. The 2026-06-18 `h10-flat-detector-refine` (supersedes-part-of checklist-9-10 §C1/C2/C8) mandates FLAT files-one-level distinguished BY FILENAME, no subdirs. Disk confirms subfolder: `runs/2026-06-18-h10-invest/` has `harvest/`+`sub-md/` dirs (not flat). **GAP-2: SE adopted checklist v1 NOT v2** (`broadcasts/outbox/ai_infra/2026-06-18-...checklist-adopted.md:16` cites `2026-06-16-Governance-checklist-harness-9-10` = v1, no `-v2`). **UNADOPTED broadcasts (2 confirmed PENDING):** `2026-06-18-h10-flat-detector-refine` + `2026-06-18-checklist-harness-9-10-v2`. NO other newer (last outbox/all = these two; SE outbox last report = 2026-06-18 v1). **C3 two-level VERIFIED:** `git check-ignore runs/` exit=1 (NOT-ignored, neg `!.claude/**` :83) + `git ls-files runs/` = 22 files committed. `wave-x/wave.md` check-ignore exit=0 (still gitignored :93). **sleep-recovery-memory-l2 = AI_INFRA-internal** (`AI_INFRA/.claude/commands/sleep-recovery-memory-l2.md` + `docs/architecture/MEMORY-SLEEP-RECOVERY-L2-DESIGN-v3.md:scope` "CHỈ L2, KHÔNG sister"; NOT in outbox/all — only its Harness-9 broadcast form is, already adopted S70). NOT a sister obligation. **SE already does equivalent FULLY** (not ad-hoc): `memory-budget.json` (caps seeded-by-measure `scripts/measure-agent-memory.ps1`) + 4× `archive/_INDEX.md` + 4× `*.gist.md` (`distill-gen:2` counter) + `.ragignore` (exclude index/gist). Tag `[s71, harness8-audit, hmw-flat-gap, checklist-v1-not-v2, sleep-recovery-ainfra-internal, gist-additive-done]`.
- **[→ git pre-S60]** S60 recon#2 V2-engine-map (ApprovalWorkflow.cs Step/Level Order 1-based per-step; OR-of-N=N rows cùng Order service GroupBy:475; ApproveV2Async:446-634 guard+UPSERT+advance; notify DRAFTER-only:748; skipToFinal F2:561-602 = precedent advance-không-ghi-opinion) · S60 PE Section-3 submit-guard (submit path POST/pe/{id}/transitions→TransitionAsync:38 ROLE-only guard NO data-check; Section-3 mục a/b/c/d map — SUPERSEDED bởi S65ter post-Mig50 Budget-drop; test mirror PurchaseEvaluationWorkflowServiceGuardTests). Full text git.

- **2026-06-16 (S65bis recon — Employee profile master-detail vs NamGroup, on-disk):** ⭐ **STALE-PREMISE CORRECTION:** fe-user `/employees` KHÔNG list-only — `hrm/EmployeesListPage.tsx` (1201 LOC) ĐÃ master-detail 2-panel (filter sidebar :117 + list table :197 + inline detail :234) với **6 collapsible section** (`<details>` :1157, KHÔNG tab) + 5 satellite inline CRUD (WorkHistory/Education/FamilyRelation/Skill/Document, `setEditing{X}Id`+`adding{X}` mutex pattern 12-ter S35). **fe-admin == fe-user `diff -q` IDENTICAL** (SHA256 same). **Entity gần đủ screenshot:** `Domain/Hrm/EmployeeProfile.cs` (137 LOC) CÓ: DOB/Gender/Ethnicity/Religion/Nationality/Height/Weight(:98-99)/IdCard(số+ngàycấp+nơicấp :52-54)/permanent+temporary addr/phone/personalEmail/code/hireDate/qualification/salary(Base+Total)/bank/4×leave-days. **THIẾU vs screenshot:** (a) BloodType CÓ nhưng "sức khỏe loại" (health-grade A/B/C) KHÔNG; (b) thâm niên = DERIVED từ HireDate (no column); (c) chức danh = `User.Position`/`PositionLevel` (Identity, KHÔNG ở EmployeeProfile) — list/detail JOIN Users (`EmployeeFeatures.cs:467`); (d) "lương BHXH/phụ cấp" tách riêng KHÔNG có (chỉ Base+Total); đơn vị=DepartmentName JOIN. **5 satellite entity + 15 endpoint FULL** (`EmployeesController.cs:75-233` 5 region×Create/Update/Delete; GET detail Include cả 5 :455-459). Skill polymorphic gộp 3 NamGroup table (Computer/Language/Other Kind :69). **GAP THẬT:** (1) **NO org-tree** — `Department.cs` FLAT (Code/Name/ManagerUserId/Note, KHÔNG ParentId), `DepartmentsController` chỉ GET list+byId (NO /tree), KHÔNG endpoint count-per-dept → cây trái + badge phải build CLIENT-side group-by departmentId từ list; (2) **5-tab layout** screenshot = 6-section `<details>` hiện tại (re-skin UI, data đủ); (3) "Hợp đồng lao động" tab = chỉ có `EmployeeDocument` type=LaborContract(5), KHÔNG entity HĐLĐ riêng (3 HĐLĐ table DEFER Plan H2 per `EmployeeProfile.cs:10`). **NamGroup source:** `D:\...\NAMGROUP\SOURCECODE_CÔNG_TY\` find .tsx/.razor = 0 hit (KHÔNG phải React/archived) — RAG `proj_namgroup_main` 0 component; tham khảo layout = screenshot anh gửi, KHÔNG có code mirror trực tiếp. ⇒ **Wire-lại-là-xong:** data + API + satellite CRUD 100% sẵn. **Build mới:** Department.ParentId migration + /tree + count endpoint (nếu muốn org-tree thật thay client-group). **Re-skin:** 6-section→5-tab + avatar header. **No new field bắt buộc** trừ health-grade nếu anh cần. Tag `[s65bis, employee-profile, master-detail-EXISTS, dept-flat-no-tree, stale-list-only-corrected]`.

- **2026-06-17 (S69 recon — NamGroup "PURO" digital-office layout, CROSS-REPO `D:\...\NAMGROUP\`):** ⭐ **PURO = UI design-language/skin (ref ERP demo.purocorp.vn), KHÔNG phải app riêng** — NamGroup mirror sidebar/typography của nó (comments `InternalLayout.tsx:33,74,109,200,332` "PURO exact spec"). Digital-office sống trong `namgroup.client/` (app NV; admin = config-only). **Shell = `components/layout/InternalLayout.tsx`** (724L): sidebar trái fixed h-screen + `<main flex-1 overflow-auto p-2.5..lg:p-4>` :609 chứa `<Outlet/>`. Sidebar = **`navTree` hardcoded array :76-122** (KHÔNG DB), flat 2-tier group→leaf, 4 group: **"Văn phòng số" :90-100 = 6 leaf** {Danh bạ `/danhba` · Phòng họp `/phonghop` · Đề xuất `/dexuat` · Đơn từ `/dontu` · Đặt xe công `/xecong` · Ticket CNTT `/ticket`}; + Nhân sự(3) + Cá nhân{Chấm công}(1) + Hệ thống(5). Routing = `App.tsx:81-140` flat `<Route element={InternalLayout}>` > `<RouteGuard>` (perm) > index=HomePage. **Landing `/` = `internal/HomePage.tsx`** (296L): grid 2-col (LEFT 2/3 stack 4 WidgetCard: Đề xuất/Nghỉ phép/Bình luận/Truyền-thông · RIGHT 1/3 Công-việc-của-tôi); WidgetCard = gradient-blue header + inline stat-chips + body/EmptyState (shared comp tại :219). **Layout pattern mỗi feature (KHÔNG dùng tab — dùng KpiCard-row + view-toggle):** (a) `<PageHeader>` shared (icon-badge accent + breadcrumb + actions slot, `components/shared/PageHeader.tsx`); (b) **KPI stat-cards clickable filter** (DeXuat :1643 6-card / Ticket :197 5-card — "PURO KPI cards" comment); (c) body = **list-table** (DeXuat/Ticket: `<table>` master + right detail panel grid-cols-3) HOẶC **list↔calendar ViewToggle** (DonTu :683 + XeCong + PhongHop: custom month-grid Sun-Sat, NO FullCalendar — comment :PhongHop "saves install friction"); DanhBa = dept-tree trái + card-grid phải. **Shared comp tái dùng:** PageHeader · DataTable · KpiCard · CrudToolbar · ActionLogList · DatePickerVN · MasterDataPage · DonutChart/MiniBarChart (`components/shared/` 16 file). **Top files mirror:** InternalLayout.tsx(shell+nav) · HomePage.tsx(dashboard) · PageHeader.tsx · DeXuatPage.tsx(1676 KPI+table+detail) · DonTuPage.tsx(1269 +DonTuCalendar.tsx toggle) · XeCongPage/PhongHopPage(month-grid) · TicketPage.tsx(595) · DanhBaPage.tsx(756 tree+grid) · ChamCongPage.tsx(809). ⚠️ Style/màu lấy chỗ khác per task — chỉ structure. SE đã có analog (fe-user `InternalLayout`/office pages) nhưng layout khác (deep-nested vs PURO flat). Tag `[s69, namgroup-puro-recon, digital-office-layout, hardcoded-navtree, kpicard-not-tab, cross-repo]`.
- **[→ archive/2026-06.md + git]** S52 P11-D/E/F 6-gap recon (IT-pool absent → S56 corrected: dept IT exists 0 user · SlaExpiryJob HostedService DI:46 pattern · OtPolicy 3-multiplier · ClosedXML exporter reuse · Attendance API cá nhân-only · FE skeleton state — full text git pre-S60) · S50 P11-C HrmConfigs add-kind 11-chỗ pattern · S50 wave h2-verify B6 gitignore ordering + POSIX-not-pwsh (curated S57bis) · S51 gotcha #57 EXT reachability 3-Master-fix/3-skip global-filter-makes-bug (curated S59).

- **Archived S29-S37 → `archive/2026-05-q4.md` + git d2f52ba (S40 curate):** S36 G-O2 Phòng họp clean-room + FullCalendar v6 MIT eval · S36 startup MEMORY-size audit · S35 G-H2 HRM clean-room verdict · S33 G-H1 NamGroup TblNhanVien 10-bảng (105 cols main) · S33 startup RAG verify · S32 Plan G 11-module backlog · S29 Plan CA+B pre-flight (3 patterns: 9-menu terrain, V1+V2 coexist, reference-template-paths cite line-range ROI). KEY absorbed: **clean-room > NamGroup port verified 4×** · Pattern 12-bis cross-module mirror · FK+freetext dual-write.

- **2026-06-18 (S71 audit — Harness-9 PART B adap-2workflow trung-thuc, on-disk):** ⭐ **VERDICT B substantially-LANDED, ZERO nac-inflation (reports UNDER-state via honest hedge).** B1/B2 ok-runtime: 2 adap cycle ran SEPARATE workflows distinct run-id — S70 impl `wf_a58e0d15-beb`≠audit `wf_9520d8cd-4fe`; S71 impl `wf_e4e46725-231`≠review `wf_636bc95b-939` (review caught real C5-L1 over-claim impl-self-check missed). B2.5 ok: reverse-findings non-empty both reports (3+4 items)+both emails. B3 ok: 2 emails `broadcasts/outbox/ai_infra/{2026-06-17,2026-06-18}-se-to-ai_infra-*.md` carry true-nac+findings+BOTH run-ids. B4 **partial/convention-met**: rule codified `adap-apply.md:38` (short-but-confirm→review) BUT no runtime instance (no short-decision task arose S70/S71) → pure-convention, lead-discipline. ⚠️ **3 PRECISION-flaws (not protocol-gap):** (1) reviewer-agent StructuredOutput unreliable both sessions → em-main self-gate git/sha (disclosed, valid-branch per feedback memory). (2) **PATH-TRAP:** S71 report/email self-verify cited bare `git ls-files runs/`=14 — WRONG; actual = `.claude/workflows/runs/` (22 tracked files, 5 runs: invest/implement/review + h910-finalize `wf_73de399d-753` + h910-curate `wf_f32987b8-03f`). Auditor running bare `runs/` from repo-root gets 0 → false "not-committed". Folder genuinely committed `8c47bd0` + NOT-ignored at correct path. (3) hmw.js RUN-TRACE runtime honest-flagged pending-restart. adap-apply.md:33-36 codify 2-workflow mandate (auto-loaded). Tag `[s71-audit, harness-9-partB, adap-2workflow, path-trap-runs-folder, b4-convention-gap, no-nac-inflation]`.

---

## 🔄 Curate trigger
- >~30KB → archive recent → L2 `archive/<period>.md`. Stale >3mo → remove.
- **Last curate: 2026-06-18 S71 Harness-9 L1→L2 (auto-inject 25600B cap)** (29.8→23.2KB): FIFO-trimmed 3 oldest 2026-06-16 entries (S66/S65ter/S65) → `archive/2026-06.md` (additive +6 -0, md5 `cedc21eb…` byte-exact) + 3 rows `_INDEX.md` (unique substring) + 3 4-field gist `2026-06.gist.md` (distill-gen:1→2, 15→18 records). 0-byte-loss verified (numstat additive + md5 match). Kept S65bis/S69/S71. Prev: S70 (47.0→24.1KB dark-matter recovery) · S40 (35.7→~20KB) · S34 q3 · S22 q1.
