Spec: F101 — CEO Brief by Project
Status: Derived from the canonical F101 CUE contract: partial. See the generated traceability view and gap view; Markdown does not carry an editable completion claim.
1. Problem & CEO Job
Decide fast + trust the signal. F12 groups risks, decisions, and commitments by type, forcing a CEO managing several projects to re-sort items and risking missed urgency. F101 groups each project’s overdue tasks, open risks, pending decisions, and missing data, with a cross-project rollup so the CEO can act without scattering attention.
Source issue: https://github.com/streamentry/lumi-fb/issues/23
2. User Stories & Acceptance Criteria
-
As a CEO, I want the Brief grouped by project, so that I see at a glance which projects need my attention and what each needs — without mentally re-sorting items.
- Given the Brief has items from 3+ projects, when generated, then items are grouped under project headings, with a cross-project rollup section at top listing projects with items requiring attention (count of decisions + risks + overdue tasks).
- Given a project has no items needing attention, when generated, then it does not appear in the Brief at all — silence is absence, not noise.
- Given a project has 2+ items, when rendered, the section orders by priority: decisions (where CEO action is required) first, then risks (severity-descending), then overdue/blocked tasks (most-overdue first), then missing data.
-
As a CEO, I want each project block to link back to the project detail, so that I can drill in from the Brief in one tap.
- Given a project heading in the Brief, when tapped, it navigates to
/work/projects/:id(with progress section if F97 is implemented). - Given a project-scoped item card, when the project badge is tapped, it navigates to the same project detail.
- Given a project heading in the Brief, when tapped, it navigates to
-
As a CEO, I want items with no project context to appear in a separate “Unassigned” section, so that I can triage them to a project rather than miss them.
- Given an item with no resolved project, when the Brief generates, it appears in an “Unassigned” section with a “Gán project” action.
-
As a CEO, I want to toggle between the current type-grouped view and the new project-grouped view, so that I am not forced into a new layout.
- Given the Brief page, when a toggle control is tapped, the view switches between “Theo loại” (type-grouped, current F12) and “Theo dự án” (project-grouped, F101). The toggle is sticky per-user (stored in
User.ui_preferences).
- Given the Brief page, when a toggle control is tapped, the view switches between “Theo loại” (type-grouped, current F12) and “Theo dự án” (project-grouped, F101). The toggle is sticky per-user (stored in
-
As a CEO, I want each project association to be honest about its source, so that I know which items are directly linked and which still need triage.
- Given a project-scoped item, when displayed, its project context comes from a direct related-object project FK and retains its existing
EvidenceSourcelinks. Items without that direct context remain inUnassigned; this feature does not claim an AI-inferred project association.
- Given a project-scoped item, when displayed, its project context comes from a direct related-object project FK and retains its existing
-
As a CEO, I want the project Brief to respect data visibility, so that items from team/private projects I cannot see are not included — and items from team projects I can see but am not a member of are still shown.
- Given a project with
visibility=team, when the CEO is not a member, then that project’s items are excluded from the Brief. - Given a project with
visibility=privateowned by another member, when the Brief generates for the CEO, then that project’s items are excluded. - Given a project with
visibility=organization, when the Brief generates, then all items are included.
- Given a project with
3. Scope Boundaries
- In: Re-grouping of existing BriefItems by project. Cross-project rollup section at top. Project detail links. Per-user view toggle (type-grouped / project-grouped). “Unassigned” section for orphaned items. Direct-FK Fact semantics and visibility-gated filtering.
- Out: AI-inferred project associations and an
Inferencelabel are not shipped by F101; unresolved items remainUnassigneduntil a separately specified, evidence-backed association path exists. - Out: Per-project standalone Brief page (deferred — the toggle view on the existing
/intel/briefroute covers this). Project-level Brief generation on a per-project schedule (future — the global Brief already covers all projects). Custom per-project Brief sections (owner/admin config — deferred). Email/push delivery of per-project Brief (F36/F77 owns delivery; this spec only changes the Brief content structure). Bot transport integration (separate spec, parent contract #15).
Assistant path (F91 doctrine): The Brief becomes a view the assistant can render and deep-link into. The toggle between views is a UI affordance, not a form — no new required interaction. In the assistant thread, the CEO can ask “Tóm tắt project Duy Tân” and the assistant retrieves the project-scoped Brief items as priority context.
4. Data Model
No new models. Additions and new service:
BriefItem (existing): no new fields. Project context is derived at generation time from related_risk.project, related_task.project, related_commitment.project, and related_decision.project. If none of these FKs point to a project, the item is “unassigned.”
BriefGenerationService (new service layer in apps/briefs/services.py): extends the existing generate_morning_brief with two output renderings:
- Type-grouped (current behavior, unchanged): items sorted by type → priority.
- Project-grouped (new): items sorted by project → priority within project.
The service produces both sets from the same candidate pool; the view layer chooses which to return based on the requesting user’s preference. The underlying item selection, ranking, and evidence linking are unchanged — only the presentation grouping changes.
User.ui_preferences (existing JSONField): extendable with brief_view: "type" | "project" (defaults to "type" so existing users see no change). No new model or migration needed.
All queries remain scoped to organization_id = request.organization_id. Project visibility filtering is applied at the Brief generation query level — invisible projects’ items are excluded from the candidate pool before grouping.
5. API Contract
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/briefs/generate/ | member | {view?: "type"|"project"} | Brief with items grouped per requested view | 403; 429 (throttle) |
| GET | /api/briefs/ | member | ?view=type|project | Brief list with items in requested grouping | 403 |
| GET | /api/briefs/<id>/ | member | ?view=type|project | Single brief with items in requested grouping | 404 |
| PATCH | /api/auth/me/preferences/ | member | {brief_view: "type"|"project"} | Updated preferences | 400 |
The view parameter is a server-side filter on the serialization, not a new endpoint. When view=project, the serializer groups items by project_title (derived from the related object’s project FK) and adds:
project_rollup:[{project_title, project_id, item_counts: {decisions, risks, tasks}}]items_by_project:{project_id: {project_title, items: [...], has_inference_items: bool}}unassigned_items:[items with no project]
Tenant scoping: existing Brief queryset filtering. Project visibility is enforced during candidate pool assembly — a project the requesting member cannot see is invisible, and its items are excluded as if they did not exist.
Evidence & confidence: every item retains its existing evidence_sources array (F12 §5). The shipped resolver labels direct related-object project context as Fact; items without a resolvable direct project remain Unassigned. Evidence-based project inference and an Inference project association are not shipped and remain a separately gated decision.
6. UI / UX
- Route:
/intel/brief— same route, newviewtoggle. - New component(s):
BriefViewToggle— a segmented control (shadcn<ToggleGroup>) in the Brief header: “Theo loại” / “Theo dự án”. Sticky per-user viaPATCH /api/auth/me/preferences/.ProjectBriefSection— a per-project heading + collapsible item group, renders aBadgewith the project name, tapping navigates to/work/projects/:id.ProjectRollupCard— top-of-brief summary: “5 project có nội dung cần chú ý” with a compact list of project names + item counts.UnassignedSection— muted section at bottom for items with no resolved project. Shows “Gán project” inline action that opens a project selector.
- Existing component enhancements:
BriefItemCard— gains optionalproject_badgechip. Tapping navigates to project detail. GainsFact/Inferencebadge when rendered in project-grouped view.
- States:
- Loading: skeleton for each project section (2–3 sections).
- Empty: “Không có nội dung cần chú ý trong Brief này — tất cả project đang ổn.”
- Populated: rollup card + N project sections. Each section collapsed by default, items visible on expand.
- No-permission: not applicable (Brief is role-gated at the page level).
- i18n namespace:
briefextended with keys:view_type_grouped,view_project_grouped,project_rollup_title,project_rollup_count,project_section_title,unassigned_section_title,unassigned_assign_action,badge_fact,badge_inference,project_link_tooltip,toggle_sticky_toast.
- DESIGN.md / TASTE.md: Project sections use
bg-cardwithborder-l-2 border-[var(--color-lumi-blue)]accent. Rollup card uses the same treatment as the Brief header. Direct-FK project context follows existing evidence-source semantics; unresolved items remain inUnassignedwithout an inference claim. Pre-flight checklist applies. - Assistant path (F91 doctrine): Primary interaction path. CEO asks “project nào đang cần tôi?” → Lumi retrieves Brief items grouped by project. The Brief page with project toggle is the secondary (optional) surface. No new forms.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Brief generate with view=project: items grouped by related object’s project FK | backend | backend/tests/test_briefs.py::TestBriefProjectGrouping |
| 2 | Brief generate: cross-project rollup counts correct per project | backend | backend/tests/test_briefs.py::TestBriefProjectGrouping |
| 3 | Brief generate: unassigned items in separate section | backend | backend/tests/test_briefs.py::TestBriefProjectGrouping |
| 4 | Brief generate: project with no items excluded from both rollup and sections | backend | backend/tests/test_briefs.py::TestBriefProjectGrouping |
| 5 | Brief generate: team/private project items excluded when CEO lacks visibility | backend | backend/tests/test_briefs.py::TestBriefProjectVisibility |
| 6 | Cross-tenant isolation: items from org B never grouped into org A’s projects | backend | backend/tests/test_multitenancy.py::TestBriefTenantIsolation |
| 7 | View toggle state persists across page reloads | backend | backend/tests/test_briefs.py::TestBriefViewPreference |
| 8 | Direct project FK is labeled Fact; unresolved project context remains Unassigned until an evidence-backed inference path exists | backend | backend/tests/brief/test_brief_project_grouping.py::TestBriefProjectGrouping |
| 9 | Toggle renders, switches view, project badge navigates to project detail | e2e | frontend/tests/e2e/brief.spec.ts new describe block |
8. Current Behavior
The project-grouped Brief view is shipped on the existing /intel/brief route. BriefSerializer resolves direct project FKs, filters invisible projects, emits project_rollup/items_by_project/unassigned_items, and the web toggle persists brief_view. The current implementation does not infer project associations from evidence text; unresolved items remain Unassigned.
9. Error States
| # | Error | User message | HTTP | Logged |
|---|---|---|---|---|
| ES1 | Cross-tenant brief access (same as F12 ES1) | 404 | 404 | No |
| ES2 | Project visibility filtering removes all items → empty project view | Brief shows empty state (not error) | 200 | No |
| ES3 | View toggle preference save fails (network error) | Toast: “Không thể lưu lựa chọn hiển thị”; toggle reverts to last saved state | n/a (client-side) | Yes (console error) |
| ES4 | Project FK on related object deleted after Brief generation | BriefItem project badge shows “(Đã xóa)” and link is disabled | n/a | No (stale FK is a known F12 edge case pattern) |
10. Diagrams & Docs Touched
- frontend/DIAGRAMS.md: update brief route to note
viewparameter in query string. - backend/DIAGRAMS.md: add
BriefGenerationServiceto the Brief generation flow diagram. - GLOSSARY.md: add entries for “Cross-project rollup”, “Project-grouped brief”.
- docs/specs/done/F12-brief.md: update §3 (in) to cross-reference F101 for project grouping; update §6 to mention the toggle.
- docs/specs/F04-projects.md: no change needed (F04 is unchanged).
11. Implementation Audit (date)
Built in round 03 (2026-07-10). Codebase constraints addressed:
BriefItemproject resolution:related_task → project(Fact),related_risk → related_task → project(Fact),related_commitment → task → project(Fact).DecisionRequesthas no project path — always “Unassigned”. Resolution implemented inBriefSerializer._resolve_item_project()inbackend/apps/briefs/serializers.py.User.ui_preferencesJSONField extended withbrief_view: "type" | "project"— no migration needed.BriefPage.tsxnow supports view toggle between type-grouped and project-grouped rendering viaBriefViewTogglecomponent.BriefSerializerextended withproject_rollup,items_by_project, andunassigned_itemsSerializerMethodFields — active whencontext["view"] == "project".- The
POST /api/briefs/generate/endpoint accepts{"view": "project"}in request body; theviewparameter is additive and does not change the generation cost. GET /api/briefs/today/,GET /api/briefs/, andGET /api/briefs/<id>/accept?view=projectquery parameter.- Project visibility filtering: organization, team (via member’s TeamMembership), and private (via project owner) visibility enforced in serializer.
- Tests: 16 tests added in
backend/tests/brief/test_brief_project_grouping.pycovering all grouping, rollup, unassigned, visibility, tenant isolation, and view preference persistence. - Frontend:
BriefViewToggle,ProjectRollupCard,ProjectBriefSection,UnassignedSectioncomponents infrontend/src/components/brief/.
12. Open Questions / Decisions
- Brief item project FK resolution: Many
BriefItemrecords have no linkedrelated_*object (e.g., Highlights generated from keyword extraction). How should these be project-grouped? Recommendation: leave them in the “Unassigned” section. If future extraction adds project context (F97), they will migrate out automatically. Commitment.projectFK: Commitment has ataskFK but no directprojectFK. Should aprojectFK be added to Commitment for performance? With the M2Mevidence_sourcesfallback, a join throughtask.projectcovers the data need. Recommendation: do not add a new FK unless query profiling shows a bottleneck — the join throughtaskis one hop and matches the existing data model.- View toggle scope: Should the toggle affect only the current user’s brief, or should the org have a consistent view? Recommendation: per-user. The CEO and a PM may want different groupings.
- Cross-project rollup reactivity: Should the rollup update in real time (WebSocket) or only on page load / re-generate? Recommendation: on generation and page load only. WebSocket pushes for Brief are a separate feature (future).
- Unassigned section action: “Gán project” inline action — should this create a new BriefItem linking to a project, or should it modify the source object (e.g., add project to the Risk)? Recommendation: link the BriefItem to the project via a new
BriefItem.projectFK (nullable, many-to-one). This is a lightweight override that does not modify the source operational object. If the source later gains a project FK, the BriefItem follows the source.
13. Bug / Change Log
2026-07-10 — Initial spec
Type: Feature
Summary:
- Draft spec for F101 — CEO Brief by project. Covers project grouping, cross-project rollup, view toggle, unassigned items, Fact vs Inference labeling, and visibility filtering.
Reason:
- Tester feedback (issue #23): CEO needs Brief items grouped by project so they can act per-project without manually re-sorting.
2026-07-10 — Round 03 implementation
Type: Feature
Summary:
- Implemented F101 per spec §§4–7. Backend: project resolution from BriefItem’s related objects (task, risk→task, commitment→task), project visibility filtering (organization/team/private),
BriefSerializeroutputsproject_rollup,items_by_project,unassigned_itemswhenview=project. Frontend:BriefViewToggle,ProjectRollupCard,ProjectBriefSection,UnassignedSectioncomponents wired intoBriefPage.tsx. View preference persisted viaPATCH /api/auth/me/preferences/. 16 backend tests + i18n keys (en/vi).
2026-07-16 — Contract Graph truth reconciliation
Type: Documentation and contract-boundary correction
Summary:
- Reconciled the older Fact/Inference acceptance language with executable behavior: F101 uses direct related-object project FKs, while unresolved items remain
Unassigned. - Kept any future inference path outside this feature until it has an explicit product decision, confidence semantics, and grounding proof.
2026-07-14 — Contract Graph migration
Type: Contract ownership and implementation-truth reconciliation
Summary:
- Added canonical CUE ownership and executable backend/Vitest proof IDs for direct-FK project grouping, visibility filtering, the Brief toggle, and preference persistence.
- Corrected the stale draft/current-behavior language and removed the nonexistent
by-projectendpoint claim. Evidence-based project inference remains an explicit product/implementation gap.