draft

Spec: F98 — Approval Tracking (from conversation)

Status: draft Source issue: lumi-feedback#20 — UC-03: Track items awaiting approval

1. Problem & CEO Job

See clearly + decide fast. Approval signals in chat and pulse check-ins are not trackable, so the CEO or PM cannot see what is pending, who decides, when, or what a sign-off blocks. F98 tracks them with the existing DecisionRequest model, retains ambiguous replies as pending, and uses Tasks for follow-up actions; it adds no Approval object.

2. User Stories & Acceptance Criteria

  • As a member/PM, I want Lumi to detect when I say “đang chờ duyệt” or “cần chốt” and create a trackable item, so that I don’t need to re-type it elsewhere.
    • Given a pulse or chat message explicitly requesting approval (with decision maker and deadline), when Lumi processes the signal, then it creates a DecisionRequest with status=open, decision_by set to the named decision maker, decision_needed_by set to the deadline, and linked EvidenceSource.
    • Given a message with ambiguous approval phrasing (“cần duyệt” but no decision maker named), when Lumi processes it, then it creates a DecisionRequest with decision_by=null and a “missing data” marker, and proposes it via the SignalInbox for confirmation before committing.
  • As a decision maker, I want to see all items pending my approval in one place, so that I know what needs my attention.
    • Given any DecisionRequest where decision_by matches the current user and status=open, then it appears in the Brief’s “Chờ bạn” section alongside F11 DecisionRequests, ranked by decision_needed_by proximity.
  • As a requester, I want Lumi to capture the approval outcome when the decision maker replies, so that the item’s status reflects reality without manual updates.
    • Given an open DecisionRequest and a subsequent message from the decision maker with a clear approve/reject signal, when Lumi processes it, then it updates status to decided and records decision_text with evidence linked.
    • Given an ambiguous reply (reaction only, “ok” without context), when Lumi processes it, then the DecisionRequest stays pending and the ambiguous response is marked “needs clarification” — no default approve inferred.
  • As a PM/CEO, I want overdue approvals with blocked dependencies surfaced as risks, so that I know what is in jeopardy.
    • Given a DecisionRequest where decision_needed_by is past and a related Task/Project has a dependency, when Lumi generates a Brief, then it proposes a Risk candidate noting the delayed approval and its impact.
  • As a PM, I want duplicate approval signals merged into one tracked item, so that the same pending approval doesn’t appear twice.
    • Given the same approval request detected in both pulse and chat, when Lumi processes both signals, then it creates one DecisionRequest with both EvidenceSources linked rather than two separate items.

3. Scope Boundaries

  • In: Approval signals extracted from pulse (F07/F08) and assistant chat (F91); DecisionRequest lifecycle for approval state (open → decided/cancelled); deadline tracking; dependency risk surfacing; duplicate merge via EvidenceSource M2M; ambiguous-reply detection (pending stays pending).
  • Out (exists separately): F26 formal multi-step approval policies (policy-defined routing, chain resolution, per-step approver queues). Approval object creation (no new model — rides on DecisionRequest). Bot channel input (scope: F91 chat + pulse only per shared contract). Escalation timers (reminder is advisory, Brief is the surfacing channel). External (inter-company) approval links (F71). A separate “Approvals” list page (approvals render inline via existing Decisions list / Brief — F26’s /crm/approvals is the formal policy-request surface, not duplicated here).

4. Data Model

No new models. Reuses:

  • operations.DecisionRequest — requested_by (requester of approval), decision_by (the decision maker / approver), decision_needed_by (deadline), status (open → decided/cancelled tracks approval lifecycle), evidence_sources M2M (multiple EvidenceSources when duplicate signals are merged), extracted_from_pulse_entry FK.
    • Existing status field values open/decided/cancelled map to pending/decided/cancelled approval states.
    • decision_text stores the approval outcome (“Approved”, “Rejected”, or the decision maker’s verbatim reply).
    • confidence_score set by extraction pipeline (0.6–0.7 keyword mode, 0.85–0.90 LLM mode).
  • work.Task — When a “chờ duyệt” signal also implies follow-up work after approval, a Task proposal is created via SignalProposal (confirm-before-commit per F91 trust level 0).
  • operations.Risk — When an overdue approval blocks a milestone, a Risk proposal is surfaced in the Brief via the existing Brief generation pipeline.
  • pulses.EvidenceSource — Every extracted approval tracking decision links its EvidenceSource with source_text. Merge detection: same normalized title across two EvidenceSources from different input channels → single DecisionRequest with both evidence_sources linked.
  • signals.SignalProposal — Confirm-before-commit: extracted approval items surface as SignalProposal(proposal_type="decision") before being committed as live DecisionRequests. This matches the existing F91 trust-level-0 pattern (propose → confirm → commit). The SignalInbox is the review surface; the assistant conversation is the confirm path.

