shipped for the MVP scope (Phase 1 + assistant capture Phase 2)

Spec: F97 — Project Progress Updates

Status: shipped for the MVP scope (Phase 1 + assistant capture Phase 2)

1. Problem & CEO Job

See clearly + trust the signal. Members and PMs report through assistant chat (F91) and Daily Pulse (F07/F83), but Lumi does not attach those reports to a project. Project-specific progress, blockers, and commitments therefore reach the Brief as generic items, forcing the CEO to re-sort them. F97 disambiguates the project at capture, groups progress, and proposes evidence-linked Task/Risk/Decision items in the right project.

Source issue: https://github.com/streamentry/lumi-fb/issues/19

2. User Stories & Acceptance Criteria

  • As a project member, I want to report progress via the assistant chat or Daily Pulse without naming the project every time, so that my update costs one sentence.

    • Given a pulse submission or chat message containing a project reference (name or acronym), when processed, then Lumi resolves to a visible Project (F04) record in the org with confidence ≥0.70.
    • Given a project-marked chat update with no single confident match, when processed, then the proposal is tagged needs_context and Lumi asks one concise clarifying question rather than guessing. Generic unscoped captures retain their existing non-project flow.
  • As a PM, I want Lumi to aggregate per-project progress from multiple members’ updates over the last N days, so that I see a project status without chasing individuals.

    • Given project-scoped signals in the last 7 days, when aggregated, then the output includes a deterministic, evidence-honest summary of done tasks, next tasks, blockers, and pending decisions where those states exist; each returned signal carries its EvidenceSource links and confidence.
    • Given no new signals for a project in 7 days, when the project detail loads, then the Progress card shows a localized “no recent updates” note — stale silence is surfaced, not hidden.
  • As a PM, I want Lumi to propose Task, Risk, or Decision items from project updates, so that I can accept or edit in one tap rather than manually entering them.

    • Given a project update contains an actionable commitment, when extracted, then a Task proposal or existing pulse commitment-to-task proposal carries the project FK and source evidence.
    • Given a project update describes a blocker or material risk to timeline/cost/quality, when extracted, then a Risk proposal is created or proposed with the project FK, risk type, severity, and evidence.
    • Given a project update asks for a decision, when extracted, then a DecisionRequest proposal is created (linked to the project) with the decision question and options if stated. Chat decision proposals remain read-only until the existing F91 decision executor is shipped.
  • As a CEO, I want project progress to appear in the CEO Brief (F12) when there are significant changes, so that I know which projects need my attention without opening a separate surface.

    • Given a project-scoped task, risk, or decision is included in the Brief, then the existing Brief section exposes project context and the item’s evidence links; urgency filtering remains owned by F12/F101.
  • As a member, I want to see a project’s latest progress summary when I open the project detail, so that I catch up without re-reading every pulse.

    • Given a project board loads, when there are project-scoped signals from the last 7 days, then the inline ProjectProgressCard renders the aggregated summary and lazily opens each available source EvidenceSource.

