@sema-agent/settings-schema 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,211 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 8.2.0 — 2026-10-08 — 迁移器逐行终判(行为改真)· fleet 端点常量与活性路由组 · 心跳三键收型 · 中心能力位的承载键 · 两处注释改真
10
+
11
+ > **判档 = minor**(README「发版纪律」:新键 / 新导出 = minor)。新导出 6 名(`LEASE_STATES` / `LeaseState` / `FLEET_PATHS` / `FLEET_LIVE_PATHS` /
12
+ > `HEARTBEAT_REPLY_CAPABILITIES_KEY`,`.` 与 `./fleet` 各 +5;`LeaseState` 是值 + 类型)+ 一个资产出口 `./fleet-paths.json`;`EngineSignal` 与 `InstanceAnnouncement`
13
+ > 各多可选键(无默认)。**唯一一处行为改真**是第一节的迁移器:它改变的是**读侧本来就拒**的文档的迁移结局(多一类 `refused` 行),合法文档的迁移结局与
14
+ > 读数一字不变(金样 m01–m04 与全部语料零 diff)。导出面登记表只多名字行,无删、无改形。
15
+
16
+ ### 一、`migrateWorkersDomain` 逐行终判(行为改真:多一类 refused 行)
17
+
18
+ **病**(管理面实测):一份已是 7.x 形、三行都不需要迁的 workers 文档里,一台在 `env` 里设了 `FLEET_GENERATION`(8.1.0 起的保留名)——
19
+ 迁移回 `changed: []` / `refused: []`,随后 `parseDomain("workers")` 整域抛:管理面 workers 唯一读口整域读不出,一行坏把整份舰队清单拖黑;
20
+ CLI 对同一份文件报 `outcome=noop` / 退出 0。根因:迁移的终判(`WorkerSpec.safeParse`)只对**它改写过的行**跑,没动的行直接放行 ——
21
+ 于是迁移与读侧对同一行各说各话,任何「存下来时合法、后来被收紧的判据拒掉」的行(保留名表变长只是其中一例)都走这条缝。
22
+
23
+ **修**(规则替换,不是加一条):终判**逐行**,迁过的行与没动的行同一只 —— 每一只认得出名字(字符串 `name`)的 worker 都过读侧自己的判据:
24
+ **只含这一行的 workers 域**读不读得进(`WorkersConfig`,就是 `parseDomain("workers")` 用的那一只 schema;逐行的规则写在 `WorkerSpec` 里的、
25
+ 写在域层的(`replicas` 与 `autoscaling` 互斥)都在,跨行规则一行时不触发)。读不进 ⇒ `refused`(`name` + `field` = schema issue 相对该行的路径 +
26
+ `reason` = schema 自己的原句,前缀说明是迁出形 `after migration the worker …` 还是存储形 `the stored worker …`;`original` 照旧逐字带回)。
27
+ 迁移不复述任何一条写闸规则、不另抄保留名表。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(迁移不判形,原样留在 `doc`)与跨行规则
28
+ (名字 / 路由前缀唯一)。迁过的行的终判拒因原句从 `after migration the worker is not a valid 7.0.0 WorkerSpec — …` 改成 `… is not a valid WorkerSpec — …`
29
+ (判据是当前版本的 schema,8.1.0 的保留名也在里面,「7.0.0」已不准)。
30
+
31
+ **拒因不回显输入值**(车内对抗复审的发现,修在同一版):zod 自带的枚举拒句以 `, received '<填的值>'` 收尾 —— 迁过的行的终判拒因与 CLI 整域终判的报告
32
+ 早就会带出它(8.1.0 已有),逐行终判又把没动的行也接到这条路上。现在 issue 一律经同一只渲染(内部 `issueText`,不在任何子路径上):
33
+ 枚举拒句只按它的闭集重写成 `Invalid enum value. Expected 'a' | 'b'`,其余 issue 的句子照旧(zod 的其余默认句只说期望的形,不带输入)。
34
+ 迁移的 `refused[].reason`、CLI 报告的 `refused[].reason` 与 stderr 结论句三处同用。
35
+
36
+ ### 二、心跳三键收型:`LEASE_STATES` / `LeaseState` · `EngineSignal.leaseState?` · `InstanceAnnouncement` 不再剥三键(additive)
37
+
38
+ - **`LEASE_STATES = ["none", "granted", "expired", "denied"]`**(+ zod `LeaseState` 与同名类型)—— 引擎持有的服务租约四态,闭集在这里定一次(引擎的词表逐字同序):
39
+ `none` = 租约不生效(中心没宣告 `servingLease` 位;行为与没有租约的引擎逐字同)· `granted` = 持有租约(`leaseUntil` 说到何时)·
40
+ `expired` = 租约生效但此刻没有有效授予(中心不可达 / 答 `unknown` / 尚未首授;照常服务直到引擎自己的不可达上限,不是被拒)·
41
+ `denied` = 中心凭正面证据拒了本实例(黏性,已排空、到期自停)。只有 fleet 成员说它;非成员没有这个键(没有第五个词表示「非成员」,缺席就是)。
42
+ - **`EngineSignal.leaseState?`**:闭形多一键(引的就是 `LeaseState` 那一只 schema);`leaseUntil` 的注改准为「在场 ⇔ `leaseState` 是 `granted`」。
43
+ `readEngineSignal` 原样读出;集外词按 `EngineSignal` 既有的律整只 `undefined`。
44
+ - **`InstanceAnnouncement` 加 `generation?` / `leaseState?` / `leaseUntil?`**(心跳此前把这三键 strip 掉 —— 引擎早已在每拍心跳上平铺它们,中心读不到):
45
+ 三键各自**引用** `EngineSignal` 上同名键的那一只 schema(规范代际串 / `LeaseState` / 非负整数 ms),不抄第二份;存下来的行(`InstanceRegistration`)随之带着。
46
+ **失败方向随含义**:三键是中心留存的证据、不是路由输入 —— 形坏的那一键落成缺席(`.catch`,与 `modelRefusals` 同姿势),心跳照收、可路由性不变;
47
+ 所以引擎以后多一个租约词,老中心不会因此拒掉它的每一拍心跳。三键之间的在场律(成员 ⇒ `generation` + `leaseState`;`leaseUntil` ⇔ `granted`)是引擎的律,本形不跨键判。
48
+ - 金样:引擎今天真发的心跳体(成员 · granted / expired 两拍、单机形)入 `test/fleet-8.2.0.test.ts`。
49
+
50
+ ### 三、两处注释改真(零行为)
51
+
52
+ - **`EngineSignal` 头注**:8.1.0 写的「心跳与 `/health` 携带同名键、同值,平铺在体上,`readEngineSignal` 读哪一个都行」(本档 8.1.0 第二节同句)**不成立**,
53
+ 且与同一份注里「心跳 / `InstanceAnnouncement` 的 `draining` 是布尔」自相矛盾。照实改为:两出口今天都不整只承载这只信号 ——
54
+ ① 心跳恒带布尔 `draining`(含 `false`),`/health` 只在排空时带 `draining: true`;② `/health` 只在**未**就绪时带 `ready`;③ 引擎把 `configTargetVersion` /
55
+ `configAppliedVersion` 写成数,本形是串。所以 `readEngineSignal` 对今天任何心跳体恒 `undefined`,对 `/health` 体在三条任一成立时 `undefined`;两出口同名同值平铺的
56
+ 只有 `restart` / `instanceId` / `generation` / `leaseState` / `leaseUntil`(心跳上读解析后的 `InstanceAnnouncement`)。下一个 major 退布尔对、两出口整只承载、两只路由谓词改吃它。
57
+ `readEngineSignal` 的注同步(不再说「两出口键都平铺、哪个都能读」)。README / llms-full 的同句一并改。
58
+ - **`ServingLeaseRequest.scope` 注**:8.1.0 写「缺席 = 单 scope 部署」。改成续租的令牌规则一句话:续租**只收 per-worker 令牌**(凭证绑定 (scope, worker)),
59
+ 所以 scope 由凭证定、本字段只能附和 —— 缺席 = 令牌绑定的 scope;在场必须与之相等(不符即拒,两头都不改写);**共享令牌不得续租**(中心答 403)。
60
+ - **心跳一侧的 scope 规则写在 `InstanceAnnouncement` 注上**:心跳体**没有** `scope` 键(本形没有这个键,发了也被 strip);行落在凭证点名的 scope ——
61
+ per-worker 令牌 = 令牌绑定的 scope(给了 `?scope=` 就必须相等);共享令牌 = `?scope=`,缺席 ⇒ `"global"`。这条是心跳的,不是续租的。
62
+ - 钉:`test/fleet-8.2.0.test.ts` ③ —— 四格注释文本钉(续租注不许出现 `?scope=` / `"global"`;在改前的注上红)+ 两格行为钉(引擎今天真发的心跳 / `/health` 金样上
63
+ `readEngineSignal` 的读数,本来就绿:注改的是话,不是行为);心跳注那一格顺带钉「心跳体里的 `scope` 被 strip」。
64
+
65
+ ### 四、fleet 端点常量:`FLEET_PATHS` · `FLEET_LIVE_PATHS` · 随包的 `fleet-paths.json`(additive)
66
+
67
+ **病**:fleet 端点路径两仓各拼一份,已经对不上 —— 引擎续租打 `/api/fleet/serving-lease`,管理面活性组预留的是 `/api/fleet/live/serving-lease`
68
+ (中心一宣告 `servingLease` 位,全舰队续租就 404);心跳 / 用量 / 配额租约三条也是两侧手写字面。
69
+
70
+ - **`FLEET_PATHS`**(`as const`,形照 `AUTH_V1_PATHS` 先例)—— 中心外部按路径打到的 `/api/fleet/*` 端点,一种拼法:
71
+ `instances: "/api/fleet/instances"`(心跳 `POST` / 告别 `DELETE`)· `usage: "/api/fleet/usage"` · `quotaLeases: "/api/fleet/leases"` ·
72
+ `servingLease: "/api/fleet/live/serving-lease"` · `hostPlanner: "/api/fleet/live/host-planner/{host}"`(执行器读一台 host 的规划记录;体是中心与执行器的契约,
73
+ 不在本包 —— 它在这里是因为部署要把它当活性组成员路由)。成员律:本包定形其体的端点 ∪ 活性组成员;中心内部路由不在。
74
+ 记法:值是路径**模板**,参数只以整段 `{name}` 出现(只有 `hostPlanner` 有);前缀路由(Ingress)取第一个 `{` 之前的部分。
75
+ **没有能力读口端点**:中心能力位的唯一载体是心跳 2xx 答复体(见第五节)。
76
+ - **`FLEET_LIVE_PATHS`** = `[instances, servingLease, hostPlanner]` —— 活性路由组:中心不经就绪门、不经配置治理门作答的那组(不读配置域、只收服务凭证),
77
+ 部署把它们路由过就绪门(Kubernetes:第二个 `publishNotReadyAddresses` 的 Service,Ingress 规则指向它)。**组是这张表,不是路径前缀**(心跳早于 `/api/fleet/live/`
78
+ 前缀);`FLEET_PATHS` 里 `/api/fleet/live/` 下的每一条都在组里;配额租约与用量不在。值就是 `FLEET_PATHS` 里那几只串,不是第二份拼写。
79
+ - **`@sema-agent/settings-schema/fleet-paths.json`**(新资产出口 → `dist/fleet-paths.json`)= `{ "FLEET_PATHS": …, "FLEET_LIVE_PATHS": … }`:给 import 不了 TS 的工具
80
+ (Helm 模板、冒烟脚本)对表。`npm run build` 在 tsc 之后由 `scripts/gen-fleet-paths.mjs` 从**构建出的** `dist/fleet.js` 写出(脚本里没有路径字面),不手改;
81
+ 钉它与常量逐字同(常量改了没重建 ⇒ 红)。导出面登记表表头的资产出口清单改成从 exports 表派生(原先手写只有 `./package.json`)。
82
+ - 钉:`src/` 里 `"/api/fleet/` 起头的字面量只出现在 `FLEET_PATHS` 那几行(第二处拼写 ⇒ 红)。
83
+
84
+ ### 五、中心能力位的承载键:`HEARTBEAT_REPLY_CAPABILITIES_KEY = "capabilities"`(additive)+ `CenterCapabilities` 注改真
85
+
86
+ - 中心把 `CenterCapabilities` 放在**心跳 2xx 答复体**的哪个键下 —— 这是能力对象的**唯一载体**(没有独立的能力读口;一事实一载体)。中心写
87
+ `{ …, [HEARTBEAT_REPLY_CAPABILITIES_KEY]: caps }`,引擎读 `readCenterCapabilities(reply[HEARTBEAT_REPLY_CAPABILITIES_KEY])`,且只在 2xx 时读;嵌套不平铺
88
+ (答复体自己的键与能力位不共享命名空间);答复体没有这个键 = 老中心,什么位都没说。两仓不再各写字面(`src/` 里这个键名字面量只出现在常量那一行,有钉)。
89
+ - `CenterCapabilities` 注:撤 8.1.0 的「本包只定形与读器、不定哪个中心端点承载」;改为「唯一载体 = 心跳 2xx 答复体的这个键」,并加一句
90
+ **位按调用方凭证答**:per-worker 令牌按中心实际支持的答;共享令牌下 `servingLease` 恒缺席(续租端点只收 per-worker 令牌,对共享令牌宣告只会把它推进不可达臂)。
91
+ - 金样:老中心 `{ ok: true }` ⇒ `undefined`;`{ ok: true, capabilities: {} }` ⇒ `{}`;`{ ok: true, capabilities: { servingLease: true } }` ⇒ 启用;位平铺在答复体上不算。
92
+
93
+ ### 用户可见变化(谁受影响 / 怎么改 / 不改会怎样)
94
+
95
+ 1. **管理面的 workers 读口(调 `migrateWorkersDomain` 的)**:一行设了保留名(或别的逐行读不进的形)的文档,以前整域读失败,现在那一行进 `refused`、其余照常加载。
96
+ 按契约「`refused` 行保住它正在跑的负载、不当删除」处理的,零动作;不改会怎样:没这样处理的编排器会把那一行当成缺席。
97
+ 2. **`migrate-workers` CLI 的脚本**:上面那类文件以前 `outcome=noop` / 退出 0,现在 `outcome=refused` / 退出 1、一字不写;读 `refused[].reason` 原句的,
98
+ 迁过的行的终判拒因少了「7.0.0」,没动的行的拒因以 `the stored worker` 起头;枚举拒句不再带 `received '<值>'`。
99
+ 3. **引擎(续租客户端)**:续租路径改 import `FLEET_PATHS.servingLease`(`/api/fleet/live/serving-lease`);不改会怎样:中心按包常量注册续租路由后,手写
100
+ `/api/fleet/serving-lease` 的引擎续租得 404(走不可达臂)。心跳答复体的能力对象改读 `reply[HEARTBEAT_REPLY_CAPABILITIES_KEY]`。
101
+ 4. **中心**:心跳 / 用量 / 配额租约 / 续租 / HostPlanner 的路由路径与活性组表从 `FLEET_PATHS` / `FLEET_LIVE_PATHS` 取;能力对象写在
102
+ `HEARTBEAT_REPLY_CAPABILITIES_KEY` 下;心跳解析结果里现在有 `generation` / `leaseState` / `leaseUntil`(存行 `InstanceRegistration` 随之)。
103
+ 5. **部署(Helm / 冒烟)**:活性组路由与对表读 `@sema-agent/settings-schema/fleet-paths.json`(`FLEET_LIVE_PATHS`;`{host}` 是路径参数,前缀路由取 `{` 之前)。
104
+ 6. **对 `EngineSignal` / `InstanceAnnouncement` 的键做穷举的**:多出的键编译红(提货成本,补一行)。**持 `LEASE_STATES` 穷举的**:以后加词同理。
105
+
106
+ ### 红先 / 门
107
+
108
+ - 每节先写钉、对着改前的树跑红,再实现:第一节 `test/migrate-workers-row-verdict-8.2.0.test.ts` 20 格(基线 14 红;6 绿是干净文档幂等、两格最后围栏、
109
+ 整域读侧对照等反向格;其中 3 格「拒因不回显值」是复审后补的,在复审前的树上红)+ 金样 m05;第二节 `test/fleet-8.2.0.test.ts` ④ 12 格(8 红;4 绿 = 旧形不铸键、失败方向、类型面等反向格);第三节 ③ 4 格注释文本钉
110
+ (改前的注上红;其中心跳注那一格顺带钉「心跳体里的 `scope` 被 strip」)+ 2 格行为钉(本来就绿:注改的是话);第四节 ② 11 格(常量格在缺常量的树上红,
111
+ JSON 三格随生成器同提交、以变异自证咬合);第五节 ⑤ 4 格(基线全红)。
112
+ `test/engine-signal-8.1.0.test.ts` 两格键集钉随 additive 加键改;`test/conformance-workers-7.0.0.test.ts` 金样清单四份 → 五份。
113
+ - 变异自证(26 个变异,每个都至少使预期那一格红):没动的行跳过终判 / 判据退回 `WorkerSpec` / 手抄保留名表 / `field` 不去前缀 / 拒因措辞对调 / `original` 被拷贝;
114
+ 心跳 `leaseState` 严判 / 抄第二份 / 改词 / `EngineSignal.leaseState` 收任意串 / 心跳 `generation` 不判规范 / 丢 `leaseUntil` / `EngineSignal` 无 `leaseState`;
115
+ 能力端点回来 / 活性组二次拼写 / 少 HostPlanner / 多配额租约 / 引擎侧旧续租拼法 / 冒号参数记法 / JSON 过期;键名改字 / 第二处字面 / 能力位注回旧句。
116
+ - 全量:build rc 0 · typecheck rc 0 · vitest 84 files / 1501 tests rc 0 · e2e rc 0 · `npm pack --dry-run` rc 0(360 文件,含 `dist/fleet-paths.json`,无 docs / scripts / test / src)。
117
+ - 导出面登记表(`docs/API-SURFACE.md`)重生成:`.` 与 `./fleet` 相对 8.1.0 各 +5 名,无删无改形;表头的资产出口清单改为从 exports 表派生。
118
+
119
+ ### 未闭环(如实)
120
+
121
+ - **`EngineSignal` 今天在两个出口上都读不出**(第三节):心跳的布尔 `draining`、`/health` 就绪时缺 `ready`、引擎把世代账两键写成数而本形是串 ——
122
+ 第三条是契约与引擎的形不一致,本版只照实写进注,不改形(改 `configTargetVersion` / `configAppliedVersion` 的型是收紧 = major);收口在下一个 major
123
+ 与引擎同版翻转。
124
+ - **心跳三键之间的在场律不判**(成员 ⇒ `generation` + `leaseState`;`leaseUntil` ⇔ `granted`):是引擎的律,本形逐键判、不跨键判。
125
+ - **`LEASE_STATES` 与引擎词表的对齐是人读的**:本仓没有取引擎源码的通道,词表逐字照引擎今天的四词定,没有机器门看「引擎加词而本包没跟」。
126
+ - **迁移器的 `field` 粒度随读侧 issue 路径**:保留名拒的 `field` 是 `["env"]` / `["secretEnv"]`(读侧的保留名判据挂在整张表上),键名在 `reason` 里;
127
+ 要指到 `["env", <键>]` 得改读侧的 issue 路径(诊断面,另一件)。
128
+ - **CLI 的 `noop` 不跑整域终判**:一份不需要迁、逐行都读得进、但跨行冲突(名字重复)的文件仍报 `noop` / 退出 0(既有行为,本版未动)。
129
+
130
+ ## 8.1.0 — 2026-10-08 — 引擎自铸信号的共享型:`RestartSignal.withheld` · `EngineSignal` · 服务租约 API 型与中心能力位 · 排空请求 / 回执型 · fleet env 名常量(全部 additive)
131
+
132
+ > **判档 = minor**(README「发版纪律」:新键 / 新导出 = minor)。本版只加**型与契约**,零行为:既有字段的语义、`InstanceHealth` / `InstanceAnnouncement`
133
+ > 的形、`isReadyForTraffic` / `isRoutable` 的行为一字未动(**唯一一处校验方向的收紧**:`FLEET_GENERATION` 进保留 env 名表,见第五节——这个名字是本版才出现的,此前没有任何合法写手可被收紧,判档仍按 minor);`RestartSignal` 只多一个**可选**位(缺席 = 旧形,读出形不多铸键)。新增的是 `./fleet`
134
+ > (与主入口)上的 27 个导出名,导出面登记表里只多名字行(合计数字随之变),无删、无改形。
135
+
136
+ ### 一、`RestartSignal.withheld?`(可选布尔,缺席 = false)
137
+
138
+ 引擎自己判定「这一拍不该被换代」(需要重启,但此刻换掉这只实例会丢它手里的状态)。它是 `restart` **里面**的限定词,不是独立信号:只在 `restart` 在场时才有。
139
+ 缺席与显式 `false` 同义;读器不替生产方铸这个键。`readRestartSignal` 原样读出,心跳 `InstanceAnnouncement.restart` 与 `/health` 两个出口仍是同一只 schema。
140
+ 金样加一格带 `withheld` 的 `/health` 体(`test/restart-signal-6.1.0.test.ts`)。
141
+
142
+ ### 二、`EngineSignal` + `readEngineSignal(health)`(+ `EngineDraining`)
143
+
144
+ - `EngineSignal` = 引擎自铸的那一只对象:`{ ready, draining?: { actionId, since, deadlineAt }, restart?, generation?, instanceId?, leaseUntil?, configTargetVersion?, configAppliedVersion? }`。
145
+ 心跳与 `/health` 携带**同名键、同值**,键**平铺**在体上(没有包装键)。`restart` 引的就是 `RestartSignal` 那一只 schema,`draining` 的三键同时是排空回执的三键(`EngineDraining`)。
146
+ `generation` 是配置渲染器所设 env 的回声(缺席 ⇔ 非 fleet 成员),**规范十进制串**(见下条);**`instanceId` 是引擎每进程自铸的身份**,不读 env(同一个 Deployment 的副本共用一份 env、容器重启复用 env,env 给不了进程身份),
147
+ 单机引擎上也在场——只有 `generation` 标记 fleet 成员。
148
+ - **与 `InstanceHealth` 的关系**:`InstanceHealth` 是 `/health` 的自描述(开放记录,`.passthrough()`,也装宿主自己加的 build / pid / port / 存储姿态);`EngineSignal` 是其中**引擎自铸**那一子集的**闭形**。
149
+ 读时走默认 strip——整份 `/health` 体进去,只出这八个键。任一键形坏 ⇒ 整只信号 `undefined`(不拿剩下的充数)。
150
+ - **读口只有一只**:`readEngineSignal` 与 `readRestartSignal` / `readModelRefusals` 同走一个私有的受保护读口(原先只服务 `/health` 单键的那只,本版把「取哪个键」参数化、改名 `readSignalBody`;
151
+ 行为不变)。同一份敌意输入,三个读器同一个结局。
152
+ - **方言**:`draining` 在 `InstanceHealth` / `InstanceAnnouncement` 上是**布尔**(8.0 方言,本版不动),在 `EngineSignal` 上是**对象**。布尔 `draining` 的体读不出 `EngineSignal`(`undefined`);
153
+ `undefined` 是「没有这只信号」,**永远不是「就绪」**——路由轴上的调用方应把它当不可路由,除非有意回落到布尔方言。
154
+ `isReadyForTraffic` / `isRoutable` 的输入留在布尔方言(`EngineSignal` 过不了它们的类型,这是故意的:`!== true` 会把对象形 `draining` 读成「就绪」);下一个 major 把布尔对(`draining` / `drainingSince`)退役、两个谓词改吃 `EngineSignal`。
155
+
156
+ ### 三、服务租约 API 型
157
+
158
+ - `ServingLeaseRequest = { worker, scope?, generation, instanceId }`;
159
+ `ServingLeaseResponse` = 判别联合,**三词闭集** `granted { ttlMs }` / `denied { reason }` / `unknown`(`SERVING_LEASE_VERDICTS`);`denied` 的原因闭集恰两词 `superseded` | `retired`(`SERVING_LEASE_DENIED_REASONS`)。
160
+ - **`unknown` 不是 `denied` 的一种**:它不带 reason(带了也被剥掉,读不成 denied),表示中心此刻没有这只实例的生命周期对象(库恢复中、scope 迁移中、尚未认领)——对「该不该继续服务」什么都没说。
161
+ 只有 `denied` 臂约束引擎停服,且**只凭正面证据**:中心对 (scope, worker) 持久化一只代际下限(永不回收),低于下限 ⇒ `superseded`;`retired` 同样要有中心持有的记录;**对象缺席 ⇒ `unknown`,永不拒**。
162
+ **读器按 `verdict` 键判**(两张脸,失败方向随含义):严格线形 `ServingLeaseResponse` 说的是中心**能发什么**(原因闭集恰两词);`readServingLeaseResponse` 返回 `ServingLeaseReading`——
163
+ `verdict: "denied"` 时 `reason` 不在闭集 / 缺 / 非串 ⇒ **仍是 `denied`**,`reason` 读成占位词 `SERVING_LEASE_UNKNOWN_REASON`(`"unknown_reason"`),引擎照样自排空,
164
+ 不会因中心学会第三个原因词就把 `denied` 读成 `undefined` 而继续服务。占位词只在读出形里,生产方发不出它。判别词本身不在三词闭集、或 `granted` 的 `ttlMs` 不合 ⇒ `undefined`(读方按中心不可达处理,既不当 `denied` 也不当 `granted`)。
165
+ - **`generation` = 非负整数的规范十进制串**(`"0"`、`"7"`、`"10"`;不许 `"07"`、符号、小数、空白、非 ASCII 数字,也不许数字型),三处 wire 形(`EngineSignal.generation` / `ServingLeaseRequest.generation` / `DrainRequest.expectedGeneration`)同一条判据。
166
+ 新导出 `isCanonicalGeneration(value)`(判别器,接 `unknown`)与 `compareGeneration(a, b)`(数值比,`"9"` 早于 `"10"`,位数不限;非规范入参响亮抛 `TypeError`)——中心持久化的代际下限靠它比较。
167
+ - **`granted` 带的是时长 `ttlMs`(正整数 ms),不是绝对时刻**:引擎按「发出请求时的本地单调钟 + `ttlMs`」自己算到期,中心按库时间记自己的到期,两边不比钟(`EngineSignal.leaseUntil` 是引擎据此算出的到期)。
168
+ - 常量:`SERVING_LEASE_TTL_DEFAULT_MS = 600_000`、`SERVING_LEASE_TTL_FLOOR_MS = 60_000`、`SERVING_LEASE_DRIFT_MARGIN_MS`(**派生自** `DEFAULT_HEARTBEAT_MS`,不是第二个数)。
169
+ 配置的 TTL 过**一只**夹取函数 `clampServingLeaseTtlMs(n)`:读不出(缺 / `NaN` / 无穷 / 非数字)⇒ 缺省;低于地板(含 0 与负数)⇒ 地板;其余取整数 ms,无上限。它给**签发侧**的配置用;
170
+ 应答里的 `ttlMs` 按授予值原样使用、**不**向上夹——引擎把短授予拉长到地板,等于持有租约超过中心记下的期限,那正是围栏要防的事。
171
+ - 读器:`readServingLeaseRequest`(中心侧)/ `readServingLeaseResponse`(引擎侧),同一只受保护读口。
172
+ - **中心能力位 `CenterCapabilities = { servingLease?: true }`** + `readCenterCapabilities(body)`:租约客户端**只在位在场时启用**;缺席(老中心 / 读不出)= 中心不支持租约 ⇒ 引擎不续租、也不被围栏,
173
+ 所以执行器先于中心上线时,不会把还没有租约端点的中心逼成 `lease_expired`。位只有「`true` 或缺席」一种拼法(没有 `false`);未知位被剥,已知位的值不是 `true` ⇒ 整次读 `undefined`(读方等同缺席)。
174
+ 这是兼容门、不是裁决——形里没有「中心拒绝租约」这句话。本仓原先没有中心能力位的表达,故自立这一只;承载它的中心端点不在本包定。
175
+
176
+ ### 四、排空请求 / 回执型
177
+
178
+ - `DrainRequest = { actionId, scope?, worker, expectedGeneration?, instanceId?, deadlineAt, term? }`:身份(`scope?` / `worker` / `expectedGeneration?` / `instanceId?`)由排空口逐字段「缺席对缺席」比对——
179
+ 非 fleet 成员没有 generation,故缺 `expectedGeneration` 是单机形而不是通配;`instanceId` 在场时是引擎自铸的进程身份。本形只说什么是合式请求,身份不符是排空口的拒因。
180
+ **两条车道,一个形**:Docker 车道与中心直调用这条 HTTP 排空请求;Kubernetes 车道不逐 pod 发 HTTP(寻址与 RBAC 不合),靠**租约拒 ⇒ 引擎自排空**。
181
+ - `DrainReceipt = EngineDraining.extend({ state: "draining" | "already_draining" })`(`DRAIN_RECEIPT_STATES`),与 `EngineSignal.draining` 同一只形,不抄一份。
182
+ - 读器:`readDrainRequest` / `readDrainReceipt`。
183
+
184
+ ### 五、fleet env 名常量
185
+
186
+ `FLEET_GENERATION_ENV = "FLEET_GENERATION"`:配置渲染器设、引擎读回并回声成 `EngineSignal.generation`;**它进了保留 env 名表**(`RESERVED_WORKER_ENV`,`WorkerSpec.env` / `secretEnv` 设它即被拒,拒因指路「set by the config renderer … not configurable」),渲染器是唯一写手;两侧都 import 常量,不各自拼字符串
187
+ (整个 `src/` 里这个字面量只出现一次,有源码文本钉)。env 缺席 ⇔ 非 fleet 成员。**刻意没有实例身份的 env 名**——实例身份由引擎每进程自铸(见第二节),`src/` 里 `FLEET_INSTANCE_ID` 一次都不许出现(负钉)。
188
+
189
+ ### 用户可见变化(谁受影响 / 怎么改 / 不改会怎样)
190
+
191
+ 1. **只用 schema / 读写 helper、不碰 fleet 的消费方**:零变化。
192
+ 2. **构造或解析 `RestartSignal` 的消费方**:可以多带一个可选布尔 `withheld`;不带照旧。**对 `RestartSignal` 的键做穷举**(`Record<keyof RestartSignal, …>`)的会编译红(多一个键)——按「提货成本」处理,补一行即可。
193
+ 3. **要读引擎信号的消费方**(中心 / 壳 / 编排器):用 `readEngineSignal(health)`,不要再各自对 `health.ready` / `health.draining` 手写判断;`undefined` 当「没有信号」,不当「就绪」。
194
+ 4. **仍读 `InstanceHealth.draining`(布尔)的**:不动。但注意:一旦某个引擎版本把 `/health` 的 `draining` 铸成对象(`EngineSignal` 方言),`InstanceHealth.safeParse` 对那份体会失败——那是该引擎版本与下一个 major 之间的接线问题,本版没有替它做兼容臂。
195
+ 5. **实现租约客户端 / 排空口的引擎(server)与中心(管理面)**:用 `ServingLeaseRequest` / `ServingLeaseResponse` / `DrainRequest` / `DrainReceipt` / `CenterCapabilities` 与 `FLEET_GENERATION_ENV` 对齐形;
196
+ 租约客户端只在 `readCenterCapabilities(...)?.servingLease === true` 时启用;`instanceId` 自己每进程铸一个,不读 env;对 `verdict` 做 `switch` 时加 `default: never` 臂,以后加词编译会红;
197
+ `granted` 用 `ttlMs`(时长)按发请求时的本地单调钟算到期;应答一律走 `readServingLeaseResponse`(按 `verdict` 键判,`denied` 的原因词读不出也照样排空);`generation` 一律写规范十进制串(`"7"`),用 `compareGeneration` 比大小。
198
+ 6. **在 `WorkerSpec.env` / `secretEnv` 里设了 `FLEET_GENERATION` 的**:现在被拒(保留名,拒因指路「set by the config renderer … not configurable」)。这个名字是本版才出现的,正常部署没人设过;
199
+ 设过的删掉那一行,由渲染器按该 worker 的代际写。**持有 `RESERVED_WORKER_ENV` 总数断言的测试**(本仓的历史钉就是一例)要 +1。
200
+
201
+ ### 红先 / 门
202
+
203
+ - 先写 `test/engine-signal-8.1.0.test.ts`(含 `restart-signal-6.1.0` 的金样格)对着 8.0.0 的 `src/fleet.ts` 跑:新增 31 格里 25 格红(新导出缺席、`withheld` 被剥掉),
204
+ 另 6 格是 additive 反向格(「不带 withheld 的旧形读出形不铸键」「`InstanceHealth` 四键 / passthrough 照旧」「就绪谓词行为照旧」等),在 8.0.0 上本来就绿;
205
+ 逐步实现后同文件全绿(含 3 格源码文本钉:读口只有一只、漂移余量的定义、env 字面量单点与实例身份无 env 名)。
206
+ 之后两轮增补(第一轮:删实例身份 env 名常量、加中心能力位、两处注;第二轮:`ttlMs`、规范 `generation`、读器按 `verdict` 键判、保留 env 名与夹取函数)里,
207
+ 除中心能力位(源码先写、事后用变异自证补咬合)外,新格都先红再绿;`test/migration-signpost-7.0.1.test.ts` 里「保留名总数 = 90」的历史钉改成「90 + 登记的增词」(增词表里目前只有 `FLEET_GENERATION`)。
208
+ - 变异自证(29 个变异,每个都至少使一格红,红的是预期的那一格):`withheld` 改 `.default(false)`、`EngineSignal` 改 `.passthrough()`、`unknown` 臂加 reason、`denied` 的 reason 改可选、漂移余量改字面量、`DrainReceipt` 改成抄一份、`EngineSignal.restart` 改成第二份形、`readEngineSignal` 自带一份 try/catch、`ready` 改可选、`draining` 兼收布尔、原因 / 判别词表多一词、env 名改字或在别处再拼一份、实例身份 env 名常量回来、能力位可写 `false`、能力位自带读口 / 严判未知位、`ttlMs` 变可选、`generation` 放宽成任意非空串 / 规范串正则放过前导零、`compareGeneration` 改字典序 / 不再抛、`denied` 读脸对 reason 严判、占位词混进闭集、`FLEET_GENERATION` 从保留表摘掉、`clampServingLeaseTtlMs` 去地板 / 把读不出夹到地板等。
209
+ - 导出面登记表(`docs/API-SURFACE.md`)重生成:`.` 与 `./fleet` 相对 8.0.0 各 +27 名,无删无改形。
210
+
211
+ ### 未闭环(如实)
212
+
213
+
9
214
  ## 8.0.0 — 2026-10-04 — 一致性门第四条腿 `strictWrite`(严写受理)+ 每条腿都要表态(BREAKING,kit 类型面与 release 律)· `migrate-workers --check` 与真跑过同一只判定(BREAKING,CLI 退出码 / `refused[]`)· 导出面登记表机器门
