@sema-agent/settings-schema 2.0.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 +78 -0
- package/dist/config-fns.d.ts +43 -1
- package/dist/config-fns.js +151 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,84 @@
|
|
|
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
|
+
|
|
9
87
|
## 2.0.0 — 2026-09-15 — 域解析**逐条容错**(行为面 BREAKING)+ `siblingResolver`→`createSiblingResolver`(删已发布导出)+ hooks 闭集补齐两员 + 上游漂移三门 + 文档腐败一次清
|
|
10
88
|
|
|
11
89
|
> **版号裁定(发布方,2026-09-15)**:按本仓「发版纪律」——删已发布导出(`siblingResolver`)与「同一份配置解析出不同结果」(逐条容错)两条都是 major ⇒ 本批发 **2.0.0**。消费方 `^1.x` 不会静默收到;迁移=补两条 `EffectiveReadWarning` 臂(`entry-dropped` / `legacy-governance-lifted`)+ 改一个名字,零行为改造。
|
package/dist/config-fns.d.ts
CHANGED
|
@@ -138,11 +138,23 @@ export interface DroppedEntry {
|
|
|
138
138
|
path: string;
|
|
139
139
|
error: unknown;
|
|
140
140
|
}
|
|
141
|
+
/** 抢救造成的**下标位移表**:数组字段名 → 该字段压缩后每个位置的**原**下标(`kept[j]` = 压缩后第 j 条
|
|
142
|
+
* 在原文档里的下标)。纯数据,无行为。
|
|
143
|
+
*
|
|
144
|
+
* 只登记**真发生过丢弃**的数组字段 —— 没丢的字段没有位移,给它登记一份恒等映射只会把「有没有位移」
|
|
145
|
+
* 从表的**在场性**挪成表的**内容**,凭空多一层判据。Record 字段永不登记:它的条目按键寻址,丢一条
|
|
146
|
+
* 不动其余键的坐标。 */
|
|
147
|
+
export type EntryKeptIndex = Readonly<Record<string, readonly number[]>>;
|
|
141
148
|
/** {@link salvageEntries} 的结果。`raw` 是剔掉坏条目后的文档;**什么都没丢时返回原引用**
|
|
142
|
-
* (与 {@link grandfatherHostsRead} 同纪律:好数据穿过读路径 byte 不变)。
|
|
149
|
+
* (与 {@link grandfatherHostsRead} 同纪律:好数据穿过读路径 byte 不变)。
|
|
150
|
+
*
|
|
151
|
+
* `keptIndex`(2.1.0)是抢救**自带**的坐标账:`raw` 里的数组被压缩了,谁拿这份压缩文档去诊断,谁
|
|
152
|
+
* 报出来的下标就是压缩后的下标。把位移随结果一起交出去,回写就能在**一个**铸点完成
|
|
153
|
+
* ({@link rebaseWarningPaths}),诊断函数自己不必知道有两套坐标。 */
|
|
143
154
|
export interface EntrySalvageResult {
|
|
144
155
|
raw: unknown;
|
|
145
156
|
dropped: DroppedEntry[];
|
|
157
|
+
keptIndex: EntryKeptIndex;
|
|
146
158
|
}
|
|
147
159
|
/**
|
|
148
160
|
* 从**schema 自身**派生条目集合(数组字段 / Record 字段),逐条判,坏的丢掉并逐条记账。
|
|
@@ -264,6 +276,31 @@ export type EffectiveReadWarning = {
|
|
|
264
276
|
domain?: DomainName;
|
|
265
277
|
why: "not-a-domain" | "non-portable-domain";
|
|
266
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;
|
|
267
304
|
/**
|
|
268
305
|
* **扩展命名空间**(1.5.0):域文档里**由别的写者按契约放置、本包刻意不认**的键。它们照旧被 zod strip
|
|
269
306
|
* (本包与引擎消费方看不见——这正是契约:壳自己回读文件取值),但 {@link droppedKeys} **不点名、不计数**。
|
|
@@ -379,6 +416,11 @@ export interface GrandfatherHostsReadResult {
|
|
|
379
416
|
droppedNames: string[];
|
|
380
417
|
/** Set when the hosts array (after drops) exceeded HOSTS_ARRAY_MAX (1024) and was sliced — the pre-slice length. */
|
|
381
418
|
truncatedFrom?: number;
|
|
419
|
+
/** 这次夹紧造成的**下标位移表**(与 {@link EntrySalvageResult.keptIndex} 同一种账、同一条律):本函数
|
|
420
|
+
* 是读路径的**第二个压缩点**,`raw` 里的 hosts 数组被剔过条目,谁拿这份文档去诊断,谁报出来的下标就是
|
|
421
|
+
* 夹紧后的下标。只在**按名剔条目**时登记 —— 纯截断保留的是原数组的**前缀**,幸存者下标不动,登记一份
|
|
422
|
+
* 恒等映射只会把「有没有位移」从表的在场性挪成表的内容(见 {@link EntryKeptIndex})。 */
|
|
423
|
+
keptIndex: EntryKeptIndex;
|
|
382
424
|
}
|
|
383
425
|
/** READ-path grandfather clamp for the 0.8.0 hosts tightening (0.8.1, S3 M-1). Clamps ONLY the two bounds
|
|
384
426
|
* 0.8.0 added (name > 63 → drop the entry; array > 1024 → slice) so 0.7-era legal stored data keeps reading;
|
package/dist/config-fns.js
CHANGED
|
@@ -157,6 +157,8 @@ function unwrapSchema(schema) {
|
|
|
157
157
|
}
|
|
158
158
|
return cur;
|
|
159
159
|
}
|
|
160
|
+
/** 一次「什么都没丢」的抢救的位移表(冻结的共享空表 —— {@link rebaseWarningPaths} 对它恒等)。 */
|
|
161
|
+
const NO_ENTRIES_DROPPED = Object.freeze({});
|
|
160
162
|
/** 一条条目的 parse —— 坏就返回它的(脱敏)错误。恶意 getter/Proxy 在遍历中抛错时**绝不**外泄其文案
|
|
161
163
|
* (可能带 secret,与 parseDomain 的 R13 同纪律),只留一句中性话。 */
|
|
162
164
|
function judgeEntry(schema, value) {
|
|
@@ -201,18 +203,19 @@ export function salvageEntries(schema, raw) {
|
|
|
201
203
|
return salvageEntriesUnsafe(schema, raw);
|
|
202
204
|
}
|
|
203
205
|
catch {
|
|
204
|
-
return { raw, dropped: [] }; // 反射/访问器抛错:抢不动就当没抢救,绝不外泄其文案
|
|
206
|
+
return { raw, dropped: [], keptIndex: NO_ENTRIES_DROPPED }; // 反射/访问器抛错:抢不动就当没抢救,绝不外泄其文案
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
function salvageEntriesUnsafe(schema, raw) {
|
|
208
210
|
if (!isPlainJsonObject(raw))
|
|
209
|
-
return { raw, dropped: [] };
|
|
211
|
+
return { raw, dropped: [], keptIndex: NO_ENTRIES_DROPPED };
|
|
210
212
|
const root = unwrapSchema(schema);
|
|
211
213
|
if (!(root instanceof z.ZodObject))
|
|
212
|
-
return { raw, dropped: [] };
|
|
214
|
+
return { raw, dropped: [], keptIndex: NO_ENTRIES_DROPPED };
|
|
213
215
|
const shape = root.shape;
|
|
214
216
|
const dropped = [];
|
|
215
217
|
const patch = {};
|
|
218
|
+
const keptIndex = {};
|
|
216
219
|
for (const [field, fieldSchema] of Object.entries(shape)) {
|
|
217
220
|
const value = raw[field];
|
|
218
221
|
if (value === undefined)
|
|
@@ -221,10 +224,16 @@ function salvageEntriesUnsafe(schema, raw) {
|
|
|
221
224
|
if (collection instanceof z.ZodArray && Array.isArray(value)) {
|
|
222
225
|
const element = collection.element;
|
|
223
226
|
const bad = [];
|
|
227
|
+
const keptFrom = [];
|
|
228
|
+
// `filter`(不是手写 `for (i < value.length)`):它在**进入时**就钉死了长度,而重读 `value.length`
|
|
229
|
+
// 会跟着一个在校验期间自我增殖的数组一路跑下去(病态 getter 在 zod 读字段时往同一个数组里再 push
|
|
230
|
+
// 一条)——抢救是尽力而为的旁路,绝不许把同步读挂死。下标在回调里收,`filter` 本来就给 index。
|
|
224
231
|
const kept = value.filter((entry, i) => {
|
|
225
232
|
const error = judgeEntry(element, entry);
|
|
226
|
-
if (error === undefined)
|
|
233
|
+
if (error === undefined) {
|
|
234
|
+
keptFrom.push(i); // 压缩后第 keptFrom.length-1 条 = 原文档第 i 条
|
|
227
235
|
return true;
|
|
236
|
+
}
|
|
228
237
|
bad.push({ path: `${field}.${i}`, error });
|
|
229
238
|
return false;
|
|
230
239
|
});
|
|
@@ -232,6 +241,7 @@ function salvageEntriesUnsafe(schema, raw) {
|
|
|
232
241
|
if (bad.length > 0 && kept.length > 0) {
|
|
233
242
|
dropped.push(...bad);
|
|
234
243
|
patch[field] = kept;
|
|
244
|
+
keptIndex[field] = keptFrom; // 只在真压缩了的字段上登记(见 {@link EntryKeptIndex})
|
|
235
245
|
}
|
|
236
246
|
}
|
|
237
247
|
else if (collection instanceof z.ZodRecord && isPlainJsonObject(value)) {
|
|
@@ -253,8 +263,8 @@ function salvageEntriesUnsafe(schema, raw) {
|
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
265
|
if (dropped.length === 0)
|
|
256
|
-
return { raw, dropped };
|
|
257
|
-
return { raw: { ...raw, ...patch }, dropped };
|
|
266
|
+
return { raw, dropped, keptIndex: NO_ENTRIES_DROPPED };
|
|
267
|
+
return { raw: { ...raw, ...patch }, dropped, keptIndex };
|
|
258
268
|
}
|
|
259
269
|
/**
|
|
260
270
|
* Shared GC predicate for worker-status rows (F10) — keeps memory/sqlite/tidb identical. Given the rows'
|
|
@@ -295,6 +305,107 @@ export function legacyGovernanceFromRuntime(rawRuntime) {
|
|
|
295
305
|
lifted[k] = r[k];
|
|
296
306
|
return Object.keys(lifted).length ? lifted : undefined;
|
|
297
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* 一条告警里的**点号路径**从「抢救压缩后的坐标」回写成「原文档的坐标」—— 纯函数,{@link EntryKeptIndex}
|
|
310
|
+
* 是它唯一的坐标账。
|
|
311
|
+
*
|
|
312
|
+
* 成案(S-323,消费方真案):整域 parse 失败后 {@link salvageEntries} 会把坏条目从数组里剔掉,之后的
|
|
313
|
+
* 诊断({@link droppedKeys} / {@link carriedUnknownKeys})是拿**压缩后**的文档做键集对比出来的,它报的
|
|
314
|
+
* `models.1` 指的是压缩后的第 1 条。运维照着这条告警去改原文档的第 1 条 —— 改的是别人的行,而且改完
|
|
315
|
+
* 告警还在(他以为改对了)。**告警必须说运维看得见的那份文档里的坐标**。
|
|
316
|
+
*
|
|
317
|
+
* 为什么回写在这里、而不是让扫描函数认识原文档:给 `scanDropped` / `scanCarried` 加一个「原始 raw」
|
|
318
|
+
* 第二入参,等于让扫描同时持有两套坐标,每条新告警都要自己决定用哪套。回写是**告警出门前**的一次
|
|
319
|
+
* 坐标换算,与诊断本身正交 —— 一个铸点,所有臂一次表态。
|
|
320
|
+
*
|
|
321
|
+
* **闭集穷举**:`switch` 覆盖 {@link EffectiveReadWarning} 的每个 `kind`,新 kind 不声明处置 = 编译红
|
|
322
|
+
* (#157 的闭集纪律)。只分两类,没有第三类:**带坐标的回写,不带坐标的原样返回**。
|
|
323
|
+
*
|
|
324
|
+
* 为什么 `entry-dropped` 也回写:它**不是**「恒为原坐标」,只是不经**铸它那一层自己**的表 —— 上面那条律
|
|
325
|
+
* 说的就是这件事。{@link salvageEntries} 铸 `entry-dropped` 时用的是它**入参**那份文档的坐标;如果那份
|
|
326
|
+
* 文档本身已被外层(hosts 的 {@link grandfatherHostsRead})压缩过,外层照样要回写它一次。把「不动」写成
|
|
327
|
+
* 一条臂的性质,就是把「哪一层」这件事错记在了「哪个 kind」上。
|
|
328
|
+
*
|
|
329
|
+
* **恒等纪律**:没有任何一条路径真的变了(含 `keptIndex` 为空)⇒ 返回**同一个对象引用**,连 `keys`
|
|
330
|
+
* 数组也不复制(与 {@link grandfatherHostsRead} 的「没夹紧就返回原引用」同一条 byte 不变律)。
|
|
331
|
+
*/
|
|
332
|
+
export function rebaseWarningPaths(w, keptIndex) {
|
|
333
|
+
switch (w.kind) {
|
|
334
|
+
case "unknown-keys-dropped":
|
|
335
|
+
case "unknown-keys-carried":
|
|
336
|
+
case "legacy-governance-lifted": {
|
|
337
|
+
const keys = rebasePaths(w.keys, keptIndex, "key");
|
|
338
|
+
return keys === w.keys ? w : { ...w, keys };
|
|
339
|
+
}
|
|
340
|
+
case "entry-dropped": {
|
|
341
|
+
const path = rebasePath(w.path, keptIndex, "entry");
|
|
342
|
+
return path === w.path ? w : { ...w, path };
|
|
343
|
+
}
|
|
344
|
+
// 以下三臂不带任何点号路径:`hosts-grandfathered` 点名的是主机**名**,另两条点名的是错误/文件名。
|
|
345
|
+
case "hosts-grandfathered":
|
|
346
|
+
case "domain-defaulted":
|
|
347
|
+
case "unread-config-file":
|
|
348
|
+
return w;
|
|
349
|
+
default: {
|
|
350
|
+
// 闭集穷举门:新 kind 必须在上面表态,否则这里编译红。运行期到不了(本联合由本模块铸造);万一
|
|
351
|
+
// 到了就**原样返回** —— 回写是诊断面的尽力而为,这里抛异常会被调用点的 catch 吞成「整域降级」,
|
|
352
|
+
// 那比不回写坏得多(一条不认识的告警 ⇒ 整张目录蒸发)。
|
|
353
|
+
const unhandled = w;
|
|
354
|
+
return unhandled;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** 一组点号路径的回写。全部原样 ⇒ 返回**原数组引用**(调用方据此判「什么都没变」)。 */
|
|
359
|
+
function rebasePaths(keys, keptIndex, grammar) {
|
|
360
|
+
let changed = false;
|
|
361
|
+
const out = keys.map((k) => {
|
|
362
|
+
const rebased = rebasePath(k, keptIndex, grammar);
|
|
363
|
+
if (rebased !== k)
|
|
364
|
+
changed = true;
|
|
365
|
+
return rebased;
|
|
366
|
+
});
|
|
367
|
+
return changed ? out : keys;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* `field.N.rest` → `field.<原 N>.rest`。**按段比对**,不在拼好的串上做前缀匹配:`models` 的位移表绝不许
|
|
371
|
+
* 动到 `modelsX.1.k`(同名前缀是两个字段)。
|
|
372
|
+
*
|
|
373
|
+
* 回写的**定义域 = `grammar` 那一支真正铸得出来的数组路径**,不是「长得像下标就算」(两支文法的差别与
|
|
374
|
+
* 合并的代价见 {@link PathGrammar})。三道门,缺一不回写:
|
|
375
|
+
* · `field` 必须在位移表里(= 那个字段真被压缩过);
|
|
376
|
+
* · 段数合文法:`"entry"` 支的数组条目**恰好两段**,`"key"` 支**至少三段**;
|
|
377
|
+
* · 下标段必须是**规范十进制**(`${i}` 的像:无前导零、无符号、无空白)且落在表长内。
|
|
378
|
+
* 不满足 ⇒ 原样返回。
|
|
379
|
+
*
|
|
380
|
+
* Record 形的条目路径(`${field}.${key}`)天然不受影响:Record 字段从不进位移表(条目按键寻址,丢一条
|
|
381
|
+
* 不动其余键的坐标),`hasOwnProperty` 那道门就把它挡在外面了。
|
|
382
|
+
*
|
|
383
|
+
* 如实登记的残余歧义(S-323c,本版**不消**):点号路径是有损编码 —— 键名里本来就允许含 `.`
|
|
384
|
+
* (见 {@link isReservedExtensionKey} 的分段匹配注),所以一个**字面叫 `models.1.k` 的顶层键**与
|
|
385
|
+
* `models[1]` 里的未知键 `k` 仍然编码成同一个串,无从分辨(裸 `models.1` 那一半已被上面的段数门挡住)。
|
|
386
|
+
* 那是编码本身的歧义(回写之前就在:两版都指不到那个字面键,只是数字不同);要消掉它得让扫描与抢救
|
|
387
|
+
* 改铸**结构化路径段**、出门时才 stringify —— 那是 {@link EffectiveReadWarning} 的 BREAKING,归下一个 major。
|
|
388
|
+
*/
|
|
389
|
+
function rebasePath(key, keptIndex, grammar) {
|
|
390
|
+
const firstDot = key.indexOf(".");
|
|
391
|
+
if (firstDot <= 0)
|
|
392
|
+
return key; // 顶层键,没有下标可言
|
|
393
|
+
const field = key.slice(0, firstDot);
|
|
394
|
+
if (!Object.prototype.hasOwnProperty.call(keptIndex, field))
|
|
395
|
+
return key;
|
|
396
|
+
const rest = key.slice(firstDot + 1);
|
|
397
|
+
const secondDot = rest.indexOf(".");
|
|
398
|
+
const bare = secondDot < 0;
|
|
399
|
+
if (bare !== (grammar === "entry"))
|
|
400
|
+
return key; // 段数不合本支文法 ⇒ 它不是这一支铸出来的数组路径
|
|
401
|
+
const indexText = bare ? rest : rest.slice(0, secondDot);
|
|
402
|
+
if (!/^(?:0|[1-9]\d*)$/.test(indexText))
|
|
403
|
+
return key; // 规范十进制才是 `${i}` 的像
|
|
404
|
+
const original = keptIndex[field][Number(indexText)];
|
|
405
|
+
if (original === undefined)
|
|
406
|
+
return key;
|
|
407
|
+
return bare ? `${field}.${original}` : `${field}.${original}.${rest.slice(secondDot + 1)}`;
|
|
408
|
+
}
|
|
298
409
|
/** 扫描深度上限(嵌套再深的配置文档不存在;有上限才不会被恶意深链拖住)。 */
|
|
299
410
|
const DROP_SCAN_MAX_DEPTH = 8;
|
|
300
411
|
/** 一条警告最多点名多少个键(告警载荷有界;超界时警告带 `truncatedFrom` = 真实总数,见 {@link droppedKeys})。 */
|
|
@@ -516,16 +627,18 @@ export const DOMAIN_READ_FALLBACK = {
|
|
|
516
627
|
* observable: a deployment that has not seen that warning across a full read cycle no longer needs the arm. */
|
|
517
628
|
export function grandfatherHostsRead(raw) {
|
|
518
629
|
if (raw === null || typeof raw !== "object" || !Array.isArray(raw.hosts)) {
|
|
519
|
-
return { raw, droppedNames: [] }; // not the expected shape — let parseDomain rule on it
|
|
630
|
+
return { raw, droppedNames: [], keptIndex: NO_ENTRIES_DROPPED }; // not the expected shape — let parseDomain rule on it
|
|
520
631
|
}
|
|
521
632
|
const original = raw.hosts;
|
|
522
633
|
const droppedNames = [];
|
|
523
|
-
|
|
634
|
+
const keptFrom = [];
|
|
635
|
+
let hosts = original.filter((h, i) => {
|
|
524
636
|
const name = h !== null && typeof h === "object" ? h.name : undefined;
|
|
525
637
|
if (typeof name === "string" && name.length > HOST_NAME_MAX) {
|
|
526
638
|
droppedNames.push(name.slice(0, HOST_NAME_MAX) + "…"); // audit label — clipped so the warning itself stays bounded
|
|
527
639
|
return false;
|
|
528
640
|
}
|
|
641
|
+
keptFrom.push(i); // 夹紧后第 keptFrom.length-1 条 = 原文档第 i 条
|
|
529
642
|
return true;
|
|
530
643
|
});
|
|
531
644
|
let truncatedFrom;
|
|
@@ -534,20 +647,37 @@ export function grandfatherHostsRead(raw) {
|
|
|
534
647
|
hosts = hosts.slice(0, HOSTS_ARRAY_MAX);
|
|
535
648
|
}
|
|
536
649
|
if (droppedNames.length === 0 && truncatedFrom === undefined)
|
|
537
|
-
return { raw, droppedNames }; // identity — byte-stable
|
|
538
|
-
|
|
650
|
+
return { raw, droppedNames, keptIndex: NO_ENTRIES_DROPPED }; // identity — byte-stable
|
|
651
|
+
// 位移**只**来自按名剔条目;纯截断留下的是原数组前缀,幸存者下标不动 ⇒ 不登记(见 EntryKeptIndex)。
|
|
652
|
+
const keptIndex = droppedNames.length > 0 ? { hosts: keptFrom.slice(0, hosts.length) } : NO_ENTRIES_DROPPED;
|
|
653
|
+
return { raw: { ...raw, hosts }, droppedNames, truncatedFrom, keptIndex };
|
|
539
654
|
}
|
|
540
655
|
export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
541
656
|
// 0.19.0 静默剥键/静默承运门:解析**成功**的域,把 schema 丢掉的键、以及 open-world 域里承运但本包
|
|
542
657
|
// 不认识的键(都含嵌套,点号路径)点名(行为不变,只是不再无声)。判据与 `getDomain` 同源
|
|
543
658
|
// ({@link parseDomainLoud})。只在成功路径上报——降级路径已经有 domain-defaulted 那条更重的警告。
|
|
544
659
|
const parseLoud = (domain, raw) => parseDomainLoud(domain, raw, onWarning);
|
|
660
|
+
/**
|
|
661
|
+
* 一层读的**告警出口**,外面裹了那一层的坐标换算(S-323b 统一律)。
|
|
662
|
+
*
|
|
663
|
+
* 读路径上有**两个压缩点**:{@link grandfatherHostsRead}(parse 之前按名剔 host 条目)与
|
|
664
|
+
* {@link salvageEntries}(整域 parse 失败后剔坏条目)。律只有一条:**一条告警用「铸它的那一层所见
|
|
665
|
+
* 文档」的坐标表达;哪一层压缩了文档,哪一层就把从它下方上来的告警回写一次。** 组合因此是自然的
|
|
666
|
+
* ——内层先换算、外层后换算,不重不漏,而每一层都只需要知道**自己**那本账。
|
|
667
|
+
*
|
|
668
|
+
* 位移表**空**(这一层这次什么都没压缩)⇒ 原样返回 `onWarning`,连一层闭包都不套 —— 与
|
|
669
|
+
* {@link rebaseWarningPaths} 的恒等纪律同一条律:没发生的事不留痕迹。
|
|
670
|
+
*/
|
|
671
|
+
const exitOf = (keptIndex) => onWarning === undefined || Object.keys(keptIndex).length === 0 ? onWarning : (w) => onWarning(rebaseWarningPaths(w, keptIndex));
|
|
545
672
|
// Generic READ guardrail (0.8.1): a domain that no longer parses degrades to its schema default + a warning
|
|
546
673
|
// — but ONLY where DOMAIN_READ_FALLBACK says the default is safe; gate-bearing domains keep throwing (决策
|
|
547
674
|
// 表与逐域理由见 DOMAIN_READ_FALLBACK). The success path is UNCHANGED (same parseDomain, same bytes).
|
|
548
|
-
|
|
675
|
+
//
|
|
676
|
+
// `emit` = 这一腿的告警出口(默认 = 本函数的 `onWarning`)。**刻意不收「外层压缩了多少」这种入参**:
|
|
677
|
+
// parseOr 不必知道自己上面还有没有一层,它只管把告警交给给它的那个出口 —— 换算是出口的事。
|
|
678
|
+
const parseOr = (domain, raw, emit = onWarning) => {
|
|
549
679
|
try {
|
|
550
|
-
return
|
|
680
|
+
return parseDomainLoud(domain, raw, emit);
|
|
551
681
|
}
|
|
552
682
|
catch (e) {
|
|
553
683
|
if (DOMAIN_READ_FALLBACK[domain] === "throw")
|
|
@@ -560,17 +690,20 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
560
690
|
try {
|
|
561
691
|
const buffered = [];
|
|
562
692
|
const parsed = parseDomainLoud(domain, salvage.raw, (w) => buffered.push(w));
|
|
693
|
+
// 统一律落地:`entry-dropped` 是**本层**(salvage)铸的,用的就是本层入参那份文档的坐标 ⇒
|
|
694
|
+
// 不过本层自己的表,直接交给出口(出口会补上更外层的换算);`buffered` 里的是对**本层压缩后**
|
|
695
|
+
// 文档的诊断,是从下方上来的 ⇒ 先过本层的表,再交给出口。scan 函数一字不动。
|
|
563
696
|
for (const d of salvage.dropped)
|
|
564
|
-
|
|
697
|
+
emit?.({ domain, kind: "entry-dropped", path: d.path, error: d.error });
|
|
565
698
|
for (const w of buffered)
|
|
566
|
-
|
|
699
|
+
emit?.(rebaseWarningPaths(w, salvage.keptIndex));
|
|
567
700
|
return parsed;
|
|
568
701
|
}
|
|
569
702
|
catch {
|
|
570
703
|
// 条目之外还有毛病(重名 / 悬空 ref / 域级判据)—— 丢条目救不了它,照常整域降级。
|
|
571
704
|
}
|
|
572
705
|
}
|
|
573
|
-
|
|
706
|
+
emit?.({ domain, kind: "domain-defaulted", error: e });
|
|
574
707
|
return parseDomain(domain, undefined); // schema default — always valid
|
|
575
708
|
}
|
|
576
709
|
};
|
|
@@ -609,7 +742,9 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
609
742
|
systems: parseOr("systems", getRaw("systems")),
|
|
610
743
|
collab: parseOr("collab", getRaw("collab")),
|
|
611
744
|
workers: parseLoud("workers", getRaw("workers")),
|
|
612
|
-
|
|
745
|
+
// hosts 是读路径上**唯一**有外层压缩点的腿:`hostsGf.raw` 已被 grandfather 按名剔过条目,所以这一腿的
|
|
746
|
+
// 告警出口要裹上它的位移表(统一律:哪一层压缩,哪一层回写从它下方上来的告警)。
|
|
747
|
+
hosts: parseOr("hosts", hostsGf.raw, exitOf(hostsGf.keptIndex)),
|
|
613
748
|
runtime,
|
|
614
749
|
governance,
|
|
615
750
|
entitlement: parseLoud("entitlement", getRaw("entitlement")),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/settings-schema",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-registry, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|