wal: flush 20260712T1757

This commit is contained in:
pqhuy1987
2026-07-12 17:57:45 +07:00
parent b942e52226
commit b5bc030e6b
4 changed files with 141 additions and 24 deletions

View File

@ -3,13 +3,13 @@ updated: 2026-07-12 | session: S113 | branch: main
goal: Supplier import v2 (anh Kiệt) — R1 đọc file thật + R2 nút "Tải file mẫu" + R3 bước nhập Mã NCC thủ công sau upload + R4 thiếu Mã NCC→draft/ẩn không public + R5 dedup theo MST(c12) + R6 không MST&không Mã NCC→không public + R7 badge Public/ẩn trên list. Flow: fable-clone invest→spec→anh chốt→HMW Opus implement.
chain:
[x] đọc cấu trúc file thật Data base-1_7_2027.xlsx (Sheet1, header ROW 4, 30 cột, MST=c12, TÊN VIẾT TẮT=c4→Code, 4 NCC = TRUONGGIANG/TANPHU/TGN/DONGDUONG)
[!] fable-clone invest ensemble 5 lane investigator-codebase — ĐANG CHẠY nền
[ ] synthesize 5 lane → ghi spec-file 3-mục (runs/2026-07-12-S113-supplier-import-v2-invest/spec-supplier-import-v2-12-07-2026.md)
[ ] present spec + open-questions → anh Kiệt chốt decisions
[ ] HMW Opus 4.8 MAX implement theo spec (BE + FE 2-app + Mig + test)
[ ] test-specialist + reviewer + deploy + cicd verify
[x] đọc cấu trúc file thật Data base-1_7_2027.xlsx (Sheet1, header ROW 4, 30 cột, MST=c12, TÊN VIẾT TẮT=c4→Code, 4 NCC)
[x] fable-clone invest ensemble 5 lane (wf_e7896ca6-901) — 4/5 structured + lane visibility garble#53 → em-main-solo recover
[x] synthesize → ghi spec-file 3-mục (runs/2026-07-12-S113-supplier-import-v2-invest/spec-supplier-import-v2-12-07-2026.md) + 30 token thật extract
[x] anh Kiệt CHỐT: D1=A(Mã NCC=Code) · D2(Code=cổng public, MST=cảnh-báo-mềm, existing-public giữ, import-scope) · D3(nới quyền cập-nhật-danh-mục, không admin-only) → spec §DECISIONS updated
[!] HMW Opus implement — implementer-backend (Mig 64+dedup-MST+PublishCmd+template+authz-loosen+re-bake-token) ⟂ implementer-frontend (2-app: badge+publish+filter+template-btn+editable-Code+MST-warn) — ĐANG spawn parallel
[ ] test-specialist ≥15 test + reviewer + build + commit + deploy + cicd verify
next: chờ wf_e7896ca6-901 xong → em-main synthesize findings → ghi spec-file 3-mục
verify: /workflows xem wf_e7896ca6-901 (5 lane); session chết giữa chừng → /tiep đọc WAL → check workflow completed chưa (journal.jsonl) → resume synthesize spec (KHÔNG re-run nếu đã có kết quả cache)
wf: wf_e7896ca6-901 | invest-ensemble | 5-lane-investigator-codebase | RUNNING | resume=synthesize-findings→spec-file
next: 2 implementer về → em-main verify contract khớp + build slnx + npm×2 → test-specialist → reviewer → commit → deploy
verify: spec-file §DECISIONS + §API-CONTRACT chốt; session chết → /tiep đọc WAL → spec có sẵn + decisions chốt → check 2 implementer done? (agent-memory diary) → tiếp test/review (KHÔNG re-invest, KHÔNG re-ask decisions)
wf: implementer-backend+frontend | HMW-implement-parallel | BE⟂FE-disjoint | SPAWNING | resume=verify-contract→test→review→deploy

View File