Status transitions: DecisionRequest status open → decided | cancelled (unchanged from F11). No new states needed — “pending” is status=open with decision_by set; “approved” is status=decided with decision_text indicating approval; “rejected” is status=decided with decision_text indicating rejection. The extraction service writes decision_text as a structured value: {"outcome": "approved"|"rejected", "source_text": "<verbatim reply>"}. Ambiguous replies leave decision_text empty and a needs_clarification flag (added as a nullable BooleanField on DecisionRequest, default False, set True when the extraction service detects a reply that is neither clearly approve nor clearly reject).

5. API Contract

MethodPathAuth/RoleRequestResponseErrors
GET/api/signals/inbox/member?status=capturedList[SignalInboxItem] with proposals404 (tenant)
POST/api/signals/capture/member{raw_text, title?, capture_channel}SignalInboxItem400
POST/api/signals/proposals/<id>/accept/manager{title?, description?}{proposal, object_id, object_type}400, 409
POST/api/signals/proposals/<id>/reject/manager{reason?}proposal409

No new endpoints. All approval-tracking operations use the existing SignalInbox and SignalProposal endpoints. The Brief already surfaces DecisionRequests via its existing pipeline (BriefItem.TYPE_CHOICES includes decision). EvidenceSource M2M already exists on DecisionRequest.

Tenant scoping: all existing endpoints tenant-scoped via request.organization_id. AI-generated DecisionRequests created by apps/ai/services/extraction.py pass organization_id at creation time.

6. UI / UX

  • No new route. Approval-tracking items rendered via existing surfaces:
    • Brief “Chờ bạn” section — DecisionRequests with decision_by=current_user and status=open appear here, ranked by decision_needed_by proximity (sooner = higher). They share the section with F11 decision requests. No visual distinction needed — the CEO decides both types from the same queue.
    • Decisions list (/intel/decisions) — Approval-tracking items appear as DecisionRequests with extracted_from_pulse_entry or created_by_type=ai metadata. The list already shows decision_by and decision_needed_by.
    • Brief items for overdue approvals with dependency risks — generated as Risk or Decision BriefItems (existing item_type values cover this).
    • SignalInbox — raw captured signals awaiting confirmation, with proposals for the user to accept/reject (confirm-before-commit pattern). Existing /inbox route.
  • States: empty = no pending approvals found; loading skeletons per existing patterns; error = toast with retry.
  • i18n: uses existing decisions and brief namespaces. New keys in an approval_tracking namespace only where strings must reference “đang chờ duyệt” semantics.
  • DESIGN.md surfaces/tokens: per existing DecisionRequest/BriefItem rendering.
  • Assistant path (F91): Complete via conversation — user messages “có gì đang chờ duyệt không” → Lumi queries open DecisionRequests scoped to decision_by=user or decision_by=null + pending. User says “duyệt cái X” or “từ chối cái X” → Lumi uses existing record_decision_outcome tool (confirm-before-commit). The entire approval tracking lifecycle is reachable through the assistant — no required form. SignalInbox is an optional review surface.

7. Test Plan

