feat(cli): tapir report — Stage-0 usage gate query
CI / Lint / Test / Vet (push) Successful in 12s
CI / Build & Import (push) Successful in 10s

ActiveWeeks computes per-user distinct active weeks (reads login_events UNION
acts summary_actions) for the gate (VISION/ADR-016: usage in >=2 distinct
weeks). Because the user-owned tables are FORCE RLS under a non-superuser owner,
a single cross-user query is deny-all; instead it enumerates users from the
un-RLS'd identity map and counts each inside withUser — no privilege escalation,
no policy change.

`tapir report` prints the per-user table and the pass/fail verdict (needs only
TAPIR_DB_DSN). Pure formatter + store query are unit-tested, including the
cross-table shared-week dedup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:46:03 +02:00
co-authored by Claude Opus 4.8
parent 2fac735837
commit 561ba79360
5 changed files with 304 additions and 0 deletions
+3
View File
@@ -56,6 +56,8 @@ func main() {
err = cmdServe(ctx, log)
case "invite":
err = cmdInvite(ctx, os.Args[2:])
case "report":
err = runReport(ctx, os.Args[2:])
default:
usage()
os.Exit(2)
@@ -77,6 +79,7 @@ usage:
tapir invite <email> mint an invitation link for a new user (host-side)
tapir list [-limit N] list stored summaries, recent first
tapir show <video-id> show one summary in full
tapir report Stage-0 usage gate: per-user distinct active weeks
configuration is via TAPIR_* environment variables (see .env.example).
`)
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"context"
"fmt"
"io"
"os"
"text/tabwriter"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
)
// gateThreshold is the Stage-0 gate (VISION/ADR-016): usage in >= 2 distinct
// weeks. The gate passes when any user reaches it.
const gateThreshold = 2
// 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).
func runReport(ctx context.Context, _ []string) error {
dsn := os.Getenv(envDSN)
if dsn == "" {
return fmt.Errorf("%s is required", envDSN)
}
s, err := store.New(ctx, dsn)
if err != nil {
return err
}
defer s.Close()
rows, err := s.ActiveWeeks(ctx)
if err != nil {
return err
}
return formatReport(os.Stdout, rows)
}
// 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 {
if len(rows) == 0 {
_, err := fmt.Fprintln(w, "no users yet")
return err
}
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
_, _ = fmt.Fprintln(tw, "USER\tNAME\tACTIVE_WEEKS\tGATE")
passed := false
for _, r := range rows {
gate := "-"
if r.ActiveWeeks >= gateThreshold {
gate = "PASS"
passed = true
}
_, _ = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", r.UserID, orDash(r.DisplayName), r.ActiveWeeks, gate)
}
if err := tw.Flush(); err != nil {
return err
}
verdict := fmt.Sprintf("\nGate (usage in >= %d distinct weeks): NOT YET MET\n", gateThreshold)
if passed {
verdict = fmt.Sprintf("\nGate (usage in >= %d distinct weeks): PASSED\n", gateThreshold)
}
_, err := fmt.Fprint(w, verdict)
return err
}
+45
View File
@@ -0,0 +1,45 @@
package main
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
)
func TestFormatReportColumnsAndGatePass(t *testing.T) {
rows := []store.UserActiveWeeks{
{UserID: "user-a", DisplayName: "Ada", ActiveWeeks: 3},
{UserID: "user-b", DisplayName: "", ActiveWeeks: 1},
}
var b strings.Builder
require.NoError(t, formatReport(&b, rows))
out := b.String()
require.Contains(t, out, "USER")
require.Contains(t, out, "ACTIVE_WEEKS")
require.Contains(t, out, "Ada")
require.Contains(t, out, "PASSED", "a user at >= 2 weeks passes the gate")
// The >=2 user is marked PASS; the 1-week user is not.
require.Contains(t, lineContaining(t, out, "user-a"), "PASS")
require.NotContains(t, lineContaining(t, out, "user-b"), "PASS")
require.Contains(t, lineContaining(t, out, "user-b"), "-", "no display name falls back to dash")
}
func TestFormatReportGateNotMet(t *testing.T) {
rows := []store.UserActiveWeeks{{UserID: "user-a", ActiveWeeks: 1}}
var b strings.Builder
require.NoError(t, formatReport(&b, rows))
require.Contains(t, b.String(), "NOT YET MET", "no user at >= 2 weeks fails the gate")
}
func TestFormatReportEmpty(t *testing.T) {
var b strings.Builder
require.NoError(t, formatReport(&b, nil))
require.Contains(t, b.String(), "no users yet")
}