@sema-agent/settings-schema 14.6.0 → 14.8.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 (95) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/README.md +89 -5
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
  5. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
  6. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
  7. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
  8. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
  9. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
  10. package/dist/api/auth-bridge.d.ts +16 -2
  11. package/dist/api/auth-bridge.js +59 -24
  12. package/dist/api/credentials.d.ts +281 -0
  13. package/dist/api/credentials.js +149 -0
  14. package/dist/api/scopes.js +5 -1
  15. package/dist/atomic-write.d.ts +15 -0
  16. package/dist/atomic-write.js +109 -5
  17. package/dist/catalog-manages.d.ts +15 -0
  18. package/dist/catalog-manages.js +60 -0
  19. package/dist/center-refs.d.ts +68 -0
  20. package/dist/center-refs.js +70 -0
  21. package/dist/config-d-file.js +33 -15
  22. package/dist/config-dir-migration.js +1 -0
  23. package/dist/config-fns.js +147 -81
  24. package/dist/config-root-documents.d.ts +106 -0
  25. package/dist/config-root-documents.js +93 -0
  26. package/dist/config-root-node.d.ts +117 -0
  27. package/dist/config-root-node.js +260 -0
  28. package/dist/conformance/canonical.js +4 -4
  29. package/dist/conformance/contract-adapter.js +26 -1
  30. package/dist/conformance/corpus.js +4 -4
  31. package/dist/conformance/derive.js +2 -2
  32. package/dist/conformance/run.d.ts +3 -0
  33. package/dist/conformance/run.js +53 -12
  34. package/dist/conformance/temp-roots.d.ts +31 -0
  35. package/dist/conformance/temp-roots.js +184 -0
  36. package/dist/cross-session.d.ts +4 -4
  37. package/dist/declared-capture.d.ts +41 -0
  38. package/dist/declared-capture.js +344 -0
  39. package/dist/default-model.d.ts +1 -1
  40. package/dist/default-model.js +5 -4
  41. package/dist/env-model-table.d.ts +163 -0
  42. package/dist/env-model-table.js +257 -0
  43. package/dist/file-edit.js +11 -15
  44. package/dist/fleet.d.ts +36 -7
  45. package/dist/fleet.js +83 -32
  46. package/dist/hash.d.ts +0 -9
  47. package/dist/hash.js +64 -7
  48. package/dist/hooks-config.js +18 -3
  49. package/dist/http-hook-allowlists.d.ts +22 -9
  50. package/dist/http-hook-allowlists.js +169 -91
  51. package/dist/index.d.ts +7 -0
  52. package/dist/index.js +7 -0
  53. package/dist/mcp-request-entry.d.ts +67 -13
  54. package/dist/mcp-request-entry.js +132 -93
  55. package/dist/mcp-server-name.d.ts +17 -0
  56. package/dist/mcp-server-name.js +19 -0
  57. package/dist/migration-receipt.d.ts +10 -6
  58. package/dist/migration-receipt.js +5 -3
  59. package/dist/node.d.ts +1 -0
  60. package/dist/node.js +2 -0
  61. package/dist/one-time-migrations.d.ts +4 -2
  62. package/dist/one-time-migrations.js +236 -96
  63. package/dist/own-key-read.d.ts +247 -0
  64. package/dist/own-key-read.js +449 -0
  65. package/dist/plain-json-object.d.ts +11 -1
  66. package/dist/plain-json-object.js +37 -1
  67. package/dist/remote-exec.js +9 -1
  68. package/dist/retired-form.d.ts +19 -0
  69. package/dist/retired-form.js +24 -1
  70. package/dist/safety-merge-spec.d.ts +6 -6
  71. package/dist/safety-merge-spec.js +50 -37
  72. package/dist/sandbox-git.d.ts +64 -0
  73. package/dist/sandbox-git.js +89 -0
  74. package/dist/scheduler-store.js +19 -11
  75. package/dist/settings-units.js +14 -12
  76. package/dist/tier-words.d.ts +33 -0
  77. package/dist/tier-words.js +43 -0
  78. package/dist/types.d.ts +12 -2
  79. package/dist/types.js +35 -6
  80. package/dist/url-shape.d.ts +16 -0
  81. package/dist/url-shape.js +18 -0
  82. package/dist/web-admin-credential.js +5 -2
  83. package/dist/web-admin-session-url.js +2 -2
  84. package/dist/worker-row-verdict.d.ts +11 -0
  85. package/dist/worker-row-verdict.js +58 -2
  86. package/dist/workers-rows.js +38 -10
  87. package/dist/zod-unwrap.d.ts +6 -0
  88. package/dist/zod-unwrap.js +14 -7
  89. package/docs/API-SURFACE.md +100 -5
  90. package/docs/CONSUMER-INTEGRATION.md +250 -2
  91. package/llms-full.txt +26 -4
  92. package/llms.txt +11 -4
  93. package/package.json +2 -2
  94. package/dist/definition-copy.d.ts +0 -23
  95. package/dist/definition-copy.js +0 -52
