@sema-agent/settings-schema 15.1.0 → 16.0.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +60 -46
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c16-deep-nesting-205/expected.json +2 -2
  5. package/conformance/cases/c16-deep-nesting-205/meta.json +1 -1
  6. package/conformance/cases/c50-s784-lane-host-legal/expected.json +6 -2
  7. package/conformance/cases/c50-s784-lane-host-legal/input.json +3 -1
  8. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +6 -2
  9. package/conformance/cases/c51-s784-lane-e2b-legal/input.json +3 -1
  10. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +6 -2
  11. package/conformance/cases/c52-s784-lane-k8s-legal/input.json +3 -1
  12. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +6 -2
  13. package/conformance/cases/c53-s784-lane-ssh-legal/input.json +3 -1
  14. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +6 -2
  15. package/conformance/cases/c54-s784-lane-adb-legal/input.json +3 -1
  16. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +6 -2
  17. package/conformance/cases/c55-s784-lane-local-docker-legal/input.json +3 -1
  18. package/conformance/cases/c56-s784-lane-device-legal/expected.json +8 -2
  19. package/conformance/cases/c56-s784-lane-device-legal/input.json +5 -2
  20. package/conformance/cases/c57-s784-g1-enabled-no-lane/meta.json +1 -1
  21. package/conformance/cases/c58-s784-g2-multitenant-host/input.json +3 -1
  22. package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +3 -1
  23. package/conformance/cases/c60-s784-g3-e2b-missing-key/input.json +3 -1
  24. package/conformance/cases/c61-s784-g3-other-lane-key/input.json +3 -1
  25. package/conformance/cases/c62-s784-g7a-session-tidb/input.json +3 -1
  26. package/conformance/cases/c63-s784-host-two-replicas/input.json +3 -1
  27. package/conformance/cases/c64-s784-multitenant-local-store/input.json +3 -1
  28. package/conformance/cases/c65-s784-pg-without-db/input.json +3 -1
  29. package/conformance/cases/c66-s784-memory-backend-mismatch/input.json +3 -1
  30. package/conformance/cases/c66-s784-memory-backend-mismatch/meta.json +1 -1
  31. package/conformance/cases/c68-s784-device-local-store/input.json +6 -1
  32. package/conformance/cases/cli04-env-value-with-newline/expected.json +6 -2
  33. package/conformance/cases/cli04-env-value-with-newline/input.json +3 -1
  34. package/dist/cli.d.ts +1 -1
  35. package/dist/config-d-file.d.ts +41 -5
  36. package/dist/config-d-file.js +105 -22
  37. package/dist/config-document-refusal.d.ts +39 -0
  38. package/dist/config-document-refusal.js +2 -0
  39. package/dist/device-relay.d.ts +21 -0
  40. package/dist/device-relay.js +23 -0
  41. package/dist/env-model-table.d.ts +2 -9
  42. package/dist/env-model-table.js +6 -14
  43. package/dist/execution-lane-index.d.ts +21 -17
  44. package/dist/execution-lane-index.js +28 -20
  45. package/dist/execution-lanes.d.ts +20 -5
  46. package/dist/execution-lanes.js +31 -7
  47. package/dist/file-store.d.ts +30 -3
  48. package/dist/file-store.js +89 -32
  49. package/dist/fleet.d.ts +6 -8
  50. package/dist/fleet.js +5 -7
  51. package/dist/hooks.d.ts +1 -1
  52. package/dist/index.d.ts +2 -1
  53. package/dist/index.js +4 -3
  54. package/dist/migration-receipt.d.ts +1 -1
  55. package/dist/migration-receipt.js +1 -1
  56. package/dist/one-time-migrations.d.ts +1 -1
  57. package/dist/one-time-migrations.js +166 -45
  58. package/dist/remote-exec.js +11 -4
  59. package/dist/safety-merge-spec.d.ts +1 -1
  60. package/dist/safety-merge-spec.js +1 -1
  61. package/dist/types.d.ts +15 -11
  62. package/dist/types.js +100 -49
  63. package/dist/url-shape.d.ts +18 -0
  64. package/dist/url-shape.js +22 -0
  65. package/docs/API-SURFACE.md +18 -7
  66. package/docs/CONFORMANCE.md +1 -7
  67. package/docs/CONSUMER-INTEGRATION.md +4 -4
  68. package/llms-full.txt +27 -25
  69. package/llms.txt +10 -8
  70. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,232 @@
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 16.0.0 — 2026-10-30 — major:worker 的执行车道由单值改为有序车道集 `runtime.execution.lanes` · device 车道的设备入口 `DEVICE_RELAY_URL` 必需且有值文法 · 记忆持久面方言词 `tidb` → `mysql` · 退役模型面组拒码 `config.summarize_model_missing` · `HostStatus.ts` 判 RFC 3339(加可选 `heartbeatMs`)· 本地配置单读口 `FileConfigStore.read()`(过深的文档是文件层的拒)· 本地文件契约 `RemoteExecSpec` 的非空串字段:只含空白 = 空 ⇒ 拒 · 一次性迁移 SS-M12
12
+
13
+ > 档位(按发版纪律):**major** —— 七处硬破坏(B1–B7,逐条三问在下),加性若干。发版纪律按消费方在**运行期**与**编译期**的可见性判档。
14
+ > 每一件先写律:一件事只有一处说法、一种读法 —— 本版每条都是把「同一件事两种说法」收成一种,规则变少,不是变多。
15
+ > 导出面:包根 +7(`DEVICE_RELAY_NONE` / `DEVICE_RELAY_RULE` / `deviceRelayFlaws` / `wsUrlFlaws` / `WS_URL_SHAPE_RULE` / `WsUrlFlaw` / `DIAL_ADDRESS_URL_FORBIDDEN_PARTS`)−1(`SUMMARIZE_MODEL_MISSING_CODE`);
16
+ > `./fleet` −1(同名);`./node` +1(`FileConfigRead`)−2(`inspectBundle` / `BundleInspection`);导出面登记表合计 1468 → 1472 名。零别名、零双读、零过渡读法 —— 消费方的 tsc 红就是通知;存下来的数据另配一只一次性迁移 SS-M12。
17
+
18
+ ### BREAKING 清单(逐条三问 + 下游动作)
19
+
20
+ **B1 · 执行车道:单值 `runtime.execution.lane` → 有序车道集 `runtime.execution.lanes`。**
21
+ 律:**一台 worker 声明的是它启用的车道集,不是一条车道** —— 根会话各自绑定其中一条,首项是缺省。
22
+ 形:`lanes` = 车道词的非空、去重有序表(`["k8s", "device"]`;首项缺省)。键归属按集判:`coords` / `secretRefs` 的每个键归表里**某一条声明的**车道
23
+ (归表外车道 ⇒ 点名它属于哪条车道);每条声明的车道各自的必需键都要在场且非空白;多租户姿态(`requirePrincipal`)对每条不分租户的车道各拒一条(落在 `lanes` 的那一格);
24
+ 「只能一个副本」读车道表新列 `ExecutionLaneRow.singleReplica`(host、device 为真:会话的活挂在一个副本上)—— 声明了这种车道的 worker 副本数 > 1 拒一条、点名这些车道,写闸不再按 `host` 这个词判;
25
+ `device ∈ lanes` ⇒ 要真开得出 SQL 池的店(同 15.x 的 device 规则)。旧单值 `lane` 是墓碑:读写两面拒,拒句点名新形与一次性迁移 SS-M12。
26
+ 保留名表里 `REMOTE_EXEC` / `REQUIRE_EXECUTION_LANE` 的归属字段改指 `runtime.execution.lanes`。
27
+ **谁需要**:执行车道从「引擎进程的属性」降为「根会话的放置」之后,部署声明的是一台引擎启用哪几条车道(device 与 k8s 共存是第一个真实形),单值装不下。
28
+ **谁受伤**:① 存下来的每一行 15.x workers(都带 `lane`)—— 整域读函数抛、`readWorkersRows` 扣下;② 按 `.lane` 读规格的代码(tsc 红);③ 渲染器:`REMOTE_EXEC` 改写成按序的逗号表(`lanes.join(",")`,首项缺省);
29
+ ④ 引擎侧:把 `REMOTE_EXEC` 当一个词读的引擎读不了两条车道的表 —— 引擎与渲染器须同窗改读有序表。
30
+ **补偿**:跑一次性迁移(`npx @sema-agent/settings-schema migrate <config.d directory>` 或 `migrateStoredDocuments`):SS-M12 把 `lane: L` 改成 `lanes: [L]`,一元表的读法与旧单值逐位同。
31
+
32
+ **B2 · device 车道的设备入口 `DEVICE_RELAY_URL` 是必需坐标,值有文法。**
33
+ 律:**设备拨哪里是部署的必填声明,只有两种合法值** —— 一个 ws(s) 绝对 URL(不带 userinfo / query / fragment:引擎把它逐字发布给设备,地址里没有凭据的位置),或字面 `none`(没有中继,设备拨引擎自己的地址)。
34
+ 车道表 device 行 `requiredEnv` = `["DEVICE_RELAY_URL"]`(此前它是 device 臂读而表漏登记的键;写在 env 逃生口里的 15.x 写闸照收)。值的唯一判官 `deviceRelayFlaws(value)`(包根,纯函数):合格 ⇒ `[]`,
35
+ 否则按判序交回 `not-ws-url` / `userinfo` / `query` / `fragment`;规则句 `DEVICE_RELAY_RULE`;`none` 只认这一种拼法。写闸按坐标文法表对有文法的坐标问它(拒句 `coords.DEVICE_RELAY_URL <规则> — found: <词>`,
36
+ 不回显值);值里认得出凭据形的片段照旧由坐标的密钥判据拒。缺席 / 空白走必需坐标的同一句(`lane "device" requires coords.DEVICE_RELAY_URL …`)。
37
+ ws(s) 的形判官 `wsUrlFlaws` 与这个座位禁的部件集 `DIAL_ADDRESS_URL_FORBIDDEN_PARTS` 住在 URL 形判官模块(本包判 URL 的正则只在那一处)。`RESERVED_WORKER_ENV` 多一名 `DEVICE_RELAY_URL`。
38
+ **谁需要**:引擎缺席即拒起;表不登记这一键,写闸、中心表单与部署清单就放行一台起不来的 device worker。
39
+ **谁受伤**:存下来的 device 行没有这一键(SS-M12 拒、原行留着 —— 迁移不在 URL 与 `none` 之间替运维猜);把它写在 `env` 里的 15.x 行;任一处把它写成别的形状的值。
40
+ **补偿**:给 device 行写 `coords.DEVICE_RELAY_URL`(经中继拨 ⇒ 那个 ws(s) URL;直拨引擎 ⇒ `none`)再跑迁移;写在 `env` 里的,SS-M12 搬进 `coords`(声明了 device 的行)或丢掉(没声明 device 的行:引擎从没读它)。
41
+ **部署序:先补、后升级** —— 15.2.x 的写闸收 `env.DEVICE_RELAY_URL`(那时它不在车道表里),旧版本下先给每个 device 行补上,升级时 SS-M12 搬进 `coords`,没有读不出的窗。
42
+
43
+ **B3 · 记忆持久面方言词 `tidb` → `mysql`(`MEMORY_ENGINE_BACKENDS` = `file` / `pg` / `mysql`)。**
44
+ 律:**MySQL 族那只 SQL 店只有一个字**,与店词 `store.backend: "mysql"` 同字。`runtime.store.memory.backend: "tidb"` 读写两面拒,拒句点名 `mysql` 与 SS-M12;方言与店的配对规则不变(`pg` 要 pg 店,`mysql` 要 mysql 店)。
45
+ **谁需要**:同一件事三个字(店词、记忆方言、引擎内部的会话店值)让每个读者各自翻译。**谁受伤**:存下来写着 `tidb` 的行;部署环境里 `MEMORY_ENGINE_BACKEND=tidb`(引擎直接读本表的词,提货后拒起)。
46
+ **补偿**:SS-M12 把存下来的 `tidb` 改写成 `mysql`(6.x 行的 `env.MEMORY_ENGINE_BACKEND=tidb` 由 SS-M4 一并写成 `mysql`);部署环境变量改写成 `MEMORY_ENGINE_BACKEND=mysql`。
47
+
48
+ **B4 · 模型面组拒码 `config.summarize_model_missing` 退役(不重定义),写者计划助手的 `fix_first` 结局一并删。**
49
+ 律:**闭集里只列真会被铸出来的码**。角色席点名目录里没有的条目时,引擎把它读成未设席、按回退链落下一级,没有哪种读法因 summarize 悬空而拒组 —— 列着它就是说一件不成立的事。
50
+ `MODEL_PLANE_REFUSAL_CODES` 五码(其余次序不动);`SUMMARIZE_MODEL_MISSING_CODE` 从包根与 `./fleet` 删。心跳 / `/health` 上若还带着它,读口按集外码丢掉(诊断键的失败方向 = 不说)。
51
+ `planEnvModelTableWrite` 的结局删 `fix_first` 一臂:summarize 悬空照样写在回执里(`dangles`),要确认的写法照旧把它给用户看;`envModelTableWriteRefusal` 只收 `confirm` 计划。
52
+ **谁需要**:按码渲染的消费方不再为一个不会出现的码写臂。**谁受伤**:import 这个常量或对 `fix_first` 分支的代码(tsc 红)。**补偿**:删那一臂;悬空的 summarize 从回执里读。
53
+
54
+ **B5 · `HostStatus.ts` 判 RFC 3339 时刻(此前只判非空串)。** 加性同节:`HostStatus.heartbeatMs?`(正整数毫秒,上报方声明的心跳节律,与实例公告的同名字段同一个义;缺席 = 没声明,**没有缺省**;多久没报算不新鲜是中心的判,本包只给字段)。
55
+ 律:**注释说是时刻的字段就判成时刻**。**谁需要**:读方按时刻比较新鲜度;`Date.parse` 读成 NaN 的串不该过写闸。**谁受伤**:上报非 RFC 3339 时刻的宿主状态写入被拒。**补偿**:上报 `new Date().toISOString()` 形。
56
+
57
+ **B6 · 本地配置的单读口 `FileConfigStore.read()`(`./node`)取代 `inspectBundle` / `BundleInspection`;过深的文档是一份文件层被拒的文档。**
58
+ 律:**一次读,一个结局**。`read(): Promise<FileConfigRead>`:每份文档只读一次 —— 文件层都读得进 ⇒ `{ kind: "effective", effective, warnings }`(effective 与严读 `getEffective()` 逐位同,读侧告警交成数据、不经 `onWarning`);
59
+ 有文档被拒 ⇒ `{ kind: "refused", refusals, version, warnings }`(拒单形、次序与版本与 15.2.0 的目录口同)。读得出却过不了闸域 schema 的拒照严读抛(不是文件层)。
60
+ 深度:一份文档嵌得比整棵目录的内容版本能算的深(根下 199 层)⇒ 文件层拒,闭集词 `CONFIG_DOCUMENT_REFUSAL_REASONS` 多一个 `too_deep`(墓碑仍在末位)—— 此前在算版本那一步抛,已收集的拒全部丢掉。
61
+ 严读(`getEffective` / `readBundle`)对过深的文档抛文件层那一句(点名文件路径);宽容读把它落进 `domainErrors`;一致性语料 c16 的本地腿拒在 `parse`(此前 `hash`)。
62
+ **谁需要**:要「读得进就要 effective、读不进就要拒单」的本地读腿 —— 此前只能严读一次、抛了再问目录口一次,两读之间文档被修好时这一拍是过期诊断。
63
+ **谁受伤**:import `inspectBundle` / `BundleInspection` 的代码(tsc 红);对 `ConfigDocumentRefusalReason` 穷举的 `switch`(多一个词,tsc 红);按「hash 层拒」认过深文档的读者。
64
+ **补偿**:改调 `new FileConfigStore(root).read()`,按 `kind` 分两支;`switch` 补 `too_deep` 一臂。
65
+
66
+ **B7 · 本地文件契约 `RemoteExecSpec`(`config.d/remote-exec.json`)的非空串字段:只含空白 = 空 ⇒ 拒。**
67
+ 律:**「空白」只有一种读法** —— 文件契约里每个非空串字段(沙箱镜像 / 命名空间 / 主机 / 用户名 / 设备序列号 / 工作目录 / 挂载路径 / shell / 模板 / 桶 … 共二十一处,此前凡拒空串的字符串字段)问的是写闸对必需坐标问的**同一只**判官 `isBlankCoordinate`:
68
+ 空串或只含空白(`trim()` 认的空白:空格 / 制表 / 换行 / 不换行空格 / 字节序标记 …)一律拒。此前同一个部署声明(沙箱镜像、命名空间)在写闸里「纯空白 = 缺」,在文件里却只拒空串、收下 `" "` —— 同一个包里两种读法。
69
+ 空串与纯空白现在是**同一条拒、同一句话**(`must not be empty or whitespace-only — give it a real value (an optional key can simply be left out)`),issue 指到那一格,不回显值;
70
+ 此前空串的拒句是 zod 内建的 `too_small` 那一句,现在换成这一句 —— 被拒的集合只增不减,但句子与 issue 码变了。判官只判「是不是空白」:非空白的值(含前后带空白的)逐字承运,不 trim;NUL 仍是它自己的一句。
71
+ **谁需要**:本地配置模式下引擎起服读 `config.d/remote-exec.json` 的部署 —— 把一个空白当镜像 / 命名空间去起容器或拉 pod,是起服之后才响的坏声明;同一件事写闸(workers 的必需坐标)早已拒。
72
+ **谁受伤**:`config.d/remote-exec.json` 里把任一非空串字段写成纯空白的部署声明(病态输入):`loadRemoteExec` 抛(读文件的引擎起服拒)、`validateRemoteExec` / `parseRemoteExecDocument` 拒、`writeRemoteExec` 不落盘;
73
+ 按 zod 的 `too_small` 码认「空串」的 issue 读者(读码不读路径的)—— 空串的拒现在也是这一句 custom issue。
74
+ **补偿**:删掉那一键(可选字段:缺席 = 缺省行为),或给真值(必填的 `image` / `namespace` / `host` / `username` / `serial`:只能给真值)。没有迁移 —— 空白值没有「正确的改写」,替运维猜只会把错藏起来。
75
+
76
+ ### 一次性迁移 SS-M12(workers 7.0–15.x 行 → 16.0.0 形)
77
+
78
+ 只认形、不认义、幂等:`lane: L` ⇒ `lanes: [L]`(moved,原位改名);`store.memory.backend: "tidb"` ⇒ `"mysql"`(rewritten);`env` 里的 `DEVICE_RELAY_URL` ⇒ 声明了 device 的行搬进 `coords`(moved),没声明的丢(dropped-inert)。
79
+ 拒且原行留盘(`readWorkersRows` 读时扣下):`lane` 与 `lanes` 并存、声明了 device 却把 `DEVICE_RELAY_URL` 写在 `secretEnv`(值读不到)或 `env` 与 `coords` 两处都写、改写后仍读不进的行(没有 `DEVICE_RELAY_URL` 的 device 行在内)。
80
+ 与 SS-M4 在同一个 workers 行步里做:先 SS-M12 改、再 SS-M4 改,两只都改完才用读侧那一只判一次(一行可以同时带 7.0+ 的单值车道与 6.x 的店字段;各判各的会互相卡死),
81
+ 改完读不进的那一张拒记在改了这一行的最新那只迁移名下;SS-M4 的产出直接是 16.0.0 形;窗 = 发布日 + 30 天,登记在 `ONE_TIME_MIGRATIONS` 一行。拒因里的指路点名**搬这个键的最新一只迁移**:`DEVICE_RELAY_URL` 指 SS-M12,其余车道 / 店键仍指 SS-M4(同一条命令跑整族)。
82
+
83
+ ### 文档
84
+
85
+ 随包文字说真话:7.0.0 的 workers 一次性抬升函数与它的文件子命令 11.0.0 就删了,llms 两档、接入档入口表、一致性门档的目录树与 README 仍有现在时的宣传 —— 改成「已删」并指向一次性迁移族;
86
+ `FileConfigStore.getDomain` 的注释把纯空白文件算进缺席,实际读法抛(纯空白 = 半写,坏文件)—— 注释改真。
87
+
88
+ ### 钉
89
+
90
+ 新:`test/lane-set-16.0.0.test.ts`(车道集形 · 去重 / 空 / 墓碑 · 键归属按集 · 每条声明车道的必需键 · 多租户逐车道 · `singleReplica` 列 · device 店规则按集)、`test/device-relay-16.0.0.test.ts`(表行 · 判官真值表 · 写闸同答 · 凭据形片段 · 拒句不回显值)、
91
+ `test/memory-backend-mysql-16.0.0.test.ts`、`test/summarize-code-retired-16.0.0.test.ts`、`test/host-status-heartbeat-16.0.0.test.ts`、`test/config-read-one-16.0.0.test.ts`(单读口 · 文件层拒交成数据 · 深度闸收进文件层 · 边界 199 / 200)、
92
+ `test/one-time-migration-ss-m12-16.0.0.test.ts`、`test/docs-rot-16.0.0.test.ts`、`test/exec-str-blank-16.0.0.test.ts`(文件契约字符串叶子从 schema 现算 · 空串与纯空白同一句 · 与写闸必需坐标对拍六对 · 只拒不改 · 读写入口同答);随形改的旧钉逐格翻到新形(车道夹具一元表、拒因落点 `lanes.<i>`、方言词、码闭集、迁移 id 闭集、指路律读两只迁移的读数、目录口的钉经单读口交出、变异靶点随新拼法)。
93
+
94
+ 另:`docs/API-SURFACE.md` 随导出面重生成;一致性语料 c50–c68 / cli04 的输入改成车道集形(device 两条补 `DEVICE_RELAY_URL: "none"`)并重新加冕,c16 本地腿的拒层改 `parse`。
95
+
96
+ **自 v15.2.0 起运行期代码有改动的公开导出(机器重算;上界 —— 可达 ≠ 一定变,不可达 ⇒ 一定不变;本版新增的导出不入表)**
97
+
98
+ ```text
99
+ CONFIG_DOCUMENT_REFUSAL_REASONS [.]
100
+ DOMAINS [., ./types]
101
+ DOMAIN_SCHEMAS [., ./types]
102
+ EXECUTION_LANES [., ./execution-lanes]
103
+ FileConfigStore [./node]
104
+ HostStatus [., ./types]
105
+ MEMORY_ENGINE_BACKENDS [., ./execution-lanes]
106
+ MODEL_PLANE_REFUSAL_CODES [., ./fleet]
107
+ ModelPlaneRefusalCode [., ./fleet]
108
+ NON_PORTABLE_DOMAINS [./conformance]
109
+ ONE_TIME_MIGRATIONS [./one-time-migrations]
110
+ ONE_TIME_MIGRATION_IDS [./one-time-migrations]
111
+ RESERVED_WORKER_ENV [., ./types]
112
+ RemoteExecSpec [., ./remote-exec]
113
+ WorkerSpec [., ./types]
114
+ WorkersConfig [., ./types]
115
+ buildEffective [., ./config-fns]
116
+ deriveLocalExpectation [./conformance]
117
+ deriveWireExpectation [./conformance]
118
+ domainFilePath [./node]
119
+ emptyEffective [., ./config-fns]
120
+ envModelTableWriteRefusal [.]
121
+ foreignKeysOf [./conformance]
122
+ isDomainName [., ./types]
123
+ loadLocalConfig [./node]
124
+ loadRemoteExec [./node]
125
+ migrateConfigDirectory [./node]
126
+ migrateConfigDocuments [./node]
127
+ migrateEffectiveSnapshot [./one-time-migrations]
128
+ migrateEnvModelsDecision [./node]
129
+ migrateStoredDocuments [./one-time-migrations]
130
+ parseDomain [., ./config-fns]
131
+ parseDomainLoud [., ./config-fns]
132
+ parseDomainSalvaged [., ./config-fns]
133
+ planEnvModelTableWrite [.]
134
+ readBundle [./node]
135
+ readEffectiveWire [., ./config-fns]
136
+ readModelRefusals [., ./fleet]
137
+ readWorkersRows [.]
138
+ resolveLegExpectation [./conformance]
139
+ runConformance [./conformance]
140
+ urlSeatPatterns [./one-time-migrations]
141
+ validateDomain [./node]
142
+ validateRemoteExec [./node]
143
+ writeBundle [./node]
144
+ writeDomainFile [./node]
145
+ writeRemoteExec [./node]
146
+ ```
147
+
148
+ ## 15.2.0 — 2026-10-29 — minor:文件层读不出 `config.d` 文档时契约自己交类型化的拒(`inspectBundle` + 闭集词 `CONFIG_DOCUMENT_REFUSAL_REASONS`)· 车道坐标判官 `isBlankCoordinate` 也从 `./execution-lanes` 子路径取(与包根同一只)· 安全合并规格 `memoryScopes` 行的 seam 显示串改叫 `MemoryEngine`
149
+
150
+ > 档位(按发版纪律):**minor** —— 加性:新导出 7 个(包根 4:`CONFIG_DOCUMENT_REFUSAL_REASONS` / `ConfigDocumentName` / `ConfigDocumentRefusal` / `ConfigDocumentRefusalReason`;`./node` 2:`inspectBundle` / `BundleInspection`;`./execution-lanes` 1:`isBlankCoordinate`);
151
+ > 导出面登记表合计 1461 → 1468 名。没有删改的导出;既有导出的答案逐位不变 —— 唯一变的值是一个自由文本显示串(下面第三条)。无 BREAKING,无迁移动作。
152
+
153
+ - **`inspectBundle(root)`(`./node`)与闭集词 `CONFIG_DOCUMENT_REFUSAL_REASONS`(包根):文件层读不出一份 `config.d` 文档时,契约自己交类型化的拒。**
154
+ 病:契约对外读 `config.d` 只有 `readBundle` 与 `FileConfigStore`,遇到坏文档就**抛一句话**:句子不带结构化的文档名,分不出「空 / 坏 JSON / 不是对象 / 读不出」;并发读时一次只抛第一个到达的坏文档(哪份先到不确定);
155
+ retired 域文档自己读不出时更是谁也归不了属。消费方只能在严读抛了之后逐份重探、自己归属、自己铸「拒的身份」—— 契约的判据在别人那里有了第二份。
156
+ 改:**读法只有一种,两种渲染**。文件读法的内核改交结构化读数(缺席 / 在场 / 被拒 + 闭集词 + 整句 + 抛出物 + 坏文档的指纹),判据只此一处;旧的抛出口(`readBundle`、`FileConfigStore` 的各读口、`loadRemoteExec`、命令行 `migrate`)是它的渲染 ——
157
+ 抛的还是原来那个对象、那句话(`io` 是原样的 fs 错误),答案逐位不变;`inspectBundle` 是同一读数在整个目录上的渲染。
158
+ `inspectBundle(root: string): Promise<BundleInspection>`,`BundleInspection = { refusals: readonly ConfigDocumentRefusal[]; version: number }`:一次读完全部便携域与 retired 域的文档,**不抛**文件层的拒,逐份交出
159
+ `{ document, reason, error }` —— `document` = 便携域名或 retired 域名(retired 域的文档以域名相称,墓碑拒的域就是它,不另设第二个键),`error` = 与旧出口抛出来的同一句;
160
+ `reason` 是闭集词 `CONFIG_DOCUMENT_REFUSAL_REASONS`:`empty`(空 / 纯空白:半写、截断)· `invalid_json` · `not_object`(`null` / 数组 / 数 / 串 / 布尔)· `io`(在场却读不出:权限、路径是目录;只有 ENOENT 是缺席)·
161
+ `retired_domain`(retired 域文档带事实;另带 `migration`,与 `RetiredDomainError.migration` 同值,`RetiredDomainError` 的类与字段不变)。次序确定:retired 域文档在前,便携域按 `PORTABLE_DOMAINS`;每份文档只读一次(没有「先抛、再重探」的第二次读)。
162
+ `version`:这一次读到的整棵目录的内容版本,与 `FileConfigStore.getVersion()` 是同一只函数 —— 文档都干净时二者同值;被拒的文档以「坏文档的指纹」参与(与宽容读 `getEffective({ tolerant: true })` 对坏文件的版本哨兵同一只):
163
+ 同样的坏内容同一个数、坏内容变了换数(指纹取 trim 之后的文本:只差首尾空白的两份坏文件同数)、修好后回到内容版本,绝不与「文件缺席」同数(`io` 拒的指纹取 fs 错误文案的哈希,文案里可能带路径,所以它随根目录路径而不同)。
164
+ 已知边(沿用宽容读既有的哨兵形,不是本版引入):一份合法文档的内容恰好写成 `{"__corruptDomainFile":"<该坏文件的指纹>"}`,与那份坏文件同数。
165
+ 范围:只覆盖文件层与墓碑。文档读得出、却过不了域 schema(门控域判形失败)与内容哈希深度闸这类拒不在这里 —— 它们属于严读的后半段,仍由 `getEffective` 抛。
166
+ 顺带:宽容读 `getEffective({ tolerant: true })` 改读同一读数 —— 坏文件的指纹取自这一次读到的字节(此前是失败后再读一次文件,读不回时退回错误文案),答案相同,少一次读,也没有两次读之间被改的缝。
167
+ 三问 —— **谁需要**:在严读拒了一棵 `config.d` 之后要说清「哪几份文档、什么成因」的消费方(服务端的本地读腿、命令行、桌面壳):此前各自重探归属、自铸拒的身份,现在直读契约;**谁受伤**:无 —— 新导出,旧出口抛的对象与句子逐位不变;**补偿**:无需。
168
+ 钉:`test/bundle-inspection-15.2.0.test.ts`(词表 · 文件层四种拒各一格〔`empty` 两种形、`invalid_json`、`not_object` 四种形、`io`〕且句子与旧出口抛的逐字同 · 墓碑拒与 `RetiredDomainError` 字段逐项同 · retired 域文档读不出归到那一份 · 一次列全且次序确定 ·
169
+ 干净树 `version` = `getVersion()` · 被拒树版本的内容寻址 · 与宽容读同一个数 · 旧出口不变:同类同句、`io` 仍是 fs 错误 · 登记表)。
170
+
171
+ - **`isBlankCoordinate(value: string): boolean` 也从 `@sema-agent/settings-schema/execution-lanes` 取,与包根是同一个函数对象。**
172
+ 病:15.1.0 把车道坐标「空白按缺」的判官升到包根,而包根是整只契约包的桶 —— 只想问这一个问题的消费方要冷载入整桶。车道词表的叶模块(`./execution-lanes`,零 import)是现成的轻量入口。
173
+ 改:判官的函数体搬进这个叶模块(它只用 `String.prototype.trim`,零依赖),包根的名字来自叶模块的 `export *`,车道表的派生索引与写闸也从叶模块 import;原先包根上的具名转口删除 —— 全包只有一处函数体,包根与子路径是同一个函数对象。
174
+ 包根上这个名字的形与答案逐位不变,`import { isBlankCoordinate } from "@sema-agent/settings-schema"` 照旧;契约(空串或只含空白 ⇒ `true`;不 trim 值;「键没设」与可选坐标的空串由调用方判)一字不动。
175
+ 三问 —— **谁需要**:只想问「这个必需坐标算不算缺」、不愿冷载入整只契约包桶的消费方(例:服务端的叶模块、部署清单的生成器);**谁受伤**:无 —— 子路径上新增一个名字;**补偿**:无需。
176
+ 钉:`test/blank-coordinate-subpath-15.2.0.test.ts`(子路径模块与包根全等 · 与写闸逐样本同答 · 全包只有一处函数体且在叶模块 · 索引模块不再放它 · 叶模块零 import · 登记表里在 `.` 与 `./execution-lanes` 两处)、
177
+ `test/execution-lanes-7.0.0.test.ts`(真走 Node exports 解析:子路径与包根上的 `isBlankCoordinate` 是同一个对象)、`test/blank-coordinate-export-15.1.0.test.ts`(随位置改的 15.1.0 旧钉)。
178
+
179
+ - **安全合并规格 `SAFETY_MERGE_SPEC.memoryScopes.seam` 由 `MemoryStore` 改为 `MemoryEngine`。**
180
+ 病:引擎把旧的记忆店族整体退役、座位改叫 `MemoryEngine`,规格表里这一行追溯用的标签仍指向旧名。
181
+ 改:只动这一个显示串。`seam` 是自由文本(schema 是 `z.string()`),不参与任何合并规则;`memoryScopes` 行的合并形 / 信任下限 / 收紧律 / 执行面 / 失败臂逐字节不变。
182
+ 1.1.0 冻结字面量对拍新开第六处豁免(只豁免这一个显示串;冻结物里仍是 1.1.0 的旧词,它是那一版的事实)。随包的 `src/` / `docs/` / `conformance/` / README / llms 里旧词零命中。
183
+ 三问 —— **谁需要**:读 `seam` 做追溯的人与文档;**谁受伤**:对 `seam` 串做精确匹配的消费方会读到新词(它是自由文本、不是合并规则的输入,按它判规则不是契约用法);**补偿**:改读新词,或不要按显示串判。
184
+ 钉:`test/seam-label-memory-engine-15.2.0.test.ts`(新词在行上 · 整表无旧词 · 规则字段不变 · 随包文字旧词零命中)、`test/safety-merge-spec.test.ts`(1.1.0 冻结对拍的第六处豁免,含「过冻结的 1.1.0 schema 读到新词」那一格)。
185
+
186
+ 另:`docs/API-SURFACE.md`(导出面登记表)随导出面重生成,`test/api-surface.test.ts` 与生成器输出逐字比对。
187
+
188
+ **自 v15.1.0 起运行期代码有改动的公开导出(机器重算;上界 —— 可达 ≠ 一定变,不可达 ⇒ 一定不变;本版新增的导出不入表)**
189
+
190
+ ```text
191
+ DOMAINS [., ./types]
192
+ DOMAIN_SCHEMAS [., ./types]
193
+ FileConfigStore [./node]
194
+ NON_PORTABLE_DOMAINS [./conformance]
195
+ SAFETY_MERGE_SPEC [., ./safety-merge-spec]
196
+ WorkerSpec [., ./types]
197
+ WorkersConfig [., ./types]
198
+ buildEffective [., ./config-fns]
199
+ deriveLocalExpectation [./conformance]
200
+ deriveWireExpectation [./conformance]
201
+ domainFilePath [./node]
202
+ emptyEffective [., ./config-fns]
203
+ foreignKeysOf [./conformance]
204
+ httpHookEnvVarsAllowed [.]
205
+ httpHookUrlVerdict [.]
206
+ isBlankCoordinate [., ./execution-lanes]
207
+ isDomainName [., ./types]
208
+ layerAdmission [.]
209
+ loadLocalConfig [./node]
210
+ loadRemoteExec [./node]
211
+ memberAdmission [., ./safety-merge-spec]
212
+ memberMinTrust [., ./safety-merge-spec]
213
+ memberTighten [., ./safety-merge-spec]
214
+ membersOf [., ./safety-merge-spec]
215
+ migrateConfigDirectory [./node]
216
+ migrateConfigDocuments [./node]
217
+ migrateEffectiveSnapshot [./one-time-migrations]
218
+ migrateEnvModelsDecision [./node]
219
+ migrateStoredDocuments [./one-time-migrations]
220
+ parseDomain [., ./config-fns]
221
+ parseDomainLoud [., ./config-fns]
222
+ parseDomainSalvaged [., ./config-fns]
223
+ planEnvModelTableWrite [.]
224
+ readBundle [./node]
225
+ readEffectiveWire [., ./config-fns]
226
+ readSettingsUnit [.]
227
+ readWorkersRows [.]
228
+ resolveLegExpectation [./conformance]
229
+ runConformance [./conformance]
230
+ unitTighten [.]
231
+ urlSeatPatterns [./one-time-migrations]
232
+ validateDomain [./node]
233
+ writeBundle [./node]
234
+ writeDomainFile [./node]
235
+ ```
236
+
11
237
  ## 15.1.0 — 2026-10-28 — minor:车道坐标「空白按缺」的唯一判官 `isBlankCoordinate` 升为包根导出(加性:包根多一个名字,函数体与既有答案逐位不变)