10
215
 
11
216
  > **判档 = major**(README「发版纪律」):kit 的闭集 `LegName` 加一员、`ConformanceAdapter.legs` 从 `Partial<Record<…>>` 改成全量
package/README.md CHANGED
@@ -48,6 +48,53 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
48
48
  > a zod-3 host (today the config-center product) still dedups to one instance. Moving this package to zod 4 is a major
49
49
  > of its own (S-777); 6.2.0 does not touch the dependency or peer range.
50
50
 
51
+ ## 升级指引:8.1.0 → 8.2.0(additive + 迁移器一处行为改真)
52
+
53
+ **行为改真(一处)—— `migrateWorkersDomain` 终判逐行**:每一只有字符串名字的 worker(迁过的、没动的一样)都要读得进「只含它一行的 workers 域」
54
+ (读侧的 `WorkersConfig`,不另抄保留名表),读不进就进 `refused`;以前只判迁过的行,没动的行直接放行。
55
+
56
+ - **谁需要**:管理面的 workers 唯一读口 —— 以前一份已是 7.x 形的文档里只要一行在 `env` 里设了保留名(例:8.1.0 起的 `FLEET_GENERATION`),迁移报零变化,
57
+ 随后 `parseDomain("workers")` 整域抛,整份舰队清单读不出;现在那一行进 `refused`(名字 + `field` + 读侧原句),其余行照常加载。文件后端 / 离线的
58
+ `migrate-workers` CLI 同理:那份文件以前报 `outcome=noop` / 退出 0(说「没事」),现在报 `outcome=refused` / 退出 1、一字不写、`refused[]` 点名那一行。
59
+ - **谁受伤**:① 读口把 `refused` 当「不加载」的消费方 —— 一行以前让整域读失败(什么都不加载),现在只有它不加载;按「`refused` 行保住它正在跑的负载、不当删除」
60
+ 处理的编排器(契约要求,见 `WorkersMigrationRefusal.original`)不受影响,没这样处理的会把这一行当成缺席。② 把 `migrate-workers` 退出 0 当「文件可用」的脚本:
61
+ 这类文件现在退出 1。③ 读 `refused[].reason` 原句的:迁过的行的终判拒因从 `… not a valid 7.0.0 WorkerSpec — …` 改成 `… not a valid WorkerSpec — …`,
62
+ 没动的行的拒因前缀是 `the stored worker …`。
63
+ - **补偿**:`refused[]` 逐条带 `name` / `index` / `field` / `reason` 与原行逐字(`original`),写回前按 `index` 放回 —— 运维的 spec 不丢;修法 = 改掉 `field` 指的那一格
64
+ (保留名:删掉 `env` / `secretEnv` 里那一键,由它的唯一写位产出)。整域读侧仍是最后围栏,只剩两类归它:没有字符串名字的条目与跨行唯一性(名字 / 路由前缀)。
65
+
66
+ **additive(零动作可升)**,全在 `./fleet`(与主入口)上:
67
+
68
+ - `LEASE_STATES` / `LeaseState` —— 引擎持有的服务租约四态闭集 `none | granted | expired | denied`。
69
+ - `EngineSignal.leaseState?`;`InstanceAnnouncement` 不再剥 `generation?` / `leaseState?` / `leaseUntil?`(引 `EngineSignal` 同名键那一只 schema;形坏的那一键落成缺席,心跳照收)。
70
+ - `FLEET_PATHS`(`instances` / `usage` / `quotaLeases` / `servingLease` = `/api/fleet/live/serving-lease` / `hostPlanner` = `/api/fleet/live/host-planner/{host}`)、
71
+ `FLEET_LIVE_PATHS`(活性路由组:心跳 / 续租 / HostPlanner 读口)与新资产出口 `@sema-agent/settings-schema/fleet-paths.json`(同两只常量的 JSON,构建时写)。
72
+ **引擎侧注意**:续租路径以 `FLEET_PATHS.servingLease` 为准(`/api/fleet/live/serving-lease`),不是 `/api/fleet/serving-lease`。
73
+ - `HEARTBEAT_REPLY_CAPABILITIES_KEY = "capabilities"` —— `CenterCapabilities` 的唯一载体是心跳 2xx 答复体的这个键;位按调用方凭证答(共享令牌看不到 `servingLease`)。
74
+ - 注释改真:`EngineSignal`(今天的心跳与 `/health` 都不整只承载它 —— 心跳恒带布尔 `draining`,`readEngineSignal` 读今天的心跳体恒 `undefined`)、
75
+ `ServingLeaseRequest.scope`(续租只收 per-worker 令牌;缺席 = 令牌的 scope,在场必须相等;共享令牌不得续租)。
76
+
77
+ 提货成本:对 `EngineSignal` / `InstanceAnnouncement` 的键做穷举的消费方会因多出的键编译红;对 `LEASE_STATES` 穷举的,以后加词同理。逐条见 CHANGELOG 8.2.0。
78
+
79
+ ## 升级指引:8.0.0 → 8.1.0(全部 additive,零动作可升)
80
+
81
+ 8.1.0 只在 `./fleet`(与主入口)上**加**型与契约,零行为:既有字段的语义、`InstanceHealth` / `InstanceAnnouncement` 的形、两个就绪谓词一字未动。新增清单:
82
+
83
+ - `RestartSignal.withheld?` —— 可选布尔,引擎自判「这一拍不该被换代」;缺席 = `false`,读出形不铸键。
84
+ - `EngineSignal`(+ `EngineDraining`)与 `readEngineSignal(health)` —— 引擎自铸信号的**闭形**(`ready` 必填,`draining?` / `restart?` / `generation?` / `instanceId?` /
85
+ `leaseUntil?` / `configTargetVersion?` / `configAppliedVersion?` 可选);与既有读器走同一只受保护读口。⚠️ 8.2.0 订正:今天的心跳与 `/health` 都不整只
86
+ 承载它(心跳恒带布尔 `draining`、`/health` 就绪时不带 `ready`、世代账两键是数),`readEngineSignal` 对今天的心跳体恒 `undefined` —— 见 8.2.0 升级指引。
87
+ `undefined` = 没有这只信号,**不是就绪**;布尔 `draining` 的旧方言体读不出它(两个就绪谓词的输入仍是布尔方言,下一个 major 收)。
88
+ - 代际 —— `generation` 是非负整数的**规范十进制串**(`"7"`,不是 `"07"`、不是数字型),判别器 `isCanonicalGeneration`、数值比较 `compareGeneration`;三处 wire 形共用这一条判据。
89
+ - 服务租约 API 型 —— `ServingLeaseRequest`、`ServingLeaseResponse`(`granted { ttlMs }` / `denied { reason: superseded | retired }` / `unknown` 三词闭集,
90
+ **`unknown` 不是 `denied` 的一种**,`denied` 只凭正面证据;读器按 `verdict` 键判,`denied` 的原因词读不出时仍是 `denied`,原因读成 `unknown_reason`)、`SERVING_LEASE_TTL_DEFAULT_MS` / `SERVING_LEASE_TTL_FLOOR_MS` / `SERVING_LEASE_DRIFT_MARGIN_MS`、读器 `readServingLeaseRequest` / `readServingLeaseResponse`;
91
+ 中心能力位 `CenterCapabilities { servingLease?: true }` + `readCenterCapabilities` —— 租约客户端只在位在场时启用,缺席 = 中心不支持租约(不续租、不围栏)。
92
+ - 排空口型 —— `DrainRequest`、`DrainReceipt`(`state: draining | already_draining`)、读器 `readDrainRequest` / `readDrainReceipt`。
93
+ - `FLEET_GENERATION_ENV` —— 配置渲染器与引擎共用的 generation env 名常量(实例身份 `instanceId` 由引擎每进程自铸,没有 env 名);它同时进了 `RESERVED_WORKER_ENV`,`worker.env` / `secretEnv` 设不了它。
94
+ - `clampServingLeaseTtlMs(n)` —— 配置的租约 TTL 的唯一夹取点(读不出 ⇒ 缺省 600 000;低于地板 ⇒ 60 000;无上限)。
95
+
96
+ 提货成本只有一处:对 `RestartSignal` 的键做穷举的消费方会因多一个键编译红。逐条(谁受影响 / 怎么改)见 CHANGELOG 8.1.0。
97
+
51
98
  ## 升级指引:7.x → 8.0.0
