fix: gate rectification rollout to canary users

This commit is contained in:
Jesse_Chen
2026-07-21 15:19:03 +08:00
parent 6827e0ca45
commit 576ebf9bf2
8 changed files with 318 additions and 34 deletions
@@ -23,6 +23,7 @@ import {
import type { RectificationNarrativeGenerator } from "../../../lib/conversational-rectification/narrative-agent.ts";
import type { BirthTimeJourneyEngine, RectificationQuestionnaire } from "../../../lib/birth-time-journey-service.ts";
import type { CandidateResult, LifeEvent } from "../../../lib/birth-time-evidence.ts";
import { conversationalRectificationCreationPolicyFromEnvironment } from "../../../lib/conversational-rectification/creation-policy.ts";
import {
conversationalRectificationLatencyBucket,
createConversationalRectificationTelemetry,
@@ -589,8 +590,9 @@ async function createProductionService(
store: createSupabaseConversationalRectificationStore(admin),
billing: createSupabaseConversationalRectificationBilling(admin),
get rectificationPriceCredits() { return priceCredits(); },
allowNewCaseCreation:
process.env.RECTIFICATION_V3_CREATE_ENABLED?.trim().toLowerCase() !== "false",
allowNewCaseCreation: conversationalRectificationCreationPolicyFromEnvironment(
authenticated.userId,
).allowNewCaseCreation,
async loadDeclaredProfile(userId) {
return loadProductionConversationalRectificationProfile({
async loadProfile(receivedUserId) {
+10 -17
View File
@@ -1,4 +1,8 @@
import { NextResponse } from "next/server";
import {
conversationalRectificationCreationPolicyFromEnvironment,
conversationalRectificationDeploymentShaFromEnvironment,
} from "../../../lib/conversational-rectification/creation-policy.ts";
type Check = {
status: "ok" | "degraded" | "blocked";
@@ -7,12 +11,6 @@ type Check = {
};
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
function deployedGitCommit(): string {
return process.env.GITHUB_SHA
?? process.env.VERCEL_GIT_COMMIT_SHA
?? process.env.NEXT_PUBLIC_GIT_COMMIT
?? "unknown";
}
function envCheck(names: string[]): Check {
const missing = names.filter((name) => !process.env[name]);
@@ -59,12 +57,10 @@ function aggregate(checks: Record<string, Check>) {
}
export async function GET() {
const gitCommit = deployedGitCommit();
const rectificationV3CreationEnabled =
process.env.RECTIFICATION_V3_CREATE_ENABLED?.trim().toLowerCase() !== "false";
const gitCommit = conversationalRectificationDeploymentShaFromEnvironment();
const rectificationV3MigrationsReady =
process.env.RECTIFICATION_V3_MIGRATIONS_READY?.trim().toLowerCase() === "true";
const smokeSha = process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA?.trim().toLowerCase() ?? "";
const creationPolicy = conversationalRectificationCreationPolicyFromEnvironment();
const checks = {
web: { status: "ok" } satisfies Check,
supabasePublicConfig: envCheck(["NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY"]),
@@ -73,12 +69,8 @@ export async function GET() {
jyotishApi: await jyotishApiCheck(),
};
const status = aggregate(checks);
const deploymentHasFullSha = /^[0-9a-f]{40}$/.test(gitCommit);
const smokeMatchesDeployment = deploymentHasFullSha && smokeSha === gitCommit;
const rectificationV3Ready = status === "ok"
&& rectificationV3CreationEnabled
&& rectificationV3MigrationsReady
&& smokeMatchesDeployment;
&& creationPolicy.audience === "public";
return NextResponse.json(
{
status,
@@ -89,9 +81,10 @@ export async function GET() {
rollout: {
conversationalRectificationV3: {
protocol: "conversational-evidence-v3",
newCaseCreation: rectificationV3CreationEnabled ? "enabled" : "paused",
newCaseCreation: creationPolicy.audience === "paused" ? "paused" : "enabled",
creationAudience: creationPolicy.audience,
migrations: rectificationV3MigrationsReady ? "ready" : "unverified",
syntheticSmoke: smokeMatchesDeployment ? "matched" : "pending",
syntheticSmoke: creationPolicy.smokeMatchesDeployment ? "matched" : "pending",
readyForNewCases: rectificationV3Ready,
},
},
@@ -0,0 +1,87 @@
export type ConversationalRectificationCreationAudience =
| "paused"
| "smoke_only"
| "public";
export type ConversationalRectificationCreationPolicyInput = Readonly<{
userId?: string | null;
creationEnabled?: string;
migrationsReady?: string;
deploymentSha?: string;
smokeSha?: string;
syntheticSmokeUserIds?: string;
}>;
export type ConversationalRectificationCreationPolicy = Readonly<{
audience: ConversationalRectificationCreationAudience;
allowNewCaseCreation: boolean;
smokeMatchesDeployment: boolean;
}>;
const fullDeploymentSha = /^[0-9a-f]{40}$/;
const canonicalUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
export function conversationalRectificationCreationPolicy(
input: ConversationalRectificationCreationPolicyInput,
): ConversationalRectificationCreationPolicy {
const deploymentSha = input.deploymentSha?.trim() ?? "";
const smokeSha = input.smokeSha?.trim() ?? "";
const baseGatesOpen = input.creationEnabled?.trim().toLowerCase() === "true"
&& input.migrationsReady?.trim().toLowerCase() === "true"
&& fullDeploymentSha.test(deploymentSha);
if (!baseGatesOpen) {
return {
audience: "paused",
allowNewCaseCreation: false,
smokeMatchesDeployment: false,
};
}
const smokeMatchesDeployment = smokeSha === deploymentSha;
if (smokeMatchesDeployment) {
return {
audience: "public",
allowNewCaseCreation: true,
smokeMatchesDeployment: true,
};
}
const smokeUsers = new Set(
(input.syntheticSmokeUserIds ?? "")
.split(",")
.map((value) => value.trim())
.filter((value) => canonicalUuid.test(value)),
);
if (smokeUsers.size === 0) {
return {
audience: "paused",
allowNewCaseCreation: false,
smokeMatchesDeployment: false,
};
}
return {
audience: "smoke_only",
allowNewCaseCreation: input.userId != null && smokeUsers.has(input.userId),
smokeMatchesDeployment: false,
};
}
export function conversationalRectificationCreationPolicyFromEnvironment(
userId?: string | null,
): ConversationalRectificationCreationPolicy {
return conversationalRectificationCreationPolicy({
userId,
creationEnabled: process.env.RECTIFICATION_V3_CREATE_ENABLED,
migrationsReady: process.env.RECTIFICATION_V3_MIGRATIONS_READY,
deploymentSha: conversationalRectificationDeploymentShaFromEnvironment(),
smokeSha: process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA,
syntheticSmokeUserIds: process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS,
});
}
export function conversationalRectificationDeploymentShaFromEnvironment(): string {
return process.env.GITHUB_SHA
?? process.env.VERCEL_GIT_COMMIT_SHA
?? process.env.NEXT_PUBLIC_GIT_COMMIT
?? "unknown";
}