12
238
 
13
239
  > 档位(按发版纪律):**minor** —— 加性:新导出一个(包根 `isBlankCoordinate`;导出面登记表 包根 761 → 762 名,合计 1460 → 1461 名)。没有新子路径、没有删改的导出;既有导出的答案逐位不变(写闸读的仍是同一个函数对象)。无 BREAKING,无迁移动作。
package/README.md CHANGED
@@ -47,6 +47,48 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
47
47
  > zod 3, nested inside every zod-4 host). The move to zod 4 did not change what any schema accepts or produces —
48
48
  > the conformance corpus reads byte-identically on both — only the wording of validation errors (see CHANGELOG 11.0.0).
49
49
 
50
+ ## 升级指引:15.2.x → 16.0.0(BREAKING:执行车道集 · 设备入口必需且有文法 · 记忆方言词 mysql · 退役一只模型面组拒码 · 宿主状态时刻判形 · 本地配置单读口 · 文件契约的纯空白字段)
51
+
52
+ - **改了什么**(逐条三问见 CHANGELOG 16.0.0):
53
+ ① worker 的 `runtime.execution.lane`(单值)换成有序车道集 `runtime.execution.lanes`(非空、去重,首项缺省);键归属、必需键、多租户与「只能一个副本」都按集判(后者读车道表新列 `singleReplica`);旧 `lane` 是墓碑。
54
+ ② device 车道的 `DEVICE_RELAY_URL` 进车道表必需栏,值只收 ws(s) 绝对 URL(无 userinfo / query / fragment)或字面 `none`;判官 `deviceRelayFlaws` / 规则句 `DEVICE_RELAY_RULE` / `DEVICE_RELAY_NONE` 在包根。
55
+ ③ `MEMORY_ENGINE_BACKENDS` = `file` / `pg` / `mysql`(`tidb` 退役,与店词同字)。
56
+ ④ `MODEL_PLANE_REFUSAL_CODES` 删 `config.summarize_model_missing` 与常量 `SUMMARIZE_MODEL_MISSING_CODE`;`planEnvModelTableWrite` 删 `fix_first` 结局(悬空的 summarize 留在回执里)。
57
+ ⑤ `HostStatus.ts` 判 RFC 3339 时刻;加可选 `HostStatus.heartbeatMs`(正整数毫秒,无缺省)。
58
+ ⑥ `./node`:`inspectBundle` / `BundleInspection` 删,单读口 `FileConfigStore.read()` → `FileConfigRead`;拒因闭集加 `too_deep`(过深的文档是一份文件层被拒的文档)。
59
+ ⑦ 本地文件契约 `RemoteExecSpec`(`config.d/remote-exec.json`)的非空串字段(镜像 / 命名空间 / 主机 / 用户名 / 序列号 / 工作目录 / 路径 …)对只含空白的值同空串一样拒 —— 问的是写闸对必需坐标问的同一只判官 `isBlankCoordinate`,空串与纯空白是同一句拒;接受的值逐字不变(不 trim)。
60
+ - **存下来的数据**:跑一次一次性迁移 —— `npx @sema-agent/settings-schema migrate <config.d directory>`,或对中心的存量调 `migrateStoredDocuments` / `migrateEffectiveSnapshot`(`./one-time-migrations`)。
61
+ SS-M12 改写 15.x workers 行(`lane: L` ⇒ `lanes: [L]`;记忆方言 `tidb` ⇒ `mysql`;`env` 里的 `DEVICE_RELAY_URL` 搬进 device 行的 `coords` 或丢掉)。
62
+ **拒且原行留着**的行要人补:没有 `DEVICE_RELAY_URL` 的 device 行(写 `coords.DEVICE_RELAY_URL`:经中继拨 ⇒ 那个 ws(s) URL,直拨引擎 ⇒ `none`)、`lane` 与 `lanes` 并存的行、声明了 device 却把 `DEVICE_RELAY_URL` 写在 `secretEnv` 或 `env` 与 `coords` 两处都写的行(没声明 device 的行里它是惰性件,迁移丢掉)。
63
+ - **部署环境**:`MEMORY_ENGINE_BACKEND=tidb` 改写成 `mysql`;device 车道的部署必须声明 `DEVICE_RELAY_URL`(URL 或 `none`)。
64
+ - **部署序(device 行):先补、后升级** —— 15.2.x 的写闸把 `DEVICE_RELAY_URL` 当普通 env 收(它那时不在车道表里),所以可以在旧版本下先给每个 device 行写 `env.DEVICE_RELAY_URL`,
65
+ 升级后 SS-M12 把它搬进 `coords`;不先补的 device 行升级后被迁移拒、原行留着,整域读函数读不出 workers 文档(按行读的 `readWorkersRows` 扣下那一行),直到补上为止。
66
+ - **各消费方的读点**:
67
+
68
+ | 消费方 | 读点 | 动作 |
69
+ |---|---|---|
70
+ | server | 本地读腿:`getEffective` 抛了再问 `inspectBundle` 的两读 | 改调 `FileConfigStore.read()` 一次读,按 `kind` 分两支;拒因 `switch` 补 `too_deep` |
71
+ | server | 记忆方言:比较 `"tidb"` 的臂、目录行说明、`/v1/capabilities` 里报的记忆后端值 | 改 `"mysql"`(类型从本包来,tsc 红指路);能力面上的值随之改字,对外是 wire 改字 |
72
+ | server | 设备入口:自持的 `DEVICE_RELAY_NONE` 与 ws(s) 形判 | 改 import 本包 `DEVICE_RELAY_NONE` / `deviceRelayFlaws`,删自己那一份;缺席判交给车道必需键门(本表 device 行已含它) |
73
+ | server | `REMOTE_EXEC` 读成一个词 | 改读有序逗号表(首项缺省),与渲染器同窗;此前读不了两条车道的表 |
74
+ | server | 模型面组拒码 | 本包这一码的读点已为零,无动作 |
75
+ | web-admin | 渲染器 `REMOTE_EXEC = execution.lane`、向导与团队形对 `execution.lane` 的读 | `REMOTE_EXEC = lanes.join(",")`(`REQUIRE_EXECUTION_LANE=true` 不变);读 `lanes`(tsc 红指路) |
76
+ | web-admin | device 行表单 | 写 `coords.DEVICE_RELAY_URL`,提示读 `DEVICE_RELAY_RULE`、判读 `deviceRelayFlaws`;「撤中继」写 `none`,不是删键 |
77
+ | web-admin | 宿主状态写入口 `HostStatus.safeParse` | 上报方的 `ts` 须是 RFC 3339;`heartbeatMs` 可选,新鲜度窗公式在中心 |
78
+ | web-admin | 存量 workers | 跑 SS-M12(同一条迁移命令 / 函数) |
79
+ | cli | 自起引擎注的 `REMOTE_EXEC=host` | 一个词即一元表,无动作 |
80
+ | cli | 由本包车道表派生的「车道 → 必需键」 | 随表:device 多 `DEVICE_RELAY_URL`(无手抄一份,提货即生效;与钉版引擎的对拍随引擎版本) |
81
+ | 部署 | 车道清单 / compose 透传 | 随车道表重生成(device 多 `DEVICE_RELAY_URL`);`MEMORY_ENGINE_BACKEND=mysql` |
82
+ | server / 部署 | 本地配置模式:`config.d/remote-exec.json` 里的非空串字段(镜像 / 命名空间 / 工作目录 / shell / 主机 …)写成纯空白 | 16.0.0 起被拒 —— `loadRemoteExec` 抛、起服拒;出路 = 删掉那一键(可选字段)或给真值(必填字段);没有迁移 |
83
+
84
+ ## 升级指引:15.1.x → 15.2.0(加性,零动作可升)
85
+
86
+ 三件,都是加性的,既有导出的答案逐位不变:
87
+ ① **文件层类型化的拒**:`@sema-agent/settings-schema/node` 多一个读口 `inspectBundle(root)` —— 一次读完 `config.d` 里契约读的每一份文档(全部便携域 + retired 域的墓碑文档),不抛文件层的拒,交 `{ refusals, version }`;每条拒 `{ document, reason, error }`,
88
+ `reason` 是包根的闭集词 `CONFIG_DOCUMENT_REFUSAL_REASONS`(`empty` / `invalid_json` / `not_object` / `io` / `retired_domain`,墓碑拒另带 `migration`),`error` 与旧出口抛出来的是同一句。次序确定,每份文档只读一次。
89
+ `readBundle` / `FileConfigStore` 等旧出口照旧抛(同一个对象、同一句话)。② **判官的子路径**:`isBlankCoordinate` 也能从 `@sema-agent/settings-schema/execution-lanes` 取(与包根是同一个函数对象),只想问这一个问题的消费方不必冷载入整只契约包桶。
90
+ ③ **显示串**:`SAFETY_MERGE_SPEC.memoryScopes.seam` 改为 `MemoryEngine`(引擎座位的新名;自由文本,不参与任何合并规则;旧名见 CHANGELOG 15.2.0)。逐条见 CHANGELOG 15.2.0。
91
+
50
92
  ## 升级指引:15.0.x → 15.1.0(加性,零动作可升)
