@sema-agent/settings-schema 10.0.0 → 10.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.
Files changed (22) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +10 -1
  3. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +2 -1
  4. package/conformance/cases/c02-hosts-clamp-stacked/expected.json +2 -1
  5. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +4 -2
  6. package/conformance/cases/c23-mixed-validity-models/expected.json +2 -1
  7. package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +14 -7
  8. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +14 -7
  9. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +10 -5
  10. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +4 -2
  11. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +10 -5
  12. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/expected.json +2 -1
  13. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +2 -1
  14. package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +2 -1
  15. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +2 -1
  16. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +4 -2
  17. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +4 -2
  18. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +2 -1
  19. package/dist/config-fns.d.ts +25 -2
  20. package/dist/config-fns.js +40 -16
  21. package/dist/conformance/types.d.ts +1 -1
  22. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,38 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 10.1.0 — 2026-10-10 — 被丢条目带机读原因 `reasonCode`(minor,additive:新导出 + 可选新键,零删零改名)
10
+
11
+ > 本节一件,additive(新导出 + 新字段),零删零改名。判档口径见下方「发版纪律」:新键 / 新导出 = minor。
12
+
13
+ ### S-1295 · 被丢条目带**机读原因**(`reasonCode`,闭集,additive)
14
+
15
+ `DroppedEntry`(`salvageEntries` 的逐条记账行)与 `EffectiveReadWarning` 的 `entry-dropped` 臂各加可选键 `reasonCode?: DroppedEntryReasonCode`
16
+ (本包铸的每一条都带 —— 铸点返回型是必填窄型,漏带词编译红;公开型上可选只为让手工构造这两个公开型的消费方不因本版编译红 —— 新键按发版纪律是可选键)。
17
+ 新导出 `DROPPED_ENTRY_REASON_CODES`(闭集常量)与 `DroppedEntryReasonCode`(型)。`error` 句照旧,不属本件。
18
+
19
+ **词与丢弃点一一对应,没有 other 兜底**(`DroppedEntry` 只有一个铸点,它的判据 `judgeEntry` 按「谁判、判成什么样」分三种丢法,读实现所得):
20
+
21
+ | 词 | 丢弃点 |
22
+ |---|---|
23
+ | `entry-schema-invalid` | 条目(数组元素 / Record 的值)没过它自己的条目 schema |
24
+ | `entry-key-invalid` | Record 的**键**没过键 schema(键先判:键坏的条目不再算值) |
25
+ | `entry-unreadable` | 判的过程抛了(病态访问器 / Proxy 陷阱);文案可能带秘密,`error` 只留一句中性话,词本身是闭集字面 |
26
+
27
+ 投影(`projectWarning`)把 `reasonCode` 带进去(缺席不铸键),一致性门比它;historical 模式的归一器与 `identity` 一同两侧剥掉(旧版铸不出)。
28
+
29
+ - **谁需要** = server `GET /v1/models` 的丢弃条目自报(S-1295):wire 上只许闭集词,不许 zod 句子;词随告警过境,server 不手抄。之后 client-core / cli 读出。
30
+ - **谁受伤** = 无。两处都是可选键:10.0.0 形的 `DroppedEntry` / `EntrySalvageResult` / `entry-dropped` 告警字面量照常编译(`test/dropped-entry-reason-code-s1295.test.ts` 的编译格咬着,
31
+ server 测试里手写的 `entry-dropped` 字面量同属此形);`DroppedEntry` 在 server / web-admin / sdk / client-core / cli / core 的 `src` 与 `test` 里零引用(逐仓 grep 核过)。
32
+ 自己**重铸** `entry-dropped` 告警再交给一致性 kit 的读腿会在告警面报 `missing` / `extra`。
33
+ - **谁补偿** = 一致性语料已再加冕(`npm run conformance:bless`,16 条 case 的 40 处告警投影(跨腿计)多出 `reasonCode`,零删、零改其它字段,`baseline.json` 零变化),消费方跑 release 门前随包拿到新语料;
34
+ 消费方**自己入库**的语料(例:web-admin `wa05-*`)提货时要按新读数重新加冕。
35
+ - **边界(如实登记)**:① 本表只管 `DroppedEntry`;读路径上另一条按名剔条目的臂(`hosts-grandfathered`,主机名超长)是它自己的告警臂,不带词;② 词按**丢弃点**分,
36
+ 不按 schema 里「哪个字段为什么没过」细分(那是 `error` 句的事,句子不是契约);③ `entry-unreadable` 的病态访问器 JSON 语料表达不出,由
37
+ `test/dropped-entry-reason-code-s1295.test.ts` 看着(语料里 `entry-key-invalid` / `entry-schema-invalid` 由 `conformance/cases` 的金样与变异靶压着)。
38
+ - **红先**:`test/dropped-entry-reason-code-s1295.test.ts` 对 10.0.0 源 **16 红 / 0 绿**(`reasonCode` 字段不存在、`DROPPED_ENTRY_REASON_CODES` 未导出),改源后 16 / 16;
39
+ 逐点变异自证(8 处变异全杀)。codex 一审 [medium] 后补两格(旧形字面量编译格 + 本包铸的行恒带词):对「公开型必填」的中间稿 `tsc` 红(TS2741),改可选后绿。
40
+
9
41
  ## 10.0.0 — 2026-10-09 — 私钥 PEM 头认串中任意位置(BREAKING:写入校验认串中任意位置的私钥 PEM 头;读侧同套 schema ⇒ 存量私钥串的条目被丢 / 域 throw)
