Spec: F85 — Pilot Instrumentation & Activation Gates
Status: in progress (shipped 2026-06-24)
Contract Graph boundary (2026-07-14): F85.cue owns the
append-only activation ledger, gate/cohort computation, and three staff-only pilot API endpoints.
The contract proves deterministic fixture behavior and authorization only; the /hq/pilot browser
surface and real Vietnam pilot outcomes remain explicitly unproven.
This is the measurement contract behind the pitch. The investor deck’s EVIDENCE and THE ASK slides do not promise features — they promise behaviour: update completion ≥ 60%, retention 3/5 firms on 6+ days, action rate ≥ 30%, expansion ≥ 2 firms inviting a second employee, ≥ 1 paying firm, and zero invented payments/deadlines/owners. Those numbers are the gates that decide whether Lumi scales — and today none of them are measurable. There is no analytics, metrics, or activation-event model anywhere in the backend (
apps/analytics/apps/metricsdo not exist); F52 references a PostHog activation event but ships no backend instrumentation. This spec defines the instrumentation spine — one append-onlyActivationEventledger plus the queries that turn it into the pilot’s pass/fail gates — so the EVIDENCE and ASK slides become executable rather than aspirational.Aligns with field-tests.md — the authority on the gate definitions (G2–G6, §4 Gate system; §12 live-test pass bar; §16 payment ladder). Gate IDs and thresholds in this spec are quoted from that file and the pitch; F85 does not invent new ones.
1. Problem & CEO Job
Trust the signal. Of the three CEO jobs (see clearly / decide fast / trust the signal), this serves the third — but for the founder as the operator of the pilot, not the customer CEO. The pitch makes hard, behaviour-based promises to investors (“gates, not guesses”). A promise you cannot measure is a guess. Right now the team would be forced to eyeball update completion, guess at retention cohorts, and have no attributable definition of “a brief caused an action” — exactly the soft, opinion-based validation field-tests.md §0 bans (“behaviour, not opinion”). Without an instrumentation spine, every gate in the EVIDENCE and ASK slides is unfalsifiable: the team cannot honestly tell an investor whether the pilot passed, and cannot tell itself whether to scale, change the wedge, or stop. This feature is the measurement contract that makes the pitch’s evidence real and the scale decision honest.
It is not a new analytics platform. It is a thin, append-only event ledger — modelled exactly
on the existing NotificationEvent and
F71 ConfirmationEvent ledgers — plus the queries that compute the
gates and a read-only founder dashboard. The product’s existing spine (Pulse, Brief, BriefItem,
EvidenceSource, ConfirmationRequest, Subscription) already holds the underlying behaviour; F85 only
observes it.
2. User Stories & Acceptance Criteria
Founder / staff (pilot operator)
-
As a founder, I want each gate’s current value vs its threshold with a clear PASS/FAIL, so that I can tell investors (and myself) whether the pilot is clearing the bar.
- Given a pilot cohort, when I open
/hq/pilot, then I see every gate (UPDATE COMPLETION, RETENTION, ACTION RATE, EXPANSION, PAYMENT, ACCURACY) with its measured value, its threshold, and a PASS/FAIL badge computed from real events — never a hardcoded or manually-entered number. - Given I am not staff (
is_staff=False), when I request the pilot dashboard endpoint or route, then I am rejected (403/ redirect to/work/today) — this surface is never visible to customer users.
- Given a pilot cohort, when I open
-
As a founder, I want a per-firm cohort table, so that I can read the HVAC/MEP vs fit-out SEGMENT SIGNAL the pitch’s ASK slide requires.
- Given the cohort, when the dashboard renders, then each firm shows its active-days count,
completion %, action rate, second-member-invited flag, and payment flag, with a segment
tag (
hvac_mep/fit_out), so the primary-wedge verdict is readable from the data.
- Given the cohort, when the dashboard renders, then each firm shows its active-days count,
completion %, action rate, second-member-invited flag, and payment flag, with a segment
tag (
-
As a founder, I want the ACTION RATE gate to count only attributable, observable actions, so that the 30% number is honest and not inflated by unrelated activity.
- Given a brief item, when within the action window (default 24h) it is assigned, edited,
marked done, shared via an F71 confirmation link, or converted
to a task, then exactly one
brief_item_actedevent is recorded, attributed to that brief item (subject_type=brief_item,subject_id=<id>) — and a second qualifying action on the same item does not double-count it toward the rate.
- Given a brief item, when within the action window (default 24h) it is assigned, edited,
marked done, shared via an F71 confirmation link, or converted
to a task, then exactly one
-
As a founder, I want the ACCURACY gate measured, not asserted, so that “zero invented payments/deadlines/owners” is backed by evidence.
- Given a sample of extracted items, when an item’s claimed payment / deadline / owner is not
supported by its linked
EvidenceSourcespan (a sampled audit), or when an owner flags an item inaccurate via F15, then anextraction_flagged_inaccurateevent is recorded and the ACCURACY gate fails until the count returns to zero for the window.
- Given a sample of extracted items, when an item’s claimed payment / deadline / owner is not
supported by its linked
Server-side emitters (existing flows)
- As a backend service (pulse submit, brief view, assign, confirmation, billing), I want a
single
emit()helper, so that instrumentation is one line inside the flow I already run and cannot drift from the behaviour it measures.- Given a pulse is submitted, when the submit transaction commits, then a
pulse_submittedevent is recorded for that member/org withoccurred_atset to the submit time. - Given any emitter is called, when the org does not exist or the actor is cross-tenant, then no event is written and the calling flow is unaffected (instrumentation never breaks a product flow).
- Given a pulse is submitted, when the submit transaction commits, then a
Every acceptance criterion above maps to at least one test in §7.
3. Scope Boundaries
- In:
- One append-only
ActivationEventledger model (tenant-scoped, mirrorsNotificationEvent/ConfirmationEvent). - A server-side
emit(event_type, organization, member=None, subject=None, metadata=None)helper in a newapps/metricsapp, called from inside existing flows (pulse submit, brief view, brief item action, confirmation send, second-member invite, payment, F15 inaccuracy flag). - Gate computation: pure-query (or a cheap periodic rollup) functions that turn the ledger into the seven pitch/field-test gate values, scoped to a pilot cohort.
- A read-only founder/staff pilot dashboard endpoint +
/hq/pilotsurface (gated behind F43 Admin HQis_staff). - The precise operational definition of “observable action” (see §4.4) and of each gate.
- One append-only
- Out:
- Enforcement of accuracy (“zero invented payments/deadlines/owners” as a guarantee in the
extraction pipeline) — that is F86 Extraction Faithfulness
(to be written). F85 measures accuracy; F86 enforces it. The
extraction_flagged_inaccurateevent is the shared boundary. - A data warehouse, OLAP store, event partitioning/sharding, or a streaming pipeline — explicitly not built for load we do not have (see §11 constraints and ENGINEERING.md → Scale-Ready Design).
- Customer-facing analytics dashboards — this surface is founder/staff only.
- The activation flow and demo-data scaffolding — that is F52; F85 only defines and records the activation event F52 references but never instrumented.
- The confirmation-link Loop-2 mechanism — that is F71; F85 reads its
ConfirmationEvents as one input to ACTION RATE / EXPANSION, it does not duplicate them. - A second analytics platform. If PostHog is later used, it is an optional downstream mirror, never the source of truth (see §12).
- Enforcement of accuracy (“zero invented payments/deadlines/owners” as a guarantee in the
extraction pipeline) — that is F86 Extraction Faithfulness
(to be written). F85 measures accuracy; F86 enforces it. The
4. Data Model
Reuses the existing event-ledger pattern; adds one model and (optionally) one cohort tag. Nothing here duplicates a model that exists.
4.1 New — ActivationEvent (the spine)
apps/metrics/models.py, extends UUIDModel, mirrors NotificationEvent/ConfirmationEvent:
class ActivationEvent(UUIDModel):
"""Append-only pilot-instrumentation ledger (F85). Never updated, never deleted in flows."""
EVENT_TYPES = [
("pulse_submitted", "Pulse submitted"), # → UPDATE COMPLETION, RETENTION
("brief_viewed", "Brief viewed"), # → activation (F52), RETENTION
("brief_item_acted", "Brief item acted on"), # → ACTION RATE (attributable)
("item_assigned", "Item assigned"), # → ACTION RATE input
("confirmation_sent", "Confirmation link sent"), # → ACTION RATE / Loop 2
("second_member_invited", "Second member invited"),# → EXPANSION
("payment_made", "Non-zero payment made"), # → PAYMENT
("extraction_flagged_inaccurate", "Extraction flagged inaccurate"), # → ACCURACY (fail signal)
]
organization = models.ForeignKey(
"organizations.Organization", on_delete=models.CASCADE,
related_name="activation_events",
)
member = models.ForeignKey(
"organizations.OrganizationMember", on_delete=models.SET_NULL,
null=True, blank=True, related_name="activation_events",
)
event_type = models.CharField(max_length=40, choices=EVENT_TYPES, db_index=True)
# Soft reference to the subject (Brief, BriefItem, Commitment, ConfirmationRequest, Subscription…)
# — a soft type+id, NOT a hard FK into a future-huge table (scale rule).
subject_type = models.CharField(max_length=40, blank=True)
subject_id = models.UUIDField(null=True, blank=True)
metadata = models.JSONField(default=dict, blank=True) # no PII beyond what the spine already holds
occurred_at = models.DateTimeField(db_index=True) # when the behaviour happened, not write time
class Meta:
db_table = "activation_events"
ordering = ["-occurred_at"]
indexes = [
models.Index(fields=["organization", "event_type", "occurred_at"]),
models.Index(fields=["organization", "member", "occurred_at"]),
]
constraints = [
# Idempotency / no double-count for attributable actions (e.g. brief_item_acted once
# per (subject, day)). dedupe_key is computed by emit(); see §5.
]
- Append-only. Flows only ever
create(); no flow updates or deletes a row (mirrorsNotificationEvent). The record is the evidence. - Soft subject reference, not a hard FK or M2M into the high-volume
BriefItem/Commitmenttables — per the scale-ready design rule: no FK into a future-huge table, no unboundedrelated_namereverse cascades from those tables. - No new PII.
metadatacarries only ids, the soft subject reference, and coarse flags (e.g.{"action": "assigned"},{"segment": "hvac_mep"}). No message bodies, no customer names, no payment amounts beyond a non-zero boolean — the gate is ”≥ 1 firm pays any non-zero amount”, so a booleanpaid=truesuffices.
4.2 Cohort tagging (reuse, not a new model)
A pilot cohort is identified by the existing Organization and its segment. Add a single field
rather than a new model (Simplicity, per F52 §4.4):
# Organization — new field
pilot_segment = models.CharField(max_length=20, blank=True, choices=[
("hvac_mep", "HVAC / MEP maintenance"),
("fit_out", "Interior fit-out"),
])
The pilot cohort = orgs with a non-blank pilot_segment. The SEGMENT SIGNAL gate splits gate
values by this field. (If the wedge’s provisional-org work from F70 §4
later lands, the tag is set at provisioning.)
4.3 Aggregates — query first, rollup only if needed
Gate values are computed by pure aggregate queries over ActivationEvent for the pilot cohort
(small N: 5 firms × 10–14 days). No materialized table is built now. The query module
(apps/metrics/gates.py) exposes one function per gate, each returning {value, threshold, passed}:
update_completion(cohort)— % of (member × working-day) cells with ≥ 1pulse_submitted.retention(cohort)— count of firms withpulse_submittedorbrief_viewedon ≥ 6 distinct days within the two-week window.action_rate(cohort)— distinctbrief_item_actedsubjects ÷ distinct briefed items in window.expansion(cohort)— count of firms with ≥ 1second_member_invited.payment(cohort)— count of firms with ≥ 1payment_made.accuracy(cohort)— count ofextraction_flagged_inaccuratein window (gate passes iff0).
If the cohort ever grows past hand-query scale, the upgrade is mechanical: a nightly Celery rollup
into a PilotGateSnapshot table keyed by (cohort, gate, date) — not built now (no load).
4.4 Operational definition of “observable action” (ACTION RATE)
A brief drives an observable action when, within the action window (default 24h) of the brief item being delivered, the item is the subject of at least one of:
- assigned to a member (F25 assignment) →
item_assignedthen abrief_item_acted{action:"assigned"}; - edited / corrected (owner changes assignee, date, or text — also feeds F15);
- marked done / resolved (status transition to a terminal state);
- shared via a confirmation link (F71
ConfirmationRequestcreated for that subject) →confirmation_sentthenbrief_item_acted{action:"confirmation"}; - converted to a task (F05).
Each is attributable (the event carries subject_type=brief_item, subject_id) and
deduplicated (one item counts once toward the rate regardless of how many qualifying actions it
receives). This matches field-tests.md §11 (“owner corrected details
rather than ignoring it”, “owner agrees to send external confirmation link”) and the pitch’s
”≥ 30% of briefs cause an observable action”.
5. API Contract
Emission was server-side only through 2026-07-09 — every emit() call sat inside an existing
authenticated flow’s own view/task, never a dedicated client-write endpoint. 2026-07-10 (W-SYS-11
follow-up) added exactly one client-write path — a pure telemetry ingestion endpoint for a signal
with no corresponding backend action to piggyback on (rage-click detection is purely client-side).
Reads remain founder/staff only, under the existing F43 /api/hq/ namespace; the new write is
any-authenticated-org-member, under /api/metrics/ (deliberately not /api/hq/ — it is not
staff-only).
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| GET | /api/hq/pilot/gates/ | is_staff (F43 IsStaff) | ?segment=hvac_mep|fit_out (optional) | 200 {window, gates:[{id, label, value, threshold, passed}], overall_pass} | 403 non-staff |
| GET | /api/hq/pilot/cohort/ | is_staff | ?segment= (optional) | 200 {firms:[{org_id, name, segment, active_days, completion_pct, action_rate, second_member_invited, paid}]} | 403 non-staff |
| POST | /api/metrics/events/rage-click/ | any authenticated org member (IsOrgMember) | {route, element_selector?, click_count, window_ms} | 204 (always — telemetry never surfaces an error to the client) | 401/403 unauthenticated or no valid X-Organization-ID |
| POST | /api/metrics/events/screen-viewed/ | any authenticated org member (IsOrgMember) | {route, occurred_at} — canonical pathname + client timestamp | 204 (always — telemetry never surfaces an error to the client) | 401/403 unauthenticated or no valid X-Organization-ID |
- Emission helper (internal, not an endpoint):
metrics.emit(event_type, *, organization, member=None, subject=None, metadata=None, occurred_at=None, dedupe_key=None)inapps/metrics/services.py. Called from inside existing flows after their own transaction commits (so an emit failure can never roll back the product action — emit is best-effort and swallows its own errors with aWARNINGlog). It computes adedupe_keyfor attributable events (e.g.f"{event_type}:{subject_id}:{occurred_at:%Y-%m-%d}") and usesget_or_create(dedupe_key=...)so re-running a flow never double-counts. - Tenant scoping (Rule 1).
emit()requires anorganizationand never accepts a cross-tenantmember/subject(it assertsmember.organization_id == organization.id). The read endpoints are staff-only and cross-org by design (the founder must see all pilot firms), but they only ever read the cohort defined bypilot_segmentand never leak one firm’s events into another firm’s row — every aggregate is grouped byorganization_id(the mandatory isolation test, §7 #3). No customer-tier endpoint can read another org’s events. - AI output: F85 produces no AI output; it measures AI output. It links to existing
EvidenceSourcespans only for the ACCURACY audit (read-only). No confidence score is produced.
5.1 Emitters wired into existing flows (one line each)
| Flow (existing) | Emits |
|---|---|
| Pulse submit (F07) | pulse_submitted |
| First brief view (F12/F52 activation) | brief_viewed |
| Brief item assigned (F25) | item_assigned + brief_item_acted |
| Brief item edited/done/converted (F12/F05) | brief_item_acted |
| Confirmation link created (F71) | confirmation_sent + brief_item_acted |
| Second member invited (F02) | second_member_invited |
| Non-zero payment (F24/billing) | payment_made |
| Item flagged inaccurate (F15) or accuracy audit | extraction_flagged_inaccurate |
Rage-click burst detected client-side (W-SYS-11, frontend/src/lib/rageClickTelemetry.ts) → POST /api/metrics/events/rage-click/ | ui_rage_click_detected |
Every authenticated TanStack Router pathname (frontend/src/lib/screenViewTelemetry.ts) → POST /api/metrics/events/screen-viewed/ | screen_viewed |
The tenant ledger write remains immediate and best-effort. Its mirrored PostHog event is routine startup telemetry: it queues in memory until the post-load telemetry window instead of downloading the analytics SDK during route paint. Explicit user interactions may still initialize analytics eagerly so the interaction itself is not dropped.
6. UI / UX
- Route:
/hq/pilot— a new tab inside the F43 Admin HQ shell (staff-only; non-staff are redirected to/work/today, identical to every other/hq/*route). - Page component:
PilotDashboardPage→GateCard(one per gate) +CohortTable(per-firm). - Gate cards: each shows the gate label, the measured value, the threshold, and a PASS/FAIL
Badge. PASS usestext-[var(--color-success)]/ soft success token; FAIL uses the destructive token — never rawgreen-*/red-*(Rule 4). A header strip shows overall PASS/FAIL and the window dates. - Cohort table: the shared
<DataTable>(F66) — columns: firm, segment, active days, completion %, action rate, second member invited, paid. Segment column makes the HVAC/MEP-vs-fit-out SEGMENT SIGNAL readable at a glance. - States to design:
- empty — no pilot orgs yet: “No pilot cohort yet. Tag an organization’s segment to begin.”
- loading — honest
Skeletoncards, not a fake spinner. - error —
metrics.error_loadwith a retry. - populated — gates + cohort table.
- no-permission — handled by the F43 route guard (redirect), never a leaky 200.
- shadcn components:
Card,Badge,Skeleton, the sharedDataTable(F66). Checkfrontend/src/components/ui/first; no raw Tailwind, only Lumi semantic tokens (Rule 4). - i18n namespace:
metrics— keys in bothen/andvi/(Rule 3):metrics.pilot_title,metrics.window_label,metrics.overall_pass,metrics.overall_failmetrics.gate_update_completion,metrics.gate_retention,metrics.gate_action_rate,metrics.gate_expansion,metrics.gate_payment,metrics.gate_accuracymetrics.threshold_label,metrics.value_label,metrics.pass,metrics.failmetrics.col_firm,metrics.col_segment,metrics.col_active_days,metrics.col_completion,metrics.col_action_rate,metrics.col_second_member,metrics.col_paidmetrics.segment_hvac_mep,metrics.segment_fit_outmetrics.empty_state,metrics.error_load
- Server state: TanStack Query (Rule per frontend/AGENTS.md), never
useEffect+fetch. - DESIGN.md + TASTE.md pre-flight applies; the dashboard is calm and scannable, not a wall of numbers.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | emit('pulse_submitted', org, member) writes one row with correct occurred_at | backend | backend/tests/test_metrics.py::TestEmit |
| 2 | emit is idempotent — same dedupe_key twice writes one row (no double-count) | backend | backend/tests/test_metrics.py::TestEmit |
| 3 | Cross-tenant isolation — org A’s gate values never include org B’s events (mandatory) | backend | backend/tests/test_multitenancy.py::TestActivationEventIsolation |
| 4 | emit rejects a cross-tenant member/subject (asserts org match), product flow unaffected | backend | backend/tests/test_metrics.py::TestEmit |
| 5 | update_completion = % of (member×working-day) cells with ≥1 pulse — correct on fixture | backend | backend/tests/test_metrics.py::TestGates |
| 6 | action_rate counts a brief item once even with multiple qualifying actions | backend | backend/tests/test_metrics.py::TestGates |
| 7 | retention counts a firm only with ≥6 distinct active days in window | backend | backend/tests/test_metrics.py::TestGates |
| 8 | accuracy fails on any extraction_flagged_inaccurate, passes at zero | backend | backend/tests/test_metrics.py::TestGates |
| 9 | expansion/payment gates count distinct firms, not events | backend | backend/tests/test_metrics.py::TestGates |
| 10 | GET /api/hq/pilot/gates/ returns all gates with value/threshold/passed for staff | backend | backend/tests/test_metrics.py::TestPilotEndpoint |
| 11 | Non-staff user gets 403 from /api/hq/pilot/gates/ and /cohort/ | backend | backend/tests/test_metrics.py::TestPilotEndpoint |
| 12 | Cohort endpoint groups by org; segment filter splits HVAC/MEP vs fit-out | backend | backend/tests/test_metrics.py::TestPilotEndpoint |
| 13 | Pulse submit flow emits pulse_submitted end-to-end | backend | backend/tests/test_metrics.py::TestEmitters |
| 14 | Brief-item assign / confirmation-send flow emits brief_item_acted attributed to the item | backend | backend/tests/test_metrics.py::TestEmitters |
| 15 | E2E: staff sees /hq/pilot gates + cohort; non-staff is redirected | e2e | frontend/tests/e2e/lumi.spec.ts describe("Pilot dashboard (F85)") |
Backend coverage must stay ≥99% (per CLAUDE.md verify gate); new E2E describe block required. The
cross-tenant isolation test (#3) is a hard gate — a metrics row that bleeds across orgs is the same
class of defect as a data leak.
8. Current Behavior
No analytics, metrics, or activation-event model exists today. apps/analytics and apps/metrics
do not exist (verified). The only analytics in the backend is the unrelated CRM deal-funnel
analytics action in apps/crm/views.py. F52 §2 references a
PostHog activation event (activation_completed) and an Admin HQ activation metric, but its §11
audit marks PostHog events as a ⚠️ gap (“Not mentioned in any BL entry”) — there is no backend
instrumentation, no event ledger, and therefore no way to compute any of the pitch’s gates. The
underlying behaviours are recorded by the spine (Pulse, Brief, BriefItem, ConfirmationEvent,
Subscription), but nothing observes them as activation events or rolls them into gate values.
9. Error States
| # | Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|---|
| 1 | emit() raises (bad subject, DB hiccup) | None — product flow unaffected | Swallowed; event simply not written | WARNING "activation_emit_failed" with event_type | None (best-effort; next event of that type will write) |
| 2 | emit() given a cross-tenant member/subject | None | Assertion fails; no row written | ERROR "activation_emit_cross_tenant" | n/a (programming error to fix) |
| 3 | Gate query has no events for the cohort | Empty/zero gate values rendered honestly (e.g. completion 0%, FAIL) | n/a | n/a | n/a |
| 4 | Non-staff hits /api/hq/pilot/* | 403 (or route redirect) | Permission denied | INFO "pilot_dashboard_permission_denied" | n/a |
| 5 | Pilot dashboard fetch fails | metrics.error_load toast + retry button | 5xx surfaced | ERROR "pilot_dashboard_load_error" | Manual retry |
| 6 | No org has a pilot_segment | metrics.empty_state | n/a | n/a | n/a |
- Never fabricate a gate value, never round a FAIL up to a PASS, never hardcode a threshold or a measured value. A gate with no supporting events reads as FAIL, honestly.
10. Diagrams & Docs Touched
- backend/DIAGRAMS.md: add
ActivationEventto the ER diagram (alongsideNotificationEvent/ConfirmationEventas a sibling append-only ledger); add the emit→ledger→gate-query request lifecycle. - GLOSSARY.md: add activation gate, observable action, pilot cohort, activation event.
- F52 §2/§11: this spec is the backend instrumentation F52’s PostHog
activation event was a placeholder for; cross-link (F52 emits
brief_viewedvia F85’semit()). - F43: the pilot dashboard is a new
/hq/pilotsurface inside Admin HQ; cross-link. - F86 Extraction Faithfulness (to be written): the enforcement
counterpart to F85’s ACCURACY measurement;
extraction_flagged_inaccurateis the shared event. - ADR: none required if the ledger reuses the established
NotificationEvent/ConfirmationEventpattern. A new ADR is required only if aPilotGateSnapshotrollup or a PostHog mirror is later adopted as more than an optional sink (see §12).
11. Implementation Audit (2026-06-24)
Built (2026-06-24). Backend app apps/metrics ships the append-only ActivationEvent ledger
(activation_events), the best-effort emit() helper (apps/metrics/services.py), the pure-query
gate module (apps/metrics/gates.py), and the staff-only read endpoints
GET /api/hq/pilot/gates/ + GET /api/hq/pilot/cohort/ (apps/metrics/views.py, mounted under the
F43 /api/hq/ namespace behind IsStaff). Organization.pilot_segment tags the cohort
(migration organizations/0012). emit() is wired one line each into pulse submit
(apps/pulses/views.py), brief view + assign + dismiss + F15 feedback (apps/briefs/views.py),
confirmation create (apps/confirmations/views.py), second-member invite
(apps/organizations/views.py), and non-zero payment via transaction.on_commit
(apps/billing/services/__init__.py). Frontend /hq/pilot (PilotDashboardPage.tsx) renders gate
cards + the shared F66 <DataTable> cohort table with PASS/FAIL badges on Lumi success/destructive
tokens, TanStack Query data, and empty/loading/error/populated states; i18n namespace metrics
(en + vi). Tests: tests/test_metrics.py (TestEmit, TestGates, TestPilotEndpoint, TestEmitters) +
tests/test_multitenancy.py::TestActivationEventIsolation (the mandatory cross-tenant gate);
src/test/F85PilotDashboard.test.tsx; E2E describe("Pilot dashboard (F85)") in
tests/e2e/lumi.spec.ts. New backend code is 100%-covered.
Constraints that bound the design (all honoured):
- Must reuse the spine, not build a parallel analytics platform. The product’s behaviours are already recorded (Pulse, Brief, BriefItem, ConfirmationEvent, Subscription). F85 observes them via a thin ledger; it does not re-store business data.
- Must mirror the existing append-only event-ledger pattern —
NotificationEventand F71ConfirmationEvent:UUIDModel,organizationFK, typedevent_type, soft subject reference,metadataJSON, indexed timestamp, create-only. - Must reuse
EvidenceSourcefor the ACCURACY audit (compare extracted claim ↔ evidence span); never invent a parallel provenance store. - No PII beyond what the spine already holds — ids, soft subject refs, coarse flags only; a non-zero payment is a boolean, never an amount in the ledger.
- Scale-ready but not over-engineered (ENGINEERING.md): soft subject ref (no FK into
future-huge
BriefItem/Commitment), id/timestamp indexes for keyset reads, no warehouse, no partitioning, no rollup table — the rollup upgrade (PilotGateSnapshot) is left mechanical and unbuilt because the pilot load (5 firms × 2 weeks) does not warrant it. - Tenant isolation is a hard gate, even though the dashboard is intentionally cross-org for
staff: every aggregate groups by
organization_id, andemit()refuses cross-tenant subjects. - Acceptance is the pilot itself. Like F70/F71, F85 is validation-gated: the spine is “done” when it correctly computes the gates on real pilot data, and the gates’ verdicts — not a green test suite alone — are what the EVIDENCE/ASK slides report.
12. Open Questions / Decisions
- Source of truth: self-hosted
ActivationEventvs PostHog. Recommendation: self-hostedActivationEventis the single source of truth. Reasons: (a) the gates must be computed inside a tenant-scoped, auditable store the team controls — an investor-facing number cannot depend on a third-party SDK that F52’s audit shows was never actually wired; (b) tenant isolation and “no PII” are enforceable in our own model and reviewable in code; (c) the ledger doubles as the audit trail for ACCURACY. PostHog (or similar) may be added later as an optional downstream mirror for funnel exploration only — fed fromActivationEvent, never read as the gate source. Adopting a mirror as anything more than an optional sink requires an ADR. - Working-day definition for UPDATE COMPLETION — calendar working days (Mon–Sat for VN
contractors?) vs each firm’s own active days. Decide with the field team; default to Mon–Sat,
configurable per cohort, recorded in
gates.py. - Action window for ACTION RATE — default 24h from brief-item delivery; tune against the field-test live-test data (field-tests §12).
- ACCURACY audit sampling rate — what fraction of extracted items get a human/automated
evidence-span audit, and how the audit emits
extraction_flagged_inaccurate. Resolve jointly with F86 when that spec is written. - Post-funding thresholds. The ASK slide raises the bars (retention ≥ 3/5 active 8+ weeks;
expansion 30–50% of firms with 2+ employees; REACH 100–250 paying companies, $10–25K MRR; known
UNIT ECONOMICS / contribution margin). The same ledger computes these by widening the window and
adding
payment_madeamount-bucket metadata + a contribution-margin input; tracked here as a forward gate, built when the pilot gates pass — not now.
13. Bug / Change Log
2026-07-16 — Screen-view analytics honors the deferred telemetry window
Type: Performance + measurement integrity
Summary:
- Automatic
screen_viewedevents now queue for PostHog until scheduled telemetry initialization; the tenant-scoped F85 ledger write is unchanged. - Explicit interaction events retain eager analytics initialization.
- The empirical route-transfer harness reports deferred PostHog/Sentry bytes separately so an 8-second timer cannot randomly change a route budget.
2026-07-16 — General route backtrack telemetry closes the W-SYS-11 gap
Type: Feature (frontend + backend)
Summary:
- Added
screen_viewedto the append-onlyActivationEventledger and a best-effortPOST /api/metrics/events/screen-viewed/endpoint. The endpoint is tenant-scoped byIsOrgMember, accepts only a canonical pathname and client timestamp, and always returns204so telemetry cannot interrupt navigation. screenViewTelemetrysubscribes to TanStack RouteronResolvedevents and mirrors authenticated route changes to the F85 ledger plus PostHog. The event contains no query strings, hashes, or user content.- Added
find_screen_backtrack_loops()and included its count/details infrustration_report;GAPSis now explicitly empty. Brief-specific backtracks remain available for continuity. - Added backend endpoint/query tests and a frontend helper test, plus the migration and API/flow documentation. Production telemetry must still be observed after deployment before PM scoring or a release result changes.
2026-07-10 — Rage-click telemetry closes the W-SYS-11 gap (first client-write endpoint)
Type: Feature (frontend + backend)
Summary:
docs/tests/evidence/web/W-SYS-11/report.mdnamedui_rage_click_detectedas a fully open gap: no client event existed for rapid repeat clicks, and even PostHog’s own autocapture (if firing) lands only in PostHog’s cloud with no per-organization/member scoping this codebase can query.- Added
("ui_rage_click_detected", "Rage-click burst detected")toActivationEvent.EVENT_TYPES(migration0004_alter_activationevent_ event_type). Nodedupe_key— every burst is distinct, never one-per-day like the attributable event types. - Added
frontend/src/lib/rageClickTelemetry.ts— a document-level, capture-phase click listener detecting >=4 clicks within 1s inside a 32px radius, debounced to ONE report per burst. Reports via both established frontend product-event paths:posthog.capture('ui_rage_click_detected', …)(dynamically imported, same singletonChatPage.tsxalso calls.capture()on) for the analytics dashboards, and the newPOST /api/metrics/events/rage-click/(§5) for the tenant-scopedActivationEventledgerfrustration_reportreads. Target descriptor is tag +data-testidonly — never text content (privacy). - Added
apps.metrics.frustration.find_rage_click_clusters()— reads bursts back ranked by click intensity (worst offender first, ties by recency).build_report()/render_report_text()now surface arage_click_clustersignal; therage_clickentry is removed fromGAPS(closed, not a gap anymore) —backtrack_loop_generalis the one remaining honest gap. - Tests:
frontend/src/test/_shared/rageClickTelemetry.test.ts(11, injected deps),backend/tests/metrics/test_rage_click_view.py(5, including tenant-isolation and unauthenticated-rejection cases),backend/tests/metrics/test_frustration_report.py(+4 newTestRageClickClusterscases + the existing gap/merge tests updated for the now-closed gap).apps/metrics/frustration.pystays at 100% statement coverage. - See MVP_SCOPE.md — this closes a query-layer gap on an already-shipped feature; no matrix status change (F85 status unchanged).
2026-07-02 — Added missing confirmation-send emitter test (§7 row 14)
Type: Test
Summary:
- Added
TestEmitters::test_confirmation_send_emits_brief_item_actedtobackend/tests/test_metrics.py. §7 row 14 claimed the confirmation-send flow emitsbrief_item_acted, and the code does emit it (confirmations/views.pylines 86-94), butTestEmittershad no test covering that code path (grep forconfirmationreturned nothing). The new test creates aCommitment, POSTs to/api/confirmations/, and asserts thatActivationEventrows exist for bothconfirmation_sentandbrief_item_acted(withsubject_id=commitment.idandmetadata.action="confirmation"). Tenant scoping respected viapilot_org/pilot_memberfixtures.
Impact:
- No production code changed. Closed the gap between §7 row 14 and the test suite. Coverage: 97.97% (above the 97% threshold).
2026-07-02 — Spec drift corrected (batch 5)
Type: Docs
Summary:
- Status updated from
drafttoin progress (shipped 2026-06-24)to match §11 (backendapps/metrics+ frontend/hq/pilot+ all tests built and green 2026-06-24; validation-gated on real pilot data per §11 “Acceptance is the pilot itself”). - All §2 ACs marked
[x]per §11:- Founder AC1: pilot dashboard with gate cards + PASS/FAIL badges — confirmed by §11 frontend description + §7 row 10 (TestPilotEndpoint, green).
- Founder AC2: non-staff
403/ redirect — confirmed by §7 row 11 (green) + E2E describe block. - Founder AC3: cohort table with segment tag — confirmed by §7 row 12 (green).
- Founder AC4:
brief_item_acteddeduplication (one per item) — confirmed by §7 row 6 (green)- row 14 (TestEmitters, green).
- Founder AC5:
extraction_flagged_inaccuratevia F15 feedback + ACCURACY gate — confirmed by §11 “F15 feedback” wiring inapps/briefs/views.py+ §7 row 8 (green). - Emitter AC1:
pulse_submittedon submit commit — confirmed by §11 + §7 row 13 (green). - Emitter AC2: cross-tenant reject + flow unaffected — confirmed by §7 row 4 (green) +
TestActivationEventIsolation.
2026-06-24 — Implemented F85
Type: Feature
Summary:
- Shipped the pilot instrumentation spine:
apps/metricswith the append-onlyActivationEventledger, the best-effortemit()helper, the pure-querygates.py(update_completion, retention, action_rate, expansion, payment, accuracy + cohort builder/summary), and the staff-only endpointsGET /api/hq/pilot/gates/andGET /api/hq/pilot/cohort/. AddedOrganization.pilot_segment(migration). Wiredemit()into the eight §5.1 flows. Added the/hq/pilotfounder dashboard (PilotDashboardPage, gate cards + shared<DataTable>cohort table,metricsi18n namespace en+vi).
Reason:
- The pitch’s EVIDENCE/ASK gates were unmeasurable (no metrics/activation-event model existed). F85 makes them executable, aligned with field-tests.md gate IDs/thresholds.
Impact:
- New
apps/metrics+ oneOrganizationfield + a staff-only HQ surface. No customer-facing change. Tenant isolation enforced even though the staff dashboard is cross-org by design.
Tests:
tests/test_metrics.py(TestEmit, TestGates, TestPilotEndpoint, TestEmitters) +tests/test_multitenancy.py::TestActivationEventIsolation;src/test/F85PilotDashboard.test.tsx; E2Edescribe("Pilot dashboard (F85)"). New backend code 100% covered; ruff clean; tsc 0 errors; design-token check clean.
Notes:
- Thresholds quoted from field-tests.md / pitch (completion ≥60%, retention ≥3 firms on 6+ days,
action_rate ≥30%, expansion ≥2 firms, payment ≥1, accuracy == 0). Working day default Mon–Sat;
window 14 days.
emit()swallows its own errors (never breaks a product flow) and refuses cross-tenant member/subject.payment_madeis a boolean (no amount, no PII).
2026-06-24 — Spec created
Type: Feature
Summary:
- Created F85 to define the pilot instrumentation spine: an append-only
ActivationEventledger (mirroringNotificationEvent/ConfirmationEvent), a server-sideemit()helper wired into existing flows, pure-query gate computation for the pitch’s seven behaviour-based gates (UPDATE COMPLETION, RETENTION, ACTION RATE, EXPANSION, PAYMENT, ACCURACY, SEGMENT SIGNAL), and a staff-only/hq/pilotdashboard. Defined “observable action” precisely and attributably.
Reason:
- The investor pitch’s EVIDENCE and ASK slides promise behaviour-based gates, but no analytics /
metrics / activation-event model exists in the backend (
apps/analytics/apps/metricsabsent; F52’s PostHog activation event is an unbuilt placeholder). The gates are unmeasurable — and a promise you cannot measure is a guess. This spec is the measurement contract that makes the pitch executable, aligned with field-tests.md gate IDs/thresholds.
Impact:
- New draft feature F85 in the registry. Adds one model + one app (
apps/metrics) + one Organization field (pilot_segment) + a staff-only HQ surface. No customer-facing change. Tenant isolation is a hard gate despite the cross-org staff dashboard.
Tests:
- Test plan defined (§7); not yet implemented.
Notes:
- F85 measures; F86 enforces the ACCURACY gate — shared event
is
extraction_flagged_inaccurate. Reuse the spine and the existing event-ledger pattern; do not build a parallel analytics platform. Self-hostedActivationEventis the source of truth; any PostHog use is an optional downstream mirror (ADR required to be more than a sink). Validation-gated: the gates’ verdicts on real pilot data are the deliverable, not a green test suite alone.