51
93
 
52
94
  包根多一个导出 `isBlankCoordinate(value: string): boolean` —— 车道坐标「空白按缺」的唯一判官:空串或只含空白 ⇒ `true`(写闸对必需坐标一直读的就是它),不 trim 值、不管值的格式;
@@ -279,7 +321,7 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
279
321
  env-NAME ref. Every `provider` word is an execution-lane word (`EXECUTION_LANE_WORDS`, pinned at runtime and at
280
322
  compile time). **7.0.0** deleted the `remote-docker` variant: no engine read point ever ran it; a remote docker
281
323
  daemon is the `local-docker` lane pointed at it (`DOCKER_HOST`). **11.0.0**: retired lane VALUES have a tombstone,
282
- `RETIRED_LANE_WORDS` (`[old, to, how]`, permanent, one hop); a worker spec's `runtime.execution.lane` or this file's
324
+ `RETIRED_LANE_WORDS` (`[old, to, how]`, permanent, one hop); a worker spec's `runtime.execution.lanes` entry or this file's
283
325
  `provider` naming one is refused with the way out (`retiredLaneWordRefusal(word)` is the one sentence; the issue code
284
326
  stays `invalid_value` / `invalid_union`).
285
327
  - **WorkerSpec execution lane + store (7.0.0, M-R2)** (`execution-lanes.ts`, also subpath `./execution-lanes`, + `types.ts`): the lane / store
