Verify: V1 conservation PASS (arithmetic full-accounting 0-byte-unaccounted) · V2 cross-doc PASS (detector 28→25, còn lại false-positive class) · V3 moved-not-cut PASS (sha-match + byte-arithmetic đóng vòng). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 KiB
Implementer-Frontend Agent — Persistent Memory
Persistent diary cross-session. Auto-injected first ~200 lines at spawn (L1 HOT). Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2
archive/on-demand · L3 RAGsearch_memoryjust-in-time. Keep entry ≤ 1.5K chars (gotcha #53). NEW agent S39 (2026-05-29) — split từ implementer (FE 2 app half). Backend scaffold history ởimplementer-backend.
🆕 S97-badge (2026-07-01) — PE chip "✅ Đã được CEO duyệt" khi phiếu ĐÃ DUYỆT đi hết lên CEO (anh Kiệt FDC UAT, em-main chốt)
FE-ONLY, PROD LIVE. Đối xứng fix endsBeforeCeo (Mig 60). EDIT1 mở rộng PeFinalizeChip.tsx (2 app) + EDIT2 wire phase={p.phase} ở 6 call-site.
- EDIT1 component: +prop
phase?+ nhánh:if(!endsBeforeCeo){ if(phase===DaDuyet) return emerald "✅ Đã được CEO duyệt"; return null }else giữ violet "⚑ Kết thúc·X". ImportPurchaseEvaluationPhasetừ@/types/purchaseEvaluation— cùng alias 2 app → byte-identical (KHÔNG ngoại-lệ import-path). SHA256 IDENTICAL ×2 =da9c3ed9…. - ⚠️ GOTCHA TYPE (đã trải): spec ghi prop
phase?: PurchaseEvaluationPhase(narrow union1|…|99) NHƯNGPeListItem.phase: number(types :113) →phase={p.phase}= TS2322 "number not assignable to PurchaseEvaluationPhase" (fail 2 site). FIX: prop →phase?: number(literalDaDuyet=7 so-khớp OK; import vẫn dùng trong=== …DaDuyet→ no unused). LESSON: list-DTO field mirror-BE =numberrộng → prop nhậnnumber, KHÔNG narrow-union; check kiểu item trước khi type prop. - EDIT2 6 site (grep
PeFinalizeChip endsBeforeCeo= 6, hết): admin{PeListPanel:162, ListPage:492} + user{PeListPanel:162, ListPage:492, InboxPage:292, UserDashboardPage:286}. MỌI item =pcó.phase(đã dùnggetPeDisplayStatus(p.phase)) → KHÔNG bịa. Grep-confirm 6/6 cóphase=, 0 sót. - Build PASS ×2 (admin 1948mod
index-CScNAtL0.js/ user 1937modindex-W2A6gg7n.js, 0 TS err). Pre-existing warn: >500KB (#69) + realtime.ts dynamic. NO BE/mig/test touch. Full precedent (S89 tạo chip). Tag[s97-badge, ceo-approved-chip, prop-type-number-not-union, ts2322-gotcha, 6-callsite, sha256-2pair].
🆕 S97-Part2 (2026-07-01) — FE gate endsBeforeCeo runtime (hook 1-dòng, on-behalf H2-proposed @S98)
- 2-app mirror
PeWorkflowPanel: gatefinalizeFlowStep = endsBeforeCeo ? find(...) : null+ banner ~:278 (làm-mờ Bước CEO theo cờ RUNTIME Mig 60), typeendsBeforeCeo: booleansẵn 2 DTO, SHA256 IDENTICALb7abbc2f…. Substance đã được cover third-party: reviewer MEMORY entry S97-bis + session-log S97 Part-2. Tag[s97-part2, endsbeforeceo-gate, hook-only].
🆕 S97 (2026-07-01) — PE finalize wording + đảo default checkbox opt-out→opt-in ×2 app (anh Kiệt FDC UAT, em-main chốt)
4 chuỗi-edit PeWorkflowPanel.tsx × {admin,user} = 8 hunk. PROD LIVE UAT (anh Kiệt xem trực tiếp). File 2 app byte-identical trước+sau → SHA-pair đúng nghĩa.
- E1 default checkbox:
useState(true)→useState(false)(~:57). Đảo S96 opt-out→opt-in: giờ default KHÔNG kết thúc, phải TÍCH mới finalize. - E2 reset onSuccess:
setApplyLevelFinalize(true)→(false)(~:221). Anchor 3-dòng (setFinalizeByCcm(false)+X+setApprovedPriceSource(null)) để KHÔNG đụngsetApplyLevelFinalize(e.target.checked)handler (:664). - E3 helper text ternary (~:669-671): reword theo default mới — nhánh true "Đã tích — …KẾT THÚC tại cấp này…", nhánh false "Mặc định: …TRÌNH TIẾP lên CEO… TÍCH vào nếu muốn KẾT THÚC…".
- E4a title (
:329): static string → template literal:331): "không trình tới" → "Kết thúc tại {finalizeLevelName ?? finalizeStepName ?? 'cấp trên'}".`Quy trình kết thúc tại ${finalizeLevelName ?? finalizeStepName ?? 'cấp duyệt thay CEO'}…`. E4b JSX text (finalizeLevelName/finalizeStepNameĐÃ trong scope (dùng banner :280-287) — no new prop/DTO. - GOTCHA "không trình tới" 2 chỗ: comment :121 (KHÔNG đụng) vs JSX :331 (đổi). Dùng ngữ-cảnh
>…</span>cho E4b unique → grep-confirm :121 comment còn nguyên. - SHA256 IDENTICAL ×2 =
bcf812fd…(git diff --no-index exit 0; diff 2 file byte-identical incl. before/after index hasha92c9db..d97a597). git diff = đúng 8 hunk (4/file, 12ins/12del), no thừa. Build PASS ×2 (admin 1948modindex-l_kwCZIF.js22.0s / user 1937modindex-DxUomy4C.js8.2s, 0 TS err). Pre-existing warn: @import CSS-order + >500KB + realtime.ts dynamic (#69). NO BE/test/other-FE touch. Full precedent, NO ambiguity. LESSON: đảo default state = sửa CẢ inituseStateLẪN reset-onSuccess (2 chỗ dễ sót 1); bare-string xuất hiện 2 chỗ (comment+JSX) → anchor context phân-biệt. Tag[s97, pe-finalize-opt-in, invert-default-checkbox, wording-reword, sha256-2pair, dual-string-anchor].
🆕 S88 D2 (2026-06-25) — PE multi-winner OUTWARD FE ×2 app (BE đã đổi: create→LIST, list DTO winner names)
3 edit OUTWARD, em-main chốt FE behavior, coord BE (impl-backend đã viết). 2 SHA-pair + 1 type-DIVERGE.
- E1 CreateContractDialog (PeDetailTabs ~:2144): mut type
{contractId:string}→{contractIds:string[]}(BE tạo 1 HĐ/mỗi IsWinner). onSuccess: toast`Đã tạo ${res.data.contractIds.length} hợp đồng từ phiếu.`+navigate(/contracts/${res.data.contractIds[0]})(giữ destination/contracts/${...}của bản single). Dialog GIỮ joined winner-names (:2174) + 1 ContractType-cho-mọi-winner. Bản gốc KHÔNG invalidate → nothing to preserve. - E2 PeListPanel label (~:168):
{p.selectedSupplierName && …✓…}→ preferwinnerSupplierNames.join(', ')khiwinnerSupplierNames?.length>0, elseselectedSupplierName. Giữ ✓ +text-emerald-600. Ternary lặp 2 lần (guard + render) — optional-chain?.lengthan-toàn undefined. - E3 type (DIVERGE 2 file):
PeListItem+winnerSupplierNames:string[]sau urgent-flags (anchorisUrgentByPro/Ccm+}byte-identical 2 app → Edit same string ×2, grep-confirm KHÔNG vào inter-app diff, stat giữ 30ins/26del). Create-contract response = inline generic trong PeDetailTabs (KHÔNG named type ở types file) → E3b no-op;contractId:string|null:121/:443 = link-HĐ-đã-gắn field KHÁC, GIỮ. - SHA256 ×2 pair: PeDetailTabs
9d8120b6…, PeListPanelc97e133d…(cp admin→user, git-diff --no-index IDENTICAL). Build PASS ×2 (adminindex-C3j9HaFh.js2.18s / userindex-Ct2cpC7h.js2.23s, 0 TS err). Pre-existing warn: >500KB + realtime.ts dynamic. NO BE/test touch. LESSON: OUTWARD multi-winner = winnerSupplierNames[] (BE null selectedSupplierName khi ≥2) → FE prefer-list-fallback-single; type DIVERGED ≠ cp → edit-same + verify-not-in-diff (S87+ confirm). Tag[s88, pe-multi-winner-outward, contractIds-list, winner-names-fallback, sha256-2pair, type-diverge-edit-same].
🆕 S87+ (2026-06-25) — FROZEN PE behavior-preserving cleanup ×2 app (em-main chốt, coord BE null-safe)
4 edit PeDetailTabs.tsx (SHA-pair) + 2 wording types/purchaseEvaluation.ts (DIVERGED — KHÔNG SHA-pair, edit identical 2 file).
- E1 HoSoLink clear:
HoSoLinkRow.save(~:2020)hoSoLink: hoSoLink || null→hoSoLink.trim()(state đã string nên??""=no-op; trim ra""khi xóa). Coord BEUpdatePeDraftHandlernull-safe:""=CLEAR, null/omit=PRESERVE, val=set (fix data-loss xóa link khi edit header). GUARD: InfoTab.save (:817) +PeHeaderFormPUT body GIỮ OMIT hoSoLink (omit=preserve = đúng fix) — verify KHÔNG thêm. - E2 #70 stale-echo guard: 2 ô PRO
adjustMut(row3 NS-kỳ-này :1548 + row8 GT-còn-lại :1649) chỉsaving={adjustMut.isPending}→|| peFetching(replace_allđúng 2 occ;peFetchingcomputed :1268). Sibling ccmPeriod/proPrice/ccmPrice/proMut/ccmMut đã có. Chống fast-double-save clobber staleev.*(2 ô share 1 absolute-set mut cross-echo). - E3 stale comment :2507: "AddItemDialog giữ (dead)…" → đã xóa (grep chỉ comment match; live =
DetailDialog). Sửa thành "nút đã bỏ (dialog = DetailDialog)". - E4 type wording (2 file):
previousSelectedTotal/currentProposalTotalinline cmt "của SelectedSupplier" → "NCC trúng (IsWinner)" (BE giờ multi-winner). Block 7-dòng byte-identical 2 app → edit same → KHÔNG new divergence (grep-confirm my-line ∉ diff). - SHA256 PeDetailTabs ×2 =
36292924…(cp admin→user, git-diff empty). PeHeaderForm untouched987291c1…identical. Build PASS ×2 (admin 1946modindex-CqiQWR8e.js/ user 1935modindex-Cp0DuKFQ.js, 0 TS err). Pre-existing warn: @import CSS + >500KB + realtime.ts. NO BE/test touch. LESSON: type files DIVERGED (S76/S79 color+prose) ≠ SHA-pair → mirror = edit-identical-block + verify-not-in-diff, KHÔNG cp. Tag[s87plus, pe-frozen-cleanup, hosolink-clear-empty, gotcha70-guard, type-diverge-edit-identical].
📦 Archived detail → archive/2026-H1.md (read on demand)
- S79 (2026-06-19) — PE live thousand-sep (
formatVndInput(parseVnd())on-keystroke 2 comp) + ghi chú giá PRO/CCM (note3-field absolute-set) + typo "Bảng so sánh giá". SHA-pair.[s79, type-diverge-2app] - S78 (2026-06-19) — PE "focus mode" overlay redesign mirror fe-user→admin CONVERGE. PeWorkflowPanel +
onApprovedprop; ListPagefixed inset-0 z-50slide overlay a11y. SHA-pair.[s78, mirror-step, converge] - S76 (2026-06-19) — PE budget ma trận 3 cột (DỰ ÁN/PRO/CCM) →
<table border-collapse>grid + badge✎ NS PRO/CCM. LESSON: flex+gap ≠ bảng, phải<table>. cwd-mislandfe-*/.claude/gitignore-guard.[s76]
🎯 Role baseline
WRITE specialist FE 2 app (fe-admin + fe-user). Cookie-cutter mirror SHA256 IDENTICAL + Pattern 16-bis 4-place + declarative KIND_CONFIG + npm build × 2. Case 1+2 only. Tools: Read, Edit, Write, Bash, Skill, Grep, Glob + 5 RAG. Skill: permission-matrix.
🚫 Split boundary
- ✅ MINE:
fe-admin/src/**+fe-user/src/** - ❌ NOT:
src/Backend/**→implementer-backend(chỉ Read DTO shape) · tests/ →test-specialist - ❌ NOT: UX flow decision (drawer/tab/modal) → em main solo
📋 Patterns proven (apply confidently)
Pattern 16-bis 4-place mirror (9× cumulative — BLESSED FOUNDATION)
Add/move page cross-app MUST mirror 4 places:
- Page/types file · 2.
App.tsxRoute · 3.lib/menuKeys.tsconst · 4. ⚠️components/Layout.tsxresolvePathstaticMap (DỄ MISS → silent sidebar drop gotcha #50) Verified clean S33/S34/S35/S36/S37/S38 = 9× cumulative. Spec MUST list 4 places explicit.
SHA256 IDENTICAL × 2 app
Viết fe-admin → cp fe-user → sha256sum verify. Khác UX (admin full sidebar vs user filter) → mirror tay + diff. Proven S36 (3 pair) + S37 (4 pair) + S38 (5 pair).
Declarative KIND_CONFIG Record (S35, 3× proven)
Single-page multi-kind CRUD URL :kind param + Record<Kind, {fields, columns, icon, label}> + renderField switch FieldType. Reuse: HrmConfigs (S35) + WorkflowApps (S38) + HrmConfigs +2 kind (S51). ADD-KIND playbook: union type + KIND_CONFIG entry + KINDS array + renderCells branch (before fallback) + import icon — :kind-driven page = NO new App.tsx route.
Pattern 14 Tailwind JIT palette
Dynamic class purged. PALETTE array full literal as const cycle index % length.
TS6 / convention
erasableSyntaxOnlycấm enum →const X = {...} as const+type X = typeof X[keyof typeof X]- Named export only (trừ App). UI 100% tiếng Việt.
- PageHeader signature (S37):
title/description/actionsonly — KHÔNGicon/childrenprop (build fail TS2322) - fe-user thiếu Card/Badge shadcn → fallback inline
<div className="rounded-lg border bg-card">
✅ Verify protocol
cd fe-admin && npm run build + cd fe-user && npm run build BOTH 0 TS error + sha256sum mirror proof. Bundle >500KB warning OK pre-existing.
📅 Recent activity (FIFO — one-line hooks; full detail → archive/2026-H1.md)
- 2026-06-17 Văn phòng số "Bảng điều khiển" — mirror fe-user→admin 4 file (PageHeader/KpiCard/WidgetCard/OfficeDashboardPage) + 4-place wiring
/office/prefix route. SHA-5file.[office-dashboard, 4-place] - 2026-06-16 (S65) PE mục "e. Link hồ sơ" NAS hyperlink + rename "Dự trù PRO"→"Ngân sách PRO". SHA-mirror 4 file. #53 empty-return (render landed OK); em-main bắt sót badge rename.
[s65, hosolink] - 2026-06-16 Department parentId cây tổ chức, fe-admin ONLY (no SHA). Select "Phòng cha" + load-all pageSize:200 + exclude-self khi Edit + cột "Thuộc"; BE cycle-guard 409.
[dept-parent, admin-only] - 2026-06-11 (S57bis) PE WorkItem FE ×2 app (select "c. Hạng mục" + PeHeaderForm/PeDetailTabs +3 field). Truncated #53 GIỮA mirror → em-main solo. LESSON: per-app stage khi slice lớn.
[s57bis, truncated-53] - 2026-06-09 (S55) Project +4 optional master fields (year/investor/location/package). String-form+convert-on-submit (S42 pattern).
package=valid TS key. SHA-pair.[s55, master-enrich] - 2026-06-08 (S54) ItTicket reassign → CONVERGE 2 app (reverse S53). Pattern: BE capability-flag gate (
GET /it-tickets/assignable-staff {canReassign,staff}) thay FE đoán role. SHA4bcaf2f.[s54, capability-flag] - 2026-06-08 (S52) ItTicket admin reassign + AttendanceReport menu, fe-admin ONLY. Menu = 2/4-place (menuKeys+Layout staticMap; types/menu.ts key:string no-union).
[s52, admin-only, menu-2place]
⚠️ Anti-patterns (DO NOT)
- ❌ Touch BE files · 2. ❌ Miss 4th Layout staticMap · 3. ❌ Skip npm build × 2 · 4. ❌
git add -A· 5. ❌ Push remote · 6. ❌ UX decision autonomous → REFUSE
🔄 Curate trigger
Size > ~30KB → archive to L2 (tiered v1). Commit scope (em main commits): FE-Admin · FE-User.