@sema-agent/settings-schema 1.10.0 → 2.1.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,218 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 2.1.0 — 2026-09-15 — 告警点名**原文档的下标**:每个压缩点自带位移表,哪一层压缩哪一层回写
10
+
11
+ > **版号裁定(发布方)**:本批新增两个导出(`rebaseWarningPaths` / `EntryKeptIndex`)与两个接口新键
12
+ > (`EntrySalvageResult.keptIndex` / `GrandfatherHostsReadResult.keptIndex`),照本档下方「发版纪律」的
13
+ > 「**新键 / 新导出 = minor**」判 **2.1.0**(additive,零删改)。行为面只有告警里的路径串换了坐标:
14
+ > 解析结果、wire 形、类型面、默认值、校验结果一字未变。
15
+
16
+ **三问**:谁需要 = 照 `entry-dropped` / `unknown-keys-dropped` / `unknown-keys-carried` 告警去改配置的
17
+ 运维,以及把这些路径串原样印进自己诊断面的消费方;谁受伤 = 无(只有那条本来就指错的路径变成了对的);
18
+ 补偿 = 不需要旋钮,坐标回真本身就是修复,没有「旧坐标」这个合法用例可退。
19
+
20
+ **病**:读路径上有**两个压缩点**,两处都会让后续诊断拿着压缩后的下标 ——
21
+ ① 逐条容错(2.0.0)在整域 parse 失败后先丢掉坏条目、再对**压缩后**的文档跑剥键/承运扫描:原文档
22
+ `models.2` 里拼错的键被报成 `models.1`;② `grandfatherHostsRead`(0.8.1)在 parse **之前**就把超长名的
23
+ host 条目剔出数组,之后的扫描同样少数一位:原文档 `hosts.1` 上的拼错键被报成 `hosts.0`。运维照着告警
24
+ 去改那一行,改的是别人的行,而且改完告警还在(他以为改对了)。只有数组字段的下标会错位;Record 字段的
25
+ 条目按键寻址,丢一条不动其余键的坐标,不受影响。
26
+
27
+ **修法 —— 一条律,按类一版解决**(以点改面;规则**变少**了:删掉一条特判,没有新增特判):
28
+
29
+ > **每个压缩点自带 `keptIndex`;一条告警用「铸它的那一层所见文档」的坐标表达;哪一层压缩了文档,
30
+ > 哪一层就把从它下方上来的告警回写一次。**
31
+
32
+ 组合因此是自然的 —— 两层同时压缩时内层先换算、外层后换算,不重不漏,而每一层都只需要知道**自己**那本账。
33
+
34
+ - `EntrySalvageResult` 加只读字段 `keptIndex: EntryKeptIndex`(= `Readonly<Record<string, readonly number[]>>`)
35
+ —— 抢救**自带**的坐标账:数组字段名 → 压缩后每个位置的原下标。**只登记真发生过丢弃的数组字段**,没丢
36
+ 的字段不登记恒等映射(否则「有没有位移」会从表的在场性挪成表的内容,凭空多一层判据)。**additive**:
37
+ 它是返回值上的新字段,消费方只读不构造。
38
+ - `GrandfatherHostsReadResult` 加只读字段 `keptIndex`(同一种账)。**只在按名剔条目时登记** —— 纯截断
39
+ (超 `HOSTS_ARRAY_MAX` 的 slice)留下的是原数组**前缀**,幸存者下标不动。
40
+ - 新增纯函数 `rebaseWarningPaths(w, keptIndex)`:对 `EffectiveReadWarning` 联合做**穷举 `switch`**
41
+ (新 `kind` 不声明处置 = 编译红)。只分**两类**,没有第三类:**带坐标的回写,不带坐标的原样返回**。
42
+ `entry-dropped` 也回写 —— 它**不是**「恒为原坐标」,只是不经**铸它那一层自己**的表;把「哪一层」
43
+ 写成「哪个 kind」的性质是错记。按**段**比对而不在拼好的串上做前缀匹配:`models` 的位移表绝不许动到
44
+ `modelsX.1.k`。定义域按**路径文法**分支(`PathGrammar`,两支各有唯一铸造者,**不合成一支**):
45
+ `"entry"` = `salvageEntries` 铸 `entry-dropped` 的形(数组条目**恰好两段**),`"key"` = `collectDropped`
46
+ 铸未知键的形(数组元素下的键**至少三段**)。再加两道门:字段在位移表里 + 下标段是**规范十进制**
47
+ (`${i}` 的像)。Record 形路径天然不受影响(Record 字段从不进位移表)。
48
+ - 回写是**一个纯函数**,在**每个压缩点的告警出口**上各裹一次:`buildEffective` 的 `exitOf(keptIndex)`
49
+ 产出那一层的出口,`parseOr` 收的是**出口**而不是「外层压缩了多少」——它不必知道自己上面还有没有一层。
50
+ 刻意**不**给 `scanDropped` / `scanCarried` 加「原始 raw」第二入参:那会让扫描同时持有两套坐标,
51
+ 每条新告警都要自己决定用哪套。
52
+ - **byte 不变律**:没有任何一条路径真的变(含 `keptIndex` 为空)⇒ 返回**同一个告警对象引用**,连 `keys`
53
+ 数组也不复制;位移表为空的那一层连出口闭包都不套(`exitOf` 原样返回 `onWarning`)。与
54
+ `grandfatherHostsRead`「没夹紧就返回原引用」同一条纪律。`truncatedFrom` 的截断计数不因回写改变 ——
55
+ 回写只换路径串,不碰载荷上限与真实总数。
56
+ - wire 腿(`readEffectiveWire` 的 skills manifest)**亲核无此面**:它走 `SkillsManifestSchema.safeParse`,
57
+ 只发 `entry-dropped`(原坐标)与 `domain-defaulted`(无坐标),不对压缩后的文档跑任何剥键扫描。
58
+
59
+ 红先:`test/warning-path-rebase-2.1.0.test.ts` 在 2.0.0 源码下 **8 格 6 红 / 2 绿**(绿的两格是 Record 形
60
+ 键路径不移位、与 hosts 那格现状钉 —— 都是**不许改坏**的不变量),另 tsc 两条红;修后 8 绿。落地后自审
61
+ 补两格红先,codex 对抗复审红先补一格(见下),统一律再补四格红先(路径文法 / `entry-dropped` 回写 /
62
+ hosts 第二压缩点 `hosts.0.typoKey`→`hosts.1.typoKey` / 两层组合 ⇒ 拼错键报原下标 `hosts.3.typoKey`)
63
+ 加两格 grandfather 夹紧叠加的边界覆盖,codex 修复验证轮再补三格红先(见下)。
64
+ 最终 19 绿 / 全量 54 文件 809 格全绿。
65
+
66
+ **codex 对抗复审 R1 真 finding(红先复现 67 vs 3 → 修绿)**:为了拿到「压缩后第 j 条 = 原第 i 条」,
67
+ 第一版把 `value.filter(...)` 改成了手写 `for (i < value.length)` —— 两者**不等价**:`filter` 在进入时
68
+ 就钉死了长度,而重读 `value.length` 会跟着一个在校验期间**自我增殖**的数组(病态 getter 在 zod 读字段
69
+ 时往同一个数组里再 push 一条)一路跑下去,把同步读挂死。修法=退回 `filter`,下标在它的回调里收
70
+ (`filter` 本来就给 index)—— 抢救腿相对 2.0.0 的行为因此逐字不变,新增的只是那本坐标账。
71
+ `grandfatherHostsRead` 的 hosts 过滤同守这一条(照旧 `filter`,只在回调里多收一个下标)。
72
+
73
+ **codex 修复验证轮真 finding(红先复现 → 修绿)**:第一版把两支路径文法合成了一支「裸下标与带后续段
74
+ 都算数组路径」。`collectDropped` 下探数组时**永远**还有后续段,所以未知键告警里的裸 `field.N` 只可能是
75
+ 一个**字面叫 `hosts.0` 的顶层键** —— 按位移表改写它 = 把一个根本没被压缩过的东西挪了位。修法=把
76
+ 「这条路径是谁铸的、按什么文法铸的」显式命名成 `PathGrammar`,两支各认各的段数,而不是给某个 `kind`
77
+ 开特判。红先三格:单元格 `models.1` 不许动、读面一层压缩下字面键 `hosts.0` 不许动、**两层都压缩**时
78
+ 字面键仍不动而同文档里的真数组路径照常回到原下标 `hosts.3`。
79
+
80
+ **已知残留(未修,有一格 🚧 现状钉在 `test/warning-path-rebase-2.1.0.test.ts` 里,修好时连格一起翻)**:
81
+ 点号路径是**有损编码** —— 键名里本来就允许含 `.`,所以一个字面叫 `models.0.typo` 的**顶层键**与
82
+ `models[0]` 里的未知键 `typo` 编码成同一个串,回写会改写前者。**同一条既有缺陷换了个数字**:上一版
83
+ 报 `models.0.typo`,运维照它去看 `models[0].typo` 同样什么都没有,两版都指不到那个字面键。真根因是
84
+ 编码丢了「这一段是下标还是键名」这个位,消它要让扫描与抢救改铸**结构化路径段**、出门时才 stringify
85
+ —— 那是 `EffectiveReadWarning` 的 BREAKING,归下一个 major(与 S-321 死出口删同车)。
86
+
87
+ ## 2.0.0 — 2026-09-15 — 域解析**逐条容错**(行为面 BREAKING)+ `siblingResolver`→`createSiblingResolver`(删已发布导出)+ hooks 闭集补齐两员 + 上游漂移三门 + 文档腐败一次清
88
+
89
+ > **版号裁定(发布方,2026-09-15)**:按本仓「发版纪律」——删已发布导出(`siblingResolver`)与「同一份配置解析出不同结果」(逐条容错)两条都是 major ⇒ 本批发 **2.0.0**。消费方 `^1.x` 不会静默收到;迁移=补两条 `EffectiveReadWarning` 臂(`entry-dropped` / `legacy-governance-lifted`)+ 改一个名字,零行为改造。
90
+
91
+ ### ① 逐条容错律 —— 整域 all-or-nothing 按类修(BREAKING)
92
+
93
+ **旧行为**:一份域文档里只要有一条条目解析不过,**整域**退回 schema 默认 —— 13 个模型里写坏一条,整张
94
+ 目录一条都不剩;一个 MCP 服务器写错一个字段,整套服务器消失。
95
+ **新行为**:只丢那一条,同域其余条目照常生效,并**逐条**发 `entry-dropped` 告警点名它的点号路径
96
+ (`models.3` / `roles.summarize`)。
97
+
98
+ - 单铸点 `salvageEntries(schema, raw)`(`config-fns.ts`):条目集合**从 schema 自身派生**(数组字段与
99
+ Record 字段),不写「域 → 条目字段」对照表 —— 那是第二份真源,开新域必漏。入参是 **schema 不是域名**:
100
+ `skills` 在 wire 上是 manifest(另一张 schema,在 `buildEffective` 之外单独判),只吃域名的抢救点会把
101
+ 那条腿漏在门外 —— 同一份病只因为换了张 schema 就没被覆盖。两条腿现在共用同一条律。
102
+ - 唯一调用点 = `buildEffective` 的 `parseOr`,且**只在整域解析已经失败之后**才跑:合法配置的解析路径
103
+ 一字不动(与 0.8.1 的 hosts grandfather 同姿势,成功路径 byte 不变)。`FileConfigStore.getEffective`
104
+ (含 `{tolerant:true}`)与 `readEffectiveWire` 都经 `buildEffective`,自动同愈,无第二机制;wire 上的
105
+ skills manifest 那条旁路也接了同一个抢救点。
106
+ - **不新开档位**:容错只发生在 `DOMAIN_READ_FALLBACK` 判 `"default"` 的域。gate 域(rosters / governance /
107
+ entitlement / execution / workers / runtime / limits / projects)的坏条目**仍然 throw** —— 悄悄丢掉一条
108
+ roster 就是那个 worker 静默拿到全量目录,与整域降级是同一条「放权,不许」的理由,所以共用同一张表。
109
+ - **一域一个结论**:丢掉坏条目后整域仍解析不了(重名 / 悬空 ref / 域级判据),只发 `domain-defaulted`,
110
+ 不留「丢了第 3 条」的半路叙述。整域缺席 / 非对象仍走整域档位(那不是「一条坏条目」)。
111
+ - 🔴 **零幸存者不算「部分损坏」**:一个集合如果**所有**条目都坏,它不是部分损坏的集合而是整个坏掉的集合,
112
+ 本版对这种字段一条都不丢,原样交给整域档位裁。这不是特判,是「抢救」的定义 —— 没有要抢救的东西;它同时
113
+ 一次性挡住一整类 fail-open:本 schema 里有若干字段**空 = 不设限**(最尖的一根是 `models.atModelAllowlist`,
114
+ 空/缺席 = 所有模型都可 @),把一份「全是坏值」的限制清单抢救成空数组等于在一次「容错」里把运维的限制
115
+ 悄悄删了。修法不是给那些字段加特判(枚举必漏一个),是**从不凭空造出一个空集合**。
116
+ 变异读数:去掉这条守卫 ⇒ 10 格红(含 `atModelAllowlist` 放权那格)。
117
+ - 告警 union `EffectiveReadWarning` 加两员:`entry-dropped`、`legacy-governance-lifted`(见 ③)。
118
+ **穷举 `switch` 的消费方会在提货时编译红** —— 那是提货成本,迁移 = 各加一条臂。
119
+ - 红先:`test/entry-salvage-1.11.test.ts` 修前 13 红 / 10 绿(绿的是本版刻意不动的不变量:gate 域仍
120
+ fail-loud、整域非对象仍降级、好配置零告警),修后 23 绿。
121
+
122
+ ### ② `PARSE_TRANSPARENT_KEYS` —— 解析透明键单点登记表
123
+
124
+ 此前「本包不校验这个键的形」这条判据在**四处** JSDoc 里各复述一遍五行同义散文,没有任何一处能回答
125
+ 「一共有几个这样的键、分别归谁校验」。现在一处列全(4 员),每行写清 `domain` · `path` · `since` ·
126
+ `validatedBy`(`engine-ingest` | `consumer` 闭集)· `why`;JSDoc 一律删成一行 `{@link}`。
127
+ 「解析透明」按**行为**定义(喂一组代表性值全收),不按 schema 的形名 —— 按形名判有两个静默错法:剥包装会
128
+ 把约束一起剥掉(`z.custom<never>()` 内里是 `ZodAny`,看着什么都收其实什么都拒),遇到没见过的包装
129
+ (`.brand()` 之类)就停下。`ModelEntry.extraBody` 因此**不在**表里:它自己有形门(必须是纯 JSON 对象),
130
+ 开放的只是对象里的键名词表。
131
+ `test/parse-transparent-keys.test.ts` 从 `DOMAIN_SCHEMAS` **派生**透明叶子与登记表双向对拍:少登记
132
+ (新键静默溜进来)红,多登记(键已经不透明了)红,陈旧行也红;派生遇到不认识的 schema 形 fail-loud。
133
+
134
+ ### ③ hooks 闭集补齐 `PreModelSwitch` / `PostModelSwitch`(additive)
135
+
136
+ `HooksConfig` 是闭集 `z.record(HookEventName, …)`,一个未收编的事件名不是「这一键被忽略」而是**整份
137
+ parse 失败** —— 一份在上游合法的 `settings.json` 只因带了 `PreModelSwitch`,连 `PreToolUse` /
138
+ `SessionStart` 一起被拒。这是同病形第 4 次(1.3.0 / 1.6.0 / 1.8.0)。
139
+
140
+ - `HOOK_EVENTS` 31 → **33**(尾插,既有成员相对次序一字不动);matcher 字段 = `to_model`;归属 shell;
141
+ 新增 `MODEL_SWITCH_SOURCES`(Pre 三词 / Post 五词,上游刻意分两张表)、`PreModelSwitchHookInput` /
142
+ `PostModelSwitchHookInput`、`HookInput` 两个联合臂、`hookSpecificOutput` 两个臂(Pre = allow/deny/ask
143
+ 许可判决,Post = additionalContext)。
144
+ - 持 `Record<HookEventName, …>` / 穷举 `switch` 的消费方提货时编译红(词表尾插的既有口径)。
145
+
146
+ ### ④ 上游漂移三门 —— 属主自己看住自己的镜像面
147
+
148
+ 本仓是这些闭集/常量的属主,却一直靠消费方替它发现上游动了,方向反了。一条律
149
+ (`driftReport(ours, upstream, registry)`)三处装载:① hook 事件闭集 vs 机器抽取的上游锚定快照
150
+ (`scripts/extract-cc-hook-events.mjs` → `test/fixtures/cc-hook-events.json`,不手抄);② 引擎
151
+ `ModelRole` 词表(结构性查找其 `.d.ts` 声明,不钉文件路径);③ 引擎 `PROJECT_ID_REGEX` 镜像
152
+ (源码里承诺了 7 个 minor 的 drift-lock,本版兑现)。差集里每个词必须有一行成文理由,未登记的差红并点名,
153
+ 差已消失却留着登记行也红。变异自证读数写在门内。
154
+
155
+ ### ⑤ 长命兼容臂:落日条款 + 静默改写改成出声
156
+
157
+ - `legacyGovernanceFromRuntime` 抬升真生效时发 `legacy-governance-lifted`(点名抬了哪几个键);
158
+ 调度存储的 durable `scope` 重铸真发生时发 `durable-scope-rekeyed`(带改写前后)。**行为一字未变**,
159
+ 只是不再无声 —— 关窗的前提是有人能证明它已不再触发。
160
+ - 四条臂逐条写落日:两条改写臂是**持久数据**面,删除归下一个 major 并按持久数据的规矩办(响亮拒收 +
161
+ 一次性迁移);hosts 夹钳本来就出声,补条款;已删字段墓碑无代码,只留「别把这个名字铸回来」。
162
+ - `SchedulerParseWarning` 由单一形变成 union(加 `durable-scope-rekeyed`):穷举它的消费方补一条臂。
163
+
164
+ ### ⑥ `siblingResolver` → `createSiblingResolver`(BREAKING,零别名)
165
+
166
+ 命名律:`build*` 出纯数据,`create*` 出活体对象;它返回闭包,此前两个前缀都不带。直接改名,**不留别名、
167
+ 不留双读** —— 消费方的 tsc 红就是通知,迁移 = 改这一个名字。
168
+
169
+ ### ⑦ 文档腐败一次清(11 处)+ 四格常驻门
170
+
171
+ - `ROADMAP.md`:删掉那张「已 SHIPPED(版本锚)」表(它在三个版本已发上 npm 之后仍把它们列在「候
172
+ publish」栏,烂了一个多月);本档自此只写还没做的。新门 H:本档提到的版本号不许已经在 CHANGELOG 里
173
+ 有一节;也不许再维护一份版本锚表。
174
+ - `docs/CONSUMER-INTEGRATION.md` §2 的「runtime 位含 governance 六闸镜像双写」是反事实(0.7.1 已撤,
175
+ 同档 §6 自己就这么写),删;「六闸」这个已撤镜像的旧称改成真话「限额残余 5 键」。新门 I:档里不许有
176
+ 「还在双写」的现行时声明;runtime 残余键数与运行时表相等。
177
+ - CI 以「`npm run typecheck` 有存量 test 文件报错」为由排除类型门 —— 亲跑是 exit 0 / 零输出,那句话早已
178
+ 不是事实,而 `test/**` 的类型面因此一直无门(`build` 只编 `src/`)。删该行,`typecheck` 挂进 CI。
179
+ - src 注释里 **24 处** `docs/*.md` 指路指向本仓根本没有的文件,且随 `dist/*.d.ts`(`removeComments:false`)
180
+ 发给消费方 = 24 条 404。逐条改写成中性指称(档在别处,这是诚实的说法)。新门 F:src 里出现的每一个
181
+ `*.md` 指路都必须能解析到真文件。
182
+ - `llms.txt` 的 17 域枚举被两条后插的 bullet 腰斩(数字对、句子断,D 组只钉数字所以门绿文坏):修文;
183
+ 新门 G:域名必须逐个出现在同一条 bullet 里。两档补 `toolFaces` / `contentOrigin` 说明与本版新面。
184
+ - `README.md` 的「What's in it」停在 1.4.x(全文零命中 hooks / scheduler / cross-session /
185
+ `EXTENSION_NAMESPACES` / `toolFaces`):按 barrel 面补齐到本版。
186
+ - CHANGELOG 的 `1.1.0` / `1.2.0` / `1.3.0` 三节补日期(npm 发布时间戳为准,与其余各节同用本地日期)。
187
+ - `src/types.ts` 里 `PROJECT_ID_REGEX` 那条「core 公开导出后加 drift-lock」的 TODO 已兑现(见 ④),改写成
188
+ 事实 + 成文的 flags 差异理由。
189
+ - `CLAUDE.md` 把 `npm run e2e` 写成「域/契约改动腐坏它就报警」的保护面,与实际不符:它只写三个域
190
+ (models / governance / skills)+ 非域的 remote-exec,也不断言 `warnings`。口径改成真话。
191
+
192
+ ### 三问
193
+
194
+ - **谁需要**:运维在 `config.d/models.json` 里加一条新模型时手滑写错一个字段 —— 今天的结果是**整张模型
195
+ 目录消失**(那台 worker 一个模型都没有),而不是「那一条没生效」。同一形在 MCP 服务器、skills、场景、
196
+ 插件目录上一模一样。这不是假想:1.7.0 / 1.7.1 连着两版,就是因为在一个**展示用的标签**上设了形判据,
197
+ 让一条坏值抹掉了整套 MCP 服务器,只好连发两个 patch 撤回。
198
+ - **谁受伤**:① 靠「坏一条 ⇒ 整域退默认」这个**连坐**当粗粒度校验信号的消费方 —— 它此前能从「整域空了」
199
+ 推出「文件有问题」,现在整域不空了,信号改为逐条的 `entry-dropped` 告警;② 穷举 `EffectiveReadWarning` /
200
+ `SchedulerParseWarning` / `HookEventName` 的消费方提货时编译红;③ 用了 `siblingResolver` 这个名字的
201
+ 消费方编译红。
202
+ - **什么补偿**:不加旋钮 —— 加一个「要不要逐条容错」的开关等于把这条律变成两条律,而两条律里一定有一条
203
+ 没人测。补偿是**信号**:每一条被丢掉的条目都有一条点名它路径的告警(`entry-dropped`),`onWarning` 是
204
+ 0.8.1 起就有的既有通道,消费方把它接到自己的审计流即可拿到比「整域空了」精确得多的输入;gate 域的
205
+ fail-loud 一条没动,所以「静默放权」这条最坏的路本就不存在。
206
+
207
+ ### 消费方点名(按能力显式表态制)
208
+
209
+ - **worker 服务**:①`EffectiveReadWarning` 穷举 `switch` 需补 `entry-dropped` / `legacy-governance-lifted`
210
+ 两臂;②读面行为变化 —— 坏条目不再连坐整域,若有「整域空 ⇒ 配置有问题」的推断需改读 `entry-dropped`;
211
+ ③`HookEventName` 穷举面加两员;④`createSiblingResolver` 改名(若有引用)。
212
+ - **CLI / 壳**:①`HooksConfig` 现在受理 `PreModelSwitch` / `PostModelSwitch`(settings.json 不再整份被拒),
213
+ 持 `Record<HookEventName, …>` 的表需补两员;②`SchedulerParseWarning` 成 union,穷举它的地方补
214
+ `durable-scope-rekeyed` 臂。
215
+ - **client-core**:透明键集从 zod shape 反射派生的那条门 —— 本版新增导出 `PARSE_TRANSPARENT_KEYS`,
216
+ 两边可以直接对拍(派生 vs 登记),不必各自反射。
217
+ - **config-center 产品(舰队管理台)**:①写侧仍走 `parseDomain`(严判,一字未变)—— 逐条容错只在**读**
218
+ 路径;②读面同 worker 服务:坏条目不再连坐;③它今天装的是改名前、冻结在 `0.15.0` 的旧名包,本版之前的
219
+ 全部契约它都不认,提货 `^1.11.0` 是它自己的车。
220
+
9
221
  ## 1.10.0 — 2026-09-09 — `models.models[].compat` 引擎兼容声明席(镜像 `OpenAICompletionsCompat`;新键=minor)