@@ -289,53 +331,24 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
289
331
  `optionalSecretEnv`, copied from the engine with per-row coordinates), `STORE_BACKENDS`, `SESSION_BACKENDS`,
290
332
  `MEMORY_ENGINE_BACKENDS`. Web-admin's write gate (this package's `WorkerSpec.safeParse`), web-admin's env
291
333
  renderer and the engine's boot checks all read that one table, so they cannot drift. A worker declares
292
- `runtime.execution = { lane, coords, secretRefs }` (non-secret coordinates as literals, secrets as host env-NAMEs,
293
- keys ⊆ that lane's columns, required ones present and not blank — an empty or whitespace-only value counts as absent) and `runtime.store = { backend, session, memory,
334
+ `runtime.execution = { lanes, coords, secretRefs }` (16.0.0: an ordered, de-duplicated, non-empty lane set — the first one is the default;
335
+ non-secret coordinates as literals, secrets as host env-NAMEs, each key owned by a declared lane, every declared lane's required ones present
336
+ and not blank — an empty or whitespace-only value counts as absent; a coordinate with a grammar — `DEVICE_RELAY_URL`, a ws(s) URL or the
337
+ literal `none` — must also match it) and `runtime.store = { backend, session, memory,
294
338
  db? }` (+ `secretRefs.dbPasswordEnv`); an enabled worker must declare both. Cross-field refusals live in the
295
339
  schema as zod issues whose `path` names the field to change and whose message carries the way out: a
296
- multi-tenant worker (`requirePrincipal`) on a lane that does not separate tenants (outlets computed from the
297
- table), the `host` lane with more than one replica (`replicas` or `autoscaling.maxReplicas`), the `device` lane
298
- without a SQL store that actually builds its coordinates (the engine's `device-durable-store`), SQL coordinates
340
+ multi-tenant worker (`requirePrincipal`) with a declared lane that does not separate tenants (one refusal per such lane; outlets
341
+ computed from the table), a lane whose row says `singleReplica` (host, device) with more than one replica (`replicas` or
342
+ `autoscaling.maxReplicas`), the `device` lane without a SQL store that actually builds its coordinates (the engine's `device-durable-store`), SQL coordinates
299
343
  without a SQL store (or the reverse), a memory backend that does not match the store, a multi-tenant worker
300
344
  without a SQL store, and the two session × store combinations the engine refuses at boot (`session: mysql` on a
301
- `memory` store; a pg / tidb memory engine with `session: memory`). `WorkerSpec` is no longer a plain object schema — the worker-name rule is exported on its
345
+ `memory` store; a pg / mysql memory engine with `session: memory`). `WorkerSpec` is no longer a plain object schema — the worker-name rule is exported on its
302
346
  own as `WORKER_NAME`. `RESERVED_WORKER_ENV` (derived from a name → owning-field table) now reserves every
303
347
  env name those blocks produce, so the `env` / `secretEnv` escape hatches can no longer set a lane or a store.
304
- Stored 6.x rows are lifted ONCE by `migrateWorkersDomain(doc)` (pure, idempotent; run it on the RAW workers
305
- document before `parseDomain` — the 6.x fields are refused loudly by the 7.0.0 schema, not silently stripped)
306
- or its file form `npx @sema-agent/settings-schema migrate-workers <workers.json> [--check]` (the package's
307
- `bin`; writes only when every worker migrates). Workers it cannot lift (an enabled worker with no
308
- `env.REMOTE_EXEC` — the migration never guesses a lane) come back in `refused` with the field to fix and the
309
- `original` row verbatim — they are NOT in the migrated `doc`, so a caller that writes `doc` back must put each
310
- `refused[].original` back at its `index` (or store it aside), or that worker's spec disappears. The final judgement
311
- 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
312
- as a one-row workers domain under the read side's own `WorkersConfig`, or it goes to `refused` — so each row of `doc`
313
- reads on its own; what the whole-domain read can still refuse is a nameless entry (left as is) and the cross-row rules
314
- (unique names / route prefixes).
315
- - **`migrate-workers` CLI: report shape + rejection signposts (7.0.1)** (`bin/settings-schema.mjs`, core in `cli.ts`):
316
- `npx @sema-agent/settings-schema migrate-workers <workers.json> [--check]` prints exactly two lines on stdout (exit 0 / 1 / 3): line 1 is the
317
- ONE conclusion line `outcome=<refused|applied|noop|pending> written=<true|false>`, line 2 is the JSON report (`event: "workers_domain_migrated"`,
318
- `outcome`, `written`, `file`, the worker lists, `refused[]`). Only `applied` writes the file (`written` is derived from `outcome`; `pending` =
319
- `--check` with changes). The list keys carry tense — `changed` / `dropped` exist **only when `written=true`**; when nothing was written
320
- (refused / noop / pending) the same lists are `wouldChange` / `wouldDrop`, so a script that reads only stdout can never take a worker that was
321
- not migrated for migrated. Exit codes are unchanged (0 applied or noop · 1 refused or the migrated document does not read as a 7.0.0 workers
322
- domain, nothing written · 2 usage / unreadable / not JSON, empty stdout · 3 `--check` with changes).
323
-
324
- | 7.0.0 stdout | 7.0.1 stdout |
325
- |---|---|
326
- | one JSON line | the conclusion line, then the JSON line (parse the **last** line) |
327
- | `changed` (always) | `changed` when `written=true`, else `wouldChange` |
328
- | `dropped` (always) | `dropped` when `written=true`, else `wouldDrop` |
329
- | — | `outcome`, `written` |
330
-
331
- **8.0.0**: `--check` and a real run go through ONE verdict (per-worker refusals → nothing to change ⇒ `noop` → the whole-domain final judgment →
332
- changes); `--check` only decides whether the "changes" branch writes (`pending`) or not. A document the whole-domain judgment refuses (two workers
333
- with one name …) is `outcome=refused` / exit 1 under `--check` too (7.x: `pending` / exit 3), and `refused[]` names the refused items in the
334
- per-worker shape (`name` / `field` / `reason`; `name` is `null` for an entry that carries no string name) instead of being empty.
335
-
336
- Rejections that a migration can lift carry ONE signpost sentence (a constant, `MIGRATION_SIGNPOST`): the seven deleted 6.x fields, and an
337
- `env` / `secretEnv` key the migration moves or drops (the execution-lane keys incl. `REMOTE_EXEC` / `REQUIRE_EXECUTION_LANE`, and the store keys
338
- `DB_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND`). A reserved name the migration cannot move (`PORT`, `MODEL_*`, `MYSQL_*` …) carries none.
348
+ Stored rows of an older form are rewritten ONCE by the one-time migrations (`./one-time-migrations`: 6.x rows by SS-M4, 15.x rows by
349
+ SS-M12 since 16.0.0; the directory form is the package's `bin`, `npx @sema-agent/settings-schema migrate <config.d directory>`) — the
350
+ read side holds a row it cannot read (`readWorkersRows`) instead of lifting it on every read. The 7.0.0 one-off lift function and its
351
+ file subcommand were deleted in 11.0.0.
339
352
  - **Safety merge-spec** (`safety-merge-spec.ts`, subpath `./safety-merge-spec`): `SAFETY_MERGE_SPEC`, the ONE
340
353
  per-category merge/enforcement table every settings resolver reads (`mergeShape` / `minTrust` / `tighten` /
341
354
  `enforcement` / `fail`) — no consumer re-implements the merge law. 1.1.0 adds **member-level admission**:
@@ -476,7 +489,8 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
476
489
  identity) and `enabledEntryCount(doc)`. The env model table read once — `readEnvModelSlots` / `envModelTableDeclared` (`MODEL_ID` present and non-empty) /
477
490
  `buildEnvModelBaseline` — and the writer's plan `planEnvModelTableWrite({ env, target, decision, candidate, intent? })` (receipt by upstream id with landings
478
491
  `{name, id}`, `confirmKey` = the receipt's sorted JSON) with its one sentence `envModelTableWriteRefusal(plan)`. Group-refusal codes
479
- `CATALOG_REPLACES_ENV_MODELS_CODE` / `SUMMARIZE_MODEL_MISSING_CODE` (tail of `MODEL_PLANE_REFUSAL_CODES`).
492
+ `CATALOG_REPLACES_ENV_MODELS_CODE` (tail of `MODEL_PLANE_REFUSAL_CODES`; the summarize code next to it was retired in 16.0.0 —
493
+ a role seat naming an entry the catalog does not serve reads as unset and falls back, so no reading refuses a model plane for it).
480
494
  - **Session web link** (`web-admin-session-url.ts`, main barrel): `webAdminSessionUrl(webBaseUrl, { worker, sessionId })` — the ONE
481
495
  template for a web-admin session link, `<webBaseUrl>/chat?agent=<worker>&session=<sessionId>`: both parts required (a link with only
482
496
  the session id lands on whichever worker the page picks and may not find it), each part `encodeURIComponent`-escaped, the base a
@@ -633,7 +647,7 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
633
647
  unchanged); `readModelRefusals(body)` reads either exit and narrows to `ModelRefusalsSignal` — the codes are CLOSED
634
648
  sets owned here, `MODEL_ENTRY_REFUSAL_CODES` (`config.model_no_brain`, `config.model_entry_invalid`) and
635
649
  `MODEL_PLANE_REFUSAL_CODES` (`config.default_model_missing`, `config.model_refused_name_minted`,
636
- `config.model_route_endpoint_missing`, `config.gateway_fallback_without_primary`), and a code this package version
650
+ `config.model_route_endpoint_missing`, `config.gateway_fallback_without_primary`, `config.catalog_replaces_env_models`), and a code this package version
637
651
  does not know is dropped ("not said"). It is a diagnostic key, so its failure direction is "not said": an unknown
