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:
+33
-3
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
||||
)
|
||||
@@ -14,6 +15,27 @@ import (
|
||||
// weeks. The gate passes when any user reaches it.
|
||||
const gateThreshold = 2
|
||||
|
||||
// defaultGateStart is the date Stage-0 return-usage tracking begins: the morning
|
||||
// the pilot was actually unblocked and summaries started flowing (2026-06-11).
|
||||
// Activity before this — testing, the period the pilot was stuck on zero — is
|
||||
// noise and must not count toward the gate. Override with TAPIR_USAGE_GATE_START
|
||||
// (YYYY-MM-DD). The gate measures whether users RETURN once it genuinely works.
|
||||
const defaultGateStart = "2026-06-11"
|
||||
|
||||
// gateStart resolves the baseline date from TAPIR_USAGE_GATE_START or the default,
|
||||
// parsed as a UTC calendar day.
|
||||
func gateStart() (time.Time, error) {
|
||||
v := os.Getenv("TAPIR_USAGE_GATE_START")
|
||||
if v == "" {
|
||||
v = defaultGateStart
|
||||
}
|
||||
t, err := time.Parse("2006-01-02", v)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("TAPIR_USAGE_GATE_START=%q: want YYYY-MM-DD: %w", v, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// runReport prints the Stage-0 usage gate: per-user distinct active weeks (reads
|
||||
// UNION acts) and the pass/fail verdict. Read-only, cross-user — needs only
|
||||
// TAPIR_DB_DSN (not TAPIR_USER_ID; the report enumerates all users itself).
|
||||
@@ -28,16 +50,24 @@ func runReport(ctx context.Context, _ []string) error {
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
rows, err := s.ActiveWeeks(ctx)
|
||||
since, err := gateStart()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return formatReport(os.Stdout, rows)
|
||||
|
||||
rows, err := s.ActiveWeeks(ctx, since)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return formatReport(os.Stdout, rows, since)
|
||||
}
|
||||
|
||||
// formatReport renders the per-user week counts and the gate verdict. Pure: no DB,
|
||||
// no env — so the layout and verdict logic are unit-testable without Postgres.
|
||||
func formatReport(w io.Writer, rows []store.UserActiveWeeks) error {
|
||||
func formatReport(w io.Writer, rows []store.UserActiveWeeks, since time.Time) error {
|
||||
if _, err := fmt.Fprintf(w, "Counting usage since %s (Stage-0 gate baseline)\n\n", since.Format("2006-01-02")); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
_, err := fmt.Fprintln(w, "no users yet")
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user