10
42
 
11
43
  > **判档 = major**(README「发版纪律」:校验收紧、让同一份配置读写出不同结果 ⇒ major)。破坏的是**持久数据面**,不是类型面:导出面登记表零 diff(没有新增 / 删除 / 改形的导出),
package/README.md CHANGED
@@ -307,7 +307,16 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
307
307
  dropped without re-reading the source document by coordinate (the document it holds may not be the one this
308
308
  read saw). `identity` is part of the warning projection, so the conformance gate compares it (corpus
309
309
  cases `c40` / `c41` pin the laws JSON can express; the historical normalizer strips it, since older versions cannot
310
- mint it — `ConformanceNormalizer.warning?`). **3.0.0** re-casts every coordinate these warnings
310
+ mint it — `ConformanceNormalizer.warning?`). **S-1295** (additive) adds `reasonCode` — WHY the entry was
311
+ dropped, as a machine-readable CLOSED word (`DROPPED_ENTRY_REASON_CODES` / `DroppedEntryReasonCode`; also on
312
+ `DroppedEntry` from `salvageEntries`): `entry-schema-invalid` (the entry — array element or Record value — failed its
313
+ own entry schema), `entry-key-invalid` (a Record key failed the key schema), `entry-unreadable` (validation threw while
314
+ reading it — a hostile accessor / Proxy trap; its message is never surfaced). One word per drop point, no `other`
315
+ fallback; the `error` sentence is unchanged and stays free text, so a consumer that must put a reason on a wire
316
+ reads the word, not the sentence. Every `entry-dropped` this package mints carries it (the key is optional on the
317
+ warning type only so consumers that hand-build that union keep compiling); like `identity` it is part of the warning
318
+ projection, compared by the conformance gate and stripped by the historical normalizer. The set covers `DroppedEntry`
319
+ only — the host-name clamp (`hosts-grandfathered`) is its own warning arm and carries no word. **3.0.0** re-casts every coordinate these warnings
311
320
  carry (`entry-dropped.path`, `unknown-keys-*.keys`) from a dot-joined STRING to a structured
312
321
  `ConfigPath = readonly (string | number)[]` — a string segment is an object key (verbatim, `.` included),
313
322
  a number segment is an array index — because the string form was a LOSSY encoding: a key literally named
@@ -79,7 +79,8 @@
79
79
  "path": [
80
80
  "models",
81
81
  0
82
- ]
82
+ ],
83
+ "reasonCode": "entry-schema-invalid"
83
84
  },
84
85
  {
85
86
  "domain": "models",
@@ -81,7 +81,8 @@
81
81
  "path": [
82
82
  "hosts",
83
83
  2
84
- ]
84
+ ],
85
+ "reasonCode": "entry-schema-invalid"
85
86
  }