638
652
  code or a shape violation on the heartbeat never fails the announcement (the key drops to absent). Present ⇔ at
639
653
  least one reportable refusal; absence is read by the announced `version` — an engine that predates the key never
@@ -754,7 +768,7 @@ defaults. Each `config.d/<domain>.json` is exactly that domain's zod object.
754
768
  自 `1.0.0`(改名)起本包按**标准 semver** 发版;`0.x` 时代的「破坏性变更必 bump minor」规矩**已撤**
755
769
  (末条仅作历史注记)。判档看**消费方可见性**,不看 diff 大小:
756
770
 
757
- - **CLI 报告形的契约边界(7.0.1 起)**:`migrate-workers` 的 stdout = 结论行 + JSON;其中 `outcome` / `written` / `refused` 三字段是机读契约(再改按 minor),其余键与行形是诊断面,patch 可变(test 认定口径 2026-10-03:CLI 报告形不在 C-R197 包导出面)。机读请取最后一行解析。
771
+ - **CLI 报告形的契约边界**:`migrate` 的 stdout = 结论行 + JSON;其中 `outcome` / `written` / `refused` 三字段是机读契约(再改按 minor),其余键与行形是诊断面,patch 可变(CLI 报告形不在包导出面)。机读请取最后一行解析。
758
772
  - **破坏性变更必 bump major**:字段/导出改名、改形(retype)、删除、默认值变更、校验收紧或放宽