@@ -0,0 +1,344 @@
1
+ /**
2
+ * 包内私有(不进主桶):**按声明键投影的快照,与校验合在一起** —— 校验器(zod)读一个对象时只读它声明的键、剥掉其余;要保护它别读到 `Object.prototype` 上被污染的继承值、别碰会抛的 getter,
3
+ * 就得给它一份**自有键副本**。整份深拷({@link captureJson})会把「校验器根本不读」的键(没声明的键里放着 `Date` / `Map` / 函数 / 环 / 一个会抛的 getter)也读进来 ——
4
+ * 一个本来读得通的 body 变成整体读不出。所以:**只捕获校验器会读的键**,而「会读哪些键」随联合选哪一臂而定,所以投影与校验一起走({@link parseDeclared})。
5
+ *
6
+ * · 对象形(`z.object`,没有 catchall —— 即「剥掉没声明的键」那一种):只读它声明的键(`shape` 的键),每个声明键的值按**对应子 schema** 递归;没声明的键一个都不读不拷;
7
+ * · **判别联合**:先按判别键(与声明键同一只描述符读)选臂,**只投影选中那一臂**的声明键;判别键**只读一次** —— 选臂的那个词就是快照里交给校验器的那个词;
8
+ * 判别键缺席 / 不是任何一臂的词 ⇒ 只交过去判别键(校验器只看它就拒);
9
+ * · **普通联合**(顶层):按序逐臂 —— 一臂一份只含该臂声明键的快照、**只拿该臂的 schema 校验**,该臂通过才接受、**第一臂通过即止**,没通过才轮到下一臂(与 zod 按序试臂一致;不取并集;
10
+ * 拿整个联合去校验的话,后臂会用默认值接受前一臂的残缺投影);**互斥联合**(`z.xor`)所有臂都走一遍、恰一臂通过才接受(两臂都通过 zod 拒,这里同样拒)。
11
+ * 嵌在对象里的普通联合、以及套着包装层的联合当不透明:整个值深拷、对整只 schema 校验;
12
+ * · `unknown` / `any`(投影走到的位置上):校验器自己不看它的内容 ⇒ 调用方的原值原样交过去(后面接的 transform 是包自己写的);
13
+ * · 其余(标量、`record`、数组、`strict` 的对象 —— 校验器要读全部键):整个值深拷(不透明)。
14
+ * 声明键的律与内核同:自有但不可枚举 ⇒ 读不成纯数据、整份拒({@link NOT_PLAIN_DATA}),**不当缺席**;自有 getter 抛 / 陷阱抛 ⇒ {@link UNINSPECTABLE};只靠继承 ⇒ 缺席。
15
+ * 任何一份快照拿不到(读不出 / 拒)⇒ 整个读不出(不再试后面的臂:v14.6.0 里校验器在那里读到的会抛)。守护在 {@link parseDeclared} 入口一处:门的 schema 内省、快照递归与校验同在里面。
16
+ *
17
+ * **门 = 闭集白名单**(`admit`,每只 schema 第一次进来时走一遍整棵 schema 树,通过的记下;递归 schema 按「节点 × 位置」只判一次):只放行**与 zod 同答**(判决与数据,对 JSON 输入)的节点种类
18
+ * —— 每一种都按 zod 4.4 / 4.6 的读法推过、有对 zod 逐例比判决与数据的钉;反例搜索不是穷举,找到不同答的形就从放行单拿掉 ——
19
+ * 叶子(不跑用户函数、没有子 schema)· 不跑看得见输入的函数的包装层(`optional` / `nullable` / `default` / `prefault`)· 后处理 `.transform(f)`(`f` 看到的是里面那只 schema 的输出,两边同一份)·
20
+ * 对象(声明键不是符号、也不叫寻常对象从 `Object.prototype` 继承来的名字 —— zod 在原值上读到继承的成员,投影读成缺席)· 判别联合(不带 `unionFallback`)· 普通 / 互斥联合 · `record` · 数组 ·
21
+ * 投影走到的位置上的 `unknown` / `any`。对象 / 数组 / record 上挂的检查(refine / superRefine / 长度 …)看到的是 zod 自己铸的新输出,两边同一份。
22
+ * 其余一律**响亮拒**(抛 `parseDeclared: unsupported schema shape — …`;这是包自己的 schema 选错了读法,不是调用方的值读不出):`.catch`(兜底函数看到整个原值 —— zod ≥ 4.6 交 `ctx.input`)·
23
+ * 输入侧变换(`z.preprocess`、字段位上单独的 `z.transform`、两侧都是 schema 的管道)· `z.custom`(谓词看到的是副本)· 深拷节点之下的 `unknown` / `any`(副本会进输出;passthrough 对象同因)·
24
+ * 声明键叫继承名 / 是符号(有限键集 record 的键同此)· loose record · 顶层普通联合自己挂的检查 · 带 `unionFallback` 的判别联合 · `tuple` / `intersection` / `map` / `set` / `lazy` / `readonly` …(没推过)。
25
+ * 不逐形补黑名单:新形默认拒。校验只问判决、不要句子(`evaluate` 给 zod 逐次 `error` 参数):副本的原型不是 `Object.prototype`,zod 拼拒句时会读副本上用户写的自有 `constructor`。
26
+ */
27
+ import { z } from "zod";
28
+ import { NOT_PLAIN_DATA, NO_PRIOR_READS, UNINSPECTABLE, captureJson, dataDescriptor, inspect, isolatedObject, readDeclaredKey } from "./own-key-read.js";
29
+ import { jsonShapeOf } from "./plain-json-object.js";
30
+ import { unwrapOnce } from "./zod-unwrap.js";
31
+ const failed = (sentence) => ({ ok: false, sentence });
32
+ /** 判语的句子(只在这里拼一次)。 */
33
+ const unsupportedShape = (what) => `parseDeclared: unsupported schema shape — ${what}`;
34
+ /** 包装层的层数界(schema 作者写出来的常数,8 层远超实际)。 */
35
+ const WRAPPER_BOUND = 8;
36
+ /** 剥掉投影会穿过的包装层拿内里的 schema:`optional` / `nullable` / `default` / `prefault` 与后处理 `.transform(f)`。`.catch` 与输入侧的变换不穿 —— 停在它们身上(白名单门拒它们)。 */
37
+ function innerOf(schema) {
38
+ let cur = schema;
39
+ for (let i = 0; i < WRAPPER_BOUND; i++) {
40
+ const step = unwrapOnce(cur);
41
+ if (step === undefined || step.via === "preprocess" || cur instanceof z.ZodCatch)
42
+ return cur;
43
+ cur = step.next;
44
+ }
45
+ return cur;
46
+ }
47
+ /**
48
+ * 对象 schema 的声明键表:读 `def.shape` —— zod 自己校验时读的就是这一份(它在第一次被读时才物化,物化失败下次再试);**不读** classic 面的 `object.shape`:
49
+ * 那是 zod 4.4 的另一只惰性属性,物化时抛(例:`Object.prototype` 被污染)就永远答 `undefined`,而 zod 自己校验从不读它 —— 读它会让门在污染撤掉之后仍读不出,v14.6.0 不会。
50
+ */
51
+ const shapeOf = (object) => object._zod.def.shape;
52
+ /** 普通联合(含互斥联合 `z.xor`),不是判别联合 —— `evaluate` 逐臂走它;白名单门按同一个判据认它。 */
53
+ const isPlainUnion = (schema) => schema instanceof z.ZodUnion && !(schema instanceof z.ZodDiscriminatedUnion);
54
+ /** 寻常对象从 `Object.prototype` 继承来的成员名(ES 规定的闭集)。声明键叫这些名字时,zod 在调用方的值上 `[[Get]]` 读到继承来的成员,投影把它读成缺席 —— 两边不同答。 */
55
+ const ORDINARY_OBJECT_MEMBERS = new Set(["constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf", "__proto__", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__"]);
56
+ /** 走过白名单门的 schema(按根;fleet 每次请求都是那七只,不重走)。 */
57
+ const ADMITTED = new WeakSet();
58
+ /** 白名单门(见文件头):整棵 schema 树只含放行的形 ⇒ 记下、交回 `undefined`;否则交回不支持的那一句(见 {@link Refusal})。与 `evaluate` / `snapshotOf` 同一套判据(`isPlainUnion` / `innerOf`)。 */
59
+ function admit(schema) {
60
+ if (ADMITTED.has(schema))
61
+ return undefined;
62
+ const refusal = admitEvaluated(schema, new Map());
63
+ if (refusal === undefined)
64
+ ADMITTED.add(schema);
65
+ return refusal;
66
+ }
67
+ /** 一串子节点逐个过门,第一句不支持就停(交回它)。 */
68
+ function admitEach(children, at, seen) {
69
+ for (const child of children) {
70
+ const refusal = admitAt(child, at, seen);
71
+ if (refusal !== undefined)
72
+ return refusal;
73
+ }
74
+ return undefined;
75
+ }
76
+ /** `evaluate` 走的那一层:普通联合逐臂;其余是一份投影。 */
77
+ function admitEvaluated(schema, seen) {
78
+ if (isPlainUnion(schema)) {
79
+ // `evaluate` 逐臂拿**臂的** schema 校验:挂在联合本身上的检查(`.refine` / `.superRefine`)一次都不跑 —— zod 会跑,两边不同答
80
+ if ((schema._zod.def.checks ?? []).length > 0)
81
+ return "a union with checks of its own at the top (`.refine` on the union: the per-arm evaluation validates with each arm's schema, so the union's own checks would never run)";
82
+ for (const option of schema._zod.def.options) {
83
+ const refusal = admitEvaluated(option, seen);
84
+ if (refusal !== undefined)
85
+ return refusal;
86
+ }
87
+ return undefined;
88
+ }
89
+ return admitAt(schema, "projected", seen);
90
+ }
91
+ function admitAt(schema, at, seen) {
92
+ const inner = innerOf(schema);
93
+ const positions = seen.get(inner) ?? new Set();
94
+ if (positions.has(at))
95
+ return undefined;
96
+ seen.set(inner, positions.add(at));
97
+ const kind = inner._zod.def.type;
98
+ switch (kind) {
99
+ // 叶子:不跑用户函数、没有子 schema —— 校验器只看这个值本身(原始值两边是同一个;对象两边都按形拒)
100
+ case "string":
101
+ case "number":
102
+ case "int":
103
+ case "boolean":
104
+ case "bigint":
105
+ case "symbol":
106
+ case "null":
107
+ case "undefined":
108
+ case "void":
109
+ case "never":
110
+ case "nan":
111
+ case "date":
112
+ case "literal":
113
+ case "enum":
114
+ case "template_literal":
115
+ case "file":
116
+ return undefined;
117
+ case "unknown":
118
+ case "any":
119
+ return at === "copied" ? "`unknown` / `any` under a copied node (record / array / a union inside a field / a catchall — passthrough included): the copy, not the caller's value, would reach the output" : undefined;
120
+ case "object": {
121
+ const object = inner;
122
+ const shape = shapeOf(object);
123
+ if (Object.getOwnPropertySymbols(shape).length > 0)
124
+ return "a symbol key (the projection reads string keys only)";
125
+ const catchall = object._zod.def.catchall;
126
+ for (const key of Object.keys(shape)) {
127
+ if (ORDINARY_OBJECT_MEMBERS.has(key))
128
+ return `a declared key named \`${key}\` (zod reads the member an ordinary object inherits; the projection reads it as absent)`;
129
+ const refusal = admitAt(shape[key], catchall === undefined ? at : "copied", seen);
130
+ if (refusal !== undefined)
131
+ return refusal;
132
+ }
133
+ return catchall !== undefined ? admitAt(catchall, "copied", seen) : undefined;
134
+ }
135
+ case "union": {
136
+ const options = inner._zod.def.options;
137
+ if (inner instanceof z.ZodDiscriminatedUnion) {
138
+ if (inner._zod.def.unionFallback === true)
139
+ return "a discriminated union with `unionFallback` (zod falls back to trying every arm on the full value; the projection of one arm's declared keys cannot say what it would do)";
140
+ return admitEach(options, at, seen);
141
+ }
142
+ return admitEach(options, "copied", seen); // 走到这里的普通联合是不透明的(嵌在字段里 / 套着包装层):整个值深拷
143
+ }
144
+ case "record": {
145
+ const record = inner;
146
+ if (record._zod.def.mode === "loose")
147
+ return "a loose record (keys the key schema does not match ride through by reference — the copy, not the caller's value, would reach the output)";
148
+ // 有限键集(枚举 / 字面量键):zod 逐个按键名在值上 `[[Get]]` —— 键名是继承名时读到继承的成员,副本上读成缺席(与对象声明键同一条)
149
+ for (const key of record._zod.def.keyType._zod.values ?? []) {
150
+ if (typeof key === "string" && ORDINARY_OBJECT_MEMBERS.has(key))
151
+ return `a record key named \`${key}\` in a finite key set (zod reads the member an ordinary object inherits; the copy reads it as absent)`;
152
+ }
153
+ return admitEach([record._zod.def.keyType, record._zod.def.valueType], "copied", seen);
154
+ }
155
+ case "array":
156
+ return admitAt(inner.element, "copied", seen);
157
+ case "catch":
158
+ return "`.catch` (its fallback sees the caller's whole value — zod ≥ 4.6 hands it `ctx.input`; a projection changes what it sees)";
159
+ case "pipe":
160
+ return `a pipe the projection does not pass through — an input-side \`z.preprocess(f, …)\` or \`a.pipe(b)\` (the function / the second schema would see a projection or a copy, not what zod sees), or wrappers nested deeper than ${WRAPPER_BOUND} layers`;
161
+ case "transform":
162
+ return "a bare `z.transform(f)` (an input-side function: it would see the projection, not the caller's value)";
163
+ case "custom":
164
+ return "`z.custom` / `z.instanceof` (the predicate would see the projection, not the caller's value)";
165
+ case "optional":
166
+ case "nullable":
167
+ case "default":
168
+ case "prefault":
169
+ return `wrappers nested deeper than ${WRAPPER_BOUND} layers`;
170
+ case "tuple":
171
+ case "intersection":
172
+ case "map":
173
+ case "set":
174
+ case "lazy":
175
+ case "promise":
176
+ case "function":
177
+ case "readonly":
178
+ case "nonoptional":
179
+ case "success":
180
+ return `\`${kind}\` (not on the allow-list: the projection has not been proven to answer as zod does)`;
181
+ default: {
182
+ const unhandled = kind;
183
+ return `an unknown schema kind \`${String(unhandled)}\``;
184
+ }
185
+ }
186
+ }
187
+ /** 一个值要不要进对象投影:是朴素对象 ⇒ `undefined`(继续投影);不是对象(`null` / 原始值)⇒ 原样交过去(校验器自己拒);数组 / 类实例 / 读不出 ⇒ 深拷(同一套坏形与句子)。 */
188
+ function nonObjectSnapshot(value) {
189
+ const kind = jsonShapeOf(value);
190
+ switch (kind) {
191
+ case "plain":
192
+ return undefined;
193
+ case "null":
194
+ case "primitive":
195
+ return { ok: true, value };
196
+ case "array":
197
+ case "non_plain_object":
198
+ case "uninspectable":
199
+ return captureJson(value);
200
+ default: {
201
+ const unhandled = kind;
202
+ throw new Error(`declared-capture: unhandled shape ${String(unhandled)}`);
203
+ }
204
+ }
205
+ }
206
+ /** 对象形投影:声明键逐个读(自有可枚举数据属性 ⇒ 取值按子 schema 递归;继承 / 缺席 ⇒ 跳过;隐藏 / 访问器 / 读不出 ⇒ 整份拒)。`prior` 里已有的键用那一次的读数,不再读。 */
207
+ function objectSnapshot(shape, value, prior) {
208
+ const skip = nonObjectSnapshot(value);
209
+ if (skip !== undefined)
210
+ return skip;
211
+ const copy = isolatedObject();
212
+ for (const key of Object.keys(shape)) {
213
+ const read = prior.get(key) ?? readDeclaredKey(value, key);
214
+ switch (read.presence) {
215
+ case "own": {
216
+ const child = snapshotOf(shape[key], read.value, NO_PRIOR_READS);
217
+ if (!child.ok)
218
+ return child;
219
+ Object.defineProperty(copy, key, dataDescriptor(child.value));
220
+ break;
221
+ }
222
+ case "inherited":
223
+ case "absent":
224
+ break;
225
+ case "hidden":
226
+ return failed(NOT_PLAIN_DATA);
227
+ case "unreadable":
228
+ return failed(UNINSPECTABLE);
229
+ default: {
230
+ const unhandled = read;
231
+ throw new Error(`declared-capture: unhandled presence ${String(unhandled.presence)}`);
232
+ }
233
+ }
234
+ }
235
+ return { ok: true, value: copy };
236
+ }
237
+ /**
238
+ * 判别联合:按判别键选臂,只投影那一臂;判别键缺席 / 不是任何一臂的词 ⇒ 只交过去判别键。**判别键只读一次**:这一次的读数交给选中那一臂的投影复用(`prior`),
239
+ * 所以快照里的判别词就是选臂的那个词 —— 校验器拿快照按判别键派发,落到的就是选中的这一臂(没有第二次读,也就没有「第二次读到另一个词、落到另一臂」)。
240
+ */
241
+ function discriminatedSnapshot(union, value, prior) {
242
+ const skip = nonObjectSnapshot(value);
243
+ if (skip !== undefined)
244
+ return skip;
245
+ const discriminator = union._zod.def.discriminator;
246
+ const read = prior.get(discriminator) ?? readDeclaredKey(value, discriminator);
247
+ if (read.presence === "hidden")
248
+ return failed(NOT_PLAIN_DATA);
249
+ if (read.presence === "unreadable")
250
+ return failed(UNINSPECTABLE);
251
+ const word = read.presence === "own" ? read.value : undefined;
252
+ const arms = union._zod.def.options;
253
+ const chosen = arms.find((arm) => innerOf(arm)._zod.propValues?.[discriminator]?.has(word) === true);
254
+ if (chosen !== undefined)
255
+ return snapshotOf(chosen, value, new Map(prior).set(discriminator, read));
256
+ // 没有一臂认这个词(或键缺席):校验器只看判别键就拒,别的键一个都不碰
257
+ const only = isolatedObject();
258
+ if (read.presence === "own") {
259
+ const kept = captureJson(word);
260
+ if (!kept.ok)
261
+ return kept;
262
+ Object.defineProperty(only, discriminator, dataDescriptor(kept.value));
263
+ }
264
+ return { ok: true, value: only };
265
+ }
266
+ /**
267
+ * `schema` 在 `value` 上会读的键的快照(恰一份;普通联合不在这里 —— 它逐臂各走一遍,见 `evaluate`;嵌在对象里的普通联合当不透明,整个值深拷)。
268
+ * `prior`:同一个值上已经读过的声明键(判别联合读过的判别键;投影走进字段值时是一张新的空表)—— **快照里这些键的在场与值恒是那一次的读数,不读第二次**:投影直接复用;
269
+ * 整份深拷的臂(`strict` / 带 catchall 的对象)把它交给 `captureJson`,列成员时这些键取那一次的读数、不再问描述符(所以判别联合选臂的词就是交给校验器的词,不论臂怎么拍快照)。
270
+ */
271
+ function snapshotOf(schema, value, prior) {
272
+ const inner = innerOf(schema);
273
+ if (inner instanceof z.ZodUnknown || inner instanceof z.ZodAny)
274
+ return { ok: true, value };
275
+ if (inner instanceof z.ZodDiscriminatedUnion)
276
+ return discriminatedSnapshot(inner, value, prior);
277
+ if (inner instanceof z.ZodObject && inner._zod.def.catchall === undefined)
278
+ return objectSnapshot(shapeOf(inner), value, prior);
279
+ return captureJson(value, "isolated", prior);
280
+ }
281
+ /**
282
+ * 按 zod 的联合语义逐臂各走一遍:一臂 = 只含该臂声明键的快照 + **只拿该臂的 schema 校验**(拿整个联合去校验的话,后臂会用默认值接受前一臂的残缺投影)。
283
+ * · 普通联合:按序,第一臂通过即止(不取并集);
284
+ * · 互斥联合(`z.xor`:`def.inclusive === false`):**所有**臂都走一遍,恰一臂通过才接受 —— 两臂都通过 zod 拒(这里同样拒,不是取第一臂丢掉另一臂的键);
285
+ * · 其余(含判别联合:它按判别键选臂,见 `discriminatedSnapshot`):恰一份快照、拿整只 schema 校验。
286
+ * 联合外面套着包装层(`optional` / `default` / `catch` …)⇒ 当不透明(整个值深拷、对整只 schema 校验),包装层的语义由 zod 自己走。
287
+ */
288
+ function evaluate(schema, value) {
289
+ if (isPlainUnion(schema)) {
290
+ const exclusive = schema._zod.def.inclusive === false;
291
+ let accepted;
292
+ for (const option of schema._zod.def.options) {
293
+ const verdict = evaluate(option, value);
294
+ if (verdict.kind === "unreadable")
295
+ return verdict;
296
+ if (verdict.kind !== "ok")
297
+ continue;
298
+ if (!exclusive)
299
+ return verdict;
300
+ if (accepted !== undefined)
301
+ return { kind: "rejected" }; // xor:两臂都通过 ⇒ 拒
302
+ accepted = verdict;
303
+ }
304
+ return accepted ?? { kind: "rejected" };
305
+ }
306
+ const snapshot = snapshotOf(schema, value, NO_PRIOR_READS);
307
+ if (!snapshot.ok)
308
+ return { kind: "unreadable" };
309
+ // 只问判决、不要句子(parseDeclared 从不交出拒因):逐次 `error` 参数让 zod 不进 locale 拼句 —— 拼句会对副本做 `parsedType`(副本的原型不是 Object.prototype,
310
+ // 用户写的自有 `constructor` 键会被读成类型名、转串时抛),联合 / 互斥联合会因此把一臂判成「读不出」、不再试后面的臂,与 zod 在原值上的判决不同。只读 `success` / `data`,
311
+ // 不构造错误对象(所以不用 `guardedSafeParse`);校验过程抛 ⇒ 入口那一只守护(`inspect`)接住
312
+ const parsed = schema.safeParse(snapshot.value, { error: () => "rejected" });
313
+ return parsed.success ? { kind: "ok", data: parsed.data } : { kind: "rejected" };
314
+ }
315
+ /**
316
+ * 对 `value` 校验的守护读法(`schema` 的 safeParse):校验器读的是投影后的自有键快照,不是调用方的对象。读不出 / 不是纯数据 / 校验不过 ⇒ `{ ok: false }`(不抛);
317
+ * 联合按 {@link evaluate} 的语义逐臂。`schema` 不在白名单里(见文件头)⇒ 抛 `parseDeclared: unsupported schema shape — …`(不论输入)。
318
+ * **守护边界就在这里一处**(内核的 `inspect`):白名单门对 schema 的内省(zod 的惰性属性 —— 声明键表、判别联合的 `propValues` —— 第一次被读时才物化,物化时 zod 用字面量描述符调
319
+ * `defineProperty`、往 `{}` 上按键名累加:`Object.prototype` 被污染成 `get` / `set` / `value` / 判别键名时,物化在这一步抛)、投影快照的递归(递归 schema × 很深的数据会爆栈)与校验,
320
+ * 全在同一只守护里 —— 抛了 ⇒ `{ ok: false }`(与 v14.6.0 那一层守护同向:读口答「读不出」,不把引擎的错 / 嵌入方的抛出值带出去)。
321
+ * 「不支持」是门**交回**的判语、在守护外抛:它是包自己的 schema 选错了读法,与输入无关。
322
+ */
323
+ export function parseDeclared(schema, value) {
324
+ const run = inspect(() => {
325
+ const refusal = admit(schema);
326
+ return refusal !== undefined ? { kind: "unsupported", what: refusal } : evaluate(schema, value);
327
+ });
328
+ if (!run.ok)
329
+ return { ok: false };
330
+ const verdict = run.value;
331
+ switch (verdict.kind) {
332
+ case "unsupported":
333
+ throw new Error(unsupportedShape(verdict.what));
334
+ case "ok":
335
+ return { ok: true, data: verdict.data };
336
+ case "rejected":
337
+ case "unreadable":
338
+ return { ok: false };
339
+ default: {
340
+ const unhandled = verdict;
341
+ throw new Error(`parseDeclared: unhandled verdict ${String(unhandled.kind)}`);
342
+ }
343
+ }
344
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { ModelsConfig, RoleTarget } from "./types.js";
9
9
  /** core 对 `default` 角色的档语义:从它的缺省档 `pro` 起**只向低档**扫 —— pro → flash → lite,绝不向上抓 max / ultra
10
- * (core `ROLE_TIER_DEFAULTS.default` + `DEFAULT_TIER_ORDER`;钉对 devDependency 里精确钉住的那一版 core)。 */
10
+ * (core `ROLE_TIER_DEFAULTS.default` + `DEFAULT_TIER_ORDER`;档序只在 `./tier-words` 一处,这里按起点派生;对拍钉对 devDependency 里精确钉住的那一版 core)。 */
11
11
  export declare const DEFAULT_ROLE_TIER_LADDER: readonly ["pro", "flash", "lite"];
12
12
  /** 命名链每一级的来源词(闭集)。 */
13
13
  export type CatalogDefaultSource = "roles.default" | `tierGroup.${(typeof DEFAULT_ROLE_TIER_LADDER)[number]}` | "fallback";
@@ -1,7 +1,8 @@
1
1
  import { resolveActiveTiers } from "./config-fns.js";
2
+ import { tierLadderFrom } from "./tier-words.js";
2
3
  /** core 对 `default` 角色的档语义:从它的缺省档 `pro` 起**只向低档**扫 —— pro → flash → lite,绝不向上抓 max / ultra
3
- * (core `ROLE_TIER_DEFAULTS.default` + `DEFAULT_TIER_ORDER`;钉对 devDependency 里精确钉住的那一版 core)。 */
4
- export const DEFAULT_ROLE_TIER_LADDER = ["pro", "flash", "lite"];
4
+ * (core `ROLE_TIER_DEFAULTS.default` + `DEFAULT_TIER_ORDER`;档序只在 `./tier-words` 一处,这里按起点派生;对拍钉对 devDependency 里精确钉住的那一版 core)。 */
5
+ export const DEFAULT_ROLE_TIER_LADDER = tierLadderFrom("pro");
5
6
  /**
6
7
  * 缺省模型由 `roles.default` 的形分两路(一条规则:点名的走命名链,不点名的交回引擎):
7
8
  * · `{select}` ⇒ `{ kind: "select", select }`(见 {@link CatalogDefaultModel});
@@ -13,8 +14,8 @@ export function resolveCatalogDefaultModel(models, has, fallback) {
13
14
  const target = models.roles?.default;
14
15
  const named = target !== undefined && "model" in target ? target.model : undefined;
15
16
  const select = target !== undefined && "select" in target ? target.select : undefined;
16
- // 判序与引擎同一只真值判(core `partsModel`:`model` 为真 ⇒ 按名;否则 `select` 为真 ⇒ 按属性挑):schema 读过的值只有一形,这一判序只对
17
- // 绕过 schema 递进来的两形并在(含 `model: ""`)起作用 —— 那时也与引擎同答。
17
+ // 判序按真值判:`model` 为非空串 ⇒ 按名;否则 `select` 为真 ⇒ 按属性挑。schema 读过的值只有一形(`model` 是非空串),这一判序只对绕过 schema
18
+ // 递进来的空串 `model` 起作用 —— 本包把它读作没写;引擎(devDependency 钉的那一版)按在场判、空串按名拒(`unknown_model_ref`),这一形两边不同答。
18
19
  if (!named && select)
19
20
  return { kind: "select", select };
20
21
  const tiers = resolveActiveTiers(models) ?? {};
@@ -0,0 +1,163 @@
1
+ import { type EnvModelsDecision } from "./config-root-documents.js";
2
+ import { SUMMARIZE_MODEL_MISSING_CODE } from "./fleet.js";
3
+ import type { ModelRole } from "./types.js";
4
+ /** 一份 env 快照(`process.env` 也满足这个形)。 */
5
+ export type EnvSnapshot = Readonly<Record<string, string | undefined>>;
6
+ /** env 模型表的键名(闭集)。 */
7
+ export declare const ENV_MODEL_TABLE_KEYS: {
8
+ readonly mainId: "MODEL_ID";
9
+ readonly cheapId: "MODEL_CHEAP_ID";
10
+ readonly verifier: "MODEL_VERIFIER";
11
+ readonly codeRoles: "MODEL_CODE_ROLES";
12
+ };
13
+ /** env 模型表的一把键名。 */
14
+ export type EnvModelTableKey = (typeof ENV_MODEL_TABLE_KEYS)[keyof typeof ENV_MODEL_TABLE_KEYS];
15
+ /** env 表里主模型的别名键(角色底的 `default` 绑它;引擎 env 表的条目键 = 这个别名 + 主 id + cheap id)。 */
16
+ export declare const DEFAULT_MODEL_KEY: "default";
17
+ /**
18
+ * env 表的 **cheap 槽**铸不铸(闭集四态,消费方穷举 `switch`;{@link mintedCheapId}):
19
+ * · `absent` —— 没设(或空串);
20
+ * · `without_main` —— 设了而主槽没设:cheap 槽是主模型「同网关换个 id」的形,没有主槽就没有它 ⇒ 不铸;
21
+ * · `same_as_main` —— 与主 id 相同:说的就是主模型 ⇒ 不铸;
22
+ * · `minted` —— 铸(`id` = `MODEL_CHEAP_ID`)。
23
+ */
24
+ export type EnvCheapSlot = {
25
+ readonly kind: "absent";
26
+ } | {
27
+ readonly kind: "without_main" | "same_as_main" | "minted";
28
+ readonly id: string;
29
+ };
30
+ /** cheap 槽铸出来的 id(不铸 ⇒ `undefined`)。 */
31
+ export declare function mintedCheapId(slot: EnvCheapSlot): string | undefined;
32
+ /**
33
+ * 包内(不进包根):把 env 模型表的键(缺省四把;调用方只要两槽时只拍两把)**读一次**进一份快照(下标读,同「读到即算」)。计划助手与一次性迁移在**要用的那一刻**拍、
34
+ * 此后的每个读法都答自同一份 —— 一只 getter 两次读答两个值时,判「env 表已声明」与列 env 模型不会各读到一个(那样结局与回执自相矛盾);用不到的键不读。
35
+ */
36
+ export declare function snapshotEnvModelTable(env: EnvSnapshot, keys?: readonly EnvModelTableKey[]): EnvSnapshot;
37
+ /** env 两槽的**唯一读口**(见文件头)。`mainId` = `MODEL_ID`(空串 = 没有主槽)。 */
38
+ export declare function readEnvModelSlots(env: EnvSnapshot): {
39
+ readonly mainId: string;
40
+ readonly cheapSlot: EnvCheapSlot;
41
+ };
42
+ /** **env 模型表已声明吗**(= `MODEL_ID` 在场且非空串;纯空白算在场)—— 引擎一侧的顶替闸(今天还不强制)与计划助手共用的那一只谓词。 */
43
+ export declare function envModelTableDeclared(env: EnvSnapshot): boolean;
44
+ /** env 表里的一条模型(槽序 main → cheap)。 */
45
+ export interface EnvModelTableEntry {
46
+ readonly id: string;
47
+ readonly slot: "main" | "cheap";
48
+ readonly env: typeof ENV_MODEL_TABLE_KEYS.mainId | typeof ENV_MODEL_TABLE_KEYS.cheapId;
49
+ }
50
+ /** env 底的一个角色绑定:env 表键(别名 `default`、主 id 或 cheap id),或带开发模式提示词标记的同一个键(`MODEL_CODE_ROLES`)。 */
51
+ export type EnvRoleBinding = string | {
52
+ readonly model: string;
53
+ readonly promptRole: "code";
54
+ };
55
+ /** env 模型表铸出来的全部事实(纯数据)。 */
56
+ export interface EnvModelBaseline {
57
+ readonly entries: readonly EnvModelTableEntry[];
58
+ /** 角色 → env 表键(键序 verifier → default → subagent → summarize)。 */
59
+ readonly roles: Readonly<Partial<Record<ModelRole, EnvRoleBinding>>>;
60
+ /** `MODEL_VERIFIER` 的读数:未设 / 指 env 表里的一个键 / 指表外(⇒ verifier 落 `default`,由消费方告警)。 */
61
+ readonly verifier: {
62
+ readonly kind: "unset";
63
+ } | {
64
+ readonly kind: "bound";
65
+ readonly ref: string;
66
+ } | {
67
+ readonly kind: "rejected";
68
+ readonly raw: string;
69
+ };
70
+ }
71
+ /**
72
+ * env 模型表的**角色底**(纯数据):条目(主槽在 ⇒ 主;cheap 槽铸了 ⇒ cheap)、角色(verifier ⇒ `MODEL_VERIFIER` 指 env 表里的键时是它、否则 `default`;
73
+ * 主角色族(`ROSTER_PRIMARY_ROLES`)⇒ `default`;cheap 角色族(`ROSTER_CHEAP_ROLES`)⇒ cheap id,cheap 没铸 ⇒ `default`;`MODEL_CODE_ROLES` 点名的、角色底自己有的键
74
+ * 加开发模式提示词标记)。env 表键 = 别名 `default` + 主 id + cheap id(主槽不在 ⇒ 表空)。
75
+ */
76
+ export declare function buildEnvModelBaseline(env: EnvSnapshot): EnvModelBaseline;
77
+ /** 目标引擎读哪条配置腿:`local` = 读配置根的 `config.d`;`remote` = 读中心;`none` = 纯 env 部署,不读 `config.d`。 */
78
+ export type EnvModelTableTarget = "local" | "remote" | "none";
79
+ /** 一个落点:目录条目的名与它的上游 id。 */
80
+ export interface EnvModelTableLanding {
81
+ readonly name: string;
82
+ readonly id: string;
83
+ }
84
+ /** 回执的一个角色行。`before.id = null` ⇒ env 表空,这个键在写前不解到任何 env 模型。 */
85
+ export interface EnvModelTableRoleRow {
86
+ readonly role: string;
87
+ readonly before: {
88
+ readonly ref: string;
89
+ readonly id: string | null;
90
+ };
91
+ readonly promptRole?: "code";
92
+ /** `candidate_binds` = 候选自己的 `roles` 绑了这个角色({select} ⇒ `landing: null`);`catalog_default` = 落到目录缺省(env 底绑别名 `default`,或 `default` 角色本身
93
+ * —— 它恒落在缺省解析器的答案上;候选把它绑到停用条目时解析器落到下一级;{select} 缺省 ⇒ `null`);
94
+ * `candidate_entry` = env 底绑的名在候选目录里解得到(一条启用条目的名,或经活跃档位组展开的档位词 / 别名);`dangles` = 落不到任何启用条目。 */
95
+ readonly after: {
96
+ readonly kind: "candidate_binds" | "catalog_default" | "candidate_entry" | "dangles";
97
+ readonly landing: EnvModelTableLanding | null;
98
+ };
99
+ }
100
+ /** 回执(纯数据;确认键 = 它的定序 JSON)。 */
101
+ export interface EnvModelTableReceipt {
102
+ /** 配置根的决定读数(`readEnvModelsDecision` 的 `kind`):读不懂的会在确认后被整份覆盖。 */
103
+ readonly decision: EnvModelsDecision["kind"];
104
+ /** 退场的 env 条目(槽序);`servedAs` = 候选里同 id 的启用条目名。 */
105
+ readonly envModels: readonly (EnvModelTableEntry & {
106
+ readonly servedAs: readonly string[];
107
+ })[];
108
+ readonly roles: readonly EnvModelTableRoleRow[];
109
+ }
110
+ /** 调用方带回的确认:上一次 `confirm` 给的键。 */
111
+ export interface EnvModelTableWriteIntent {
112
+ readonly kind: "replace";
113
+ readonly confirmKey: string;
114
+ }
115
+ export interface EnvModelTableWriteInput {
116
+ /** 目标引擎起服看到的 env(只用于预测与回执;引擎用它自己的 env 复判)。 */
117
+ readonly env: EnvSnapshot;
118
+ readonly target: EnvModelTableTarget;
119
+ /** 配置根上的决定读数(`readEnvModelsDecision` 的答案)。 */
120
+ readonly decision: EnvModelsDecision;
121
+ /** 将要原子写入的完整 models 文档。 */
122
+ readonly candidate: unknown;
123
+ readonly intent?: EnvModelTableWriteIntent;
124
+ }
125
+ /** 计划(闭集;写者穷举 `switch`)。 */
126
+ export type EnvModelTableWritePlan = {
127
+ readonly kind: "write";
128
+ readonly why: "not_read" | "not_managing" | "env_table_empty";
129
+ } | {
130
+ readonly kind: "write";
131
+ readonly why: "acknowledged";
132
+ readonly receipt: EnvModelTableReceipt;
133
+ }
134
+ /** 先写 `config.env-models.json = replaced`(`writeEnvModelsDecision(root, "replaced", { write: "overwrite" })`),再写 models.json。 */
135
+ | {
136
+ readonly kind: "write_with_decision";
137
+ readonly receipt: EnvModelTableReceipt;
138
+ }
139
+ /** 把回执给用户看;用户确认 ⇒ 带 `{ kind: "replace", confirmKey }` 再调一次。`intent`:没带过键 `absent` / 带的键已过期 `stale`。 */
140
+ | {
141
+ readonly kind: "confirm";
142
+ readonly receipt: EnvModelTableReceipt;
143
+ readonly confirmKey: string;
144
+ readonly intent: "absent" | "stale";
145
+ }
146
+ /** 不可确认:先改文档(summarize 要落到一条启用条目)。 */
147
+ | {
148
+ readonly kind: "fix_first";
149
+ readonly code: typeof SUMMARIZE_MODEL_MISSING_CODE;
150
+ readonly receipt: EnvModelTableReceipt;
151
+ };
152
+ /**
153
+ * 写者在写 `config.d/models.json` 之前调的**计划**(判序见文件头)。纯函数:不读环境、不读盘、不改入参。
154
+ * 目标腿 / 决定读数不在闭集 ⇒ `TypeError`;候选读不出(撤销的 Proxy 等)或整份读不进 models 域(重名、悬空的角色名 …)⇒ `TypeError`(先修文档)。
155
+ */
156
+ export declare function planEnvModelTableWrite(input: EnvModelTableWriteInput): EnvModelTableWritePlan;
157
+ /**
158
+ * 计划停下来的那两种结局(`confirm` / `fix_first`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
159
+ * 写类结局不是拒 ⇒ `TypeError`。
160
+ */
161
+ export declare function envModelTableWriteRefusal(plan: Extract<EnvModelTableWritePlan, {
162
+ readonly kind: "confirm" | "fix_first";
163
+ }>): string;