diff --git a/.claude/WAL.md b/.claude/WAL.md index 33bb23f..492b02c 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -11,8 +11,9 @@ chain A (Supplier import): chain B (PE sign-off change): [x] investigate wf_fd8f2bfe-9f6 + anh Kiệt chốt 6 decision: (1)A1-bypass-subordinate-only+creator-ký-tay (2)no-hard-guard-single-person (3)EDGE-5-hard-lock-DaDuyet (4)1-nút (5)opinion-history (6)phiếu-mới-only [x] close-review wf_7df56b89-7bb GO-WITH-ADJUSTMENTS (2/3 lane, L3 garble): A1 Min-rule OK · EDGE-5 guard đỉnh-method SVC:53 (L2 refine, chặn admin-override :290) · history=Changelog-MVP · 🔴 A1 MỞ CEO-skip MỚI (finalize-flag) L2-catch -[!] CHẶN fan-out — anh Kiệt chốt 3 oQ: (oQ1)CEO-skip creator-self-finalize [rec: guard hẹp] (oQ2)"cứng" có khóa admin-edit-budget/giá phiếu DaDuyet? (oQ3)confirm mất admin-escape-hatch un-terminal -[ ] sau chốt → fan-out Opus implement (A1+EDGE-5-đỉnh-method+CEO-skip-guard+history-Changelog+test) → reviewer → commit+push → cicd DEPLOY (gates pass) +[x] anh Kiệt chốt 3 oQ: (oQ1)finalize-theo-quyền → KHÔNG guard, tôn-trọng AllowApproverFinalize · (oQ2)"Admin đầy-đủ-nhất" → EDGE-5 guard chỉ chặn NON-ADMIN (admin+system EXEMPT, giữ override+edit+un-terminal), oQ3 resolved +[!] fan-out Opus implement — implementer-backend (BG) BE: A1(bypass bịa filler) · Windows byte-verify GROUND-TRUTH = `.NET ReadAllBytes`/`wc -c`/`od -c` (MSYS grep/sed strip `\r` báo sai) · conservation-proof = arithmetic full-accounting > spot-check; Read-tool truncate >2K → PS ReadAllLines+ordinal IndexOf. Detail → `archive/2026-07.md` (recovered S102 — cut-not-moved f229b07). -- **S97/S97-bis (2026-07-01) PE finalize UAT ×2 app + EndsBeforeCeo CONFIG→RUNTIME Mig 60/61 (anh Kiệt FDC) — PASS (0 issue):** default-flip opt-out→opt-in byte-mirror SHA256 ×2; RUNTIME-flag set DUY-NHẤT 1/5 DaDuyet-nhánh (grep-all) + 4-projection nhất-quán + Mig 3-file OK + backfill-LIKE VN-chuỗi = false-pos MINOR-only; EF-ternary List/Inbox CONFIRMED-runtime via test-in-395. Detail → `archive/2026-07.md` (recovered S102 — cut-not-moved f229b07). -- **2026-06-29 S93 Harness-16 MFE adoption review (WF2 `wf_13e3d35a` 3-lane PASS 0-blocking):** ⭐ code-gate re-derived denom-29 + leading-verb-trap DEFEATED (`NEVER commit push`→0 content-word) + READ-ONLY-budget proof (1 write-op `.mfe-state.json`); WF1-reviewer caught BLOCKING vocab-fork (memory-fidelity H6.7≠H16 → MFE+alias-map §H). Detail → adap-report harness-16-mfe. → _INDEX. +- **S98 (2026-07-02) 3 verify-lane — PASS (minor):** fidelity-gate 3-lớp (verbatim+pointer-arith+ground-truth-code); Windows byte-verify = .NET ReadAllBytes/`wc -c`/`od -c` (MSYS strip `\r` báo sai). → `archive/2026-07.md`. +- **S97/S97-bis (2026-07-01) PE finalize UAT ×2 + EndsBeforeCeo Mig 60/61 — PASS:** default-flip byte-mirror SHA256 ×2; RUNTIME-flag 1/5 DaDuyet grep-all + 4-projection; backfill-LIKE VN false-pos MINOR. → `archive/2026-07.md`. +- **S93 (2026-06-29) Harness-16 MFE adopt (WF2 3-lane) — PASS 0-blocking:** code-gate re-derived denom-29; READ-ONLY-budget 1 write-op; WF1 caught vocab-fork H6.7≠H16. → adap-report + _INDEX. -- **S92 (2026-06-29) Adversarial PROD-security hide 5 menu-groups admin-only on eoffice (uncommitted) — PASS (0 blocking, 1 awareness note):** A-E all upheld; CatalogManager Danh-mục access stripped (intended-by-spec, role assigned to 0 users post-S89, flag em-main only). Detail + per-attack file:line → [project_s92_admin_only_modules_revoke.md](project_s92_admin_only_modules_revoke.md). -- **S91 (2026-06-25) PE D2 create-contract 1→N multi-winner + winner-names (FROZEN, NOT-deployed) — PASS:** fix for S89-bis dead-end; codegen mid-loop SaveChanges flushes ONLY seq-row (contract built LOCAL); GiaTri per-winner Quote-sum join PES.Id; positional DTO arg-order verified 3 sites; FE mirror byte-identical; 419 PASS. Detail → `archive/2026-06.md`. -- **S90 (2026-06-25) PE stability-fix batch D1 (5 chg+11 test, FROZEN) — PASS:** Block B re-key SelectedSupplierId→IsWinner (single unchanged proven); CEO-notify SaveChanges (was Add-but-never-flush); HoSoLink null-safe+clear-via-empty option(b); #70 ||peFetching 2 PRO cells; 413 PASS. Detail → `archive/2026-06.md`. -- **S89-bis (2026-06-25) AREA-6 FE-consumers (PE FROZEN, investigator verify) — 4/4 confirm + 1 NEW miss:** all 4 hold; NEW catch = create-contract joint-winner DEAD-END (FE shows button on `some(isWinner)` but BE hard-blocks `SelectedSupplierId is null`). Anti-pattern: single→multi conversion stops at detail-display layer. Detail → `archive/2026-06.md`. -- **S89 (2026-06-25) AREA-4 workflow-edit (PE FROZEN, investigator verify) — 3 confirm/1 do-not-touch:** HoSoLink #73-class clear-on-partial-edit CONFIRMED + NEW 2nd destructive call-site (PeDetailTabs:817 InfoTab.save omits hoSoLink). Anti-pattern: echo-all-siblings convention rots when NEW absolute-set field added. Detail → `archive/2026-06.md`. -- **S86 (2026-06-24) PE Section B 3-cột Dự-án|PRO|CCM (Mig 59) — PASS:** authz byte-mirror UpdatePeSuggestedPriceCcm (NotFound→Forbidden, fail-closed); FE `canEditCcm` KHỚP BIT-EXACT BE gate; submit-guard untouched (grep CcmBudgetPeriod in Services=ZERO); div-by-0 safe; 402 PASS. Detail → `archive/2026-06.md`. -- **S82 (2026-06-21) Harness-15-v2 adopt review (0 code) — 3/3 lane PASS:** caught 3 MINOR. Memory-note: `broadcasts/_index.md` sha = notify self-declared `content_sha256` frontmatter NOT recompute → don't flag index-vs-file mismatch before reading frontmatter. Stamped-mark mid-session edit OK if only refresh confirmed-decision What-cell. -- **S76 (2026-06-19) PE budget 3-cột Mig 56 + badge — PASS:** MAJOR race fixed gotcha #70 (useIsFetching gate). Badge role-set MUST mirror gate bit-for-bit. SURPRISE: spec "KHÔNG migration" FALSE — đọc changed-set thật, đừng tin scope-framing em-main. → _INDEX S76. -- **S72* (2026-06-18) Mig 54 PE giá-đề-xuất + CCM-finalize OPT-IN — financial go-live PASS:** fail-closed guard throw BEFORE set Phase=DaDuyet; finalize-bypass = 3 orthogonal gate + server-recompute amount no-trust-client. → _INDEX S72*. -- **S71 (2026-06-18) Harness-10 run-trace — PASS/GAPS:** "TRACKED" 2-level = check-ignore(eligible) vs git-ls-files(committed), model only post `git add`. → _INDEX S71*. -- **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`. +- **S92 (2026-06-29) PROD-security hide 5 menu-groups admin-only (uncommitted) — PASS (1 note):** A-E upheld; CatalogManager Danh-mục stripped (by-spec, 0 users). → [project_s92_admin_only_modules_revoke.md](project_s92_admin_only_modules_revoke.md). +- **S91 (2026-06-25) PE create-contract 1→N multi-winner (FROZEN) — PASS:** codegen mid-loop SaveChanges flush ONLY seq-row; GiaTri per-winner Quote-sum; positional DTO 3 sites; FE byte-mirror; 419 PASS. → `archive/2026-06.md`. +- **S90 (2026-06-25) PE stability D1 (FROZEN) — PASS:** Block B re-key IsWinner; CEO-notify SaveChanges (was never-flush); HoSoLink null-safe; #70 ||peFetching; 413 PASS. → `archive/2026-06.md`. +- **S89-bis (2026-06-25) AREA-6 FE-consumers (FROZEN) — 4/4 + 1 NEW:** create-contract joint-winner DEAD-END (FE `some(isWinner)` vs BE `SelectedSupplierId is null`). → `archive/2026-06.md`. +- **S89 (2026-06-25) AREA-4 workflow-edit (FROZEN) — 3 confirm/1 do-not-touch:** HoSoLink #73 clear-on-partial + NEW 2nd call-site PeDetailTabs:817. → `archive/2026-06.md`. +- **S86 (2026-06-24) PE Section B 3-cột (Mig 59) — PASS:** authz byte-mirror UpdatePeSuggestedPriceCcm (NotFound→Forbidden fail-closed); FE canEditCcm BIT-EXACT; 402 PASS. → `archive/2026-06.md`. +- **S82 (2026-06-21) Harness-15-v2 adopt — 3/3 PASS (3 MINOR):** `broadcasts/_index.md` sha = self-declared frontmatter NOT recompute → đọc frontmatter trước khi flag index-vs-file mismatch. → _INDEX. +- **S76 (2026-06-19) PE budget 3-cột Mig 56 — PASS:** #70 race fixed (useIsFetching gate); Badge mirror gate bit-for-bit; SURPRISE spec "KHÔNG migration" FALSE → đọc changed-set thật. → _INDEX S76. +- **S72* (2026-06-18) Mig 54 PE giá-đề-xuất + CCM-finalize OPT-IN — PASS:** fail-closed guard throw BEFORE Phase=DaDuyet; bypass = 3 gate + server-recompute no-trust-client. → _INDEX S72*. +- **S71 (2026-06-18) Harness-10 run-trace — PASS/GAPS:** "TRACKED" = check-ignore(eligible) vs git-ls-files(committed), model post `git add`. → _INDEX S71*. +- **S69 (2026-06-17) Office re-skin + golive authz — PASS:** re-skin = grep api-call+queryKey sorted -u byte-equal; public-grant root NOT inherit-root; accent -800 silent Tailwind v4. → _INDEX S69*. +- **S65 (2026-06-16) public HRM + PE mục E — PASS:** upgrade MUST MUTATE row NOT skip-existing when prior revoke false (S58-class); menu-hide ≠ API-lock. → _INDEX S65*. +- **S88 (2026-06-25) Fidelity-gate L2 gist distill — FAIL (fabrication-by-merge):** gist gán "Mig 45" cho cluster 2-episode (Master=Mig 47) — merge-distill fabricates false-specificity; QUALITY gate after presence gate. → `archive/2026-06.md`. -- **2026-07-10 (S108 first-real-run H19) `[fable-real-single]`** — **FAIL** (1 CRITICAL + 4 MAJOR + 6 minor; em-main 37-fix cùng phiên): email claim “đã sửa comment” khi hmw.js chưa sửa. Lesson: doc assemble-từ-N-worker → grep stale-claim theo CLASS toàn file sau fix đại diện; mọi outward-claim “đã sửa X” phải cat X trước gate. → `archive/2026-07.md`. -- **2026-07-10 (S109 fidelity-gate 2 gist L2) `[fable-real-single]`:** **PASS_WITH_FIXES** (3 fix): (1) gist meta-count "15" ≠ disk 14 (count COPY brief không đếm disk); (2) delta tự-tính "+8.8KB" cạnh 32.2→41.1 (=8.9) = **self-computed derivative = false-specificity MỚI** ngoài S88; (3) N/A-just "(resolved S96)" over-claim ERD-debt §16+ pending → **fact-claim trong N/A-note verify như content**. Lesson: gist-metadata + số-tự-tính + drop-just = 3 bề mặt token-coverage mù. → `archive/2026-07.md`. -- **2026-07-12 (S111 H22 WAL adopt cổng-cuối) `[fable-real-single]`:** **PASS_WITH_FIXES** (0C/1M). Fault-inject cây tạm 2 ca PASS; CATCH engine CAVEAT "STAGED" còn khi N.3 cùng-file đã nâng VERIFIED-RUNTIME = **nâng-nấc-quên-sync-câu-cũ cùng-diff**. Lessons: live-witness>forensics>lời-khai (commit sinh GIỮA review = tự chứng hook fire); NTFS CreationTime×commit-time phân-định RUN-tay vs hook-FIRE; PATH 3-context bất-định → PS1 path-independent. → `archive/2026-07.md`. -- **2026-07-11 (S110 H21+MTv3 adopt cổng-cuối) `[fable-real-single]`:** **PASS_WITH_FIXES** (0C/5M). CATCH: (1) worker đổi-fact khi reword — doc "VALID_ROLES 12 vai" nhưng code=10 → **claim-về-code phải grep lại kể cả chỉ reword**; (2) roster-12×VALID_ROLES-10 → 2 vai monitor degrade DEFAULT silent (floor mới EXPOSE edge code cũ); (3) point-edit sót cụm GIỮA-section + asymmetric-mirror-edit §3.2/§3.3; (4) supersede-note trỏ adap-report CHƯA tạo = dangling. Lesson: sweep hậu-supersede theo CONCEPT-cluster; #53 garble return → in-session re-emit recover. → `archive/2026-07.md`. +- **S108 (2026-07-10 first-real-run H19) — FAIL (1C+4M+6m):** email claim "đã sửa comment" khi hmw.js chưa sửa. Lesson: outward-claim "đã sửa X" phải cat X trước gate; grep stale-claim theo CLASS sau fix đại diện. → `archive/2026-07.md`. +- **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. +- **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). + --- ## 🔄 Curate trigger diff --git a/.claude/agent-memory/reviewer/project_s112_supplier_import_review.md b/.claude/agent-memory/reviewer/project_s112_supplier_import_review.md new file mode 100644 index 0000000..1b7f7d5 --- /dev/null +++ b/.claude/agent-memory/reviewer/project_s112_supplier_import_review.md @@ -0,0 +1,20 @@ +--- +name: project-s112-supplier-import-review +description: S112 Supplier Excel-import (Phase B, Approach A) FE+E2E+DEPLOY close-review — GO-WITH-ADJUSTMENTS; caught FE guard/authz-scope mismatch + header-vs-realfile residual risk +metadata: + type: project +--- + +# S112 (2026-07-12) Supplier Excel-import close-review — GO-WITH-ADJUSTMENTS (FE/E2E/DEPLOY lane) + +Uncommitted working-tree. Feature: upload Excel "Database NCC" → preview → confirm upsert (fill-nulls-safe, all-or-nothing). Verdict **GO-WITH-ADJ**: 1 mandatory known adjust + 1 MAJOR independent catch + fail-safe residual. + +**Mandatory adjust (planned, gating):** `SupplierExcelImportService.cs:35-67` ExpectedHeaderTokens = BEST-GUESS ≠ 30 real tokens → EVERY real upload rejected until baked. Test Case-9 (`SupplierExcelImportServiceTests.cs:310`) is robust 2-way self-adjusting (`preview.LayoutValid.Should().Be(shouldMatch)`) — bake flips it to assert true, all 10 tests stay green. RealFileHeaderTokens() at :419 already holds the 30 targets. + +**MAJOR independent catch (I found, not in brief):** FE Import button guarded `PermissionGuard menuKey=Suppliers action=Create` (SuppliersPage.tsx:214) but import endpoints require `[Authorize(Roles="Admin,CatalogManager")]` (SuppliersController.cs:64,77). Per S59 Create-endpoint is open to every auth user → users with Suppliers.Create-menu but no Admin/CatalogManager role (notably regular fe-user) see a non-functional button → 403 on upload. BE SECURE (no hole), UX defect. Guard broader than endpoint authz. **Lesson: when reusing an existing menu-permission guard for an Admin-scoped NEW action, the guard scope silently under-restricts — re-derive guard from the ENDPOINT authz, not the sibling button.** + +**Residual (MINOR, fail-safe):** header fingerprint + HeaderRow=4/DataStartRow=5 + absolute-col map validated only vs brief tokens, NOT the actual file on anh Kiệt's machine. Mismatch → LayoutValid=false + "Header nhận được" diagnostic in FE red box → 1-iteration re-bake, no data corruption. + +**Deploy dup-risk (point 8) CONFIRMED safe:** 4 real NCC (TRUONGGIANG/TANPHU/TGN/DONGDUONG) seeded ungated `DbInitializer.cs:2775-2781` (SeedRealMasterDataAsync), Codes = Excel col-4. Re-import → CI-match → Update → fill-nulls-safe → no dup, no overwrite. Nuance: it's fill-nulls (enriches DB-null extended cols), NOT strict no-op; fill-nulls-safe means re-import canNOT correct an already-populated field. Mig 63 = 2 nullable AddColumn (datetime2+nvarchar200), metadata-only, safe on live prod; 3-file rule OK. + +**Resisted scrutiny (positive):** (1) FE↔BE int-enum EXACT (Type 1-6, Status 1-3, RowImportStatus 0-3); no JsonStringEnumConverter registered (only UtcDateTimeJsonConverter) → ints confirmed. (2) 24 service Max* clamp constants match EF column lengths BYTE-EXACT → no 500-on-save gap. (3) multipart `Content-Type:multipart/form-data` manual header matches 4+ existing working uploads (ContractAttachments/PeDetailTabs/FormsPage) — not a boundary bug. (4) confirm re-derives New/Update from DB (doesn't trust client status) — secure. (5) CI dedup in-memory not DB-reliant → prevents prod SQL Server unique-violation 500 (test proves via SQLite binary collation). (6) SourceUpdatedAt UTC-converter treats naive as UTC → no VN day-shift. (7) dialog + SuppliersPage byte-identical ×2 app (mirror exit 0); SupplierType enum block identical. (8) invalidate `['suppliers']` prefix-matches list key `['suppliers',{...}]`. Round-trip camelCase fields all present.