/** * [ref] —— **跨键不变量的单源表**([ref] §2 A′,codex 设计审 F2)。 * * 病灶:boot 期的跨域不变量(拒启 throw)散在 `config.ts` 的装配尾与 `main.ts` 的开头。把配置改热之后, * 每一条都变成一个**热路径上的漏检**:一次发布可以在运行期造出 boot 期会当场拒启的姿态(无鉴权写面被 * 打开、bake runner 凭据被删、直连门半开、reaper 追杀活 run……),而进程照跑。逐条在 stage 里手抄一遍 * 是必错的形(抄漏一条没人会发现),所以本文件是**唯一**的表: * * · stage 面(config-center):批1 的任一键触发**全表**校验,过不了 ⇒ 整批拒 + 旧值继续服务 + 铸 * `restartReason`(运维据此知道「这个变更只能重启生效」,而不是被静默吞掉)。 * · boot 面:每条 throw 点旁挂 `CROSS-INVARIANT:` 锚,由 test/config-hot/batch1-cross-invariants.test.ts * 双向对表 —— 新增一条拒启却不登记 = 门红。boot 的判据文本与拒启行为**逐字不变**(本批不改拒启), * 表在这一侧承担的是「不许有表外不变量」的登记职责。 * * 🔴 判据形状:每条不变量收 `{live, next}` 两个视图。有些条目判的是**状态**(next 自身是否自洽),有些 * 判的是**转换**(宽→窄、门姿态翻转)。boot 期没有「上一代」,调用方传 `live === next`,于是转换型条目 * 在 boot 天然惰性 —— 这正确:boot 不存在「热切」这回事。 * * 依赖纪律:本文件是**叶**(只 type-import config-types + 值引 deployment-governance 的守卫集编译门), * 不引 config.ts,故 config.ts / runs.ts 都可以安全引它。 */ import type { ServiceConfigFlat } from "./config-types.js"; /** * 活体心跳周期(ms)——`runs.ts` 的 `HEARTBEAT_MS` 就是它(那边 re-export,单源在此)。 * * 为什么常量住在不变量文件里:它唯一的**判据用途**是「陈旧窗必须大于心跳」这条不变量,而该不变量现在 * 有 boot 与 stage 两个消费面。放在这里,两面读的是同一个数,`runs.ts` 的心跳环也读同一个数。 */ export declare const LIVENESS_HEARTBEAT_MS = 30000; /** 闭集:新增一条不变量必须在这里加词(拼错 = 编译红,而不是运行期悄悄不判)。 */ export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo"; /** 表读的键集(取自平铺配置型 ⇒ 键名/类型永不与真配置漂)。 */ export type CrossInvariantView = Readonly>; export interface CrossInvariantInput { /** 现役配置(boot 期 = 候选自身)。 */ readonly live: CrossInvariantView; /** 候选(stage 期 = 现役 + 本批 staged 赋值)。 */ readonly next: CrossInvariantView; } export interface CrossInvariant { readonly id: CrossInvariantId; /** 本条读哪些键(诊断面按键回答「我这次改动会被哪条不变量看着」)。 */ readonly keys: readonly (keyof ServiceConfigFlat)[]; /** 热拒时铸给运维的重启因由(与 restart-signal 的 slice 词表同族的自由词,不是 RestartSlice 闭集)。 */ readonly restartReason: string; /** boot 期同一条不变量的拒启锚(机械门用;缺席 = 本条只存在于 stage 面)。 */ readonly bootAnchor?: { readonly file: string; readonly marker: string; }; /** 返回拒因;`undefined` = 通过。 */ check(input: CrossInvariantInput): string | undefined; } export declare const CONFIG_CROSS_INVARIANTS: readonly CrossInvariant[]; /** * 按 id 取**一条**(S-157②:boot 面的单源化出口)。 * * 为什么不是在 boot 期跑 `checkCrossInvariants` 全表:全表里有若干条的 boot 拒启点在 `config.ts` 的装配尾 * (各自有自己的锚),在这里再跑一遍全表等于给 boot 加上今天不存在的拒启臂 —— 那是行为改动,不是单源化。 * 本出口只做一件事:让 boot 与 stage 两面调**同一只** `check`(连拒因文本也来自它),消灭手抄的第二份判据。 * 闭集越界不可达(`CrossInvariantId` 是编译期闭集),真撞上就响亮抛 —— 不返回 undefined 让调用方静默跳过。 */ export declare function crossInvariant(id: CrossInvariantId): CrossInvariant; export type CrossInvariantVerdict = { ok: true; } | { ok: false; id: CrossInvariantId; reason: string; restartReason: string; }; /** 全表校验(**第一条不过即拒**:候选是整批的,报第一个成因足够行动,继续跑其余条目只会淹掉它)。 */ export declare function checkCrossInvariants(input: CrossInvariantInput): CrossInvariantVerdict; //# sourceMappingURL=config-invariants.d.ts.map