Compare commits

...

5 Commits

Author SHA1 Message Date
c2135bae2b [CLAUDE] Auth: PRO role giu quyen Master/NCC qua deploy (seeder chay-sau-revoke) + scope-isolation test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m33s
Van de (UAT anh Kiet FDC, S118): user tra.bui (Bui Le Thuy Tra, Procurement TP) dang nhap khong
thay menu quan ly NCC/Suppliers. Nguyen nhan: seeder S92 RevokeTemporarilyHiddenModulesAsync (chay
moi startup) set CanRead=false cho Master/Suppliers/Projects/Departments/Catalog* voi moi role
non-Admin ("chi Admin thay").

Fix (owner chot mo ngoai-le CHI role Procurement, ben qua deploy):
- Them SeedProcurementMasterAccessAsync chay NGAY SAU revoke de THANG (mirror pattern Hrm S65 /
  Office S69). Chi dung role Procurement -- S92 "chi Admin thay" GIU NGUYEN cho moi role khac.
- Grant 9 key: Suppliers (R+C+U -- quan ly + cong bo NCC, S113 publish can Suppliers.Update);
  Master/Projects/Departments/Catalogs + 4 Catalog leaf (Read-only). KHONG Delete.
- Dashboard/Reports CO Y KHONG cap (reviewer bat: Reports lo tong gia tri HD + top NCC/du an theo
  gia tri, nguoc S92; owner chot bo).
- Upgrade-only idempotent: nang co desired len true, khong ha co, khong dung role khac.

Test: +4 regression scope-isolation (ProcurementMasterAccessSeedTests) -- Procurement duoc nang
dung ma tran, role khac (DeptManager rieng) VAN bi revoke. 501 -> 505.