86
87
  ],
87
88
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "roles",
82
82
  "[redacted-secret]"
83
- ]
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
84
85
  },
85
86
  {
86
87
  "domain": "models",
@@ -88,7 +89,8 @@
88
89
  "path": [
89
90
  "roles",
90
91
  "[redacted-secret]"
91
- ]
92
+ ],
93
+ "reasonCode": "entry-key-invalid"
92
94
  }
93
95
  ],
94
96
  "metadata": {
@@ -91,7 +91,8 @@
91
91
  "path": [
92
92
  "models",
93
93
  1
94
- ]
94
+ ],
95
+ "reasonCode": "entry-schema-invalid"
95
96
  }
96
97
  ],
97
98
  "metadata": {
@@ -83,7 +83,8 @@
83
83
  "path": [
84
84
  "models",
85
85
  1
86
- ]
86
+ ],
87
+ "reasonCode": "entry-schema-invalid"
87
88
  },
88
89
  {
89
90
  "domain": "models",
@@ -96,7 +97,8 @@
96
97
  "path": [
97
98
  "models",
98
99
  2
99
- ]
100
+ ],
101
+ "reasonCode": "entry-schema-invalid"
100
102
  },
101
103
  {
102
104
  "domain": "models",
@@ -107,7 +109,8 @@
107
109
  "path": [
108
110
  "models",
109
111
  3
110
- ]
112
+ ],
113
+ "reasonCode": "entry-schema-invalid"
111
114
  },
112
115
  {
113
116
  "domain": "models",
@@ -118,7 +121,8 @@
118
121
  "path": [
119
122
  "models",
120
123
  4
121
- ]
124
+ ],
125
+ "reasonCode": "entry-schema-invalid"
122
126
  },
123
127
  {
124
128
  "domain": "models",
@@ -129,7 +133,8 @@
129
133
  "path": [
130
134
  "models",
131
135
  5
132
- ]
136
+ ],
137
+ "reasonCode": "entry-schema-invalid"
133
138
  },
134
139
  {
135
140
  "domain": "models",
@@ -140,7 +145,8 @@
140
145
  "path": [
141
146
  "models",
142
147
  6
143
- ]
148
+ ],
149
+ "reasonCode": "entry-schema-invalid"
144
150
  },
145
151
  {
146
152
  "domain": "models",
@@ -148,7 +154,8 @@
148
154
  "path": [
149
155
  "roles",
150
156
  "[redacted-secret]"
151
- ]
157
+ ],
158
+ "reasonCode": "entry-key-invalid"
152
159
  }
153
160
  ],
154
161
  "metadata": {
@@ -83,7 +83,8 @@
83
83
  "path": [
84
84
  "models",
85
85
  1
86
- ]
86
+ ],
87
+ "reasonCode": "entry-schema-invalid"
87
88
  },
88
89
  {
89
90
  "domain": "models",
@@ -94,7 +95,8 @@
94
95
  "path": [
95
96
  "models",
96
97
  2
97
- ]
98
+ ],
99
+ "reasonCode": "entry-schema-invalid"
98
100
  },
99
101
  {
100
102
  "domain": "models",
@@ -105,7 +107,8 @@
105
107
  "path": [
106
108
  "models",
107
109
  3
108
- ]
110
+ ],
111
+ "reasonCode": "entry-schema-invalid"
109
112
  },
110
113
  {
111
114
  "domain": "models",
@@ -117,7 +120,8 @@
117
120
  "path": [
118
121
  "models",
119
122
  4
120
- ]
123
+ ],
124
+ "reasonCode": "entry-schema-invalid"
121
125
  },
122
126
  {
123
127
  "domain": "models",
@@ -129,7 +133,8 @@
129
133
  "path": [
130
134
  "models",
131
135
  5
132
- ]
136
+ ],
137
+ "reasonCode": "entry-schema-invalid"
133
138
  },
134
139
  {
135
140
  "domain": "models",
@@ -140,7 +145,8 @@
140
145
  "path": [
141
146
  "models",
142
147
  6
143
- ]
148
+ ],
149
+ "reasonCode": "entry-schema-invalid"
144
150
  },
