diff --git a/.claude/WAL.md b/.claude/WAL.md index 0c27ced..0d09c02 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -3,14 +3,14 @@ updated: 2026-07-12 | session: S114 | branch: main goal: PE multi-NCC per hạng mục (anh Kiệt) — cho ≥2 NCC-TP trúng thầu/1 hạng mục, mỗi nhà 1 giá riêng — 6-bước fable-clone→hmw→verify→deploy→session-end chain: -[x] B1 fable-clone invest ensemble (4-lane inv-cb) DONE — wf_dd49ddc0-f29, spec-file ghi -[x] B2a anh Kiệt chốt D1=SUM · D2=THAY-THẾ · D3=giá-gốc · D4=1HĐ/NCC (all recommended, D5 default) — spec LOCKED, 0 bảng/cột mới -[!] B2b fable-clone review ensemble (3 reviewer lane: consumer-completeness + correctness + checklist-risk) → spec cuối — RUNNING -[ ] B3 hmw Opus 4.8 MAX implement (BE re-key 6 read-site + FE 2-app + test 6-7 file) -[ ] B4 fable-clone reviewer verify (grep-suite 0 sót IsWinner-aggregation) +[x] B1 fable-clone invest ensemble (4-lane inv-cb) DONE — wf_dd49ddc0-f29 +[x] B2a anh Kiệt chốt D1=SUM · D2=THAY-THẾ · D3=giá-gốc · D4=1HĐ/NCC (D5 default) — spec LOCKED, 0 bảng/cột mới +[x] B2b fable-clone review ensemble DONE — wf_1e127f4b-de5 (2/3 clean, lane correctness #53-garble→em-recover); 6 adjustment A1-A6 fold vào spec cuối +[!] B3 hmw Opus 4.8 MAX implement — RUN-TRACE 3-lane (implementer-backend + implementer-frontend + test-specialist) +[ ] B4 fable-clone reviewer verify (grep-suite 0 sót IsWinner-aggregation + threshold-crossing) [ ] B5 deploy + commit (cicd verify) [ ] B6 session-end -next: chờ review ensemble return → em-main synthesize → cập nhật spec cuối → B3 hmw implement -verify: reuse Quote.IsSelected (0 migration cột), re-key 6 BE read-site (winnerQuoteTotal:1189 · submit-guard:196 · ccm-threshold:885 · CreateContract:56 · budget-B:907 · projection) + FE PeDetailTabs SHA-lock 2-app + backfill IsWinner→IsSelected phiếu cũ. Adversarial-grep tìm consumer sót (S87/S88 lesson) -run-folder: .claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/ +next: launch B3 hmw implement per spec cuối. DTO chốt: POST /select-winner {detailId, supplierIds[]} per-detail. 6 adjustment: A1 giaTri=SUM(IsSelected) bỏ detailsSum (⚠️ single đổi budget→bid) · A2 derive IsWinner mọi writer+seed+backfill-2-chiều · A3 re-key SPLIT (5 financial→IsSelected, winner-names GIỮ IsWinner-derived) · A4 endpoint+FE per-cell grid · A5 test-before+threshold-crossing · A6 mig 3-file Down-noop +verify: build 0-error + dotnet test (7 file cũ update + test mới per-hạng-mục total + threshold-crossing) + npm build ×2 + FE PeDetailTabs SHA 2-app +run-folder: .claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/ (run.md + spec-multi-ncc-per-hangmuc-12-07-2026.md) diff --git a/.claude/agent-memory/reviewer/MEMORY.md b/.claude/agent-memory/reviewer/MEMORY.md index d3abcf2..206ea53 100644 --- a/.claude/agent-memory/reviewer/MEMORY.md +++ b/.claude/agent-memory/reviewer/MEMORY.md @@ -14,7 +14,9 @@ - [S103 H20+crystallized-backfill review](project_s103_h20_crystallized_backfill_review.md) — WF2 PASS (fable-clone ensemble); 6/6 hash recompute MATCH + 4 already-met verified file:line. **Lesson: multi-axis warning phải nằm trong OUTPUT của script, không chỉ doc** — reader chạy script inline. - **S101 H19 WF2 review LANE-C (adap-reports+honesty)** — PASS; 4 hash recompute MATCH; hex→SHA = recompute CẢ strip/no-strip chứng content-hash ≠ commit-SHA. → `archive/2026-07.md`. - [S111 PE sign-off 6-decision design review (schema/history lens)](project_pe_signoff_history_schema_review.md) — GO-WITH-ADJ; Decision-5 opinion-history: RECOMMEND (a) append-only table, REJECT (b) version-col (drops UNIQUE + breaks UPSERT read every site), (c) Changelog zero-mig fallback. Hook @ ApproveV2Async UPSERT :784-790. Decision-3 caller-safe (SlaExpiryJob=Contract-only, not PE). A1 breaks 4 bypass tests. +- [PE multi-NCC per-hạng-mục cardinality#3 review (consumer-completeness lens)](project_pe_multiwinner_perhangmuc_review.md) — PASS_WITH_ADJ; 6 BE read-site inventory COMPLETE (grep 1:1, no sót — unlike S87). 2 MUST: (1) giaTri isSingle/detailsSum branch silent-financial under per-hạng-mục; (2) derive-completeness — seed:1310-1316 sets IsSelected never IsWinner → seeded phiếu empty names + CreateContract throws. Lesson: derived source-of-truth = EVERY write-path of base field must re-derive. - [S113 Supplier import v2 close-review (owner anh Kiệt)](project_s113_supplier_import_v2_review.md) — GO-WITH-ADJ 2 must-fix: consumers (PeDetailTabs:2222 + ContractCreatePage:321 ×2app) DON'T pass published=true → drafts leak into pickers (R4/R6 "ẩn" undelivered) + CreateSupplier:86 missing IsPublic=true. Dedup/publish/mig/authz SOLID. Lesson: "filter added" ≠ "drafts hidden" — grep-all-consumer + create write-path. +- [PE multi-NCC per-hạng-mục spec review (winner-truth cardinality lần 3)](project_pe_multi_ncc_perhangmuc_specreview.md) — PASS_WITH_ADJ. MUST: CreateContract isSingle→detailsSum (`:76-77,89-90`) over-count khi 1-winner≠cả-gói, phá `Σgiari==winnerQuoteTotal`; spec cite `84-93` LOẠI isSingle = bẫy partial-re-key S87. SHOULD: derive-integrity 3 write-path (winner-cmd + UpsertQuote:308/332 + seed:1257 KHÔNG set IsWinner). 6-site IsWinner grep ĐỦ (0 sót). Lesson: grep write-path field-NGUỒN (IsSelected) không chỉ field-đọc; cite-range bọc branch-decision. --- @@ -94,17 +96,15 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod - **S109 (2026-07-10 fidelity-gate 2 gist L2) — PASS_WITH_FIXES (3):** gist meta-count 15≠disk 14; delta tự-tính +8.8KB = self-computed false-specificity; N/A-just over-claim → fact-claim trong N/A verify như content. → `archive/2026-07.md`. - **S111 (2026-07-12 H22 WAL adopt) — PASS_WITH_FIXES (0C/1M):** fault-inject 2-ca PASS; CATCH CAVEAT "STAGED" còn khi N.3 nâng VERIFIED-RUNTIME = nâng-nấc-quên-sync cùng-diff; live-witness>forensics (commit GIỮA review = chứng hook fire). → `archive/2026-07.md`. - **S110 (2026-07-11 H21+MTv3 adopt) — PASS_WITH_FIXES (0C/5M):** doc "VALID_ROLES 12" nhưng code=10 → claim-về-code grep lại kể cả reword; supersede trỏ adap-report chưa tạo = dangling. → `archive/2026-07.md`. -- **2026-07-12 (S112 Supplier Excel-import Phase-B close-review, FE/E2E/DEPLOY lane) `[fable-real-single]`:** **GO-WITH-ADJUSTMENTS** — detail → [project_s112_supplier_import_review.md](project_s112_supplier_import_review.md). Mandatory known adjust = bake ExpectedHeaderTokens (svc:35-67); MAJOR independent catch = FE Import button reuses Suppliers.Create guard but endpoint needs Admin/CatalogManager role → non-functional button for fe-user drafters (BE secure). Deploy dup-risk CONFIRMED safe (4 real NCC ungated-seed Codes=col4 → re-import Update/fill-nulls). Lesson: reusing sibling menu-guard for Admin-scoped NEW action under-restricts — derive guard from ENDPOINT authz. Positive: 24 clamp-consts=EF-len byte-exact, int-enum contract exact (no StringEnumConverter), multipart matches 4 working uploads. -- **2026-07-12 (S112 PE sign-off approach-review PRE-fan-out+deploy, security/governance lane) `[fable-real-single]`:** **GO-WITH-ADJUSTMENTS.** (1) EDGE-5 decision-3 hard-lock: spec placement (reject-branch SVC:92 + EnsureCanReject:321 + handler) **INCOMPLETE — bỏ sót admin-override path SVC:290** (`if(isAdmin) evaluation.Phase=targetPhase`) → admin un-terminal DaDuyet bằng decision=Approve (KHÔNG qua reject). Fix = 1 guard TOP-of-method sau `var fromPhase` SVC:53. Lesson: **terminal-lock gác FROM-state ở ĐỈNH method, KHÔNG rải per-branch** (per-branch bỏ sót admin/fall-through). (2) CEO-skip decision-2: A1 (creator tự ký ô mình thay auto-bypass) **TẠO đường CEO-skip MỚI** multi-step-có-CEO — trước A1 bypass advance-qua slot creator (bỏ check finalize); sau A1 creator qua ApproveV2Async → slot có `AllowApproverFinalize`=true + tick → DaDuyet bỏ CEO (SVC:867). invest-C "đã tồn hôm nay" IMPRECISE (chỉ đúng 1-step-no-CEO). Escalate owner. Lesson: **đổi auto-advance→manual-approve = mở lại per-slot flag mà auto-path che khuất** — re-scan flag khi đổi ai-đi-qua-code-nào. +- **S112 (2026-07-12) close-reviews `[fable-real-single]` — GO-WITH-ADJ ×2:** (import) FE Import btn reuses Suppliers.Create guard but endpoint = Admin/CatalogManager → dead button drafters → **derive guard from ENDPOINT authz not sibling-menu**; (PE sign-off) EDGE-5 terminal-lock sót admin-override SVC:290 → **gác FROM-state ở ĐỈNH method KHÔNG per-branch**; A1 mở CEO-skip mới. → [topic](project_s112_supplier_import_review.md), `archive/2026-07.md`. -- **2026-07-12 (S112 Supplier Excel-import close-review PRE-deploy, BE+ADJUST) `[fable-real-single]`:** **GO-WITH-ADJ.** 6 axes PASS: OrdinalIgnoreCase preview+confirm (BuildCiIndex:446/batchByCode:157/seen:158), FillNulls:381-409 skip Code/Name/Type, all-or-nothing gate:137-152 pre-mutate + SaveChanges:188, parser abs Cell:284 (KHÔNG CellsUsed) +#REF!→null:289 +NAS-raw:290 +fingerprint-Ordinal-reject:223, Mig 63 reversible+3-file+snapshot:3330, col→field 30/30 OK (Code=col4 viết-tắt, Name=col5 tên-cty). REQUIRED adjust = bake 30 real token → ExpectedHeaderTokens:37-66 (nguồn test RealFileHeaderTokens:420-451 == brief exact; Case 9 auto-flip). **CAUGHT (MAJOR de-risk): Unicode NFC/NFD** — test dựng workbook từ literal NFC nên KHÔNG chứng file Excel THẬT accept; đề `.Normalize(FormC)` NormalizeHeader:306. Lesson: self-flip token-test = internal-consistency KHÔNG external-file-acceptance (artifact ngoài repo = bất-khả-verify). -- **2026-07-12 (S112 FINAL pre-commit+deploy review — BÓ 2-feature PE-signoff + Supplier-import) `[fable-real-single]` — VERDICT GO (0 must-fix):** Cả 2 close-review adjust ĐÃ áp đúng. **F1 PE**: EDGE-5 guard đúng ĐỈNH-method SVC:66 (sau fromPhase:53) exempt admin+system, precede CẢ 5 branch incl admin-override:306 (:306 vốn `if(isAdmin)` nên non-admin không tới) + EnsureCanRejectV2Async:332 untouched; A1 minOwn=Min(Order):607 bypass1)`:637 (StepIdx giữ 0 từ submit:265), 4 nhánh cũ (auto-sign/next-Bước/step2/terminal) DELETED; History CHANGE4:751-777 log opinion-cũ→Changelog TRƯỚC 4 dòng overwrite, chỉ non-empty, keyed per-level-row (matchingLevel.Id) → chỉ fire khi CÙNG-NV re-sign, enum Workflow=5/Update=2 tồn. Tests 8-new/4-updated adversarial-grade (EDGE-5 system-exempt chứng qua msg "không hỗ trợ"≠"kết thúc"; history 2-boundary). **F2 import**: 30 ExpectedHeaderTokens==test byte-identical all-NFC (script verify 0-mismatch); NormalizeHeader FormC:313 áp cả 2 phía; endpoint [Authorize(Roles=Admin,CatalogManager)] = pattern Update/Delete; Mig 63 = 2 nullable additive (prod-safe, expansion-cols từ Mig 62 `778fc98` deployed per 4-NCC-in-prod). **Nice-to-have (KHÔNG block)**: (a) test comment :31-37 + svc :31-34 stale "BEST-GUESS chưa khớp" (đã bake) — misleading, harmless; (b) test NormalizeJoin:407-414 thiếu `.Normalize(FormC)` mà svc:313 có — moot vì token toàn NFC nhưng latent nếu sau thêm token NFD. **UAT-visible behavior change (by-design, không phải bug)**: A1 bỏ auto-terminal-on-submit → TP tạo phiếu-1-bước-tự-là-cấp-cuối GIỜ phải bấm Duyệt thêm 1 lần (trước tự DaDuyet). **Lesson: Case-9 self-adjusting (shouldMatch tính runtime) PASS ≠ chứng real==expected → phải diff byte-level 2 mảng token TAY; build/test-green (458) nhận theo claim, review logic bằng đọc.** +- **S112 (2026-07-12) import BE-close + FINAL bundle — GO 0-must-fix:** 6 axes PASS; adjust applied (30 ExpectedHeaderTokens byte-identical NFC + `.Normalize(FormC)` both sides; guard @top SVC:66). UAT by-design: A1 bỏ auto-terminal-on-submit. **Lessons: self-flip token-test = internal-consistency NOT external-file-accept; build/test-green nhận theo claim, review LOGIC bằng đọc; byte-diff token-array TAY.** → `archive/2026-07.md`. --- ## 🔄 Curate trigger - Hook-cap **>17.1KB** (24.4KB read-limit — đổi từ ~30KB cũ, S109) → archive recent → L2 `archive/.md` (append additive) + `_INDEX.md` substring pointer. Stale >3mo → remove. -- **S109 curate (self, 2026-07-10, hook 22.4KB):** moved 4 verbatim S100/S101 (2 Area + 2 Recent, added 07-03) → `archive/2026-07.md` + `_INDEX.md` +4 pointer (count=1 verified); digest 1-dòng giữ L1. -- **S102 recovery (em-main, 2026-07-06):** cut-not-moved ×5 tại `f229b07` → recovered 5 entry từ git vào `archive/2026-07.md` + `_INDEX` +5 pointer. **Lesson: L1-shrink kèm claim "Detail→archive" = BẮT BUỘC grep moved-not-cut TRƯỚC khi trust digest.** -- **Last curate: 2026-06-20 S80 (em-main, archive-gate keep-floor-hit → manual)** (45.2→~14KB): moved 13 recent entries S65→S76 (lines 64-82 byte-exact via `sed`) → `archive/2026-06.md` (32.7→70KB, +13 entries) + `_INDEX.md` +13 substring pointers (all verified count=1). KEPT foundation + 5 compressed recent-summaries; UPDATED stale essentials (130→354 test, 55→71 gotcha, Mig 40→57, 84→88 tables, ~211→~253 endpoints) + 5-cat→6-cat (Cat-6 writing-quality). gist NOT updated (em-main distill later). -- **Prev curate: 2026-06-18 S71** (36.7→24.2KB): moved 10 entries; KEPT foundation + newest cluster. **Prev S70** (42.5→24.8KB): built `_INDEX.md` + `.gist.md` gen:1. **Prev S40** (28.4→18KB). +- **S113 curate (self, 2026-07-12, hook 21.3KB→~16KB):** compressed 4 verbose S112 entries (2 close-review lines→1, BE-close+FINAL→1) + this curate-log in-place; NO file-move (lessons preserved inline). digest kept L1. **+review-lane addendum:** 4 verbatim ALSO moved → `archive/2026-07.md` + 4 `_INDEX` pointer (move-not-cut) + trimmed 2 digest. +- **S109 curate:** moved 4 verbatim S100/S101 → `archive/2026-07.md` +4 pointer. +- **S102 recovery (em-main):** cut-not-moved ×5 @`f229b07` recovered từ git. **Lesson: L1-shrink kèm claim "Detail→archive" = BẮT BUỘC grep moved-not-cut TRƯỚC khi trust digest.** +- **Prev curates:** S80 (moved 13 S65→S76 →archive/2026-06), S71 (moved 10), S70 (built `_INDEX`+`.gist`), S40. diff --git a/.claude/agent-memory/reviewer/archive/2026-07.md b/.claude/agent-memory/reviewer/archive/2026-07.md index e59d256..506686d 100644 --- a/.claude/agent-memory/reviewer/archive/2026-07.md +++ b/.claude/agent-memory/reviewer/archive/2026-07.md @@ -34,3 +34,9 @@ - **S69 (2026-06-17) Office re-skin + golive authz — PASS:** re-skin proof = grep api-call+queryKey sorted -u byte-equal; public-grant = granted root NOT inherit-root (no sibling cascade); accent missing -800 stop = silent no-class Tailwind v4. → _INDEX S69*. - **S65 (2026-06-16) public HRM Hồ sơ + PE mục E — PASS:** upgrade-path MUST MUTATE row (`if(!row.CanRead){...}`) NOT skip-existing when prior revoke pre-set false (S58-class); menu-hide ≠ API-lock. → _INDEX S65*. - **S88 (2026-06-25) Fidelity-gate L2 gist distill (test-specialist) — FAIL (1 fabrication-by-merge):** gist gán "Mig 45" cho cluster span 2 episode khi chỉ 1 mang số đó (Master = Mig 47). Anti-pattern: merge-distill fabricates false specificity; token-PRESENCE pass BLIND to cross-episode mis-attribution → QUALITY gate after presence gate. Detail → `archive/2026-06.md`. + + +- **2026-07-12 (S112 Supplier Excel-import Phase-B close-review, FE/E2E/DEPLOY lane) `[fable-real-single]`:** **GO-WITH-ADJUSTMENTS** — detail → `project_s112_supplier_import_review.md`. Mandatory known adjust = bake ExpectedHeaderTokens (svc:35-67); MAJOR independent catch = FE Import button reuses Suppliers.Create guard but endpoint needs Admin/CatalogManager role → non-functional button for fe-user drafters (BE secure). Deploy dup-risk CONFIRMED safe (4 real NCC ungated-seed Codes=col4 → re-import Update/fill-nulls). Lesson: reusing sibling menu-guard for Admin-scoped NEW action under-restricts — derive guard from ENDPOINT authz. Positive: 24 clamp-consts=EF-len byte-exact, int-enum contract exact (no StringEnumConverter), multipart matches 4 working uploads. +- **2026-07-12 (S112 PE sign-off approach-review PRE-fan-out+deploy, security/governance lane) `[fable-real-single]`:** **GO-WITH-ADJUSTMENTS.** (1) EDGE-5 decision-3 hard-lock: spec placement (reject-branch SVC:92 + EnsureCanReject:321 + handler) **INCOMPLETE — bỏ sót admin-override path SVC:290** (`if(isAdmin) evaluation.Phase=targetPhase`) → admin un-terminal DaDuyet bằng decision=Approve (KHÔNG qua reject). Fix = 1 guard TOP-of-method sau `var fromPhase` SVC:53. Lesson: **terminal-lock gác FROM-state ở ĐỈNH method, KHÔNG rải per-branch** (per-branch bỏ sót admin/fall-through). (2) CEO-skip decision-2: A1 (creator tự ký ô mình thay auto-bypass) **TẠO đường CEO-skip MỚI** multi-step-có-CEO — trước A1 bypass advance-qua slot creator (bỏ check finalize); sau A1 creator qua ApproveV2Async → slot có `AllowApproverFinalize`=true + tick → DaDuyet bỏ CEO (SVC:867). invest-C "đã tồn hôm nay" IMPRECISE (chỉ đúng 1-step-no-CEO). Escalate owner. Lesson: **đổi auto-advance→manual-approve = mở lại per-slot flag mà auto-path che khuất** — re-scan flag khi đổi ai-đi-qua-code-nào. +- **2026-07-12 (S112 Supplier Excel-import close-review PRE-deploy, BE+ADJUST) `[fable-real-single]`:** **GO-WITH-ADJ.** 6 axes PASS: OrdinalIgnoreCase preview+confirm (BuildCiIndex:446/batchByCode:157/seen:158), FillNulls:381-409 skip Code/Name/Type, all-or-nothing gate:137-152 pre-mutate + SaveChanges:188, parser abs Cell:284 (KHÔNG CellsUsed) +#REF!→null:289 +NAS-raw:290 +fingerprint-Ordinal-reject:223, Mig 63 reversible+3-file+snapshot:3330, col→field 30/30 OK (Code=col4 viết-tắt, Name=col5 tên-cty). REQUIRED adjust = bake 30 real token → ExpectedHeaderTokens:37-66 (nguồn test RealFileHeaderTokens:420-451 == brief exact; Case 9 auto-flip). **CAUGHT (MAJOR de-risk): Unicode NFC/NFD** — test dựng workbook từ literal NFC nên KHÔNG chứng file Excel THẬT accept; đề `.Normalize(FormC)` NormalizeHeader:306. Lesson: self-flip token-test = internal-consistency KHÔNG external-file-acceptance (artifact ngoài repo = bất-khả-verify). +- **2026-07-12 (S112 FINAL pre-commit+deploy review — BÓ 2-feature PE-signoff + Supplier-import) `[fable-real-single]` — VERDICT GO (0 must-fix):** Cả 2 close-review adjust ĐÃ áp đúng. **F1 PE**: EDGE-5 guard đúng ĐỈNH-method SVC:66 (sau fromPhase:53) exempt admin+system, precede CẢ 5 branch incl admin-override:306 (:306 vốn `if(isAdmin)` nên non-admin không tới) + EnsureCanRejectV2Async:332 untouched; A1 minOwn=Min(Order):607 bypass1)`:637 (StepIdx giữ 0 từ submit:265), 4 nhánh cũ (auto-sign/next-Bước/step2/terminal) DELETED; History CHANGE4:751-777 log opinion-cũ→Changelog TRƯỚC 4 dòng overwrite, chỉ non-empty, keyed per-level-row (matchingLevel.Id) → chỉ fire khi CÙNG-NV re-sign, enum Workflow=5/Update=2 tồn. Tests 8-new/4-updated adversarial-grade (EDGE-5 system-exempt chứng qua msg "không hỗ trợ"≠"kết thúc"; history 2-boundary). **F2 import**: 30 ExpectedHeaderTokens==test byte-identical all-NFC (script verify 0-mismatch); NormalizeHeader FormC:313 áp cả 2 phía; endpoint [Authorize(Roles=Admin,CatalogManager)] = pattern Update/Delete; Mig 63 = 2 nullable additive (prod-safe, expansion-cols từ Mig 62 `778fc98` deployed per 4-NCC-in-prod). **Nice-to-have (KHÔNG block)**: (a) test comment :31-37 + svc :31-34 stale "BEST-GUESS chưa khớp" (đã bake) — misleading, harmless; (b) test NormalizeJoin:407-414 thiếu `.Normalize(FormC)` mà svc:313 có — moot vì token toàn NFC nhưng latent nếu sau thêm token NFD. **UAT-visible behavior change (by-design, không phải bug)**: A1 bỏ auto-terminal-on-submit → TP tạo phiếu-1-bước-tự-là-cấp-cuối GIỜ phải bấm Duyệt thêm 1 lần (trước tự DaDuyet). **Lesson: Case-9 self-adjusting (shouldMatch tính runtime) PASS ≠ chứng real==expected → phải diff byte-level 2 mảng token TAY; build/test-green (458) nhận theo claim, review logic bằng đọc.** diff --git a/.claude/agent-memory/reviewer/archive/_INDEX.md b/.claude/agent-memory/reviewer/archive/_INDEX.md index 821b27c..7d5cb3b 100644 --- a/.claude/agent-memory/reviewer/archive/_INDEX.md +++ b/.claude/agent-memory/reviewer/archive/_INDEX.md @@ -68,3 +68,9 @@ 2026-07-03 · toggle-mechanism lane · S101 H19 fable-clone Lane-A PWC; marker write-only dead-artifact CONCERN (closed @S102, reader 0.5b wired) · substring:"S101 H19 fable-clone WF2 Lane-A" → `2026-07.md` 2026-07-03 · synthesis + Lane-B · S100 H18 WF2 PWC 0-blocking; stale run-id NOT stamped HELD · substring:"S100 H18 WF2 synthesis + Lane-B" → `2026-07.md` 2026-07-10 · first-real-run gate (fable-real-single) · S108 H19 3-artifact FAIL 1-CRITICAL (outward-claim "đã sửa" chưa cat) + 4 MAJOR; em-main 37-fix cùng phiên · substring:"S108 first-real-run H19" → `2026-07.md` + + +2026-07-12 · FE/E2E/DEPLOY lane · S112 Supplier import Phase-B GO-WITH-ADJ; FE Import btn reuses Create-guard but endpoint Admin/CatalogManager → non-functional for drafters; deploy dup-safe · substring:"S112 Supplier Excel-import Phase-B close-review, FE/E2E/DEPLOY" → `2026-07.md` +2026-07-12 · security/governance lane · S112 PE sign-off approach GO-WITH-ADJ; EDGE-5 terminal-lock bỏ sót admin-override SVC:290 → guard ĐỈNH-method; A1 opens new CEO-skip path · substring:"S112 PE sign-off approach-review PRE-fan-out" → `2026-07.md` +2026-07-12 · BE+ADJUST lane · S112 Supplier import PRE-deploy GO-WITH-ADJ 6-axis; bake 30 ExpectedHeaderTokens; CAUGHT Unicode NFC/NFD self-flip-test ≠ external-file-accept · substring:"S112 Supplier Excel-import close-review PRE-deploy, BE+ADJUST" → `2026-07.md` +2026-07-12 · FINAL pre-commit+deploy · S112 BÓ 2-feature PE-signoff+import VERDICT GO 0-must-fix; both adjust applied; Case-9 self-adjust PASS ≠ real==expected must byte-diff · substring:"S112 FINAL pre-commit+deploy review — BÓ 2-feature" → `2026-07.md` diff --git a/.claude/agent-memory/reviewer/project_pe_multi_ncc_perhangmuc_specreview.md b/.claude/agent-memory/reviewer/project_pe_multi_ncc_perhangmuc_specreview.md new file mode 100644 index 0000000..e56bff3 --- /dev/null +++ b/.claude/agent-memory/reviewer/project_pe_multi_ncc_perhangmuc_specreview.md @@ -0,0 +1,20 @@ +--- +name: project-pe-multi-ncc-perhangmuc-specreview +description: S113+ spec review — PE winner-truth cardinality-change lần 3 (whole-supplier IsWinner → per-Detail Quote.IsSelected); correctness/edge lane +metadata: + type: project +--- + +# PE multi-NCC per hạng mục — spec review (correctness/edge lane), verdict PASS_WITH_ADJ + +Cardinality-change lần 3 trên winner-truth (S87 single→multi sót 2 read; S88 bắt). Trục C `Quote.IsSelected` (Mig 12 DORMANT) activate; D2 = IsSelected nguồn-sự-thật, `Supplier.IsWinner` DERIVED+STORED giữ cho create-HĐ + display. + +**Exhaustive `IsWinner` grep (non-migration) = spec 6-site list ĐỦ** — 0 financial-sum site sót (S87 check PASS). Sites: winnerQuoteTotal `PurchaseEvaluationFeatures.cs:1190` · submit-guard `PurchaseEvaluationWorkflowService.cs:197` · CCM-threshold `:885` · Block-B `:908/914/922` · CreateContract winners `:56`+giaTri `:89-93`. Display winner-names (List:637/Inbox:758/ListApproved:189/DTO:1226) reads IsWinner — **giữ được trên IsWinner (derived), KHÔNG cần re-key**. `SelectedSupplierId` = display-only (grep: mọi use = JOIN-name; 0 money site). + +**MUST (top catch): `CreateContractFromEvaluationFeatures.cs:76-77,89-90` isSingle→detailsSum.** `giaTri = isSingle ? detailsSum : SUM(quotes)`; `detailsSum = Details.Sum(ThanhTienNganSach)` = budget MỌI hạng mục. Per-hạng-mục: 1-winner ≠ thắng-cả-gói (D5 cho 0-winner hạng mục) → over-count budget hạng-mục-không-thắng + phá invariant `Σ giaTri == winnerQuoteTotal`. Spec cite re-key `84-93` **LOẠI** quyết-định isSingle `76-77` = đúng bẫy partial-re-key S87. D4 nói giaTri=SUM(IsSelected) mọi count → phải bỏ ternary. Historical single-winner giaTri đổi budget→quote-sum (escalate anh Kiệt). + +**SHOULD: derive-integrity ≥3 write-path IsSelected.** IsWinner STORED derived → mọi nơi ghi IsSelected phải re-derive: (a) winner-cmd [spec có], (b) `UpsertQuoteCommand:308/332` ghi IsSelected KHÔNG derive, (c) seed `DbInitializer:1257` set IsSelected nhưng loop supplier `1203-1214` KHÔNG set IsWinner (seed phiếu IsWinner=false hết — money-site đọc IsSelected hiện số, winner-names đọc IsWinner rỗng → desync). Centralize hoặc derive-tại-mọi-path; "verify tổng seed" phải siết = seed set IsWinner khớp. + +**Confirmations:** backfill chain sound (Mig 58:31-35 SelectedSupplierId→IsWinner; new IsWinner→IsSelected all-quotes; whole-package + liên-danh preserve total exact, no drift). CCM-threshold boundary `>=throws` (`:893` strict-<) unchanged, chỉ nguồn total đổi — phải khớp site 1. + +**Lesson:** derived-column STORED = burden maintain ở MỌI write-path (không chỉ command trong spec); grep write-path của field-nguồn (IsSelected) KHÔNG chỉ field-đọc (IsWinner). Cite-range của re-key phải BỌC quyết-định branch (isSingle) chứ không chỉ dòng-gán. diff --git a/.claude/agent-memory/reviewer/project_pe_multiwinner_perhangmuc_review.md b/.claude/agent-memory/reviewer/project_pe_multiwinner_perhangmuc_review.md new file mode 100644 index 0000000..6d2f956 --- /dev/null +++ b/.claude/agent-memory/reviewer/project_pe_multiwinner_perhangmuc_review.md @@ -0,0 +1,18 @@ +--- +name: pe-multiwinner-perhangmuc-review +description: S(ensemble) review PE cardinality-change #3 single→multi (Supplier.IsWinner whole-pkg → Quote.IsSelected per-hạng-mục); consumer-completeness lens +metadata: + type: project +--- + +# PE multi-NCC per hạng mục (cardinality single→multi, trục C Quote.IsSelected) — consumer-completeness review + +**Verdict: PASS_WITH_ADJ.** 6 BE read-site inventory COMPLETE — independent grep của `IsWinner`/`winnerQuoteTotal`/`SelectedSupplierId` khắp `src/Backend` map 1:1, KHÔNG sót financial read-site (khác S87 sót 2). Nhưng 2 MUST-precision. + +**MUST caught:** +1. **giaTri silent-financial** — CreateContractFromEvaluationFeatures.cs:89-93 `isSingle ? detailsSum : SUM(all w quotes)`. Dưới per-hạng-mục, single derived-winner ≠ whole-package → `detailsSum` over-count hạng-mục-không-thắng → contract GiaTri SAI ÂM THẦM. CẢ 2 nhánh phải = `SUM(w quotes WHERE IsSelected)`; drop isSingle special-case. Spec "SUM IsSelected của NCC" đúng nhưng implementer dễ chỉ sửa nhánh else. +2. **Derive-completeness** — D2 giữ Supplier.IsWinner DERIVED cho display+tạo-HĐ, nhưng derive chỉ wired trong SelectWinner. DbInitializer seed:1310-1316 set SelectedSupplierId + Quote.IsSelected per-cell nhưng KHÔNG BAO GIỜ set IsWinner (grep IsWinner = 0 hit DbInitializer). Seeded DaDuyet phiếu → total đúng (SUM IsSelected) NHƯNG winner-names rỗng + CreateContract:56-58 throw "chưa chọn NCC". Backfill 1-chiều (IsWinner→IsSelected) thiếu chiều ngược (IsSelected→IsWinner derive). Prod protected Mig 58; Dev/test/demo vỡ. + +**Lesson: derived-field (IsWinner) làm source-of-truth phái sinh = mọi write-path của field-gốc (IsSelected) PHẢI re-derive.** Spec chỉ derive 1/3 path: SelectWinner ✓, UpsertQuote(:308/332) ✗, seed ✗. Failure mode = VISIBLE (empty names / block create), KHÔNG silent-money → SHOULD/MUST tùy path; nhưng giaTri branch mới là silent-financial thật. + +**Confirmed clean (anti-rubber-stamp):** SlaExpiryJob Contract-only (db.Contracts :76-79, không PE); no AutoMapper/notification/export/validator đọc winner; FE 0 client-side winner-sum recompute (chỉ consume BE `ev.winnerQuoteTotal` + boolean `.some(isWinner)`) → BE re-key + derive là đủ; FE type files admin/user offset ~4 dòng (admin :120/144/167/177 vs user :124/148/171/181) xác nhận spec cảnh báo "types drift patch-delta KHÔNG blind-copy" ĐÚNG; 7/7 named test file tồn tại. diff --git a/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/run.md b/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/run.md index 0300008..9f8e703 100644 --- a/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/run.md +++ b/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/run.md @@ -9,7 +9,9 @@ Cho phép chọn **≥2 NCC-TP trúng thầu vào 1 hạng mục** (PurchaseEval ## taskList snapshot (6-bước anh Kiệt) - [x] **B1** fable-clone invest ensemble (4-lane inv-cb) → spec — `wf_dd49ddc0-f29` DONE (4/4 clean) -- [!] **B2** CHỜ anh Kiệt chốt D1-D5 → fable-clone review (reviewer ensemble) → spec cuối +- [x] **B2a** anh Kiệt chốt D1-D4 all recommended (D5 default) — spec LOCKED +- [x] **B2b** fable-clone review ensemble → spec cuối — `wf_1e127f4b-de5` DONE (2/3 clean, lane correctness #53-garble→em-recover; 6 adjustment A1-A6 fold) +- [!] **B3** hmw Opus 4.8 MAX implement (BE re-key 5 financial + derive-invariant + FE per-cell grid 2-app + test-before) - [ ] **B3** hmw Opus 4.8 MAX implement (BE + FE 2-app + test) - [ ] **B4** fable-clone reviewer verify implementation - [ ] **B5** deploy + commit (cicd-monitor verify) diff --git a/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/spec-multi-ncc-per-hangmuc-12-07-2026.md b/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/spec-multi-ncc-per-hangmuc-12-07-2026.md index bc5e6a8..506b32c 100644 --- a/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/spec-multi-ncc-per-hangmuc-12-07-2026.md +++ b/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/spec-multi-ncc-per-hangmuc-12-07-2026.md @@ -58,6 +58,31 @@ Hiện tại mỗi **hạng mục** (`PurchaseEvaluationDetail`) trong phiếu D | C5 | reviewer | Verify 6 read-site re-key đủ (không sót) + total đúng + kế-thừa-HĐ N-winner | PASS: grep-suite 0 sót IsWinner-aggregation; giaTri HĐ = SUM won-lines | | C6 | cicd-monitor | Deploy verify bundle rotate + Mig prod + smoke PE | Run PASS; byte-verify feature string | +## 🔴 REVIEW ADJUSTMENTS (Bước 2 — fable-clone review ensemble `wf_1e127f4b-de5` → FINAL spec) +> 2/3 lane PASS_WITH_ADJ (consumer-completeness + checklist-risk); lane correctness-edgecase = **#53 return-garble (null)** → em-main-solo recover (territory phủ bởi 2 lane hội-tụ). Cả 2 lane VERIFY 6 read-site inventory ĐÚNG, 0 sót. 6 adjustment fold vào implement: + +### A1 (MUST) — giaTri: BỎ shortcut isSingle→detailsSum +`CreateContractFromEvaluationFeatures.cs:89-93` hiện `isSingle ? detailsSum : SUM(all w.Quotes)`. CẢ 2 nhánh SAI dưới per-hạng-mục. **Fix: giaTri per-winner w = `SUM(w.Quotes WHERE IsSelected)` cho CẢ single lẫn multi — bỏ nhánh detailsSum.** +> ⚠️ **HEADS-UP anh Kiệt:** phiếu single-winner giaTri đổi **budget (detailsSum) → bid (SUM quote)** để đúng D4. Số HĐ có thể khác số UAT cũ (comment :73-75) — đây là hệ-quả D4, đúng hơn (giá HĐ = giá chào thực, không phải ngân sách). + +### A2 (MUST) — Derive IsWinner ở MỌI writer + backfill 2 chiều +Invariant BẤT BIẾN: **`supplier.IsWinner == supplier.Quotes.Any(IsSelected)`** — bảo trì bởi TẤT CẢ writer của IsSelected: (a) SelectWinner-per-detail · (b) UpsertQuote `DetailFeatures.cs:308/332` · (c) **seed DbInitializer `:1310-1316`** (hiện KHÔNG set IsWinner → seed DaDuyet vỡ). Backfill **2 chiều**: IsWinner→IsSelected (phiếu cũ) VÀ IsSelected→IsWinner (seed/dormant). Acceptance: fresh Dev DB seeded → winner-names hiện + create-contract OK. + +### A3 (MUST) — Re-key SPLIT theo loại (KHÔNG blanket 6-site) +- **(i) Financial-SUM → `WHERE IsSelected`:** winnerQuoteTotal `:1189` · submit-guard `:196` · CCM-threshold `:885` · giaTri `:89` · budget-B `:907`. +- **(ii) Winner-existence/names → GIỮ IsWinner (derived):** projection List:637/Inbox:758/ListApproved:189 · CreateContract winners-list `:56`. Re-key nhóm này = vỡ display. + +### A4 (SHOULD) — Endpoint + FE UX = breaking (chốt DTO) +`POST /purchase-evaluations/{id}/select-winner` payload: whole-supplier `{supplierIds}` → **per-detail `{detailId, supplierIds[]}`** ("hạng mục detailId, các NCC này thắng" → set Quote.IsSelected per cell + derive IsWinner). FE **redesign UX chọn-theo-ô** trong DetailCard `:2638-2792` (checkbox per cell Detail×NCC), hạ-cấp picker phiếu-wide `:941-1020` thành summary. Mirror CẢ 2 app + patch-delta types (KHÔNG blind-copy). + +### A5 (SHOULD) — Test-before + threshold-crossing + acceptance đo được +- Test-before (rules §7): test ĐỎ (over-count repro + threshold-crossing) TRƯỚC/CÙNG impl. +- Threshold-crossing regression: phiếu total-IsWinner ≥ ngưỡng CEO nhưng total-IsSelected < ngưỡng → assert đúng nhánh CEO/miễn-CEO. +- Acceptance: C5 = grep `\.IsWinner`/`\.Sum(` phân-loại financial-vs-display, 0 SUM còn key IsWinner. C6 = curl phiếu joint-winner, `winnerQuoteTotal == SUM(cell IsSelected)`. +- FE consumer thêm: PeListPanel.tsx:170-178 + display helper PeDetailTabs :76-79/:344-346/:1578-1579. + +### A6 (NIT) — Migration 3-file dù data-only backfill; Down = empty no-op (ghi comment). Stale comment `:920` (:1087→:1189) sửa khi động budget-B. + ## Rủi ro then chốt - **Silent total-lệch:** 6 read-site tài chính — sót 1 = số sai runtime, build/test cũ vẫn PASS (S87/S88 chính vụ). - **R3 CCM-threshold = gate tài chính:** over/under-count → chặn/cho-qua sai quyền CCM duyệt-miễn-CEO.