52
99
 
53
100
  8.0.0 的破坏面只在**一致性门 kit**(`./conformance`)的类型与 release 律、以及 CLI `migrate-workers --check` 的一种结局上;契约 schema、域默认值、
@@ -110,7 +157,11 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
110
157
  `bin`; writes only when every worker migrates). Workers it cannot lift (an enabled worker with no
111
158
  `env.REMOTE_EXEC` — the migration never guesses a lane) come back in `refused` with the field to fix and the
112
159
  `original` row verbatim — they are NOT in the migrated `doc`, so a caller that writes `doc` back must put each
113
- `refused[].original` back at its `index` (or store it aside), or that worker's spec disappears.
160
+ `refused[].original` back at its `index` (or store it aside), or that worker's spec disappears. The final judgement
161
+ is per row (since 8.2.0 the same one for rows it rewrote and rows it left alone): every worker with a string name must read
162
+ as a one-row workers domain under the read side's own `WorkersConfig`, or it goes to `refused` — so each row of `doc`
163
+ reads on its own; what the whole-domain read can still refuse is a nameless entry (left as is) and the cross-row rules
164
+ (unique names / route prefixes).
114
165
  - **`migrate-workers` CLI: report shape + rejection signposts (7.0.1)** (`bin/settings-schema.mjs`, core in `cli.ts`):
