- 3 over-cap sub L1 -> L2 archive byte-exact: reviewer 45->10KB, investigator-codebase 40->10KB, cicd-monitor 39->12KB - 31 entries moved (sed, +N -0 additive, 0 byte-loss) + 31 _INDEX substring pointers; A7 GATE PASS 217/217 resolve - stale foundation counts flushed: 130/263->354 test, 55->71 gotcha, Mig 40/55->57, 84->88 table, bundle->#330 - 0 production code, state unchanged (Mig 57 / 88 tables / 354 test / gotcha 71) - WATCH (A6 strike-1, no-action): frontend-designer 26KB + test-specialist 28KB - lesson: _INDEX substring MUST quote-free (A7 quote-parser caught escaped-quote PURO pointer that self-grep missed) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 KiB
Reviewer Agent - Archive 2026-06 (S69 curate)
Verbose Recent-activity entries moved from MEMORY.md during S69 Harness-9 curate (L1 over-cap ~43.5KB -> under 28KB). FROZEN / additive-only. Entries are BYTE-EXACT copies from MEMORY.md L1 (newest-first there); ordered here chronological earliest-first. Foundation (role + bug patterns #42/#43/#44/#47 + 6-category checklist + Smart Friend guard + review essentials) + 6 newest activity entries + S49/S43/S35/S33 tail + Smart-Friend-cumulative + archive-pointers KEPT in MEMORY.md. Span: S51 -> S57 (9 entries). See
_INDEX.mdfor the cross-archive table.
Archive entries (chronological - earliest session first)
-
2026-06-08 (S51 P11-C Vehicle+Driver + gotcha #57 pre-commit — PASS, 1 MAJOR caught) [em main proxy — reviewer return truncated gotcha #53]: Reviewed Mig 44 (Vehicle/Driver catalog) + Mig 45 (filter 3 HRM unique) + FE KIND_CONFIG +2 + 5 tests (186 PASS). Independent build+test re-verify GREEN. CAUGHT 1 MAJOR (Cat 3 cross-stack contract): Driver FE↔BE required-field mismatch — FE render phoneNumber/licenseNumber/licenseClass OPTIONAL nhưng BE validator
NotEmpty()+ EF.IsRequired()NOT NULL → empty submit = 400/500. Root = inconsistent em-main brief (BE "mirror Vehicle"=required vs FE spec quên required). Fix: FE +required:true(align BE all-required như Vehicle). Cats khác clean (Mig diff clean, Authorize Roles=Admin writes, gotcha #57 grep-complete 3 HRM, DbInitializer idempotent + #51 infra-gated, SHA256 mirror, no copy-paste Driver↔Vehicle). Learned: parallel fan-out (BE∥FE file-disjoint) → bất kỳ inconsistency trong SHARED em-main contract chỉ lộ lúc integration; green tests ≠ correct contract (no test chạm empty-optional path). reviewer = the net. surprise: transient mid-deploy bundle hash (cicd lesson) + reviewer self-truncate trước khi ghi MEMORY → em main proxy. Verdict PASS post-fix. Tag [s51, p11-c, gotcha57, contract-mismatch-catch]. -
2026-06-08 (S52 P11-E AttendanceReport + P11-F MaTicket codegen pre-commit — PASS, 0 blocker): Migration-free (no schema). Independent re-verify: build 0-err · 191 PASS (58 Dom + 133 Infra, +5: 3 ItTicketCodeGen + 2 AttendanceReport) · fe-admin
tsc --noEmitexit 0. Cat3 gotcha #44 attack DISARMED:[Authorize(Roles="Admin")]×2 report endpoints — verifiedAppRoles.Admin = "Admin"literal (AppRoles.cs:5) == attribute string == FEuser?.roles.includes('Admin'); "QTV" (DbInit:1454) = display-code NOT role-name; pattern proven (Catalogs/HrmConfigs identical). Cat3 camelCase contract MATCH field-for-field BE record PascalCase→FE interface (year/month/rows/grandTotal*/userId/fullName/ot*) — ASP.NET default camelCase, no Program.cs override. BE handler correct:.Year/.Monthin IQueryable (EF-translatable DateTime),.DayOfWeek+holidaySet only AFTER.ToListAsync()(in-memory) — IQueryable-translation attack handled; holiday-check BEFORE weekend BEFORE weekday (test 2026-06-01 Mon-but-holiday proves override);DateOnly.FromDateTimecorrect (Holiday.Date=DateOnly); OtPolicy fallback 1.5/2.0/3.0;IsDeletedvia AuditableEntity all 3 entities. Exporter mirrors ContractExcelExporter, ClosedXML 0.105.0,RenderResult(Content,FileName,ContentType)ctor order correct, DI registered. MaTicket codegen:euntracked at codegen time → inner SaveChanges persists ONLY sequence row, no double-insert; gen-on-Create (kanban no-workflow) vs Leave/OT gen-on-Submit — semantically correct; git-show confirms MaTicket was ALWAYS null pre-P11-F (closes gap). 1 MINOR (informational, defer): sequence-gap-on-failure — codegen commits seq in own Serializable tx BEFOREAdd(e)+SaveChanges; ticket-insert fail → burned IT/2026/NNN gap. NOT new defect = identical to existing Leave/OT pattern (project-wide accepted trade-off, cosmetic). MyAttendancePage MIRROR divergence intentional+documented (fe-user untouched, §3.9 OK). 0 mock markers. Learned: when spec NAMES an attack vector (gotcha #44 role-string), verify the LITERAL const value not just attribute presence — "QTV" display-code was the decoy; role-name match is the real check. surprise: Bash tool = bash not PowerShell (Select-String fails exit 127 → use grep). Verdict PASS — safe to commit. Tag [s52, p11ef, attendance-report, mat-codegen, gotcha44-disarmed]. -
2026-06-08 (S52-late Task C ItTicket admin reassign + Task D AttendanceReport menu-key pre-commit — PASS, 0 blocker): Migration-FREE (menu = idempotent DbInitializer seed). 5 prod files: MenuKeys.cs (const+All[]), DbInitializer.cs (1 seed tuple), fe-admin {menuKeys.ts, Layout.tsx staticMap, ItTicketsPage.tsx}. Independent re-verify GREEN:
dotnet build SolutionErp.slnx0-warn/0-err ·npm run buildfe-admin tsc-b+vite OK (1945 modules, only pre-existing CSS @import + >500KB + ineffective-dynamic-import warns). menuKeyMatchOk:"Off_AttendanceReport"byte-identical 4 places (MenuKeys.cs:125 const == menuKeys.ts:68 == seed parent key == Layout staticMap:87); seed leaf parent=MenuKeys.Offorder=8 icon=FileBarChart(verified valid lucide aliasFileChartColumn as FileBarChart— getIcon resolves via Icons[name]); App.tsx route/attendance/report → AttendanceReportPagePRE-EXISTING committed S52 (6a66429, no diff, 170-LOC real page not stub) — Layout maps to REAL route.types/menu.tscorrectly NOT mirrored (key:stringnot typed union). admin auto-perm viaSeedAdminPermissionsAsynciteratingMenuKeys.All(DbInit:1917, idempotent Contains:1919); new-leaf-on-existing-DB confirmed (upsert loop:1845-1862 TryGetValue-miss→Add). reassignCorrect: FE PUT/it-tickets/{id}/assignbody{assignedToUserId}MATCHES BE recordAssignItTicketBody(Guid AssignedToUserId)(ItTicketsController:42); endpoint[Authorize(Roles="Admin")](:34) under class[Authorize]— admin-app FE calling = correct; user-list reuses EXISTING/usersGET (PagedResult<UserDto>items{id,fullName,email}, no new BE endpoint, lazyenabled:target!==null); 204 NoContent handled (no body-parse);invalidateQueries(['it-tickets'])on success; handler sets BOTH AssignedToUserId+AssignedToFullName (WorkflowAppsFeatures:467-468) + validates assignee IsActive→NotFound, no try-catch (GlobalExceptionMiddleware). feUserUnchanged:git diff -- fe-user/EMPTY (Task C = fe-admin-only divergence, documented top-comment ItTicketsPage:3-5). noScopeCreep: git status prod = EXACTLY the 5 expected files, agent-memory noise ignored, no new migration, no BE beyond MenuKeys+DbInitializer, ItTicketsPage diff 98+/5- all Task-C-scoped (imports+state+mutation+Pencil-btn+Dialog), 0 mock/alert markers. Learned: menu-key wiring = verify byte-identity across the FULL mirror set (BE const + BE All[] + seed parent + FE menuKeys + FE staticMap) + confirm the target route actually EXISTS (grep App.tsx) — a staticMap entry pointing to a non-existent route silently drops the leaf (gotcha #50). surprise: lucideFileBarChartis a deprecated-alias (re-exported from FileChartColumn) but still valid — d.ts grep confirmed before flagging. Verdict PASS — safe to commit. Tag [s52-late, it-ticket-reassign, attendance-report-menukey, menukey-mirror-5way, gotcha44-disarmed, gotcha50-disarmed]. -
2026-06-08 (S53 gotcha #57 EXT Mig 47 — Master catalog filtered-unique pre-commit — PASS, 0 blocker, Smart Friend clean): 4th/5th/6th cumulative gotcha #57 (after Holiday Mig 43 S45 + HRM ×3 Mig 45 S51). 3 Master configs (Department:18/Project:19/Supplier:24) Code unique index
.IsUnique()→+.HasFilter("[IsDeleted] = 0")+ Mig 47 (3-file) + 3 new tests. Independent re-verify ALL GREEN: build SolutionErp.slnx 0-warn/0-err · full suite 203 PASS (58 Dom + 145 Infra, Failed:0 Skipped:0, +3) · 3 new tests run isolated 3-passed-0-skipped. Cat correctness: filter string byte-identical to HolidayConfiguration:18 (xxd5b 49 73 44 65 6c 65 74 65 64 5d 20 3d 20 30— spaces around=, not guessed); index STAYSunique:true(2 active same-Code still violate — active uniqueness preserved); Supplier Type index (:25) UNTOUCHED non-unique unfiltered (snapshot:3590 bare). Mig Up=3×Drop+3×Create-filtered, Down=3×reverse-unfiltered (reversible). Snapshot+Designer both show filter on all 3 Master Code idx. Test NOT tautology: seeds IsDeleted=true row → real Create*CommandHandler (app-checkAnyAsync(Code==req.Code)thru HasQueryFilter !IsDeleted PASSES) → asserts NotThrow + active-count==1 + IgnoreQueryFilters all==2; RED-before confirmed (3 failed SqliteException UNIQUE on unfiltered). Cmd signatures match test calls (Project 7-arg/Supplier 9-arg/Dept 4-arg). noScopeCreep: git status = exactly 3 configs + snapshot + Mig47 (2 untracked) + 1 test + 2 MEMORY; no FE, no extra mig, no stray. Mig 47 latest in seq (after Mig46 ItTicket SLA). Learned: cookie-cutter EXT of proven pattern → discriminator = byte-compare filter string (xxd) vs canonical sibling + verify index still unique (filter must NARROW scope not DROP uniqueness). app-level dup-check existence = the test premise; verify handler actually hasAnyAsync(Code)else test premise false. surprise: implementer claimed "2 pre-existing DocxRenderer warnings" but clean incremental rebuild = 0 warn (unrelated, non-issue). Verdict PASS — safe commit. Tag [s53, gotcha57-ext, mig47, master-catalog, smart-friend-clean]. -
2026-06-08 (S54 ItTicket reassign authz Admin-OR-dept-IT cross-stack pre-commit — PASS, 0 blocker, gotcha #44 disarmed correctly): Controller
/assignhạ[Authorize(Roles="Admin")]→[Authorize]any-auth; authz moved INTOAssignItTicketHandler(Admin-OR-IT Forbidden + assignee-must-IT Conflict) + newGetAssignableItStaffQuerycapability endpoint. Independent re-verify GREEN:dotnet test SolutionErp.slnx= 216 PASS (58 Dom + 158 Infra, Failed:0 Skipped:0, +13 matches 203→216 claim exactly) · fe-admin + fe-usertsc -p tsconfig.app.json --noEmitBOTH exit 0 clean. #2 CHÍ MẠNG role-string "Admin" CONFIRMED REAL (full chain traced):AppRoles.Admin="Admin"literal (AppRoles.cs:5) → SeedRolesAsyncName=roleName(DbInit:1485) so DB Role.Name=="Admin" → Identity GetRolesAsync returns NAMES → JwtTokenService:32new Claim(ClaimTypes.Role, r)→ CurrentUserService:30-31FindAll(ClaimTypes.Role)→cu.Roles.Contains("Admin")CORRECT. "QTV" (DbInit:1458 RoleLabels) = ShortName DISPLAY label only = decoy. Program.cs JWT sets NORoleClaimTypeoverride (ClaimTypes.Role symmetric write/read). Same proven pattern as every existing Roles="Admin" endpoint. Bypass airtight: controller any-auth → handler sole gate; guardif(!isAdmin && !(itDeptId is Guid mine && myDeptId==mine)) Forbiddenfail-CLOSED when itDeptId null (non-admin blocked; admin still passes role-branch).Department.Code=="IT"IS seeded (DbInit:2082 "Phòng CNTT") so live non-null. Capability 0-leak: non-auth →{canReassign:false, staff:[]}(no name leak),[Authorize]any-auth → 0 silent-403. Defense-in-depth intact: FE nút{canReassign&&}but PUT still hits handler guard → 403/409 →onError: toast.error(getErrorMessage(err))surfaces (NOT swallowed). FE SHA256 page 4bcaf2f IDENTICAL both apps (types.ts correctly NOT identical — admin AttendanceReportDto vs user HrDashboardDto diverge below; added AssignableStaff block byte-identical); old fe-user page was read-only kanban (NO app-specific logic lost — diff purely additive); fe-user has all imports (apiError.getErrorMessage, Dialog size/footer/onClose props match, Select passthrough, sonner). Test 13-fact NOT happy-path: Case5 Forbidden side-effect assertAssignedToUserId.Should().BeNull()(red-able by contrast vs Case6/7 same-handler success), Case3/3b empty-staff 0-leak, Case8 Conflict msg-exact. prove-by-contrast ĐỦ CHẶT (partition non-IT-throws vs IT/admin-succeed identical handler). 1 MINOR defer: assignee-must-IT NEW vs old handler (git HEAD: old allowed admin→ANY active user); itDeptId-null → even admin Conflict — fail-closed acceptable+spec-requested, cosmetic (prod IT-dept seeded). Learned: authz role-string review = trace FULL chain const→seed Name→GetRolesAsync(names not codes)→Claim(ClaimTypes.Role)→reader AND grep JWT cfg forRoleClaimTypeoverride (none=symmetric) — display-code (QTV) in RoleLabels/ShortName dict = classic decoy. surprise: moving authz controller→handler is the CORRECT gotcha #44 fix (not a smell) when paired with BE-computed capability flag for FE gating + handler as sole gate. Verdict PASS — safe commit. Tag [s54, it-ticket-reassign-authz, gotcha44-disarmed, role-string-chain-verified, cross-stack-clean]. -
2026-06-09 (S55 master-data import pre-commit — PASS [em main proxy — reviewer return truncated gotcha #53 before verdict, mirror S51]): Reviewed Mig 48
AddProjectMasterFields(Project +4 nullable col Year/Investor/Location/Package) +SeedRealMasterDataAsync(62 Project+71 WorkItem+3 Supplier per-code idempotent ungated) + FE ProjectsPage form +4 ×2 app. Reviewer ran 293s/31-tools nhưng truncated mid-thought (nghi cached-binary 2.76s build → muốn forced clean rebuild + Project tests). Em main COMPLETED đúng việc nó định làm:dotnet test SolutionErp.slnx= clean rebuild + 216 PASS (58+158, 0 fail/skip) → giải tỏa cached-binary concern (test = fresh build). 10 dims GREEN: Mig Up=4 AddColumn/Down=4 DropColumn reversible + 3-file; seed 62/71/3 0-dup; per-code idempotent ungated line 118 (reaches prod); FLOCK01 collision skip-demo-wins; FE↔BE 4 nullable both sides (tránh S51 mismatch); test-file compile-fix +4 null legit; gotcha #57 index untouched; runtime Dev proof (data landed, Investor col populates). 0 rogue write (read-only respected, git clean of code). Learned: long adversarial review return truncates (gotcha #53) → reviewer nên emit PASS/FAIL verdict SỚM (trước deep re-verify) để sống sót truncation; em main complete được đúng pending-check (clean dotnet test) deterministic. Verdict PASS — safe commit. Tag [s55, master-import, em-main-proxy-truncate, runtime-dev-proof]. -
2026-06-09 (S55 Phase-1 FE visual redesign pre-commit — PASS, 0 blocker, verdict-first survived): 14 fe-admin files VISUAL/CSS-only (NAMGROUP density + SOLUTION brand). Independent re-verify GREEN:
npm run buildfe-admin = ✓ 607ms, 1945 modules, 0 TS err (only PRE-EXISTING warns: CSS @import-order + >500KB chunk + INEFFECTIVE_DYNAMIC_IMPORT realtime.ts — git-confirmed none introduced, @import lines untouched in diff). Regression Cat1 ALL preserved: Button cva variant keys (primary/secondary/outline/ghost/danger) + size (sm/md/lg) STABLE — only Tailwind class VALUES swapped, defaultVariants intact (51 call-sites safe); Input/Select/Textarea/Label =forwardRef+...props+classNamepassthrough unchanged, onlycn()literal; Dialog{open,onClose,title,children,footer,size}destructure + sm/md/lg→max-w map intact (+aria-label="Đóng" = a11y GAIN); DataTableColumn<T>type UNCHANGED (diff starts after type def) — render/sortable/align/width + sort + Pagination props intact, RowActions/RowActionButton purely ADDITIVE; Layout MenuLeaf className-only (brand left-rail via before:), nav/resolver/permission-filter/routing untouched; PhaseBadge phase→ContractPhaseColor/Label map intact; PageHeader/EmptyState/TopBar pure class. DashboardPage data-flow (useQuery/navigate/fmtMoney/BarChart/PhaseBadge) preserved, STAT_TONE+SectionLabel additive, +cnimport only. Brand Cat3: Be Vietnam Pro KEPT (grep: @import:3 + --font-sans:22 + font-family:34 all unchanged — initial blocker RETRACTED after grep); only brand-/slate/semantic colors, 0 off-brand hex/indigo. a11y: focus-visible rings present everywhere (brand-500); Label self-documents slate-500 (~4.6:1 AA-pass) chosen over NAMGROUP zinc-400. Tailwind v4 (^4.2.3) —ring-current/15,shadow-xs, slash-opacity all valid v4. noScopeCreep: exactly 14 fe-admin, 0 fe-user, 0 BE/src (only noise = frontend-designer/MEMORY.md agent file). 2 MINOR (non-block, a11y-floor):text-slate-400on white for small hint/empty text (DashboardPage hints ~line 50/64, DataTable empty-cell, EmptyState was-400-stays-400) ≈3.5-4:1 — borderline-fail WCAG-AA for <18px, but these are de-emphasized hints not primary content + PRE-EXISTING tone (redesign mostly UPGRADED slate-400→500 on EmptyState desc + Pagination); accept for hint role, revisit if audit. Learned: font-drop scare = grep the 3 load-bearing lines (@import/--font-sans token/font-family) BEFORE flagging — diff hunk lower in file ≠ font removed; emit PASS/FAIL line-1 FIRST (gotcha #53 truncation survival, mirror S51/S55). surprise: Tailwind v4shadow-xsis real (v3's shadow-sm renamed) — don't flag as typo; v4 slash-opacity on currentColor (ring-current/15) is valid. Verdict PASS — safe commit+deploy. Tag [s55-fe, visual-redesign, namgroup-density, verdict-first, regression-clean, slate400-minor]. -
2026-06-09 (S56 pre-golive authz live-curl — PASS, 0 blocker): Live prod curl 8 new endpoints. 8/8 return 401 unauth; admin-authed: hrm-configs/vehicles(2)+drivers(2), leave-balances/my(5 lazy), attendances/report+excel(200, 6797B xlsx) all 200; non-admin Drafter correctly 403 on the 2 Admin-only attendance endpoints. gotcha #44 silent-403 sweep CLEAN: capability GET /it-tickets/assignable-staff returns HTTP 200
{canReassign:false,staff:[]}for non-IT Drafter (NOT swallowed 403) +{true,[]}admin — handler returns flag, doesn't throw (WorkflowAppsFeatures.cs:466). assign-mutation guard fail-closed (:504). E2E: GET /projects payload has all +4 fields (70/70), CAL01 Investor live. Off_AttendanceReport menu key in admin /menus/me. 1 MINOR (non-block, defense-in-depth): PUT /it-tickets/{id}/assign checks NotFound BEFORE Admin-OR-IT Forbidden (WorkflowAppsFeatures.cs:496-508) → existence-oracle leak; mutation itself fail-closed → post-golive hardening only. Tag [s56, pre-golive-verify, authz-clean, gotcha44-clean, notfound-before-forbidden-minor]. -
2026-06-10 (S57-resume Harness-4 two-tier adopt gate — PASS-with-fixes, 0 blocker): Gate trước send-email + commit (governance, không product code). Self-report spawn:
claude-fable-5[1m](reviewer = promote-list inherit → direct promote-tier evidence, em main cite được). Independent re-verify ALL GREEN: grep frontmatter = đúng 7 pinclaude-opus-4-8+ 4inherit+ 0[1m]-in-frontmatter (2 body-text hits hợp lệ: database-agent.md:46 + README.md:9 MỚI — adap-report "match duy nhất" stale-by-own-edit) + 0 project-pin settings. Evidence track-record 8/8 REAL vs HANDOFF/STATUS/own-memory (S51 MAJOR · S54 QTV-decoy · S53 Mig46 · S56 H2-4.5/5 + dept-IT-0-user · S57 ×3 controller +5/+5/+5[Authorize(Roles="Admin,CatalogManager")]working-tree). Nấc G-011 đúng mọi chỗ load-bearing (demote = executed-file·pending-restart, 0 overclaim runtime). Fixes: hash PLACEHOLDER trước send (nac: sent+ "SENT ✓" premature = đúng status-verb class broadcast cảnh báo) · STATUS "(runtime resolve 1M)" thiếu attribution AI_INFRA-s20 · hmw.js:91 log "same-model inherit" stale + :9 "8-agent" vs 9 roles · adap-report "(13)" vs "11" count · invalid-role typo → rơi 'opus' (fail-direction xuống vs H4.5 nghiêng-quality). Learned: gate adopt-governance = re-run MỌI grep claim + cross-check evidence vs HANDOFF nguyên văn; n=2 demoted spawn-test double-duty làm inherit-chain proof là HỢP LỆ (registry cached = chạy config cũ) nhưng cần phrase rõ kẻo đọc nhầm thành promote-list spawn-test. Tag [s57, harness-4, two-tier-gate, pre-send-gate, g011].
--- (S71 curate 2026-06-18 — moved from L1 MEMORY.md: oldest FIFO tail S33→S49 + die-meta S57bis/S60 + redundant bottom Harness-10 R2/R3. Byte-exact, additive-only.) ---
-
2026-05-26 (S33 Plan B G-H1 Phase 2 pre-commit — PASS, Smart Friend 6× CLEAN): 17 file (3 BE + 6 FE new + 6 mod + 2). SHA256 mirror 3 file IDENTICAL admin==user. 5 endpoint real mediator.Send 0 mock. Mig 34
AddEmployeeProfiles7 table UNIQUE indexes + FK Cascade. SeedDemoEmployeeProfiles NOT gated DemoSeed (gotcha #51 ✓). gotcha #50 Layout staticMap mirror ✓. 3 MINOR defer: EmployeeCode race SERIALIZABLE low-risk · Update 3 bool not nullable (partial reset) · Delete DateTime.UtcNow direct. Verdict PASS. Tag[s33, hrm-mig34, smart-friend-6x]. -
2026-05-28 (S35 G-H2 BE CRUD 16 endpoint pre-commit — PASS, Smart Friend 8× CLEAN): 2 NEW file
HrmConfigFeatures.cs439 + Controller 137. build clean, 130/130 PASS. Cat1: 0 mock, 8 ConflictException (Holiday Update composite(Year,Date)BOTH fields). Cat3: class[Authorize]+ 12 per-action[Authorize(Roles="Admin")]. Cat5: 8 Validator MaxLength MATCH EF source (Code=50 not spec 20). 2 MINOR defer: ListHolidays no IsActive filter (inconsistent sibling) · OtPolicy "1 active unique" NOT enforced handler (G-P1 ambiguous nếu 2+ active). Verdict PASS. Tag[s35, smart-friend-8x-clean]. -
2026-05-30 (S43 P11-B LeaveBalance pre-commit — PASS, Max no-truncate): 14 file (LeaveBalance entity+config+Mig42 + Features + Controller + deduction hook + Create/Update LeaveType guard + embed balance + FE×4 + tests). 154 PASS (130→154). Deduction exactly-once VERIFIED (terminal else only, guard Status!=DaGuiDuyet chặn re-approve; advance/reject/return no-deduct). FK invariant fully closed — grep 2 write site LeaveTypeId (Create + UpdateDraft) cả 2 guard AnyAsync→Conflict, bogus type không thể tới terminal FK insert. Embed balance = RequesterUserId (approver thấy đúng người tạo). admin
[Authorize(Roles=Admin)]. 2 MINOR defer: concurrency lost-update UsedDays (no RowVersion — human-sequential accept) · stale line-num comment. Verdict PASS. Tag[s43, p11b-leavebalance, max-clean]. -
2026-06-07 (S49 Harness 1/2/3 adopt pre-commit — PASS all 3, no blocker): Governance/infra adopt (no product code, no test impact). VERIFIED: H1/H2 = 2 sub scope-DISJOINT + tools
[Read,Grep,Glob,Bash+4RAG]NO store_memory/Write (INFORM-only); genuinely TAILORED not copy-paste (SE 4-RAG vs AI_INFRA 2-RAG · dropped effort:max + agent-ops-monitor/sister · Fidelity→SEreviewer). H2 5-trục in harvest-curator.md + session-end §L.b(f). H2 wave-mode hmw.js mirror AI_INFRA + B6git check-ignoreVERIFIED (wave-*/+agent-teams/ ignored · hmw.js/README tracked). H3 self=secomplete substitution · SHA256 canonical formula byte-identical send==check · 13 .gitkeep exact · adap-apply base-pathoutbox\all\. honest nấc executed-file/verified-runtime-PENDING. G-015 scan = 6 hits ALL negating ("KHÔNG enforced") = correct honesty. 1 MINOR (non-block): README:11/18 "7-agent" ASCII diagram = PRE-EXISTING drift (git diff proved work này chỉ touch load-bearing title/decision-tree/tool-grant/matrix; diagram predates S47 frontend-designer) → tooling-auditor H1 designed-to-catch = self-validating adoption. learned:git diff base..head= discriminator introduced-defect vs pre-existing-drift (đừng đổ lỗi work mới cho drift cũ); name-collision tailor-verify = diff frontmatter AI_INFRA-canonical vs SE-instance. surprise: mojibake scan false-pos trên "ĐÃ" (U+00C3 = valid VN uppercase, KHÔNG double-encode → verify codepoint in-context trước flag); broadcast floor "12 .gitkeep" UNDERCOUNT (correct=13 inclall/adap-channel — em main đúng). Verdict PASS, safe commit + restart. Tag [s49, harness-adopt, governance, max-clean]. -
Smart Friend cumulative 8× CLEAN: (1) S22 #44 silent-403 · (2) S25 #48 SQLite tie-break · (3) S29 password ≥12 · (4) S29 ApplicableType cross-module · (5) S33 BW test · (6) S33 Plan B Phase 2 · (7) S35 FE forms · (8) S35 G-H2. Plus 9× G-O2 (S36, em không track ở đây). 2 MAJOR catches total (S29 password + S29 ApplicableType); rest clean với MINOR defer.
-
Archived S29-S33 detail + S32 startup →
archive/2026-05-q2.md+ gitd2f52ba(S40 curate): S33 Plan C B-Wrap 9/9 [Fact] verify · S33 startup drift audit (CLAUDE.md SEVERE → patched S40) · S32 wrap/startup standby · S29 wrap 2 MAJOR catch detail. KEY absorbed in bug patterns + Smart Friend cumulative above. -
2026-06-11 (S57bis product gate — KHÔNG DELIVER, die-0-byte ×2, on-behalf em main ghi hộ, H2-proposed): Cả 2 spawn (email-gate đầu + final gate) chết 0-byte output 0 return (resume-kill class #3, ref
feedback_agent_kill_recovery) → em main SELF-GATE evidence-checklist: grep authz key-set + role-string vs AppRoles + Mig 49 Up/Down reversible + 240 test + Run #381 + prod smoke 401/404-control. LEARNED: output-file size=0 + im >5 phút = chết, KHÔNG đợi thêm; KHÔNG re-spawn >2 lần trong session có--resume. SURPRISE: khác S52 killed-with-partial — lần này 0-byte tuyệt đối (không gì recover được từ return). Tag[s57bis, die-0-byte-x2, self-gate, on-behalf]. -
2026-06-12 (S60 đợt1 PE submit-guard + drafter-bypass gate — KHÔNG DELIVER, die mid-run, on-behalf em main ghi hộ, H2-proposed): Task: review
37122f0cross-stack (BE TransitionAsync submit-guard đủ-4-thông-tin mục 3 + bypass người-soạn-trong-chuỗi V2 BƯỚC-ĐẦU-only + FE PeDetailTabs ×2 + 14 PeSubmitGuardAndBypassTests 240→254). Die mid-run #53-class (commit body tự khai "Reviewer die mid-run → em main self-gate evidence-checklist PASS 0 blocker") → ship Run #283 PASS prod-verified, bundle rotate both. LEARNED: self-gate em main đứng vững lần 2 (sau S57bis) — checklist deterministic (test gate + diff scope + prod smoke 401/404-control) đủ cho PE refinement cross-stack. SURPRISE: die lần 3 trong 2 ngày (S57bis die-0-byte ×2 + S60 mid-run) DÙ promote-tier inherit Fable 5 → model-tier KHÔNG phải nguyên nhân die (nghi resume-kill/harness class) — trend data cho Harness-4. Tag[s60, die-mid-run-3rd, self-gate, on-behalf]. -
2026-06-18 (Harness-10 adap R2-lens hmw.js ENGINE integrity — CONCERN, confirms sibling L1 over-claim still live, pre-commit): Lens = hmw.js engine integrity (em-main rename wave→run-trace). Engine itself CLEAN — all 4 R2 checks PASS: (1) structure valid —
const wave=(A.run&&A.run.dir)?A.run:((A.wave&&A.wave.dir)?A.wave:null):91 nested-ternary paren-balanced 3/3, accepts args.run primary + args.wave alias (additive, old callers OK), varwaveinternal-name kept consistent :91/:92/:95/:103/:107/:132; subMd path :103${wave.dir}/sub-md/${role||'task'}-${i}.mdmatches spec; template-literals balanced (backtick 54 EVEN all-escaped, brace 56/56, paren 140/140, bracket 14/14). (2) zero operative WAVE-MODE — grepWAVE-MODE=0; all 6 wave refs contextualized (legacy-alias :19/:90/:91, "supersedes Harness 2 wave" :87/:109); :113 ISOLATION contains "tracked-change NGOÀI run-folder (runs//)+code-disjoint=vi-phạm" ✓. (3) fan-out logic UNCHANGED —git diff -U0hunks = ONLY :91 behavioral (alias-accept); resolveModel/SCHEMA/checkpointApproved-guard/parallel/results.filter untouched. (4) valid JS (balance + structural, NO node --check per top-level-await). THE CATCH (CONCERN, intersects R2): runs/README.md:51 documents L1 in-run-reminder as firing in "hmw.jsprompt-builder" w/ exact text 'run đang OPEN—nhớ scaffold@P1'+'run trước OPEN-beat đã harvest chưa' → grep that in hmw.js = 0. hmw.js writeGuard :114 emits ONLY C4 return-instruction ("Harvest per-turn primary (C4)..."), NO scaffold/OPEN orchestrator-reminder; :92 is a log() at mode-detect not prompt-injection + still lacks the promised text. Plan-vs-applied gap proven: invest-synthesis:17 PLANNED "C5 Layer1: thêm reminder vào prompt-builder"; implement-synthesis NEVER lists applying L1 to hmw.js (only L2 :71 + L3 :51 applied); yet README:51+C7:72 present L1 as live. Doc asserts engine-behavior grep proves absent = over-claim. Sibling reviewer (same adap, prior run today) already CONCERN on this exact gap — I independently re-confirm UNFIXED. Cross-file PASS: gitignore runs/ TRACKED via!.claude/**:83 (check-ignore -v confirms negation) + wave-*/ kept IGNORED; containment wording synced 4 files (_ledger:4↔hmw:89/113↔runs/README:78); frozen evidence (broadcasts/adap-harness-2/error-ledger/STATUS/HANDOFF/archive_INDEX/sessions) ALL empty-diff; 0 mojibake. Residual (non-block, self-flagged): investigator-codebase/MEMORY.md +6 (29819B ~just-under-cap) = 4 same-role INVEST agents race (concurrency risk #7 invest-synthesis flagged) → em-main reconcile @closeout; new :113 guard forbids sub agent-memory writes = prevents recurrence. Learned: narrow lens (hmw.js JS structure) ≠ excuse to wave a doc-asserts-engine over-claim — when README says a layer "fires in ", grep the engine for the CLAIMED text not a sibling instruction; INVEST-plan ≠ IMPLEMENT-applied. Surprise: engine rename genuinely flawless (dual-alias/balance/logic-frozen) — ONLY defect is adjacent doc over-stating what the clean engine does; engine-perfect + doc-overclaim coexist in one adap. Smart-Friend held: did NOT downgrade to PASS despite narrow lens + clean engine + sibling already-flagged. Tag [harness10, r2-hmwjs-engine, engine-clean-doc-overclaim, c5-L1-overclaim-reconfirm, plan-vs-applied-gap, dual-alias-additive]. -
2026-06-18 (Harness-10 adap run-trace folder R3-floor review — CONCERN, 1 over-claim, pre-commit): Reviewed adap thay wave-mode →
runs/<run-id>/3-part (run.md+sub-md/+harvest/) git-TRACKED. Floor C1-C8 disk-verified. C1/C2 PASS — all 3 runs (invest/implement/review) scaffolded full 3-part (lsconfirm + .gitkeep placeholders). C3 PASS correct-nấc (NO over-claim) —git check-ignore runs/=NOT-IGNORED (tracked-eligible via!.claude/**:83) ANDgit ls-files runs/=EMPTY=NOT-committed-yet; _ledger:4 + runs/README:80 + gitignore:89-99 document "tracked" correctly, NEVER falsely claim "committed". Nấc THẬT = tracked-ELIGIBLE pre-commit (must commit to realize — expected, not defect). C4 PASS — invest+implement synthesis present per-turn; review harvest empty=correct (in-progress). C5 CONCERN (the catch) — L2 (session-start:71 orphan scanclosed=⏳+harvest-rỗng) + L3 (session-end:51 idempotent VERIFY-not-re-APPEND) genuinely wired. BUT L1 OVER-CLAIM: runs/README:51 documents L1 in-run reminder firing in "hmw.js prompt-builder" w/ exact text 'run đang OPEN—nhớ scaffold@P1'+'run trước...harvest chưa' →grep -cthat text in hmw.js = 0. hmw.js writeGuard only emits C4 return-instruction ("Harvest per-turn primary (C4)"), NO scaffold/OPEN reminder. INVEST planned it ("C5 Layer1: thêm reminder vào prompt-builder"), IMPLEMENT synthesis never mentions applying it, yet runs/README:51+C7:72 present L1 as live. Doc-vs-reality gap = over-claim. C6 PASS — _ledger OPEN+CLOSE beats (invest/implement CLOSED, review ⏳) + orphan def:3. C7 PASS — caveat genuinely honest (engine no-fs · C2 fragile · 3-layer=lưới-không-khóa · G-015 TRACKED≠read-only-enforced); strong. C8 PASS — wave→runs migration done (0 wave-/ remain), wave-/ kept IGNORED (verified). Frozen evidence 0-byte-loss CONFIRMED (broadcasts/·adap-harness-2·error-ledger·STATUS·HANDOFF all empty-diff vs HEAD). hmw.jsnode --check=OK, dual-alias A.run/A.wave intact. Containment wording synced 4 files (_ledger:4↔hmw:113↔workflows/README:38↔runs/README:78). Learned: for a multi-layer "anti-miss net" adap, the catch is grepping each layer's CLAIMED trigger-site against the actual engine file — a layer documented as "fires in hmw.js prompt-builder" must have backing text there, not just a sibling instruction; INVEST-plan ≠ IMPLEMENT-applied (cross-check synthesis-plan vs disk). Surprise: README's own C1-C7 section-numbering ≠ task's C1-C8 reviewer-axes (two schemes, NOT a defect — README documents convention, task axes evaluate it); don't conflate. Over-claim=CONCERN per task rule (would be PASS if README:51 softened L1 to "C4 return-instruction" matching reality, OR hmw.js actually added the scaffold reminder). Tag [harness10, run-trace-folder, c5-L1-overclaim, tracked-not-committed-correct-nac, frozen-evidence-clean, plan-vs-applied-gap].
-
2026-06-19 (S76 Part2+3 PE budget-edit BADGE display-only — Lens-2 FE badges+render-safety, PASS, 0 blocker): 2 cờ bool CanEditProBudget/CanEditCcmBudget vào 2 DTO approver (AwLevelDto designer + PurchaseEvaluationApprovalLevelApproverDto PE-flow), suy từ ROLE (KHÔNG đổi authz). Badge "✎ NS PRO" (amber) / "✎ NS CCM" (sky). PASS Lens-2: (1) role-set KHỚP gate —
proBudgetEditors=GetUsersInRoleAsync(Procurement)∪Admin/ccmBudgetEditors=CostControl∪Admin(ApprovalWorkflowV2AdminFeatures.cs:155-157 + PurchaseEvaluationFeatures.cs:976-978) đảo-chiều set-lookup 3-query/req no-N+1; khớp 1:1 gate thật canEditPro/canEditCcm:800-801=isAdmin‖Procurement / isAdmin‖CostControl; AppRoles consts verified (AppRoles.cs:5,9,10). (2) role-set DEFINE trước cả 2 site — PE-flow define:978< approvalFlow:1045< currentApproval:1064(cùng V2-branch scope); designer define:155< ToDto:184. (3) DTO flag flows CẢ flow+current —PurchaseEvaluationApprovalFlowLevelDto.Approvers(:152) +PurchaseEvaluationCurrentApprovalDto-path (:145) đều dùngPurchaseEvaluationApprovalLevelApproverDto→ badge hiện ở Panel-flow lẫn current. (4) PeWorkflowPanel.join('/')→mapgiữ separator "/" ({i>0 && <span>/</span>}), giữ case rỗnglength===0?'(chưa cấu hình)', key=a.userIdSAFE (validatorHaveNoDuplicateApproverInSameLevel:289chặn dup ApproverUserId trong 1 level → no React key-collision). (5) render-safety — designer wrapperflex→flex flex-wrap+ panelflex flex-wrap gap-x-1 gap-y-0.5→ badge KHÔNG vỡ layout khi nhiều approver/badge. (6) mirror 2-app PERFECT — PeWorkflowPanel fe-admin==fe-user byte-IDENTICAL cả base lẫn now (diff empty); types +2 cờ ở CẢ 2 app (admin:235-236/user:238-239), block diff-identical (chỉ pre-existing inline-comment//0-basedlệch = noise KHÔNG do change này). (7) FE typecheck CLEAN cả 2 app (tsc --noEmitexit 0). no mock/alert/TODO. ⚠️ SPEC-vs-DIFF MISMATCH (em-main framing wrong, NOT a code bug): spec nói "KHÔNG migration" nhưng diff BUNDLES S76 Part1 (migrationAddProBudgetSplitToPeWorkItemBudget+ PeWorkItemBudget domain + PeBudgetSummaryDto + PeDetailTabs 306-LOC matrix rewrite WIRED PUT /budget/pro). Part1 spot-check sound (mig 3-file OK pure-ASCII gotcha#30-respect, PUT wired real not-mock). 🟡 MAJOR race STILL PRESENT (carry-over Part1, line 64 entry): PeDetailTabs 2 PRO cell (:1283proInitial +:1305proAdjust) cùngproMut.mutateecho sibling từbsserver-snapshot,invalidate()fire-and-forget (:1161không await) → double-Save<refetch wipes sibling. Sev MAJOR data-loss tiềm ẩn, prob thấp. LEARNED: display-only capability-flag review = (a) confirm flag-compute KHỚP gate-thật bit-for-bit (đảo-chiều set-lookup must mirror the forward Roles.Contains check) + (b) confirm DEFINE-before-all-consumer-site trong cùng scope + (c) which DTO carries flag determines which UI surface shows badge (flow vs current — both here). For.join→maprefactor verify separator+empty-case preserved + key-uniqueness backed by a BE validator. SURPRISE: adversarial value here = catching the spec's "KHÔNG migration" claim is FALSE (diff is combined Part1+2+3) — don't trust em-main's scope framing, read the actual changed-set. Tag [s76-part23, pe-budget-badge, display-only-capability-flag, role-set-mirrors-gate, join-to-map-separator-preserved, key-uniqueness-validator-backed, mirror-2app-byte-identical, spec-vs-diff-mismatch, major-race-carryover]. -
2026-06-19 (S76 Part1 PE budget MA-TRẬN 3 cột Mig 56 — uncommitted, PASS w/ 1 MAJOR race + 2 MINOR): Form ngân sách 1-cột→ma-trận [Dự án|PRO|CCM]. Entity +ProInitialAmount/+ProAdjustmentAmount (cột PRO mirror CCM Initial/Adjustment); ProEstimateAmount→LEGACY, Mig 56 Sql() UPDATE migrate idempotent (
WHERE ProEstimate NOT NULL AND ProInitial NULL— chạy-1-lần-safe). PASS: authz fail-closed Forbidden TRƯỚC side-effect 2 handler (PRO=Admin‖Procurement:90, CCM=Admin‖CostControl:160; Admin nhập cả 2 đúng ý; neither-role blocked); computefullAmount=CCM nếu hasCcm else proFull(ProInit+ProAdj):852, migrate-value flow đúng (legacy ProEstimate→ProInitial→hasPro=true→fullAmount);fullIsEstimate!hasCcm→!hasCcm&&hasPro(improve, no badge khi empty); DTO 17-arg positional khớp def (2 new appended last, build-PASS compiler-checked); Block B 9-row dùngfullauthoritative INTACT; Mig 3-file OK (.cs+Designer+snapshot 18,2); FE 2-app SHA-twina93c8aa0; 32 PeWorkItemBudget tests PASS (+5 S76: set-both-neg, validator neg-initial-fail/neg-adjust-pass, full-proFull-150, neg-proAdjust-70); no mock; anti-fiddle clean. 🟡 MAJOR race (pre-existing pattern, S76 WORSENS): BudgetCell cross-field echo từbs(server snapshot) KHÔNG local-state — PRO "Ban hành" save gửiproAdjustmentAmount: bs.proAdjustmentAmount, "V0" save gửiproInitialAmount: bs.proInitialAmount.invalidate()fire-and-forget (:1170không await refetch). 2 PRO cell nay đồng-cột → click Save cell-2 trong window [onSuccess fires (isPending→false, btn re-enabled) → refetch lands] đè cell-1 về STALE (vd: lưu Ban-hành=100 → ngay lưu V0=50 trước refetch → Ban-hành WIPED null). Trước S76 PRO chỉ 1 số nên window này vô hại; ma-trận 2-cột-PRO làm reachable. Sev MAJOR (data-loss tài-chính tiềm ẩn) nhưng prob thấp (cần double-click <refetch-latency); fix gợi-ý: disable sibling-cell Save khi mut.isPending HOẶC onMutate optimistic-merge HOẶC await invalidate. 🔵 MINOR: (a)parseVndstrip.→ "1.5"→15 (input cho.nhưng VND whole-number nên harmless); (b) strayfe-user/.claude/agent-memory/implementer-frontend/MEMORY.mdNOT-gitignored (cwd-misland gotcha) → em main ĐỪNGgit add -A. LEARNED: cross-field echo-from-server an-toàn khi 1-field/cột; thành race khi N-field cùng-cột share 1 mutation + fire-and-forget invalidate — window mở SAU isPending=false (btn enable) chứ không phải lúc in-flight; load-bearing = đếm field-cùng-cột share mutation + check invalidate awaited. Tag [s76, pe-budget-matrix, mig56-migrate-idempotent, cross-field-echo-race-worsened, fullIsEstimate-improve, cwd-misland-stray, parseVnd-dot-minor]. -
2026-06-18 (S72ter-WIRE Mig 54 cross-stack-wire + verify-fix lane — uncommitted priceMissing, PASS no-new-deadlock): Complement to S72ter-AUTHZ below (same fix, deadlock-lens). Fix =
priceMissingoldlength>0 && !source→ new(length===0 || !source), 2-app SHA-twin4d6c89d9. No new deadlock — 4-fact: (1) fix CHỈ THÊM disable-condlength===0lên branch đã unreachable-by-invariant (submit-guard:194hard-block winnerQuoteTotal<=0 ALL-paths → Ncc candidate luôn ≥1 ở ChoDuyet) ⇒ không sinh lockout mới; (2) có giá→chọn→source set→priceMissing=false→nút "Xác nhận" mở→duyệt OK; (3) empty (giả định)→nút khoá + amber:537"nhập PRO/CCM hoặc chọn NCC" = lối-thoát RÕ, setter-path KHÔNG phase-gated (mirror-budget) cho nhập giá bất kỳ lúc→candidate xuất hiện→mở lại (no hard-lock); (4) intermediate-approveshouldPickPrice=false(chỉcurrentIsFinalApprover||finalizeByCcm)→nút mở bình thường, khớp BE ApplyApprovedPrice chỉ terminal:885+CCM-deleg:853(intermediate advance:870/:893không gọi). 7-layer threading 0-drop re-confirm (ctrl:129/:337→cmd:462→handler:515→iface:30→svc:47→ApproveV2:822). OR-of-NcurrentIsFinalApprovertrue mọi viewer cấp cuối (ComputeLevelStatus:987position-based) nhưng nút dialogdisabled=blockedByV2Level+!isDisabled&&setTarget:310/:320→non-approver không mở→price-selector vô hại. LEARNED: "fix tạo deadlock?" = THÊM-disable lên branch-unreachable-by-invariant không thể sinh lockout; verify lối-thoát = setter KHÔNG phase-gated (giá nhập-được bất kỳ lúc) + amber-message ⇒ user luôn thoát empty-state. Tag [s72ter-wire, verify-fix, no-new-deadlock, escape-hatch-amber, 7layer-0drop]. -
2026-06-18 (S72ter Mig 54 AUTHZ+SECURITY lane double-check — uncommitted priceMissing FE-fix + committed
1d86abcre-verify, PASS, 0 issue): anh giao 3 lane laser (a setters / b CCM-finalize bypass / c controller authz) on commit1d86abc(deployed Run #313) + 1 uncommitted FE-fix. Uncommitted diff = 2 LOC product only (priceMissingboth apps, SHA-identical4d6c89d9) + memory/ledger noise — em main đúng kỷ luật chỉ-touch-2-file. (a) PASS —PeSuggestedPriceFeatures.cscả 2 setter ForbiddenException TRƯỚC mọi mutate+SaveChanges (load+NotFound→role-gate:40-41/:109-110→mutate); role đúng PRO=Admin‖Procurement, CCM=Admin‖CostControl; AppRoles consts tồn tại (:5,9,10). Phase-guard cố-tình-thiếu, documented mirror-budget S61 (non-regression). (b) PASS no-bypass — 3 gate trực-giao chặn non-CostControl finalize-bỏ-CEO, TẤT CẢ throw TRƯỚCPhase=DaDuyet(:854): (1) approver-match:702-713non-admin phải ∈ pendingLevel.ApproverUserId else Forbidden → forged-caller-not-at-level KHÔNG tới được finalize block; (2)finalizeByCcmDelegation:830-851threshold-null→Conflict / role≠CostControl→Forbidden /winnerQuoteTotal>=ceoThresholdstrict-<→Conflict — 3 throw trước set; (3) blockreturnno-fallthrough.winnerQuoteTotalrecompute server-side từ Suppliers+Quotes.ThanhTien của SelectedSupplier (:839-847) KHÔNG trust client; threshold từ DBaw.CeoApprovalThreshold. skipToFinal+finalizeByCcm combo safe (skipToFinal:818return non-last-slot HOẶC:797no-op fall-through last-slot → finalize once, 3 guard vẫn áp). (c) PASS — class[Authorize]:14→ 2 endpoint mới inherit any-auth, fine-grained ở handler Forbidden (gotcha#44-safe KHÔNG class-Policy-overstrict). FE-fix sound strict-tightening: oldlength>0 && !sourceđể nút ENABLED khi candidates-empty → click → BE Conflict "Chọn 1 giá chốt"; new(length===0 || !source)disable nút khớp amber empty-state:537(trước fix message-hiện-cùng-nút-enabled = UX mâu thuẫn).winnerQuoteTotal:numbernon-null → candidates-non-empty thực tế (submit-guard >0), fix thuần defensive nhưng đúng. LEARNED: "finalize-bypass?" load-bearing proof = đếm guard giữa caller-entry và state-mutation + xác nhận MỖI guard throw TRƯỚC mutation đầu tiên (đây Phase=DaDuyet) + recompute-vs-trust-client của giá-trị-so-ngưỡng (winnerQuoteTotal server-Sum, không nhận body) → 3 gate độc lập (approver-match ∩ role ∩ amount<threshold) mạnh hơn 1; client chỉ chọn-source-label, BE tự tính amount-vs-threshold. SURPRISE: uncommitted-fix chỉ là edge defensive (candidates thực tế luôn ≥1 do submit-guard) nhưng vẫn đáng — nó xoá UX-mâu-thuẫn enabled-button-cùng-amber-empty + chống regression nếu submit-guard nới sau này. Tag [s72ter, mig54-authz-lane, finalize-bypass-3gate-proof, server-recompute-not-trust-client, fe-fix-strict-tighten, phase9-uat-pass]. -
2026-06-18 (S72 Q2 phản-biện CONFIRM finding isReal=false / not-an-issue — Mig 54 isSystem-exempt dead-branch): anh giao bác-bỏ finding "isSystem miễn-chọn-giá AN TOÀN/dead-code". Cố refute ×3 angle, KHÔNG bác được → finding ĐÚNG mọi điểm. (1)
IPurchaseEvaluationWorkflowService.TransitionAsync= DUY NHẤT 1 caller backend-wide (PurchaseEvaluationFeatures.cs:505human handler, throws Unauthorized nếu UserId null:499, passcurrentUser.UserIdnon-null:508) →isSystem(:54cần actorUserId null) LUÔN false trên PE-path. (2)SlaExpiryJob:63injectIContractWorkflowService(KHÔNG PE) + querydb.Contractsonly → caller AutoApprove duy nhất KHÔNG chạm PE. (3)ApplyApprovedPriceOnFinalize(:908) chỉ gọi từApproveV2Async(:853,885) reachable chỉ qua approve-block:243gatedecision==Approve, còn isSystem cầnAutoApprove→ mutually-exclusive (lý do độc lập #2). (4) KHÔNG PE SLA hosted-service (chỉ SlaExpiryJob/Contract + ItTicketSlaJob). (5) non-admin AutoApprove tới ChoDuyet →throw Conflict :275(no alt-finalize bypass price). Test headerPeApprovedPriceFinalizeTests.cs:27-31tự-ghi OBSERVATION report-em-main KHÔNG-fix + reflection-invoke isolation. Finding line# lệch (cite 911-916/SlaExpiryJob 76,100, actual 908-923) nhưng substance khớp. Dead-branch harmless defensive-return. LEARNED: "dead-code an-toàn?" load-bearing proof = đếm CALLER của method chứa branch (grep\.TransitionAsync\(+ verify mỗi caller's service-TYPE qua DI) — single-human-caller + actorUserId-non-null-invariant kills isSystem độc lập với decision-gate; 2 lý do trực-giao mạnh hơn 1. Tag [s72, q2-phan-bien, isSystem-dead-branch, single-caller-proof, not-an-issue, confirm-finding]. -
2026-06-18 (S72bis Mig 54 RE-REVIEW commit
1d86abc— anh 4 regression-Q a/b/c/d focus, PASS, 0 finding): Independent 2nd pass on SAME commit as S72 entry below, anh asked 4 targeted Qs. (a) AUTO→OPT-IN regression — in-flight + V1 SAFE: S69 auto-finalize block REMOVED; in-flight V2 phiếu mid-ChoDuyet carry NO new flags (come from NEW request not stored state) → intermediate levels advance normal (no ApplyApprovedPrice call line 870/894), terminal calls ApplyApprovedPrice → final approver picks price (candidate guaranteed exist, see d). CCM below-threshold WITHOUT tick now advances to CEO (intended safer, no strand). V1 legacyApproveV1LegacyAsyncsignature does NOT receive new params + terminal line~990 does NOT call ApplyApprovedPrice → V1 finalize 100% unchanged, ApprovedPrice* stays null per entity comment. Tests cover: NoFlag→advance-CEO, AtLastSlot-no-double, all 3 fail-closed guards throw-before-mutate. (b) Mig 54 safe: 5-col additive-nullable, 0 backfill, 0 lock (AddColumn nullable=metadata-only SQL Server no rebuild); Down() drops all 5 reversible; 3-file rule OK (.cs+Designer+snapshot all 5 cols); EF config HasPrecision(18,2)×4 + HasMaxLength(20) match migration types. (c) DTO positional OK: 7 fields inserted between CeoApprovalThreshold↔ApprovalWorkflowId in BOTH record def + construction, same order (ProMin/ProMax/Ccm/ApprovedAmount/ApprovedSource/canEditPro/canEditCcm), types match (5 nullable + 2 bool); build-PASS confirms compiler-checked positional. (d) NO deadlock — decisive: submit-guardPurchaseEvaluationWorkflowService.cs:174-216enforces (ALL paths incl Admin/system) winnerQuoteTotal>0 (line194 "chưa có giá chào thầu" if<=0) → Ncc candidate{amount:winnerQuoteTotal}ALWAYS present+positive at final approval →priceCandidates.length>=1always → amber "Chưa có giá nào" (length===0) is DEAD UI unreachable → human always can pick ≥Ncc → priceMissing disables btn til pick → BE never gets null human-path → no Conflict-loop.winnerQuoteTotalBESum()over empty=0m (never null), FE typenumbernon-null. OR-of-N currentIsFinalApprover =lastFlowLevel.status==='Current'true for EVERY viewer at last level (position-based BE ComputeLevelStatus:987), BUT approve buttonsdisabled=blockedByV2Level+ onClick!isDisabled&&setTarget(line310-321) → non-approver can't open dialog → price-selector-for-all harmless. FE mirror: PeWorkflowPanel+PeDetailTabs byte-identical 2 apps (hash df2975a/ab08dad); type files differ pre-existing BUT Mig54 fields diff-identical. Setter handlers fail-closed Forbidden-before-side-effect, PRO Min<=Max validator, NO phase-guard (documented intentional mirror-budget S61). LEARNED: for "deadlock?" Q the load-bearing proof is tracing the SUBMIT-guard invariant (winnerQuoteTotal>0) forward to the finalize candidate-set — the FE dead-UI branch (length===0) is provably unreachable BECAUSE submit already rejected zero-price phiếu; never assess FE button-enable in isolation. SURPRISE: isSystem-exempt in ApplyApprovedPrice = dead via public ApproveV2Async (needs decision==AutoApprove + PE has no SLA-job) — test-specialist self-flagged OBSERVATION header, honest. Tag [s72bis, mig54-reReview, regression-Q-abcd, submit-guard-invariant-forward-trace, no-deadlock-proof, v1-untouched, or-of-n-safe]. -
2026-06-18 (S72 Mig 54 PE giá-đề-xuất + CCM-finalize OPT-IN — financial go-live review, PASS, 0 blocker): Pre-commit uncommitted diff 17-file (+922/-102), DUYỆT TÀI CHÍNH go-live thứ Hai. 3 nhóm: ① giá đề xuất PRO(Min/Max)+CCM(1 giá) setter role-gate + người-duyệt-cuối chọn giá CHỐT (
ApplyApprovedPriceOnFinalize); ③ CCM-finalize ĐỔI AUTO(S69)→OPT-IN ô-tích-tayfinalizeByCcmDelegation. Threading 7-lớp KHỚP (body→Send→command→handler→interface→service→ApproveV2; controller:129+ TransitionPeBody:337-341+ command:462-465+ handler:515-517+ iface:30-34+ svc sig:47-49) — 0 lớp drop param (bẫy "F1+F2 wire fail 2 ngày" né). ③ fail-closed order verified (PurchaseEvaluationWorkflowService.cs:830-867): flag=false→skip finalize advance-CEO (test 1a, đổi-chính); flag=true check THEO THỨ TỰ threshold-null→Conflict(:832) / role≠CostControl→Forbidden(:835) /winnerQuoteTotal>=ceoThresholdstrict-<→Conflict(:849) TRƯỚC set DaDuyet — 0 lỗ CCM/khác bỏ CEO. ① ApplyApprovedPriceOnFinalize gọi CẢ 2 nhánh DaDuyet (terminal:885+ CCM-deleg:853); human null-giá→Conflict, isSystem miễn, source∈{Ncc,ProMin,ProMax,Ccm} whitelist. Setter authz (PeSuggestedPriceFeatures.cs) Forbidden fail-closed TRƯỚC side-effect, đúng role (Pro=Procurement:53, Ccm=CostControl:109, Admin cả 2). Cross-stack FE/BE field-name khớp camelCase (finalizeByCcmDelegation/approvedPriceAmount/approvedPriceSource). FE currentIsFinalApprover =lastFlowLevel.status==='Current'(BE ComputeLevelStatus:978-991= pointer==last) — OR-of-N: group cấp cuối 1 entry → "Current" cho mọi viewer NHƯNG nút mở-dialog disable bởiblockedByV2Level(:310,321) khi actor∉approvers → người-không-phải-cuối KHÔNG thấy bộ chọn. priceMissing disable Xác nhận đúng. Migration 3-file OK (Mig 54 additive-nullable, Designer 5 col, snapshot). Tests 334 PASS (45 Dom+289 Infra, +28: PeCcm 6→11 + PeApprovedPrice 10 + PeSuggestedSetter 13). 3 MINOR non-block: (a)ApplyApprovedPriceOnFinalizeTRUST clientamount— KHÔNG cross-check amount==stored-value-của-source (snapshot-semantic CHỦ ĐÍCH per comment; field display/audit-only, grep xác nhận KHÔNG drive Contract-from-PE value → low-sev); (b) edge winnerQuoteTotal==0 candidate amount=0 hợp lệ (submit-guard ép >0 nên unreachable thực tế); (c) strayfe-user/.claude/agent-memory/implementer-frontend/MEMORY.mdNOT-gitignored (sub-agent cwd-misland gotcha) — em main ĐỪNGgit add -A(chỉ add file cụ thể) + reconcile→canonical. LEARNED: combined-flag probe (skipToFinal+finalizeByCcmDelegation) SAFE — skipToFinalreturn(:818) trước finalize khi không-last-slot, last-slot no-op fall-through finalize-once (no double, guard vẫn full). For financial-approve review the 2 load-bearing proofs: (1) fail-closed guard order = throw TRƯỚC mọi set Phase=DaDuyet (reload-assert ChoDuyet trong test) + (2) trusted-client-amount chỉ MINOR khi field không feed downstream money (grep consumer = DTO-only). SURPRISE: isSystem-exempt branch trong ApplyApprovedPriceOnFinalize = defensive/dead qua public ApproveV2Async (approve-branch gate decision==Approve, isSystem cần AutoApprove; PE no SLA-job) — test-specialist tự ghi OBSERVATION header, honesty tốt. Tag [s72, mig54-pe-price, ccm-finalize-opt-in, fail-closed-order, trusted-client-amount-minor, currentIsFinalApprover-or-of-n, cwd-misland-stray, financial-golive-pass]. -
2026-06-18 (S71 FINALIZE double-check H9+H10+checklist — lens R3 cross-cutting+residuals, GAPS-FOUND, 3 completion-gap): anh giao "hoàn chỉnh lại TOÀN BỘ" (not just Part C). Verdict GAPS-FOUND (no defect/no-bug — all gaps are deferred-incompleteness anh now wants closed). PASS items: (1) Containment model đồng-bộ MỌI file — 4 owning (
_ledger.md:4/hmw.js:89,113/workflows/README:38/runs/README:78) + agents/README:162 + harvest-curator:52 + tooling-auditor + session-end/start ALL repoint Harness-10 "tracked-change NGOÀI run-folder+code-disjoint=vi-phạm". 0 file giữ old B6 operative. (agents/README:8 wave-mode = frozen 06-07 chronology, OK; harness_123 user-mem:13 = stale FE-ref noted below.) (2) Frozen-evidence INTACT —git diff --name-status f36aab8^..HEAD: broadcasts/_index.md additive-2-rows + 2 outbox NEW (A), 0 modify; harness-2 adap-report/error-ledger/pre-S70 sessions NOT in changed-set. (3) 3 h10 run-folder = run.md+harvest/ complete, sub-md/ only .gitkeep (EXPECTED — read-only subs scribed to harvest). ledger 2-beat all CLOSED, 0 orphan. (4) gitignore runs/=NOT-IGNORED, wave-*/agent-teams=IGNORED ✓. (5) email content_sha256 e5f09d57c22e MATCH body-lstrip, outward-VN full-grammar Cat-6 PASS. 🔴 3 COMPLETION-GAP (em-main fix to "hoàn chỉnh"): (G1 HIGH) over-cap curate-debt — reviewer/MEMORY.md 33782B (>30720 soft + >25600 auto-inject; spawn already truncated ~8KB HOT) + investigator-codebase 29819B (>25600). memory-budget.jsonmeasuredSTALE (reviewer 24795/inv 24052 = S70 snapshot) → re-runscripts/measure-agent-memory.ps1+ curate L1→L2 (additive, archive/ + _INDEX exist). (G2 MED) stale memory claims — Harness-9 user-mem line14 "cả 4 <25KB (đóng P1 curate-debt)" now FALSE post-S71; harness_123 user-mem:13 describes wave-mode as operative (superseded). (G3 MED) NO Harness-10 user-memory — biggest structural change (wave→tracked-runs + containment flip) has 0 feedback/project memory; 3 lessons uncaptured (engine-no-fs→em-main-scaffold-fragile · custom-workflow-needs-delta-guard-race · check-ignore-exit-trap). 2 MINOR-info: check-ignore exit-trap EXPLANATION imprecise in gitignore:96-98 + email#3 ("exit 0 for BOTH") — plaincheck-ignoreactually exits 1 for negation (only-v --no-indexgives 0-for-both); the recommended COMMAND still works correct → low-sev, email frozen. Learned: "complete the whole thing" audit must check budget.json measured_bytes vs DISK (snapshot drift re-accumulates after each over-cap session); honest-self-disclosure (STATUS+email both flag the over-cap) ≠ done — disclosure is what anh asks to CLOSE. surprise: I am ADDING to the very curate-debt I'm flagging (this entry pushes reviewer further over-cap) — G1 curate must run NOW. Tag [s71, finalize-r3, over-cap-curate-debt, stale-memory-claim, missing-h10-usermem, gaps-found]. -
2026-06-18 (S71 Harness-10 adap run-trace convention — Stage-3 REVIEW lens R1 frozen-evidence+containment, PASS, 0 blocker): Governance/infra-only (wave-folder→run-trace
.claude/workflows/runs/<run-id>/TRACKED). 10 modified (8 H10 + investigator MEMORY residual + CLAUDE.md pre-existing) + 1 untrackedruns/. NO product/test/csproj/package.json/migration → test baseline 306 untouched, deps N/A. Spec path trap: spec saidruns/...but actual.claude/workflows/runs/...(verify disk, không tin claim path). R1 verify ALL PASS: (1) Frozen-evidence 0-touch —git status --porcelainon broadcasts/** · adap-reports/2026-06-07-harness-2 · error-ledger · sessions/* · STATUS · HANDOFF ·*/archive/*ALL empty = none touched. (2) Containment wording đồng-bộ 4 chỗ —_ledger.md:4↔hmw.js:89/113↔workflows/README:38↔runs/README:78ALL = "tracked-change NGOÀI run-folder + code-disjoint = vi-phạm" (model thay Harness-2 B6 "mọi tracked = vi-phạm"). (3) gitignore exit-code-trap —check-ignore runs/.../run.md && echo IGNORED || echo NOT=NOT (re-included via:83 !.claude/**);wave-x/wave.md=IGNORED (legacy:93kept); trap-note PRESENT gitignore:96-98. No new ignore rule shadows runs/. residuals verified as-claimed: investigator MEMORY +6 (3 S71 diary, 29819B≈29.8KB over-cap, race artifact closeout); CLAUDE.md pure test-count 263→306 flush. hmw.jsnode --check=PARSE-OK,args.runw/ legacyargs.wavefallback:91,sub-md/subdir:103. harvest-curator DEDUP axis (sha/substring before APPEND); session-end idempotent VERIFY-not-re-APPEND; session-start orphan-scan. 6×.gitkeeppresent. 1 MINOR (non-block, actionable): runs/ currently UNTRACKED (git ls-filesempty,?? runs/) = tracked-ELIGIBLE not-yet-committed; docs say "TRACKED" = post-commit steady-state — em main MUSTgit add runs/in SAME commit else run-trace invisible to git-diff audit model depends on. Learned: "TRACKED" containment = 2-level — check-ignore NOT-IGNORED (eligible) vsgit ls-files(committed); model only works aftergit add. surprise: internal varconst wave = (A.run&&A.run.dir)?A.run:...keeps namewavebut readsA.runfirst — cosmetic-only, downstream identical (not bug). Verdict PASS — safe commit (git-add-runs/ caveat). Tag [s71, harness-10-runtrace, frozen-evidence-clean, containment-wording-4file-sync, gitignore-exit-trap, tracked-eligible-vs-committed]. -
2026-06-17 (S69 GOLIVE Văn phòng số public-all-roles authz — PASS, 0 blocker, gotcha #44-family CLEAN): 1-file BE-only DbInitializer.cs (+81, new
SeedAllRolesOfficeModulePermissionsAsync:2261 + call :2055 AFTER S65 HRM grant → AFTER revoke :2042). NOT deployed (static + Dev-DB review, build PASS). Near-exact mirror of S65 HRM method, ONLY delta =+CanCreate=true(HRM was read-only). 8 verify ALL PASS: (1) Ordering — grant call sits afterRevokeTemporarilyHiddenModulesAsync(:2042) + after S65 (:2048) → grant wins revoke. (2) Allow-list EXACTLY 16 Off keys — Off/Dashboard/DanhBa/PhongHop(+View+Book)/DeXuat(+List+Create+Inbox)/DonTu(+Leave+Ot+Travel)/DatXe/ItTicket; const names map correct values per MenuKeys.cs:99-120; NO PhongHopManage/AttendanceReport/ChamCong; array contains ZERO Hrm*/Personal/Pe*/Master key → no leak. (3) Upgrade-only correct — row exists→only flips CanRead/CanCreate false→true (if(!row.CanRead)+if(!row.CanCreate)), NEVER touches CanUpdate/CanDelete, never lowers; new row→read+create=true, update/delete=false (Permission.cs defaults false anyway). (4) 3 excluded keys STAY HIDDEN — decisive cascade check:Offis NOT one of the 4 inherit-roots in GetMyMenuTreeQuery (:56-59,:70-73,:80-83 = Contracts/Workflows/PE/PeWorkflows ONLY) → granting Off does NOT cascade to children; each Off child reads its OWNresolvedflags (:65, falls to false-tuple if no row); PhongHop_Manage(parent=Off_PhongHop:1830)/AttendanceReport(parent=Off:1845) not-in-list→revoke-false→filtered by HasAccess(:96); ChamCong re-parented to Personal(:1850/:1962) under hidden Personal root, not under Off, not granted→hidden. (5) Admin unharmed — MenuPermissionHandler:27 Admin bypass; Dev DB: all 18 Off rows belong to Admin already read+create=true → upgrade branch no-op. (6) No real write-path opened — KEY for golive: grep Controllers for Off menu keys = 0 matches; Office controllers gate writes by class-level[Authorize](any-auth, self-service create) + per-action[Authorize(Roles="Admin")]for true admin writes (MeetingRoomsController Create/Update/Delete=Manage-rooms :26/34/43, Attendances :37/42, LeaveBalances :23/28) — NOT by Off_*.Create policy. So broad CanCreate grant only drives FE menu+button (usePermission/PermissionGuard); API write-auth untouched, admin CRUD stays Admin-only regardless. (7) No migration — seed-logic only; all 16 keys in MenuKeys.All:157-161 (seeded). (8) Idempotent — 2nd run: rows already true→0 change; SaveChanges gatedif(added>0||upgraded>0). Dev DB baseline (307 perms,13 roles): 0 non-admin Off rows exist→method takes add-branch for 12 non-admin roles (creates 16 read+create rows each, 3 excluded never added). build Infrastructure 0err/0warn. 0 rogue write (only cicd-monitor/MEMORY.md noise, read-only respected). Learned: for a public-grant golive the load-bearing security proof is TWO-fold — (a) cascade-safety = confirm the granted root is NOT an inherit-root (else siblings leak, gotcha #44-family) AND trace excluded keys' ParentKey to a non-granted/hidden parent; (b) write-path-safety = grep that the broadly-granted menu key is NOT used as a controller[Authorize(Policy=)](here Office uses class[Authorize]+per-action Roles=Admin, so CanCreate is FE-only — granting it cannot escalate API writes). surprise: the "Manage rooms" admin function is double-protected — excluded from allow-list (menu hidden) AND its API is[Authorize(Roles=Admin)]; menu-hide alone would've been insufficient but the controller gate makes the broad grant safe even if a key had slipped. Verdict PASS — safe commit+deploy. Tag [s69, office-golive-authz, public-all-roles, inherit-root-no-cascade, off-not-policy-key-fe-only-grant, gotcha44-family-clean, admin-write-double-protected]. -
2026-06-17 (S69 Văn phòng số RE-SKIN static logic-preservation — PASS, 0 blocker): 10 pages presentation-only re-skin → PURO PageHeader/KpiCard + Hồ sơ-NS idiom (9 fe-user office + 1 fe-admin AttendanceReport). NOT built yet, fe-admin not mirrored (em main next). Strongest proof = exact API/queryKey diff OLD-vs-NEW byte-identical ALL 8 fe-user pages (grep
api\.(get|post|put|delete)+queryKey:[...]sorted -u, zero delta): proposals POST /submit + /{kind} · workflow-apps POST /{k}+/submit+PUT /workflow · meeting-bookings POST/DELETE+invalidate · it-tickets PUT /{id}/assign · directory/departments/attendance-report/excel-blob all UNCHANGED. Mutation side-effects (onSuccess/onError/invalidateQueries/setActionDialog/setComment/navigate) 1:1 (line-shift only). ProposalCreate validation!title.trim()throw + required + submit-disabled intact. AttendanceReport exportExcel blob (createObjectURL→a.download→click→revoke) intact. Cat2 orphans CLEAN: 0 unused import — flagged Users(=UsersIcon alias) + FormEvent/ReactNode (React.* namespace not named-import) + Accent(comment word) all FALSE-alarm verified. Cat3 shared-comp contract: PageHeader{eyebrow,title,subtitle,icon,accent,actions} + KpiCard{label,value,icon,accent,active,onClick} props all match real sig; KpiCard onClick wired to REAL filter state (ItTicketssetFilter/WorkflowAppsListsetStatusFilter/ProposalsList — driving actual client.filter()), InternalDirectory 2 KpiCards INTENTIONALLY inert (no onClick=presentational counts, matches comp design — NOT dummy). Shared comps + index.css NOT modified (git status -- ui/ + *.css EMPTY; sha256 identical fe-user==fe-admin per ls). Cat4 color-trap CLEAN: grep added lines for(teal|violet|amberx|greenx)-(200|300|400|800|900)= ZERO; index.css confirms accents ship only 50/100/500/600/700 (brand has full 50-900 so brand-800 valid); gotcha #66 — 0 gradient/dark-bg headings added (all headers on light surface use accent-ink text-brand-800/{accent}-700 via PageHeader). Cat1 mock-markers: 0 //Mock/alert/TODO-wire. Client-side filter additions (ItTickets filter/breached, WorkflowAppsList statusFilter useMemo) = presentation views over fetched items, NO new query/endpoint. 2 MINOR (non-block): (a) ProposalDetail status badge now renders TWICE — PageHeader actions slot + existing status-row (cosmetic dup, both presentation); (b) it-tickets/workflow-apps client-filter is view-only over apageSize:100/50first-page fetch (pre-existing pagination limit, re-skin doesn't worsen). Learned: for pure re-skin, the decisive logic-preservation proof isgrep api-call + queryKey sorted -uOLD-vs-NEW byte-equality across every page — faster + more rigorous than reading each hunk; orphan-import heuristic (body-occ<=1) flagsX as Yaliases +React.Xnamespace + comment-words as false-positives, always grep the actual usage line before flagging build-break. surprise: custom accent palettes (amberx/greenx/teal/violet) deliberately ship NO -800 stop so headings MUST use -700 (brand is the only -800-bearing accent) — a -800 on a non-brand accent = silent no-class Tailwind v4, the re-skin respected this everywhere. Verdict PASS — safe for em main to build+mirror. Tag [s69, office-reskin, presentation-only, api-querykey-byte-equal, color-trap-clean, kpicard-inert-vs-filter, gotcha66-clean]. -
2026-06-16 (S65 PE mục E HoSoLink review — em-main PROXY, PE-Workflow reviewer-stage died-empty): Review mục-E hyperlink render + HoSoLink BE wiring (
5a0aaa4). Reviewer-stage trong Workflowpe-hoso-link-rename-proreturn RỖNG → em main self-gate evidence: Detail DTOhoSoLinkpresent +nullbackward-compat phiếu thật (Run #293 GET 200); Create/Update +trailing-optionalHoSoLink=nullKHÔNG vỡ call-site (grep 0 manual ctor — KHÁC CreateDepartmentCommand #291 CS7036 vì positional-required vs trailing-optional); mirror fe-user==fe-admin SHA256 IDENTICAL (PeDetailTabs+PeWorkspaceCreateView); hyperlink<a target=_blank rel=noopener noreferrer>no reverse-tabnabbing; rename "Dự trù PRO"→"Ngân sách PRO" CHỈ display (giữ "Ghi chú từ PRO" + field-code). LEARNED: hyperlink free-text = no server-side XSS (render-as-href client-only); absolute-set Update (null=clear) chủ đích. SURPRISE: reviewer-stage chết-rỗng trong fan-out = lý do verify-heavy task vẫn cần em-main self-gate dù có Workflow (verdictfeedback_workflow_fanout_reliability). Tag[s65, pe-section-e-review, em-main-proxy-self-gate, hosolink-backward-compat, workflow-fanout]. -
2026-06-16 (S65 public Hồ sơ NS read for all roles — static pre-commit, PASS, 0 blocker, gotcha #44 family CLEAN): 1-file change DbInitializer.cs (+66, call-site :2046 SAU revoke :2040 + new
SeedAllRolesHrmProfileReadPermissionsAsync:2203). Prod NOT deployed (static review, build PASS đã claim). 7 verify ALL PASS: (1) Ordering — grant gọi SAURevokeTemporarilyHiddenModulesAsynctrong SeedAsync → grant thắng (git diff confirms call sits immediately after revoke). (2) Upgrade path prod-critical — method MUTATES existing rowif(!row.CanRead){row.CanRead=true;upgraded++}(EF change-tracked → SaveChanges persists); NOT skip-existing-noop. Correctly fixes S58-class bug (revoke set CanRead=false on prod rows → upgrade flips true). (3) Scope precise —hrmKeys = new[]{MenuKeys.Hrm, MenuKeys.HrmHoSo}EXACTLY 2; NO Hrm_Dashboard/Hrm_Config*/Off*/Personal.Hrmis NOT one of 4 inherit-roots (Contracts/Workflows/PE/PeWorkflows in GetMyMenuTree:56-59) so granting Hrm root does NOT cascade to Dashboard/Config children → they keep own false flags → filtered out byHasAccess(n)=n.CanRead||Children.Any(HasAccess). Menu shows Hrm root → Hồ sơ NS leaf ONLY (HrmHoSo ParentKey=Hrm:1806, Dashboard sibling ParentKey=Hrm:1850 stays hidden). (4) Read-only — add-path CanCreate/Update/Delete=false; upgrade-path touches ONLY CanRead. (5) No regression — Admin bypass at MenuPermissionHandler:27 untouched; revoke unchanged; Off/Personal/Dashboard/Config stay hidden after full seed. (6) Idempotent — 2nd run: row.CanRead already true →if(!row.CanRead)false → 0 change. (7) No non-Admin write path —MenuPermissionHandlerRead→AnyAsync(CanRead) is what GET checks; all 19 EmployeesController write actions (main+5 satellite) require Hrm_HoSo.Create/Update/Delete which grant leaves false → 403. surprise/monitor-note (NOT a defect, NOT introduced by this change): HrDashboardController/HrmConfigsController/Attendances/LeaveBalances carry ONLY class-level[Authorize](any-auth, NO per-action Hrm_.Read policy) — so their data was already reachable by direct URL pre+post S65 (menu-hide ≠ API-lock; S58 revoke comment DbInit:2153-2155 explicitly acknowledged this). S65 does NOT widen it (only touches perm matrix rows Hrm+Hrm_HoSo + menu filter). cicd-monitor must NOT assume "Dashboard hidden in menu"=="dashboard data unreachable". Spec comment said "6 catalog Hrm_Config" but there are 6 config leaves + Hrm_Config subgroup = 7 keys — cosmetic count, all stay hidden, not a code bug. Learned: for menu-key read-grant, verify the granted root is NOT an inherit-root (else cascade leaks siblings) + trace HasAccess filter + confirm leaf ParentKey chains to the visible root; upgrade-path correctness = grep that method MUTATES row (not skip-existing) when a prior revoke pre-set the flag false on prod. Verdict PASS — safe commit. Tag [s65, public-hrm-hoso, upgrade-path-correct, inherit-root-no-cascade, gotcha44-family-clean, menu-only-not-api-lock-monitor-note].