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/approvalsis 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_sourcesM2M (multiple EvidenceSources when duplicate signals are merged),extracted_from_pulse_entryFK.- Existing status field values
open/decided/cancelledmap to pending/decided/cancelled approval states. decision_textstores the approval outcome (“Approved”, “Rejected”, or the decision maker’s verbatim reply).confidence_scoreset by extraction pipeline (0.6–0.7 keyword mode, 0.85–0.90 LLM mode).
- Existing status field values
work.Task— When a “chờ duyệt” signal also implies follow-up work after approval, aTaskproposal is created via SignalProposal (confirm-before-commit per F91 trust level 0).operations.Risk— When an overdue approval blocks a milestone, aRisk proposalis 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 asSignalProposal(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
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| GET | /api/signals/inbox/ | member | ?status=captured | List[SignalInboxItem] with proposals | 404 (tenant) |
| POST | /api/signals/capture/ | member | {raw_text, title?, capture_channel} | SignalInboxItem | 400 |
| POST | /api/signals/proposals/<id>/accept/ | manager | {title?, description?} | {proposal, object_id, object_type} | 400, 409 |
| POST | /api/signals/proposals/<id>/reject/ | manager | {reason?} | proposal | 409 |
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_userandstatus=openappear here, ranked bydecision_needed_byproximity (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 withextracted_from_pulse_entryorcreated_by_type=aimetadata. The list already showsdecision_byanddecision_needed_by. - Brief items for overdue approvals with dependency risks — generated as Risk or Decision BriefItems (existing
item_typevalues cover this). - SignalInbox — raw captured signals awaiting confirmation, with proposals for the user to accept/reject (confirm-before-commit pattern). Existing
/inboxroute.
- Brief “Chờ bạn” section — DecisionRequests with
- States: empty = no pending approvals found; loading skeletons per existing patterns; error = toast with retry.
- i18n: uses existing
decisionsandbriefnamespaces. New keys in anapproval_trackingnamespace 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=userordecision_by=null+ pending. User says “duyệt cái X” or “từ chối cái X” → Lumi uses existingrecord_decision_outcometool (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
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Pulse with “đang chờ duyệt” + decision maker + deadline → DecisionRequest created via SignalProposal | backend | tests/test_ai_extraction.py |
| 2 | Pulse with “cần duyệt” but no decision maker → DecisionRequest with decision_by=null, needs_clarification=False (missing data), proposal created | backend | tests/test_ai_extraction.py |
| 3 | Clear approve reply from decision maker → DecisionRequest status=decided, decision_text={“outcome”:“approved”} | backend | tests/test_signal_processing.py |
| 4 | Ambiguous reply (emoticon, “ok”) → DecisionRequest stays open, needs_clarification=True | backend | tests/test_signal_processing.py |
| 5 | Overdue approval with dependent milestone → Risk proposal in Brief generation | backend | tests/test_brief_generation.py |
| 6 | Same approval signal from pulse + chat → one DecisionRequest, two EvidenceSources | backend | tests/test_multitenancy.py (merge) |
| 7 | Cross-tenant isolation: org B gets 404 on org A’s DecisionRequest | backend | tests/test_multitenancy.py |
| 8 | Confirm-before-commit flow: accept proposal creates live DecisionRequest | backend | tests/test_signal_proposals.py |
| 9 | Reject proposal → no DecisionRequest created, signal ignored | backend | tests/test_signal_proposals.py |
| 10 | Brief renders pending approvals (where decision_by=current_user) in Chờ bạn section | frontend | frontend/tests/e2e/lumi.spec.ts describe("Approval Tracking") |
| 11 | Assistant: “có gì đang chờ duyệt” returns open DecisionRequests for user | backend | tests/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
| # | Error | Response | Notes |
|---|---|---|---|
| ES1 | Ambiguous reply (no clear approve/reject signal) | DecisionRequest stays open; needs_clarification=True | Extraction service sets needs_clarification; no alert generated |
| ES2 | Missing decision maker in extraction | DecisionRequest created with decision_by=null; proposal includes “missing data” label | User assigns via assistant or SignalInbox |
| ES3 | Missing deadline in extraction | DecisionRequest created with decision_needed_by=null | No overdue tracking possible; handled by user editing the item |
| ES4 | Conflicting outcomes (same approval, two replies) | First outcome recorded; second creates separate DecisionRequest with EvidenceSource linking to the conflict | Handled at extraction level |
| ES5 | Cross-tenant access | 404 (existing DecisionRequestViewSet behavior) | Tenant-scoped query |
| ES6 | Duplicate signal (same wording, same source channel within 5 min) | Ignored — no new DecisionRequest created | Matches 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:
DecisionRequestmodel (backend/apps/operations/models.py:136) — hasdecision_by,decision_needed_by,decision_text,status,evidence_sourcesM2M,extracted_from_pulse_entry.needs_clarificationfield added in r05 (BooleanField, default=False, db_default=False).apps/ai/services/extraction.py— existing decision extraction (_DECISION_PATTERNS+_APPROVAL_TRACKING_PATTERNSincludes “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 withcreated_by_type="ai"andneeds_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_destinationforproposal_type="decision". Wired in r05 through the existing SignalInbox +extract_proposals+accept_proposalflow.apps/ai/prompts.py—_EXTRACTION_RULESupdated in r05 with approval tracking guidance for the LLM path.apps/approvals/(F26) — exists independently withApprovalPolicy,ApprovalRequest, chain resolution. Not modified by this spec.apps/work/models.py— Task model withsource_type,evidence_sourcesM2M.
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_clarificationbe 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 ofarchived_by_assistanton 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 inapps/ai/prompts.pyper 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_entrycontext, 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_clarificationBooleanField (default=False, db_default=False) to DecisionRequest model + migration0013_add_decision_needs_clarification. - Added
_APPROVAL_TRACKING_PATTERNStoextraction_patterns.pyfor keyword-based approval detection: “đang chờ duyệt”, “cần chốt”, “chờ duyệt”, “pending approval”, “awaiting sign-off”, etc. - Updated
_EXTRACTION_RULESinprompts.pywith 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_clarificationflag — no default approval inference. - Builds on existing input channels (assistant chat F91 + pulse) only.