759
773
  (例:`.passthrough()` → 默认 strip、宽读 → 严判)、枚举**重排**——凡是能让一个按上一版契约写的
760
774
  消费方在**运行期**变红、或让同一份配置解析出不同结果的改动;编译期同判,**唯一裁定例外** = 下一条的
@@ -770,7 +784,7 @@ defaults. Each `config.d/<domain>.json` is exactly that domain's zod object.
770
784
  但标记不豁免版号义务。每版 CHANGELOG 一节,git tag `v<version>` 同名。
771
785
  - **持久数据面的破坏性变更(例:7.0.0)**:类型 / API 面直接删改、零别名(消费方 tsc 红即通知);**存下来的数据**不会自己红,
772
786
  所以同版配三件:读到旧形即**响亮拒**(拒因指路迁移命令,不是被 zod 静默剥掉)、一只**一次性迁移**(纯函数、幂等、
773
- 表达不了就 refused 点名,不替运维猜)、以及一个**写明的删除窗**(7.0.0 的 `migrateWorkersDomain` 在 web-admin 接上迁移后的下一版删除)——
787
+ 表达不了就 refused 点名,不替运维猜)、以及一个**写明的删除窗**(窗只登记在一次性迁移的登记表 `ONE_TIME_MIGRATIONS`;7.0.0 那一只一次性抬升函数已在 11.0.0 删)——
774
788
  绝不长成长命双读。
