fix(release): harden v1.31 arrival contracts

This commit is contained in:
Marc-André Bombeck committed 2026-07-20 00:40:20 +02:00
1 parent 44add3bc1e
commit 4591899657
98 files changed
+5560 -1600

No files matched your search

@@ -0,0 +1,9 @@
-- Durable arrival freshness plus ownership and spend linkage for
-- arrival-reaction provider attempts.
ALTER TABLE "arrival_reactions"
ADD COLUMN "arrived_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "generation_claim_id" TEXT,
ADD COLUMN "generation_claimed_at" TIMESTAMP(3),
ADD COLUMN "generation_reserved_tokens" INTEGER,
ADD COLUMN "generation_budget_date_key" TEXT,
ADD COLUMN "generation_provider_invoked_at" TIMESTAMP(3);
@@ -0,0 +1,6 @@
-- Preserve the original landing time for markers that predate `arrived_at`.
-- Migration 0260 must add the NOT NULL column with a default; this follow-up
-- restores historical rows to their actual first-arrival timestamp before the
-- application starts serving the release.
UPDATE "arrival_reactions"
SET "arrived_at" = "created_at";
@@ -0,0 +1,34 @@
-- Durable single-spend ownership and local-day capacity reservations for
-- per-workout Activity Insight generation. Existing insight rows remain valid
-- and are counted by the worker until a claim row exists for their workout.
CREATE TABLE "workout_insight_generation_claims" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"workout_id" TEXT NOT NULL,
"local_date" TEXT NOT NULL,
"claim_id" TEXT,
"claimed_at" TIMESTAMP(3),
"provider_invoked_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "workout_insight_generation_claims_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "workout_insight_generation_claims_workout_id_key"
ON "workout_insight_generation_claims"("workout_id");
CREATE UNIQUE INDEX "workout_insight_generation_claims_claim_id_key"
ON "workout_insight_generation_claims"("claim_id");
CREATE INDEX "workout_insight_generation_claims_user_id_local_date_idx"
ON "workout_insight_generation_claims"("user_id", "local_date");
ALTER TABLE "workout_insight_generation_claims"
ADD CONSTRAINT "workout_insight_generation_claims_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "workout_insight_generation_claims"
ADD CONSTRAINT "workout_insight_generation_claims_workout_id_fkey"
FOREIGN KEY ("workout_id") REFERENCES "workouts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+40
View File
@@ -797,6 +797,7 @@ model User {
// claim every reaction surface reads. Retained 14 days.
arrivalReactions ArrivalReaction[]
workoutInsights WorkoutInsight[]
workoutInsightGenerationClaims WorkoutInsightGenerationClaim[]
// v1.17.1 — preventive-care / measurement (Vorsorge) reminders. One row
// per user-created reminder ("BP every 7 days", "annual blood panel").
// Server computes the canonical `nextDueAt`; the cron fires through the
@@ -1941,6 +1942,7 @@ model Workout {
route WorkoutRoute?
samples WorkoutSamples?
insight WorkoutInsight?
insightGenerationClaim WorkoutInsightGenerationClaim?
/// Ingest dedup. NULL externalId is distinct so two manual
/// entries on the same day never collide here.
@@ -2046,6 +2048,7 @@ model WorkoutSamples {
// ever creates one: a workout with no row renders no card, and that includes
// every workout that predates this model.
model WorkoutInsight {
id String @id @default(cuid())
userId String @map("user_id")
workoutId String @unique @map("workout_id")
@@ -2076,6 +2079,29 @@ model WorkoutInsight {
@@map("workout_insights")
}
// Durable ownership and local-day capacity reservation for workout insight
// generation. The row is created before expensive evidence work. A live
// `claimId` owns the pre-provider lease; `providerInvokedAt` is the terminal
// boundary after which retries must never spend again.
model WorkoutInsightGenerationClaim {
id String @id @default(cuid())
userId String @map("user_id")
workoutId String @unique @map("workout_id")
localDate String @map("local_date")
claimId String? @unique @map("claim_id")
claimedAt DateTime? @map("claimed_at")
providerInvokedAt DateTime? @map("provider_invoked_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
workout Workout @relation(fields: [workoutId], references: [id], onDelete: Cascade)
@@index([userId, localDate])
@@map("workout_insight_generation_claims")
}
// ─── Strain personal anchor cache (v1.10.3) ─────────────────
//
// Server-internal per-(user, day) cache that lets the nightly Strain
@@ -5341,6 +5367,9 @@ model ArrivalReaction {
localDate String @map("local_date")
/// Timestamp of the newest sample that triggered it.
occurredAt DateTime @map("occurred_at")
/// When the newest sample actually reached HealthLog. Unlike `createdAt`,
/// this advances when a later same-kind arrival refreshes the daily marker.
arrivedAt DateTime @default(now()) @map("arrived_at")
/// Kind-scoped referent: the workout id for `workout`, the panel day for labs.
refId String? @map("ref_id")
/// AES-256-GCM ciphertext of the generated reaction line. Null until — and
@@ -5350,6 +5379,17 @@ model ArrivalReaction {
/// the status cards use. Registered in `scripts/rotate-encryption-key.ts`.
lineEncrypted Bytes? @map("line_encrypted")
generatedAt DateTime? @map("generated_at")
/// Opaque owner token and lease for pre-provider setup. A stale worker may
/// reclaim only while `generationProviderInvokedAt` is null.
generationClaimId String? @map("generation_claim_id")
generationClaimedAt DateTime? @map("generation_claimed_at")
/// The token reservation is written in the same transaction as the
/// `coach_usage` increment. A reclaimed pre-provider attempt reuses it.
generationReservedTokens Int? @map("generation_reserved_tokens")
generationBudgetDateKey String? @map("generation_budget_date_key")
/// Durable provider boundary. Once set, this marker is terminal unless the
/// encrypted line is committed; no retry may invoke a provider again.
generationProviderInvokedAt DateTime? @map("generation_provider_invoked_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)