Reviewer PASS (scope cach-ly sach, no over-grant). Grant tam da ap prod (tra.bui verified 20 menu);
deploy nay = ban ben. Follow-up: gate ReportsController thieu policy (task rieng).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 15:29:44 +07:00
b56659b6b7 wal: flush 20260714T1519 2026-07-14 15:19:32 +07:00
e88e75d8f8 [CLAUDE] Docs: S118 bootstrap monitor-gate reconcile - CLAUDE.md mig-col + skills/README ef-core S117
H1 tooling-auditor @session-start flagged 2 LOW derived-doc staleness (ton-dong S113-S117, khong phai drift S117-moi):
- CLAUDE.md module-table Migration column: PE row +65,66 (multi-NCC per-hang-muc Mig 65 S114 gotcha #81 + duyet-khoang Min-Max Mig 66 S117); Master row +48,51,62-64 (Project fields Mig 48 / Department.ParentId Mig 51 / Supplier expand-import-publish Mig 62-64)
- .claude/skills/README.md ef-core-migration status-cell: Updated S113 -> S117 (Mig 66 AddPeApprovedPriceRange row)

Derived-flush (B1 pointer discipline) - canonical so van o docs/STATUS.md, day chi la enumeration lag o doc dan-xuat. Mig-column = ung vien B1-hoa cho monthly re-tier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:57:16 +07:00
b6b7c03cba wal: flush 20260714T1351 2026-07-14 13:51:36 +07:00
5c7569ed7f wal: flush 20260714T1156 2026-07-14 11:56:15 +07:00
10 changed files with 378 additions and 14 deletions

View File

@ -1,6 +1,6 @@
# WAL — auto-generated, không sửa tay
updated: — | session: — | branch: —
goal: (trống — không có mạch dở; S116 PE-negative-quote SHIP `88368fd` + CI Run 386 PASS + closeout `73f2939` done)
goal: (trống — không có mạch dở; S117 PE lũy-kế-display-fix + duyệt-theo-khoảng Min-Max SHIP `316a82f` + CI Run #387 PASS + closeout `fc1b8d9`/`d08f2a3`/`1b8b065` done)
chain:
(rỗng)

View File

@ -40,7 +40,8 @@ H2 harvest-MD-integrity auditor **SOLUTION_ERP-self**. Read-only + **propose-onl
> **S102→S106 (5 entry) verbatim → `archive/2026-07.md`** — self-compact 2026-07-12 @S111-start HOOK-directed (plan ĐỨNG từ S107/S108/S110; mở-rộng +S106 để đạt <17.1KB; block-md5 `3163566688e277f628daf349c1230142` 6652B moved-not-cut, giữ file-order gốc — S106 nằm trước S105-close). Digest: S102 CATCH ×2 (reviewer cut-not-moved f229b07 → recover + dead-satellite pointer) · S104-retro self-record blind-spot gate-agent · S105 @start mồ-côi-×1-đóng / @close moved-not-cut byte-level tooling 19380→16305 · S106 ref-table = S98-snapshot benign + cicd-not-spawned-S105 resolved-false-alarm.
> **S107→S109 (5 entry) verbatim → `archive/2026-07.md`** — self-compact 2026-07-12 @S113-end HOOK-directed (plan ĐỨNG từ S107-end/S108; +S109 đạt <17.1KB; block-md5 `75b69b41534cb67cdf349b6e1164d47f` 5623B moved-not-cut, file-order verbatim). Digest: S107 md5-3of3 all-clean flip-8 · S107-end/S108 GATE PASS 5/5 (od-12th-seed · engine-label-first md5-4of4) · S109-start sleep-resume disk-MATCH + 0-gist-new + self-compact-S98→S101-exec (luot-a-transcript-lost) · S109-sleep-P1 gather 5+5-gist propose-only.
> **S110→S111 (4 entry) verbatim → `archive/2026-07.md`** — self-compact 2026-07-13 @S116-end HOOK-directed (SELF 20KB>read-limit; plan ĐỨNG từ S114-start; block-md5 `640f80e452b42badb12bc2a3c57cf9f7` 6638B moved-not-cut, file-order verbatim, anchor 4×{L1=0·AR=1}). Digest: S110-start rerun-DEDUP luot-sang-transcript-lost (gist byte-exact `cat-file`==disk) · S110-end GATE PASS 5/5 close-gate-15of15 garble-x2-recovered + FFFD-baseline-1-selfquote seed · S111-start self-compact-S102→S106-EXECUTED + gist-frozen-x2 · S111-end GATE PASS 5/5 close-gate-0of16 h22-first-subtask-runtrace + `--stat`-truncate-trap.
- **2026-07-12 (S113 @start RE-REPORT — self-write diary):** Model `claude-opus-4-8` (Opus). Verdict 🟡 **H2 MOSTLY-CLEAN — 0 orphan/20 · corruption 0 · 2 coverage-gap propose-APPEND** (post-S112 closeout `8fa2fcc` 16:51; harvest-curator KHÔNG spawn S112 → em-main harvest direct qua `0d912ad`+`8fa2fcc`). (1) Orphan **0/20**: 4 S112 folder dùng synthesis **EMBEDDED-in-run.md** (`## synthesis` populated — pattern MỚI cho invest/review ensemble, KHÁC h21/h22 separate `implement-synthesis.md`); 14×06-18 synthesis nằm SUBFOLDER → maxdepth-1 scan FALSE-flag 5 (h10-implement/invest/review + h910-curate/finalize) → maxdepth-2 resolve sep-synth=1 (dual-accept, đừng scan nông). All 4 S112 git-tracked (6/6/4/4, 0 untracked). (2) Harvest 4/6 role CLEAN: inv-cb :20 stamp `[engine: fable-clone-ensemble · Opus-S112]` 4-field khớp run.md · reviewer ×4 :96-100 (stamp `[fable-real-single]`) · impl-fe top-🆕 date-labeled (grep-'S112' MISS = method-#4) · cicd :73 #496. (3) 🔴 2 GAP propose-APPEND: **impl-be** chỉ ghi PE (:77), THIẾU Supplier-BE (SupplierExcelImportService+preview/confirm endpoint+Mig 63 `AddSupplierImportSourceFields`) — shipped Run #496 nhưng 0 spawn-record nhà mình · **test-specialist** chỉ bump baseline 440→458 'as of D4' (nên D10/S112), 0 narrative cho ~950 test-LOC (SupplierExcelImportServiceTests +452 NEW / PeSubmitGuardAndBypass +498). Knowledge KHÔNG mất (đủ ở run.md+reviewer+cicd) → propose-only, em-main verify+APPEND B3. (4) Corruption 0: 0-byte 0 · tail 0a 6/6 · FFFD 1 = self-baseline (method-#9). Fidelity 0 fabrication → KHÔNG escalate; minor nit impl-fe '450 tests'(final 458) + test-spec label 'D4'≠'D10'. SELF ~19.2KB > 17.1 target → compact S107→S108 phiên tới. Tag `[s113-start, 0-orphan-20, embedded-synthesis-pattern, 2-coverage-gap-implbe-testspec, maxdepth1-false-flag]`.
- **2026-07-12 (S113 @end GATE — self-write diary, VERIFY-idempotent 0 re-APPEND):** Model `claude-opus-4-8`. GATE **PASS 5/5 trục + close-gate 0/21**. Coverage 6/6 sub S113 entry đủ 4-field: impl-be :77 (Mig 64 IsPublic+dedup-MST-primary) · impl-fe :9 (FE ×2app, DEVIATION-flag template-btn) · test-spec :59 (458→477 +19, 🔴 BUG NRE null-Code) · reviewer :17 **TIERED** pointer→`project_s113_supplier_import_v2_review.md` 3408B (GO-WITH-ADJ 2 must-fix) · cicd :73 `🟢 #383/id497 5fa11b5 GO 5m14s` · inv-cb :20 ensemble stamp `[engine: fable-clone-ensemble · Opus-S113]` (em-main harvest, RETURN-only). **Landing pattern MỚI: 6 diary committed qua H22 WAL-flush** (`58f1c5f``8304d08` HEAD, KHÔNG closeout `ccdcbe7`=docs-only H1) — tree clean, 0 uncommitted/mồ-côi. Fidelity MẠNH: `5fa11b5` file-list xác nhận MỌI claim on-disk (Mig 64 3-file · 2 test-file `SupplierImportV2Dedup`+`SupplierPublishAndList` · `PublishSupplierCommand`+`GetSupplierImportTemplateQuery` NEW · FE 5-file×2app) → 0 fabrication, KHÔNG escalate. **S113-start 2 GAP CLOSED** (idempotent): impl-be :80 nay có full S112-Supplier entry · test-spec narrative đủ. Close-gate: `runs/2026-07-12-S113-supplier-import-v2-invest/` run.md 4202B (**embedded=2**: `## Synthesis`+`## OUTCOME`) + spec 12900B, cả 2 tracked; orphan 0/21 (14×06-18 sep-synth · h21/h22 sep · 4 S112 embed=1 · S113 embed=2). Corruption 0: 0-byte 0 · tail `0a` 8/8 · FFFD 0/8 (S113-touched files) · moji 0. **Nit ×2 benign (INFORM, 0 data-loss, KHÔNG escalate):** (a) inv-cb entry trỏ "journal.jsonl" NHƯNG folder RETURN-only 0 jsonl/sub-*.md — content THẬT ở run.md Synthesis/OUTCOME+spec → propose sửa pointer · (b) impl-be "backfill 22 prod" stale-est, thật 37 (cicd+inv+run.md reconcile 37/37, all-rows UPDATE→moot). Chore: inv-cb 21.6KB cao nhất (em-main defer-curate next-session per plan, KHÔNG miss) · reviewer 20.3 · cicd 19.9 · impl-be 19.8 · SELF 19.3 → self-compact S107→S108 phiên tới ĐỨNG; all < 25.6 cap. Tag `[s113-end-gate, pass-5truc, close-gate-0of21, wal-flush-landing, 2gap-closed, journal-jsonl-deadpointer-nit]`.
> **S113 ×2 entry (start+end) verbatim → `archive/2026-07.md`** — self-compact 2026-07-14 @S118-start (plan ĐỨNG từ S117-end soft-due; block-md5 `0c782c7cebc8f1d8b16f65b8e1474820` 4064B moved-not-cut, file-order verbatim, anchor 2×{L1=0·AR=1}). Digest: S113-start MOSTLY-CLEAN 0-orphan/20 + 2-coverage-gap propose (impl-be thiếu Supplier-BE · test-spec thiếu narrative; embedded-synth pattern mới · maxdepth-1 false-flag lesson) · S113-end GATE PASS 5/5 0/21 (2-gap CLOSED idempotent · wal-flush-landing 6-diary · Fidelity `5fa11b5` file-list exact · nit journal-jsonl dead-pointer + backfill-22-vs-37).
- **2026-07-12 (S114 @start RE-REPORT — self-write diary, VERIFY-idempotent 0 re-APPEND):** Model `claude-opus-4-8` (Opus). Verdict 🟢 **ALL-CLEAN 5/5 trục + 0/21 orphan** (post-S113 final wal-flush `a3e9e14` 19:39:33; S113 harvest ĐÃ HOÀN-TẤT @S113-end, đây = re-verify sau flush cuối). (1) Harvest-mới **0**: porcelain 0 dòng + 12/12 L1 mtime ≤19:36:52 < `a3e9e14` (method-#3) ⟹ 0 mồ-côi by-construction; post-end DUY-NHẤT 1 commit = `a3e9e14` chạm CHỈ diary SELF (S113@end-entry + self-compact). (2) Orphan **0/21** (embedded-synth re-scan sau khi tự-bắt `-cl`-bug false-flag: 4 S112 embed=1 `## synthesis` · S113 embed=2 `## Synthesis`+`## OUTCOME` · 14×06-18 sep-synth · h21/h22 sep). (3) Coverage 6/6 sub S113 landed đủ 4-field (impl-be:77·impl-fe:9·test-spec:59·reviewer:17-TIERED·cicd:73·inv-cb:20-ensemble); **#53-garble ×2 CẢ HAI captured** (inv-cb:23 visibility-lane em-solo-recover + impl-be:77 backend-first-return SendMessage-resume); **4-BUG captured** đúng nhà (NRE-null-Code#80 test-spec:59 · draft-leak+CreateSupplier-IsPublic reviewer:17 · authz-403→D3 role→policy-fix impl-be:77); S113-start 2-GAP STAY-CLOSED (impl-be:80 S112-Supplier entry + test-spec narrative). (4) Corruption 0: 0-byte 0/12 · tail `0a` 12/12 · FFFD 1 = SELF-baseline self-quote (method-#9). (5) Self-compact S107→S109 @S113-end (`a3e9e14`) **moved-not-cut VERIFIED** (5 anchor L1=0/AR=1; S108-GATE L1=1 = digest-line mention đúng; archive 18390B tail-`0a`). **2 KNOWN benign nit persist** (em-main chưa fix, 0-data-loss, INFORM KHÔNG escalate): inv-cb:21 "journal.jsonl" dead-pointer (folder RETURN-only 0-jsonl, content THẬT @run.md-synth+narrative:20-25) · impl-be:77 "backfill 22 prod" stale-est (thật 37, `UPDATE SET IsPublic=1` all-rows→moot). Self-inconsistency benign: S113@end body ghi "compact deferred" nhưng digest+diff = EXECUTED HOOK-directed (pattern S111). Chore: inv-cb 21.6K cao nhất · reviewer 20.8 · cicd 20.4 · impl-be 20.2 · SELF 16.8K post-compact healthy → self-compact S110→S111 phiên tới; all < 25.6 cap. Tag `[s114-start, all-clean, 0-orphan-0of21, verify-idempotent, garble-x2-captured, 4bug-captured, self-compact-s107-s109-moved-not-cut]`.
- **2026-07-13 (S116 @end GATE self-write diary, VERIFY-idempotent 0 re-APPEND):** Model `claude-opus-4-8`. GATE **PASS 5-trục + close-gate 0/24** (PE nhập giá ÂM ô báo giá, shipped `88368fd` + CI #386; HEAD `0edbc58` wal-flush-landing, tree clean). **Coverage 🟡 5/6 role-bucket đủ (1 sub-gap):** 3 impl self-write đủ 4-field (impl-fe :9-15 sign-toggle-immediate-commit `math.abs`-no-global-mod SHA `3e16dba5` ×2 + gotcha `details[].quotes``suppliers[]` · impl-be :77 guard-1line ACCEPT + stale-comment-flag · test-spec :59 486495 +9 T1-T4) + cicd :79 #386 GO 5m28s; invest-ensemble 4-lane RETURN-only em-main consolidated 42 (:78 Budget-consumers + :97 edge/precedent, FE+BE-surface embedded, knowledge-complete); review-ensemble 3-lane RETURN-only Lane1 correctness-wire :21 **TIERED-pointer** (`project_pe_negative_quote_correctness_review.md` 2908B EXISTS not-dead) + Lane3 financial-invariant :77 **NHƯNG Lane2 mirror-completeness KHÔNG có spawn-record riêng** (knowledge NOT-lost: run.md:11 'mirror clean' + correctness-pointer + invest:97 SHA-identical + impl-fe SHA-pair) propose em-main APPEND minimal Lane2. fable-real deep-pass #53-GARBLE 0-verdict em-main-solo gate (run.md:13), nothing-to-harvest. **Fidelity STRONG 0-fab → KHÔNG escalate:** `88368fd` = 7-file EXACT (2FE+1BE+4test no-mig) · BE `Sum()<=0``.All(x==0)` verbatim in-diff · 9 new `[Fact]/[Theory]` = +9 = gate 495. Corruption 0: 0-byte 0 · tail 0a 7/7 · FFFD 0 worker (self=1 baseline #9) · cicd:58 `$(cygpath)` = intentional-cmd-example không shell-baked · moved-not-cut ×2 VERIFIED (reviewer2026-07 3-anchor · impl-be FIFO2026-06 5-anchor). Close-gate: run.md embedded-synth `✅ PIPELINE COMPLETE`:19 + run.md/spec git-tracked HEAD; orphan **0/24** (14×06-18 sep · h21/h22/presence sep · 4×S112+S113emb2+S114+S116 embedded). **method-#10 DOUBLE session-drift-tag S116** (cả 2 = em-main ON-BEHALF harvest, self-write all-correct-S116): reviewer:77 label `S117`-forward + invest:78 tag `[s115?]`-backward mislabel clusters tại on-behalf-harvest propose normalizeS116. Nit: run.md:20-23 leftover template `[ ]` rows dup completed Stages4-6 (benign residue, propose trim). Chore: fe-designer 21.5K idle-07-01 · invest-cb 21.1 · impl-fe 21.1 · cicd 20.4 · reviewer 19.2 (post-curate + re-append) · SELF 19.1→~21K **self-compact S110→S111 STILL-DUE** phiên tới; all < 25.6 cap. Tag `[s116-end-gate, pass-5truc, close-gate-0of24, review-lane2-mirror-gap, double-session-drift-tag-m10, fidelity-strong-7file-exact]`.
- **2026-07-14 (S117 @end GATE self-write diary, VERIFY-idempotent 0 re-APPEND):** Model `claude-opus-4-8`. GATE **PASS 5-trục + close-gate 0-orphan** (2 chỉnh PE UAT anh Kiệt: lũy-kế display-lie fix + duyệt-theo-khoảng MinMax ProMinMax, shipped `316a82f` + CI #387 501-gate; HEAD `fc1b8d9` closeout tree-clean). **Coverage 5/5 spawn** (inv×2 share investigator-codebase/ + test-spec + impl-fe + cicd): inv-luyke :107 `[fable-real-single·Fable-S117]` wf ab2657ee (#53-garbleresume + em-main prod-DB verify) · inv-minmax :104 wf abbbb86d 0-garble Model-A · test-spec :59 +5 ProMinMax · impl-fe :9 `[s117]` SHA 011ee8cf/617b0aa0 · cicd :41/59/60/61 #387. **BE both-features = em-main-solo by-design** (financial signature-ripple) 0 impl-backend record expected (KHÔNG gap). **Fidelity STRONG 0-fab → KHÔNG escalate:** commit 316a82f file-list EXACT match mọi claim (Mig 66 3-file `ApprovedPriceMaxAmount` + entity+9 + service+34 + 6 FE 3/app symmetric + 2 test-file); STATUS 501; Mig 66 COL_LENGTH=9 prod; 2 garble-lane (invest-1+cicd) 0 claim-bịa (em-main-curl cross-verify khớp). Corruption 0: tail 0a 6/6 · FFFD 0 · impl-fe compact 23.313.3KB **MOVED-NOT-CUT VERIFIED** (archive/2026-H1.md +5 entry verbatim, 6/6 SHA-anchor; HOT S117/S116/S113 giữ) · cicd:58 `$(cygpath)` intentional-cmd. Close-gate: 2 /fable-real folder spec-*.md present+tracked (luyke 5828B/minmax 4202B, dual-accept hmw-synthesis); 0 orphan (no run.md-sans-synth); delta mồ-côi 0 (4 memory WAL-committed 11:0011:29, porcelain clean). **3 benign nit INFORM (0 data-loss, KHÔNG escalate):** (a) inv-minmax mis-tag **S118→S117** (method-#10 drift fwd-1; content+spec đều S117) · (b) test-spec baseline **500/455→501/456** (thiếu em-main-solo +1 luyke `PeFinalizeProjectionTests`; đếm own +5 only) · (c) cicd:17 self-claim "S117 CLEAN no-garble ×3" vs session-log+task = garble-then-resume (under-report own garble; verdict-substance disk-true). Chore: 0 memory >25.6 cap (cicd 19.8/inv-cb 19.6 max); impl-fe healthy 13.3 post-compact; SELF 15.6→~17.2K → self-compact S113 phiên tới soft-due. Tag `[s117-end-gate, pass-5truc, close-gate-0orphan-fable-real-spec, moved-not-cut-verified, 3nit-s118mistag-testcount500-cicdgarbleclaim, fidelity-strong-commit-exact]`.
- **2026-07-14 (S118 @start RE-REPORT — self-write diary, VERIFY-idempotent 0 re-APPEND):** Model `claude-fable-5`. Verdict 🟢 **ALL-CLEAN — 0 harvest-mới · 0 orphan/26 · 0 delta mồ-côi · corruption 0 · 3-nit-S117 ALL-FIXED**. Post-S117-gate đúng 2 commit chạm memory, cả 2 accounted: `1b8b065` (em-main reconcile 3 diary = đóng trọn 3 benign-nit INFORM @S117-end, diff-verified: (a) inv-cb `## S118``## S117` · (b) test-spec 500/455→501/456 + nguồn +1 em-main-solo `PeFinalizeProjectionTests` · (c) cicd "no-garble ×3"→"×2; S117 garble→SendMessage-resume verdict-substance OK") + `5c7569e` final-flush (+1 dòng = chính S117@end-entry SELF). Porcelain 0 + 12/12 L1 mtime ≤ 11:56:15 ⟹ 0 mồ-côi by-construction (method-#3). Orphan **0/26** dual-accept: 14×06-18 (5 sub-synth maxdepth-2 + 9 flat) · 3 flat Jul (h21/h22/presence) · 6 embedded (4×S112+S113+S114) · **pe-negative-quote embedded NON-heading `✅ PIPELINE COMPLETE`:19** (regex `^## synthesis` MISS → verify-trực-tiếp trước flag, đừng tin heading-scan đơn thuần) · 2×S117 fable-real spec-only KHÔNG run.md (spec 5828B/4202B tracked) = by-design ≠ orphan. Corruption 0: 0-byte 0/12 · tail 0a 8/8 touched · FFFD 0/12 — SELF-baseline FFFD 1→0 (entry chứa self-quote đã rời L1 qua archive, sạch hơn không phải mất; cập nhật baseline method-#9). Chore: 0 L1 > 25.6 cap (fe-designer 21.5K idle-07-01 · cicd 19.96 · inv-cb 19.6); SELF ~19.3K > 17.1 target → **self-compact S113-block DUE phiên này** (plan đứng từ S117-end). Delta cần APPEND: 0. Tag `[s118-start, all-clean, 0-orphan-0of26, 3nit-all-fixed-1b8b065, embedded-nonheading-pipeline-complete, fffd-baseline-1to0]`.

View File

@ -24,3 +24,5 @@
- **2026-07-11 (S110 @end GATE — self-write diary):** GATE **PASS 5/5 trục + close-gate**. Coverage 5/5: invest S110 H21+MTv3 deep-pass PASS 5-finding `[engine: fable-real-single · Fable-S110]` (in `debe82f`) · reviewer :93 PASS_WITH_FIXES 0C/5M CATCH-4 `[fable-real-single]` · cicd #493 :73 GO 5m24s GOVERNANCE 3-commit + :55 hash-line re-verify (`38262d8`) · tooling+harvest @start `162f0b9`; 3 worker Opus WF `wf_0c3b307f-a64` role-less 0-folder đúng chuẩn return-delta. Completeness 4-field đủ cả 3 entry mới. Fidelity khớp việc-thật: verdict khớp commit-msg debe82f "PASS_WITH_FIXES 5M-fixed" · cả 2 Fable-entry tự khai #53-garble→coordinator re-emit in-session recover, diary ghi TRƯỚC re-emit (KHÔNG on-behalf) · #493 timeline khớp `6ee60cd` 20:39:15. Corruption: FFFD 0/5 diary · tail `0a` 5/5 · 0-byte 0 · 2 gist untouched đúng kỳ-vọng. C5 close-gate: `runs/2026-07-11-h21-mtv3-adopt/` run.md 1508B + spec 9372B committed, `implement-synthesis.md` 3057B non-zero (em-main scribe 21:06) — orphan **15/15 PASS**; synthesis UNTRACKED → propose commit closeout-bundle. Method-#6 tái: grep byte-window in `<60>` GIẢ (cắt multibyte) — file FFFD=0, đừng false-alarm. #53 ×2/phiên (invest+reviewer, đều recovered) = garble-rate watch. Chore: SELF ~20K → self-compact S102→S105 phiên tới ĐỨNG. Tag `[s110-end-gate, pass-5truc, close-gate-15of15, garble-x2-recovered]`.
- **2026-07-12 (S111 @start RE-REPORT — self-write diary):** Model `claude-fable-5`. Verdict 🟢 **ALL-CLEAN 5/5 trục** (post-S110 closeout `191532e` 21:14:52). (1) Harvest-mới **0**: porcelain 0 dòng + 12/12 L1 mtime ≤21:13:08 < closeout (method-#3) 0 mồ-côi by-construction; S110 landing đủ 3-commit (`debe82f` invest+reviewer · `38262d8` cicd #493 · `191532e` 3-diary-@end + session-log + run-synthesis). (2) Orphan-run **0/15** (run=1·syn_nz=1·syn_z=0 cả 15; run MỚI `2026-07-11-h21-mtv3-adopt` FLAT synthesis 3057B đã committed-in-closeout, 0 sub-*.md = return-delta-only by-design, KHÔNG flag). (3) Corruption 0 THẬT: 0-byte 0/12 · tail `0a` 12/12 · FFFD 1 hit = entry S110-@end self-quote (method-#6/#9 literal lần-đầu nằm L1, **baseline FFFD=1 từ nay**, đừng false-alarm; entry này cố-ý KHÔNG lặp literal; line-ref bỏ compact-shift) · mojibake-sig 1 hit = entry S108 self-quote known. (4) Gist ×2 FROZEN byte-exact == HEAD: tooling 6141B · SELF 6544B, diff-lines 0. (5) Fidelity: 5 diary S110 last-touch đúng 3 commit S110, S110-mention {cicd 3·tool 2·self 2·inv 1·rev 1} khớp spawn-set; 7 diary còn lại untouched = not-spawned đúng. ChoreACTION cùng lượt: hook memory-gate fire (22.2K) self-compact **EXECUTED HOOK-directed, mở-rộng S102→S106** (5 entry 6652B archive/2026-07.md, block-md5 tại digest-line trên, anchor 5×{L1=0·AR=1}, L1 22160→~16.3K) · impl-be 23955B 93.6% idle từ 06-25 · fe-designer 21.5K · cicd 20.2K · inv-cb 19.7K · reviewer 18.9K > 17.1K watch. Tag `[s111-start, all-clean, 0-orphan-0of15, gist-frozen-x2, fffd-baseline-1-selfquote, self-compact-s102-s106-executed]`.
- **2026-07-12 (S111 @end GATE — self-write diary):** GATE **PASS 5/5 trục + close-gate** (verify-idempotent, 0 re-APPEND). Coverage 7-spawn: 2 monitor @start `318a271` bundle-4-file · invest-cb +2-line & reviewer +1-line S111 tự-ghi NẰM TRONG `8aa3869` (engine-label `[engine: fable-real-single · Fable-S111]` cả 2, 0-garble) · cicd ĐANG verify Run 8aa3869 = pending-legit (S111=0 đúng trạng thái, backstop closeout) · tooling @end in-flight song-song (L1 16897B = state-318a271, pattern S107) · 3 worker `wf_5ba3daf6-fe0` role-less → memoryDelta vào synthesis đúng run-trace. Close-gate H22 run ĐẦU có sub-*.md: `runs/2026-07-12-h22-wal-adopt/` 6/6 file in-`8aa3869` (run.md 3298B `## taskList snapshot` ×1 chuẩn-MỚI · synthesis 3695B memoryDelta ×1 tail-0a · spec 16220B · sub-task-0/1/2 5737/5478/6172B) — orphan **0/16**. Fidelity: reviewer PASS_WITH_FIXES 0C/1M/~7m = 8 finding khớp commit-msg "8/8-fix" · wal-squash K=2 sạch (0 `wal:` subject trong `191532e..HEAD`; `12071c5`/`67be443` chỉ còn evidence-ref trong msg). Corruption: 0-byte 0/12 · tail `0a` 12/12 · FFFD 1 = baseline self-quote · moji 1 known · WAL.md dirty 1372B (< HEAD 1616B) = sổ-WAL live post-squash by-design flush @closeout, KHÔNG mồ-côi. Method-trap MỚI tự-bắt: `--stat` truncate `.../` nuốt path dài grep false-absence 5 file run-folder; lọc path PHẢI `--name-only`. Tag `[s111-end-gate, pass-5truc, close-gate-0of16, h22-first-subtask-runtrace, stat-truncate-trap]`.
- **2026-07-12 (S113 @start RE-REPORT self-write diary):** Model `claude-opus-4-8` (Opus). Verdict 🟡 **H2 MOSTLY-CLEAN — 0 orphan/20 · corruption 0 · 2 coverage-gap propose-APPEND** (post-S112 closeout `8fa2fcc` 16:51; harvest-curator KHÔNG spawn S112 em-main harvest direct qua `0d912ad`+`8fa2fcc`). (1) Orphan **0/20**: 4 S112 folder dùng synthesis **EMBEDDED-in-run.md** (`## synthesis` populated pattern MỚI cho invest/review ensemble, KHÁC h21/h22 separate `implement-synthesis.md`); 14×06-18 synthesis nằm SUBFOLDER maxdepth-1 scan FALSE-flag 5 (h10-implement/invest/review + h910-curate/finalize) maxdepth-2 resolve sep-synth=1 (dual-accept, đừng scan nông). All 4 S112 git-tracked (6/6/4/4, 0 untracked). (2) Harvest 4/6 role CLEAN: inv-cb :20 stamp `[engine: fable-clone-ensemble · Opus-S112]` 4-field khớp run.md · reviewer ×4 :96-100 (stamp `[fable-real-single]`) · impl-fe top-🆕 date-labeled (grep-'S112' MISS = method-#4) · cicd :73 #496. (3) 🔴 2 GAP propose-APPEND: **impl-be** chỉ ghi PE (:77), THIẾU Supplier-BE (SupplierExcelImportService+preview/confirm endpoint+Mig 63 `AddSupplierImportSourceFields`) shipped Run #496 nhưng 0 spawn-record nhà mình · **test-specialist** chỉ bump baseline 440458 'as of D4' (nên D10/S112), 0 narrative cho ~950 test-LOC (SupplierExcelImportServiceTests +452 NEW / PeSubmitGuardAndBypass +498). Knowledge KHÔNG mất (đủ run.md+reviewer+cicd) propose-only, em-main verify+APPEND B3. (4) Corruption 0: 0-byte 0 · tail 0a 6/6 · FFFD 1 = self-baseline (method-#9). Fidelity 0 fabrication KHÔNG escalate; minor nit impl-fe '450 tests'(final 458) + test-spec label 'D4'≠'D10'. SELF ~19.2KB > 17.1 target → compact S107→S108 phiên tới. Tag `[s113-start, 0-orphan-20, embedded-synthesis-pattern, 2-coverage-gap-implbe-testspec, maxdepth1-false-flag]`.
- **2026-07-12 (S113 @end GATE — self-write diary, VERIFY-idempotent 0 re-APPEND):** Model `claude-opus-4-8`. GATE **PASS 5/5 trục + close-gate 0/21**. Coverage 6/6 sub S113 entry đủ 4-field: impl-be :77 (Mig 64 IsPublic+dedup-MST-primary) · impl-fe :9 (FE ×2app, DEVIATION-flag template-btn) · test-spec :59 (458→477 +19, 🔴 BUG NRE null-Code) · reviewer :17 **TIERED** pointer→`project_s113_supplier_import_v2_review.md` 3408B (GO-WITH-ADJ 2 must-fix) · cicd :73 `🟢 #383/id497 5fa11b5 GO 5m14s` · inv-cb :20 ensemble stamp `[engine: fable-clone-ensemble · Opus-S113]` (em-main harvest, RETURN-only). **Landing pattern MỚI: 6 diary committed qua H22 WAL-flush** (`58f1c5f``8304d08` HEAD, KHÔNG closeout `ccdcbe7`=docs-only H1) — tree clean, 0 uncommitted/mồ-côi. Fidelity MẠNH: `5fa11b5` file-list xác nhận MỌI claim on-disk (Mig 64 3-file · 2 test-file `SupplierImportV2Dedup`+`SupplierPublishAndList` · `PublishSupplierCommand`+`GetSupplierImportTemplateQuery` NEW · FE 5-file×2app) → 0 fabrication, KHÔNG escalate. **S113-start 2 GAP CLOSED** (idempotent): impl-be :80 nay có full S112-Supplier entry · test-spec narrative đủ. Close-gate: `runs/2026-07-12-S113-supplier-import-v2-invest/` run.md 4202B (**embedded=2**: `## Synthesis`+`## OUTCOME`) + spec 12900B, cả 2 tracked; orphan 0/21 (14×06-18 sep-synth · h21/h22 sep · 4 S112 embed=1 · S113 embed=2). Corruption 0: 0-byte 0 · tail `0a` 8/8 · FFFD 0/8 (S113-touched files) · moji 0. **Nit ×2 benign (INFORM, 0 data-loss, KHÔNG escalate):** (a) inv-cb entry trỏ "journal.jsonl" NHƯNG folder RETURN-only 0 jsonl/sub-*.md — content THẬT ở run.md Synthesis/OUTCOME+spec → propose sửa pointer · (b) impl-be "backfill 22 prod" stale-est, thật 37 (cicd+inv+run.md reconcile 37/37, all-rows UPDATE→moot). Chore: inv-cb 21.6KB cao nhất (em-main defer-curate next-session per plan, KHÔNG miss) · reviewer 20.3 · cicd 19.9 · impl-be 19.8 · SELF 19.3 → self-compact S107→S108 phiên tới ĐỨNG; all < 25.6 cap. Tag `[s113-end-gate, pass-5truc, close-gate-0of21, wal-flush-landing, 2gap-closed, journal-jsonl-deadpointer-nit]`.

View File

@ -19,6 +19,7 @@
- [PE per-hạng-mục spec review (winner-truth lần 3)](project_pe_multi_ncc_perhangmuc_specreview.md) — PASS_WITH_ADJ. MUST: CreateContract isSingle→detailsSum (`:76-90`) over-count 1-winner≠cả-gói. Lesson: grep write-path field-NGUỒN (IsSelected) không chỉ field-đọc; cite-range bọc branch-decision.
- [S114 PE per-hạng-mục IMPL review (flip lần 3)](project_s114_multi_ncc_perhangmuc_impl_review.md) — NEEDS-FIX. CAUGHT: DeleteQuote (`...Features.cs:371-397`) KHÔNG re-derive IsWinner → xóa quote cuối (nút Xóa FE:2897) VỠ invariant → phantom winner + HĐ giá 0. Lesson: cardinality grep-consumer PHẢI gồm Delete-handler field-nguồn; spec-enum writer KHÔNG đáng tin. SOLID: 5 SUM + non-tautology test + hash ae3788e9.
- [PE negative-quote FE-only spec review (correctness-wire)](project_pe_negative_quote_correctness_review.md) — PASS_WITH_FIXES. R1/R2 no-op verified accurate (CreateContractFromEval:88-100 no-clamp · BE Sum()<=0 :206 + FE :201). MUST: sign-loss — form.thanhTien read DIRECT at POST :2232/:2875+gate :2226; toggle phải re-sign field-chung KHÔNG chỉ flip neg-flag (VndInlineEdit defer-parse-at-own-save không có ở 2 dialog). Consumer inventory COMPLETE 0-break (fmtMoney/toLocaleString neg-safe). Lesson: defer-parse widget→shared-form dialog = sign phải tới field-chung ở MỌI mutation incl toggle.
- [S118 Procurement master-access seeder review](project_s118_procurement_master_access_review.md) — PASS(cond); scope-isolation PERFECT (RoleId==Procurement, S92 held) + build 0/0. MAJOR: Reports.Read leaks contract-financials to PRO (`/reports/dashboard` unguarded `[Authorize]`-only) + `Suppliers.Update` grant does NOT enable edit (PUT role-gated Admin/CatalogManager) only Publish+Import. Lesson: menu-flag grant ≠ API capability — grep target controller authz-attrs; bare `[Authorize]` GET = open-to-all so Read-grant is FE-visibility-only.
---

View File

@ -0,0 +1,26 @@
---
name: project-s118-procurement-master-access-review
description: S118 review of SeedProcurementMasterAccessAsync (DbInitializer) — PRO role exception to S92 revoke; menu-flag vs role-gated authz reality
metadata:
type: project
---
# S118 (2026-07-14) Procurement master-access seeder review — DbInitializer.cs +85 LOC
**Verdict: PASS (conditional) — code correct/safe/scope-isolated + build 0/0; 2 MAJOR owner-decisions before commit.**
Change: new `SeedProcurementMasterAccessAsync` runs AFTER `RevokeTemporarilyHiddenModulesAsync` (line 2008→2014), grants role Procurement 11 keys (Suppliers R+C+U, rest Read).
## Clean PASS items (verified)
- Ordering 2008→2014 wins revoke. Scope isolation PERFECT — every query/insert `RoleId==role.Id` (Procurement only); S92 held for all other roles. `.ToDictionary(MenuKey)` safe — `IX_Permissions_RoleId_MenuKey` UNIQUE (Mig `20260421042236`). All 11 MenuKeys exist + in `MenuKeys.All`. Upgrade-only (false→true only), CanDelete never touched. Null-guard OK. Build clean.
## MAJOR (owner sign-off)
- **M1 Reports grant leaks contract financials to PRO:** `Reports.Read` surfaces "Báo cáo" menu → `GET /reports/dashboard` returns `TotalValueActive` + top suppliers/projects BY CONTRACT VALUE + 12-month value trend; `contracts/export` dumps contracts. `ReportsController` = `[Authorize]` ONLY (no policy) → already reachable by any authed user (pre-existing gap), but grant makes it first-class UI for PRO — contradicts S92 "hide contracts/financial from non-admin". Stated goal (suppliers) does NOT need Reports → recommend drop Reports (+maybe Dashboard).
- **M2 `Suppliers.Update` does NOT enable standard edit:** PUT/DELETE `/suppliers/{id}` = `[Authorize(Roles="Admin,CatalogManager")]` (SuppliersController :48/:57) — menu-flag irrelevant → PRO still 403 editing supplier fields. `Suppliers.Update` policy only gates Publish + bulk Import/confirm (:67-101). If "quản lý"=edit, grant insufficient. Blast radius includes mass Import.
## MINOR
- Idempotency claim FALSE: revoke re-hides any non-admin true-flag row every startup → grant re-sets true every startup → 22 writes/boot (not "0 change 2nd run"). End-state correct. Tiny no-access window between the 2 SaveChanges at boot (negligible).
- `Suppliers.Create` menu-flag = API no-op (POST /suppliers has NO policy, open to all authed per S59) — FE button visibility only.
- Recommend 1 scope-isolation regression test (SqliteDbFixture+IdentityFixture exist): DeptManager-only role stays Master/Suppliers=false after seed. Prod-verify acceptable per seeder precedent (Hrm/Off untested) → SHOULD not MUST.
**Lesson: menu-flag grant ≠ API capability — MANY master controllers gate GET with bare `[Authorize]` (open-to-all) + writes with `[Authorize(Roles=...)]`, NOT menu-key policy. So a "Read/Create/Update" permission-row grant is mostly FE-menu-visibility; real API delta = only the endpoints that actually carry `[Authorize(Policy="X.Y")]`. ALWAYS grep the target controllers' actual authz attributes before trusting a permission-grant's intended effect. Reports/Dashboard menu surfaces company financials via unguarded endpoint — "menu-only Read grant" can still leak sensitive aggregates.**

View File

@ -2,7 +2,7 @@
> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
> Update BEFORE every stop. Tiered Memory v1: L1 HOT (hook hard-limit <17KB) · L2 `archive/2026-06.md` verbatim on-demand · L3 RAG `search_memory`. Keep entry ≤ 1.5K chars.
> **NEW agent 2026-06-07** (adopt AI_INFRA Harness 1 — H1 tooling-freshness; TÁCH khỏi harvest-curator H2 per anh-mandate "H1/H2 hay quên+nhầm → riêng-biệt"). Compacted S99 2026-07-02 (19.6KB→~9KB, 12 entry 06-xx → archive NGUYÊN VĂN) · S105 2026-07-07 (19.4→~16KB, S98-S100 → `archive/2026-07.md`) · S106 2026-07-07 (19.7→14.9KB, S102-S104 @start → `archive/2026-07.md`; byte-verified md5×3 @S107) · S107 2026-07-10 @end (17.8→**16.2KB**, S101+S105 @start → archive md5×2) · S109 2026-07-10 @start (17.2→~13KB, S105-end+S106-start → archive md5×2) · S111 2026-07-12 @start+@end (18.35→16.9→~16.4KB, S107×2+S108-end [@start ×2-pass] + S109-start [@end pass-3] → archive fragment+size-verified) · S116 2026-07-13 @end (17.06→13.5→~15.4KB, S110 @start+@end → `archive/2026-07.md` verbatim sed-append +3643B-verified).
> **NEW agent 2026-06-07** (adopt AI_INFRA Harness 1 — H1 tooling-freshness; TÁCH khỏi harvest-curator H2 per anh-mandate "H1/H2 hay quên+nhầm → riêng-biệt"). Compacted S99 2026-07-02 (19.6KB→~9KB, 12 entry 06-xx → archive NGUYÊN VĂN) · S105 2026-07-07 (19.4→~16KB, S98-S100 → `archive/2026-07.md`) · S106 2026-07-07 (19.7→14.9KB, S102-S104 @start → `archive/2026-07.md`; byte-verified md5×3 @S107) · S107 2026-07-10 @end (17.8→**16.2KB**, S101+S105 @start → archive md5×2) · S109 2026-07-10 @start (17.2→~13KB, S105-end+S106-start → archive md5×2) · S111 2026-07-12 @start+@end (18.35→16.9→~16.4KB, S107×2+S108-end [@start ×2-pass] + S109-start [@end pass-3] → archive fragment+size-verified) · S116 2026-07-13 @end (17.06→13.5→~15.4KB, S110 @start+@end → `archive/2026-07.md` verbatim sed-append +3643B-verified) · S118 2026-07-14 @start (15.7→~15.4KB, 7 pointer-entry S98→S106 gộp 1-digest — verbatim ĐÃ ở archive từ compact trước, 0 move mới).
---
@ -34,19 +34,15 @@ H1 tooling-freshness auditor **SOLUTION_ERP-self**. Read-only + **propose-only**
- **Model spawn-test:** frontmatter no hot-reload — pin/inherit ăn SAU restart CLI; self-report nguyên văn = bằng chứng runtime (H4 S57bis · H8 S98).
- **Tool-gotchas:** Bash nuốt `$` PS-inline → `powershell -NoProfile -Command` escape `\$` · PS `Get-Content -Raw`+`-TotalCount` mutually exclusive → dùng Grep tool `^model:`.
- **Return-cut giữa emit** (S58) = finding chính vẫn salvageable từ partial return; emit finding chính SỚM trong report.
- **Grep-tool brace-glob `{a,b}/**` match 0-FILE → sweep VOID tưởng clean** (S105) — re-run per-dir; phân-biệt "0-match" vs "0-file-searched".
## 📅 Recent activity (FIFO — older → `archive/2026-06.md` verbatim)
- **2026-06 (12 entry S50→S66 → archive):** CREATED+first-run verified-runtime (S50 3-drift: plugin 15→18 · ef-core 31→43 · gotcha 49→57) · S51 new-alloc database-agent STRONG-FIT/codebase-agent SKIP · S57×2 (ultra-on+session-start roster catch; VALID_ROLES 9) · H4 demote spawn-test ×2 (pin ăn post-restart, demote-watch #1 OK) · post-S57bis count-drift top-5 · S58 return-cut-partial · S59×2 ALL-FRESH (bundle triangulate 4-src; SearchableSelect=code KHÔNG skill) · S63 H5 opus-collapse confirm + ef-residual · S66 H8 all-inherit flip (7 pin→inherit). → `archive/2026-06.md`.
- **2026-07-02 (3 entry S98/S99/S100 → `archive/2026-07.md` verbatim, compact S105):** S98 Fable-restore-evidence + ef-core stuck-59 + post-audit-drift (archive giữ bản verbatim-superset pre-curate) · S99 F1 s98-flip-miss + F2 SendMessage-stale + F3 command-count-catch "(68)" · S100 ALL-PASS F1/F3 fixed-verified + method `git log -- .claude/skills/` last-touch shortcut.
- **2026-07-03 (S101 @start`archive/2026-07.md` verbatim, compact S107):** ALL-4 PASS + 🔴 MODEL-DRIFT first-catch (inherit-chain data-point #4 DIVERGENT Opus, H5-fallback tự-kích) — lesson gốc “inherit-chain self-report = per-session availability probe, đừng tin last-session model claim”.
- **2026-07-03 (S101 @closeout`archive/2026-07.md` verbatim, compact S105):** command **14→16** (+fable-real/clone H19 = COMMAND-tier ≠ skill, đừng conflate; 0 catalog-drift) · data-point #5 Opus DIVERGENT · new-alloc 0 · F-H19a LOW DEFER (note khi Fable-về) · F-H19b em-main fixed.
- **2026-07-06 (S102/S103/S104 @start`archive/2026-07.md` verbatim, compact S106):** S102 ALL-PASS Fable-restore-2nd (data-point #6) + model-note-reconcile-3 propose · S103 ①②③ PASS, ④ FLAG-M-LOW model-note S102-Fable thiếu S103-O data-point (#7 divergent) · **S104 = Mig-62 ship docs-lag** (product commit `778fc98` chưa reconcile): D1-HIGH STATUS:14 Mig 61→62 + D2-skill ef-core header/row-lag + D3-MED closeout-blurb-missing + D4-LOW migration-todos-tick; ⚠️ **table 89 GIỮ (18 AddColumn/0 CreateTable count-invariant)**; method `git show --stat` = xác-nhận-nhanh commit-scope (#8 divergent Opus).
- **2026-07-07 (S105 @start`archive/2026-07.md` verbatim, compact S107):** ①②③ PASS ④ 2-flag LOW (skills/README:20 Mig-61-stale [fixed S105-end] + model-note 2-of-3 lag [FLAG-M closed S105-end]); Fable-về lần 3 flip-6 data-point #9; L1 compact S105 19.4→16KB md5-verified.
- **2026-07-07 (S105 @closeout`archive/2026-07.md` verbatim, compact S109):** ALL-4 PASS 0-sót · 4 patch em-main LANDED verified · FLAG-M CLOSED 3/3 · 🔴 method-gotcha **Grep-tool brace-glob `{a,b}/**` match 0-FILE → sweep VOID tưởng clean** — re-run per-dir; phân-biệt "0-match" vs "0-file-searched".
- **2026-07-07 (S106 @start`archive/2026-07.md` verbatim, compact S109):** ①②③ PASS ④ 2-FLAG (D-BUNDLE MED STATUS:27 live-pointer stale #484#488 [pattern gốc] · D-MODEL LOW flip-note); flip-7 Opus data-point #10 · CADENCE-proposal born-here (adopted S107). Method: ĐỪNG tin brief-baseline HEAD — re-ground `git log` + `git show --stat` mỗi commit-mới · bundle-live triangulate cicd-MEMORY (2-source-cùng-doc cũng cross-check).
- **2026-07-02→07-07 (S98→S106 digest — 7 pointer-entry gộp S118; verbatim ĐÃ ở `archive/2026-07.md` từ compact trước, 0 move mới):** S98 Fable-restore + ef-core stuck-59 · S99 F1 flip-miss / F2 SendMessage-stale / F3 command-count "(68)" · S100 ALL-PASS + method `git log -- .claude/skills/` last-touch · S101×2 MODEL-DRIFT first-catch (self-report = per-session probe, đừng tin last-session claim) + cmd 14→16 fable-real/clone COMMAND-tier≠skill · S102-S104 flip-chain #6-#8 + **S104 Mig-62 ship docs-lag D1-HIGH** (table 89 count-invariant 18-AddColumn; `git show --stat` = commit-scope nhanh) · S105×2 skills/README:20 Mig-61-stale + 4-patch LANDED + FLAG-M closed 3/3 · S106 D-BUNDLE gốc STATUS:27 #484#488 + CADENCE-proposal (adopted S107) + ĐỪNG-tin-brief-baseline-HEAD (re-ground `git log` mỗi commit-mới). Lesson brace-glob hoisted → Method.
- **2026-07-10 (S107 @start + @end`archive/2026-07.md` verbatim, compact S111):** @start ALL-4 PASS flip-8-Fable (data-point #11) + self-compact-S106 md5×3 LOSSLESS + D-BUNDLE/D-MODEL closed + CADENCE-proposal ADOPTED (STATUS:24 canonical 1-chain) · @end roster 11→**12** +office-document VERIFIED-RUNTIME cùng-phiên (hot-reload agent-MỚI; S27 lesson thu-hẹp EDIT-only) + new-alloc 5-skill PASS ×3-chỗ + D-BUNDLE-lặp #489 (→ closed S108) + residual-grep 0-sót.
- **2026-07-10 (S108 @end`archive/2026-07.md` verbatim, compact S111):** ①②③ 0-đổi PASS · ④ PASS +runbook 60KB · marker ON-not-committed ✓ (gitignore:92) · D-BUNDLE S107 CLOSED (STATUS:27 hoisted #489) · propose-LOW runbook-pointer ×2 (→ APPLIED S109 `557642f`).
- **2026-07-10 (S109 @start`archive/2026-07.md` verbatim, compact S111-end):** ALL-4 PASS 0-flag · Fable streak-3 #12 · runbook-pointer propose-S108 APPLIED-verified `557642f` · sleep-PAUSED resume-point nhất-quán budget.json · ⚠️ lesson marker-name: resolve tên THẬT từ source (fable-real:11/.gitignore:92) TRƯỚC khi flag absence (suýt false-alarm `.fable-real-active`).
- **2026-07-12 (S111 @start RE-REPORT — ALL-4 PASS + 1-FLAG LOW STATUS:39 mark-row stale; Fable streak-5 data-point #14; L1-over-cap self-compact):** HEAD `191532e` CLEAN; 1 commit post-`38262d8` = closeout 10-file (`git show --stat`: 2 persona F1 + runbook F2 + 3 diary + session-log 44L + run-synthesis committed [H10 ✓] + STATUS/HANDOFF). **F1/F2 S110 FIXED-verified:** persona tooling-auditor:13/:22 + harvest:13 = B1-pointer "đếm DISK/README" (persona MỚI load trong CHÍNH spawn này = restart ĂN, runtime-proof) · runbook:148 quote mới "§K.C:332 … 12 vai khớp hmw.js:22-30". ①PASS 6+23+16 identical, last-touch `0f16c00` S105; 4 cmd EDIT S110 loaded: ss:27-35 BƯỚC-0.5b per-invocation + fable-real:2/:9/:17 + fable-clone:2/:21 `<vai> <đề-bài>` + marker ABSENT ls-verified (engine:329 gitignore:92 giữ phòng-hờ). ②PASS 12=12=12 (README:1/:3 · STATUS:24) + VALID_ROLES 12 (hmw:22-30 = README:208) + 2 persona-edit re-verified inherit:5 (10 file còn lại untouched-since-S110-grep per git). ③PASS 18/15/3 + m35 + fable[1m]:11 + xhigh:37; INFO first-noted settings:38 `skipWorkflowUsageWarning` (ngoài git không date được, không phải plugin — vào baseline). ④1-FLAG LOW: **STATUS:39 "Mark-proposal … ⚪ pending anh confirm" STALE** — mark ĐÃ stamped (ACTIVE-MARKS:19 Active-High anh-confirm-S110 + :31 H19→📦 + chính STATUS:38 🟢 done + HANDOFF:5 "stamped") → propose flip :39 → 🟢 done stamped-S110 `RC-…20-42-01` (row thêm pre-stamp trong debe82f, quên flip @ac3f122/closeout). Còn lại PASS: engine §K.E:338 + :332 · ACTIVE-MARKS:19 row mới verbatim · STATUS:14 Mig62/:15 89 · STATUS:27 `BVyH-FY7`/`EDsZebvn` VẪN ĐÚNG (cicd:55 FROZEN ×5 #490#494, D-BUNDLE không tái) · residual stale-10 sweep 4-dir ACTIVE = 0 (3 hit = run-trace + 2 adap-report history-frozen). Model: self-report `claude-fable-5` (…S109F→S110F→**S111F** streak-5). Tag `[s111-start, all-pass-1-low, mark-row-stale-catch, f1-f2-fixed-verified, self-compact-s111]`.
- **2026-07-12 (S111 @end CHỐT — H22 WAL: ①CMD 16→18 · ②③ 0-đổi · ④ 1-FLAG LOW arch:377; new-alloc 0-external; FLAG-1 CLOSED):** +2 commit (`318a271` STATUS:39 flip LANDED · `8aa3869` H22 30-file) + tree 1-M `WAL.md` = hook-persist expected (squash §5.0 — ĐỪNG flag stray). ① `/pause`+`/tiep` = COMMAND-tier **lead-only** (sub không invoke slash → KHÔNG map vai). ③ **TOOLING-MỚI: project `.claude/settings.json` FIRST-EVER = hooks.Stop `wal-flush.ps1` ONLY** · WAL.md TRACKED 15/40-dòng · hook VERIFIED-RUNTIME ×2 `12071c5`/`67be443`. ④ anchors đủ (chi-tiết → session-log S111 + return-text): ss:47-54 BƯỚC-0.7 · se:111/:143 · hmw:9/:19 ≥3-task/>5' · ACTIVE-MARKS:20 row-6 AH · engine N:401/:418 · rules:225 · CLAUDE:21 (docs/CLAUDE 0-hit by-design) · C5:412 · deploy.yml D10 +2 path → **AS-6 class đổi — cicd-monitor lane**. 🟡 FLAG LOW: `docs/architecture.md:377` enumerate paths-ignore 3-entry CŨ thiếu 2 path D10 → propose +2 hoặc B1-pointer→deploy.yml (gotchas #41/#47 = narrative-history OK; migration-todos:154 generic OK). Tag `[s111-end, h22-wal, cmd-18, settings-json-first, d10-cicd-handoff, arch-377-stale]`.
- **2026-07-13 (S116 @end CHỐT — PE nhập-giá-ÂM = product-only; ①②③ 0-đổi PASS · ④ 2 STALE derived-summary STATUS:20+:27; new-alloc 0; ⚠️ mem-gap S112-S115 re-grounded-disk):** HEAD `88368fd` product commit (FE PeDetailTabs.tsx ×2 + BE `:206` 1-cond `.All(x=>x==0)` cho giá-ÂM=hoàn-tiền-NCC + 4 test-file, **NO migration** → tables 89 giữ). ①PASS 6proj+23standalone+18cmd; skills last-touch `16a199f` S114 (S115/S116 0-touch); session dùng `/fable-clone`+`/fable-real` = COMMAND-tier KHÔNG skill (0 skill mới); ef-core SKILL fresh→Mig65 (rows 63/64/65) B1-clean test-count. ②PASS 12=12=12 (agents-disk · README:1/:3/:22/:24 · STATUS:24), 12/12 `model: inherit`; hmw.js MOVED→`.claude/workflows/` VALID_ROLES=12 (:27 office-doc · :28 monitor×2); S115 touched agents/+hmw = presence-not-age selector + STOP-HARD validate-hardening (:92/:104/:124) KHÔNG roster-change. ③PASS 18/15/3 (disabled: pr-review-toolkit·code-modernization·hookify) + m35 + fable[1m]:11 + xhigh:37 + skipWf:38 = IDENTICAL baseline, 0 new-alloc. ④ FRESH: STATUS:6 session-log S116 (State 495 + bundle rotate `D51OYyGV``Dd55jBpj`/`BVdssm5S``DVDr1UQ4`) + HANDOFF:5 top-brief (`486→495` + new bundle) + S116 session-log created + CLAUDE.md/skills-README B1-clean. 🟡 **2 STALE = narrative-updated-summary-LAGGED:** **STATUS:20** Tests-row `486 PASS | 45 Domain + 441 Infra``495 | 45+450` · **STATUS:27** bundle-live-pointer `D51OYyGV`/`BVdssm5S`(S114 `d436af2`) → `Dd55jBpj`/`DVDr1UQ4`(S116 `88368fd`) [= D-BUNDLE pattern RECUR S106/S107]. Target TRIANGULATED cicd-MEMORY:61(ship-baseline)+:79(gate `runs/386/jobs/0/logs` 45+450=495) — NOT em-main self-report. Method: STATUS mega-line → Grep `-o` context-window `.{0,45}(486|495).{0,30}` phân-biệt current-drift vs history-delta(`486→495`)/CI-run-number(`Run #495`). Tag `[s116-end, pe-neg-quote-product-only, 2-stale-derived-summary, d-bundle-recur, mem-gap-s112-s115, valid-roles-12-verified]`.
- **2026-07-14 (S118 @start RE-REPORT — ALL-4 PASS + 2 FLAG LOW tồn-đọng [CLAUDE.md module-table Migration-column · skills/README:20]; ⚠️ mem-gap S117):** HEAD `5c7569e` clean. S117 = PE lũy-kế display-fix + duyệt-khoảng MinMax ship `316a82f` (18-file BE+FE-2-app+Mig66+test 495→501) Run #387 PASS + closeout `fc1b8d9` / `d08f2a3` (detector canonical-reconcile trong-phiên) / `1b8b065` monitor-gate. **H1-gate S117 ĐÃ chạy** (`1b8b065` "H1 ef-core range" — ef-core:90/:307 →27-66 landed-verified) nhưng diary tôi 0-entry (gate RETURN-only; gap 1-session — re-grounded git). ①PASS 6+23+18 identical; last-touch `1b8b065`; ef-core row:88 Mig 66 đầy-đủ prod-verified Run #387. ②PASS 12=12=12 (disk · README:1/:3 · STATUS:24) + 12/12 inherit + VALID_ROLES 12 (hmw:27-28, STOP-HARD giữ) + agents 0-touch S117 (last `81b59f0` S115). ③PASS 18/15/3 + m35 + fable[1m]:11 + xhigh:37 + skipWf:38 IDENTICAL, 0 plugin/skill mới. ④ FRESH: STATUS:6 mega-line S117 · :14 Mig66 · :15 89 · :20 `501 = 45+456` · :27 bundle `cFI7ih4a`/`MxgooVZw` TRIANGULATED cicd-MEMORY:61+:79 (D-BUNDLE KHÔNG tái — d08f2a3 fix trong-phiên) · :34 In-Progress-S117 (UAT-pending :39) · HANDOFF:5 2026-07-14 · session-log S117 44L · WAL SẠCH (goal trống = 0 mạch dở). 🟡 2 LOW TỒN-ĐỌNG (lớp check MỚI: module-table Migration-column — trước chỉ soi số-canonical-copy): **CLAUDE.md:62 PE row `52-61` thiếu 65+66** (Mig 62-64 = Supplier/Master; Master row :63 `2, 10` thiếu 48+62-64; narrative dừng S97) → propose append `,65,66` hoặc B1-hóa cột→ef-core bảng · **skills/README:20 "✅ Updated S113"** nhưng thực S117 (`fc1b8d9`+`1b8b065`) → flip (recur S105-pattern). Model: self-report `claude-fable-5`. Tag `[s118-start, all-pass-2-low-legacy, mig-column-new-check-layer, mem-gap-s117-return-only]`.

View File

@ -17,7 +17,7 @@ Skill này là tài liệu chuyên biệt để Claude (và developer khác) dù
| Skill | Mục đích | Trigger ví dụ | Trạng thái |
|---|---|---|---|
| `dependency-audit-erp` | Scan CVE NuGet + npm 2 FE, respect pin constraint (MediatR 12.4.1, Swashbuckle 6.9.0) | "npm audit", "dotnet vulnerable", "deps scan", "nâng cấp package" | ✅ New Tier 3 |
| `ef-core-migration` | Tạo/revert EF Core 10 migration, 3-file rule, DesignTimeDbContextFactory, migration history (mới nhất → row cuối bảng skill; số → `docs/STATUS.md`) | "thêm migration", "EF migration", "schema update", "snapshot lỗi" | ✅ Updated S113 (mig-ref → pointer anti-drift B1; Mig 63 AddSupplierImportSourceFields row added; số → `docs/STATUS.md`) |
| `ef-core-migration` | Tạo/revert EF Core 10 migration, 3-file rule, DesignTimeDbContextFactory, migration history (mới nhất → row cuối bảng skill; số → `docs/STATUS.md`) | "thêm migration", "EF migration", "schema update", "snapshot lỗi" | ✅ Updated S117 (mig-ref → pointer anti-drift B1; Mig 66 AddPeApprovedPriceRange row added; số → `docs/STATUS.md`) |
| `iis-deploy-runbook` | 3 IIS site + win-acme cert + gitea-runner + LibreOffice + debug 500/502/SignalR prod + **G-084 IPv4/IPv6 hardening** | "prod 500", "IIS fail", "cert hết hạn", "restart app pool", "deploy IIS", "port hijack" | ✅ Updated (G-084) |
## Format chuẩn 1 skill

View File

@ -59,9 +59,9 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser
| Module | Namespace | Migration | Trạng thái |
|---|---|---|---|
| Contract (HĐ) | `Domain/Contracts/` | 1-11 | Feature-complete (7 ContractType × 9 phase) |
| PurchaseEvaluation (Duyệt NCC tiền-HĐ) | `Domain/PurchaseEvaluations/` | 12,13,15,49,50,52-61 | Feature-complete — +Hạng mục (Mig 49) +ngân sách per-gói-thầu role PRO/CCM (Mig 50) +Link hồ sơ NAS (Mig 52) +cờ gấp PRO/CCM + CCM duyệt-final theo ngưỡng (Mig 53) +giá đề xuất PRO/CCM + CCM-note (Mig 54/55) +PRO-split + ghi-chú-giá (Mig 56/57) +multi-winner + level-finalize + NS-kỳ-CCM (Mig 58/59) +**`EndedByLevelFinalize` runtime-flag + backfill-fix (Mig 60/61, S97 — gotcha #78/#79)**. Export PDF pending |
| PurchaseEvaluation (Duyệt NCC tiền-HĐ) | `Domain/PurchaseEvaluations/` | 12,13,15,49,50,52-61,65,66 | Feature-complete — +Hạng mục (Mig 49) +ngân sách per-gói-thầu role PRO/CCM (Mig 50) +Link hồ sơ NAS (Mig 52) +cờ gấp PRO/CCM + CCM duyệt-final theo ngưỡng (Mig 53) +giá đề xuất PRO/CCM + CCM-note (Mig 54/55) +PRO-split + ghi-chú-giá (Mig 56/57) +multi-winner liên-danh + level-finalize + NS-kỳ-CCM (Mig 58/59) +**`EndedByLevelFinalize` runtime-flag + backfill-fix (Mig 60/61, S97 — gotcha #78/#79)** +multi-NCC per-hạng-mục (Mig 65, S114 — gotcha #81) +duyệt-khoảng giá MinMax (Mig 66, S117). Export PDF pending |
| ~~Budget (Ngân sách dự án)~~ | — | 14 → **Mig 50 DROP** | ⚠️ **REMOVED S61** — module Budget cũ XÓA, thay bằng PE-budget-per-gói-thầu (`PeWorkItemBudgets`). FE pages/types/menu `Bg_*` gỡ hết |
| Master (Supplier/Project/Department) | `Domain/Master/` | 2, 10 | Feature-complete |
| Master (Supplier/Project/Department) | `Domain/Master/` | 2, 10, 48, 51, 62-64 | Feature-complete — +Project fields Year/Investor/Location/Package (Mig 48) +Department.ParentId org-tree (Mig 51) +Supplier expand 30-field/import/publish-state (Mig 62-64) |
| Identity (User/Role/Permission/MenuItem) | `Domain/Identity/` | 1, 3, 11 | Feature-complete (30 demo user — 16 sample + 14 Solutions thật) |
| Forms (Template + Clause) | `Domain/Forms/` | 4 | Feature-complete |
| Notifications | `Domain/Notifications/` | 6 | In-app + SignalR OK, email SMTP TODO |

View File

@ -2007,6 +2007,12 @@ public static class DbInitializer
// Mở lại sau golive: gỡ prefix khỏi revoke + thêm lại vào InReviewScope.
await RevokeTemporarilyHiddenModulesAsync(db, roleManager, logger);
// [S118 2026-07-14 — anh Kiệt FDC] CHẠY SAU revoke để THẮNG: NGOẠI-LỆ role Procurement
// (Phòng Cung ứng) — mở lại Danh mục (Master/Suppliers/Projects/Departments/Catalog*) RIÊNG
// cho PRO (cần quản lý + CÔNG BỐ NCC — Suppliers R+C+U). S92 "chỉ Admin thấy" GIỮ NGUYÊN cho
// MỌI role khác (seeder chỉ đụng role Procurement). Dashboard/Reports KHÔNG cấp (anh chốt bỏ).
await SeedProcurementMasterAccessAsync(db, roleManager, logger);
// [S65 2026-06-16] CHẠY SAU revoke để THẮNG: mở lại quyền XEM "Hồ sơ Nhân sự"
// (+ root nhóm "Nhân sự") cho MỌI role — anh chốt public module Nhân sự cho
// user thường tra cứu hồ sơ. Dashboard NS (Hrm_Dashboard) + 6 catalog
@ -2180,6 +2186,83 @@ public static class DbInitializer
}
}
// [S118 2026-07-14 — anh Kiệt FDC] NGOẠI-LỆ role Procurement (Phòng Cung ứng): mở lại quyền
// Danh mục (Master + Suppliers + Projects + Departments + Catalog*) RIÊNG cho role Procurement
// — anh chốt PRO cần quản lý + CÔNG BỐ NCC (Suppliers R+C+U; S113 publish cần Suppliers.Update).
// S92 "chỉ Admin thấy" GIỮ NGUYÊN cho MỌI role khác. CHẠY SAU RevokeTemporarilyHiddenModulesAsync
// để THẮNG revoke (mirror pattern S65/S69). Dashboard/Reports CỐ Ý KHÔNG cấp (anh chốt bỏ —
// Reports lộ tổng giá trị HĐ + top NCC/dự án theo giá trị, ngược S92; reviewer S118 bắt).
// - Suppliers: Read+Create+Update (quản lý + công bố NCC). KHÔNG Delete.
// - Master + Catalogs (group) + Projects + Departments + 4 Catalog leaf: Read-only.
// - UPGRADE-ONLY (mirror Hrm S65 / Office S69): row đã tồn tại (revoke vừa set false) → NÂNG
// cờ desired lên true. Row chưa có (DB mới) → tạo. KHÔNG hạ cờ + KHÔNG đụng role khác.
// Thu hồi khi cần: xóa call ở SeedAsync — revoke sẽ tự che Master/Suppliers lại lần seed kế.
private static async Task SeedProcurementMasterAccessAsync(
ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
{
var role = await roleManager.FindByNameAsync(AppRoles.Procurement);
if (role is null)
{
logger.LogWarning("SeedProcurementMasterAccessAsync: skip — Procurement role chưa seed.");
return;
}
// (MenuKey, CanRead, CanCreate, CanUpdate) — CanDelete luôn giữ nguyên (KHÔNG cấp Delete).
var grants = new (string Key, bool R, bool C, bool U)[]
{
(MenuKeys.Master, true, false, false),
(MenuKeys.Suppliers, true, true, true), // quản lý + CÔNG BỐ NCC (S113 cần Update)
(MenuKeys.Projects, true, false, false),
(MenuKeys.Departments, true, false, false),
(MenuKeys.Catalogs, true, false, false),
(MenuKeys.CatalogUnits, true, false, false),
(MenuKeys.CatalogMaterials, true, false, false),
(MenuKeys.CatalogServices, true, false, false),
(MenuKeys.CatalogWorkItems, true, false, false),
};
var keys = grants.Select(g => g.Key).ToArray();
var existing = (await db.Permissions
.Where(p => p.RoleId == role.Id && keys.Contains(p.MenuKey))
.ToListAsync())
.ToDictionary(p => p.MenuKey);
var added = 0;
var upgraded = 0;
foreach (var g in grants)
{
if (existing.TryGetValue(g.Key, out var row))
{
var changed = false;
if (g.R && !row.CanRead) { row.CanRead = true; changed = true; }
if (g.C && !row.CanCreate) { row.CanCreate = true; changed = true; }
if (g.U && !row.CanUpdate) { row.CanUpdate = true; changed = true; }
if (changed) upgraded++;
continue;
}
db.Permissions.Add(new Permission
{
RoleId = role.Id,
MenuKey = g.Key,
CanRead = g.R,
CanCreate = g.C,
CanUpdate = g.U,
CanDelete = false,
});
added++;
}
if (added > 0 || upgraded > 0)
{
await db.SaveChangesAsync();
logger.LogInformation(
"Seeded Procurement master access: {Added} added + {Upgraded} upgraded " +
"(Master/Suppliers/Projects/Departments/Catalog* — PRO exception to S92, S118)",
added, upgraded);
}
}
// [S65 2026-06-16] Mở quyền XEM (Read-only) "Hồ sơ Nhân sự" cho MỌI role: anh chốt
// public module Nhân sự (trọng tâm Hồ sơ NS) để user thường tra cứu hồ sơ nhân sự.
// CHẠY SAU RevokeTemporarilyHiddenModulesAsync (SeedAsync) để THẮNG revoke: revoke

View File

@ -0,0 +1,255 @@
using System.Reflection;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using SolutionErp.Domain.Identity;
using SolutionErp.Infrastructure.Persistence;
using SolutionErp.Infrastructure.Tests.Common;
namespace SolutionErp.Infrastructure.Tests.Application;
// [S118 2026-07-14 — anh Kiệt FDC] Regression test SECURITY cho seeder MỚI
// DbInitializer.SeedProcurementMasterAccessAsync (private static). Đây là thay đổi
// security-sensitive → guard thuộc-tính-an-ninh CỐT LÕI: grant KHÔNG rò sang role khác.
//
// Bối cảnh chain (SeedAsync, order thực tế):
// 1. RevokeTemporarilyHiddenModulesAsync (S58+S92) → set 4 cờ CRUD=false cho MỌI role
// non-Admin trên Master/Suppliers/Projects/Departments/Catalog*/Contracts/Ct_*/Hrm*/
// Off*/Personal ("chỉ Admin thấy").
// 2. SeedProcurementMasterAccessAsync (S118) → CHẠY NGAY SAU để THẮNG revoke, mở NGOẠI-LỆ
// CHỈ role Procurement (Phòng Cung ứng):
// - Suppliers = Read + Create + Update (quản lý + CÔNG BỐ NCC, S113). Delete=false.
// - Master/Projects/Departments/Catalogs/4-Catalog-leaf = Read-only.
// UPGRADE-ONLY: row đã có → nâng cờ desired lên true; row chưa có → insert; KHÔNG hạ cờ;
// KHÔNG cấp Delete. (Dashboard/Reports CỐ Ý bị gỡ khỏi seeder — KHÔNG test 2 key này.)
//
// Thuộc-tính an ninh test bảo vệ:
// T1 upgrade-path — Procurement sau revoke→seed có ĐÚNG ma trận grant (Suppliers R+C+U D=false;
// 8 key read-only chỉ R). Chứng seeder THẮNG revoke đúng phạm vi.
// T2 SCOPE ISOLATION (quan trọng nhất) — role non-Admin KHÁC (DeptManager, đứng RIÊNG) VẪN bị
// ẩn hết sau cùng chuỗi → grant CHỈ đụng Procurement, S92 giữ nguyên role khác.
// Kèm CONTRAST assert Procurement THẬT được nâng (chống vacuous-pass).
// T3 insert-path — DB mới (chưa có row, không revoke) → seeder INSERT 9 row; Delete=false (an ninh).
// T4 idempotent — chạy lần 2 → end-state bất biến + KHÔNG tạo row trùng + KHÔNG chạm role khác.
//
// Kỹ thuật: 2 method là `private static` (KHÔNG sửa prod đổi visibility) → gọi qua REFLECTION
// (BindingFlags.NonPublic|Static). Signature cả 2: (ApplicationDbContext, RoleManager<Role>, ILogger).
// Test ĐÚNG behavior code thật, KHÔNG re-implement predicate/grant-set.
//
// FK (PermissionConfiguration): Permission.MenuKey → MenuItem.Key (Cascade) + RoleId → Role
// (Cascade) → PHẢI seed MenuItem rows + Role TRƯỚC khi seed Permission (nếu không SQLite FK Error 19).
// Revoke skip nếu Admin role chưa seed → luôn seed Admin dù test không assert Admin.
public class ProcurementMasterAccessSeedTests
{
// Suppliers = key DUY NHẤT được Read+Create+Update (quản lý + công bố NCC — S113). Delete=false.
private const string SuppliersKey = MenuKeys.Suppliers;
// 8 key CHỈ đọc (Read-only) — seeder chỉ nâng CanRead.
private static readonly string[] ReadOnlyKeys =
{
MenuKeys.Master,
MenuKeys.Projects,
MenuKeys.Departments,
MenuKeys.Catalogs,
MenuKeys.CatalogUnits,
MenuKeys.CatalogMaterials,
MenuKeys.CatalogServices,
MenuKeys.CatalogWorkItems,
};
// Toàn bộ 9 key seeder đụng (Suppliers + 8 read-only). Dùng const/factory MenuKeys
// (KHÔNG hardcode string) để bắt đồng-bộ nếu key đổi.
private static readonly string[] AllGrantKeys =
ReadOnlyKeys.Append(SuppliersKey).ToArray();
// ── Reflection invoke private static (ApplicationDbContext, RoleManager<Role>, ILogger) ──
private static async Task InvokePrivateStaticAsync(IdentityFixture fix, string methodName)
{
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var rm = fix.Services.GetRequiredService<RoleManager<Role>>();
var mi = typeof(DbInitializer).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
mi.Should().NotBeNull(
$"DbInitializer.{methodName} phải tồn tại (private static) — đổi signature thì cập nhật test");
var task = (Task)mi!.Invoke(null, new object[] { db, rm, NullLogger.Instance })!;
await task;
}
private static Task InvokeRevokeAsync(IdentityFixture fix) =>
InvokePrivateStaticAsync(fix, "RevokeTemporarilyHiddenModulesAsync");
private static Task InvokeSeedProcurementAsync(IdentityFixture fix) =>
InvokePrivateStaticAsync(fix, "SeedProcurementMasterAccessAsync");
// Seed roles: Admin (để Revoke KHÔNG skip) + Procurement (target grant) + DeptManager
// (control isolation — đứng RIÊNG, KHÔNG kèm Procurement). Trả (adminId, procurementId, otherId).
private static async Task<(Guid adminId, Guid procurementId, Guid otherId)> SeedRolesAsync(IdentityFixture fix)
{
var rm = fix.Services.GetRequiredService<RoleManager<Role>>();
var admin = new Role { Id = Guid.NewGuid(), Name = AppRoles.Admin };
var procurement = new Role { Id = Guid.NewGuid(), Name = AppRoles.Procurement };
var other = new Role { Id = Guid.NewGuid(), Name = AppRoles.DeptManager };
await rm.CreateAsync(admin);
await rm.CreateAsync(procurement);
await rm.CreateAsync(other);
return (admin.Id, procurement.Id, other.Id);
}
// Seed MenuItem rows (FK target cho Permission.MenuKey — cả pre-grant lẫn seeder-insert cần).
private static async Task SeedMenuItemsAsync(TestApplicationDbContext db)
{
foreach (var key in AllGrantKeys)
db.MenuItems.Add(new MenuItem { Key = key, Label = key });
await db.SaveChangesAsync(CancellationToken.None);
}
private static void AddPerm(
TestApplicationDbContext db, Guid roleId, string menuKey,
bool canRead = false, bool canCreate = false, bool canUpdate = false, bool canDelete = false)
=> db.Permissions.Add(new Permission
{
RoleId = roleId,
MenuKey = menuKey,
CanRead = canRead,
CanCreate = canCreate,
CanUpdate = canUpdate,
CanDelete = canDelete,
});
private static async Task<Permission?> GetPermAsync(TestApplicationDbContext db, Guid roleId, string menuKey)
=> await db.Permissions.AsNoTracking()
.FirstOrDefaultAsync(p => p.RoleId == roleId && p.MenuKey == menuKey);
private static async Task AssertFlagsAsync(
TestApplicationDbContext db, Guid roleId, string key,
bool r, bool c, bool u, bool d, string because)
{
var row = await GetPermAsync(db, roleId, key);
row.Should().NotBeNull($"{key}: {because}");
row!.CanRead.Should().Be(r, $"{key}.CanRead — {because}");
row.CanCreate.Should().Be(c, $"{key}.CanCreate — {because}");
row.CanUpdate.Should().Be(u, $"{key}.CanUpdate — {because}");
row.CanDelete.Should().Be(d, $"{key}.CanDelete — {because}");
}
// Ma trận grant kỳ vọng cho Procurement sau khi seeder chạy: Suppliers R+C+U (D=false),
// 8 key còn lại chỉ Read. Dùng chung cho T1/T2-contrast/T3/T4.
private static async Task AssertProcurementGrantMatrixAsync(TestApplicationDbContext db, Guid procurementId)
{
await AssertFlagsAsync(db, procurementId, SuppliersKey,
r: true, c: true, u: true, d: false,
"Suppliers phải R+C+U cho Procurement (quản lý + CÔNG BỐ NCC — S113), Delete=false");
foreach (var key in ReadOnlyKeys)
await AssertFlagsAsync(db, procurementId, key,
r: true, c: false, u: false, d: false,
"read-only grant — CHỈ CanRead, KHÔNG Create/Update/Delete");
}
// T1 — UPGRADE PATH (chain prod-realistic): Procurement từng có CanRead → revoke S92 hạ false
// → seeder S118 nâng lại đúng ma trận. Chứng seeder THẮNG revoke, đúng phạm vi cờ.
[Fact]
public async Task Procurement_AfterRevokeThenSeed_HasExactGrantMatrix()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (_, procurementId, _) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
// Procurement từng có CanRead trên cả 9 key (grant cũ trước S92) → revoke có gì để hạ.
foreach (var key in AllGrantKeys)
AddPerm(db, procurementId, key, canRead: true);
await db.SaveChangesAsync(CancellationToken.None);
await InvokeRevokeAsync(fix); // S92: 9 key CanRead=false cho non-Admin
await InvokeSeedProcurementAsync(fix); // S118: NGOẠI-LỆ Procurement — nâng lại
await AssertProcurementGrantMatrixAsync(db, procurementId);
}
// T2 — ⭐ SCOPE ISOLATION (thuộc-tính an ninh QUAN TRỌNG NHẤT): role non-Admin KHÁC
// (DeptManager, đứng RIÊNG — KHÔNG kèm Procurement) VẪN có Master/Suppliers CanRead=false
// sau cùng chuỗi seed → grant S118 CHỈ đụng Procurement, S92 "chỉ Admin thấy" giữ nguyên.
[Fact]
public async Task SeedProcurement_DoesNotLeakToOtherNonAdminRole()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (_, procurementId, otherId) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
// CẢ Procurement + DeptManager từng có CanRead trên 9 key (grant cũ) → revoke hạ CẢ HAI.
foreach (var key in AllGrantKeys)
{
AddPerm(db, procurementId, key, canRead: true);
AddPerm(db, otherId, key, canRead: true);
}
await db.SaveChangesAsync(CancellationToken.None);
await InvokeRevokeAsync(fix); // revoke CẢ HAI role về false
await InvokeSeedProcurementAsync(fix); // CHỈ nâng lại Procurement
// ⭐ ISOLATION: DeptManager row TỒN TẠI (revoke giữ vết) NHƯNG mọi cờ = false — seeder
// Procurement KHÔNG chạm role khác. Assert cả Master + Suppliers (2 key task chốt) + 7 key kia.
foreach (var key in AllGrantKeys)
await AssertFlagsAsync(db, otherId, key,
r: false, c: false, u: false, d: false,
"DeptManager KHÔNG được Procurement-grant chạm — S92 'chỉ Admin thấy' giữ nguyên role khác");
// CONTRAST (chống vacuous-pass): Procurement THẬT SỰ được nâng → chứng seeder CÓ chạy,
// nên isolation ở trên KHÔNG phải "false vì seeder no-op".
await AssertProcurementGrantMatrixAsync(db, procurementId);
}
// T3 — INSERT PATH (DB mới): chưa có Permission row + KHÔNG revoke → seeder phải INSERT 9 row.
// Chốt security: hàng insert KHÔNG cấp Delete (CanDelete=false trên cả 9).
[Fact]
public async Task SeedProcurement_InsertPath_FreshDb_CreatesRowsWithoutDelete()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (_, procurementId, _) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
// KHÔNG pre-grant + KHÔNG revoke → nhánh insert (db.Permissions.Add) của seeder.
await InvokeSeedProcurementAsync(fix);
await AssertProcurementGrantMatrixAsync(db, procurementId);
var count = await db.Permissions.AsNoTracking().CountAsync(p => p.RoleId == procurementId);
count.Should().Be(AllGrantKeys.Length, "seeder insert đúng 9 row (Suppliers + 8 read-only), không dư");
}
// T4 — IDEMPOTENT: chạy seeder lần 2 → end-state bất biến + KHÔNG tạo row trùng + KHÔNG chạm role khác.
[Fact]
public async Task SeedProcurement_Idempotent_SecondRunNoChange()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (_, procurementId, otherId) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
foreach (var key in AllGrantKeys)
{
AddPerm(db, procurementId, key, canRead: true);
AddPerm(db, otherId, key, canRead: true);
}
await db.SaveChangesAsync(CancellationToken.None);
await InvokeRevokeAsync(fix);
await InvokeSeedProcurementAsync(fix);
await InvokeSeedProcurementAsync(fix); // ⭐ chạy lần 2 — end-state phải bất biến
await AssertProcurementGrantMatrixAsync(db, procurementId);
// Không nhân đôi row Procurement.
var procCount = await db.Permissions.AsNoTracking().CountAsync(p => p.RoleId == procurementId);
procCount.Should().Be(AllGrantKeys.Length, "idempotent — chạy lại KHÔNG tạo row trùng");
// Role khác vẫn bị ẩn sau lần 2 (isolation bền vững).
foreach (var key in AllGrantKeys)
await AssertFlagsAsync(db, otherId, key,
r: false, c: false, u: false, d: false,
"idempotent — lần seed thứ 2 KHÔNG rò sang DeptManager");
}
}