88 lines
3.6 KiB
TypeScript
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://%'
|
|
)
|
|
)`;
|