3. Scope Boundaries

  • In: Project disambiguation from existing pulse/chat input (F91 assistant path + F07/F83 pulse form). Per-project aggregation of progress signals. Proposal of project-scoped Task/Risk/Decision items. Brief integration: project-scoped items appear in their Brief section with project context. Project detail page progress summary card.
  • Delegated to F99: commitment deduplication by content fingerprint and EvidenceSource merge; F97 carries project context into that existing proposal path. F99 owns the duplicate merge acceptance and proof; it is not an F97 shipped criterion.
  • Out: Bot transport (Telegram/WhatsApp integrations — separate spec). Standalone project reporting dashboard (deferred; the Brief + project detail page are sufficient for MVP). Full-editor project timeline/Gantt (F04 §3 explicitly out). Milestone tracking (not in scope — requires timeline model). ERP/material/BOQ integration (parent contract #15 explicitly out).

Assistant path (F91 doctrine): This feature is completable entirely through the assistant conversation. A member sends a progress update in the chat; Lumi confirms the project, proposes extracted items. The pulse form is the secondary path. No new intake forms. Form parity: the project detail page’s progress section is a read-only view, not a form.

4. Data Model

No new models. Additions to existing models:

PulseEntry (existing, extended): no schema change. Project disambiguation happens at extraction time, not at submission — the answers_json is unchanged.

SignalInboxItem (existing, extended): project FK already exists (F32 §4). This spec fills the gap: when a chat message is processed via the agent loop (F91), if project is detected, the resulting SignalInboxItem (or the extraction output directly) links the project.

Task (existing, work.Task): project FK already exists (F04). No change.

Risk (existing, operations.Risk): project FK — verify existence; if absent, add nullable FK to work.Project. Each risk item carries evidence_sources M2M (already exists).

DecisionRequest (existing, operations.DecisionRequest): project FK — same check as Risk; add nullable FK to work.Project if absent. Each decision carries evidence_sources M2M (already exists).

BriefItem (existing): no new FK needed. Project context for BriefItems is derived via related_risk.project / related_task.project / related_decision.project at generation time. The Brief generator resolves project context by joining through the related object’s project FK.

All additions follow existing project FK pattern (nullable, SET_NULL on delete). Scale rule: all new FKs use composite indexes (organization, project_id, created_at DESC) for the paginated project signal queries.

5. API Contract

MethodPathAuth/RoleRequestResponseErrors
GET/api/projects/<id>/progress/member (project visibility)?days=7&cursor={summary: str?, signals: [{type, id, title, status, confidence, created_at, evidence_source_ids}...], cursor: str?}404 (cross-tenant/visibility); 403
The MVP uses the GET response as the aggregate read path; no separate POST aggregation route is needed.

Tenant scoping: all queries filter via organization_id = request.organization_id. Project visibility filtering uses project’s existing visibility field (team/private/organization).

The current aggregation summary is deterministic rather than LLM-generated: it counts only states present in the returned evidence-backed rows. Each sourced item includes linked EvidenceSource ids and confidence. A future LLM summary must preserve this distinction and never replace the source rows.

6. UI / UX

  • Route additions: None. Progress is surfaced in existing pages:
    • /work/projects/:id/board (project board page) — inline “Progress” section above the kanban board
    • /intel/brief (Brief page) — existing sections now carry project_name on each item card when a project is linked
    • /intel/chat (Assistant) — confirmed progress updates appear as action proposal cards
  • New component(s): ProjectProgressCard — an expandable card in the project board page showing aggregated progress summary + individual sourced items. States: loading, empty (“No recent updates”), populated, error. Uses shadcn <Card> and the existing EvidencePopover for lazy source inspection.
  • Brief item enhancement: BriefItem cards gain an optional project_badge — a small chip with the project name, clicking which filters the page or links to /work/projects/:id. Render only when related_item.project is non-null.
  • i18n namespace: projects extended with keys: progress_title, progress_empty, progress_item_done, progress_item_next, progress_item_blocked, progress_item_risk, progress_item_decision, progress_badge, progress_since, progress_evidence_link, progress_needs_context, progress_expand, progress_collapse, and progress_error.
  • Assistant path (F91 doctrine): Primary path. After confirming a proposal, the assistant shows the project context on the action card. The pulse form is the secondary path (no new intake).
  • States: loading (skeleton), empty (muted “Chưa có cập nhật cho project này trong 7 ngày qua”), populated (grouped timeline), error (toast + retry).

7. Test Plan

#CaseTypeWhere
1Pulse text with explicit project name → project resolved, signal created with project FKbackendbackend/tests/test_projects.py::TestProjectProgress
2Pulse text with ambiguous project reference → clarification asked via needs_contextbackendbackend/tests/test_projects.py::TestProjectProgress
3Project progress aggregation: 3 signals over 5 days → summary + 3 items returnedbackendbackend/tests/test_projects.py::TestProjectProgress
4Project progress aggregation: no signals in 7 days → empty statebackendbackend/tests/test_projects.py::TestProjectProgress
5Duplicate commitment from two sources → one Task created, both EvidenceSources appendedbackendbackend/tests/test_projects.py::TestProjectDedup
6Cross-tenant: project from org A not accessible from org Bbackendbackend/tests/test_multitenancy.py::TestProjectTenantIsolation
7Cross-tenant: progress signal from org A not leaked to org Bbackendbackend/tests/test_multitenancy.py::TestProjectTenantIsolation
8Brief generation: project-scoped items appear with project badgebackendbackend/tests/test_briefs.py::TestBriefProjectScope
9Brief generation: project-scoped items filtered by visibility (team/private)backendbackend/tests/test_briefs.py::TestBriefProjectScope
10Project progress card: populated, empty, and error statesfrontend unitfrontend/src/test/work/ProjectProgressCard.test.tsx
11Project progress page: populated state renders summary + itemse2edeferred to the existing F49 project-board E2E surface
12Assistant project context survives capture → serializer → confirmbackendbackend/tests/assistant/test_f91_p25_capture_confirmable.py::TestCaptureProposalRoundTrip

8. Current Behavior

Project names and two-word acronyms are resolved during both pulse extraction paths and assistant run-capture, scoped to active/paused projects visible to the current member. A single safe match is carried into the proposal and confirm payload; a project-marked chat update with no single match is shown as needs_context and is not given a write-capable action. Private and team projects are rechecked at confirmation time.

GET /api/projects/<id>/progress/ returns tenant- and visibility-scoped Task/Risk/Decision rows from the requested window, with evidence ids and a deterministic summary. ProjectProgressCard renders loading, populated, empty, and error states above the project board. Brief project badges and source popovers reuse the existing F101/F86 surfaces.

The implementation intentionally does not invent a project for generic unscoped captures. Alias vocabulary and cross-project acknowledgement remain explicit follow-up work owned by F83 rather than hidden heuristics in F97. Commitment deduplication is F99-owned and is not claimed by this contract.

9. Error States

#ErrorUser messageHTTPLogged
ES1Project not found during disambiguation”Lumi chưa xác định được project — vui lòng bổ sung tên project.”n/a (in-thread)Yes
ES2Cross-tenant project access404404No
ES3Project visibility (team/private) blocks access to progress404404No
ES4AI extraction failure during project signal processingSignal saved as failed; retry available via pulse /process/200 (pulse saved)Yes
ES6LLM provider unavailable for aggregationAggregation remains available because the MVP summary is deterministic; no model call is required200Yes

10. Diagrams & Docs Touched

  • backend/DIAGRAMS.md: add Project → Risk, Project → DecisionRequest arrows; note project FK on existing models.
  • GLOSSARY.md: add “Project progress signal”, “Project disambiguation”.
  • docs/specs/F04-projects.md: add forward reference to F97 under §12 Open Questions.
  • docs/specs/F12-brief.md: add forward reference to F97 for project-scoped Brief items.

11. Implementation Audit (2026-07-12)

Phase 1 + assistant capture Phase 2 built. Backend + frontend implementation complete for the MVP scope. Codebase state:

  • ✅ work.Project exists (F04) with status + visibility fields.
  • ✅ work.Task has project FK (pre-existing).
  • ✅ operations.Risk and operations.DecisionRequest — nullable project FK added (migration 0014_f97_project_fk_on_risk_and_decision.py), composite indexes risk_org_proj_created_idx and decision_org_proj_created_idx.
  • ✅ Pulse extraction pipeline (apps/ai/services/extraction.py) — project resolution hooked into both LLM path (_extract_via_llm) and keyword path (_extract_pulse_entry_atomic).
  • ✅ apps/ai/services/project_disambiguation.py — resolve_project_from_text() pure function (exact name 0.95, acronym 0.85, threshold 0.70); resolve_projects_from_text() for cross-project mentions.
  • ✅ Assistant run-capture project context: resolved project metadata is serialized on proposals and carried into confirmable Task/Risk actions; ambiguous project-marked chat updates ask for clarification before creating an action.
  • ✅ GET /api/projects/<id>/progress/?days=7 — returns {summary, signals, cursor} with visibility enforcement (private / team / org).
  • ✅ Progress summary: deterministic done/next/blocked/blocker/pending-decision counts, with empty-state copy in the card.
  • ✅ BriefItemSerializer — added project_name and project_id SerializerMethodFields; traverses direct risk.project and decision.project FKs and task→project chain; select_related updated.
  • ✅ ProjectProgressCard React component — TanStack Query, expandable card, type-tagged signal rows (task/risk/decision), skeleton + error + empty states.
  • ✅ ProjectBoardPage — ProjectProgressCard rendered above kanban columns.
  • ✅ BriefPage — project badge link (data-testid="brief-project-badge") linking to /work/projects/:id/board.
  • ✅ i18n: progress keys and assistant project-context keys are present in en, vi, and all 59 other locale files.
  • ✅ Backend tests: project progress, visibility, action payload, and chat capture round-trip coverage pass; the focused F97 run is 44 tests green.
  • ✅ Frontend tests: ProjectProgressCard populated/empty/error states, board integration, and ProposalCard project context pass; TypeScript is clean.
  • ✅ TypeScript: 0 errors.

Deferred by boundary or ownership:

  • A dedicated resolve_project tool is unnecessary for the current capture contract; resolution runs inside run-capture and is covered by the round-trip test.
  • LLM-written aggregation prose is intentionally deferred; the deterministic summary is the honest MVP output.
  • Cross-project pulse acknowledgement and alias support remain future F83 design work.
  • Commitment deduplication remains F99-owned.
  • A dedicated /work/projects/:id/progress route remains unnecessary while the inline board card is sufficient.

12. Open Questions / Decisions

  1. Project disambiguation scope: Exact names and two-word acronyms ship first. Nicknames/project codes should be added through the existing OrgPulseConfiguration.custom_company_vocab contract, with tests before enabling automatic attachment.
  2. Pulse vs chat path divergence: Both paths now call the shared resolver; pulse remains batch extraction while chat can ask an immediate clarification question.
  3. Risk/DecisionRequest project FK: Determine whether these models need a new FK or can derive project via evidence_sources → SignalInboxItem.project. The latter avoids a new migration but requires a join at query time. Recommendation: add nullable FK for direct query performance (the join path is available as fallback for historical data).
  4. Cross-project signals: A single pulse or message may mention multiple projects. Should Lumi create progress items for each project, or require the member to separate them? Recommendation: create for each resolved project, with a mention of the ambiguity in the response (“Ghi nhận cập nhật cho 2 project: Duy Tân và Vincom.”).
  5. UI for project progress on project detail page: Should this be a dedicated route (/work/projects/:id/progress) or an inline section on the existing detail page? The latter reduces navigation; choose inline section first, promote to a tab if content grows.

13. Bug / Change Log

14. Contract Graph evidence boundary

The canonical contract is contracts/features/F97.cue. It registers the shipped backend and frontend proof locations without treating source inspection or proof collection as execution. Missing backend/frontend dependencies, the authenticated browser journey remain explicit gaps. F99-owned duplicate commitment merging is outside F97 scope and is not represented as an F97 acceptance gap.

2026-07-10 — Initial spec

Type: Feature

Summary:

  • Draft spec for F97 — project progress updates via existing channels (assistant chat + Daily Pulse). Covers project disambiguation, per-project aggregation, proposal of project-scoped Task/Risk/Decision items, deduplication, and Brief integration.

Reason:

  • Tester feedback (issue #19): members/PMs need to report progress without leaving existing channels; Lumi needs to aggregate per-project status and propose evidence-linked items.

2026-07-12 — Assistant capture and honest progress summary

Type: Feature completion

Summary:

  • Added member-visible project resolution to assistant capture and both pulse extraction paths.
  • Carried resolved project metadata through proposal serialization and confirmable Task/Risk actions, with a clarification gate for ambiguous project-marked chat updates.
  • Replaced the count-only progress sentence with a deterministic summary of done, next, blocked, blocker, and pending-decision states; added frontend states and EvidenceSource popovers.

Verification:

  • Backend focused F97 suite: 44 passed.
  • Frontend ProjectProgressCard, ProjectBoardPage, and ProposalCard suites: 32 passed; TypeScript and locale parity passed.