145
151
  {
146
152
  "domain": "models",
@@ -148,7 +154,8 @@
148
154
  "path": [
149
155
  "roles",
150
156
  "[redacted-secret]"
151
- ]
157
+ ],
158
+ "reasonCode": "entry-key-invalid"
152
159
  }
153
160
  ],
154
161
  "metadata": {
@@ -83,7 +83,8 @@
83
83
  "path": [
84
84
  "models",
85
85
  1
86
- ]
86
+ ],
87
+ "reasonCode": "entry-schema-invalid"
87
88
  },
88
89
  {
89
90
  "domain": "models",
@@ -95,7 +96,8 @@
95
96
  "path": [
96
97
  "models",
97
98
  2
98
- ]
99
+ ],
100
+ "reasonCode": "entry-schema-invalid"
99
101
  },
100
102
  {
101
103
  "domain": "models",
@@ -103,7 +105,8 @@
103
105
  "path": [
104
106
  "roles",
105
107
  "[redacted-secret]"
106
- ]
108
+ ],
109
+ "reasonCode": "entry-key-invalid"
107
110
  },
108
111
  {
109
112
  "domain": "models",
@@ -111,7 +114,8 @@
111
114
  "path": [
112
115
  "roles",
113
116
  "[redacted-secret]"
114
- ]
117
+ ],
118
+ "reasonCode": "entry-key-invalid"
115
119
  },
116
120
  {
117
121
  "domain": "models",
@@ -119,7 +123,8 @@
119
123
  "path": [
120
124
  "roles",
121
125
  "[redacted-secret]"
122
- ]
126
+ ],
127
+ "reasonCode": "entry-key-invalid"
123
128
  }
124
129
  ],
125
130
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "roles",
82
82
  "[redacted-secret]"
83
- ]
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
84
85
  },
85
86
  {
86
87
  "domain": "models",
@@ -88,7 +89,8 @@
88
89
  "path": [
89
90
  "roles",
90
91
  "path%2Fto%E4%B8%AD"
91
- ]
92
+ ],
93
+ "reasonCode": "entry-key-invalid"
92
94
  },
93
95
  {
94
96
  "domain": "models",
@@ -110,7 +110,8 @@
110
110
  "path": [
111
111
  "models",
112
112
  1
113
- ]
113
+ ],
114
+ "reasonCode": "entry-schema-invalid"
114
115
  },
115
116
  {
116
117
  "domain": "models",
@@ -121,7 +122,8 @@
121
122
  "path": [
122
123
  "models",
123
124
  2
124
- ]
125
+ ],
126
+ "reasonCode": "entry-schema-invalid"
125
127
  },
126
128
  {
127
129
  "domain": "models",
@@ -132,7 +134,8 @@
132
134
  "path": [
133
135
  "models",
134
136
  3
135
- ]
137
+ ],
138
+ "reasonCode": "entry-schema-invalid"
136
139
  },
137
140
  {
138
141
  "domain": "models",
@@ -144,7 +147,8 @@
144
147
  "path": [
145
148
  "models",
146
149
  4
147
- ]
150
+ ],
151
+ "reasonCode": "entry-schema-invalid"
148
152
  },
149
153
  {
150
154
  "domain": "models",
@@ -167,7 +171,8 @@
167
171
  "path": [
168
172
  "servers",
169
173
  1
170
- ]
174
+ ],
175
+ "reasonCode": "entry-schema-invalid"
171
176
  }
172
177
  ],
173
178
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "roles",
82
82
  "https://admin:hunter2@db.example.com/x"
83
- ]
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
84
85
  }
85
86
  ],
86
87
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "models",
82
82
  0
83
- ]
83
+ ],
84
+ "reasonCode": "entry-schema-invalid"
84
85
  }
85
86
  ],
86
87
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "roles",
82
82
  "[redacted-secret]"
83
- ]
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
84
85
  }
85
86
  ],
86
87
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "roles",
82
82
  "[redacted-secret]"
83
- ]
83
+ ],
84
+ "reasonCode": "entry-key-invalid"
84
85
  }
85
86
  ],
