Derived from the canonical F101 CUE contract: **partial**.

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.
  • 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).
  • 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 EvidenceSource links. Items without that direct context remain in Unassigned; this feature does not claim an AI-inferred project association.
  • 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=private owned 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.

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 Inference label are not shipped by F101; unresolved items remain Unassigned until a separately specified, evidence-backed association path exists.
  • Out: Per-project standalone Brief page (deferred — the toggle view on the existing /intel/brief route 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:

  1. Type-grouped (current behavior, unchanged): items sorted by type → priority.
  2. 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

MethodPathAuth/RoleRequestResponseErrors
POST/api/briefs/generate/member{view?: "type"|"project"}Brief with items grouped per requested view403; 429 (throttle)
GET/api/briefs/member?view=type|projectBrief list with items in requested grouping403
GET/api/briefs/<id>/member?view=type|projectSingle brief with items in requested grouping404
PATCH/api/auth/me/preferences/member{brief_view: "type"|"project"}Updated preferences400

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, new view toggle.
  • New component(s):
    • BriefViewToggle — a segmented control (shadcn <ToggleGroup>) in the Brief header: “Theo loại” / “Theo dự án”. Sticky per-user via PATCH /api/auth/me/preferences/.
    • ProjectBriefSection — a per-project heading + collapsible item group, renders a Badge with 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 optional project_badge chip. Tapping navigates to project detail. Gains Fact / Inference badge 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: brief extended 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-card with border-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 in Unassigned without 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

#CaseTypeWhere
1Brief generate with view=project: items grouped by related object’s project FKbackendbackend/tests/test_briefs.py::TestBriefProjectGrouping
2Brief generate: cross-project rollup counts correct per projectbackendbackend/tests/test_briefs.py::TestBriefProjectGrouping
3Brief generate: unassigned items in separate sectionbackendbackend/tests/test_briefs.py::TestBriefProjectGrouping
4Brief generate: project with no items excluded from both rollup and sectionsbackendbackend/tests/test_briefs.py::TestBriefProjectGrouping
5Brief generate: team/private project items excluded when CEO lacks visibilitybackendbackend/tests/test_briefs.py::TestBriefProjectVisibility
6Cross-tenant isolation: items from org B never grouped into org A’s projectsbackendbackend/tests/test_multitenancy.py::TestBriefTenantIsolation
7View toggle state persists across page reloadsbackendbackend/tests/test_briefs.py::TestBriefViewPreference
8Direct project FK is labeled Fact; unresolved project context remains Unassigned until an evidence-backed inference path existsbackendbackend/tests/brief/test_brief_project_grouping.py::TestBriefProjectGrouping
9Toggle renders, switches view, project badge navigates to project detaile2efrontend/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

#ErrorUser messageHTTPLogged
ES1Cross-tenant brief access (same as F12 ES1)404404No
ES2Project visibility filtering removes all items → empty project viewBrief shows empty state (not error)200No
ES3View toggle preference save fails (network error)Toast: “Không thể lưu lựa chọn hiển thị”; toggle reverts to last saved staten/a (client-side)Yes (console error)
ES4Project FK on related object deleted after Brief generationBriefItem project badge shows “(Đã xóa)” and link is disabledn/aNo (stale FK is a known F12 edge case pattern)

10. Diagrams & Docs Touched

  • frontend/DIAGRAMS.md: update brief route to note view parameter in query string.
  • backend/DIAGRAMS.md: add BriefGenerationService to 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:

  • BriefItem project resolution: related_task → project (Fact), related_risk → related_task → project (Fact), related_commitment → task → project (Fact). DecisionRequest has no project path — always “Unassigned”. Resolution implemented in BriefSerializer._resolve_item_project() in backend/apps/briefs/serializers.py.
  • User.ui_preferences JSONField extended with brief_view: "type" | "project" — no migration needed.
  • BriefPage.tsx now supports view toggle between type-grouped and project-grouped rendering via BriefViewToggle component.
  • BriefSerializer extended with project_rollup, items_by_project, and unassigned_items SerializerMethodFields — active when context["view"] == "project".
  • The POST /api/briefs/generate/ endpoint accepts {"view": "project"} in request body; the view parameter is additive and does not change the generation cost.
  • GET /api/briefs/today/, GET /api/briefs/, and GET /api/briefs/<id>/ accept ?view=project query 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.py covering all grouping, rollup, unassigned, visibility, tenant isolation, and view preference persistence.
  • Frontend: BriefViewToggle, ProjectRollupCard, ProjectBriefSection, UnassignedSection components in frontend/src/components/brief/.

12. Open Questions / Decisions

  1. Brief item project FK resolution: Many BriefItem records have no linked related_* 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.
  2. Commitment.project FK: Commitment has a task FK but no direct project FK. Should a project FK be added to Commitment for performance? With the M2M evidence_sources fallback, a join through task.project covers the data need. Recommendation: do not add a new FK unless query profiling shows a bottleneck — the join through task is one hop and matches the existing data model.
  3. 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.
  4. 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).
  5. 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.project FK (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), BriefSerializer outputs project_rollup, items_by_project, unassigned_items when view=project. Frontend: BriefViewToggle, ProjectRollupCard, ProjectBriefSection, UnassignedSection components wired into BriefPage.tsx. View preference persisted via PATCH /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-project endpoint claim. Evidence-based project inference remains an explicit product/implementation gap.