20 KiB
Implementer-Backend 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). Full verbatim history pre-S40 → gitd2f52ba+archive/2026-05-q1..q4.md+archive/2026-06.md. 🗺️ Lookup map (Harness-9 S70):archive/_INDEX.md— 1 dòng/bản-ghi + con-trỏ substring (sha-keyed, Ctrl-F fallback); đọc verbatim +2026-0{5,6}.gist.md(nén 4-field) theo nhu cầu. Renamed S39: implementer → implementer-backend (.NET half). FE patterns →implementer-frontendMEMORY. Test patterns →test-specialistMEMORY.
🎯 Role baseline
WRITE specialist .NET backend SOLUTION_ERP (Domain+Application+Infrastructure+Api). Case 1+2+3+5 only. Tools: Read, Edit, Write, Bash, Skill, Grep, Glob + 5 RAG MCP. Skills: ef-core-migration + permission-matrix + contract-workflow + form-engine. Output: commits + verify report.
🚫 Split boundary (S39) + auto-refuse
- ✅ MINE:
src/Backend/SolutionErp.{Domain,Application,Infrastructure,Api}/** - ❌ NOT:
fe-*/**→implementer-frontend·tests/**→test-specialist· schema/UX/architecture decision → em main - REFUSE if ANY: 1 schema design (FK/nullable/discriminator) · 2 UX flow · 3 cross-stack >2 layer · 4 bug fix reasoning chain · 5 integration multi-component · 6 <30min trivial · 7 first-time no precedent · 8 spec ambiguity >20%
📋 BE Patterns proven (apply confidently)
Pattern 1: Per-chunk discipline A-E
A Domain+Mig (3-file) · B Application CQRS (Command/Query/Validator) · C Service (workflow logic) · D Api Controller · E commit. Build+test pass mỗi chunk. Commit [CLAUDE] <scope>: Chunk X — ... + Co-Authored-By Claude Opus 4.8 (1M context).
Pattern 2: EF migration 3-file rule (gotcha #17 — BẮT BUỘC commit đủ)
{TS}_{Name}.cs + .Designer.cs + ApplicationDbContextModelSnapshot.cs. Path src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/.
dotnet ef migrations add <Name> --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api
# Apply Dev (runtime): --connection "Server=(localdb)\MSSQLLocalDB;Database=SolutionErp_Dev;Trusted_Connection=True;TrustServerCertificate=true"
# Apply Design (ef default): không cần --connection
Apply BOTH DB per feedback_designtime_runtime_db.
Pattern 3: Audit reuse trước khi clone (feedback_audit_reuse_before_clone)
"Clone X→Y": grep discriminator (ApplicableType/Type/Kind) → check Service/Handler hardcode → check FE route dynamic → check menu key (BE const + FE menuKeys.ts thường thiếu) → default reuse 80%, chỉ thêm menu key + sample seed.
Pattern 4: Service hook vs CRUD endpoint cho derived state (feedback_service_hook_vs_endpoint)
State X derived của action Y → UPSERT trong handler Y, KHÔNG endpoint /X riêng. VD ApproveV2Async UPSERT LevelOpinion qua match ApproverUserId==actorUserId (fallback first khi Admin override). 0 endpoint mới.
Pattern 7: Per-NV admin opt-in flag (Mig 29/30/31)
ApprovalWorkflowLevel +1 bool DEFAULT 0 (opt-in). EF HasDefaultValue(false). DTO extend. FE Designer checkbox inline. Scope role-context → table mapping (Approver→Level table carry ApproverUserId FK, Drafter→User table direct, feedback_per_nv_permission_scope). Reusable F5/F6.
Pattern 8: Tách endpoint riêng cho narrow scope
1 action 2 scope theo role → tách endpoint (guard tự nhiên + audit). VD Drafter UpdatePeDraft (Section 1 rộng, phase Nháp/Trả lại) vs Approver AdjustBudget (Budget rows hẹp, phase Đang duyệt + per-NV flag). KHÔNG default expand Drafter scope cho Approver.
Pattern 9: Defense-in-depth FE+BE guard pair
UI disabled={!canX} + BE helper EnsureCanXAsync(id, userId) throw 403 (NOT inline handler) — tránh forge qua DevTools. Bất kỳ action sensitive (approve/reject/adjust).
Pattern 12-bis: Cross-module entity cookie-cutter mirror (S29 Plan B, proven 3×)
"Mirror entity X từ module A→B": 6 file MAX — (1) new entity Domain/<Mod>/<Entity>.cs rename FK+nav · (2) parent +nav collection · (3) IApplicationDbContext +DbSet · (4) ApplicationDbContext +Set<X>() · (5) new <Entity>Configuration.cs (separate file mirror PE, NOT inline) · (6) dotnet ef migrations add 3-file. AuditableEntity inherit. FK: parent Cascade + 3rd-party Restrict + User skip-nav (denorm <Type>ByFullName). Apply 2 DB. ⚠️ Catalog-mega variant (S35 HrmConfig): HRM entities KHÔNG có global HasQueryFilter(!IsDeleted) (vs Master) → list query MUST .Where(!IsDeleted) thủ công (verify Grep HasQueryFilter Configurations FIRST). Validator MaxLength MATCH EF config (verify source-of-truth, KHÔNG trust spec blind).
Pattern 12-ter: N≤7 satellite CRUD scaffold same parent (S34, feedback_within_module_n_satellite_scaffold)
"N satellite cùng parent" → 1 mega file <Parent>SatelliteFeatures.cs N region cookie-cutter (Create verify parent AnyAsync → Update FirstOrDefault !IsDeleted → Delete soft IsDeleted+DeletedAt+DeletedBy ICurrentUser) + 1 Controller extend (3 verb × N). Endpoint verify parentId==cmd.ParentId BadRequest mismatch. Per-action policy override class-level Read.
Patterns moved (split S39)
- FE patterns (5 mirror 2-app · 6 VND helpers · 13 read-only Designer · 14 Tailwind JIT palette · 15 rowSpan builder · 16-bis 4-place mirror) →
implementer-frontendMEMORY (seeded). - Test patterns (10 reflection authz · 11 test infra helper · 12 InternalsVisibleTo) →
test-specialistMEMORY (seeded).
⚠️ Anti-patterns (DO NOT)
- ❌ Skip MEMORY · 2. ❌
--no-verify· 3. ❌git add -A/git add .(specific files) · 4. ❌ Touch outside spec scope · 5. ❌ Push remote autonomous (em main pushes) · 6. ❌ ModifySolutionErp.slnxautonomous · 7. ❌ Lower bar match em main (Smart Friend) · 8. ❌ Proceed khi ambiguity >20% → REFUSE
🧠 SOLUTION_ERP BE conventions (S40)
- BE .NET 10: PascalCase entities + DTO records + command names. CQRS+MediatR+FluentValidation+AutoMapper. Repository qua
IApplicationDbContext.GlobalExceptionMiddleware→ ProblemDetails (NO try-catch controllers). - State S53: 47 mig (last
FilterMasterCatalogUniqueIndexesByIsDeletedMig 47, index-only) · 93 SQL tables · ~224 endpoints · 203 test (58 Domain + 145 Infra, test-specialist owns). Phase 9 UAT skip per chunk (feedback_uat_skip_verify). - Build:
dotnet build SolutionErp.slnxclean 0 err. Commit[CLAUDE] <scope>: <msg>+ Co-Authored-By Claude Opus 4.8 (1M context). - Pin (KHÔNG
*/latest): MediatR12.4.1(14 fail DI) · Swashbuckle6.9.0· Node CI20.x· LibreOffice25.8.6· @microsoft/signalr8.0.7.
📅 Recent activity (FIFO — older → archive/git)
-
2026-07-12 (S113 Supplier import v2 BE — spec
wf_e7896ca6fable-clone-ensemble, owner anh Kiệt, Mig 64, D1/D2/D3 pre-chốt → ACCEPT Case 1+2; ⚠️ return-1 garbled #53 → coordinator SendMessage-resume, xong solo): IsPublic publish-gate + dedup MST-primary. Mig 64AddSupplierPublishState(Supplier +bool IsPublicNOT NULL default false +Sql("UPDATE Suppliers SET IsPublic=1")backfill 22 prod + DropIndex→CreateIndex đổi Code-unique filter[IsDeleted]=0 AND [Code]<>''cho draft-row Code="" coexist; no new table 89). ServiceSupplierExcelImportService: (a) re-bake ExpectedHeaderTokens BYTE-EXACT raw-\nform = single-source cho validator(NormalizeHeader collapse \n→space)+BuildTemplate (spec §② 30 token, c7 trailing-space); (b) dedup MST-primary + Code-backstop —BuildMstIndex(NormalizeMst=Trim+strip-inner-ws\s+→"" OrdinalIgnoreCase, blank→skip) precedence existing-MST→existing-Code→batch-MST→batch-Code,countedUpdates HashSet<Supplier>tránh double-count, blank-MST KHÔNG vào batchByMst (2 blank=2 insert); (c) BỎ hard-error blank-Code (=draft), GIỮ Name+all-or-nothing; (d) NewIsPublic=!blank(Code), update-path KHÔNG đụng IsPublic; +MstMissing(soft-warn) +DraftCount; (e)BuildTemplate()ClosedXML sheet "Sheet1" 30 token ROW4 WrapText+FreezeRows. PublishSupplierCommand(Id,MaNcc?,Publish): set Code+unique-CI-check + gate Publish→Code-non-empty ConflictException, KHÔNG UpdateSupplierCommand (#73 clobber). API THẬT: GET/api/suppliers/import/template(xlsx "Mau-Database-NCC.xlsx") · POST/api/suppliers/{id}/publishbody{maNcc?,publish}· GET/api/suppliers?published={t/f}filter · SupplierDto +bool IsPublicappend-CUỐI (sửa 2 projection List+Get). D3 authz: import preview/confirm/template/publish[Roles=Admin,CatalogManager]→[Policy="Suppliers.Update"](khớp FE PermissionGuard). DbInit +IsPublic=true 4 real+18 sample. Build slnx 0/0, apply BOTH DB (Dev catch-up+Design). KHÔNG commit/FE/test. LESSON: S112 CI-dedup-at-service mở rộng → dual-key MST-primary; blank-key KHÔNG index. Tag[s113, supplier-import-v2, mig64, ispublic-publish-gate, dedup-mst-primary-code-backstop, blank-code-draft, buildtemplate-single-source, publish-cmd-no-#73]. -
2026-07-12 (S112 PE sign-off close-review implement —
wf_7df56b89-7bbGO-WITH-ADJ, owner anh Kiệt PROD-UAT-live, 4-region/1 filePurchaseEvaluationWorkflowService.cs, NO mig/FE/test → ACCEPT Case 1): C1 A1 (ApplyDrafterBypassOnSubmitAsync): người soạn KÝ TAY ô cấp mình —k=Max→minOwn=Min(drafterSlots.Order), bypass<=k→<minOwn(chỉ NV cấp dưới THUẦN), XÓA canned-opinion UPSERT (không tự ký hộ ai) + deadmaxLevelOrder/ownSlot; advance 3-nhánh→1if(minOwn>1){pointer=minOwn}STOP tại minOwn (người soạn tự ApproveV2Async); minOwn=1→bypass rỗng pointer giữ (0,1); non-contiguous Cấp1+Cấp3 ký tay đúng thứ tự. C2 EDGE-5 hard-lock: topTransitionAsyncsau isAdmin/isSystem TRƯỚC mọi branch →if(fromPhase is DaDuyet or TuChoi && !isAdmin && !isSystem) throw Conflict(chặn non-admin lật phiếu terminal; Admin+System MIỄN); KHÔNG đụng EnsureCanRejectV2Async. C3 no self-finalize (owner oQ1 permission-driven). C4 opinion-history (ApproveV2AsyncUPSERT else): old.Comment→Changelog (Workflow/Update/EntityId=level.Id) TRƯỚC overwrite chỉ khi non-empty — NO mig/bảng/endpoint. Build slnx 0/0. Infra 4 FAIL/401 pass: fail=C1 (PeSubmitGuardAndBypassTests 9/10/12/14 assert OLD auto-sign/terminal-on-submit); C2/C4 break 0 test → test-specialist ADD. LESSON: behavior-change write-site → run consumer tests, hand VERIFIED fail-list (KHÔNG sửa test). Tag[s112, pe-signoff, a1-sign-own-level, edge5-hardlock, opinion-history-changelog, consumer-test-verified-handoff]. -
2026-07-12 (S112 Supplier Excel-import Phase B BE —
wf_e2f23a84invest-ensemble →wf_75d6149bclose-review GO-WITH-ADJ, owner anh Kiệt, commite100ef0, Mig 63 → ACCEPT Case 1/2): ServiceSupplierExcelImportService(preview + confirm 2-stage) + 2 endpoint (/suppliers/import/preview[415 non-xlsx] ·/suppliers/import/confirm[401 auth]) + Mig 63AddSupplierImportSourceFields(Supplier +SourceUpdatedAt datetime2?+SourceUpdatedBy nvarchar(200)?provenance cột 29/30; 2 AddColumn nullable no-new-table 89). FE 2-app byte-identical (SupplierImportDialog). LESSONS: (1) dedup Code = OrdinalIgnoreCase ở SERVICE — DB unique-index CI-collation lật (SQLite fixture BINARY KHÔNG bắt, prod SQL Server CI bắt) → dedup KHÔNG dựa DB; (2) parser absolute cell-index (row-4 header-fingerprint), KHÔNGCellsUsed()(column-drift); (3) all-or-nothing gate TRƯỚC mutate (1 hard-error→Committed=false DB unchanged); (4) fill-nulls SKIP Code/Name/Type (chỉ điền field trống); (5).Normalize(FormC)cho real-file NFD (close-review adjust bake 30RealFileHeaderTokensbyte-verify). SURPRISE: Approach-B (extend seed) ĐÃ ship 4 NCC prod (Mig 62) → spec "partial" SAI; import ghi entity TRỰC TIẾP (fill-where-non-empty) KHÔNG quaUpdateSupplierCommand(tránh NULL-clobber #73). Shipped Run #496 PASS, reviewer GO-WITH-ADJ (BE 6/6 axes). Tag[s112, supplier-excel-import, mig63, ordinalignorecase-dedup-at-service, abs-cell-index-parser, all-or-nothing, fill-nulls-safe, normalize-formc-nfd, direct-entity-write]. -
2026-06-25 (S88 PE stability cleanup — FROZEN-module mechanical relocation + 1 stale-comment fix BE — NO migration, 3 edit/2 file, em-main spec deterministic 100% file/line/boundary given → ACCEPT Case 1): Pure mechanical move ZERO behavior-change. T1 misplaced-command relocate:
SetPeCcmBudgetPeriodCommand+Validator+Handler (BUDGET setter writespe.CcmBudgetPeriodAmountSection B "NS kỳ này" CCM, Mig 59) sống NHẦM trong suggested-PRICE file → move về budget file. (a) REMOVEPeSuggestedPriceFeatures.cslines 144-199 (blank +===== CCM NS kỳ này =====header →SetPeCcmBudgetPeriodCommandHandlerclosing}); file end clean tại line 143 (UpdatePeSuggestedPriceCcmCommandHandler}), suggested-price PRO/CCM trên giữ nguyên. (b) APPENDPeWorkItemBudgetFeatures.csEND sau line 200 (UpdatePeBudgetCcmCommandHandler}) → cạnh budget settersUpdatePeBudgetPro/Ccm. Code BYTE-IDENTICAL (logic/authz Admin|CostControl fail-closed/validation GTE(0).When(HasValue)/absolute-set null=clear/changelog UNCHANGED); CHỈ prepend 1 comment line// [S88] Chuyển từ PeSuggestedPriceFeatures.cs về đây (đúng nhóm budget setter).. Usings 8-header IDENTICAL 2 file → KHÔNG add duplicate. NamespaceSolutionErp.Application.PurchaseEvaluationsSAME → MediatR type-resolution transparent, controller:125KHÔNG đụng. T2 stale-comment fix:Dtos/PurchaseEvaluationDtos.cs~250-254 comment trêndecimal WinnerQuoteTotalghi SUM "của SelectedSupplierId" (OLD single-winner) → verified actual computePurchaseEvaluationFeatures.cs:1093-1099=Suppliers.Where(s=>s.IsWinner)(multi, Mig 58) → sửa CHỈ comment-text "MỌI đơn vị IsWinner (multi-winner Mig 58)". Field/code UNCHANGED. LESSON: frozen-relocation = read BOTH file FIRST exact boundary + verify usings-superset target + verify stale-comment claim vs real compute-site TRƯỚC sửa (KHÔNG trust comment blind, mirror S35 EF-source-of-truth). BuildSolutionErp.Application.csproj -v q0 warn 0 err (4s). KHÔNG test (test-specialist)/FE/mig/commit. Tag[s88, frozen-relocation, byte-identical-move, mediatr-type-resolve-transparent, stale-comment-verify-compute-site, no-behavior-change]. -
2026-06-24 (PE Section B 3-cột THỰC HIỆN — CCM "NS kỳ này" cột riêng BE — Mig 59
AddCcmBudgetPeriodToPurchaseEvaluation3-file, 5 edit/0 new file, em-main CHỐT spec+API-contract 100% → ACCEPT Case 1): Section B tách 3 cột Dự án|PRO|CCM giống Section A. PRO=BudgetPeriodAmountsẵn có; CCM nhập cột riêng độc-lập. COOKIE-CUTTER mirror S77 suggested-price-Ccm + S65b HoSoLink one-column. (1)PurchaseEvaluation.cs+decimal? CcmBudgetPeriodAmountsau ExpectedRemainingAmount. (2)PurchaseEvaluationConfiguration.cs+HasColumnType("decimal(18,2)")sau ExpectedRemaining (spec asked literal decimal(18,2); siblings dùng HasPrecision(18,2) NHƯNG emit identical column — tôi follow spec verbatim HasColumnType). (3) Mig: Up=1 AddColumn decimal(18,2) nullable NO table/index, Down=1 DropColumn, snapshot verified — clean mirror Mig 52. (4) DTOPurchaseEvaluationDetailBundleDto+decimal? CcmBudgetPeriodAmountsau ExpectedRemainingAmount TRƯỚC BudgetSummary + projection PEFeatures.cs:1110 positional-insert đúng order (positional record → projection order BẮT BUỘC khít). (5) SetterSetPeCcmBudgetPeriodCommand(Guid Id, decimal? CcmBudgetPeriodAmount):IRequestappendPeSuggestedPriceFeatures.cs(same concern PE role-gate setters) + ValidatorGreaterThanOrEqualTo(0).When(HasValue)(budget=0 hợp lệ) + Handler fail-closed Forbidden Admin|CostControl TRƯỚC side-effect (S56 #5) + absolute-set null=clear (gotcha #73) + changelog Header (sibling-consistency; spec min=SaveChanges). (6) Controller[HttpPatch("{id:guid}/budget/ccm-period")]+CcmBudgetPeriodBody(spec EXPLICIT PATCH dù siblings PUT — follow spec). Call-site verify:new PurchaseEvaluationDetailBundleDto(repo-wide → EXACTLY 1 (PEFeatures:1100, đã sửa) → positional-safe. Submit-guardBudgetPeriodAmountPRO KHÔNG đụng. Build SolutionErp.slnx (6 proj incl 2 test, gotcha #65) 0 warn 0 err. KHÔNG apply DB/FE/test/commit. Route FE/test mirror: DTO fieldccmBudgetPeriodAmountcamelCase + PATCH/api/purchase-evaluations/{id}/budget/ccm-periodbody{ ccmBudgetPeriodAmount }. Tag[pe-section-b-3col, ccm-budget-period, mig59, one-column-no-table, absolute-set-fail-closed, positional-dto-order, patch-per-spec]. -
2026-06-19 (PE chuông báo approver BE — NO migration, 1 edit/1 file, em-main spec deterministic 100% → ACCEPT Case 1): Tra Sol (Zalo) "không thấy chuông — việc có hồ sơ cần duyệt": approver Cấp hiện tại KHÔNG nhận notify khi phiếu ENTER/ADVANCE tới ChoDuyet (chỉ drafter báo terminal). FIX = +1 block trong
LogTransitionAsync(PurchaseEvaluationWorkflowService.cs ~line 1058) NGAY SAU drafter-notify, KHÔNG endpoint mới (Pattern 4 — notify = side-effect của Submit/Approve-advance, cả 2 đã gọi LogTransitionAsync + end ChoDuyet). Resolution mirror EXACT canonicalEnsureActorInLevel(line ~301):db.ApprovalWorkflows.AsNoTracking().Include(Steps).ThenInclude(Levels)→stepsOrdered[CurrentWorkflowStepIndex](bounds-check) →.Levels.Where(Order==CurrentApprovalLevelOrder && ApproverUserId!=Guid.Empty && !=actorUserId).Select(ApproverUserId).Distinct()→NotifyManyAsync(ids, Generic, "Phiếu cần bạn duyệt: {MaPhieu??TenGoiThau}", "Có phiếu Duyệt NCC đang chờ bạn duyệt.", /purchase-evaluations/{Id}, refId:Id, ct). Recon: (1)ApprovalWorkflowLevel.ApproverUserId= non-nullGuid(NOT Guid?) → "exclude null" =!=Guid.Emptydefensive (OR-of-N rows mỗi 1 NV). (2)NotifyManyAsyncsig verified =(IEnumerable<Guid>, NotificationType, title, string? desc, string? href, Guid? refId, CT)khớp urgent-feature ref. (3) actor-exclude qua!= actorUserId(Guid != Guid? lift OK). Guard V2-only:ApprovalWorkflowId is Guid && CurrentWorkflowStepIndex is int && CurrentApprovalLevelOrder is int→ V1/no-workflow skip. Best-effort try/catch nuốt lỗi (phiếu đã SaveChanges; notify fail KHÔNG rollback). Block đọc pointer SAU set → Submit (idx0/lvl1) + Approve-advance (pointer advance rồi LogTransition) đều fire đúng. Build SolutionErp.slnx (2 test project, gotcha #65) 0 warn 0 err. KHÔNG test/FE/mig/commit. Tag[pe-approver-bell, notify-many, log-transition-hook, no-mig, v2-only, best-effort-trycatch, pattern4-side-effect]. -
(S77→S56 [2026-06-10..19, 7 entries] FIFO'd →
archive/2026-06.mdat S112; recall verbatim there /_INDEX.md/ RAGsearch_memory.)
🔄 Curate trigger
-
~30KB → archive recent → L2
archive/<period>.md. Stale >3mo → remove. - Last curate: 2026-07-12 S112 (implementer-backend self-FIFO, hook-triggered @24KB) (25.6→~16KB): moved Recent-activity S77→S56 (7 entries, 2026-06-10..19) verbatim →
archive/2026-06.md(16.7→26.4KB), kept 4 newest (S112/S88/S87/chuông). Prev S70 Harness-9 (em-main + Stage-B) (33.2→17.4KB): L2 dark-matter recovery — 14 Recent-activity entries (S55→S35) → NEWarchive/2026-06.md+_INDEX.md(substring sha-keyed) +2026-0{5,6}.gist.md(distill-gen:1). 0-byte-loss md5 byte-exact (Stage C audit CONCERN → read-side-gap MEMORY-L5→_INDEX fixed). (cosmetic: 2 curate-meta lines carryS?worker-label.) Prev: S40 (30.9→~18KB dedup-split BE/FE/test) · S34 q3 · S22 q1.