775
789
  - **历史注记(0.x 时代)**:0.x 下 `^0.7.0` 这类 range 只自动收 patch,故当年规矩是「破坏性必 bump
776
790
  minor,patch 只装 additive/修 bug/文档」。0.7.1(2026-07-10)曾携带两件预协调 BREAKING(撤 governance
@@ -1 +1 @@
1
- 15.1.0
1
+ 16.0.0
@@ -101,7 +101,7 @@
101
101
  },
102
102
  "localRoot": {
103
103
  "outcome": "rejected",
104
- "stage": "hash",
105
- "why": "本地腿的 version 是全量文件的内容哈希,200 层深度闸在它那里先炸;domains/wire 两腿不哈希"
104
+ "stage": "parse",
105
+ "why": "本地腿读的是文件:16.0.0 起一份文档嵌得比整棵目录的内容版本能算的深(根下 199 层)是这一份文档的文件层拒(`too_deep`,在读文档那一步),不再等到算内容哈希时才抛;domains/wire 两腿不读文件、不哈希"
106
106
  }
107
107
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "contributor": "contract",
3
- "claim": "205 层嵌套的未知值:域解析照常(剥键扫描有深度上限,只报顶层那个键),但 local-root 腿的**内容哈希**有 200 层硬闸 ⇒ 它在 hash 层确定性地拒。同一份文档 domains/wire 两腿读得出、local 腿读不出 —— 这条差异必须是**报告里看得见**的,而不是某一腿悄悄回落。",
3
+ "claim": "205 层嵌套的未知值:域解析照常(剥键扫描有深度上限,只报顶层那个键),但 local-root 腿读文件:一份文档嵌得比整棵目录的内容版本能算的深 ⇒ 它在读文档那一步(文件层,parse)确定性地拒(16.0.0 前在算内容哈希那一步抛)。同一份文档 domains/wire 两腿读得出、local 腿读不出 —— 这条差异必须是**报告里看得见**的,而不是某一腿悄悄回落。",
4
4
  "firstRedOn": []
