Spec: F07 — Daily Pulse
Status: shipped · Audited: 2026-06-10
1. Problem & CEO Job
The wedge — everything downstream depends on this. Six honest sentences per person per day replace the standup the CEO cannot attend and the status report nobody writes. It must take under two minutes and never feel like surveillance, or participation stops and the Brief starves; a first-time warehouse worker must finish it on a phone without training.
2. User Stories & Acceptance Criteria
- As a CEO, I want to replace the daily standup I cannot attend (or that no one writes honestly) with a format the whole org can do in under two minutes, so that I get honest, traceable operating intelligence without changing anyone’s routine.
- What it replaces: Today the owner gets status the only way the team will give it — scattered Zalo/WhatsApp messages and a verbal “anh ơi xong chưa?” — unstructured, unrecorded, and impossible to act on without re-reading every thread. A Zalo status message scrolls away; nothing pulls the commitment, the risk, or the decision out of it. Lumi’s pulse is the opposite: six fixed questions (this §, below), each mapped to an extraction target, so the owner never re-reads raw chat — they read extracted Commitments, Risks, and DecisionRequests (F08).
- Given the form, then all questions are free-text, all optional — blank answers are valid; a single answered question is a valid submission.
- Given I already submitted today, when I open
/intel/pulse, then I see my submission (read-back), not an empty form — one pulse per person per day (DB-enforced).
- As a team member, I want my daily answers to produce visible, traceable signals in the Brief without me writing a report, so that the habit sustains without extra effort.
- What it replaces: A Zalo group message records what someone typed, then buries it under the next fifty messages; an Excel “daily report” tab, where one exists at all, records a row no one extracts meaning from. Neither turns an update into Commitments, Risks, or Decisions — the owner must re-read the thread and decide what matters. Lumi extracts items (F08) and attributes them to the member with EvidenceSource links, so the member’s pulse is the single input that feeds the Brief, Commitment, Risk, and Decision pipelines. Without this feedback loop, the pulse habit starves.
- Given a submitted pulse, when processed, then extracted items (F08) are attributed to me and my team, traceable via EvidenceSource.
- As a CEO, I want to see at a glance who is in the pulse habit and who is not, so that I fix the behaviour gap before the Brief starves — not chase individuals one by one.
- What it replaces: A Zalo group shows who happened to message today, but not habit history — which members go quiet on which days of the week, and the owner is left chasing each person by hand (“anh ơi báo cáo đi”). Lumi tracks per-member pulse history × working-day cadence, and surfaces adherence gaps that correlate with Brief signal degradation. The owner’s diagnostic question becomes “is the pulse supply chain healthy?” not “did thi công report today?”
- Given today’s date, pulse list shows submissions; gaps are visible.
The six questions (fixed, Vietnamese-first — DEFAULT_PULSE_QUESTIONS)
- Hôm nay bạn hoàn thành việc gì quan trọng?
- Bạn đang kẹt ở đâu?
- Bạn đã hứa gì với khách / team / nhà cung cấp?
- Việc nào cần CEO hoặc quản lý quyết?
- Có rủi ro nào về khách, doanh thu, công nợ, đơn hàng hoặc deadline không?
- Ngày mai việc quan trọng nhất của bạn là gì?
Each maps to an extraction target (2→risk/blocked, 3→commitment, 4→decision, 5→risk). Changing questions = changing the AI contract → requires updating F08 in the same PR.
3. Scope Boundaries
- In: web form, Telegram entry (F21), today/submit/list/process endpoints, EvidenceSource creation, per-day uniqueness.
- Out: custom per-org questions (MVP keeps the extraction contract stable), reminders/nags (Telegram daily prompt covers it), manager review/approval of pulses (it’s a trust tool, not a control tool).
Competitive note — the Excel “daily report” tab and owner memory. Where a Vietnamese SME has any written status habit at all beyond chat, it is a shared spreadsheet — a manual-entry tab that records rows but produces no structured signal unless the owner builds formulas, rollups, and relations they will never build, because the value is not obvious until it is built. The common case is even thinner: status lives in owner memory, reconstructed by asking each person. Both produce notes the owner must mentally filter; Lumi’s pulse produces evidence-linked operating intelligence — Commitments, Risks, and DecisionRequests extracted from each answer, traceable to the person who said it, persisting in the Brief. The difference is not the form; it is what happens after the submit button is pressed.
Competitive note — WhatsApp/Zalo groups. Vietnamese SME owners currently get daily status by reading WhatsApp or Zalo group messages — unstructured, unrecorded, invisible to anyone who wasn’t in the thread. Lumi’s fixed six-question pulse (§2) replaces this with a structured, phone-first form that captures every answer as traceable evidence. The feedback loop is the difference: a Zalo status message vanishes; a pulse submission creates evidence-linked Commitments, Risks, and DecisionRequests (via F08) that persist in the Brief. The real first-market competitor is Zalo/WhatsApp + Excel + owner memory — not any US check-in feature — and the win is treating the daily check-in as a first-class intelligence source rather than a message that scrolls away.
Relation to the F70 wedge. The pulse is the steady-state capture habit. It assumes a team already in Lumi submitting six sentences a day — which is exactly what a cold owner does not have on day one. The cold-start is F70 (Chaos Import → Boss Brief): the owner pastes one messy Zalo/Excel dump and gets a brief in their first session, before anyone is in the pulse habit. F70 is the wedge; the daily pulse is the habit it graduates into once staff are onboarded (Loop 1).
4. Data Model
pulses.PulseEntry—status: draft → submitted → processed | failed;raw_text(joined answers),answers_json, unique (org, user, date).pulses.EvidenceSource— the provenance anchor for all AI items;source_type=pulse_entry,content_textfrom the pulse.
5. API Contract
| Method | Path | Min role | Notes |
|---|---|---|---|
| GET | /api/pulses/today/ | member | My pulse for today (200 not_submitted if none — not 404) |
| POST | /api/pulses/submit/ | member | Submit answers; creates EvidenceSource; auto-triggers extraction. Accepts optional idempotency_key (UUID) — same key + same org+user → returns existing pulse without re-extraction (issue #1647) |
| GET | /api/pulses/ | viewer | List (read-only ViewSet) |
| POST | /api/pulses/<id>/process/ | member | Idempotent retry for failed entries |
| CRUD | /api/pulses/evidence-sources/ | member | Tenant-scoped evidence records; the Contract Graph declares list, retrieve, create, update, and delete operations explicitly |
6. UI / UX
/intel/pulse(PulsePage.tsx). One screen, six labeled textareas, one submit. No wizard, no progress bar theatrics.- States: not-yet-submitted (form), already-submitted (read-back + “Đã gửi ✓”), submitting, error (actionable toast, answers preserved — never lose typed text).
- Mobile-first: this is the one page used by every employee, mostly on phones.
- Leader-only Team check-ins card above the form (gated on
request.membership.role ∈ {owner, admin, manager}): badge withsubmitted/total, success state when all done, otherwise the list of missing members. Carries a one-tap Share affordance that turns the day’s adherence into a chat-pasteable artifact ({org} · Lumi Pulse · {date} · ✅ count line · missing list · F59 referral footer) — same distribution-object hook the Brief and Today surfaces use (buildPulseAdherenceShareText+usePulseAdherenceShare, mirrorsbuildBriefShareText+useBriefShare). The button is hidden on loading / error / missing-data paths so there is never a share affordance without a concrete artifact to share. - i18n:
pulse.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Submit happy path; resubmit same day overwrites the same-day entry and returns 200 on repeat submit | backend | tests/test_api.py::TestPulse, tests/test_lumi.py::TestPulseSubmission |
| 2 | Submit creates EvidenceSource with pulse text | backend | tests/test_api.py::TestPulse |
| 3 | Submit auto-triggers extraction; failure sets failed; /process/ retries failed pulses idempotently | backend | tests/test_api.py::TestPulse |
| 4 | Form fill + submit + read-back; failed shows retry button | e2e | describe("Daily Pulse") |
| 5 | Team check-ins share artifact is deterministic, carries org name when present, plural-aware missing list, attributed footer when ?ref= available | frontend | frontend/src/test/pulseAdherenceShare.test.ts::buildPulseAdherenceShareText |
| 6 | Pulse share hook uses native navigator.share first, falls back to clipboard, treats AbortError as no-op, surfaces toast on success/error | frontend | ⚠️ test not present — usePulseAdherenceShare.test.tsx does not exist; hook behaviour is partially covered by frontend/src/test/TeamCheckInsCard.test.tsx (row 7); frontend/src/test/pulseAdherenceShare.test.ts covers the builder only |
| 7 | Leader card renders a Share button when adherence data is present and clicking it invokes the hook; hidden on loading/error/null-data paths | frontend | frontend/src/test/TeamCheckInsCard.test.tsx |
| 8 | Submit with idempotency_key — same key + same org+user returns existing pulse without re-extraction (one Commitment, not two) | backend | backend/tests/api/test_pulses.py::TestPulse::test_submit_with_idempotency_key_returns_same_on_repeat |
| 9 | Different idempotency keys for same org+user+date create independent submissions | backend | backend/tests/api/test_pulses.py::TestPulse::test_submit_with_idempotency_key_different_keys_are_independent |
| 10 | Same idempotency key in different orgs does not collide | backend | backend/tests/api/test_pulses.py::TestPulse::test_submit_idempotency_key_tenant_isolation |
| 11 | Omitting idempotency_key still works (backward compat with web forms) | backend | backend/tests/api/test_pulses.py::TestPulse::test_submit_without_idempotency_key_still_works |
| 12 | KMP submit generates and passes an idempotency key to the repository | shared | android/shared/src/commonTest/.../PulseScreenModelTest.kt — submit passes an idempotency key to the repository |
| 13 | KMP flushQueue passes stored idempotency key on retry | shared | android/shared/src/commonTest/.../PulseScreenModelTest.kt — flushQueue passes stored idempotency key on retry |
| 14 | Queued submission persists idempotency key in PendingPulseSubmission | shared | android/shared/src/commonTest/.../PulseScreenModelTest.kt — queued submission persists idempotency key in PendingPulseSubmission |
8. Current Behavior
Shipped. The six-question pulse form (PulsePage.tsx), submission endpoint (POST /api/pulses/submit/), evidence capture (EvidenceSource creation), and downstream extraction pipeline all exist. Submit auto-triggers extraction (2026-06-10 fix); extraction failures are caught — the pulse is saved with status="failed" and a retry button is shown. The draft status exists in the model but is unused by the web flow (the Telegram staged-answering flow uses TelegramConversation state, not draft pulse records). Submission is intentionally resilient: answers are never lost even when enrichment fails.
Known gap — pulse questions are not localised. DEFAULT_PULSE_QUESTIONS is a hardcoded Python list of Vietnamese strings. Users in Malaysia, Saudi Arabia, or other target markets see questions in Vietnamese. Fixing this requires changing the AI extraction contract (see §11 audit row 11 and §12 open question).
9. Error States
| # | Error | Behavior | Code reference |
|---|---|---|---|
| ES1 | AI extraction failure on submit — extract_pulse_entry raises during submit. | Pulse saved with status="failed", exception logged. Submit still returns 201. Frontend shows retry button. Answers never lost. | views.py:63–67 try/except; test_api.py::TestPulse::test_submit_extraction_failure_sets_failed |
| ES2 | Retry failed pulse via /process/ — POST /api/pulses/<id>/process/ on a failed pulse. | Retries extraction; on success: status→"processed", items created. On failure: status="failed", error_message set, returns 200 (consistent with submit). | views.py PulseViewSet.process; test_api.py::TestPulse::test_process_retries_failed_pulse; test_red_r18.py::TestF08ExtractionFailure::test_process_endpoint_sets_failed_on_error |
| ES3 | Re-processing already-processed pulse — /process/ on status="processed" entry. | Idempotent: returns 200 with empty extraction lists; no duplicate items created. | services.py:87–89; test_api.py::TestPulse::test_process_idempotent_on_processed |
| ES4 | No EvidenceSource found for /process/ — no source_type=pulse_entry record for this user. | evidence is None; extract_pulse_entry creates items without evidence links rather than erroring. | views.py:80–86 None fallback; services.py:93–99 |
| ES5 | today_pulse when no pulse exists — GET /api/pulses/today/ on a day with no submission. | Returns 200 {"questions": DEFAULT_PULSE_QUESTIONS, "date": today, "status": "not_submitted"} — not a 404. (§5 originally said “or 404 → show form” — code returns 200.) | views.py:28–30 except DoesNotExist path |
| ES6 | Unauthenticated / not-org-member — requests without auth or org membership. | Unauthenticated → 401. Authenticated without org membership → 403. | views.py:22, 33 @permission_classes([IsAuthenticated, IsOrgMember]) |
| ES7 | /process/ extraction exception — extraction raises on retry attempt. | Wraps in try/except: logs exception, sets pulse.status="failed" with error_message, returns 200 instead of 500. | test_red_r18.py::TestF08ExtractionFailure::test_process_endpoint_sets_failed_on_error |
| ES8 | Cross-tenant access — user from org B calls GET /api/pulses/. | Returns empty list (not 403) — queryset filters by organization_id. Same for today_pulse. | views.py:72–75; test_api.py::TestPulse::test_pulse_tenant_isolation |
10. Diagrams & Docs Touched
DIAGRAMS.md AI pipeline · docs/AI_PIPELINE.md · GLOSSARY Pulse terms.
11. Implementation Audit (2026-06-14)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | Submit auto-triggers extraction; /process/ is idempotent retry for failed | ✅ match | 2026-06-10 fix; test_api.py::TestPulse::test_submit_extraction_failure_sets_failed |
| 2 | status=failed on extraction exception; submit returns 201; retry button shown in UI | ✅ match | views.py:63–67 try/except; PulsePage.tsx retry affordance |
| 3 | draft status: model has it, web flow never uses it — documented, not removed | ✅ match | models.py:7; views.py:47 always sets "submitted" |
| 4 | EvidenceSource created on submit with pulse text (source_type=pulse_entry) | ✅ match | views.py submit path; test_api.py::TestPulse |
| 5 | Slot-based confidence scoring: preferred slots (q3/q4/q5) → 0.75; other slots → 0.6 | ✅ match | apps/ai/services.py inline at ~922 (commitment), ~965 (risk), ~1008 (decision) — no _compute_confidence function exists; confidence is assigned inline per extraction loop |
| 6 | At most one item per type per pulse: break after first keyword match | ✅ match | apps/ai/services.py:955, 1039; multi-commitment pulses silently drop later mentions |
| 7 | Company name auto-linking on commit/risk extraction | ✅ match | apps/ai/services.py detect_company_in_text calls after each Commitment/Risk creation |
| 8 | Resubmission same day: update_or_create silently overwrites and returns 200 on repeat submit | ✅ match | views.py:47–60; test_api.py 201 first then 200 repeat; §7 corrected 2026-07-02 |
| 9 | today_pulse returns 200 {"status": "not_submitted"} — not 404 | ✅ match | views.py:28–30; §5 corrected 2026-06-22 |
| 10 | Date parameter not validated against today — past/future dates accepted | ⚠️ gap | serializers.py:18 accepts any date; views.py:34 uses client-supplied date; no test for future-date pulse |
| 11 | DEFAULT_PULSE_QUESTIONS — hardcoded Vietnamese; international orgs see Vietnamese questions | ✅ fixed (F83) | F83 replaces hardcoded questions with a single free-text textarea + role/industry-aware hint chips. DEFAULT_PULSE_QUESTIONS remains as a legacy fallback in today_pulse for orgs with no resolved profile, but usePulseToday() is defined in usePulses.ts and never imported by any component — PulsePage reads usePulseProfile() for hint chips and usePulsesList() for history. The .questions field emitted by today_pulse is not user-facing. See F83 spec. |
| 12 | Timezone mismatch: today_pulse uses server-local date, no per-user timezone adjustment | ⚠️ gap | views.py:132 today = date.today(); user ahead of server timezone sees yesterday’s data at their local midnight |
| 13 | Visual QC: screenshot flow captures the actual /intel/pulse page, not Today fallback content | ✅ match | 2026-06-18 screenshot flow navigates directly to /intel/pulse; 05-daily-pulse/01-pulse-page.png shows the F07 form/history page. Vietnamese title localized in vi/pulse.json. |
Summary: 11/13 claims verified; 2 gaps flagged (date validation, timezone). The two previously-recorded spec drifts are now closed in the contract text: same-day resubmission overwrite behavior is documented in §7, and today_pulse’s 200 not_submitted empty state is documented in §5 and §9.
12. Open Questions
- Per-org custom questions: addressed in F83 — Daily Pulse v2, which introduces role-aware hint chips, industry-aware extraction, and
OrgPulseConfigurationfor company-level overrides.
13. Bug / Change Log
2026-07-13 — Contract Graph migration and pulse writer boundary
Type: Contract and authorization traceability
Summary:
- Added the canonical F07 CUE contract for today, submit, list, retrieve, and process, with 10 executable proof records covering retention on extraction failure, evidence, tenant isolation, and role boundaries.
- Contract proof collection found that
submit_pulseandPulseViewSet.processapplied onlyIsOrgMember, allowing viewers to submit and process pulses despite the member-write/viewer-read contract. AddedIsPulseWriterto both paths and regression proofs. - Evidence-source CRUD, adherence, F83 configuration, follow-up, and F103 rollup routes remain explicitly outside the core F07 contract.
Impact: Viewer pulse access is now read-only; active non-viewer members retain submit/process access.
Tests: The Contract Graph verifier collects and passes all 10 F07 pytest proof records. F07 remains
partial because client-date validation, timezone semantics, and browser/native runtime gaps are
explicit in contracts/features/F07.cue.
2026-07-10 — PulseReceipt fingerprint + 409 enforcement + concurrent race test + dead-letter UI + clearScope hooks (issue #1647 r03)
Type: Feature + Bug Fix
Summary:
- PulseReceipt now stores request_fingerprint (SHA-256 of {date, answers}) and links to PulseEntry. Same-key/different-payload returns 409 Conflict. Added concurrent race test (PostgreSQL) and interrupted-first-attempt test. Updated backend DIAGRAMS.md.
- KMP: wired draftStore + clearScope on Android/iOS. Added DeadLetterEntry UI with en/vi retry/discard strings. Added clearPulseStore() to Android SharedKmpViewModel and iOS APIClient.logout()/switchOrganization().
2026-07-02 — Correct stale test-file path reference (batch-1 spec audit)
Type: Documentation Fix
Summary:
- §7 row 6: annotated
frontend/src/test/usePulseAdherenceShare.test.tsxas absent — the hook test file was described in the 2026-06-22 change log entry but was never shipped to the repo (find frontend/src -name usePulseAdherenceShare.test.tsxreturns nothing). The builder test atfrontend/src/test/pulseAdherenceShare.test.ts(row 5) does exist. The 2026-06-22 change log entry is preserved as-is (historical record); the live §7 row now reflects the actual test surface.
2026-07-02 — Close stale pulse spec-drift notes
Type: Documentation Change
Summary:
- Updated §7 row 1 to match shipped same-day resubmission behavior: repeat submit overwrites the same-day entry and returns 200.
- Closed §11 audit rows 8 and 9 from
⚠️ spec driftto✅ matchbecause the contract text now matches shipped behavior. - Tightened the §11 summary so it reports only the two real remaining gaps: date validation and timezone handling.
Reason:
- The spec had already been partially corrected in §5 and §9, but §7 and the §11 summary still recorded old contradictions as live drift.
Impact:
- Documentation only. No code, API, or test behavior changed.
2026-07-02 — Fix stale evidence citations and i18n gap clarification (batch-2 spec audit)
Type: Documentation Fix
Summary:
- §11 row 5: corrected stale evidence reference — there is no
_compute_confidencefunction inapps/ai/services.py; confidence is assigned inline at ~922 (commitment), ~965 (risk), ~1008 (decision) within the keyword extraction loops. - §11 row 11: expanded the F83 “fixed” note to clarify that
today_pulsestill emitsDEFAULT_PULSE_QUESTIONS(Vietnamese) as a legacy field, but this is NOT a real i18n bug for international users —usePulseToday()is exported fromusePulses.tsbut never imported by any component;PulsePagereadsusePulseProfile()for hint chips andusePulsesList()for history. The.questionsfield from thetoday_pulseresponse is fully non-user-facing. - §11 row 12: fixed stale code reference —
views.py:132callsdate.today(), nottimezone.now().date()at line 23; the per-user-timezone gap remains real. - §7 row 6: expanded the “file absent” note to name the partial coverage source (
TeamCheckInsCard.test.tsx) and the builder test (pulseAdherenceShare.test.ts).
2026-06-22 — Team check-ins share affordance (third distribution object)
Type: Feature (growth — mirrored from F12 Brief share)
Summary:
- Added a leader-only Share button to the
TeamCheckInsCardon/intel/pulse. Tapping it turns the day’s adherence into a chat-pasteable artifact ({org} · Lumi Pulse · {date}header, ✅ submitted/total line, plural-aware missing-names list, F59 attributed referral footer) and routes through nativenavigator.sharewith a clipboard fallback. The hook is gated by the same leader check that already gates the card, so non-leaders never see the affordance. - Files:
frontend/src/lib/pulseAdherenceShare.ts(pure builder),frontend/src/hooks/usePulseAdherenceShare.ts(orchestration hook — mirrorsuseBriefShare),frontend/src/components/pulse/TeamCheckInsCard.tsx(button + wiring), plus i18n keys inpulsenamespace propagated across all 62 locales (en + vi hand-translated; others fall back to English viascripts/propagate-new-i18n-keys.mjsuntil the i18n-translate fleet runs).
Reason:
- Standing #1 next candidate in
docs/improvements/2026-06/week-04/f13-today-share-brief-affordance.md,f12-brief-share-org-name-header.md, andf12-brief-item-share-targeted-distribution-object.md. Brief and Today already carry the one-tap share; Pulse was the open hole in the trifecta. Pulse adherence is the artifact a leader naturally screenshots and forwards to a co-founder at 09:30 (“đội mình hôm nay còn 3 người chưa báo cáo”); turning that into one tap + F59 attribution closes the leakage.
Tests:
frontend/src/test/pulseAdherenceShare.test.ts(10 cases — builder shape, org-stamp present/absent/whitespace, all-clear, missing-names, plural singular vs plural, referral vs branded footer).frontend/src/test/usePulseAdherenceShare.test.tsx(5 cases — native success, AbortError, clipboard fallback, clipboard error, builder receives referral + org name).frontend/src/test/TeamCheckInsCard.test.tsx(extended — Share button presence/absence + invokes hook on click).
Impact: Additive UI on a leader-gated card. No model/API change. No new endpoint. No deploy migration. The leader already sees the data on screen; the share button is purely an editorial-discretion forward — same pattern as Brief share.
2026-07-10 — Pulse idempotency key + platform-persistent offline queue (issue #1647)
Type: Feature + Bug Fix
Summary:
- Added
idempotency_key(UUIDField, nullable) toPulseEntrymodel. On submit, the backend checks for an existing pulse with the same (org, user, key) — if found, returns the existing response without re-extraction. This prevents duplicate downstream Commitment/Risk/DecisionRequest items when the client retries the same logical submission (network timeout → reconnect → same key). - Added
idempotency_keyfield toPulseSubmitSerializer(optional, nullable — backward compatible with web forms). - Updated KMP
PulseSubmitRequestDTO withidempotencyKey: String?. - Updated
PulseScreenModel.submit()to generate a UUID idempotency key per submission and persist it in the offline queue. - Updated
PulseScreenModel.flushQueue()to pass the stored idempotency key on retry. - Wired persistent
JsonPulseSubmitQueueandJsonPulseProfileCacheon Android (viaLumiPrefs.getString/putString/removeString→EncryptedSharedPreferenceswrapper) and iOS (viaNSUserDefaults-backedStringStoreadapter). These replace the previous in-memory defaults, so offline queued pulses and the resolved profile cache survive process death on both platforms.
Impact:
- Backend: new migration (0009), 4 new tests, coverage ≥97%.
- Shared KMP: 3 new tests in PulseScreenModelTest (34→37), all pass.
- Android: persistent offline queue wires
JsonPulseSubmitQueue(prefsStringStore)+JsonPulseProfileCache(prefsStringStore). - iOS: persistent offline queue wires
JsonPulseSubmitQueue(iosStore)+JsonPulseProfileCache(iosStore)viaNSUserDefaults. - Doc updates: F07 spec §5/§7/§13, backend/DIAGRAMS.md §5 (idempotency guard).
- Never claims physical packet exactly-once — network delivery can retry; only the tenant-scoped logical submission is exactly-once.
Summary:
- The screenshot gallery’s Daily Pulse section captured the Today dashboard instead of the F07 pulse page.
- This made the visual artifact misleading: it did not verify the six-question form, submitted read-back, or pulse history.
- Fixed the screenshot flow to navigate directly to
/intel/pulseand capture the form/read-back state underfrontend/screenshots/05-daily-pulse/. - Localized the Vietnamese page title from
Daily PulsetoBáo cáo hằng ngày, so the page chrome matches the forced-Vietnamese screenshot flow. - Localized the Vietnamese sidebar label for the same route, removing the mixed-language first viewport.
- Aligned the frontend
pulsequestion labels and sample answers withDEFAULT_PULSE_QUESTIONSso q3/q4/q5/q6 feed the expected commitment, decision, risk, and tomorrow-priority slots.
Verification:
frontend/screenshots/05-daily-pulse/01-pulse-page.pngshows the actual Pulse UI.pnpm exec playwright test tests/e2e/screenshot-flow.spec.ts --project=chromium --reporter=list -g "05-daily-pulse"
2026-06-15 — Frontend route namespaced
Frontend route namespaced: /pulse -> /intel/pulse (app-wide IA restructure; API paths unchanged).
2026-06-14 — Strategy v2 realignment (positioning)
Type: Documentation Change
Summary:
- Reframed the §2 “Competitor job” lines (×3) from Asana/Monday/Notion check-ins to what the pulse actually replaces in the first market: scattered Zalo/WhatsApp messages, an Excel “daily report” tab, and owner memory (with trade voice: “anh ơi xong chưa?”, thi công). Acceptance-criteria
[x]lines left untouched. - §3: rewrote “Competitive note — Asana/Monday/Notion check-ins” → “Competitive note — the Excel ‘daily report’ tab and owner memory” (the chat side is already covered by the existing Zalo note); tightened the Zalo note to name the real competitor explicitly (Zalo/WhatsApp + Excel + owner memory).
- Added a “Relation to the F70 wedge” note: the pulse is the steady-state capture habit that the F70 cold-start wedge (paste→brief) graduates into once staff are onboarded (Loop 1).
Reason:
- Realign to competitive-strategy-v2.md (Inversion 7 — real competitor is Zalo/WhatsApp + Excel + owner memory; §7.2 staff updates/pulse as low-friction capture; §10.4 Loop 1) and the registry “Positioning doctrine (v2)” rules 1, 4, and 5. F07 is the natural home to cross-link the F70 wedge (which the registry §1 names as the cold-start the steady-state pulse depends on).
Impact: Positioning only. No behavior change — the six fixed questions, submit/extraction pipeline, EvidenceSource creation, and per-day uniqueness are all unchanged.
Tests: None (documentation change).
Notes: No edits to §4 Data Model, §5 API Contract, §7 Test Plan, §9 Error States, §11 Implementation Audit, the six-question contract, or the Status line.
2026-06-14 — Restructure to TEMPLATE.md section order; expand §11 audit (SP fleet)
Type: Documentation Change
Summary:
- Numbered “Current Behavior” as §8; §8 Diagrams → §10; §10 Edge Cases absorbed into §11; §11 Error States → §9 (table); §9 Implementation Audit → §11 (12-row table); §12 Open Questions stays §12; §13 Bug/Change Log stays §13.
- §9 Error States converted from narrative to table with code references.
- §11 audit: 12 rows, 9 ✅, 3 gaps + 2 benign spec drifts documented.
- §5 API contract:
today_pulsenote corrected to “200not_submittedif none — not 404”.
2026-06-14 — Document pulse question localisation gap (international markets)
Type: Documentation Change
Summary:
- Added to §9 Implementation Audit:
DEFAULT_PULSE_QUESTIONSare hardcoded Vietnamese strings. Users in international target markets (MY, ID, KSA, PH, LV) see Vietnamese questions. Changing question text changes the AI extraction contract — requires dedicated rework spec before implementation. Cross-referenced to F69 §10 Open Question 3.
Impact: Spec-only. No code changes. Surfaces a known gap before international market launch.
2026-06-10 — Submit auto-triggers extraction; /process/ becomes retry-only
Type: Behavior Change
Summary:
POST /api/pulses/submit/now callsextract_pulse_entry(pulse.id, evidence.id)synchronously after creating theEvidenceSource. Success →status="processed"; exception →status="failed"(logged vialogger.exception), submission still returns 201.POST /api/pulses/<id>/process/is now idempotent: re-processing an already-processedpulse returns 200 with no duplicate items; processing afailedpulse retries extraction.PulsePage.tsxshows a retry button (pulsesApi.process(id)) whenstatus="failed".
Reason:
- A two-step submit-then-process flow forced employees/CEOs to orchestrate the AI pipeline manually — the single most important UX fix in the pipeline per §9.
Impact:
- Extraction now happens transparently on submit.
failedpulses are recoverable via retry without re-typing answers.
2026-06-12 — Add Edge Cases and Error States sections
Type: Documentation Change
Summary:
- Added §10 Edge Cases (E1–E11) and §11 Error States (ES1–ES8). Renumbered Open Questions → §12, Bug/Change Log → §13.
2026-06-12 — Success metrics + competitive positioning (G-r18)
Type: Documentation Change
Summary:
- Added §1b Success Metrics with four tracked signals (completion rate, time-to-complete, extraction yield, weekly stickiness).
- Added competitive framing to §3 — names WhatsApp/Zalo groups as the concretely worse alternative.
2026-06-12 — JTBD stories + extraction precision + Asana/Monday/Notion differentiation (G-r23)
Type: Documentation Change
Summary:
- Added extraction precision metric to §1b.
- Rewrote §2 User Stories as CEO/member jobs-to-be-done with named competitor comparison per story.
- Expanded §3 competitive framing to name Asana/Monday/Notion check-in approaches directly.
14. Success Metrics (Appendix)
- Pulse completion rate > 70% within 2 hours of daily prompt: measured as the share of active members who submit a pulse on a given day within 2 hours of the daily prompt (Telegram or channel notification). Tied to §1’s claim that “if completing it takes more than ~2 minutes or feels like surveillance, people stop” — completion rate is the direct signal of whether the pulse habit is sustainable.
- Median time-to-complete ≤ 120s (P95 ≤ 180s): measured client-side from first field interaction to submit response. Tied to §1’s “~2 minutes” benchmark and §2 story 1 (“up to 6 plain questions, all optional”) — if a warehouse staffer takes longer than 3 minutes, the form is too heavy.
- Signal extraction yield ≥ 0.4 items per submitted pulse: tracked as extracted Commitment + Risk + DecisionRequest objects created within 5 minutes of a pulse’s submission, divided by total pulse submissions in the same period. Tied to §2 story 1 (“Lumi extracts commitments, risks, and decisions from six plain questions”) — low yield means pulses are being written but the Brief is not being fed. Computable from existing models (PulseEntry count vs. downstream item counts); no new instrumentation.
- Extraction precision > 75%: measured as the share of extracted items (Commitments + Risks + DecisionRequests) that remain open, monitoring, or resolved (not cancelled, not dismissed) 7 days after creation. This is the quality counterpart to extraction yield (which measures quantity). Tied to §2 story 1’s competitor distinction (“Asana/Monday produce raw text the CEO must mentally filter; Lumi extracts structured signals”) — if precision drops below 75%, the CEO loses trust and reverts to reading raw pulse text, defeating the extraction wedge. Computable from existing model status changes and
created_at; no new instrumentation. - Weekly habit stickiness > 60%: measured as the share of active members who submit a pulse on ≥ 4 of their org’s working days per week. Tied to §2 story 3 (“who pulsed today and who never does”) — if fewer than 60% of members sustain the pulse habit, the Brief permanently starves of new signals.