10
222
 
11
223
  - 引擎 `OpenAICompletionsCompat`(五键 `thinkingFormat` / `reasoningEffortLevels` / `maxTokensField` /
@@ -211,7 +423,7 @@ permissions 先例形)。红先:3 新钉先红(members 缺席)→ 加表后 2
211
423
  4→0 / `dist/remote-exec.d.ts` 4→0;**全 dist 52→1**,唯一剩余 = 上述默认值行。钉
212
424
  `test/docs-hygiene.test.ts` E 组(src 三族零命中、唯一放行行 = 该默认值字面;裸人名零命中)。
213
425
 
214
- ## 1.3.0 — hooks 域加员 `DirectoryAdded`(CC 2.1.219 新事件;`HooksConfig` 拒键病形修复)
426
+ ## 1.3.0 — 2026-08-31 — hooks 域加员 `DirectoryAdded`(CC 2.1.219 新事件;`HooksConfig` 拒键病形修复)
215
427
 
216
428
  **`HOOK_EVENTS` 30 → 31:`POST_198_HOOK_EVENTS` **尾插** `DirectoryAdded`(1.2.0 三员声明序逐字节不动;重排 = BREAKING,
217
429
  免裁)。additive:既有 30 员的 matcher 字段 / owner / 输入形 / `hookSpecificOutput` 臂一字不动(FROZEN 1.2.0 全表字面量夹具钉,
@@ -298,7 +510,7 @@ export type DirectoryAddedHookInput = z.infer<typeof DirectoryAddedHookInput>;
298
510
  错 + 40 文件 631 用例),归因 codex `--sandbox read-only` 下 vitest 写 `node_modules/.vite/vitest` 缓存失败(目录亲验存在;
299
511
  归因为假设,复跑绿为实测)。
