Gaps & Plan
Consolidated issue tracker từ
01-create.md→07-edge-cases.md. Ưu tiên theo impact + effort. Mỗi item link ngược về file gốc.
Legend
- P0 — Blocker cho production real-world. Salon thật sẽ gặp trong tuần đầu vận hành.
- P1 — Important trước launch công khai. Gap UX hoặc safety.
- P2 — Nice-to-have / post-MVP.
- P3 — Future / out-of-scope.
Checkbox:
[x]done + merged[~]in-progress / partial[ ]not started
P0 — Production blockers
[x] P0-1 · OWNER / ADMIN force cancel out-of-window
File: 03-cancel.md §3, §5, §7 · Real-world: case #4, #5, #6.
Vấn đề: booking.service.ts:804 block cả OWNER/ADMIN bằng !isSystem guard. Salon không cancel được khi emergency (staff ốm, khách gọi xin hủy sát giờ).
Fix:
- Sửa
booking.service.ts:804:if (newStatus === CANCELLED && !isSystem && !isAdmin) { validateCancellationWindow(...); } - Mở rộng
booking.controller.ts:116nhận body:@Body() body: { reason?: string; force?: boolean } - Khi
isAdmin && out-of-window→ bắt buộcreasonnon-empty. - Audit log: thêm action
STATUS_CHANGE_FORCEDvớireasonpersistent. - Frontend: modal "Reason" trước confirm force cancel (shadcn Dialog + Textarea).
- Test: unit test policy branch + e2e OWNER cancel out-of-window pass.
Effort: ~1 ngày. 1 API + 1 FE modal + tests.
[x] P0-2 · Deposit-required guard cho manual confirm
File: 02-happy-path.md §T1 · Real-world: case #3, #4 trong §8 01-create.md.
Vấn đề: STAFF có thể PENDING → CONFIRMED dù depositEnabled=true + Payment chưa AUTHORIZED. Booking CONFIRMED không deposit.
Fix:
- Thêm guard trong
updateStatuschoPENDING → CONFIRMED:if (newStatus === CONFIRMED && !isSystem) { if (tenant.settings.depositEnabled && !isAdmin) { const payments = await paymentRepo.findByBookingId(id, tenantId); const hasValid = payments.some(p => p.status === AUTHORIZED || p.status === CAPTURED); if (!hasValid) throw UnprocessableEntityException('BOOKING_DEPOSIT_REQUIRED'); } } - Exception:
payableTotal = 0(full loyalty discount) → exempt guard (xem P2-9). - OWNER override với
{ force, reason }body (dùng chung với P0-1). - Audit log:
CONFIRMED_WITHOUT_DEPOSITflag. - Test: STAFF blocked, OWNER force pass, loyalty-free case pass.
Effort: ~1 ngày. Gắn với P0-1 chung 1 PR.
Lưu ý: Cross-context dependency — Booking gọi PaymentRepo. Hoặc:
- Option A (simpler): check
booking.depositStatusprojection (nhưng cần P1-9 trước). - Option B (current recommendation): Booking module inject
PAYMENT_REPOSITORYport — OK về layering vì Payment đã expose port.
[x] P0-3 · Walk-in không emit BookingCreated
File: 01-create.md §4, §8 · Related: 06-loyalty-coupling.md case 1.
Vấn đề: Walk-in skip create event → loyalty không reserve redemption (nếu có), payment không init (OK cho walk-in vì pay tại salon), analytics miss events.
Decision cần: walk-in có nên emit BookingCreated không?
- Pro: uniform event stream, loyalty auto-earn trên BookingCompleted vẫn chạy
- Con: emit sẽ trigger Payment.initiate nếu depositEnabled → không phù hợp walk-in
Fix: emit BookingCreated với flag paymentMode: IN_PERSON (chung với P1-3):
- Payment listener: skip initiate khi
paymentMode = IN_PERSON. - Loyalty listener: vẫn chạy cho redemption + autoStamp.
Effort: ~0.5 ngày. Gắn với P1-3 chung PR.
P1 — Important before launch
[x] P1-1 · Customer self-cancel endpoint (public)
File: 03-cancel.md §7 · Real-world: case #1, #2. Shipped: 2026-04-24.
Shipped:
POST /public/tenants/:slug/bookings/:id/cancelwith@CustomerAuth()— requires a valid customer JWT, 403 for guests / mismatched owner, 404 when booking is scoped to another tenant.- Performer sent to
BookingService.updateStatusis{ role: 'CUSTOMER', userId: customerId }socancelledBy=CUSTOMERis emitted bybuildStatusTransitionEventand policy takes the customer branch. - Cancellation-window guard kicks in automatically via the
!isSystem && !isAdminbranch — customer out-of-window gets422 BOOKING_CANCELLATION_TOO_LATE(P1-2 will add CTA). - Frontend:
account/BookingsSectionexposes a red "Cancel" CTA on PENDING/CONFIRMED/ARRIVED rows behind aConfirmDialog. API client teaches itself that/public/tenants/.../cancelis a customer-authed public path (refresh + retry via customer JWT). - Tests: 6 new unit tests on
PublicBookingController(happy path, guest booking, owner mismatch, tenant mismatch, window-guard passthrough, slug not found).
[x] P1-2 · "Contact salon" CTA khi out-of-window
File: 03-cancel.md §7, §8 case #2. Shipped: 2026-04-24.
Shipped:
OutOfWindowDialog.tsx(webcomponents/bookings/) — warning icon + policy message, booking reference card (salon name, booking ID, appointment time — customer copy/screenshot khi gọi salon), primary CTA link sang/b/{slug}để xem thông tin salon, secondary "Close".BookingsSectiononError phân biệt:ApiError.code === 'BOOKING_CANCELLATION_TOO_LATE'→ mở dialog; các lỗi khác giữ toast đỏ như cũ.- State
cancellingđổi từ{slug,id}sang fullCustomerBookingđể dialog có đủ thông tin mà không cần fetch thêm booking. - Dialog lazy-fetch
/public/tenants/:slug(staleTime 5min) để lấysettings.cancellationHours, message fallback khi không có. - i18n: 8 new keys dưới
customerAuth.outOfWindow.*(en + nb).
Deferred: tel:/mailto:/pre-filled compose — Tenant schema chưa có contactPhone/contactEmail field (chỉ OWNER User có). Sẽ add sau khi onboarding có bước capture salon contact, hoặc coi là P2 follow-up.
[x] P1-3 · paymentMode: IN_PERSON cho phone/walk-in booking
File: 01-create.md case #3 · Shipped 2026-04-24.
Implementation:
BookingService.createderivespaymentMode='IN_PERSON'whendto.source === BookingSource.PHONEand passes it tobuildCreatedEvent. Walk-in path already set it (P0-3).OnBookingCreatedlistener already short-circuits onpaymentMode === 'IN_PERSON'(P0-3 wiring) — no further change.
- Schema column deferred — the value only flows through the event payload and
onBookingCreatedis the sole consumer, so persisting onBookingadds no business value today. The adminBookingDraweralready exposes asourcedropdown (ONLINE/PHONE/ADMIN), which the staff picks; the derivation is automatic from that. ADMINsource kept on the PSP path — admin-entered bookings may still send a payment link to the customer via SMS (future: explicit paymentMode override when admin needs to bypass).
Tests: 3 new unit tests in booking.service.spec.ts (PHONE→IN_PERSON, ONLINE→undefined, ADMIN→undefined).
[x] P1-4 · Distinguish transient vs permanent Payment failure + retry UI
File: 05-payment-driven.md §3 · 07-edge-cases.md §3a · Shipped 2026-04-25.
Implementation:
- Domain — new
PaymentFailureKindenum (TRANSIENT | PERMANENT).Payment.markFailed(code, message, kind, at)stamps the kind ontoPaymentFailedPayload. Not persisted on the aggregate (failed Payment is terminal so rehydration doesn't need it) — listeners read it once when the event fires. - Webhook classification —
process-webhook-inbox.service.tsmaps Bamborapayment.rejected/payment.rejected_capture→PERMANENT(card declined / fraud / expired). TRANSIENT path is reserved for future provider-timeout / 5xx flows that don't yet exist (Bambora's adapter retries 5xx inside the call). - Listener split (
OnPaymentSettledNegativeListener):- PERMANENT + PENDING + failed-count
<PAYMENT_RETRY_CAP(3) → keep PENDING; projection writes RETRY_PENDING; customer can retry with a different card. - PERMANENT + PENDING + failed-count
>=cap → cancel withreason=PAYMENT_RETRY_EXHAUSTED. - TRANSIENT + PENDING → never cancels (provider-health, not customer-card).
- PaymentExpired path unchanged from P1-11.
- PERMANENT + PENDING + failed-count
- Projection —
PaymentFailed(any kind) →BOOKING_DEPOSIT_STATUS.RetryPending. New value added to the const map;PAYMENT_FAILEDremains as the legacy projection key for completeness but the listener never writes it (the listener owns "give up and cancel"; once cancelled, depositStatus is informational and UI keys off booking.status). - Retry endpoint —
POST /public/tenants/:slug/bookings/:id/payment/retry(CustomerAuth). Mints a new Payment row withidempotencyKey = bk-${id}-retry-${attempt}, reusing the original Money + intent + captureMode so the customer pays the SAME deposit they originally saw (no settings re-derivation). Guests deferred to a future magic-link flow (docs/flows/status-matrix/05-payment-driven.md§retry-guests). - Notifications (
OnPaymentFailedRetryNotificationListener) — fires on PaymentFailed. Sends SMS (existing channel) + email (new channel viaEmailProviderport;LOGimpl ships, SMTP/SendGrid pluggable viaEMAIL_PROVIDERenv). Skips when booking already past PENDING (race), failed-count at cap (about-to-cancel), or no contact at all. New templateBOOKING_PAYMENT_RETRYwith{retryUrl}placeholder pointing to the customer portal deep link. - Frontend —
account/BookingsSection: yellow "Retry payment" button whendepositStatus=RETRY_PENDING && status=PENDING, firesPOST /payment/retryand redirects to the newcheckoutUrl. Email deep-link (?retry=<id>) auto-fires the mutation on landing and strips the param so a refresh doesn't churn.api-client.CUSTOMER_AUTHED_PUBLIC_PATHSextended to cover/payment/retry+/cancel-preview.
Tests: 9 new listener specs + 9 new retry-endpoint specs + 5 new processor email-channel specs + 4 new domain specs around failureKind. Existing markFailed callers updated to pass PaymentFailureKind.PERMANENT.
Deferred:
- Guest retry (no JWT) — needs magic-link via email + transient
bookingReftoken. Tracked under P2-20 follow-up (TBD).
- Real SMTP/SendGrid impl for
EmailProvider—LogEmailProvideris dev-only; production wiring waits on vendor sign-off.
- Server-side automatic TRANSIENT retry (no customer click) — current spec only triggers retry on customer action.
[x] P1-5 · autoConfirm=true + depositEnabled=true guard
File: 01-create.md §3, 02-happy-path.md §T1 real-world case #4. Shipped: 2026-04-24.
Shipped (Option 1 — block the combo, per recommendation):
- New shared helper
tenant-settings.validation.ts::validateSettingsCombination— runs on every settings write (create, update, onboarding) against the full merged state (not just the patch) so a per-field PATCH that would end up with both true still throws. - Error code
TENANT_SETTINGS_AUTOCONFIRM_DEPOSIT_CONFLICT(422UnprocessableEntityException) + nb/en translations. - Frontend
BookingPolicyEditorwires the two toggles as mutually exclusive — eachSwitchFieldgains adisabledprop (new) driven by the other's current value; description swaps to an explanation of why the toggle is locked ("Turn off Require deposit to enable this"). User never hits the API guard under normal flow. - Tests: 6 pure-helper unit tests + 3 service integration tests (reject on update, reject when both flipped in one patch, allow disabling one side of a legacy row).
Why Option 1: deterministic, simple, no risky downgrade-to-PENDING listener logic, and keeps the negative payment listener's status !== PENDING short-circuit intact (was the whole reason for the bug).
[ ] P1-6 · Loyalty L4 lifecycle listeners
File: 06-loyalty-coupling.md §4, 04-no-show.md case #6.
Vấn đề: RESERVED redemption stuck forever nếu booking COMPLETED/CANCELLED/NO_SHOW vì L4 chưa ship.
Fix:
on-booking-completed.loyalty.listener.ts— RESERVED → CONSUMED.on-booking-cancelled.loyalty.listener.ts— phân biệt pre-capture (restore) vs post-capture (forfeit).on-booking-no-show.loyalty.listener.ts— RESERVED → FORFEITED.- Clawback POINTS_BASED: create
LoyaltyPointTransaction{type: CLAWBACK, points: +N}. - Stamp restore VISIT_BASED: re-insert stamp rows to cycle trước.
- Idempotent via status check.
- Migration dọn data orphan RESERVED cho booking terminal trước L4.
Effort: ~2 ngày. Listeners + migration + unit/e2e tests.
[ ] P1-7 · Reschedule rules + UX
File: 03-cancel.md §9.
Vấn đề: Update booking startTime không có guard khi out-of-window. Spec ngành: cho đổi đến X giờ trước.
Fix:
- Thêm setting
rescheduleHours(separate từcancellationHours, default = same). updateendpoint check guard khistartTimechanges + performer = CUSTOMER.- FE: prefer "Reschedule" CTA trong out-of-window dialog (P1-2) thay vì chỉ "Contact salon".
Effort: ~1 ngày.
[x] P1-8 · Cancel preview: hiển thị refund amount
File: 03-cancel.md §10 admin UX · Shipped 2026-04-24.
Implementation:
- Pure helper
buildCancellationPreview()sits on top ofdecideCancellationRefundincancellation-refund-policy.ts— returns{ decision, refundAmount, forfeitAmount, voidAmount, hoursUntilStart, withinWindow, cancellationWindowHours, currency }. Mirrors theFULL_REFUND → VOIDdefensive fallback fromPaymentIntegrationService.onBookingCancelledso the preview matches what actually executes. - Admin endpoint
GET /bookings/:id/cancel-preview(STAFF/OWNER/ADMIN) —cancelledBy=SALON. - Public endpoint
GET /public/tenants/:slug/bookings/:id/cancel-preview(CustomerAuth) — same ownership check as POST cancel;cancelledBy=CUSTOMER. - FE
CancelPreviewDialoginterceptsCANCELLEDstatus change — admin (BookingList + BookingDrawer) and customer portal (account/BookingsSection) all fetch the preview before firing the mutation. Messages localized (en + nb) per decision: refund amount, void release, forfeit, no-action, no-payment.
Tests: 11 new specs in cancellation-refund-policy.spec.ts, 7 in booking.service.spec.ts, 1 in booking.controller.spec.ts, 4 in public-booking.controller.spec.ts.
Effort: ~1 ngày.
[x] P1-9 · depositStatus projection listener
File: 05-payment-driven.md §4, §5, §6. Shipped: 2026-04-24.
Shipped:
- Schema: nullable
Booking.depositStatus TEXTcolumn (enum-isation deferred to P2-8). - Migration
20260424085654_add_booking_deposit_status_projection— schema + backfill from the latest DEPOSIT-intent Payment per booking in a single SQL upsert (DISTINCT ON (booking_id)). OnPaymentStateProjectionListenerinbooking/— subscribes all 8 Payment events, maps viaBOOKING_DEPOSIT_STATUSconst (PENDING, AUTHORIZED, PAID, VOIDED, REFUNDED, PARTIALLY_REFUNDED, PAYMENT_FAILED, EXPIRED). Write viaupdateManywithwhere: { id, tenantId, NOT: { depositStatus } }— cross-tenant guard + idempotent re-delivery in a single statement. Ad-hoc payments (bookingId=null) skipped.- Event payloads enriched:
PaymentCaptured,PaymentRefunded,PaymentPartiallyRefunded,PaymentVoidednow carrybookingIdso the projection listener doesn't need a Payment lookup. Authorized / Failed / Expired / Initiated already had it. - Tests: 13 new unit tests (happy path for each of the 8 events table-driven, subscription wiring, null-bookingId skip, cross-tenant guard, idempotent re-delivery, unrelated-event no-op).
Follow-ups: admin UI deposit badge (front-end work, unblocked), P1-10 customer refund/void notifications, P1-5 autoConfirm + depositEnabled guard (depends on this projection).
[x] P1-10 · Notification customer khi refund / void
File: 05-payment-driven.md §5, §6. Shipped: 2026-04-24.
Shipped:
- 3 new
NotificationType:BOOKING_REFUNDED,BOOKING_PARTIALLY_REFUNDED,BOOKING_VOIDED. - Norwegian SMS templates +
formatMoneyhelper (minor-unit → "125 NOK" viatoLocaleString('nb-NO')).{amount}và{cumulativeAmount}placeholder reuse shared renderer. OnPaymentNotificationListenerincore/booking/— subscribes PaymentRefunded / PartiallyRefunded / Voided, resolves booking + phone (prefer linked customer, fallback to booking snapshot phone for guest bookings), enqueues viaNotificationService. Cross-tenant guard, null-bookingId skip, no-phone skip (debug log), queue-failure swallow (fire-and-forget so outbox doesn't retry the real work).- Tests: 11 new unit tests (3 happy-path per event type, template render, null-bookingId, cross-tenant, phone fallback, no-phone drop, queue failure soft-fail, unrelated-event no-op, subscription wiring).
Follow-up: email channel (SMS-only for now, per current NotificationProcessor). P2-7 NoShow notification reuses the same template pattern.
[x] P1-11 · PaymentExpired cho booking CONFIRMED (lead-time > 7d)
File: 07-edge-cases.md §2b · Shipped 2026-04-25.
Implementation (combined option 1 + option 2):
- Settings hard cap —
validateSettingsCombinationrejectsdepositEnabled=true && maxBookingDaysInAdvance > 7withTENANT_SETTINGS_DEPOSIT_LEAD_TIME_CONFLICT. New constantDEPOSIT_LEAD_TIME_CAP_DAYS = 7(the cap is the PSP's 7-day auth hold; bump if we move to a provider with longer holds). FEbookingPolicySchema.superRefinemirrors the rule (inline errormaxBookingDaysDepositMax); error code translated to a friendly toast in en + nb. - Listener auto-cancel —
OnPaymentSettledNegativeListenerextended:PaymentExpirednow also fires forCONFIRMED(PENDING was already handled). CallsbookingService.updateStatus(CANCELLED, role: SYSTEM, { reason: 'AUTHORIZATION_EXPIRED' }). SYSTEM bypassesisValidTransition+ cancellation-window guard (noforce=trueneeded).PaymentFailedkeeps PENDING-only behaviour (CONFIRMED with a failed retry must keep its prior auth). ARRIVED/IN_PROGRESS/COMPLETED/CANCELLED skipped. - Audit reason for SYSTEM —
BookingService.updateStatusnow writesoptions.reasonto the auditnotecolumn when called from SYSTEM (previously only forced admin overrides got a reason recorded). - Notifications — existing
BOOKING_CANCELLEDSMS path fires automatically via the resultingBookingCancelledevent; no new notification listener needed.
Tests: 5 new validator + tenant-service spec cases; 5 new listener spec cases (CONFIRMED+expired, ARRIVED skip, COMPLETED skip, plus undefined reason arg on existing PENDING paths).
P2 — Post-MVP nice-to-have
[ ] P2-1 · Phân biệt STAFF-cancel-for-customer vs SALON-cancel
File: 03-cancel.md §1, case #3.
Hiện tất cả non-CUSTOMER = 'SALON' → always full refund. Case staff cancel hộ khách muộn nên theo CUSTOMER policy (forfeit).
Fix: add body onBehalfOf: 'CUSTOMER' | 'SALON' trong cancel request. STAFF default = ON_BEHALF_OF_CUSTOMER, OWNER default = SALON.
[ ] P2-2 · VIP per-customer skip deposit
File: 01-create.md case #5.
Thêm TenantCustomer.trustLevel: 'NORMAL' | 'VIP'. Create booking với customerId VIP → depositAmount = 0 regardless of settings.
[ ] P2-3 · Walk-in emit BookingCreated with IN_PERSON flag
Đã cover trong P0-3 + P1-3.
[ ] P2-4 · UX cảnh báo lead-time > auth hold
File: 07-edge-cases.md §2b.
Settings form đã có warning. UX book customer: nếu chọn date > 7 ngày + depositEnabled → hiển thị inline "Deposit hold chỉ 7 ngày, nếu không có update từ salon sẽ cần re-auth."
[ ] P2-5 · Force cancel IN_PROGRESS với partial refund UX
File: 03-cancel.md case #7.
Khi OWNER force IN_PROGRESS → CANCELLED: hỏi "refund bao nhiêu?" + gọi RefundPaymentCommand với amount.
[ ] P2-6 · Customer no-show counter + display
File: 04-no-show.md §4.
Schema: TenantCustomer.noShowCount. Listener OnBookingMarkedNoShow increment. UI: badge "3 no-shows trong 6 tháng" trong customer detail + booking drawer để staff warning.
[ ] P2-7 · Notification email cho NoShow + void
File: 04-no-show.md §4, 05-payment-driven.md §6.
Templates nb/en cho NoShow apology + void confirmation.
[ ] P2-8 · Enum hóa depositStatus
File: 05-payment-driven.md §4.
Thay string bằng Prisma enum. Migration backfill. Tight coupling Booking schema với Payment lifecycle → cần cân nhắc.
[ ] P2-9 · payableTotal = 0 case
File: 06-loyalty-coupling.md case #1, 07-edge-cases.md §5a.
Khi loyalty discount = 100% → không có payment → booking stuck PENDING nếu autoConfirm=false.
Fix: onBookingCreated nếu depositAmount = 0 + depositEnabled=true → auto-publish BookingConfirmed via SYSTEM role.
[ ] P2-10 · Loyalty event contract + notification
File: 06-loyalty-coupling.md §8.
LoyaltyRedemptionConsumed + StampEarned + PointsEarned → notification "Bạn đã có stamp mới / +N điểm".
[ ] P2-11 · Cross-aggregate outbox ordering
File: 07-edge-cases.md §1a.
Nghiên cứu: cần guarantee ordering giữa PaymentAuthorized + BookingCancelled cho cùng booking? Hiện mỗi aggregate có ordering riêng. Risk assessment.
[ ] P2-12 · Admin ops UI: outbox dead letter + webhook retry
File: 07-edge-cases.md §2d, §7a.
/admin/ops/outbox + /admin/payments/webhooks. List + retrigger manual.
[ ] P2-13 · Reconciliation cron verify shipped
File: 07-edge-cases.md §3c.
Audit code: ReconciliationJob có chưa? Nếu chưa → implement (Flow 13 payment-flow.md).
[ ] P2-14 · Toggle depositEnabled=false migration
File: 07-edge-cases.md §4a.
Dialog cảnh báo N booking pending + lựa chọn "cancel all" / "keep and skip deposit".
[ ] P2-15 · Grandfather cancellationHours change
File: 07-edge-cases.md §4b.
Snapshot cancellationHours lên booking tại create. updateStatus dùng snapshot thay vì live settings.
[ ] P2-16 · Update booking items sau Payment captured
File: 07-edge-cases.md §5b.
Policy: block edit items sau capture, hoặc auto-refund difference. Decision cần.
[ ] P2-17 · Optimistic locking booking edits
File: 07-edge-cases.md §6a.
If-Match: updatedAt header.
[ ] P2-18 · Outbox retention job
File: 07-edge-cases.md §7b.
Cron xóa published_at < now() - 30d.
[ ] P2-19 · Manual payment reconcile endpoint
File: 07-edge-cases.md §7d.
PATCH /admin/payments/:id/reconcile để fix aggregate corrupt.
P3 — Future / out-of-scope
- Cron auto-mark NoShow sau grace (no manual click).
- Loyalty L5 customer portal UI.
- Loyalty L6 admin redemption picker.
- POS integration (gate M2 guard).
- Multi-location (Organization layer).
- SMS retry payment link.
Recommended execution order
Sprint 1 (1 tuần) — unblock real salon
- P0-1 + P0-2 + P0-3 (combined PR) — force cancel + deposit guard + walk-in event
- P1-1 customer cancel endpoint
- P1-2 out-of-window dialog
- P1-9 depositStatus projection (enabler cho P1-5, P1-10)
Sprint 2 (1 tuần) — stability + UX
- P1-3 paymentMode IN_PERSON
- P1-4 payment retry
- P1-5 autoConfirm+deposit conflict block
- P1-11 7-day lead-time enforcement
- P1-10 refund notifications
- P1-8 cancel preview
Sprint 3 (1 tuần) — loyalty L4
- P1-6 L4 listeners (COMPLETED + CANCELLED + NO_SHOW)
- P1-7 reschedule guard
Post-MVP (ad-hoc)
- P2 items theo priority của business.
Tracking
Mỗi item resolved → update:
- File gốc: đổi
[!]/[ ]thành[x]. - File này: check box của item.
- Git commit message:
fix(status-matrix): resolve P0-1 force cancel out-of-window. - Re-run
npx gitnexus analyzesau merge để refresh index.