shipped

Spec: F09 — Commitments

Status: shipped · Audited: 2026-06-10

1. Problem & CEO Job

Trust the signal. Cam kết is a first-class Vietnamese business concept: a promise made before colleagues carries personal weight. Customers churn and suppliers defect when promises go untracked. Lumi makes every customer, internal, and supplier promise visible, owned, dated, and impossible to silently forget.

What we replace: Zalo + Excel + the owner’s memory. A fit-out contractor’s promises — “anh hứa gửi báo giá thứ 5,” “đặt cọc xong em giao vật tư tuần sau” — live in Zalo, an occasionally updated spreadsheet, or the boss’s head. The team has no task system; retyping a verbal cam kết takes 30–60 seconds, breaks the flow, and is usually skipped, so chat scroll erases it. When a pulse captures the promise, Lumi creates a dated, owner-attributed record with no extra click for its maker. It is not a better task inbox; it is the first durable promise record for a Vietnamese SME.

2. User Stories & Acceptance Criteria

  • JTBD (member): “When I mention a promise I made in my daily check-in — ‘tôi đã hứa gửi báo giá thứ 5’ — I need it to become a tracked commitment automatically, so I don’t have to re-type it into the Zalo group or a shared Excel sheet that I’ll forget to update.”
    • Given AI extraction (F08), then commitment carries owner, team, evidence, created_by_type=ai.
    • Given no member matches the owner name extracted, then owner_name_raw preserves the AI’s name guess and the commitment renders with an unassigned indicator — the CEO can assign later.
  • JTBD (CEO): “When I open the commitment list, I need to instantly see who owns what promise and when it’s due — without opening individual rows — so I can tell which relationships need my attention before they break. In Excel this means a sheet someone has to keep current by hand, and in Zalo it means scrolling threads to reconstruct who promised what. Lumi renders it in one page with zero upkeep.”
    • Given the list, then each row shows title, owner (or owner_name_raw for unmatched names), stakeholder, due date, status — scannable without opening.
    • Given due_date in the past and status open/in_progress, then the commitment renders as overdue and feeds Brief §3 (F12).
    • Given an unassigned commitment (no owner, owner_name_raw only), then the row shows a visual “needs owner” indicator distinct from overdue — because a promise without a named person attached has no one to hold accountable, and the CEO’s job is to close that gap.
  • JTBD (CEO): “When Lumi extracts a commitment but can’t identify who owns it from context, I need to assign an owner in one tap from the list, so the commitment doesn’t rot in an unassigned state — the way a promise in a Zalo group has no named owner and a row in Excel sits blank until someone backfills it.”
    • Given a commitment with owner=NULL and owner_name_raw populated, then the list row shows an inline “Gán chủ sở hữu” button. (CommitmentsPage.tsx: button rendered only when row.needs_owner === true)
    • Given the CEO taps assign, then a member selector appears; on selection the commitment’s owner is set immediately with no page reload. (inline select with onChange → handleAssignOwner, no reload)
  • JTBD (manager): “When I notice a promise someone made in conversation that the AI missed — maybe they said it in a Zalo group, not in their pulse — I need to record it manually with stakes (money, stakeholder) so AI coverage gaps don’t lose promises.”
    • Given a manual create with only a title, then it saves (open, medium, customer stakeholder default).
    • Given money amount and stakeholder_type filled, then the commitment surfaces in the CEO’s view with higher visual priority than undated low-stakes promises.
  • As a client traversing long commitment history, I want an indexed cursor path without breaking shipped page clients.
    • pagination=cursor traverses tenant-scoped commitments by descending UUIDv7 id, preserves filters, and returns the existing four-key envelope without deep-page OFFSET.

3. Scope Boundaries

  • In: CRUD, AI-extracted creation, stakeholder typing (customer/internal/supplier/finance/candidate/other), money fields, due date+time, status lifecycle, link to a fulfilling Task.
  • Out: customer CRM records (stakeholder is a name string by design — no CRM), notifications on due (Brief is the surfacing channel), approval workflows.
  • Cross-link (F71 / Loop 2): a commitment to an external party — a customer (“anh hứa duyệt báo giá thứ 5”), a supplier (delivery/price), or a subcontractor (handover/nghiệm thu) — is a natural home for a confirmation link (F71). The owner can send the counterparty a no-login link to confirm the promise (đã xác nhận); the confirmation updates the brief and puts a future Lumi buyer in contact with the product. F09 owns the commitment record; F71 owns the external confirmation surface — do not duplicate the sign-off flow here.

4. Data Model

operations.Commitment — status: open → in_progress → done | blocked | cancelled (+ overdue, see §9). confidence_score, evidence_sources M2M, extracted_from_pulse_entry, created_by_type (user/ai), resolved_at. owner_name_raw keeps the AI’s name guess when no member matches.

5. API Contract

MethodPathMin roleNotes
CRUD/api/commitments/member (write) / viewer (read)Router ViewSet, tenant-scoped; page default, pagination=cursor opt-in

Cursor mode is ordered by descending UUIDv7 id; clients follow opaque links and never construct cursor values. Alternate cursor ordering is rejected with 400 rather than silently ignored. Page mode remains the default. Both modes return {count,next,previous,results}.