115
166
  `npx @sema-agent/settings-schema migrate-workers <workers.json> [--check]` prints exactly two lines on stdout (exit 0 / 1 / 3): line 1 is the
116
167
  ONE conclusion line `outcome=<refused|applied|noop|pending> written=<true|false>`, line 2 is the JSON report (`event: "workers_domain_migrated"`,
@@ -272,7 +323,27 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
272
323
  sends it (UNKNOWN, not "none"). A name that cannot ride out is left out by the engine, judged by ONE predicate,
273
324
  `isReportableRefusalName` (the wire can carry the key — `__proto__` it cannot — and `isDisclosableIdentity`, the
274
325
  rule the dropped-entry identity on read warnings already uses: the `DISCLOSABLE_IDENTITY_RE` allowlist ∧ not
275
- secret-shaped, 6.3.0).
326
+ secret-shaped, 6.3.0). **8.1.0** (additive) puts the engine's own state in one closed shape: `RestartSignal` gains the
327
+ optional `withheld` ("the engine says this beat must not be handed to a new generation"; absent ≡ false);
328
+ `EngineSignal` (`{ ready, draining?: EngineDraining, restart?, generation?, instanceId?, leaseUntil?,
329
+ configTargetVersion?, configAppliedVersion? }`) is the closed form of the engine-minted subset of the /health
330
+ self-description (`InstanceHealth` stays the open record; neither exit carries it whole yet — see its doc comment) and
331
+ `readEngineSignal(health)` reads it through the same guarded reader as `readRestartSignal` / `readModelRefusals` —
332
+ `undefined` means "no such signal", never "ready", and a body carrying the boolean `draining` of the 8.0 dialect is not
333
+ one. The serving-lease wire (`ServingLeaseRequest` / `ServingLeaseResponse`: `granted { ttlMs }` | `denied { reason:
334
+ superseded | retired }` | `unknown` — a closed three-word set in which `unknown` is NOT a kind of `denied`; plus
335
+ `SERVING_LEASE_TTL_DEFAULT_MS` 600 000, `SERVING_LEASE_TTL_FLOOR_MS` 60 000 and `SERVING_LEASE_DRIFT_MARGIN_MS`, derived
336
+ from `DEFAULT_HEARTBEAT_MS`; `denied` rests on positive evidence only), the center capability bit `CenterCapabilities`
337
+ (`servingLease?: true` — absent means the center has no lease, so the engine neither renews nor is fenced), the drain
338
+ endpoint's `DrainRequest` / `DrainReceipt`, one reader per wire shape, and the env name `FLEET_GENERATION_ENV` are defined
339
+ here once for the renderer and the engine (an instance's id is minted by the engine per process — it has no env name).
340
+ **8.2.0** (additive) adds the engine's lease vocabulary `LEASE_STATES` (`none | granted | expired | denied`) as
341
+ `EngineSignal.leaseState?`, keeps `generation` / `leaseState` / `leaseUntil` on the heartbeat (`InstanceAnnouncement` no longer
342
+ strips them; a malformed one drops to absent, the heartbeat still lands), spells the center's fleet endpoints once —
343
+ `FLEET_PATHS` (heartbeat, usage, quota leases, serving lease `/api/fleet/live/serving-lease`, host planner
344
+ `/api/fleet/live/host-planner/{host}`) and the liveness group `FLEET_LIVE_PATHS` (heartbeat, serving lease, host planner), also
345
+ shipped as `@sema-agent/settings-schema/fleet-paths.json` for tooling that cannot import TypeScript — and names the one carrier of
346
+ the center's capability bits: `HEARTBEAT_REPLY_CAPABILITIES_KEY` (`"capabilities"`) on the heartbeat's 2xx reply.
276
347
  - **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE registry of keys this package
277
348
  carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side or
278
349
  a consumer's enforcement point). Today: `models[].compat` (1.10.0), `models[].headers` (6.4.0; the header-name
@@ -1 +1 @@
1
- 8.0.0
1
+ 8.2.0
@@ -0,0 +1,82 @@
1
+ {
2
+ "doc": {
3
+ "workers": [
4
+ {
5
+ "name": "alpha",
6
+ "enabled": true,
7
+ "host": "default",
8
+ "image": "sema-server:latest",
9
+ "secretRefs": {
10
+ "apiKeyEnv": "MODEL_KEY"
11
+ },
12
+ "runtime": {
13
+ "requirePrincipal": false,
14
+ "rateLimitRpm": 0,
15
+ "execution": {
16
+ "lane": "host",
17
+ "coords": {},
18
+ "secretRefs": {}
19
+ },
20
+ "store": {
21
+ "backend": "local",
22
+ "session": "auto"
23
+ }
24
+ },
25
+ "coords": {}
26
+ },
27
+ {
28
+ "name": "gamma",
29
+ "enabled": false,
30
+ "host": "default",
31
+ "image": "sema-server:latest",
32
+ "secretRefs": {
33
+ "apiKeyEnv": "MODEL_KEY"
34
+ },
35
+ "runtime": {
36
+ "requirePrincipal": false,
37
+ "rateLimitRpm": 0
38
+ },
39
+ "coords": {}
40
+ }
41
+ ]
42
+ },
43
+ "changed": [],
44
+ "dropped": [],
45
+ "refused": [
46
+ {
47
+ "name": "beta",
48
+ "index": 1,
49
+ "field": [
50
+ "env"
51
+ ],
52
+ "reason": "the stored worker is not a valid WorkerSpec — env: env cannot set reserved key \"FLEET_GENERATION\" — set it via (set by the config renderer from the worker's generation — not configurable)",
53
+ "original": {
54
+ "name": "beta",
55
+ "enabled": true,
56
+ "host": "default",
57
+ "image": "sema-server:latest",
58
+ "secretRefs": {
59
+ "apiKeyEnv": "MODEL_KEY"
60
+ },
61
+ "runtime": {
62
+ "requirePrincipal": false,
63
+ "rateLimitRpm": 0,
64
+ "execution": {
65
+ "lane": "host",
66
+ "coords": {},
67
+ "secretRefs": {}
68
+ },
69
+ "store": {
70
+ "backend": "local",
71
+ "session": "auto"
72
+ }
73
+ },
74
+ "coords": {},
75
+ "env": {
76
+ "FLEET_GENERATION": "7",
77
+ "LOG_LEVEL": "debug"
78
+ }
79
+ }
80
+ }
81
+ ]
82
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "workers": [
3
+ {
4
+ "name": "alpha",
5
+ "enabled": true,
6
+ "host": "default",
7
+ "image": "sema-server:latest",
8
+ "secretRefs": {
9
+ "apiKeyEnv": "MODEL_KEY"
10
+ },
11
+ "runtime": {
12
+ "requirePrincipal": false,
13
+ "rateLimitRpm": 0,
14
+ "execution": {
15
+ "lane": "host",
16
+ "coords": {},
17
+ "secretRefs": {}
18
+ },
19
+ "store": {
20
+ "backend": "local",
21
+ "session": "auto"
22
+ }
23
+ },
24
+ "coords": {}
25
+ },
26
+ {
27
+ "name": "beta",
28
+ "enabled": true,
29
+ "host": "default",
30
+ "image": "sema-server:latest",
31
+ "secretRefs": {
32
+ "apiKeyEnv": "MODEL_KEY"
33
+ },
34
+ "runtime": {
35
+ "requirePrincipal": false,
36
+ "rateLimitRpm": 0,
37
+ "execution": {
38
+ "lane": "host",
39
+ "coords": {},
40
+ "secretRefs": {}
41
+ },
42
+ "store": {
43
+ "backend": "local",
44
+ "session": "auto"
45
+ }
46
+ },
47
+ "coords": {},
48
+ "env": {
49
+ "FLEET_GENERATION": "7",
50
+ "LOG_LEVEL": "debug"
51
+ }
52
+ },
53
+ {
54
+ "name": "gamma",
55
+ "enabled": false,
56
+ "host": "default",
57
+ "image": "sema-server:latest",
58
+ "secretRefs": {
59
+ "apiKeyEnv": "MODEL_KEY"
60
+ },
61
+ "runtime": {
62
+ "requirePrincipal": false,
63
+ "rateLimitRpm": 0
64
+ },
65
+ "coords": {}
66
+ }
67
+ ]
68
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "8.2.0 逐行终判:一份已是 7.x 形的 workers 文档(三行都不需要迁),其中 beta 在 env 里设了保留名 FLEET_GENERATION(渲染器是它的唯一写手)⇒ beta 进 refused(field = env,拒因是读侧自己那句,点名保留键与它的唯一写位),alpha / gamma 原样留在 doc,changed / dropped 为空;过滤后的 doc 整域读得进。8.1.0 及以前这份文档迁移回零变化、随后整域读抛。"
4
+ }
@@ -265,14 +265,14 @@ export declare const VerifiedIdentity: z.ZodObject<{
265
265
  scope: z.ZodOptional<z.ZodString>;
266
266
  }, "strip", z.ZodTypeAny, {
267
267
  principal: string;
268
+ scope?: string | undefined;
268
269
  role?: string | undefined;
269
270
  grantId?: string | undefined;
270
- scope?: string | undefined;
271
271
  }, {
272
272
  principal: string;
273
+ scope?: string | undefined;
273
274
  role?: string | undefined;
274
275
  grantId?: string | undefined;
275
- scope?: string | undefined;
276
276
  }>;
277
277
  export type VerifiedIdentity = z.infer<typeof VerifiedIdentity>;
278
278
  /** The verifier's total result — exactly one of ok/error. */
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { formatConfigPath, sanitizeZodError } from "./config-fns.js";
21
21
  import { MIGRATE_WORKERS_COMMAND } from "./execution-lane-index.js";
22
+ import { issueText } from "./issue-text.js";
22
23
  import { migrateWorkersDomain } from "./migrate-workers.js";
23
24
  import { DOMAIN_SCHEMAS } from "./types.js";
24
25
  const USAGE = [
@@ -69,11 +70,11 @@ function verdictOf(m) {
69
70
  refused: issues.map((i) => {
70
71
  const [head, index, ...rest] = i.path;
71
72
  if (head !== "workers" || typeof index !== "number")
72
- return { name: null, field: i.path, reason: i.message };
73
+ return { name: null, field: i.path, reason: issueText(i) };
73
74
  const entry = workers[index];
74
- return { name: isRecord(entry) && typeof entry.name === "string" ? entry.name : null, field: rest, reason: i.message };
75
+ return { name: isRecord(entry) && typeof entry.name === "string" ? entry.name : null, field: rest, reason: issueText(i) };
75
76
  }),
76
- why: `the migrated document does not read as a 7.0.0 workers domain — ${issues.map((i) => `${formatConfigPath(i.path)}: ${i.message}`).join("; ")}`,
77
+ why: `the migrated document does not read as a 7.0.0 workers domain — ${issues.map((i) => `${formatConfigPath(i.path)}: ${issueText(i)}`).join("; ")}`,
77
78
  };
78
79
  }
79
80
  const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
@@ -0,0 +1,14 @@
1
+ {
2
+ "FLEET_PATHS": {
3
+ "instances": "/api/fleet/instances",
4
+ "usage": "/api/fleet/usage",
5
+ "quotaLeases": "/api/fleet/leases",
6
+ "servingLease": "/api/fleet/live/serving-lease",
7
+ "hostPlanner": "/api/fleet/live/host-planner/{host}"
8
+ },
9
+ "FLEET_LIVE_PATHS": [
10
+ "/api/fleet/instances",
11
+ "/api/fleet/live/serving-lease",
12
+ "/api/fleet/live/host-planner/{host}"
13
+ ]
14
+ }