86
87
  "metadata": {
@@ -76,7 +76,8 @@
76
76
  "path": [
77
77
  "models",
78
78
  0
79
- ]
79
+ ],
80
+ "reasonCode": "entry-schema-invalid"
80
81
  },
81
82
  {
82
83
  "domain": "models",
@@ -84,7 +85,8 @@
84
85
  "path": [
85
86
  "models",
86
87
  1
87
- ]
88
+ ],
89
+ "reasonCode": "entry-schema-invalid"
88
90
  }
89
91
  ],
90
92
  "metadata": {
@@ -89,7 +89,8 @@
89
89
  "path": [
90
90
  "models",
91
91
  0
92
- ]
92
+ ],
93
+ "reasonCode": "entry-schema-invalid"
93
94
  },
94
95
  {
95
96
  "domain": "models",
@@ -97,7 +98,8 @@
97
98
  "path": [
98
99
  "models",
99
100
  1
100
- ]
101
+ ],
102
+ "reasonCode": "entry-schema-invalid"
101
103
  }
102
104
  ],
103
105
  "metadata": {
@@ -80,7 +80,8 @@
80
80
  "path": [
81
81
  "models",
82
82
  1
83
- ]
83
+ ],
84
+ "reasonCode": "entry-schema-invalid"
84
85
  },
85
86
  {
86
87
  "domain": "models",
@@ -192,12 +192,31 @@ export interface DroppedEntryIdentity {
192
192
  id?: string;
193
193
  enabled?: boolean;
194
194
  }
195
+ /**
196
+ * 一条条目为什么被丢 —— **机读原因**的闭集(S-1295)。词与**丢弃点**一一对应:`DroppedEntry` 只有一个铸点
197
+ * (`droppedEntry`),它的上游判据(`judgeEntry`)按「谁判、判成什么样」分三种丢法,每种一个词,没有 other 兜底:
198
+ * - `entry-schema-invalid` —— 条目(数组元素 / Record 的值)没过它自己的条目 schema;
199
+ * - `entry-key-invalid` —— Record 的**键**没过键 schema(键先判:键坏的条目不再算值);
200
+ * - `entry-unreadable` —— 判的过程抛了(病态访问器 / Proxy 陷阱;文案可能带秘密,所以只留一句中性话,见 {@link judgeEntry})。
201
+ *
202
+ * 为什么是词而不是 `error` 句:句子随 zod 小版本改字、可经脱敏,拿它当判据就是拿实现当契约;wire 上也不许有自由文本。
203
+ * 这张表由 `test/dropped-entry-reason-code-s1295.test.ts` 的两格咬住:穷举 `switch`(给表加词而不补臂 ⇒ `tsc` 红,门 = `npm run typecheck`)与
204
+ * 满射格(每个词至少有一个真丢弃点触发它 = 没有死词;每个被丢条目的词都在表里 = 没有无词的丢弃点)。
205
+ * 边界(如实登记):本表只管 `salvageEntries` 铸的 {@link DroppedEntry};读路径上另一条按名剔条目的臂
206
+ * (`hosts-grandfathered`,主机名超长)走它自己的告警臂,不是 `DroppedEntry`,不在本表。
207
+ */
208
+ export declare const DROPPED_ENTRY_REASON_CODES: readonly ["entry-schema-invalid", "entry-key-invalid", "entry-unreadable"];
209
+ export type DroppedEntryReasonCode = (typeof DROPPED_ENTRY_REASON_CODES)[number];
195
210
  /** 一条被丢掉的条目:`path` = 段坐标({@link ConfigPath};`["models", 3]` / `["roles", "summarize"]`),
196
- * `error` 已脱敏,`identity`(S-712)见 {@link DroppedEntryIdentity}(读不出 ⇒ 键缺席)。 */
211
+ * `error` 已脱敏,`identity`(S-712)见 {@link DroppedEntryIdentity}(读不出 ⇒ 键缺席),
212
+ * `reasonCode`(S-1295)= 为什么丢的**机读闭集词**,见 {@link DROPPED_ENTRY_REASON_CODES}。本包铸的每一行都带(铸点是唯一的,返回型是必填窄型
213
+ * {@link MintedDroppedEntry},漏带词 = 编译红);公开型上是可选键,只因为这个接口是公开导出的,手工构造它的消费方(夹具、适配器)不该被一个
214
+ * additive 的 minor 编译红 —— 发版纪律:新键是可选键。 */
197
215
  export interface DroppedEntry {
198
216
  path: ConfigPath;
199
217
  error: unknown;
200
218
  identity?: DroppedEntryIdentity;
219
+ reasonCode?: DroppedEntryReasonCode;
201
220
  }
202
221
  /** 可披露身份的**闭形**(6.3.0):字符集 `A-Z a-z 0-9 . _ : / @ + -`、长度 1–256(上限与 web-admin 读侧名长上限 256 对齐;`ModelEntry.name`
203
222
  * 本身不设上限,超 256 字的合法名按无名处置)。身份是**标识符**不是自由文本 ——
@@ -359,13 +378,17 @@ export type EffectiveReadWarning =
359
378
  * O(域里的条目数),与被读文档同阶,不放大,故不设条数上限(有上限的是**单条**告警里的键列表)。
360
379
  * 2.0.0 起;只可能来自 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域,gate 域坏形仍然 throw。
361
380
  * **S-712**(additive):`identity` = 被丢条目原文里读得出的身份({@link DroppedEntryIdentity};
362
- * 读不出 ⇒ 键缺席),消费方据此知道「丢的是谁」而不必拿坐标回读原文档。 */
381
+ * 读不出 ⇒ 键缺席),消费方据此知道「丢的是谁」而不必拿坐标回读原文档。
382
+ * **S-1295**(additive):`reasonCode` = 为什么丢的机读闭集词({@link DROPPED_ENTRY_REASON_CODES}),消费方据此知道「为什么丢」
383
+ * 而不必解析 `error` 句。本包铸的每一条都带;类型上是可选键只因为这个联合有手工构造它的消费方(测试夹具、别家实现的适配器)——
384
+ * 新键按发版纪律是可选键,不因此让他们编译红。 */
363
385
  | {
364
386
  domain: DomainName;
365
387
  kind: "entry-dropped";
366
388
  path: ConfigPath;
367
389
  error: unknown;
368
390
  identity?: DroppedEntryIdentity;
391
+ reasonCode?: DroppedEntryReasonCode;
369
392
  }