6. UI / UX

  • /intel/commitments (CommitmentsPage.tsx) — list, status badges, overdue emphasis.
  • AI-created rows show provenance (confidence + evidence) — a tap answers “why does Lumi think this?”
  • States: empty (“Lumi sẽ tự phát hiện cam kết từ pulse — hoặc thêm thủ công”), loading skeletons.
  • Per-commitment Share on every open Commitment card on Today (growth / atomic distribution-object). Each open Commitment card in the F13 Today Commitments section renders a ghost-variant Share2 icon button alongside the card content. Clicking it builds a focused single-commitment artifact via the new frontend/src/hooks/useCommitmentShare.ts hook — same fallback chain, same AbortError no-fallback, same share_copied / share_error toasts from the brief namespace as the whole-brief, per-item-brief, per-decision, and per-risk shares, so the five share surfaces can never drift. The artifact shape is commitment-specific: an identity-stamp header that reads {org} · Lumi · Commitment · {date} when the sender’s organization name is available (the Notion / Figma / Calendly distribution-object invariant — mirroring the whole-brief share #392, the share-card image #437, and the per-item brief share #442 so every CEO-forwarded artifact carries identifiable letterhead instead of an anonymous Lumi header) and falls back to Lumi · Commitment · {date} when missing / whitespace / hydrating (sharing is never blocked), commitment title, 👤 Owner: {owner_name_raw} line (when populated), 📅 Due: {due_date} line (when populated), ⏰ Overdue line (derived state — due_date < todayDate, both required, matching the F09 §13 2026-06-10 derived-state convention), and the same generic-or-referral footer (”— Made with Lumi · join free: {url}”) embedded with the CEO’s F59 referral link via buildReferralUrl(referralCode?.code). Rendered only for open commitments (c.status === 'open') — done/cancelled commitments are not forwarded. The button stays inside the executive-gated Commitments section, so members never see it (matches the existing role gate on the section itself). This completes the atomic-share family across every Today item type: commitments are the accountability-forward CEO gesture (“@head_of_sales, you committed Friday — status?”), the volume play in the share series (higher daily frequency than risks, lower stakes per share than decisions).
  • Per-commitment Share on the /intel/commitments triage queue (growth / atomic distribution-object). Same affordance shipped to the deeper triage page (CommitmentsPage.tsx) — the page CEOs walk through every open commitment on during morning triage. The triage queue is the highest-volume of the intel-trio Share family (often 20+ rows on a typical CEO morning), making this the volume play in the multi-surface share series. Implementation reuses the same useCommitmentShare hook, the same buildCommitmentShareText builder, the same share_button aria-label pattern (${t('share_button')}: ${row.title}), the same c.status === 'open' gate, and the same todayIso derivation as the Today twin — so the two commitment-share surfaces (Today and /intel/commitments) cannot drift on artifact, fallback chain, attribution footer, or aria semantics. The button is rendered at the head of the existing actions column on every status === 'open' row (mirroring the Share · Resolve · Dismiss ordering on RisksPage and the Share · Decide · Cancel ordering on DecisionsPage). Closes the intel-trio Share-affordance audit: per-risk on /intel/risks (#419) · per-decision on /intel/decisions (#421) · per-commitment on /intel/commitments (this PR, #TBD).
  • Per-commitment Share-image affordance on every open Commitment card on both Today and /intel/commitments (growth · per-item visual identity-stamp arc, third and final leg). Sibling ImageDown ghost-variant icon button rendered next to the text Share2 button on every open Commitment card / row. Wired to the new frontend/src/hooks/useCommitmentShareImage.ts hook, which builds a 1080×1080 branded PNG card via frontend/src/lib/commitmentShareCard.ts (a pure SVG builder that mirrors riskShareCard.ts 1:1 — same palette, identity-stamp position, eyebrow shape, doorway-footer band — with a deadline-coloured accent rail on the left edge of the white sheet that matches the in-app railRed emphasis TodayPage paints on past-due Commitment cards, and a due-date line in the same accent so the receiver reads urgency at chat-thumbnail size). Card shape: identity letterhead at top-left ({org} when present), COMMITMENT eyebrow with brand mark and today’s date, commitment title as headline (wrap up to 4 lines × 30 chars, navy weight 700 size 50), owner line (👤 Owner: {owner_name_raw} when populated — the named-receiver invariant), due-date line in the deadline accent colour, ⏰ Overdue marker (when derived — due_date < todayDate, both present, F09 §13 2026-06-10 derived-state convention), optional description excerpt (up to 2 lines), F59-referral doorway-footer band. Deadline accent mapping: overdue → risk-red #C2410C, due-today → signal-amber #F4A62A, future → lumi-blue #006093, unscheduled / no-today → soft-slate #5E6677. Rasterises via the existing svgToPngBlob from briefImage.ts (zero new dependency), then runs the same Web Share API ({ files, text }) → download fallback chain useRiskShareImage already uses. buildCommitmentShareText rides along as the share-sheet text payload so a recipient with image previews disabled still gets readable context. Falls back to a PNG download with the commitments.card_share_image_saved toast on desktop; share_image_error toast (reused from brief namespace) on failure. Disabled while saving. Same c.status === 'open' / row.status === 'open' gate as the text-share button. Closes the per-item visual identity-stamp arc 3/3 (decisions ✅ #472, risks ✅ #477, commitments ✅ this PR). In Lumi’s image-first wedge markets (Zalo / WhatsApp / Messenger / Viber) screenshots forward where pasted text doesn’t, and the commitment image is the highest-volume named-owner accountability forward in the share family.
  • i18n: commitments (adds share_button, share_header, share_header_nodate, share_owner, share_due, share_overdue — six commitment-specific keys; plus share_header_with_org and share_header_with_org_nodate — two identity-stamp variants used when the sender’s organization name is present, mirroring the brief namespace’s _with_org keys from PR #392; plus card_eyebrow (“COMMITMENT”) and card_share_image_saved added 2026-06-23 for the per-commitment share-card image). The per-commitment share artifact reuses the brief namespace’s footer (share_footer, share_footer_referral), toast (share_copied, share_error), and image-affordance keys (share_image_button, share_image_error, card_footer, card_footer_referral) — same copy as every other share surface, so one copy edit lands on all five shares.

7. Test Plan

#CaseTypeWhere
1CRUD, defaults, cross-tenant blockedbackendtests/test_api.py::TestCommitment
2AI creation path with evidencebackendtests/test_api.py::TestAIExtraction
3Overdue feeds briefbackendtests/test_api.py::TestBriefGeneration
4List + create flowe2edescribe("Commitments")
5Per-commitment share artifact builder: dated/undated header, title, owner line (present/absent), due line (present/absent), overdue derivation (past/future/null today), full ordering, generic vs referral footer, identity-stamp header (org name present dated/undated), defensive whitespace fallback to anonymous header, org name + referral footer co-exist (identity & growth doorway hold together)frontendcommitmentShare.test.ts (15 deterministic cases — 11 original + 4 identity-stamp)
6Per-commitment Share button renders inside every open Commitment card on Today (executive section), absent on done/cancelled commitmentsfrontendTodayPage.test.tsx::renders per-commitment Share button on open commitments only
7Per-commitment Share opens native share sheet with the commitment artifact + referral footer; clipboard fallback + success toast when no navigator.sharefrontendTodayPage.test.tsx::per-commitment Share opens native sheet with the referral-footer artifact / per-commitment Share falls back to clipboard when no native share sheet
8Per-commitment Share button renders inside every open Commitment row on /intel/commitments triage queue, absent on done/cancelled rowsfrontendCommitmentsPage.test.tsx::renders per-commitment Share button on open commitments only
9Per-commitment Share on /intel/commitments opens native share sheet with the commitment artifact + F59 referral footer; clipboard fallback + brief-namespace success toast when no navigator.sharefrontendCommitmentsPage.test.tsx::per-commitment Share opens native share sheet with the referral-footer artifact / per-commitment Share falls back to clipboard when no native share sheet
10Per-commitment share-card image SVG builder: well-formed 1080×1080 doc, eyebrow + date, commitment title headline, owner line (when populated), due line (when populated), overdue marker (derived past/future/null today), description body (when populated), generic vs referral footer, XML-escape, code-point-aware truncate; deadline-coloured accent rail + due-line accent (overdue/due-today/future/unscheduled map to risk-red/signal-amber/lumi-blue/soft-slate, plus slate fallback when todayDate is null); identity-stamp organization name (present, empty/whitespace/undefined fallback, over-long truncate, XML-escape, surrogate-pair safety)frontendcommitmentShareCard.test.ts (26 deterministic cases — 15 base + 5 deadline-accent + 5 identity-stamp + baseline preservation)
11Page compatibility, cursor traversal, tenant isolation, filter retention, no SQL OFFSET, indexbackendtests/core/test_high_value_cursor_pagination.py

8. Current Behavior

Shipped. Commitments are extracted, listed, and managed as first-class operational objects with tenant scoping and ordinary CRUD/edit flows in the web UI. Active viewers remain read-only: they can list and retrieve commitments, but create/update/delete/resolve mutations are denied by IsCommitmentWriter.

9. Error States

#ErrorResponseNotes
ES1Missing required title400 {"title": ["This field is required."]}Implicit in CRUD tests
ES2Invalid status value — POST/PATCH with status="overdue" (removed from STATUS_CHOICES) or status="invalid"400test_create_commitment_rejects_overdue_status
ES3Cross-tenant commitment access404test_commitment_tenant_isolation
ES4Cross-tenant task FK — POST with task UUID from another org400 — CommitmentSerializer.validate_tasktest_commitment_cross_tenant_task_fk_rejected (tests/api/test_operations.py ~262); test_commitment_rejects_cross_tenant_task (tests/test_red_r24_e.py ~190)
ES5Cross-tenant evidence_sources — POST with evidence_sources UUIDs from another org400 — CommitmentSerializer.validate_evidence_sourcestest_cross_tenant_evidence_source_rejected (tests/api/test_operations.py ~252); test_commitment_rejects_cross_tenant_evidence (tests/test_red_r24_e.py ~175)
ES6Invalid stakeholder_type400 (model choices)test_create_commitment_rejects_invalid_stakeholder_type
ES7Frontend load failuretoast.error(t('error_load'))No retry; no E2E test
ES8Frontend create failuretoast.error(t('error_create'))Form input preserved; no E2E test
ES9Frontend resolve failuretoast.error(t('error_resolve')); list reloadsNo E2E test
ES10Overdue filter with NULL due_date — ?status=overdueNULL due_date excluded (never < today); correct behaviortest_overdue_filter_excludes_null_due_date
ES11Cross-tenant evidence_sources — serializer validates org scope400 — CommitmentSerializer.validate_evidence_sourcestest_commitment_rejects_cross_tenant_evidence (tests/test_red_r24_e.py ~175)
ES12Cross-tenant task FK — serializer validates org scope400 — CommitmentSerializer.validate_tasktest_commitment_rejects_cross_tenant_task (tests/test_red_r24_e.py ~190)

10. Diagrams & Docs Touched

backend/DIAGRAMS.md Commitment state machine · GLOSSARY.

11. Implementation Audit (2026-06-14)

#ClaimStatusEvidence
1overdue removed from STATUS_CHOICES; ?status=overdue derived from due_date__lt=today in queryset; displayStatus() in frontend✅ match2026-06-10; migration 0003_alter_commitment_status; test_overdue_filter_excludes_null_due_date
2AI-extracted commitment carries owner, team, evidence, created_by_type=ai✅ matchF08 extraction; test_api.py::TestAIExtraction; EvidenceSource linked
3needs_owner computed field: true when owner=null and owner_name_raw populated✅ matchA-r27; CommitmentSerializer.needs_owner; 5 dedicated tests
4Inline “Gán chủ sở hữu” button — member select, handleAssignOwner, no page reload✅ matchA-r27 / 2026-06-14 closed; CommitmentsPage.tsx
5due_time granularity preserved (optional, not displayed when null)✅ matchCommitment.due_time nullable; frontend conditional display
6Overdue commitments feed Brief §3 (F12) via due_date__lt=today, status in (open, in_progress)✅ matchtest_api.py::TestBriefGeneration; Brief ranking §3
7Money-amount visual priority (higher sort/display weight for money_amount non-null)✅ fixedClosed 2026-06-18: CommitmentsPage.tsx renders stakeholder + money metadata in title cell; client-side prioritization favors money_amount rows, then due-date, then priority (changelog §13 entry 2).
8No unique constraint on (title, owner, org) — AI creates duplicate commitments for same promise⚠️ gapE4: test_duplicate_commitment_title_allowed; no dedup in F08 extraction pipeline
9Cross-tenant evidence_sources M2M rejected with 400✅ fixedCommitmentSerializer.validate_evidence_sources (backend/apps/operations/serializers.py ~26); test_cross_tenant_evidence_source_rejected (tests/api/test_operations.py ~252); test_commitment_rejects_cross_tenant_evidence (tests/test_red_r24_e.py ~175)
10Cross-tenant task FK rejected with 400✅ fixedCommitmentSerializer.validate_task (backend/apps/operations/serializers.py ~33); test_commitment_cross_tenant_task_fk_rejected (tests/api/test_operations.py ~262); test_commitment_rejects_cross_tenant_task (tests/test_red_r24_e.py ~190)
11Visual QC: commitment list opens and screenshots the F71 confirmation-link dialog✅ match2026-06-18 fix; CommitmentsPage.tsx now renders SendConfirmationDialog; CommitmentsPage.test.tsx; 09-commitments/02-send-confirmation-dialog.png
12Visual QC: stakeholder and money exposure are visible in-list, and money-heavy promises rise above undated low-stakes rows✅ match2026-06-18 Commitments pass; CommitmentsPage.tsx renders stakeholder + money metadata in the title cell and applies client-side prioritization for money_amount / due-date / priority

Summary: 11/12 match; 1 gap remains (duplicate extraction — row 8). Row 7 (money-amount visual priority) closed 2026-06-18.

Edge Cases Catalog (formerly §11, 2026-06-12): 13 edge cases (E1–E13) documented: no due_date behavior, no FSM enforcement, resolve idempotency, AI duplication (no dedup), cross-team org-scoped visibility, owner SET_NULL, AI field spoofing, cross-tenant evidence FK, stakeholder_name/company inconsistency, due_time without due_date, blocked→done transition, concurrent resolve, cross-tenant task FK. Key items integrated into audit rows 7–10 and §9 error states. Full catalog in git history (2026-06-12 changelog entry).

12. Open Questions / Decisions

  • Linking a commitment to the Task that fulfills it exists in the model but has no UI — promote only when users ask “how do I mark this handled?” in practice.

Known implementation gaps (§2 unchecked ACs)

  1. Inline owner assignment ✅ CLOSED (2026-06-14): CommitmentsPage.tsx renders the amber “Gán chủ sở hữu” button only when row.needs_owner === true (i.e., owner=NULL and owner_name_raw populated). Clicking it opens an inline <select> populated from useMembersList(); onChange calls useAssignCommitmentOwner() which PATCHes owner FK and invalidates the list query (no page reload). Backend needs_owner computed field in CommitmentSerializer drives the condition.

  2. Money-amount visual priority (§2 story 1, AC line 33): ✅ CLOSED (2026-06-18). CommitmentsPage.tsx now shows stakeholder + money metadata directly in the title cell and sorts default list order to favor commitments with money_amount, then higher priority, then dated promises over undated low-stakes rows. This closes the CEO-facing scan gap without changing the backend API contract.

13. Bug / Change Log

2026-07-15 — Vietnamese commitment search is diacritic-insensitive

Type: Bug fix (frontend search)

The commitment title search now indexes both the original text and a folded form that removes Vietnamese tone marks and maps đ to d. Queries without a Vietnamese keyboard therefore find the same commitments while accented queries and displayed titles remain unchanged. A focused page regression covers the reported Hành động → hanh dong case.

2026-07-13 — Enforce viewer read-only commitment permissions

Type: Security hardening / Contract Graph proof

Summary:

  • CommitmentViewSet now applies IsCommitmentWriter to create, update, partial update, delete, and resolve actions while preserving viewer read access.
  • Added executable proofs for viewer create/resolve denial, member allow paths, and tenant isolation; F09 is now represented by a canonical CUE contract.
  • The remaining F09 gap is explicit: frontend unit coverage, browser journey evidence, and named Android/iOS/macOS runtime evidence are not part of this web API slice.

2026-07-13 — Additive UUIDv7 cursor pagination

Type: Performance + API compatibility

Added pagination=cursor to the commitment list with tenant-scoped -id traversal and a matching (organization, -id) index. Existing page-number clients and the four-key mobile envelope remain unchanged. Proof is shared with the other growth-sensitive chronological lists in docs/test-plan/high-value-cursor-pagination.md.

2026-07-12 — Persist commitment resolve before success feedback (tester feedback #21)

Type: Bug fix

The commitments page previously showed the success toast immediately but waited until that toast auto-closed before calling the resolve API. A CEO who refreshed while the toast was visible could therefore see the apparent completion vanish. The page now awaits the resolve mutation before showing success feedback. A frontend regression verifies mutation ordering and refreshed state, while frontend/tests/e2e/feedback-21-commitment-resolve.spec.ts proves the real route still renders the commitment as done after a full browser reload.

2026-07-02 — Upgrade cross-tenant FK validation from ⚠️ xfail to ✅ shipped (F09 spec audit)

Type: Documentation Change

Summary:

  • §9 ES4 / ES5: changed from “silently succeeds (xfail)” to 400 — CommitmentSerializer.validate_evidence_sources and validate_task (backend/apps/operations/serializers.py ~26–39) are both implemented and enforce org scope.
  • §9 ES11 / ES12: were “correct: 400 but xfail” — removed the xfail qualifier; behavior is now the canonical shipped state.
  • §11 rows 9 & 10: upgraded from ⚠️ gap to ✅ fixed; evidence and test names corrected to match real test locations (tests/api/test_operations.py ~252/262; tests/test_red_r24_e.py ~175/190).
  • §11 summary updated: 10/12 match; 2 gaps remain (duplicate extraction, money-amount server-side ranking).

Impact:

  • Documentation only. No code, API, or test behavior changed.

2026-06-23 — Per-commitment share-card image (F09 × F02 × F12 — closes the per-item visual identity-stamp arc 3/3)

Type: Feature (growth / distribution-object — additive)

  • Third and final leg of the per-item visual identity-stamp arc (decisions ✅ #472, risks ✅ #477, commitments ✅ this PR). The per- commitment image is the highest-volume named-owner accountability forward in the share family — every open commitment on a CEO’s morning Today and the deeper /intel/commitments triage queue triggers an “@head_of_sales, you committed Friday — status?” forward to one named owner. Image > text in chat-first wedge markets (Zalo / WhatsApp / Messenger / Viber): a board-deck-quality preview lands in the chat thumbnail with the deadline rail visible at thumbnail scale, where pasted text loses formatting on group-forward. With the per-decision and per-risk image cards already shipped, this PR completes the intel-trio image arc and the share family reaches its convergence point — three per-item artifacts plus the whole-brief image, every one carrying the identity letterhead.

  • New pure SVG builder frontend/src/lib/commitmentShareCard.ts produces a deterministic 1080×1080 branded PNG card for one open commitment: identity-stamp letterhead at top-left, COMMITMENT eyebrow + brand mark + today’s date, commitment title as headline (wrap up to 4 lines × 30 chars, navy weight 700 size 50), owner line (👤 Owner: {owner_name_raw} — the named-receiver invariant), due-date line in the deadline accent colour, ⏰ Overdue marker (derived state — due_date < todayDate, both present, matching the F09 §13 2026-06-10 convention), optional description excerpt (up to 2 lines), F59-referral doorway-footer band. Adds a deadline-coloured accent rail on the left edge of the white sheet — overdue → risk-red #C2410C, due-today → signal-amber #F4A62A, future → lumi-blue #0E3EB8, unscheduled / no-today → soft-slate #5E6677. Mirrors the in-app railRed emphasis TodayPage paints on past-due Commitment cards so the forwarded thumbnail carries the same urgency signal at chat-thumbnail size that the in-app row carries at desktop size. Mirrors riskShareCard.ts 1:1 (palette, esc/cp/truncate/wrap/ textEl helpers, identity-stamp position, eyebrow shape, doorway band).

  • New frontend/src/hooks/useCommitmentShareImage.ts mirrors useRiskShareImage 1:1 — useReferralCode() + the same auth-store organization?.name selector, svgToPngBlob rasteriser (zero new dependency — reuses the same helper introduced by PR #310 for the whole-brief image), Web Share API ({ files, text }) with the text artifact riding along, AbortError-silent share-sheet cancellation, download-blob + commitments.card_share_image_saved toast fallback, brief.share_image_error toast on failure.

  • Sibling ImageDown button next to the existing text-Share button on both /intel/commitments (CommitmentsPage.tsx) and the Today Commitments section (TodayPage.tsx). Disabled while saving; localized aria-label and tooltip via the reused brief.share_image_button key. Same c.status === 'open' / row.status === 'open' gate as the text-share button.

  • Two new keys in commitments.json: card_eyebrow (“COMMITMENT”) and card_share_image_saved (“Commitment image saved — share it to the owner”). Vietnamese hand-translated (CAM KẾT / Đã lưu ảnh cam kết — chia sẻ cho người phụ trách); 60 other locales seeded with the English value by propagate-new-i18n-keys.mjs. Per-locale translation arrives via the standing i18n-translate fleet — the same first-step convention as PRs #392/#437/#442/#448/#453/#461/ #472/#477.

  • No backend change, no API change, no model change, no migration, no DIAGRAMS change, no MVP_SCOPE matrix change (F09 ✅; F02 ✅).

  • Growth metric this bets on: per-commitment share-card-image attributed signups per forwarded artifact — the per-commitment image is the highest-volume named-owner forward in the share family. In chat-first wedge markets the image form should out-perform the text form on attributed-signup-per-share by ≥1.5× because the receiver sees a board-deck-quality preview in the chat thumbnail with the deadline rail visible at thumbnail scale — not a wall of pasted text that loses formatting on group-forward. Secondary: time from non-user → first share click on a receiver who sees the deadline-railed card with the org letterhead reads it as a real accountability tool from a peer’s company, not anonymous marketing. Tertiary: per-item image-arc closure rate — at the close of this PR, the share family reaches 3/3 image-arc coverage for the intel trio (decisions ✅ #472, risks ✅ #477, commitments ✅ this PR), the convergence point of the multi-quarter visual identity-stamp arc.

Verification (pre-PR, local):

  • pnpm test -- src/test/commitmentShareCard.test.ts — 26/26 green (15 base + 5 deadline-accent + 5 identity-stamp + 1 baseline preservation).
  • Sibling assurance suite (src/test/commitmentShareCard.test.ts src/test/commitmentShare.test.ts src/test/CommitmentsPage.test.tsx src/test/TodayPage.test.tsx src/test/riskShareCard.test.ts src/test/decisionShareCard.test.ts src/test/RisksPage.test.tsx src/test/DecisionsPage.test.tsx) — 155/155 green (text-share + per-decision + per-risk cards unchanged; pages render the new button without regression).
  • Full frontend suite pnpm test — 10,155/10,155 green (16 skipped — exactly +26 from the per-risk baseline of 10,129, matching the new commitmentShareCard test count).
  • ./node_modules/.bin/tsc --noEmit — 0 errors.
  • node scripts/check-design-tokens.mjs — clean (SVG palette inlined hexes per DESIGN.md §16.1, same as riskShareCard.ts; no React-tree stock-Tailwind hits in the new ImageDown buttons).
  • pnpm validate-i18n — parity green across all 62 locales (120 entries added across 60 non-en non-vi locales; exactly 2 keys × 60 locales as expected).

2026-06-22 — Growth/identity-stamp: stamp the sender’s organization name on the per-commitment text share artifact (F09 × F02)

Type: Feature (growth / distribution-object — additive)

  • Opens the identity-stamp arc on the intel-trio text shares (per-commitment, per-risk, per-decision). Mirrors the brief artifact’s three-form closure on the trio’s highest-volume * accountability* forward: per-commitment shares the named-owner gesture (“@head_of_sales, you committed Friday — status?”), and the head of the share artifact now reads as identifiable letterhead ({org} · Lumi · Commitment · {date}) rather than the anonymous Lumi · Commitment · {date}. The receiver — a named owner who reads the artifact personally — sees the sender’s company in line 1, the Notion/Figma/Calendly distribution-object invariant on the most common accountability forward in the product. This closes the first of the three intel-trio identity-stamp gaps; the per-risk and per-decision rollouts ship as the next two improvement runs in parallel.

  • frontend/src/lib/commitmentShare.ts — adds an optional orgName?: string parameter (last position, after referralUrl, mirroring briefItemShare.ts). When trimmed non-empty, line 1 becomes t('share_header_with_org', { ns: 'commitments', org, date }) (or the _nodate variant when todayDate is null); when empty / whitespace / undefined the existing share_header / share_header_nodate code path runs unchanged. Bit-for-bit identical artifact for any caller that doesn’t pass the new parameter — every pre-existing test stays green.

  • frontend/src/hooks/useCommitmentShare.ts — adds const orgName = useAuthStore((s) => s.organization?.name) (the same selector useBriefShare.ts / useBriefItemShare.ts use) and threads it as the fifth argument to buildCommitmentShareText. No change to the share-sheet → clipboard fallback chain, toasts, or AbortError handling — the hook contract is preserved for both callers (the Today Commitments section’s BriefSection cards and the /intel/commitments triage queue’s CommitmentsPage).

  • Two new keys in the commitments namespace: share_header_with_org ({{org}} · Lumi · Commitment · {{date}} in EN; {{org}} · Lumi · Cam kết · {{date}} in VI) and share_header_with_org_nodate ({{org}} · Lumi · Commitment in EN; {{org}} · Lumi · Cam kết in VI). The remaining 60 locales receive English placeholders via node scripts/propagate-new-i18n-keys.mjs (parity green now, per-locale translation later via the i18n-translate fleet — the same first-step convention #392/#437/#442 followed for the brief namespace; existing share_header in 60/62 locales is also the English string verbatim, so this matches established per-namespace precedent).

  • Tested in frontend/src/test/commitmentShare.test.ts — four new TDD-red cases (15/15 green after implementation): present dated, present undated, defensive whitespace fallback, and identity + referral footer co-exist. The 11 pre-existing cases cover the no-orgName regression contract (optional parameter → anonymous header → bit-for-bit identical artifact for legacy callers).

  • Growth metric this bets on: per-commitment-share-attributed signups per forwarded commitment — the per-commitment share is the highest-volume accountability forward in the product (commitments accumulate across the week, each one targets a named owner who reads it personally). Identifiable letterhead on the gesture aimed at a named receiver should out-perform the anonymous header in signup attribution. Tertiary: non-user → first share time on a receiver who sees “Acme Corp · Lumi · Commitment · 22 Jun 2026 / 👤 Owner: me / 📅 Due: Friday” — they read it as their CEO’s accountability tool and are more likely to recognize Lumi as a real product than from the anonymous header.

Verification:

  • frontend/src/test/commitmentShare.test.ts (15 cases — 11 original + 4 identity-stamp) — all green.
  • frontend/src/test/CommitmentsPage.test.tsx · TodayPage.test.tsx · briefShare.test.ts · briefItemShare.test.ts · briefShareCard.test.ts — all green (the hook’s caller surfaces and the prior identity- stamp artifacts are unchanged).
  • ./node_modules/.bin/tsc --noEmit — 0 errors.
  • node scripts/check-design-tokens.mjs — clean.
  • pnpm validate-i18n — parity green across all 62 locales (the two new keys propagated mechanically).
  • Full pnpm test — fully green, in line with the main baseline.

2026-06-22 — Growth/atomic-distribution: per-commitment Share on every open Commitment row on /intel/commitments triage queue

Type: Feature (growth / atomic distribution-object — additive)

  • Closes the intel-trio Share-affordance audit: per-risk on /intel/risks (#419), per-decision on /intel/decisions (#421), and now per-commitment on /intel/commitments (this entry). Every triage page in the intel trio now carries the same one-tap, F59-attributed share affordance the Today landing page does — same hook, same artifact, same fallback chain, same gate, same aria semantics. Convergence-not-accumulation: the share gesture lives on every surface where a CEO weighs an open intel item, no surface silently losing the F59 attribution loop.

  • frontend/src/pages/intel/CommitmentsPage.tsx — adds Share2 ghost- icon button at the head of the actions column (Share · Done · Send confirmation) on every row.status === 'open' row. Reuses the existing useCommitmentShare hook + buildCommitmentShareText builder from the Today twin (zero new helpers, zero new hooks). New todayIso constant — same ISO date the Today, RisksPage, and DecisionsPage shares use, so an artifact forwarded from any surface lands identically in a Zalo thread.

  • Zero new locale keys — the six commitment-specific share keys (share_button, share_header, share_header_nodate, share_owner, share_due, share_overdue) and the four brief-namespace shared keys (share_footer, share_footer_referral, share_copied, share_error) all already exist from the Today work (#413). One copy edit on those keys still lands on both commitment-share surfaces at once.

  • Tested in frontend/src/test/CommitmentsPage.test.tsx — three new component cases mirroring the DecisionsPage/RisksPage one-for-one: (1) Share button present on open rows, absent on done/cancelled rows; (2) native share sheet receives the commitment artifact + referral footer; (3) clipboard fallback + brief-namespace success toast when no navigator.share. Tests scoped to within(getTable()) since the shared DataTable renders both a desktop table and an md:hidden mobile card list (both visible in JSDOM).

  • Growth metric this bets on: shares-launched-per-CEO-from- /intel/commitments (currently 0 by construction — no affordance exists). Commitments are the volume play in the intel-trio share series (often 20+ open rows on a typical CEO morning vs. 5–15 risks and 3–8 decisions), so the per-CEO share rate from this surface should exceed the per-risk / per-decision rate from their respective triage pages within two weeks of ship. Secondary: shares- attributed-to-F59-signup climbs as a function of share volume across all per-item surfaces. The intel-trio share-audit completion rate flips from 2/3 → 3/3.

Verification:

  • frontend/src/test/CommitmentsPage.test.tsx — 18/18 green (3 new + 15 existing).
  • frontend/src/test/DecisionsPage.test.tsx · RisksPage.test.tsx · TodayPage.test.tsx · commitmentShare.test.ts — 90/90 green (the shared share-hook contract is unchanged, so sibling surfaces don’t regress).
  • ./node_modules/.bin/tsc --noEmit — 0 errors.
  • node scripts/check-design-tokens.mjs — clean.
  • pnpm validate-i18n — parity green across all locales.

2026-06-22 — Growth/atomic-distribution: per-commitment Share on every open Commitment card on Today

Type: Feature (growth / atomic distribution-object — additive)

  • Completes the four-member atomic-share family across every Today item type (brief #270/#350, decision #398, risk #407, now commitment). Commitments are the accountability-forward CEO gesture (“@head_of_sales, you committed Friday — status?”) — the volume play in the share series (higher daily frequency than risks, lower stakes per share than decisions).

  • New frontend/src/lib/commitmentShare.ts pure helper builds the artifact: Lumi · Commitment · {date} header, title, 👤 Owner and 📅 Due lines when populated, ⏰ Overdue derived line when due_date < todayDate, generic-or-referral footer. New frontend/src/hooks/useCommitmentShare.ts orchestrates clipboard / share-sheet, reusing the same share_copied / share_error toasts from the brief namespace, same AbortError no-fallback, same clipboard fallback chain as useBriefShare / useBriefItemShare / useDecisionShare / useRiskShare. Five hooks, one viral loop — they can never drift on semantics.

  • Six new keys in commitments namespace (share_button, share_header, share_header_nodate, share_owner, share_due, share_overdue) propagated to all 62 locales via node scripts/propagate-new-i18n-keys.mjs.

  • Tested in frontend/src/test/commitmentShare.test.ts (11 deterministic cases) and three new component cases in frontend/src/test/TodayPage.test.tsx (button presence on open commitments only, native share with the referral artifact, clipboard fallback + success toast).

  • Growth metric this bets on: shares per active CEO per week — closes the every-Today-card-is-forwardable loop. Secondary: referral-attributed signups per week downstream of more shares (F59).

Verification:

  • frontend/src/test/commitmentShare.test.ts (11 cases) — all green.
  • frontend/src/test/TodayPage.test.tsx (3 new cases, 28 total) — all green.
  • ./node_modules/.bin/tsc --noEmit — 0 errors.
  • node scripts/check-design-tokens.mjs — clean.
  • pnpm validate-i18n — parity green across all 62 locales.
  • Full pnpm test — 10042 passed, 16 skipped, 0 failures.

2026-06-21 — CRUD-scope decision: lifecycle actions, not free delete (recorded)

Type: Decision (no code change)

During the 2026-06-21 full-CRUD audit, Commitments were reviewed for edit/delete affordances. Decision: Commitments are evidence-bound items (each links an EvidenceSource); their UI lifecycle is resolve (and owner assignment), not free edit/delete. A hard delete would sever the evidence/audit trail (F16) and is intentionally not offered in the operator UI. This is a deliberate scope boundary, not a missing feature.

2026-06-18 — Visual QC: surface stakeholder and money priority in the commitments list

Type: UX Fix

Summary:

  • The commitments screenshot hid stakeholder context entirely and gave financial promises the same visual weight as undated low-stakes rows, despite the F09 contract requiring both signals in the CEO scan.
  • The title cell now includes stakeholder and money metadata inline, and the default list order now raises money-linked, higher-priority, dated commitments ahead of vague low-stakes items.

Verification:

  • frontend/screenshots/09-commitments/01-commitments-list.png
  • frontend/screenshots/09-commitments/03-commitments-scroll.png
  • pnpm test -- src/test/CommitmentsPage.test.tsx
  • pnpm exec playwright test tests/e2e/screenshot-flow.spec.ts --project=chromium --reporter=list -g "09-commitments"
  • ./node_modules/.bin/tsc --noEmit

2026-06-18 — Visual QC: render and capture commitment confirmation dialog

Type: Bug Fix + QC Harness Improvement

Summary:

  • The Gửi xác nhận button set local state but the page never rendered SendConfirmationDialog, so the action was dead.
  • CommitmentsPage.tsx now renders the selected commitment’s confirmation dialog with stakeholder/title/description/due-date defaults.
  • The screenshot flow now opens the first Gửi xác nhận action and captures the pending confirmation dialog without submitting or mutating data.

Verification:

  • frontend/screenshots/09-commitments/02-send-confirmation-dialog.png
  • pnpm test -- src/test/CommitmentsPage.test.tsx
  • pnpm exec playwright test tests/e2e/screenshot-flow.spec.ts --project=chromium --reporter=list -g "09-commitments"

2026-06-15 — Frontend route namespaced

Frontend route namespaced: /commitments -> /intel/commitments (app-wide IA restructure; API paths unchanged).

2026-06-14 — Strategy v2 realignment (positioning)

Type: Documentation Change

Summary:

  • Reframed the §1 “Competitive differentiation vs Asana / Jira task inbox” paragraph to “What we replace: Zalo + Excel + the owner’s memory” — the actual incumbent for a Vietnamese interior fit-out contractor, who owns no task system.
  • Replaced Asana/Notion/Jira comparisons in three §2 JTBD stories (member re-typing into Zalo/Excel; CEO scanning vs. hand-kept sheets and scrolled threads; unassigned owner vs. a blank Excel row / unnamed Zalo promise).
  • Added a §3 cross-link: an external-party commitment (customer/supplier/subcontractor) is a natural home for an F71 confirmation link (Loop 2), with a boundary note that F71 owns the external sign-off surface.
  • Reframed the lone Asana comparison in the §14 owner-assignment metric to the Zalo/Excel incumbent (metric definition unchanged). Historical changelog entries (e.g. the 2026-06-12 “R24” line) preserved verbatim.

Reason:

  • Aligns with competitive-strategy-v2.md and the registry’s Positioning doctrine (v2): the competitor is Zalo/WhatsApp + Excel + owner memory, not US SaaS; surface the F71 wedge where natural.

Impact:

  • Positioning copy only. No change to data model, API, acceptance criteria, error states, audit verdicts, or status.

Tests:

  • None (documentation change).

Notes:

  • Every product fact, AC, API row, and §11 audit row left intact; only competitive framing and one cross-link added.

2026-06-14 — Restructure to TEMPLATE.md section order; expand §11 audit (SP fleet)

Type: Documentation Change

Summary:

  • Added §2 User Stories heading (JTBD stories were embedded in §1b — extracted to correct section).
  • Numbered “Current Behavior” as §8; §8 Diagrams → §10; §11 Edge Cases absorbed into §11 audit gaps note; §12 Error States → §9 (table format); §9 Implementation Audit → §11 (10-row table); §10 Open Questions → §12; §13 Bug/Change Log stays §13.
  • §11 audit: 10 rows, 6 ✅, 4 ⚠️ gaps (money-priority, duplicate extraction, cross-tenant evidence xfail, cross-tenant task FK xfail).

2026-06-13 — “needs owner” indicator for unassigned commitments (A-r27)

Type: Feature

Summary:

  • CommitmentSerializer.needs_owner (computed field): true when owner is null and owner_name_raw is populated, false otherwise.
  • CommitmentsPage.tsx renders a Badge (“Needs owner” / “Cần chủ sở hữu”) next to owner_name_raw when needs_owner is true, distinct from the overdue badge.
  • i18n: commitments.needs_owner added to en/ and vi/.

Reason:

  • §2 AC (line 27): an unassigned commitment needs a visual indicator distinct from overdue so the CEO can close the accountability gap.

Tests:

  • test_commitment_needs_owner_true_when_unassigned, test_commitment_needs_owner_false_when_assigned, test_commitment_needs_owner_false_when_no_owner_name, test_commitment_needs_owner_list_field_present, test_commitment_needs_owner_cross_tenant_works.

2026-06-10 — overdue is now a derived display state, not a stored status

Type: Breaking Change

Summary:

  • Removed "overdue" from Commitment.STATUS_CHOICES (now open | in_progress | done | blocked | cancelled).
  • Migration apps/operations/migrations/0003_alter_commitment_status.py narrows the field choices and data-migrates any existing status="overdue" rows to "open".
  • CommitmentViewSet.get_queryset keeps accepting ?status=overdue as a query filter — it now derives status in (open, in_progress) AND due_date < today instead of an exact match.
  • CommitmentsPage.tsx derives the overdue badge via a new displayStatus() helper (due_date passed + status open/in_progress) instead of relying on a stored status.
  • GLOSSARY status table and Commitment entry updated.

Reason:

  • Two sources of truth for “overdue” invited drift between stored status and the Brief’s derived computation.

Impact:

  • API: POST/PATCH /api/commitments/ with status="overdue" now returns 400.
  • ?status=overdue query filtering behavior is unchanged from the CEO’s perspective (still returns overdue items).

2026-06-12 — Add Edge Cases and Error States to F09 spec

Type: Documentation Change

Summary:

  • Added §11 Edge Cases (E1–E13) and §12 Error States (ES1–ES10).

2026-06-13 — D-r25: Add ES11/ES12 cross-tenant xfail error states

Type: Documentation Change

Summary:

  • Added §12 ES11: cross-tenant evidence_sources M2M FK silently succeeds.
  • Added §12 ES12: cross-tenant task FK silently succeeds.

2026-06-12 — R24: JTBD stories, Asana/Jira diff, owner-assignment metric, zero-config list

Type: Documentation Change

Summary:

  • Rewrote §2 user stories from As a <role> format to JTBD format.
  • Added §1 competitive differentiation section.
  • Added JTBD for inline owner assignment.
  • Added owner-assignment rate > 80% within 24h success metric to §1b.

2026-06-12 — Add Success Metrics section (§1b) to F09 spec

Type: Documentation Change

14. Success Metrics (Appendix)

  • Commitment-extraction yield ≥ 0.3 per submitted pulse: measured as commitments created within 5 minutes of a pulse submission divided by the number of pulse submissions in the same period. The leading signal that the pulse-to-commitment pipeline (F08) is working — if fewer than 30% of pulses produce a tracked promise, promises are being left in the chat. Tied to §1 (“Lumi makes every promise visible, owned, dated”) and §2 story 1 (“‘tôi đã hứa gửi báo giá thứ 5’ becomes a tracked commitment”).
  • % of commitments with a due date > 60%: measured as the share of extracted commitments where due_date is non-null. A commitment without a date is a wish, not a promise. Tied to §1 (“impossible to silently forget”) — a commitment with no due date can be silently forgotten. This is the single most actionable dimension: a CEO whose org has < 60% dated commitments can immediately tell members to include deadlines in their pulse answers.
  • Due-date honour rate > 50%, trending up: measured as the share of commitments with status=done and resolved_at ≤ due_date (or closed within a 1-day grace window) over a rolling 4-week period. The lagging proof that commitments are changing behaviour: promises are being kept, not just collected. Tied to §1 (“customers churn and suppliers defect from broken promises nobody tracked”).
  • Owner-assignment rate > 80% within 24h of extraction: measured as the share of AI-extracted commitments created in a rolling 24-hour window whose owner FK is non-null within 24 hours of created_at. An extracted commitment with no named owner has nobody accountable — it degrades from “tracked promise” to “orphaned observation.” A promise left in a Zalo group or an unfilled Excel cell has the same problem and no one ever notices; Lumi surfaces the gap and lets the CEO close it in one tap. Tied to §2 JTBD (“I need to assign an owner in one tap”).
  • Competitive framing — WhatsApp and memory. A Vietnamese SME CEO currently tracks promises through Zalo messages, WhatsApp voice notes, or personal memory — all of which fragment across channels and vanish when the inbox scrolls. Lumi’s commitment list (§2 story 2) turns any promise mentioned in a pulse (F07) into a persisted, dated, owner-attributed row that a senior manager can view at any time, regardless of whose chat history is involved. No other tool treats cam kết as a first-class operational object rather than a to-do line item.
  • Anti-metric — average commitments per pulse trending above 1.5: a sustained ratio above 1.5 suggests the extraction pipeline is fragmenting one promise into multiple rows, or the CEO is using commitment creation as a proxy for note-taking. Either case degrades signal — see F08 extraction cap (max N extractions per pulse, defined in F08 spec). Track ceiling alongside yield to distinguish “filling gaps” from “making noise.”