#CaseTypeWhere
1Pulse with “đang chờ duyệt” + decision maker + deadline → DecisionRequest created via SignalProposalbackendtests/test_ai_extraction.py
2Pulse with “cần duyệt” but no decision maker → DecisionRequest with decision_by=null, needs_clarification=False (missing data), proposal createdbackendtests/test_ai_extraction.py
3Clear approve reply from decision maker → DecisionRequest status=decided, decision_text={“outcome”:“approved”}backendtests/test_signal_processing.py
4Ambiguous reply (emoticon, “ok”) → DecisionRequest stays open, needs_clarification=Truebackendtests/test_signal_processing.py
5Overdue approval with dependent milestone → Risk proposal in Brief generationbackendtests/test_brief_generation.py
6Same approval signal from pulse + chat → one DecisionRequest, two EvidenceSourcesbackendtests/test_multitenancy.py (merge)
7Cross-tenant isolation: org B gets 404 on org A’s DecisionRequestbackendtests/test_multitenancy.py
8Confirm-before-commit flow: accept proposal creates live DecisionRequestbackendtests/test_signal_proposals.py
9Reject proposal → no DecisionRequest created, signal ignoredbackendtests/test_signal_proposals.py
10Brief renders pending approvals (where decision_by=current_user) in Chờ bạn sectionfrontendfrontend/tests/e2e/lumi.spec.ts describe("Approval Tracking")
11Assistant: “có gì đang chờ duyệt” returns open DecisionRequests for userbackendtests/test_assistant_chat.py

8. Current Behavior

No existing behavior. The F26 formal multi-step approval request system is shipped and operates independently — it provides a /crm/approvals page with policy-defined routing chains. The approval tracking described in this spec differs: it detects “đang chờ duyệt” signals from unstructured conversation (pulse, assistant chat) and tracks them using the existing DecisionRequest model, without creating a separate approval workflow. The DecisionRequest extraction pipeline currently detects “cần ceo”, “cần duyệt”, “approve”, “xin phép” as decision signals and creates DecisionRequest objects — this spec extends that extraction to also capture the named decision maker, deadline, and dependency context, and handles ambiguous-reply detection. The SignalProposal mechanism exists but is not wired for approval-tracking DecisionRequests. The needs_clarification field does not exist on DecisionRequest yet.

9. Error States

#ErrorResponseNotes
ES1Ambiguous reply (no clear approve/reject signal)DecisionRequest stays open; needs_clarification=TrueExtraction service sets needs_clarification; no alert generated
ES2Missing decision maker in extractionDecisionRequest created with decision_by=null; proposal includes “missing data” labelUser assigns via assistant or SignalInbox
ES3Missing deadline in extractionDecisionRequest created with decision_needed_by=nullNo overdue tracking possible; handled by user editing the item
ES4Conflicting outcomes (same approval, two replies)First outcome recorded; second creates separate DecisionRequest with EvidenceSource linking to the conflictHandled at extraction level
ES5Cross-tenant access404 (existing DecisionRequestViewSet behavior)Tenant-scoped query
ES6Duplicate signal (same wording, same source channel within 5 min)Ignored — no new DecisionRequest createdMatches F08 extraction dedup rules (same source, same normalized text, within 5 min)

10. Diagrams & Docs Touched

  • backend/DIAGRAMS.md — DecisionRequest state machine already covers open → decided/cancelled. Add a note that F98 rides on DecisionRequest status for approval tracking.
  • GLOSSARY.md — Add “Approval Tracking (F98)” entry clarifying the distinction from F26 Approval Requests.
  • docs/MVP_SCOPE.md — Add F98 registry row.
  • docs/AI_PIPELINE.md — Add “approval tracking” to decision extraction signals.

11. Implementation Audit (2026-07-10)

Built in r05 (2026-07-10). The following shipped code constrains the design:

  • DecisionRequest model (backend/apps/operations/models.py:136) — has decision_by, decision_needed_by, decision_text, status, evidence_sources M2M, extracted_from_pulse_entry. needs_clarification field added in r05 (BooleanField, default=False, db_default=False).
  • apps/ai/services/extraction.py — existing decision extraction (_DECISION_PATTERNS + _APPROVAL_TRACKING_PATTERNS includes “cần ceo”, “cần duyệt”, “approve”, “xin phép”, “chờ quyết định”, “đang chờ duyệt”, “cần chốt”, “chờ duyệt”). Current output creates DecisionRequest with created_by_type="ai" and needs_clarification=False. Approval tracking keywords added in r05.
  • apps/signals/ — SignalProposal model with proposal_type=decision, accept/reject flow. Proposal pipeline already handles decision proposals via _create_destination for proposal_type="decision". Wired in r05 through the existing SignalInbox + extract_proposals + accept_proposal flow.
  • apps/ai/prompts.py — _EXTRACTION_RULES updated in r05 with approval tracking guidance for the LLM path.
  • apps/approvals/ (F26) — exists independently with ApprovalPolicy, ApprovalRequest, chain resolution. Not modified by this spec.
  • apps/work/models.py — Task model with source_type, evidence_sources M2M.

