feat(report): Stage-0 usage gate counts from a baseline date (default 2026-06-11)
The return-usage gate (ADR-016) counted distinct active weeks over ALL history, so pre-launch noise — testing churn and the period the pilot sat blocked on zero summaries — would inflate the signal. Add a baseline: ActiveWeeks(ctx, since) filters login_events + summary_actions to seen_at/acted_at >= since. The report command sets it to TAPIR_USAGE_GATE_START (YYYY-MM-DD, default 2026-06-11 — the morning the pilot was unblocked) and prints the baseline. The gate now measures whether users RETURN once it genuinely works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
@@ -32,7 +33,12 @@ type UserActiveWeeks struct {
|
||||
// Scope note: the enumeration covers users with a Dex identity (the web users the
|
||||
// gate is about). A CLI-only user created by the store sink without an identity
|
||||
// row would not appear — out of scope for this gate.
|
||||
func (s *Store) ActiveWeeks(ctx context.Context) ([]UserActiveWeeks, error) {
|
||||
// ActiveWeeks counts each user's distinct active weeks from `since` onward. A zero
|
||||
// `since` means no lower bound (count all history). The Stage-0 gate baseline is
|
||||
// set by the caller (the report command) to the date real usage tracking began,
|
||||
// so pre-launch noise — testing, the period the pilot was blocked — does not count
|
||||
// toward the return-usage signal (ADR-016).
|
||||
func (s *Store) ActiveWeeks(ctx context.Context, since time.Time) ([]UserActiveWeeks, error) {
|
||||
userIDs, err := s.identityUserIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -40,7 +46,7 @@ func (s *Store) ActiveWeeks(ctx context.Context) ([]UserActiveWeeks, error) {
|
||||
|
||||
out := make([]UserActiveWeeks, 0, len(userIDs))
|
||||
for _, uid := range userIDs {
|
||||
row, err := s.activeWeeksFor(ctx, uid)
|
||||
row, err := s.activeWeeksFor(ctx, uid, since)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -85,18 +91,18 @@ func (s *Store) identityUserIDs(ctx context.Context) ([]string, error) {
|
||||
// activeWeeksFor counts one user's distinct active weeks (reads UNION acts) and
|
||||
// reads their display name, RLS-scoped via withUser. The UNION dedups a week that
|
||||
// has both a login and an action so it counts once.
|
||||
func (s *Store) activeWeeksFor(ctx context.Context, userID string) (UserActiveWeeks, error) {
|
||||
func (s *Store) activeWeeksFor(ctx context.Context, userID string, since time.Time) (UserActiveWeeks, error) {
|
||||
res := UserActiveWeeks{UserID: userID}
|
||||
if err := s.withUser(ctx, userID, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx,
|
||||
`WITH weeks AS (
|
||||
SELECT date_trunc('week', seen_at) AS wk
|
||||
FROM login_events WHERE user_id = $1
|
||||
FROM login_events WHERE user_id = $1 AND seen_at >= $2
|
||||
UNION
|
||||
SELECT date_trunc('week', acted_at)
|
||||
FROM summary_actions WHERE user_id = $1
|
||||
FROM summary_actions WHERE user_id = $1 AND acted_at >= $2
|
||||
)
|
||||
SELECT count(DISTINCT wk) FROM weeks`, userID).Scan(&res.ActiveWeeks); err != nil {
|
||||
SELECT count(DISTINCT wk) FROM weeks`, userID, since).Scan(&res.ActiveWeeks); err != nil {
|
||||
return fmt.Errorf("store: count active weeks: %w", err)
|
||||
}
|
||||
if err := tx.QueryRow(ctx,
|
||||
|
||||
@@ -3,6 +3,7 @@ package store_test
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -54,7 +55,7 @@ func TestActiveWeeksCountsDistinctWeeksAcrossReadsAndActs(t *testing.T) {
|
||||
($1, 'vid-2', 'saved', '2026-01-19T18:00:00Z')`, userA)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := s.ActiveWeeks(ctx)
|
||||
got, err := s.ActiveWeeks(ctx, time.Time{}) // zero since = no lower bound
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2, "both identity users must appear")
|
||||
|
||||
@@ -72,7 +73,33 @@ func TestActiveWeeksEmptyWhenNoUsers(t *testing.T) {
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
got, err := s.ActiveWeeks(ctx)
|
||||
got, err := s.ActiveWeeks(ctx, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, got)
|
||||
}
|
||||
|
||||
// TestActiveWeeksExcludesBeforeGateStart proves the baseline cutoff: activity
|
||||
// before `since` does not count, so pre-launch noise (testing, the pilot's blocked
|
||||
// period) is excluded from the Stage-0 return-usage gate (ADR-016).
|
||||
func TestActiveWeeksExcludesBeforeGateStart(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p)
|
||||
|
||||
seedReportUser(t, p, userA, "subject-a", "Ada")
|
||||
|
||||
// One read well before the baseline, two reads in distinct weeks after it.
|
||||
_, err := p.Exec(ctx,
|
||||
`INSERT INTO login_events (user_id, seen_at) VALUES
|
||||
($1, '2026-05-01T09:00:00Z'),
|
||||
($1, '2026-06-12T09:00:00Z'),
|
||||
($1, '2026-06-19T09:00:00Z')`, userA)
|
||||
require.NoError(t, err)
|
||||
|
||||
since := time.Date(2026, 6, 11, 0, 0, 0, 0, time.UTC)
|
||||
got, err := s.ActiveWeeks(ctx, since)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1)
|
||||
require.Equal(t, 2, got[0].ActiveWeeks, "only the two post-baseline weeks count; the May read is excluded")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user