300
512
 
301
- ## 1.2.0 — `MinTrust` 排序原语 + `task` 员([5796] 序题 → [5805] schema 认领 / [5807] cli 表态三点)
513
+ ## 1.2.0 — 2026-08-31 — `MinTrust` 排序原语 + `task` 员([5796] 序题 → [5805] schema 认领 / [5807] cli 表态三点)
302
514
 
303
515
  **新导出 `trustRank(layer)` / `isAtLeast(layer, minTrust)`——规范信任阶梯的**单源**(v2-design §1:29 逐字);
304
516
  `MinTrust` **尾插** `task`。发布表 `SAFETY_MERGE_SPEC` **一字不动**(1.1.0 全表字面量冻结钉),枚举声明序不重排
@@ -380,7 +592,7 @@ for (const m of membersOf("permissions")) {
380
592
  - 全量 39 文件 / 621 用例绿(1.1.0 基线 610 + 11);tsc 0;build 绿(dist `.d.ts` 含两原语);dist 运行时冒烟
381
593
  (`node -e import('./dist/index.js')`)六词/六序/未知词 RangeError 如述。
382
594
 
383
- ## 1.1.0 — `permissions` 成员级准入(权限双通道终裁四件套之④,clay 终裁 [5797];合流形 [5794])
595
+ ## 1.1.0 — 2026-08-30 — `permissions` 成员级准入(权限双通道终裁四件套之④,clay 终裁 [5797];合流形 [5794])
384
596
 
385
597
  **`SAFETY_MERGE_SPEC` 类目可声明 `members`(成员级 minTrust + 方向);首个也是当前唯一成员化的类目 = `permissions`
386
598
  (`allow` / `deny` / `ask`,闭集)。纯 additive:类目级 `minTrust` 一个字不动,老消费方行为逐字节不变。**
package/README.md CHANGED
@@ -50,7 +50,7 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
50
50
  `domainWriteRole`, …), `resolveActiveTiers`(0.9.0 档位组→引擎 `RunnerDeps.tiers` 组存 `models.tierGroups`+`models.activeTierGroup`,组切换=换表下发), and the pure data