5
5
  }
@@ -65,7 +65,9 @@
65
65
  "coords": {
66
66
  "HOST_WORKSPACE_BASE": "/work"
67
67
  },
68
- "lane": "host",
68
+ "lanes": [
69
+ "host"
70
+ ],
69
71
  "secretRefs": {}
70
72
  },
71
73
  "rateLimitRpm": 0,
@@ -108,7 +110,9 @@
108
110
  "coords": {
109
111
  "HOST_WORKSPACE_BASE": "/work"
110
112
  },
111
- "lane": "host",
113
+ "lanes": [
114
+ "host"
115
+ ],
112
116
  "secretRefs": {}
113
117
  },
114
118
  "rateLimitRpm": 0,
@@ -13,7 +13,9 @@
13
13
  },
14
14
  "runtime": {
15
15
  "execution": {
16
- "lane": "host",
16
+ "lanes": [
17
+ "host"
18
+ ],
17
19
  "coords": {
18
20
  "HOST_WORKSPACE_BASE": "/work"
19
21
  }
@@ -65,7 +65,9 @@
65
65
  "coords": {
66
66
  "E2B_TEMPLATE": "base"
67
67
  },
68
- "lane": "e2b",
68
+ "lanes": [
69
+ "e2b"
70
+ ],
69
71
  "secretRefs": {
70
72
  "E2B_API_KEY": "HOST_E2B_KEY"
71
73
  }
@@ -110,7 +112,9 @@
110
112
  "coords": {
111
113
  "E2B_TEMPLATE": "base"
112
114
  },
113
- "lane": "e2b",
115
+ "lanes": [
116
+ "e2b"
117
+ ],
114
118
  "secretRefs": {
115
119
  "E2B_API_KEY": "HOST_E2B_KEY"
116
120
  }
@@ -13,7 +13,9 @@
13
13
  },
14
14
  "runtime": {
15
15
  "execution": {
16
- "lane": "e2b",
16
+ "lanes": [
17
+ "e2b"
18
+ ],
17
19
  "coords": {
18
20
  "E2B_TEMPLATE": "base"
19
21
  },
@@ -67,7 +67,9 @@
67
67
  "K8S_RUNTIME_CLASS": "",
68
68
  "K8S_SANDBOX_IMAGE": "registry.internal/sandbox@sha256:abc"
69
69
  },
70
- "lane": "k8s",
70
+ "lanes": [
71
+ "k8s"
72
+ ],
71
73
  "secretRefs": {
72
74
  "K8S_TOKEN": "HOST_K8S_TOKEN"
73
75
  }
@@ -114,7 +116,9 @@
114
116
  "K8S_RUNTIME_CLASS": "",
115
117
  "K8S_SANDBOX_IMAGE": "registry.internal/sandbox@sha256:abc"
116
118
  },
117
- "lane": "k8s",
119
+ "lanes": [
120
+ "k8s"
121
+ ],
118
122
  "secretRefs": {
119
123
  "K8S_TOKEN": "HOST_K8S_TOKEN"
120
124
  }
@@ -13,7 +13,9 @@
13
13
  },
14
14
  "runtime": {
15
15
  "execution": {
16
- "lane": "k8s",
16
+ "lanes": [
17
+ "k8s"
18
+ ],
17
19
  "coords": {
18
20
  "K8S_SANDBOX_IMAGE": "registry.internal/sandbox@sha256:abc",
19
21
  "K8S_NAMESPACE": "sandboxes",
@@ -67,7 +67,9 @@
67
67
  "SSH_PORT": "2222",
68
68
  "SSH_USER": "deploy"
69
69
  },
70
- "lane": "ssh",
70
+ "lanes": [
71
+ "ssh"
72
+ ],
71
73
  "secretRefs": {
72
74
  "SSH_PRIVATE_KEY": "HOST_SSH_KEY"
73
75
  }
@@ -114,7 +116,9 @@
114
116
  "SSH_PORT": "2222",
115
117
  "SSH_USER": "deploy"
116
118
  },
117
- "lane": "ssh",
119
+ "lanes": [
120
+ "ssh"
121
+ ],
118
122
  "secretRefs": {
119
123
  "SSH_PRIVATE_KEY": "HOST_SSH_KEY"
120
124
  }
@@ -13,7 +13,9 @@
13
13
  },
14
14
  "runtime": {
15
15
  "execution": {
16
- "lane": "ssh",
16
+ "lanes": [
17
+ "ssh"
18
+ ],
17
19
  "coords": {
18
20
  "SSH_HOST": "deploy.internal",
19
21
  "SSH_USER": "deploy",
@@ -65,7 +65,9 @@
65
65
  "coords": {
66
66
  "ADB_SERIAL": "emulator-5554"
67
67
  },
68
- "lane": "adb",
68
+ "lanes": [
69
+ "adb"
70
+ ],
69
71
  "secretRefs": {}
70
72
  },
71
73
  "rateLimitRpm": 0,
@@ -108,7 +110,9 @@
108
110
  "coords": {
109
111
  "ADB_SERIAL": "emulator-5554"
110
112
  },
111
- "lane": "adb",
113
+ "lanes": [
114
+ "adb"
115
+ ],
112
116
  "secretRefs": {}
113
117
  },
114
118
  "rateLimitRpm": 0,
@@ -13,7 +13,9 @@
13
13
  },
14
14
  "runtime": {
15
15
  "execution": {
16
- "lane": "adb",
16
+ "lanes": [
17
+ "adb"
18
+ ],
17
19
  "coords": {
18
20
  "ADB_SERIAL": "emulator-5554"
19
21
  }