@ -77,20 +77,18 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
- **2026-07-07 (S105 BVAAU cross-repo auth RCA — on-behalf em-main B3; #53-garble lần-7 → SendMessage-resume recover):** Task: invest read-only repo BVAAU (Home Google login "đá ra ngoài" + CRM1 guard + Zalo User_ID). Verdict: **Q1 root-cause = FE Home MÙ session** — bundle prod cũ gọi relative `/api` trên site TĨNH (`web.config` loại `^/api/` khỏi SPA-fallback) → `/api/auth/me` 404 → anonymous vĩnh viễn, TRONG KHI hub-side OK hết (state→callback→auto-provision→cookie `bvaau.auth` Domain=`.benhvienaau.com` SameSite=Lax; 7 nghi vấn a-g đều KHÔNG); fix API_BASE-absolute = session-BVAAU commit `51c69fb` 13:45 CÙNG-LÚC điều tra → em-main byte-verify bundle mới Home `CtTKjYz2` + CRM1 `Cr8wJMpn` marker PRESENT = LIVE. **Q2** CRM1 trước đó hở 2 tầng (FE soft-gate + API `enforceCrmGate:false`); FE-redirect-home live 51c69fb; kín API = flip `AUTH_ENFORCE_CRM_GATE=true` chờ anh. **Q3** "User_ID" KHÔNG có trong UI 3 app (duyệt Zalo = StaffCode+Note); `ProviderUserId` backend = identity-map GIỮ (`ApprovePendingLogin.cs:114,141`). Nấc: verified (em-main first-hand git-show + curl byte + config live). Learned: class **"hub tập-trung + site tĩnh: relative `/api` sống dev (Vite proxy), chết im prod"** → auth-hub call luôn absolute-host; garble lần-7 = reasoning-lane, recover mới = **SendMessage-resume khi diary trống** (context sống trong-session). Tag `[s105-bvaau-auth-rca, fe-blind-session, 53-garble-7-resume, cross-repo-readonly]`.
- **2026-07-06 Supplier 30-field invest+review WF `wf_5ac2cdb0` (anh Kiệt FDC — 🅰️ schema go-live):** ⭐ schema-domain lane **#53-GARBLED (null, "no StructuredOutput")** nhưng full design RECOVERED từ chính AS-10 memory-write của nó (reverted sau harvest — `feedback_agent_return_garble_recover`); em-main authored authoritative schema. **6 ground-truth correction vs brief:** (1) **ClosedXML 0.105.0 = project Excel-standard, KHÔNG phải OpenXml raw**`csproj:8` + mọi path (`XlsxRenderer.cs:18`/`ContractExcelExporter.cs:56`/`AttendanceReportExcelExporter.cs:14`) dùng `XLWorkbook`; `cell.GetString()` auto-decode shared-string → "shared-string pitfall" MOOT (cả 2 pkg referenced, OpenXml KHÔNG dùng cho read). (2) **reuse-existing-cols > parallel-new-cols** — map TÊN CÔNG TY→`Name`(cũ), ĐỊA CHỈ XUẤT HĐ→`Address`, SĐT→`Phone`; thêm cột MỚI chỉ cho TYPE thật-mới (OfficeAddress/MailingAddress/ContactPhone); tạo companyName/invoiceAddress = ORPHAN cột seed live. (3) **`UpdateSupplierCommand.cs:49-57` blind absolute-set cả 9 incl Note = latent #73 clobber** → import (🅱️) PHẢI ghi entity direct + fill-where-non-empty, KHÔNG route qua Update cmd. (4) **seed idempotent-by-Code (`DbInitializer.cs:2781`) SKIP existing → cột mới NULL** → population = seed **fill-nulls-if-exists** (KHÔNG seed-insert-alone). (5) **`SupplierDto` hand-projected 2 site** (`GetSupplierQuery.cs:21`+`ListSuppliersQuery.cs:48-50` positional, no AutoMapper) → +6-file thread mỗi cột. (6) FE: **`Dialog.tsx:48` đã `max-h-70vh overflow-auto`** → form-expand cần 0 layout-code (go-live win lớn nhất); HoSoLink `toFileUrl`/`PathWithCopy` LOCAL-không-export trong `PeDetailTabs.tsx:2087/2098` → extract shared `ui/PathLink.tsx`. Design chốt: +18 nullable AddColumn (net 89), `SupplierType +CaHai=6` APPEND, new `SupplierStatus` enum nullable, Mig 62. Reviews 3× PASS_WITH_CONCERNS (2 goLiveSafe, import-robustness=false chờ 🅱️). Tag `[supplier-30field, wf-invest, closedxml-not-openxml, reuse-cols, hand-dto-2site, update-blind-set-73, seed-fillnulls, dialog-autoscroll]`.
- **2026-07-12 (S112+ Supplier-import-v2 LENS-3 FE-dialog-UX invest `wf` fable-clone):** Task R2/R3/R7 FE. Ground-truth: SupplierImportDialog.tsx + SuppliersPage.tsx **SHA-identical 2-app** (`6313529c`/`e9143d93`) — but types/master.ts DRIFTS (fe-user thiếu `Department.parentId` Mig51, Supplier-portion vẫn mirror). Dialog = 2-step preview→confirm, table READ-ONLY (`:284-325`), **round-trips `preview.rows` verbatim** (`:190`+`:132-134`) → per-row edit ĐÃ khả-thi cho R3 (mutate `row.code`→confirm). Confirm gate `errorCount>0` disable (`:191`). **R2:** 0 template endpoint tồn tại; `ExpectedHeaderTokens` 30-token baked `SupplierExcelImportService.cs:35-68` = single-source cho BE-gen .xlsx row-4 (test Case6 chứng round-trip); blob-download precedent `PeDetailTabs.tsx:3273-79` (`api.get responseType:'blob'`+createObjectURL). **R7:** SuppliersPage cols code/name/type/taxCode/phone/actions (`:174-206`) — **KHÔNG có status/public col**; `Supplier` type có `status` (biz-status ≠ public) NHƯNG **0 field isPublic/visibility** (grep⌀) → R4/R7 cần field MỚI. **Mã NCC=Code** (Supplier.cs:7 comment + col4 TÊN-VIẾT-TẮT→Code). ⚠️**authz-mismatch:** import BE `[Roles=Admin,CatalogManager]` (`SuppliersController.cs:64,77`) vs FE btn gate `Suppliers/Create` (mở-all S59) → non-admin thấy nút→403; template sẽ kế-thừa. NO new menuKey/route. Tag `[s112, supplier-import-v2, fe-lens3, sha-mirror-drift-parentId, expectedheadertokens-template, no-ispublic-field, import-authz-mismatch]`.
- **[→ git+session-log 07-07] S105 BVAAU cross-repo auth RCA:** Q1 root-cause = FE Home MÙ session (bundle cũ relative `/api` trên site TĨNH `web.config` loại `^/api/``/api/auth/me` 404 → anonymous); fix API_BASE-absolute `51c69fb` LIVE. Q2 CRM1 hở 2 tầng (FE soft + API `enforceCrmGate:false`) → flip `AUTH_ENFORCE_CRM_GATE=true`. Q3 Zalo "User_ID" KHÔNG có UI. Class **"hub+site-tĩnh relative-`/api` chết im prod"**; #53-garble-7 → SendMessage-resume recover. *(verbatim → archive/2026-07.md)*
- **[→ 07-12 LENS-3 entry above + git 07-06] S104 Supplier 30-field 🅰️ schema-invest WF `wf_5ac2cdb0`:** SUPERSEDED anchors giữ: ClosedXML 0.105.0 = Excel-standard (KHÔNG OpenXml-raw, `csproj:8`); reuse-existing-cols>parallel; `UpdateSupplierCommand.cs:49-57` blind-set=latent#73; seed fill-nulls-if-exists (`DbInitializer.cs:2781`); `SupplierDto` hand-projected 2-site (`GetSupplierQuery.cs:21`+`ListSuppliersQuery.cs:48-50`); `Dialog.tsx:48` max-h-70vh; extract `ui/PathLink.tsx`. Chốt Mig 62 +18 nullable. *(verbatim → archive/2026-07.md)*
- **[→ git+session-log 07-03 + adap-reports] S101 H19-adopt Lane-2:** map reviewer=review-gate / investigator=ground-truth-lane; 12-pt tier-checklist ALL✅ (11/11 inherit, C3 no-`[1m]`-in-frontmatter); toggle-marker MUST copy `.gitignore:87` last-match placement; VACUOUS-MET all-inherit+Fable. *(verbatim → archive/2026-07.md)*
- **[→ git+session-log 07-02 + adap-reports] S100 H18-adopt WF1:** ⭐ ghost-wire-class **config-key-defined-but-unread** (`budget.json:54-55` script re-hardcode same-value→no-flag; catch=grep-key-across-scripts); cron=ghost-wire (CronList=0); `_ledger` orphan-catch 0/15-lifetime. *(verbatim → archive/2026-07.md)*
- **[→ git+session-log 07-02] S98 verify-lane V2:** detector 28→25, 8 derived-stale=100% FP count-token-soft-net vs canonical 61/89/440/79; grep-gotcha "Fable RESTORED" exact misses README variant ":3 Fable 5 1M RESTORED". *(verbatim → archive/2026-07.md)*
- **[→ git+session-log 07-01] S97 feedback-audit:** #77/#78/#79 present `gotchas.md:1301/1317/1325`; #53-garble ×6 = cicd MEMORY-self-target ~17.5KB structural-root-cause; Mig 60 wrong-table Approvals→Mig 61 Changelogs both present. *(verbatim → archive/2026-07.md)*
- **[→ 07-06 entry above + session-log] 2026-07-01 Supplier GAP predecessor:** SUPERSEDED by 07-06 schema-design entry above (same anchors: 9-prop lean, seed-cram→Note, HoSoLink NAS precedent, prod 20-NCC). *(verbatim → archive/2026-07.md)*
- **[→ `scripts/mfe-eval.ps1` §H + session-log 06-29] S93 MFE ground-truth:** denominator=marks/AS/guards/gotcha dedup-by-AS-xref=29; per-role coverage denom=numbered-anti-pattern numer=diary-match exact-id OR ≥2-content-word (beats leading-verb-giả). *(verbatim → archive/2026-07.md)*
- **2026-07-12 (S112-b) Supplier import-v2 LENS-4 public/visibility recon (anh Kiệt FDC, RETURN-only):** ⭐ **3 SELECTION consumers ALL hit `GET /suppliers` (ListSuppliersQuery)** → (1) master `SuppliersPage.tsx:83` ×2-app (admin, MUST see draft opt-in) (2) PE add-supplier dropdown `PeDetailTabs.tsx:2222` pageSize=1000 (3) Contract create `ContractCreatePage.tsx:321` pageSize=200. **DISPLAY-of-existing (join by stored SupplierId → MUST NOT filter)**: ContractFeatures:293/392/434 · ContractWorkflowService:168/373 · ContractExcelExporter:44 · GetDashboardStats:42 (TopSuppliers group-over-contracts → draft never appears) · PeFeatures:560/728/874/952 · CreateContractFromEvaluation:63/168. **WRITE-GUARD backstops** (validate-supplier-exists on attach → add draft-reject): `ContractFeatures.cs:64` + `PurchaseEvaluationSupplierFeatures.cs:49`. **NO IsPublic field today**; `SupplierStatus` enum = business (DangHoatDong/Blacklist/NgungHopTac) NOT visibility. **SupplierConfiguration.cs:47/50** = Code unique-filtered `[IsDeleted]=0` + IsRequired + `HasQueryFilter(!IsDeleted)` only. c4 Tên-viết-tắt→Code (SupplierExcelImportService:249) + import HARD-requires Code (:103-108) → conflicts R3 "Mã NCC nhập sau, Excel không có cột". DTO hand-projected 2-site (List+Get). Rec: fail-closed default (GET public-only → dropdowns unchanged, S87/S88-safe) + stored `IsPublic bit`. Tag `[s112b, supplier-visibility, lens4, fail-closed-default, no-global-queryfilter]`.
- **[all verbatim → archive/2026-07.md] S98 verify-lane (detector 28→25, 8 derived-stale=FP vs canonical 61/89/440/79; grep exact misses README variant) · S97 feedback-audit (#77/#78/#79 `gotchas.md:1301/1317/1325`; #53-garble×6=cicd MEMORY-self-target ~17.5KB; Mig60 wrong-table Approvals→Mig61 Changelogs) · 07-01 Supplier-predecessor SUPERSEDED by 07-06 · S93 MFE denom=marks/AS/guards dedup-AS-xref=29, coverage exact-id OR ≥2-content-word.**
- **PE FROZEN-Mig59 patchwork + go-live recon cluster (S87→S89, 8 entries → `archive/2026-06.md`):** AREA-2 budget-matrix (`SetPeCcmBudgetPeriodCommand` band-aid misplaced in `PeSuggestedPriceFeatures.cs:153-199`; PRO=drafter-flow vs CCM=role-flow authz asymmetry Section-B row3; "Dự án" col intentional FE-only "—") · AREA-3 finalize/price (AllowApproverFinalize AUTO Mig58 + finalizeByCcmDelegation OPT-IN Mig54 coexist, AUTO-first; ApplyApprovedPriceOnFinalize allow-list {Ncc,ProMin,ProMax,Ccm}; isSystem-branch DEAD via public ApproveV2Async) · AREA-4 UpdatePeDraft (**LATENT #73-class BUG:** HoSoLink absolute-set `PurchaseEvaluationFeatures.cs:280` but PeHeaderForm PUT OMITS → clears; 2 PUT call-sites mutually destructive; no HoSoLink-preserve test) · AREA-5 urgent/notify/TZ (SignalR push BYPASSES UtcDateTimeJsonConverter benign-Kind.Utc; urgent SET/UNSET symmetric fail-closed; possible double-notify-then-skip drafter=Cấp1) · AREA-6 FE-consumers (all 6 PE-comp+3 pages SHA-identical 2-app; residual single-winner `PeListPanel.tsx:168` blank ≥2 joint; #70 stale-echo PRO row3/8 no-peFetching) · S89 AllowApproverFinalize-NOTE feasibility (**GAP: BE field MỚI cần** — flag chỉ ở `currentLevelOptions` current-level; approvalFlow+list+dashboard DTO KHÔNG carryexpose `EndsBeforeCeo`+FinalizeStep/LevelName; FE note đầu `PeWorkflowPanel.tsx` flow `<ol>`) · 06-24 3-change (winner=SINGLE `SelectedSupplierId`, winnerQuoteTotal DERIVED 3-site; budget=0 blocked 2-layer; terminal=NEW per-Level bool mirror SkipToFinal) · 06-23 D2/D4/D5 UAT (urgent authz BẤT-ĐỐI-XỨNG gắn-role/gỡ-DeptMgr; PE-create dropdown `PeWorkspaceCreateView.tsx:94`; workflow-required 2-tầng).
- **2026-06-20 governance-landing map (RC-sig + User-Mark H12/13 + objective-criteria):** ⭐ WHERE-to-land 3 AI_INFRA gov broadcasts. `harness-11-engine.md` = CANONICAL engine (D5/D6/D7 safety-tier + D8 one-direction-lock + D9 single-writer + D10 Bash-residual + CAVEAT no-OS-hook). `error-ledger.md` = §L.a AS-1..13 + §L.b 7-step. adap-report FORMAT richest = `2026-06-18-Governance-harness-11.md`. rules.md §6.6 = objective-criteria. **report-before-stamp ⊂ D7 (extend, đừng duplicateC3 vocab-fork).** → _INDEX.
- **2026-06-20 Harness-14 Eval/Budget/Outcome audit:** ⭐ H-14 = time/age/recency-decay KHÔNG được làm căn-cứ cắt feature. (1) BUDGET aligned: `memory-budget.json` 0 decay/TTL/age hit; `keep_floor=5`=newest-protection (drains OLDEST), cap seed-by-MEASURE. (2) detectors = canonical-anchor-vs-STATUS + disk-cross-check, ZERO age-window. (3) EVAL genuine: `eval/golden-set-solution_erp.jsonl` (14q) + `evaluator.md` recall@5≥0.7 — weekly-manual no-automation. (4) anti-downgrade = Harness-8 "all-inherit chất-lượng>chi-phí" PARTIAL (no generic rule in rules.md, grep 0). → _INDEX.
- **PE recon cluster (S69→S76, → _INDEX):** "Giá chào thầu" mục-c = `WinnerQuoteTotal`=SUM(Quote.ThanhTien WHERE supplier==Selected) DERIVED (3 nơi đồng-predicate) — PRO-Min/Max+CCM-proposed = Mig 54 NEW field. **Budget gate = PURE ROLE no-phase** (`canEditPro=Admin||Procurement`/`canEditCcm=Admin||CostControl` `PurchaseEvaluationFeatures.cs:800-801`; comment "bảng NS=tài-liệu-sống như Excel"); **submission-count-lock NEXISTS** (grep 0) → "khóa Initial sau lần-trình-đầu" = feature-MỚI. **Value-threshold HOOK B** = `ApproveV2Async` advance `:816-845` (reuse skipToFinal mechanic); **urgent flag** = AddColumn mirror `ItTicketPriority`. Badge ✎NS = +2 bool DTO via `GetUsersInRoleAsync` batch (no-N+1) both sites. PRO/CCM/CEO = `Procurement/CostControl/Director` (domain-shorthand NOT constants); V2 routing IGNORES roles (= specific ApproverUserId), 100% LINEAR no value/threshold config.
- **Office/HRM recon cluster (S65→S69, → _INDEX):** 21 `Off_*` keys, 10 office pages `{fe-admin,fe-user}/src/pages/office/` SHA256-mirror (except MyAttendance + AttendanceReport admin-only); golive-flip = remove `RevokeTemporarilyHiddenModulesAsync` call `DbInitializer.cs:2040`. Employee **master-detail EXISTS** (not list-only), Dept FLAT no-tree (pre-Mig51), 5 satellite 15-endpoint full. NamGroup **PURO = UI-skin** (ref demo.purocorp.vn) hardcoded-navTree, KpiCard-not-tab, shared PageHeader/WidgetCard. **gotcha #66:** `index.css h1-4{color:#0b1220}` OUTSIDE @layer → TW-v4 unlayered wins → heading-in-gradient MUST `text-white!`.
- **PE FROZEN-Mig59 patchwork + go-live recon cluster (S87→S89, 8 entries verbatim `archive/2026-06.md`):** key catches — AREA-4 **LATENT #73-class BUG** HoSoLink absolute-set `PurchaseEvaluationFeatures.cs:280` but PeHeaderForm PUT OMITS→clears (2 PUT mutually destructive, no preserve-test) · AREA-3 AllowApproverFinalize AUTO(Mig58)+finalizeByCcmDelegation OPT-IN(Mig54) coexist AUTO-first, ApplyApprovedPrice allow-list {Ncc,ProMin,ProMax,Ccm} · AREA-2 budget-matrix band-aid `PeSuggestedPriceFeatures.cs:153-199` PRO=drafter vs CCM=role authz-asymm · AREA-6 FE all 6-comp+3-pages SHA-mirror 2-app, #70 stale-echo · S89 AllowApproverFinalize-NOTE **GAP BE-field-MỚI** (flag only `currentLevelOptions`, DTO no-carryexpose EndsBeforeCeo) · 06-24 winner=SINGLE `SelectedSupplierId` derived-3-site, terminal=per-Level bool · 06-23 urgent authz asymmetric, PE-create dropdown `PeWorkspaceCreateView.tsx:94`.
- **2026-06-20 governance recon (both → archive/_INDEX):** (a) landing-map — `harness-11-engine.md`=CANONICAL engine (D5/6/7 safety-tier+D8 one-dir-lock+D9 single-writer+D10 Bash-residual+CAVEAT no-OS-hook); `error-ledger.md`=§L.a AS-1..13+§L.b 7-step; rules.md §6.6=objective-criteria; report-before-stamp ⊂ D7 (extend not duplicateC3 vocab-fork). (b) Harness-14 = age/recency-decay KHÔNG căn-cứ cắt feature; `memory-budget.json` 0 decay `keep_floor=5`=newest-protect drains-OLDEST; detectors canonical-anchor-vs-STATUS+disk 0 age-window; EVAL `golden-set-solution_erp.jsonl` 14q recall@5≥0.7 weekly-manual; anti-downgrade=H8 all-inherit PARTIAL (no generic rule grep 0).
- **PE recon cluster (S69→S76, verbatim → _INDEX):** "Giá chào thầu" mục-c=`WinnerQuoteTotal`=SUM(Quote.ThanhTien WHERE supplier==Selected) DERIVED 3-site; Budget gate=PURE ROLE no-phase (`canEditPro/Ccm` `PurchaseEvaluationFeatures.cs:800-801`); submission-count-lock NEXISTS (grep 0); Value-threshold HOOK B=`ApproveV2Async :816-845`; PRO/CCM/CEO=`Procurement/CostControl/Director` shorthand; V2 routing IGNORES roles (=ApproverUserId) 100% LINEAR.
- **Office/HRM recon cluster (S65→S69, verbatim_INDEX):** 21 `Off_*` keys, 10 office pages `{fe-admin,fe-user}/src/pages/office/` SHA-mirror (exc MyAttendance+AttendanceReport admin-only); golive-flip=remove `RevokeTemporarilyHiddenModulesAsync` `DbInitializer.cs:2040`; Employee master-detail EXISTS, Dept FLAT pre-Mig51; NamGroup PURO=UI-skin hardcoded-navTree; **gotcha #66** `index.css h1-4{color}` OUTSIDE @layer→TW-v4 unlayered wins→heading MUST `text-white!`.
- **Governance/Harness recon cluster (S71, → _INDEX):** H8 all-inherit FULLY adopted both layers (12/12 frontmatter + hmw.js resolveModel); **run-trace "TRACKED" = 2-level** (`git check-ignore` eligible vs `git ls-files` committed — model works only post `git add`); G-015 containment shift (Harness-2 "mọi tracked=vi-phạm" → H10 "tracked NGOÀI run-folder+code-disjoint=vi-phạm"). **path-trap:** bare `runs/` from repo-root = 0 → actual `.claude/workflows/runs/` (22 tracked). detector refine(b) = `.claude/hooks`/`.claude/scripts` not-exist (now S75 has `scripts/governance-detectors.ps1`).
- **[→ git pre-S60]** S60 recon#2 V2-engine-map (ApproveV2Async:446-634 guard+UPSERT+advance; skipToFinal F2:561-602 precedent) · S60 PE Section-3 submit-guard ROLE-only. Full text git.
- **[→ archive/2026-06.md + git]** S52 P11-D/E/F 6-gap recon · S50 HrmConfigs add-kind 11-spot · S51 gotcha #57 EXT reachability (curated S59).

View File

@ -27,8 +27,12 @@ Sheet1 · 31 col · **header ROW 4** · 4 data row (R5-R8 = 4 NCC đã seed prod
5. `invest:dedup-validation-rules` — dedup-by-MST edge (null/dup), R6 publish-guard, reconcile 22 NCC prod, test cases
## Status
- [!] invest ensemble **RUNNING** (`wf_e7896ca6-901`)
- next: em-main (single-writer) synthesize 5 lane RETURN → `spec-supplier-import-v2-12-07-2026.md` (3-mục: ①đề-bài ②cách-implement ③checklist) → present open-questions cho anh Kiệt.
- [x] invest ensemble **DONE** (`wf_e7896ca6-901`) — 4/5 lane structured (schema · import-service · fe-ux · dedup-validation) + 1 lane `public-visibility-consumers` **#53-garble** → em-main-solo recover (grep BE 16 + FE 11 consumer).
- [x] spec-file **WRITTEN**`spec-supplier-import-v2-12-07-2026.md`
- [!] **CHỜ anh Kiệt chốt** D1 (Mã NCC identity) · D2 (publish gate R4 vs R6) · D3 (authz) → rồi HMW Opus implement.
## Synthesis
_(pending — điền sau khi 5 lane về; em-main refute + synthesize P3/P4)_
## Synthesis (em-main refute + P3/P4)
**Hội tụ 4 lane (auto-decide):** IsPublic bool (KHÔNG đụng SupplierStatus) · dedup MST-primary+Code-backstop (chống 500) · backfill 22 prod→public · PublishSupplierCommand riêng (né #73) · template BE-gen ClosedXML · LayoutValid re-bake 30-token-thật (đã extract byte-exact).
**Chẻ đôi (→ anh chốt):** D1 Mã-NCC = Code[schema-lens] vs field-mới[import-lens]. D2 R4-strict vs R6-literal (mâu thuẫn thật). D3 authz FE⧣BE mismatch.
**Recover visibility:** consumers = PE picker (PeWorkspaceCreateView/PeDetailTabs) · ContractCreatePage · CreateContractFromEvaluationFeatures · dashboard — đều qua ListSuppliersQuery → filter `Published`. HMW grep-all trước implement.
→ Chi tiết đầy đủ ở `spec-supplier-import-v2-12-07-2026.md`.

View File

@ -0,0 +1,115 @@
# SPEC — Supplier import v2 (12-07-2026, S113)
- **run:** `wf_e7896ca6-901` (fable-clone ensemble 5 lane investigator-codebase; 4 structured + 1 garble#53 [visibility] recover em-main-solo)
- **engine:** `fable-clone-ensemble · Opus-S113` · **owner:** anh Kiệt · **lead:** Opus 4.8 (1M) Max
- **source file:** `C:\Users\pqhuy\Downloads\Data base - 1_7_2027.xlsx` (Sheet1, header ROW 4, 30 cột, 4 data row)
- **downstream:** HMW Opus 4.8 MAX implement THEO spec này (sau khi anh Kiệt chốt §Open-Decisions).
---
## ① TÍNH NĂNG / ĐỀ BÀI
Nâng cấp Supplier Excel-import (S112 Phase B) thành v2:
- **R1** Import đọc ĐÚNG cấu trúc file thật (header ROW 4, 30 cột).
- **R2** Nút "Tải file mẫu" — download template xlsx trống đúng layout.
- **R3** Sau upload: bước nhập **Mã NCC** thủ công per-dòng (file KHÔNG có cột này).
- **R4** Thiếu Mã NCC → **draft/ẩn**, không public (nháp trước khi public).
- **R5** Dedup theo **MST** (Mã số thuế, cột 12).
- **R6** Không MST **VÀ** không Mã NCC → không được public.
- **R7** List NCC hiện trạng thái **Public / đang-ẩn(draft)**.
**PHÁT HIỆN GỐC (mọi lane đồng thuận):** `Supplier.Code` (comment "Mã NCC") nhưng S112 import NHỒI c4 "TÊN VIẾT TẮT" vào Code. Field mang nhãn "Mã NCC" đang chứa viết-tắt → đây chính là chỗ R3 muốn tách.
---
## ★ DECISIONS — anh Kiệt ĐÃ CHỐT (S113)
> ✅ **D1 = A** (Mã NCC = `Code`) · ✅ **D2** (Code = cổng public; MST = cảnh-báo-mềm; existing-public giữ nguyên; logic import-scope) · ✅ **D3** (nới quyền: mọi user có quyền cập-nhật danh-mục, KHÔNG admin-only). Chi tiết áp + API-contract ở cuối mục. Phần option gốc bên dưới giữ để tra cứu.
### CHỐT áp dụng:
- **D1=A:** Mã NCC = `Supplier.Code`. KHÔNG field mới, KHÔNG ShortName. Import pre-fill Code từ c4 (editable grid). Draft-row Code="" → filtered-unique đổi `WHERE IsDeleted=0 AND Code<>''`. Code non-empty = publish-able, "" = draft.
- **D2:** `IsPublic` tại confirm = `!IsNullOrWhiteSpace(Code)` [R4]. MST thiếu → KHÔNG chặn, WARNING preview "⚠️ Chưa có MST — không kiểm tra được trùng, có thể bị trùng" [R6→mềm]. 22 prod public giữ (backfill=1). List toggle "Công bố/Ẩn" (PublishSupplierCommand, gate Code-non-empty).
- **D3:** BE import preview/confirm + template + publish: `[Authorize(Roles="Admin,CatalogManager")]` → policy quyền **Suppliers Update/Create** (danh mục). FE guard = PermissionGuard Suppliers Update/Create (khớp BE, hết 403).
### API CONTRACT (chốt cho BE⟂FE parallel):
- `SupplierDto` +`bool IsPublic` (append CUỐI positional record → sửa 2 site projection khít: `ListSuppliersQuery:48-54` + `GetSupplierQuery:21-26`).
- `GET /api/suppliers?published={true|false}` — ListSuppliersQuery +`bool? Published`; null=all (admin quản lý).
- `POST /api/suppliers/{id}/publish` body `{ maNcc?: string, publish: boolean }` → PublishSupplierCommand (nếu maNcc gửi: set Code + unique-CI-check; publish=true gate Code-non-empty; KHÔNG dùng UpdateSupplier).
- `GET /api/suppliers/import/template` → .xlsx (ClosedXML, 30 token row-4, sheet "Sheet1", freeze row-4).
- Import confirm rows: Code editable per-row; confirm set `IsPublic=has-Code`; preview row DTO +`bool MstMissing` (FE render warning).
---
## (option gốc — tra cứu) OPEN-DECISIONS ban đầu
**D1 (GỐC — chi phối toàn bộ) — "Mã NCC" là field nào?**
- **Opt A** — Mã NCC = `Supplier.Code` hiện có; c4 "TÊN VIẾT TẮT" chuyển sang field MỚI `ShortName`. Import thôi bắt buộc Code; draft-row Code="". Đổi filtered-unique `WHERE IsDeleted=0 AND Code<>''`. Contract-number `SOL&{Code}` giữ đúng nghĩa. *(schema-lens đề xuất; churn cao hơn + cần guard "contract phải có Code")*
- **Opt B** — Mã NCC = field MỚI `PublicCode`; `Code` giữ = c4 viết-tắt (required/unique/dedup-backstop/codegen giữ nguyên). *(import-service-lens đề xuất — go-live rủi-ro-thấp nhất; nhược: Code mang nhãn "Mã NCC" thành sai)*
- **Lead lean:** **Opt B** cho go-live an toàn (codegen + dedup-backstop nguyên vẹn), NHƯNG cách anh diễn đạt nghiêng A → **cần anh chốt rõ**.
**D2 — Cổng public (R4 vs R6 mâu thuẫn thật):** R4="không Mã NCC→ẩn" (Mã NCC bắt buộc) vs R6="không MST VÀ không Mã NCC→ẩn" (MST thay được).
- **Opt 1 (R6-literal):** public ⟺ có Mã NCC **HOẶC** có MST.
- **Opt 2 (R4-strict):** public ⟺ **bắt buộc** có Mã NCC (MST không mở khóa).
- **Lead lean:** **Opt 2** (khớp R3/R4 "user tự nhập Mã NCC mới hiện"); import luôn tạo draft, publish là bước người làm tay. Enforce server-side trong `PublishSupplierCommand`.
**D3 — Authz nút Import/Template:** FE gate `Suppliers/Create` (mở mọi user từ S59) ⧣ BE import `[Authorize(Roles="Admin,CatalogManager")]` → non-admin bấm bị 403.
- **Lead lean:** siết FE nút Import+Template về role `Admin/CatalogManager` cho khớp BE (đây cũng là follow-up S112 "FE-Import role-gate"). *(Hoặc nới BE — anh chọn.)*
**D-auto (lead tự quyết, đồng thuận 4 lane — anh phủ quyết nếu muốn):**
- Trạng thái = **`bool IsPublic` default false** (KHÔNG đụng enum `SupplierStatus` = business-status c27).
- Dedup = **MST-primary + Code-backstop** (chống 500 unique-violation), normalize MST (Trim+bỏ space nội, CI); MST rỗng → KHÔNG coi trùng nhau.
- Backfill **22 NCC prod → IsPublic=true** (đang live, PE/Contract tham chiếu — để draft sẽ ẩn NCC đang dùng + vỡ picker).
- Publish qua **`PublishSupplierCommand` riêng** (chỉ set Mã NCC + IsPublic), KHÔNG dùng `UpdateSupplierCommand` (blind absolute-set → #73 clobber data draft).
- Template R2 = **BE endpoint** `GET /suppliers/import/template` (ClosedXML từ token single-source, 0 drift).
---
## ② CÁCH IMPLEMENT
### BE — Migration 64 `AddSupplierPublishState` (3-file rule)
- `Supplier.cs` +`bool IsPublic` (default false). [D1=A] thêm `string? ShortName` + đổi comment Code. [D1=B] thêm `string? PublicCode`.
- `SupplierConfiguration.cs` +`Property(IsPublic)`; [D1=A] đổi unique-filter `[Code]<>''` (DropIndex→CreateIndex); [D1=B] index giữ nguyên + (option) config PublicCode.
- Migration Up: `AddColumn<bool>("IsPublic", nullable:false, defaultValue:false)` + `Sql("UPDATE Suppliers SET IsPublic=1")` backfill (idempotent). Down: DropColumn. **No new table (89 giữ).**
- `DbInitializer.cs` seed IsPublic + [D1] ShortName/PublicCode cho 4 real + 16 sample (4 real: IsPublic=true).
### BE — `SupplierExcelImportService.cs`
- **R1 LayoutValid (CRITICAL):** re-bake `ExpectedHeaderTokens` BYTE-EXACT theo 30 token THẬT dưới đây; re-run test Case 9. Sửa test drift `NormalizeJoin.Norm` thiếu `.Normalize(FormC)` (khớp service :313).
- **R5 dedup→MST:** build `existingByTaxCode` (bỏ blank) + GIỮ `existingByCode` backstop. `NormalizeMst`=Trim+strip inner ws, CI. Precedence: MST-match→Update · else Code-match→Update · else New. In-batch: `batchByMst`+`batchByCode`. MST rỗng → rơi Code-path, KHÔNG merge 2 blank-MST. Update = FillNulls (#73-safe).
- **R3 Mã NCC per-row:** [D1=A] preview để Code editable, confirm round-trip `row.code`. [D1=B] +`string? ManualCode` vào `SupplierImportRowDto` round-trip → NewSupplier ghi vào `PublicCode`.
- **R4 draft:** confirm set `IsPublic=false` cho MỌI new-row (file không có Mã NCC → tất cả nháp). Update-path KHÔNG demote NCC đang public. +`DraftCount` vào ResultDto.
- Endpoint preview/confirm giữ chữ ký (rows mang thêm field). **NEW** `GET /suppliers/import/template` (ClosedXML, sheet "Sheet1", 30 token vào ROW 4, freeze row 4) via `BuildTemplate()` trên `ISupplierExcelImportService`.
### BE — Publish + List filter
- **NEW** `PublishSupplierCommand(Guid Id, string MaNcc, bool publish)`: load entity → nếu publish: enforce D2-gate (Opt2: Mã NCC non-empty; Opt1: Mã NCC OR MST) + unique-CI → set Mã-NCC-field + IsPublic → save. Endpoint `POST /suppliers/{id}/publish`.
- `ListSuppliersQuery` +`bool? Published``Where(x=>x.IsPublic==published)` khi có. `SupplierDto` +`IsPublic` (sửa CẢ 2 site projection: `ListSuppliersQuery:48-54` + `GetSupplierQuery:21-26` — positional record khít thứ tự).
- **R4/R6 VISIBILITY (recover lane):** consumer pass `published=true` → ẩn draft. Consumers cần lọc: PE picker (`PeWorkspaceCreateView.tsx`, `PeDetailTabs.tsx`), `ContractCreatePage.tsx`, `CreateContractFromEvaluationFeatures.cs`, `PurchaseEvaluationSupplierFeatures.cs`, dashboard. **HMW PHẢI grep MỌI consumer** (bài học cardinality S87/S88) + quyết nơi nào lọc public-only, nơi admin-quản-lý thấy cả draft.
### FE — 2 app SHA-mirror (Pattern 16-bis)
- `types/master.ts` (2 app): `Supplier` +`isPublic: boolean` (+[D1=B] `publicCode`).
- `SuppliersPage.tsx` (2 app): +cột "Trạng thái" badge Public(emerald)/Nháp-ẩn(slate) [Pattern 14 full-class-literal] + nút "Công bố" (guard Suppliers/Update, mở dialog nhập Mã NCC→POST publish) + filter select + [D3] siết guard nút Import.
- `SupplierImportDialog.tsx` (2 app): +nút "Tải file mẫu" (blob-download mẫu `PeDetailTabs.tsx:3273-3279`) + cột "Mã" editable (`updateRow` patch state) + helper "NCC chưa nhập Mã sẽ lưu nháp (ẩn)" + nới `disabled` chỉ chặn lỗi THẬT (layout/dup), KHÔNG chặn vì thiếu Mã NCC.
### 30 TOKEN HEADER THẬT (row 4 — bake byte-exact vào ExpectedHeaderTokens)
```
c1 STT · c2 GÓI THẦU · c3 "PHÂN LOẠI\n(NTP/NCC/Cả hai)" · c4 "TÊN VIẾT TẮT\n(Dùng trong HĐ)" · c5 "TÊN CÔNG TY\n(Đầy đủ, đúng pháp lý)" · c6 "ĐỊA CHỈ XUẤT HÓA ĐƠN\n(Địa chỉ đăng ký kinh doanh)" · c7 "ĐỊA CHỈ VĂN PHÒNG \n(nếu có)" · c8 "SỐ ĐIỆN THOẠI\nCÔNG TY" · c9 FAX · c10 "SỐ TÀI KHOẢN+ TÊN+CN. NGÂN HÀNG\n(Đầy đủ, đúng pháp lý)" · c11 "SỐ TK PHỤ\n(nếu có)" · c12 MÃ SỐ THUẾ · c13 "NGƯỜI ĐẠI DIỆN\nPHÁP LUẬT" · c14 "CHỨC VỤ\nĐẠI DIỆN" · c15 "GIẤY ỦY QUYỀN\n(số, ngày, người ủy quyền)" · c16 Link GUQ · c17 Link GPKD · c18 Link HSNL · c19 "NGƯỜI LIÊN HỆ\nCHÍNH" · c20 "CHỨC VỤ\nNGƯỜI LH" · c21 SĐT CHÍNH · c22 EMAIL · c23 ĐỊA CHỈ GỬI THƯ · c24 "NGƯỜI NHẬN THƯ/\nSDT" · c25 "NGUỒN\nGIỚI THIỆU" · c26 "NGƯỜI PHỤ TRÁCH\n(PMH)" · c27 "TÌNH TRẠNG\nHIỆN TẠI" · c28 "GHI CHÚ / LÝ DO\nBLACKLIST" · c29 "NGÀY CẬP NHẬT\nCUỐI" · c30 NGƯỜI CẬP NHẬT
```
(NormalizeHeader = upper + collapse-ws + FormC → `\n`/space bị chuẩn hóa, nhưng text phải khớp. c21 "SĐT" + c24 "SDT" = shorthand cần khớp đúng.)
---
## ③ CHECKLIST (vai + deliverable + acceptance ĐO ĐƯỢC)
| Vai | Deliverable | Acceptance đo được |
|---|---|---|
| 🟨 implementer-backend | Mig 64 `AddSupplierPublishState` (3-file) + Supplier/config/DbInit + re-bake tokens + dedup-MST + PublishSupplierCommand + import/template endpoint + ListSuppliersQuery filter + SupplierDto | `dotnet build slnx` 0/0; Mig Up/Down reversible; sys.tables=**89** (no new table); backfill Sql idempotent |
| 🟧 implementer-frontend | 2-app mirror: types +isPublic · SuppliersPage badge+nút Công bố+filter+guard · ImportDialog nút mẫu+cột Mã editable+helper | `npm run build` ×2 PASS; SHA-identical 2 dialog + 2 page (Pattern 16-bis); badge full-class-literal |
| 🟪 test-specialist | dedup T1-T7 (MST/Code/blank/conflict) · publish T8-T11 (draft-on-import/gate/unpublish) · list T12-T13 filter · reconcile T14 backfill · LayoutValid Case9 real-token · all-or-nothing T15 | ≥15 test mới; 0 fail/skip; test-before cho dedup+publish-guard (critical/security) |
| 🟥 reviewer | Adversarial pre-commit: dedup edge (null/dup MST), publish-gate D2, visibility grep-all-consumer, #73 clobber check | GO/GO-WITH-ADJ verdict; 0 must-fix |
| 🟩 cicd-monitor | post-deploy verify Run # | bundle rotate ×2 + Mig 64 prod applied + smoke 200 + byte-verify nút "Tải file mẫu"/badge |
**Ràng buộc:** UI 100% tiếng Việt · commit `[CLAUDE] Supplier: ...` · KHÔNG đụng SupplierStatus enum · publish KHÔNG qua UpdateSupplierCommand (#73).
---
## 🔻 #53-garble note
Lane 3 `public-visibility-consumers` (agentId af249d48) completed without StructuredOutput (#53 tái diễn — ×4 phiên gần đây). Recover: em-main-solo grep consumers (BE 16 file + FE fe-user 11 file) → consumers chính = PE picker + Contract create + dashboard, đều qua `ListSuppliersQuery` → filter `Published` giải quyết. KHÔNG re-spawn.