51
51
  shapes (`User`, `PublishedSnapshot`, …).
52
52
  - **Roster resolution** (`resolve-roster.ts`): `resolveEffectiveForWorker` → the per-worker wire payload.
53
- - **Ref integrity** (`cross-domain.ts`): `refIntegrityIssues` + `siblingResolver` (the pure half — fleet
53
+ - **Ref integrity** (`cross-domain.ts`): `refIntegrityIssues` + `createSiblingResolver` (the pure half — fleet
54
54
  auto-placement stays in the config-center 产品).
55
55
  - **Hashing** (`hash.ts`): `stableHash` (FNV-1a 64) for ETags / version detection.
56
56
  - **Skills manifest** (`skills-manifest.ts`): content-addressing for the lazy-pull skill bodies.
@@ -108,7 +108,34 @@ sound). `npm ls --prod` therefore shows only `zod`. Node built-ins only (`node:c
108
108
  limits/workers) stay fail-loud even in tolerant mode. 0.19.0 adds a FAIL-LOUD sweep on the same read:
109
109
  a `config.d/*.json` the local set will never read (a name that is not a domain, or a non-portable one)
110
110
  and any top-level key a domain schema strips are both NAMED through `onWarning`
111
- (`unread-config-file` / `unknown-keys-dropped`) instead of vanishing silently.
111
+ (`unread-config-file` / `unknown-keys-dropped`) instead of vanishing silently. **2.0.0** adds per-ENTRY
112
+ tolerance on the same read path: one malformed catalog entry is dropped on its own (named through
113
+ `onWarning` as `entry-dropped`, dot-path) and its siblings survive, instead of the whole domain falling to
114
+ its schema default. It is the SAME `DOMAIN_READ_FALLBACK` table that decides where this applies — gate
115
+ domains keep failing loud, entry by entry included.
116
+ - **Hooks contract** (`hooks.ts`, subpath `./hooks`): the CC-parity hook surface — `HOOK_EVENTS` (33 events:
117
+ the CC 2.1.198 set of 27 plus `CwdChanged` / `FileChanged` / `MessageDisplay` / `DirectoryAdded` and, from
118
+ 2.0.0, the model-switch pair `PreModelSwitch` / `PostModelSwitch`), the single-owner map
119
+ `HOOK_EVENT_OWNER` (engine | shell), the matcher-field table, a per-event input schema (`HookInput`),
120
+ the output/decision shapes (`SyncHookOutput` / `HookSpecificOutput` / `HookPermissionRequestDecision`),
121
+ `HooksConfig` (the `settings.json` `hooks` shape — a CLOSED key set, so a typo is caught at the wire),
122
+ and stop attribution (`StoppedBy` / `stopAttributionText`).
123
+ - **Cross-session settings** (`cross-session.ts`): the two `settings.json` keys `crossSessionInbound`
124
+ (accept/hold/refuse, `default` = unset) and `dialogExpiry` (60s/5m/10m/never), each with a reader that
125
+ answers `ok:false` on a bad value instead of silently folding to the default.
126
+ - **Scheduler store contract** (`scheduler-store.ts`, subpath `./scheduler-store`; the fs binding is in
127
+ `./node`): the durable self-wake record shape + `parseSchedulerStore` / `serializeSchedulerStore`
128
+ (forward-compatible: unknown fields survive a round-trip, malformed records are dropped one by one).
129
+ - **Fleet contract** (`fleet.ts`, subpath `./fleet`): drain protocol, instance discovery, dispatch policy,
130
+ quota lease, usage report, quota-reset event.
131
+ - **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE registry of keys this package
132
+ carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side,
133
+ a consumer's projection, or the key's own guard). Today: `models[].extraBody`, `models[].compat` (1.10.0),
134
+ `mcp.servers[].source` (1.7.2), `mcp.servers[].toolFaces` (1.9.0), and an entitlement override's `value`.
135
+ - **Extension namespaces** (`EXTENSION_NAMESPACES`, 1.5.0): keys another writer puts in a domain document by
136
+ contract and this package deliberately does not model (today: the shell's entry-level `sema*` fields on
137
+ `models`). They are still stripped, but they are NOT reported as unknown keys — intent is declared at the
138
+ registration site, so every remaining unknown key is a typo, with no per-consumer special-casing.
112
139
 
113
140
  ## Secret boundary
114
141
 
@@ -13,7 +13,7 @@
13
13
  * The fat `ConfigStore` interface itself STAYS in sema-registry (app-only, ~32 publish/RBAC/worker-status
14
14
  * methods); only the narrow {@link ConfigReader} (reader.ts) is carried here.
15
15
  */
16
- import type { z } from "zod";
16
+ import { z } from "zod";
17
17
  import { type DomainConfig, type DomainName, type EffectiveConfig, type EffectiveWire } from "./types.js";
18
18
  /** Redact secret-shaped tokens from a validation MESSAGE — a zod error echoes the offending value (e.g.
19
19
  * `received 'sk-live-…'`), so a secret pasted into the WRONG field would leak via the error string. Redacts
@@ -53,7 +53,7 @@ export declare function isRole(v: unknown): v is UserRole;
53
53
  /**
54
54
  * Minimum role to WRITE a domain. Default = `editor`; `skills` and `mcp` are raised to `publisher`
55
55
  * because their content goes straight into the agent's system prompt / tool surface — one edit retargets
56
- * the prompt of every worker × scenario that loads it (docs/MCP-SKILLS.md §3.3, decision §6 Q3 从严).
56
+ * the prompt of every worker × scenario that loads it (the mcp/skills design note §3.3, decision §6 Q3 从严).
57
57
  * Looked up by the [domain] PUT route. Any domain absent here uses {@link DEFAULT_DOMAIN_WRITE_ROLE}.
58
58
  */
59
59
  export declare const DEFAULT_DOMAIN_WRITE_ROLE: UserRole;
@@ -133,6 +133,58 @@ export interface PublishedSnapshot {
133
133
  export declare function emptyEffective(): EffectiveConfig;
134
134
  /** Validate + fill defaults for a domain payload (or the schema's empty default when absent/invalid). */
135
135
  export declare function parseDomain<K extends DomainName>(domain: K, raw: unknown): DomainConfig[K];
136
+ /** 一条被丢掉的条目:`path` = 点号路径(`models.3` / `roles.summarize`),`error` 已脱敏。 */
137
+ export interface DroppedEntry {
138
+ path: string;
139
+ error: unknown;
140
+ }
141
+ /** 抢救造成的**下标位移表**:数组字段名 → 该字段压缩后每个位置的**原**下标(`kept[j]` = 压缩后第 j 条
142
+ * 在原文档里的下标)。纯数据,无行为。
143
+ *
144
+ * 只登记**真发生过丢弃**的数组字段 —— 没丢的字段没有位移,给它登记一份恒等映射只会把「有没有位移」
145
+ * 从表的**在场性**挪成表的**内容**,凭空多一层判据。Record 字段永不登记:它的条目按键寻址,丢一条
146
+ * 不动其余键的坐标。 */
147
+ export type EntryKeptIndex = Readonly<Record<string, readonly number[]>>;
148
+ /** {@link salvageEntries} 的结果。`raw` 是剔掉坏条目后的文档;**什么都没丢时返回原引用**
149
+ * (与 {@link grandfatherHostsRead} 同纪律:好数据穿过读路径 byte 不变)。
150
+ *
151
+ * `keptIndex`(2.1.0)是抢救**自带**的坐标账:`raw` 里的数组被压缩了,谁拿这份压缩文档去诊断,谁
152
+ * 报出来的下标就是压缩后的下标。把位移随结果一起交出去,回写就能在**一个**铸点完成
153
+ * ({@link rebaseWarningPaths}),诊断函数自己不必知道有两套坐标。 */
154
+ export interface EntrySalvageResult {
155
+ raw: unknown;
156
+ dropped: DroppedEntry[];
157
+ keptIndex: EntryKeptIndex;
158
+ }
159
+ /**
160
+ * 从**schema 自身**派生条目集合(数组字段 / Record 字段),逐条判,坏的丢掉并逐条记账。
161
+ *
162
+ * 入参是 **schema 而不是域名**:`skills` 在 wire 上不是域 schema 而是 manifest(`SkillsManifestSchema`),
163
+ * 在 `buildEffective` 之外单独判 —— 只吃域名的抢救点会把那条腿漏在门外,同一份病只因为换了张 schema
164
+ * 就没被覆盖。吃 schema ⇒ 域面与 wire 面共用同一条律,没有第二实现。
165
+ *
166
+ * 为什么从 schema 派生而不是写一张「域 → 条目字段」表:那张表是第二份真源,开新域必漏。schema 里
167
+ * 「这个字段是一组条目」这件事本来就写着,读它即可 —— 新域天然被覆盖,零登记动作。
168
+ *
169
+ * 🔴 **零幸存者不算「部分损坏」**:一个集合如果所有条目都坏,那它不是「部分损坏的集合」,而是**整个坏掉
170
+ * 的集合** —— 本函数对这种字段**一条都不丢**,原样留着让整域档位去裁。这条不是特判,是「抢救」的定义:
171
+ * 没有要抢救的东西。它同时把一整类 fail-open 陷阱一次性挡住 —— 本 schema 里有若干字段**空 = 不设限**
172
+ * (最典型的 `models.atModelAllowlist`:空/缺席 = 所有模型都可 @),把一份「全是坏值」的限制清单抢救成
173
+ * 空数组,等于把运维的限制悄悄删了。这里不去枚举「哪些字段空了等于放权」(枚举必漏),而是**从不凭空
174
+ * 造出一个空集合**。
175
+ *
176
+ * 🔴 **告警不许比它诊断的东西更危险**(复审 R1 两条,红先复现):① Record 的键是用户写的,完全可能就是
177
+ * 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(不变量①:secret 值永不随错误外流,
178
+ * 与 parseDomain 的 R13/R14 同一条);② 整段遍历包 try/catch —— 读字段、枚举 Record 都会触发访问器,
179
+ * 抛出来的文案可能带 secret。抢救是**尽力而为**的:抢不动就当没抢救(返回原引用),让既有的整域档位去裁,
180
+ * 绝不把解析拖下水、更不回显它的错误(与同文件 `scanDropped` 逐字同纪律)。
181
+ *
182
+ * 边界(如实登记):① 只看**顶层**字段(条目集合就在顶层,嵌套的坏形属于条目自身的问题);② raw 不是
183
+ * 纯 JSON 对象、字段值与 schema 形状对不上(schema 说数组、文档给对象)⇒ 原样返回,交给整域档位裁;
184
+ * ③ 域级判据(重名 / 悬空 ref)不在本函数视野内 —— 丢条目救不了它们,调用方据此回落整域档位;
185
+ * ④ 只认 `ZodArray` / `ZodRecord` 两种条目集合形 —— 别的形(元组等)不抢救,退回整域档位(保守方向)。
186
+ */
187
+ export declare function salvageEntries(schema: z.ZodTypeAny, raw: unknown): EntrySalvageResult;
136
188
  /**
137
189
  * Shared GC predicate for worker-status rows (F10) — keeps memory/sqlite/tidb identical. Given the rows'
138
190
  * `{name, updatedAt}` and the GC options, returns the names to prune (A: not in `keepNames`; B: older than
@@ -154,7 +206,13 @@ export declare function statusRowsToPrune(rows: {
154
206
  /** Lift the governance trio out of a LEGACY runtime doc (pre-0.6 stored runtime carried autonomy/commandPolicy/
155
207
  * approvalRequire). Used as the governance domain's read fallback when governance was NEVER set — so a 0.5
156
208
  * store upgrades with zero data migration and zero gate loss. An explicitly-set governance domain WINS
157
- * (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime"). */
209
+ * (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime").
210
+ *
211
+ * SUNSET (1.11.0): removal is a **persistent-data** break — a store still carrying the 0.5 shape would lose
212
+ * its guardrails silently, so this arm cannot simply be deleted at a minor. It goes at the next MAJOR, with
213
+ * the standard persistent-data treatment (a loud refusal naming the domain + a one-time migration on the
214
+ * writer side), and the closing condition is observable rather than a promise: `buildEffective` now emits
215
+ * `legacy-governance-lifted` every time the arm actually fires, so a deployment can prove it no longer does. */
158
216
  export declare function legacyGovernanceFromRuntime(rawRuntime: unknown): unknown;
159
217
  /** A warning emitted by {@link buildEffective} when it clamps/degrades instead of throwing (0.8.1), or by
160
218
  * {@link FileConfigStore} when a file it will never read sits in `config.d/` (0.19.0). Callers route it to
@@ -193,6 +251,24 @@ export type EffectiveReadWarning = {
193
251
  keys: string[];
194
252
  truncatedFrom?: number;
195
253
  }
254
+ /** 0.5 期的 runtime 文档里还带着治理三闸,而 governance 域从未被设过 —— 读侧抬升腿真生效了。
255
+ * `keys` = 这次抬起来的那几个键。**照旧生效,只是不再无声**:兼容窗要能关掉,前提是有人知道它还开着。
256
+ * 1.11.0 起。 */
257
+ | {
258
+ domain: "governance";
259
+ kind: "legacy-governance-lifted";
260
+ keys: string[];
261
+ }
262
+ /** 一条**条目**没通过它自己的 entry schema,被单独丢掉了(域内其余条目照常生效)。`path` = 点号路径
263
+ * (`models.3` / `roles.summarize`),与 `unknown-keys-dropped` 的路径形同一套。逐条一发 —— 载荷是
264
+ * O(域里的条目数),与被读文档同阶,不放大,故不设条数上限(有上限的是**单条**告警里的键列表)。
265
+ * 1.11.0 起;只可能来自 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域,gate 域坏形仍然 throw。 */
266
+ | {
267
+ domain: DomainName;
268
+ kind: "entry-dropped";
269
+ path: string;
270
+ error: unknown;
271
+ }
196
272
  /** `config.d/` 里存在、但本地读取集**永远不会读**的文件(名字不是域,或是域但不便携)。 */
197
273
  | {
198
274
  kind: "unread-config-file";
@@ -200,6 +276,31 @@ export type EffectiveReadWarning = {
200
276
  domain?: DomainName;
201
277
  why: "not-a-domain" | "non-portable-domain";
202
278
  };
279
+ /**
280
+ * 一条告警里的**点号路径**从「抢救压缩后的坐标」回写成「原文档的坐标」—— 纯函数,{@link EntryKeptIndex}
281
+ * 是它唯一的坐标账。
282
+ *
283
+ * 成案(S-323,消费方真案):整域 parse 失败后 {@link salvageEntries} 会把坏条目从数组里剔掉,之后的
284
+ * 诊断({@link droppedKeys} / {@link carriedUnknownKeys})是拿**压缩后**的文档做键集对比出来的,它报的
285
+ * `models.1` 指的是压缩后的第 1 条。运维照着这条告警去改原文档的第 1 条 —— 改的是别人的行,而且改完
286
+ * 告警还在(他以为改对了)。**告警必须说运维看得见的那份文档里的坐标**。
287
+ *
288
+ * 为什么回写在这里、而不是让扫描函数认识原文档:给 `scanDropped` / `scanCarried` 加一个「原始 raw」
289
+ * 第二入参,等于让扫描同时持有两套坐标,每条新告警都要自己决定用哪套。回写是**告警出门前**的一次
290
+ * 坐标换算,与诊断本身正交 —— 一个铸点,所有臂一次表态。
291
+ *
292
+ * **闭集穷举**:`switch` 覆盖 {@link EffectiveReadWarning} 的每个 `kind`,新 kind 不声明处置 = 编译红
293
+ * (#157 的闭集纪律)。只分两类,没有第三类:**带坐标的回写,不带坐标的原样返回**。
294
+ *
295
+ * 为什么 `entry-dropped` 也回写:它**不是**「恒为原坐标」,只是不经**铸它那一层自己**的表 —— 上面那条律
296
+ * 说的就是这件事。{@link salvageEntries} 铸 `entry-dropped` 时用的是它**入参**那份文档的坐标;如果那份
297
+ * 文档本身已被外层(hosts 的 {@link grandfatherHostsRead})压缩过,外层照样要回写它一次。把「不动」写成
298
+ * 一条臂的性质,就是把「哪一层」这件事错记在了「哪个 kind」上。
299
+ *
300
+ * **恒等纪律**:没有任何一条路径真的变了(含 `keptIndex` 为空)⇒ 返回**同一个对象引用**,连 `keys`
301
+ * 数组也不复制(与 {@link grandfatherHostsRead} 的「没夹紧就返回原引用」同一条 byte 不变律)。
302
+ */
303
+ export declare function rebaseWarningPaths(w: EffectiveReadWarning, keptIndex: EntryKeptIndex): EffectiveReadWarning;
203
304
  /**
204
305
  * **扩展命名空间**(1.5.0):域文档里**由别的写者按契约放置、本包刻意不认**的键。它们照旧被 zod strip
205
306
  * (本包与引擎消费方看不见——这正是契约:壳自己回读文件取值),但 {@link droppedKeys} **不点名、不计数**。
@@ -315,11 +416,20 @@ export interface GrandfatherHostsReadResult {
315
416
  droppedNames: string[];
316
417
  /** Set when the hosts array (after drops) exceeded HOSTS_ARRAY_MAX (1024) and was sliced — the pre-slice length. */
317
418
  truncatedFrom?: number;
419
+ /** 这次夹紧造成的**下标位移表**(与 {@link EntrySalvageResult.keptIndex} 同一种账、同一条律):本函数
420
+ * 是读路径的**第二个压缩点**,`raw` 里的 hosts 数组被剔过条目,谁拿这份文档去诊断,谁报出来的下标就是
421
+ * 夹紧后的下标。只在**按名剔条目**时登记 —— 纯截断保留的是原数组的**前缀**,幸存者下标不动,登记一份
422
+ * 恒等映射只会把「有没有位移」从表的在场性挪成表的内容(见 {@link EntryKeptIndex})。 */
423
+ keptIndex: EntryKeptIndex;
318
424
  }
319
425
  /** READ-path grandfather clamp for the 0.8.0 hosts tightening (0.8.1, S3 M-1). Clamps ONLY the two bounds
320
426
  * 0.8.0 added (name > 63 → drop the entry; array > 1024 → slice) so 0.7-era legal stored data keeps reading;
321
427
  * everything else (regex, types, duplicates) is left for parseDomain — this is a targeted grandfather, not a
322
- * lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds. */
428
+ * lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds.
429
+ *
430
+ * SUNSET (1.11.0): same persistent-data class as the governance lift — it goes at the next MAJOR. This one was
431
+ * never silent (`hosts-grandfathered` fires whenever it clamps), so the closing condition is already
432
+ * observable: a deployment that has not seen that warning across a full read cycle no longer needs the arm. */
323
433
  export declare function grandfatherHostsRead(raw: unknown): GrandfatherHostsReadResult;
324
434
  export declare function buildEffective(version: number, getRaw: (d: DomainName) => unknown, updatedAt: string, onWarning?: (w: EffectiveReadWarning) => void): EffectiveConfig;
325
435
  /** READ-side tolerant judgment for a GET /effective **wire payload**(the {@link EffectiveWire} contract)at