370
393
  /** `config.d/` 里存在、但本地读取集**永远不会读**的文件(名字不是域,或是域但不便携)。 */
371
394
  | {
@@ -223,6 +223,20 @@ function outgoingPath(p) {
223
223
  function outgoingSegment(seg) {
224
224
  return seg.length > DISCLOSABLE_IDENTITY_MAX ? "[redacted-secret]" : redactSecrets(seg);
225
225
  }
226
+ /**
227
+ * 一条条目为什么被丢 —— **机读原因**的闭集(S-1295)。词与**丢弃点**一一对应:`DroppedEntry` 只有一个铸点
228
+ * (`droppedEntry`),它的上游判据(`judgeEntry`)按「谁判、判成什么样」分三种丢法,每种一个词,没有 other 兜底:
229
+ * - `entry-schema-invalid` —— 条目(数组元素 / Record 的值)没过它自己的条目 schema;
230
+ * - `entry-key-invalid` —— Record 的**键**没过键 schema(键先判:键坏的条目不再算值);
231
+ * - `entry-unreadable` —— 判的过程抛了(病态访问器 / Proxy 陷阱;文案可能带秘密,所以只留一句中性话,见 {@link judgeEntry})。
232
+ *
233
+ * 为什么是词而不是 `error` 句:句子随 zod 小版本改字、可经脱敏,拿它当判据就是拿实现当契约;wire 上也不许有自由文本。
234
+ * 这张表由 `test/dropped-entry-reason-code-s1295.test.ts` 的两格咬住:穷举 `switch`(给表加词而不补臂 ⇒ `tsc` 红,门 = `npm run typecheck`)与
235
+ * 满射格(每个词至少有一个真丢弃点触发它 = 没有死词;每个被丢条目的词都在表里 = 没有无词的丢弃点)。
236
+ * 边界(如实登记):本表只管 `salvageEntries` 铸的 {@link DroppedEntry};读路径上另一条按名剔条目的臂
237
+ * (`hosts-grandfathered`,主机名超长)走它自己的告警臂,不是 `DroppedEntry`,不在本表。
238
+ */
239
+ export const DROPPED_ENTRY_REASON_CODES = ["entry-schema-invalid", "entry-key-invalid", "entry-unreadable"];
226
240
  /** 可披露身份与出门坐标段的**长度上界**(一只常量,两处用:{@link DISCLOSABLE_IDENTITY_RE} 的上限、{@link outgoingSegment}
227
241
  * 的「超长不扫描」界)。与 web-admin 读侧名长上限 256 对齐。 */
228
242
  const DISCLOSABLE_IDENTITY_MAX = 256;
@@ -290,24 +304,34 @@ function readEntryIdentity(entry) {
290
304
  /** 一条记账行 → 它的 `entry-dropped` 告警。域面({@link buildEffective})与 wire 面(skills manifest)共用
291
305
  * 这**一只** —— 告警形只在这里拼,新字段不会在一条腿上漏铸。 */
292
306
  function entryDroppedWarning(domain, d) {
293
- return d.identity === undefined ? { domain, kind: "entry-dropped", path: d.path, error: d.error } : { domain, kind: "entry-dropped", path: d.path, error: d.error, identity: d.identity };
307
+ return {
308
+ domain,
309
+ kind: "entry-dropped",
310
+ path: d.path,
311
+ error: d.error,
312
+ ...(d.reasonCode === undefined ? {} : { reasonCode: d.reasonCode }),
313
+ ...(d.identity === undefined ? {} : { identity: d.identity }),
314
+ };
294
315
  }
295
- /** 一条被丢条目的记账行 —— `salvageEntries` 两种集合形(数组 / Record)共用的**唯一**铸点。 */
296
- function droppedEntry(path, error, entry) {
316
+ /** 一条被丢条目的记账行 —— `salvageEntries` 两种集合形(数组 / Record)共用的**唯一**铸点。词随判决来({@link EntryVerdict}),
317
+ * 不在这里另行推断:判的那一刻才知道是哪种丢法。 */
318
+ function droppedEntry(path, verdict, entry) {
297
319
  const identity = readEntryIdentity(entry);
298
- return identity === undefined ? { path, error } : { path, error, identity };
320
+ const base = { path, error: verdict.error, reasonCode: verdict.reasonCode };
321
+ return identity === undefined ? base : { ...base, identity };
299
322
  }
300
323
  /** 一次「什么都没丢」的抢救的位移表(冻结的共享空表 —— {@link rebaseWarningPaths} 对它恒等)。 */
301
324
  const NO_ENTRIES_DROPPED = Object.freeze({});
302
- /** 一条条目的 parse —— 坏就返回它的(脱敏)错误。恶意 getter/Proxy 在遍历中抛错时**绝不**外泄其文案
303
- * (可能带 secret,与 parseDomain 的 R13 同纪律),只留一句中性话。 */
304
- function judgeEntry(schema, value) {
325
+ /** 一条条目的 parse —— 坏就返回它的判决(脱敏错误 + 闭集词)。`rejectedAs` = 调用点在集合里的位置赋予「schema 没过」的那个词
326
+ * (条目 / Record 值 ⇒ `entry-schema-invalid`;Record 键 ⇒ `entry-key-invalid`);判的过程抛了则恒为 `entry-unreadable`,与位置无关。
327
+ * 恶意 getter/Proxy 在遍历中抛错时**绝不**外泄其文案(可能带 secret,与 parseDomain 的 R13 同纪律),只留一句中性话。 */
328
+ function judgeEntry(schema, value, rejectedAs) {
305
329
  try {
306
330
  const r = schema.safeParse(value);
307
- return r.success ? undefined : sanitizeZodError(r.error);
331
+ return r.success ? undefined : { error: sanitizeZodError(r.error), reasonCode: rejectedAs };
308
332
  }
309
333
  catch {
310
- return new Error("entry could not be read during validation");
334
+ return { error: new Error("entry could not be read during validation"), reasonCode: "entry-unreadable" };
311
335
  }
312
336
  }
313
337
  /**
@@ -369,12 +393,12 @@ function salvageEntriesUnsafe(schema, raw) {
369
393
  // 会跟着一个在校验期间自我增殖的数组一路跑下去(病态 getter 在 zod 读字段时往同一个数组里再 push
370
394
  // 一条)——抢救是尽力而为的旁路,绝不许把同步读挂死。下标在回调里收,`filter` 本来就给 index。
371
395
  const kept = value.filter((entry, i) => {
372
- const error = judgeEntry(element, entry);
373
- if (error === undefined) {
396
+ const verdict = judgeEntry(element, entry, "entry-schema-invalid");
397
+ if (verdict === undefined) {
374
398
  keptFrom.push(i); // 压缩后第 keptFrom.length-1 条 = 原文档第 i 条
375
399
  return true;
376
400
  }
377
- bad.push(droppedEntry(outgoingPath([field, i]), error, entry));
401
+ bad.push(droppedEntry(outgoingPath([field, i]), verdict, entry));
378
402
  return false;
379
403
  });
380
404
  // 零幸存者 ⇒ 没有要抢救的东西(见头注:绝不凭空造出一个空集合)。
@@ -390,11 +414,11 @@ function salvageEntriesUnsafe(schema, raw) {
390
414
  const bad = [];
391
415
  const kept = {};
392
416
  for (const [key, entry] of Object.entries(value)) {
393
- const error = judgeEntry(keySchema, key) ?? judgeEntry(valueSchema, entry);
394
- if (error === undefined)
417
+ const verdict = judgeEntry(keySchema, key, "entry-key-invalid") ?? judgeEntry(valueSchema, entry, "entry-schema-invalid");
418
+ if (verdict === undefined)
395
419
  kept[key] = entry;
396
420
  else
397
- bad.push(droppedEntry(outgoingPath([field, key]), error, entry)); // 键是用户写的,可能就是一条粘错位置的凭据
421
+ bad.push(droppedEntry(outgoingPath([field, key]), verdict, entry)); // 键是用户写的,可能就是一条粘错位置的凭据
398
422
  }
399
423
  if (bad.length > 0 && Object.keys(kept).length > 0) {
400
424
  dropped.push(...bad);
@@ -486,7 +510,7 @@ export function projectWarning(w) {
486
510
  case "legacy-governance-lifted":
487
511
  return { kind: w.kind, domain: w.domain, keys: w.keys };
488
512
  case "entry-dropped":
489
- return { kind: w.kind, domain: w.domain, path: w.path, ...(w.identity === undefined ? {} : { identity: w.identity }) };
513
+ return { kind: w.kind, domain: w.domain, path: w.path, ...(w.identity === undefined ? {} : { identity: w.identity }), ...(w.reasonCode === undefined ? {} : { reasonCode: w.reasonCode }) };
490
514
  case "unread-config-file":
491
515
  return { kind: w.kind, file: w.file, ...(w.domain === undefined ? {} : { domain: w.domain }), why: w.why };
492
516
  default: {
@@ -229,7 +229,7 @@ export interface ConformanceNormalizer {
229
229
  /** 把一条坐标归一到共同空间。有损归一器在这里把段拼成串。 */
230
230
  path(p: readonly (string | number)[]): unknown;
231
231
  /** 把一条**告警投影**归一到共同空间(可选;先于 {@link ConformanceNormalizer.path} 作用,期望与实际两侧同过)。
232
- * 用途:剥掉旧版**铸不出**的 additive 字段(S-712 的 `entry-dropped.identity`)—— 旧版没有这个字段不是读法
232
+ * 用途:剥掉旧版**铸不出**的 additive 字段(S-712 的 `entry-dropped.identity`、S-1295 的 `entry-dropped.reasonCode`)—— 旧版没有这个字段不是读法
233
233
  * 分歧,不许被记成「旧版行为红」。剥掉的字段同样是有损的一部分,`why` 里写明。release 模式禁归一器,故
234
234
  * 这只钩子只作用于 historical 读数。 */
235
235
  warning?(p: WarningProjection): WarningProjection;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "10.0.0",
3
+ "version": "10.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",