Files

88 lines
3.6 KiB
TypeScript

export const LAUNCH_GATE_SEEDS = [
["sealed-e2e", "P0", "security", "sealed 命令与附件端到端实证"],
["expiry-boundary", "P0", "entitlement", "到期后的操作边界与重放测试"],
["tenant-isolation", "P0", "infrastructure", "租户隔离、备份与删除验证"],
["billing-idempotency", "PAID", "billing", "订单到权益的幂等与对账"],
["pairing-claim-security", "P0", "security", "主机配对认领、限速与尝试预算"],
["public-auth", "P0", "identity", "国内个人用户登录、恢复与管理员身份"],
["legal-entity", "P0", "compliance", "免费公测主体、域名与备案路径"],
["payment-provider", "PAID", "billing", "支付商户准入、验签、退款与对账"],
["tax-invoice", "PAID", "compliance", "税务、含税口径与数电发票"],
["privacy-retention", "P0", "privacy", "数据清单、保存、删除与跨境路径"],
["terms-consumer", "PAID", "compliance", "付费服务条款、取消、退款与消费者规则"],
["backup-restore", "P1", "operations", "备份与恢复演练"],
["capacity-economics", "P1", "operations", "容量、成本与支持工时实测"],
["beta-policy", "P1", "product", "公测结束、通知、宽限与反滥用限制"],
["build-toolchain-audit", "P1", "security", "构建工具链残余公告与上游替换"],
["account-lifecycle", "P1", "privacy", "导出、注销、保留例外与备份擦除"],
["host-lifecycle-recovery", "P1", "operations", "主机恢复、换绑、停用、重装与凭据轮换"],
["incident-response", "P1", "security", "安全事件分级、值守、通知与服务流程"],
["release-provenance", "P1", "security", "PWA 构建来源、CSP、依赖与回滚 provenance"],
] as const;
export const REQUIRED_PUBLIC_BETA_P0_KEYS = LAUNCH_GATE_SEEDS
.filter(([, priority]) => priority === "P0")
.map(([key]) => key);
export type LaunchGateEvidence = {
key: string;
priority: string;
status: string;
owner: string | null;
notes: string;
evidence_url: string | null;
};
export function hasPassingGateEvidence(gate: LaunchGateEvidence): boolean {
if (
gate.status !== "passed"
|| !gate.owner?.trim()
|| !gate.notes.trim()
|| !gate.evidence_url
) {
return false;
}
try {
return new URL(gate.evidence_url).protocol === "https:";
} catch {
return false;
}
}
export function countBlockedPublicBetaP0(gates: readonly LaunchGateEvidence[]): number {
const required = new Set<string>(REQUIRED_PUBLIC_BETA_P0_KEYS);
const presentRequired = new Set(
gates
.filter((gate) => gate.priority === "P0" && required.has(gate.key))
.map((gate) => gate.key),
);
const missingRequired = REQUIRED_PUBLIC_BETA_P0_KEYS.length - presentRequired.size;
const invalidP0 = gates.filter(
(gate) => gate.priority === "P0" && !hasPassingGateEvidence(gate),
).length;
return missingRequired + invalidP0;
}
const requiredP0SqlList = REQUIRED_PUBLIC_BETA_P0_KEYS
.map((key) => `'${key.replaceAll("'", "''")}'`)
.join(", ");
/**
* Fail closed when a required P0 row is missing or any P0 row lacks complete
* passing evidence. This fragment is embedded only in repository-owned SQL.
*/
export const PUBLIC_BETA_GATE_READY_SQL = `
(SELECT COUNT(*) FROM launch_gates
WHERE priority = 'P0' AND key IN (${requiredP0SqlList})) = ${REQUIRED_PUBLIC_BETA_P0_KEYS.length}
AND NOT EXISTS (
SELECT 1 FROM launch_gates
WHERE priority = 'P0'
AND (
status != 'passed'
OR trim(COALESCE(owner, '')) = ''
OR trim(COALESCE(notes, '')) = ''
OR evidence_url IS NULL
OR lower(evidence_url) NOT LIKE 'https://%'
)
)`;