Design constraint: F98 re-routes low-confidence decision extractions through the SignalProposal confirm-before-commit mechanism when decision_by or decision_needed_by is missing. High-confidence extractions (title + decision_by + deadline all present) commit directly. This dual-path matches F91 trust-level-0. The r05 implementation adds needs_clarification flag for ambiguous reply handling and _APPROVAL_TRACKING_PATTERNS for detection. Tests in backend/tests/approval_tracking/test_f98_approval_tracking.py cover all 11 spec §7 cases.

12. Open Questions / Decisions

  • Should needs_clarification be a BooleanField on DecisionRequest, or a new status value? Decision: BooleanField (default False). Adding a new status value (open → ambiguous → decided) would cascade into Brief filtering, frontend badge logic, and the GLOSSARY state machine. A nullable Boolean is simpler and matches the pattern of archived_by_assistant on Commitment/Task.
  • What is the exact detection heuristic for ambiguous replies? Decision: a reply is ambiguous when it (a) contains no explicit approval vocabulary (“duyệt”, “ok duyệt”, “approved”, “đồng ý”) AND (b) contains no explicit rejection vocabulary (“từ chối”, “reject”, “không duyệt”, “không đồng ý”). Reactions, emoticons, “ok” without qualifier, and non-committal responses (“để xem”, “check lại sau”) all count as ambiguous. Implementation: extend the keyword-based reply classifier in apps/ai/services/extraction.py (or the LLM prompt in apps/ai/prompts.py per ADR-031) to return a ternary outcome: approved, rejected, ambiguous.
  • When does a Risk get created for overdue approvals? Decision: only when a traceable milestone/blocker is identified (a Task or Project explicitly linked to the DecisionRequest via extracted_from_pulse_entry context, or via a mention in the same conversation). Not all overdue approvals create Risks — the spec avoids blanket risk generation.

13. Bug / Change Log

2026-07-10 — r05 buildout (approval tracking implementation)

Type: Feature Implementation

Summary:

  • Added needs_clarification BooleanField (default=False, db_default=False) to DecisionRequest model + migration 0013_add_decision_needs_clarification.
  • Added _APPROVAL_TRACKING_PATTERNS to extraction_patterns.py for keyword-based approval detection: “đang chờ duyệt”, “cần chốt”, “chờ duyệt”, “pending approval”, “awaiting sign-off”, etc.
  • Updated _EXTRACTION_RULES in prompts.py with approval tracking guidance for the LLM path (decision maker extraction, naming).
  • Written 16 tests across all 11 spec §7 test cases in tests/approval_tracking/test_f98_approval_tracking.py:
    • approval signal extraction (with/without decision maker)
    • approve/reject/ambiguous reply handling
    • overdue approval risk surfacing in brief
    • duplicate merge (pulse+chat → one DR, two EvidenceSources)
    • cross-tenant isolation (404 on other org)
    • confirm-before-commit (accept/reject proposal creates/doesn’t create DecisionRequest)
    • brief rendering of pending approvals
    • needs_clarification field default and set behavior
  • Updated MVP_SCOPE.md: F98 row marked ✅ backend.

2026-07-10 — Initial draft

Type: Feature Spec

Summary:

  • First draft of F98 approval-tracking spec from lumi-feedback#20 (UC-03).
  • Design rides on existing DecisionRequest model per shared product contract (#15): no new Approval object in MVP.
  • Uses SignalProposal confirm-before-commit pattern for low-confidence extractions; direct DecisionRequest creation for high-confidence extractions.
  • Ambiguous replies keep items pending with needs_clarification flag — no default approval inference.
  • Builds on existing input channels (assistant chat F91 + pulse) only.