@sema-agent/settings-schema 14.7.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.
- package/CHANGELOG.md +209 -0
- package/README.md +58 -2
- package/conformance/VERSION +1 -1
- package/dist/atomic-write.d.ts +15 -0
- package/dist/atomic-write.js +109 -5
- package/dist/catalog-manages.d.ts +15 -0
- package/dist/catalog-manages.js +60 -0
- package/dist/center-refs.d.ts +68 -0
- package/dist/center-refs.js +70 -0
- package/dist/config-dir-migration.js +1 -0
- package/dist/config-fns.js +5 -3
- package/dist/config-root-documents.d.ts +106 -0
- package/dist/config-root-documents.js +93 -0
- package/dist/config-root-node.d.ts +117 -0
- package/dist/config-root-node.js +260 -0
- package/dist/conformance/run.d.ts +3 -0
- package/dist/conformance/run.js +10 -6
- package/dist/conformance/temp-roots.d.ts +31 -0
- package/dist/conformance/temp-roots.js +184 -0
- package/dist/default-model.d.ts +1 -1
- package/dist/default-model.js +5 -4
- package/dist/env-model-table.d.ts +163 -0
- package/dist/env-model-table.js +257 -0
- package/dist/fleet.d.ts +25 -7
- package/dist/fleet.js +24 -6
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/mcp-request-entry.d.ts +58 -6
- package/dist/mcp-request-entry.js +45 -13
- package/dist/mcp-server-name.d.ts +17 -0
- package/dist/mcp-server-name.js +19 -0
- package/dist/migration-receipt.d.ts +10 -6
- package/dist/migration-receipt.js +5 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +2 -0
- package/dist/one-time-migrations.d.ts +4 -2
- package/dist/one-time-migrations.js +29 -1
- package/dist/own-key-read.d.ts +1 -1
- package/dist/retired-form.d.ts +19 -0
- package/dist/retired-form.js +24 -1
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/tier-words.d.ts +33 -0
- package/dist/tier-words.js +43 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +24 -0
- package/dist/worker-row-verdict.js +7 -1
- package/docs/API-SURFACE.md +68 -4
- package/docs/CONSUMER-INTEGRATION.md +72 -2
- package/llms-full.txt +4 -1
- package/llms.txt +9 -3
- package/package.json +2 -2
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「一份 models 文档管不管模型面」—— **原文半场的唯一判官**(纯、零依赖、浏览器安全)。
|
|
3
|
+
*
|
|
4
|
+
* ## 律(一条)
|
|
5
|
+
* **文档的 `models` 里至少一条不是「显式停用」的条目 ⇔ 管。** 「显式停用」= 纯 JSON 对象、自有数据属性 `enabled` 恰为 `false`
|
|
6
|
+
* ({@link ownEnabledFlag} 是这一读法的**唯一**实现:被丢条目身份的 `enabled`(`./config-fns` 的 `readEntryIdentity`)读的是同一只;{@link entryEnabledFlag} 只是它前面判一次形的入口)。
|
|
7
|
+
* 判官 = 计数 > 0:「管不管」与「几条在管」是同一个事实的两种问法,只有一种读法。
|
|
8
|
+
*
|
|
9
|
+
* · 文档不是纯对象、`models` 缺席或不是数组 ⇒ 0 条(不管);数组里的洞不是条目;
|
|
10
|
+
* · 条目本身不是对象(坏条目)⇒ 不是「显式停用」⇒ 计入 —— 引擎读腿那一半(被丢条目载体:形不对 ⇒ 算管)同答;
|
|
11
|
+
* · 读不出的值(撤销的 Proxy、访问器 / 隐藏的自有键、类实例 —— JSON 解析不出这些)⇒ 抛 `TypeError`,带全包那一句:判官的两个调用方失败方向相反
|
|
12
|
+
* (写者的计划助手:「管」= 要问人;一次性迁移:「管」= 替人铸确认),不替它们猜。
|
|
13
|
+
*
|
|
14
|
+
* 读后表示(引擎的读腿载体半场)不在本包:那一半读的是引擎自己的被丢条目载体,由引擎在本判官之上 ∨ 一格。
|
|
15
|
+
*/
|
|
16
|
+
import { captureJson, isAccessorDescriptor } from "./own-key-read.js";
|
|
17
|
+
import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js";
|
|
18
|
+
/**
|
|
19
|
+
* 一个**已核过是纯 JSON 对象**的条目的 `enabled`:自有数据属性且是布尔 ⇒ 它;其余(缺席、只靠继承、访问器、不是布尔)⇒ `undefined`。不调 getter。
|
|
20
|
+
* 反射本身抛(Proxy 的描述符陷阱)⇒ **照抛**,由调用方的失败臂收:被丢条目身份整份按读不出处置(不带半份身份);原文判官读的是捕获出来的快照,不会走到这里。
|
|
21
|
+
* 包内私有(不进包根):这是「`enabled` 怎么读」的**唯一实现**;条目的形由调用方判一次(被丢条目身份在读 `name` / `id` 之前已判过,这里不再问第二次)。
|
|
22
|
+
*/
|
|
23
|
+
export function ownEnabledFlag(entry) {
|
|
24
|
+
const d = Object.getOwnPropertyDescriptor(entry, "enabled");
|
|
25
|
+
const value = d !== undefined && !isAccessorDescriptor(d) ? d.value : undefined; // 访问器不调(描述符的字段只认自有的,见内核)
|
|
26
|
+
return typeof value === "boolean" ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** 一条目录条目**原文**的 `enabled`:先判形(不是纯 JSON 对象 ⇒ `undefined`),再读 {@link ownEnabledFlag}。「显式停用」与计划助手的启用过滤都调它。 */
|
|
29
|
+
export function entryEnabledFlag(entry) {
|
|
30
|
+
return isPlainJsonObject(entry) ? ownEnabledFlag(entry) : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** 「显式停用」:{@link entryEnabledFlag} 恰为 `false`。 */
|
|
33
|
+
const isExplicitlyDisabled = (entry) => entryEnabledFlag(entry) === false;
|
|
34
|
+
/**
|
|
35
|
+
* 文档里**不是显式停用**的条目数(日志计数用;判官 {@link catalogManagesModelPlane} 就是它 > 0)。入参是 models 文档原文或读过的 `ModelsConfig`(两者同形)。
|
|
36
|
+
* 读不出 ⇒ `TypeError`(见文件头)。
|
|
37
|
+
*/
|
|
38
|
+
export function enabledEntryCount(doc) {
|
|
39
|
+
const copy = captureJson(doc); // 一次自有键快照:之后的读都在副本上(不调 getter、不读继承)
|
|
40
|
+
if (!copy.ok)
|
|
41
|
+
throw new TypeError(`enabledEntryCount: the models document could not be read — ${copy.sentence}`);
|
|
42
|
+
const snapshot = copy.value;
|
|
43
|
+
if (!isPlainJsonObject(snapshot))
|
|
44
|
+
return 0;
|
|
45
|
+
const models = snapshot.models; // 副本是隔离原型的袋:没有继承成员可读
|
|
46
|
+
if (!isJsonArray(models))
|
|
47
|
+
return 0;
|
|
48
|
+
let count = 0;
|
|
49
|
+
for (let i = 0; i < models.length; i++) {
|
|
50
|
+
if (!Object.hasOwn(models, i))
|
|
51
|
+
continue; // 洞不是条目:只认自有下标(`forEach` / `for…of` 会读到被污染的 `Array.prototype[i]`)
|
|
52
|
+
if (!isExplicitlyDisabled(models[i]))
|
|
53
|
+
count++;
|
|
54
|
+
}
|
|
55
|
+
return count;
|
|
56
|
+
}
|
|
57
|
+
/** 这份 models 文档管模型面吗(见文件头的律)。读不出 ⇒ `TypeError`。 */
|
|
58
|
+
export function catalogManagesModelPlane(doc) {
|
|
59
|
+
return enabledEntryCount(doc) > 0;
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中心引用的**解析口** `resolveCenterRefs(refs, lookup)` —— 纯函数、浏览器安全。
|
|
3
|
+
*
|
|
4
|
+
* 中心名册里 MCP server 的传输带两张**引用表**:stdio 的 `envRefs`(目标 env 键 → 变量名)、http 的 `headerRefs`(目标头名 → 变量名)。名册只存**变量名**、从不存值;
|
|
5
|
+
* 值要在消费方自己的宿主环境里读。此前本包只发引用表的形、不发读它的法:变量没设、设了但读到的不是串、读它的时候抛了,留给每个宿主自己写 —— 各按各的口径处置、各留各的句子。
|
|
6
|
+
* 现在一个读法:**每个成员一句判定,整台一个结论;本口只判,不决定挂不挂**(挂不挂是调用方按自己那条腿的规则定的事)。
|
|
7
|
+
*
|
|
8
|
+
* ## 律(一条)
|
|
9
|
+
* 引用表怎么读、每个变量怎么问、读不出怎么说,只有这一处:
|
|
10
|
+
* · **引用表经公开的记录读器读**({@link readPlainStringRecord}:同一个函数、同一张原因词表 {@link PLAIN_STRING_RECORD_FAULTS}),不另写形状 / 描述符判据。读不出(整张不是纯数据、有成员的变量名不是串、
|
|
11
|
+
* 读成员就抛、叫 `__proto__` 的成员)⇒ 整台结论 `refs_unreadable`,带读器的词,**一个变量都不问** —— 引用表本身不可信时,问了的值也没有可以挂的地方。
|
|
12
|
+
* 读器会把叫 `__proto__` 的串值成员**剔出副本**交其余;本口不许这样悄悄丢一条引用再挂一台少了鉴权的 server,所以同样答 `refs_unreadable`、词 `member_name_reserved`。
|
|
13
|
+
* · **查找函数 `lookup(name)` 由调用方给**,本口**不读 `process.env`**(源里没有 `process` / `globalThis`;宿主怎么读环境、读哪一层环境、要不要缓存,都是调用方的事)。没给 / 不是函数 ⇒ `TypeError`
|
|
14
|
+
* (这是调用方的编程错,不是数据错:不折成成员的 `read_threw`,更不回落任何环境)。
|
|
15
|
+
* · 每个成员恰问一次 `lookup`(按引用表副本的自有键序 —— 整数形的键在前、其余按记录列出的次序;实参只有变量名一个),判成**一个词**({@link CenterRefMemberVerdict}):
|
|
16
|
+
* `resolved`(答了串,空串也是串)· `missing_name`(答 `undefined`:变量没设)· `not_a_string`(答了别的种类 —— 例如对普通对象做 `obj[name]` 读到继承来的函数)· `read_threw`(`lookup` 抛了,不传出)。
|
|
17
|
+
* 一个成员坏了**不早停**:每个成员都有自己的判定,调用方可以一次点名所有缺的变量。同一个变量被两个目标键引用就问两次(缓存是查找函数的事)。
|
|
18
|
+
* · 整台结论只有三个词({@link CenterRefsVerdict}):`resolved`(每个成员都是 `resolved`;零成员也是)· `unresolved`(有成员不是)· `refs_unreadable`(引用表读不出)。
|
|
19
|
+
* **值只在 `resolved` 臂的 `values` 里**;`unresolved` 臂的行只有词,没有任何变量的值(失败读数会进日志)。
|
|
20
|
+
*
|
|
21
|
+
* ## 引用表缺席
|
|
22
|
+
* `refs` 是 `undefined`(这台 server 没声明引用)⇒ `resolved`、零成员、一次都不问 —— 与「空表」同一个答案。别的不是纯数据记录的值(`null`、数组、串……)是 `refs_unreadable`,不当缺席。
|
|
23
|
+
*
|
|
24
|
+
* ## 读数的构造
|
|
25
|
+
* 成员表用 `map`,`values` 用 `Object.defineProperty` + 零原型描述符({@link dataDescriptor})逐键写进一个新对象(定义语义,不走 [[Set]]):`Object.prototype` 上挂了数字键 setter 时目标键 `"0"` 的值也不会被吞;
|
|
26
|
+
* `constructor` / `toString` 这类原型链上的名字作目标键照落成自有数据属性。交出的 `values` 原型是 `Object.prototype`,与读器交出的副本同形。
|
|
27
|
+
*/
|
|
28
|
+
import { type PlainStringRecordFault } from "./mcp-request-entry.js";
|
|
29
|
+
/** 一个成员没解到的**原因词**,闭集,三个:`missing_name`(查找函数答 `undefined` —— 变量没设)· `not_a_string`(答了别的种类)· `read_threw`(查找函数抛了)。消费方按词分派,穷尽 `switch`,不读句子(本口没有句子)。 */
|
|
30
|
+
export declare const CENTER_REF_FAULTS: readonly ["missing_name", "not_a_string", "read_threw"];
|
|
31
|
+
export type CenterRefFault = (typeof CENTER_REF_FAULTS)[number];
|
|
32
|
+
/** 一个成员的判定词:解到了(`resolved`)或{@link CenterRefFault}之一。 */
|
|
33
|
+
export type CenterRefMemberVerdict = "resolved" | CenterRefFault;
|
|
34
|
+
/** 整台结论的词,闭集,三个:`resolved` · `unresolved`(有成员没解到)· `refs_unreadable`(引用表读不出)。 */
|
|
35
|
+
export declare const CENTER_REFS_VERDICTS: readonly ["resolved", "unresolved", "refs_unreadable"];
|
|
36
|
+
export type CenterRefsVerdict = (typeof CENTER_REFS_VERDICTS)[number];
|
|
37
|
+
/** 调用方给的查找函数:变量名 → 它的值(没设 ⇒ `undefined`)。可以抛(本口接住,判 `read_threw`)。本口不替它读任何环境。 */
|
|
38
|
+
export type CenterRefLookup = (name: string) => string | undefined;
|
|
39
|
+
/** 一个成员的判定:`key` = 目标键(env 键 / 头名,按引用表所写)、`ref` = 它引用的变量名(按所写)、`verdict` = 判定词。行里没有值。 */
|
|
40
|
+
export interface CenterRefMember {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly ref: string;
|
|
43
|
+
readonly verdict: CenterRefMemberVerdict;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* {@link resolveCenterRefs} 的读数,按 `verdict` 分三臂(穷尽 `switch`):
|
|
47
|
+
* · `resolved`:`members` 每行都是 `resolved`;`values` = 目标键 → 解到的串(新铸的朴素对象);
|
|
48
|
+
* · `unresolved`:`members` 里至少一行不是 `resolved`(行的次序 = 询问的次序,见文件头注);没有 `values`;
|
|
49
|
+
* · `refs_unreadable`:引用表没读成 —— `fault` = 读器的原因词({@link PlainStringRecordFault}),坏在某个成员时 `member` 点名它的回显形;没问过任何变量。
|
|
50
|
+
*/
|
|
51
|
+
export type CenterRefsResolution = {
|
|
52
|
+
readonly verdict: "resolved";
|
|
53
|
+
readonly members: readonly CenterRefMember[];
|
|
54
|
+
readonly values: Readonly<Record<string, string>>;
|
|
55
|
+
} | {
|
|
56
|
+
readonly verdict: "unresolved";
|
|
57
|
+
readonly members: readonly CenterRefMember[];
|
|
58
|
+
} | {
|
|
59
|
+
readonly verdict: "refs_unreadable";
|
|
60
|
+
readonly fault: PlainStringRecordFault;
|
|
61
|
+
readonly member?: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* 解析一张中心引用表(见文件头注的律)。`refs` 是名册里 `envRefs` / `headerRefs` 的值(`undefined` = 没声明);`lookup` 是调用方给的查找函数(必给)。
|
|
65
|
+
* 判而不决:`resolved` 之外的两个结论,调用方按自己那条腿的规则处置(例如整台不挂、并点名没解到的成员)。
|
|
66
|
+
* `lookup` 不是函数 ⇒ `TypeError`;别的一切输入都得到一个读数,不抛。
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveCenterRefs(refs: unknown, lookup: CenterRefLookup): CenterRefsResolution;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中心引用的**解析口** `resolveCenterRefs(refs, lookup)` —— 纯函数、浏览器安全。
|
|
3
|
+
*
|
|
4
|
+
* 中心名册里 MCP server 的传输带两张**引用表**:stdio 的 `envRefs`(目标 env 键 → 变量名)、http 的 `headerRefs`(目标头名 → 变量名)。名册只存**变量名**、从不存值;
|
|
5
|
+
* 值要在消费方自己的宿主环境里读。此前本包只发引用表的形、不发读它的法:变量没设、设了但读到的不是串、读它的时候抛了,留给每个宿主自己写 —— 各按各的口径处置、各留各的句子。
|
|
6
|
+
* 现在一个读法:**每个成员一句判定,整台一个结论;本口只判,不决定挂不挂**(挂不挂是调用方按自己那条腿的规则定的事)。
|
|
7
|
+
*
|
|
8
|
+
* ## 律(一条)
|
|
9
|
+
* 引用表怎么读、每个变量怎么问、读不出怎么说,只有这一处:
|
|
10
|
+
* · **引用表经公开的记录读器读**({@link readPlainStringRecord}:同一个函数、同一张原因词表 {@link PLAIN_STRING_RECORD_FAULTS}),不另写形状 / 描述符判据。读不出(整张不是纯数据、有成员的变量名不是串、
|
|
11
|
+
* 读成员就抛、叫 `__proto__` 的成员)⇒ 整台结论 `refs_unreadable`,带读器的词,**一个变量都不问** —— 引用表本身不可信时,问了的值也没有可以挂的地方。
|
|
12
|
+
* 读器会把叫 `__proto__` 的串值成员**剔出副本**交其余;本口不许这样悄悄丢一条引用再挂一台少了鉴权的 server,所以同样答 `refs_unreadable`、词 `member_name_reserved`。
|
|
13
|
+
* · **查找函数 `lookup(name)` 由调用方给**,本口**不读 `process.env`**(源里没有 `process` / `globalThis`;宿主怎么读环境、读哪一层环境、要不要缓存,都是调用方的事)。没给 / 不是函数 ⇒ `TypeError`
|
|
14
|
+
* (这是调用方的编程错,不是数据错:不折成成员的 `read_threw`,更不回落任何环境)。
|
|
15
|
+
* · 每个成员恰问一次 `lookup`(按引用表副本的自有键序 —— 整数形的键在前、其余按记录列出的次序;实参只有变量名一个),判成**一个词**({@link CenterRefMemberVerdict}):
|
|
16
|
+
* `resolved`(答了串,空串也是串)· `missing_name`(答 `undefined`:变量没设)· `not_a_string`(答了别的种类 —— 例如对普通对象做 `obj[name]` 读到继承来的函数)· `read_threw`(`lookup` 抛了,不传出)。
|
|
17
|
+
* 一个成员坏了**不早停**:每个成员都有自己的判定,调用方可以一次点名所有缺的变量。同一个变量被两个目标键引用就问两次(缓存是查找函数的事)。
|
|
18
|
+
* · 整台结论只有三个词({@link CenterRefsVerdict}):`resolved`(每个成员都是 `resolved`;零成员也是)· `unresolved`(有成员不是)· `refs_unreadable`(引用表读不出)。
|
|
19
|
+
* **值只在 `resolved` 臂的 `values` 里**;`unresolved` 臂的行只有词,没有任何变量的值(失败读数会进日志)。
|
|
20
|
+
*
|
|
21
|
+
* ## 引用表缺席
|
|
22
|
+
* `refs` 是 `undefined`(这台 server 没声明引用)⇒ `resolved`、零成员、一次都不问 —— 与「空表」同一个答案。别的不是纯数据记录的值(`null`、数组、串……)是 `refs_unreadable`,不当缺席。
|
|
23
|
+
*
|
|
24
|
+
* ## 读数的构造
|
|
25
|
+
* 成员表用 `map`,`values` 用 `Object.defineProperty` + 零原型描述符({@link dataDescriptor})逐键写进一个新对象(定义语义,不走 [[Set]]):`Object.prototype` 上挂了数字键 setter 时目标键 `"0"` 的值也不会被吞;
|
|
26
|
+
* `constructor` / `toString` 这类原型链上的名字作目标键照落成自有数据属性。交出的 `values` 原型是 `Object.prototype`,与读器交出的副本同形。
|
|
27
|
+
*/
|
|
28
|
+
import { PLAIN_STRING_RECORD_RESERVED_MEMBER, readPlainStringRecord } from "./mcp-request-entry.js";
|
|
29
|
+
import { dataDescriptor, inspect } from "./own-key-read.js";
|
|
30
|
+
/** 一个成员没解到的**原因词**,闭集,三个:`missing_name`(查找函数答 `undefined` —— 变量没设)· `not_a_string`(答了别的种类)· `read_threw`(查找函数抛了)。消费方按词分派,穷尽 `switch`,不读句子(本口没有句子)。 */
|
|
31
|
+
export const CENTER_REF_FAULTS = ["missing_name", "not_a_string", "read_threw"];
|
|
32
|
+
/** 整台结论的词,闭集,三个:`resolved` · `unresolved`(有成员没解到)· `refs_unreadable`(引用表读不出)。 */
|
|
33
|
+
export const CENTER_REFS_VERDICTS = ["resolved", "unresolved", "refs_unreadable"];
|
|
34
|
+
/** 问一次 `lookup(ref)` 并判成一个词。抛了 ⇒ `read_threw`(经内核的检视口,不另包 `try`);答 `undefined` ⇒ `missing_name`;答串 ⇒ 解到;答别的 ⇒ `not_a_string`。 */
|
|
35
|
+
function answerOf(ref, lookup) {
|
|
36
|
+
const asked = inspect(() => lookup(ref));
|
|
37
|
+
if (!asked.ok)
|
|
38
|
+
return { verdict: "read_threw" };
|
|
39
|
+
const answer = asked.value;
|
|
40
|
+
if (typeof answer === "string")
|
|
41
|
+
return { verdict: "resolved", value: answer };
|
|
42
|
+
return { verdict: answer === undefined ? "missing_name" : "not_a_string" };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 解析一张中心引用表(见文件头注的律)。`refs` 是名册里 `envRefs` / `headerRefs` 的值(`undefined` = 没声明);`lookup` 是调用方给的查找函数(必给)。
|
|
46
|
+
* 判而不决:`resolved` 之外的两个结论,调用方按自己那条腿的规则处置(例如整台不挂、并点名没解到的成员)。
|
|
47
|
+
* `lookup` 不是函数 ⇒ `TypeError`;别的一切输入都得到一个读数,不抛。
|
|
48
|
+
*/
|
|
49
|
+
export function resolveCenterRefs(refs, lookup) {
|
|
50
|
+
if (typeof lookup !== "function") {
|
|
51
|
+
throw new TypeError("resolveCenterRefs: `lookup` must be a function (variable name => string | undefined) supplied by the caller — this function reads no environment of its own");
|
|
52
|
+
}
|
|
53
|
+
if (refs === undefined)
|
|
54
|
+
return { verdict: "resolved", members: [], values: {} };
|
|
55
|
+
const read = readPlainStringRecord(refs);
|
|
56
|
+
if (!read.ok)
|
|
57
|
+
return { verdict: "refs_unreadable", fault: read.fault, ...(read.member !== undefined ? { member: read.member } : {}) };
|
|
58
|
+
if (read.reservedMember)
|
|
59
|
+
return { verdict: "refs_unreadable", fault: "member_name_reserved", member: PLAIN_STRING_RECORD_RESERVED_MEMBER };
|
|
60
|
+
const judged = Object.entries(read.record).map((pair) => ({ key: pair[0], ref: pair[1], answer: answerOf(pair[1], lookup) }));
|
|
61
|
+
const members = judged.map((j) => ({ key: j.key, ref: j.ref, verdict: j.answer.verdict }));
|
|
62
|
+
if (judged.some((j) => j.answer.verdict !== "resolved"))
|
|
63
|
+
return { verdict: "unresolved", members };
|
|
64
|
+
const values = {};
|
|
65
|
+
for (const j of judged) {
|
|
66
|
+
if (j.answer.verdict === "resolved")
|
|
67
|
+
Object.defineProperty(values, j.key, dataDescriptor(j.answer.value));
|
|
68
|
+
}
|
|
69
|
+
return { verdict: "resolved", members, values };
|
|
70
|
+
}
|
package/dist/config-fns.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { MODEL_ENTRY_REFUSAL_CODES, ModelRefusalsWire } from "./fleet.js";
|
|
20
20
|
import { UNINSPECTABLE, dataDescriptor, emptyDocument, guardedSafeParse, inspect, isAccessorDescriptor, isolatedObject, ownEntriesOf, ownRow, ownValueOf } from "./own-key-read.js";
|
|
21
|
+
import { ownEnabledFlag } from "./catalog-manages.js";
|
|
21
22
|
import { isJsonArray, isPlainJsonObject, jsonShapeOf } from "./plain-json-object.js";
|
|
22
23
|
import { carriesUserinfo, secretScanForms } from "./secret-scan.js"; // package-private normalization (both scanners read it) + the userinfo judgement (not in the barrel)
|
|
23
24
|
import { sha256Hex } from "./sha256.js";
|
|
@@ -362,7 +363,8 @@ export function withheldNameToken(name) {
|
|
|
362
363
|
return `withheld:${sha256Hex(`${WITHHELD_NAME_TOKEN_DOMAIN}${JSON.stringify(name)}`).slice(0, 12)}`;
|
|
363
364
|
}
|
|
364
365
|
/** 一条被丢条目原文的身份({@link DroppedEntryIdentity} 的**唯一**读法)。只看纯 JSON 对象的**自有数据
|
|
365
|
-
* 属性**;任何反射/陷阱抛错 ⇒ 当作读不出(身份是诊断面的附件,绝不许把抢救拖下水)
|
|
366
|
+
* 属性**;任何反射/陷阱抛错 ⇒ 当作读不出(身份是诊断面的附件,绝不许把抢救拖下水)。`enabled` 读的是「显式停用」的那一只
|
|
367
|
+
* ({@link ownEnabledFlag},`./catalog-manages`):目录管不管模型面的原文判官与这里同一份读法。 */
|
|
366
368
|
function readEntryIdentity(entry) {
|
|
367
369
|
try {
|
|
368
370
|
if (!isPlainJsonObject(entry))
|
|
@@ -373,11 +375,11 @@ function readEntryIdentity(entry) {
|
|
|
373
375
|
};
|
|
374
376
|
const name = own("name");
|
|
375
377
|
const id = own("id");
|
|
376
|
-
const enabled =
|
|
378
|
+
const enabled = ownEnabledFlag(entry); // 形上面已判过一次(`isPlainJsonObject`):不再经判形的入口问第二次
|
|
377
379
|
const identity = {
|
|
378
380
|
...(typeof name === "string" ? (isDisclosableIdentity(name) ? { name } : { nameWithheld: true, token: withheldNameToken(name) }) : {}),
|
|
379
381
|
...(typeof id === "string" ? (isDisclosableIdentity(id) ? { id } : { idWithheld: true }) : {}),
|
|
380
|
-
...(
|
|
382
|
+
...(enabled === undefined ? {} : { enabled }),
|
|
381
383
|
};
|
|
382
384
|
return Object.keys(identity).length > 0 ? identity : undefined;
|
|
383
385
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置根上的**根级文档**(纯、零 I/O、浏览器安全;读写函数在 `./node`,见 `./config-root-node`)。
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* <根>/
|
|
6
|
+
* config.d/… 便携域文档(文件仓库只扫这一个目录)
|
|
7
|
+
* config.env-models.json env 模型表被目录顶替的部署决定 {"envModels":"replaced"|"kept"}
|
|
8
|
+
* config.migration.SS-M11.json 一次性迁移 SS-M11 的运行记录 {"migration":"SS-M11"}(按 id 一份)
|
|
9
|
+
* .env 秘密值(不归本包读)
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* ## 律
|
|
13
|
+
* · **根归客户端(TOC)所有**,引擎在根上只写这两份文档;两份都在 `config.d/` 之外 —— 已核的各版文件仓库都只扫 `<根>/config.d`(抽样,不证全称),
|
|
14
|
+
* 被检视的配置读者不把这两份当域文档、不因它们在场拒配置,所以写者不探引擎版本。
|
|
15
|
+
* 离线预置(引擎在本根起服之前铺好:镜像 / ConfigMap / 还原)时两份文档与域文档可按任意顺序写;运行时写序是定的:写者先写决定、再写 `config.d/models.json`
|
|
16
|
+
* (都在它对 models.json 的那把锁里),迁移先建运行记录、再铸决定。
|
|
17
|
+
* · **一份读法、一张闭集答案表**:决定只有 {@link EnvModelsDecision} 这一种答案(从字节读出的带同一次读的 `digest`,读不到字节的带 `errno`),
|
|
18
|
+
* 读这份决定的三方 —— 引擎(本地配置腿强制这道闸之后;今天引擎还不读它)、一次性迁移 SS-M11、写者的计划助手 —— 吃同一个答案;它的身份(告警去重键、本地 etag 的贡献)只有一种拼法 {@link envModelsDecisionTag}。
|
|
19
|
+
* · **运行记录按 id 一份**:「迁移 X 跑过」⇔ `<根>/config.migration.<X>.json` 在场({@link MigrationMarker});没有聚合文件、没有追加写;
|
|
20
|
+
* 读不懂的记录按「跑过」处置(fail-closed)。记录由迁移以「不存在才建」写:同一个 id 只有一个进程建得成 —— 它是那条迁移在本根上唯一的执行者。
|
|
21
|
+
* · 两份文档的形都是 strict 的:多一个键(含自有 `__proto__`)、拼错的词都读成 `bad_shape`,不剥、不猜;digest 对读到的**字节**求。名字只在这里定一次(引擎的目录信任判官取 {@link CONFIG_ROOT_DOCUMENTS})。
|
|
22
|
+
*/
|
|
23
|
+
import type { OneTimeMigrationId } from "./migration-receipt.js";
|
|
24
|
+
/** 配置根上的 env 模型表决定文档的名字(文件 `<根>/config.env-models.json`)。 */
|
|
25
|
+
export declare const ENV_MODELS_DOCUMENT: "config.env-models";
|
|
26
|
+
/** 运行记录文档名的前缀(包内:名字 = 前缀 + 迁移 id,文件 = 名字 + `.json`)。 */
|
|
27
|
+
declare const MIGRATION_MARKER_PREFIX: "config.migration.";
|
|
28
|
+
/** 一条一次性迁移的运行记录文档名(包内)。 */
|
|
29
|
+
export declare const migrationMarkerDocument: <I extends OneTimeMigrationId>(id: I) => `${typeof MIGRATION_MARKER_PREFIX}${I}`;
|
|
30
|
+
/**
|
|
31
|
+
* 根级文档名单(闭集、序固定;文件 = 名字 + `.json`,与 `config.d` 文档同一拼法):决定文档,与今天唯一会写运行记录的迁移 SS-M11 的那一份。
|
|
32
|
+
* 引擎对配置根的目录信任判官、客户端的持久文件登记都取这一张,不手抄。判法与 `config.d` 里的文档**同一条律**(属主可信、路径上非链接组件不得组 / 他人可写、
|
|
33
|
+
* 常规文件、有大小上界)—— 一条律、两张名单(`config.d` 读回面一张,本张一张);`config.d` 不在时也照判。
|
|
34
|
+
* `<根>/.env` **不在**这张名单:引擎今天的目录信任判官只判 `config.d` 与它的读回面,不判 `.env`(秘密值文件,本包的文件仓库不读它);本名单只登记引擎会读的根级文档。
|
|
35
|
+
*/
|
|
36
|
+
export declare const CONFIG_ROOT_DOCUMENTS: readonly ["config.env-models", "config.migration.SS-M11"];
|
|
37
|
+
export type ConfigRootDocument = (typeof CONFIG_ROOT_DOCUMENTS)[number];
|
|
38
|
+
/** 有运行记录的迁移 id(由 {@link CONFIG_ROOT_DOCUMENTS} 派生:名单里有它的记录文档 ⇔ 它写记录)。 */
|
|
39
|
+
export type MarkedMigrationId = {
|
|
40
|
+
[I in OneTimeMigrationId]: `${typeof MIGRATION_MARKER_PREFIX}${I}` extends ConfigRootDocument ? I : never;
|
|
41
|
+
}[OneTimeMigrationId];
|
|
42
|
+
/** 包内:`id` 是写运行记录的迁移吗(读运行记录之前判:别的 id 没有记录可读,更不许拿它拼路径)。 */
|
|
43
|
+
export declare const isMarkedMigrationId: (id: unknown) => id is MarkedMigrationId;
|
|
44
|
+
/** 决定文档里能写的两个词(闭集):`replaced` = 目录管模型面时顶替 env 模型表;`kept` = 不顶替(与缺席同义,运行记录在场之后)。 */
|
|
45
|
+
export declare const ENV_MODELS_DECISIONS: readonly ["replaced", "kept"];
|
|
46
|
+
export type EnvModelsDecisionWord = (typeof ENV_MODELS_DECISIONS)[number];
|
|
47
|
+
/** 包内:是不是决定词(闭集,判法只此一只;写函数与读法同调)。 */
|
|
48
|
+
export declare const isDecisionWord: (v: unknown) => v is EnvModelsDecisionWord;
|
|
49
|
+
/** 根级文档读不懂的三种原因(闭集):`bad_json` = 空 / 纯空白 / 不是 JSON;`bad_shape` = 是 JSON 但不是这份文档的形;`io` = 字节读不出(权限 —— 可信属主的 0000 文件读恒 `EACCES`,是持久的 —— 不是常规文件、悬空链接、瞬时读错)。 */
|
|
50
|
+
export declare const ROOT_DOCUMENT_UNREADABLE_REASONS: readonly ["bad_json", "bad_shape", "io"];
|
|
51
|
+
export type RootDocumentUnreadableReason = (typeof ROOT_DOCUMENT_UNREADABLE_REASONS)[number];
|
|
52
|
+
/**
|
|
53
|
+
* 决定文档的读数(闭集;三方穷举 `switch`)。`absent` 只在文件不存在时答 —— 读不出的字节是 `unreadable`,不是缺席。
|
|
54
|
+
* 有字节的四臂(`replaced` / `kept` / `unreadable{bad_json|bad_shape}`)带 `digest` = **同一次读**得到的字节的 SHA-256 前 16 位十六进制;
|
|
55
|
+
* 读不到字节的 `unreadable{io}` 带 `errno`(错误上没有 errno ⇒ `uncoded`)。`{` 改成 `[` 这种「答案同为 bad_json、字节不同」的改动 digest 也变。digest 不可逆,不带内容。
|
|
56
|
+
*/
|
|
57
|
+
export type EnvModelsDecision = {
|
|
58
|
+
readonly kind: "absent";
|
|
59
|
+
} | {
|
|
60
|
+
readonly kind: "replaced";
|
|
61
|
+
readonly digest: string;
|
|
62
|
+
} | {
|
|
63
|
+
readonly kind: "kept";
|
|
64
|
+
readonly digest: string;
|
|
65
|
+
} | {
|
|
66
|
+
readonly kind: "unreadable";
|
|
67
|
+
readonly reason: "bad_json" | "bad_shape";
|
|
68
|
+
readonly digest: string;
|
|
69
|
+
} | {
|
|
70
|
+
readonly kind: "unreadable";
|
|
71
|
+
readonly reason: "io";
|
|
72
|
+
readonly errno: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* 一份决定读数的**身份**(带标签的一种拼法):`absent` / `d:<digest>`(有字节的四臂)/ `io:<errno>`。引擎读这份决定时(今天引擎还不读),「每份内容只告警一次」的去重键与本地 etag 的贡献都取它 ——
|
|
76
|
+
* 来自同一次读,不再开文件(那就是第二个读者)。
|
|
77
|
+
*/
|
|
78
|
+
export declare function envModelsDecisionTag(decision: EnvModelsDecision): string;
|
|
79
|
+
/** 一份运行记录的读数(闭集):`present` = 那条迁移跑过(或正被它的执行者跑);读不懂的按「跑过」处置(fail-closed)。 */
|
|
80
|
+
export type MigrationMarker = {
|
|
81
|
+
readonly kind: "absent";
|
|
82
|
+
} | {
|
|
83
|
+
readonly kind: "present";
|
|
84
|
+
} | {
|
|
85
|
+
readonly kind: "unreadable";
|
|
86
|
+
readonly reason: RootDocumentUnreadableReason;
|
|
87
|
+
};
|
|
88
|
+
/** 包内:决定文档的**字节读法**(唯一一份;`./config-root-node` 的读者在拿到字节之后调它)—— 字节 → 答案(带这些字节的 digest)。 */
|
|
89
|
+
export declare function readEnvModelsDecisionBytes(bytes: Uint8Array): Exclude<EnvModelsDecision, {
|
|
90
|
+
readonly kind: "absent";
|
|
91
|
+
} | {
|
|
92
|
+
readonly reason: "io";
|
|
93
|
+
}>;
|
|
94
|
+
/** 包内:字节没读到时的决定读数(errno 的拼法只在本文件)。 */
|
|
95
|
+
export declare function ioDecision(errno: string | undefined): Extract<EnvModelsDecision, {
|
|
96
|
+
readonly reason: "io";
|
|
97
|
+
}>;
|
|
98
|
+
/** 包内:运行记录文档的值(`{"migration":<id>}`,写与读同一只形)。 */
|
|
99
|
+
export declare const migrationMarkerValue: (id: MarkedMigrationId) => {
|
|
100
|
+
readonly migration: MarkedMigrationId;
|
|
101
|
+
};
|
|
102
|
+
/** 包内:运行记录的**字节读法**(唯一一份):`{"migration":<这条记录的 id>}` strict ⇒ `present`;别的 ⇒ 读不懂(id 对不上也是坏形)。 */
|
|
103
|
+
export declare function readMigrationMarkerBytes(id: MarkedMigrationId, bytes: Uint8Array): MigrationMarker;
|
|
104
|
+
/** 包内:字节 → 文本(`config.d/models.json` 的读也经它,与上面两份同一解码)。 */
|
|
105
|
+
export declare const decodeRootBytes: (bytes: Uint8Array) => string;
|
|
106
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
2
|
+
import { sha256Digest, toHex } from "./sha256-core.js";
|
|
3
|
+
/** 配置根上的 env 模型表决定文档的名字(文件 `<根>/config.env-models.json`)。 */
|
|
4
|
+
export const ENV_MODELS_DOCUMENT = "config.env-models";
|
|
5
|
+
/** 运行记录文档名的前缀(包内:名字 = 前缀 + 迁移 id,文件 = 名字 + `.json`)。 */
|
|
6
|
+
const MIGRATION_MARKER_PREFIX = "config.migration.";
|
|
7
|
+
/** 一条一次性迁移的运行记录文档名(包内)。 */
|
|
8
|
+
export const migrationMarkerDocument = (id) => `${MIGRATION_MARKER_PREFIX}${id}`;
|
|
9
|
+
/**
|
|
10
|
+
* 根级文档名单(闭集、序固定;文件 = 名字 + `.json`,与 `config.d` 文档同一拼法):决定文档,与今天唯一会写运行记录的迁移 SS-M11 的那一份。
|
|
11
|
+
* 引擎对配置根的目录信任判官、客户端的持久文件登记都取这一张,不手抄。判法与 `config.d` 里的文档**同一条律**(属主可信、路径上非链接组件不得组 / 他人可写、
|
|
12
|
+
* 常规文件、有大小上界)—— 一条律、两张名单(`config.d` 读回面一张,本张一张);`config.d` 不在时也照判。
|
|
13
|
+
* `<根>/.env` **不在**这张名单:引擎今天的目录信任判官只判 `config.d` 与它的读回面,不判 `.env`(秘密值文件,本包的文件仓库不读它);本名单只登记引擎会读的根级文档。
|
|
14
|
+
*/
|
|
15
|
+
export const CONFIG_ROOT_DOCUMENTS = [ENV_MODELS_DOCUMENT, migrationMarkerDocument("SS-M11")];
|
|
16
|
+
/** 包内:`id` 是写运行记录的迁移吗(读运行记录之前判:别的 id 没有记录可读,更不许拿它拼路径)。 */
|
|
17
|
+
export const isMarkedMigrationId = (id) => typeof id === "string" && CONFIG_ROOT_DOCUMENTS.includes(`${MIGRATION_MARKER_PREFIX}${id}`);
|
|
18
|
+
/** 决定文档里能写的两个词(闭集):`replaced` = 目录管模型面时顶替 env 模型表;`kept` = 不顶替(与缺席同义,运行记录在场之后)。 */
|
|
19
|
+
export const ENV_MODELS_DECISIONS = ["replaced", "kept"];
|
|
20
|
+
/** 包内:是不是决定词(闭集,判法只此一只;写函数与读法同调)。 */
|
|
21
|
+
export const isDecisionWord = (v) => ENV_MODELS_DECISIONS.includes(v);
|
|
22
|
+
/** 根级文档读不懂的三种原因(闭集):`bad_json` = 空 / 纯空白 / 不是 JSON;`bad_shape` = 是 JSON 但不是这份文档的形;`io` = 字节读不出(权限 —— 可信属主的 0000 文件读恒 `EACCES`,是持久的 —— 不是常规文件、悬空链接、瞬时读错)。 */
|
|
23
|
+
export const ROOT_DOCUMENT_UNREADABLE_REASONS = ["bad_json", "bad_shape", "io"];
|
|
24
|
+
/**
|
|
25
|
+
* 一份决定读数的**身份**(带标签的一种拼法):`absent` / `d:<digest>`(有字节的四臂)/ `io:<errno>`。引擎读这份决定时(今天引擎还不读),「每份内容只告警一次」的去重键与本地 etag 的贡献都取它 ——
|
|
26
|
+
* 来自同一次读,不再开文件(那就是第二个读者)。
|
|
27
|
+
*/
|
|
28
|
+
export function envModelsDecisionTag(decision) {
|
|
29
|
+
switch (decision.kind) {
|
|
30
|
+
case "absent":
|
|
31
|
+
return "absent";
|
|
32
|
+
case "replaced":
|
|
33
|
+
case "kept":
|
|
34
|
+
return `d:${decision.digest}`;
|
|
35
|
+
case "unreadable":
|
|
36
|
+
return decision.reason === "io" ? `io:${decision.errno}` : `d:${decision.digest}`;
|
|
37
|
+
default: {
|
|
38
|
+
const unhandled = decision;
|
|
39
|
+
throw new Error(`envModelsDecisionTag: unhandled decision ${JSON.stringify(unhandled)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** 同一次读得到的**字节**的 digest(见 {@link EnvModelsDecision};对字节求,不对解码后的文本 —— 非法 UTF-8 解码成 U+FFFD 之后不同的字节会同 digest)。 */
|
|
44
|
+
const digestOf = (bytes) => toHex(sha256Digest(bytes)).slice(0, 16);
|
|
45
|
+
/** 字节 → 文本(UTF-8;非法序列换 U+FFFD;BOM 留在文本里,由 {@link parseJsonText} 的 trim 去掉 —— 与 `config.d` 文档的文本读法同一结果)。 */
|
|
46
|
+
const textOf = (bytes) => new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes);
|
|
47
|
+
/**
|
|
48
|
+
* 一个 JSON 值若是「恰好这一个自有键」的纯对象 ⇒ 那个键的值;否则(多一个键 —— 含 `JSON.parse` 铸出的自有 `__proto__` 键 —— 少一个键、不是纯对象)⇒ `undefined`。
|
|
49
|
+
* 两份文档的形都是 strict 的,判在这里一处;不交给 zod 的 strict:它对自有 `__proto__` 键的处置随版本变(4.4.x 跳过它、4.6.x 拒它),本包的 zod 区间两端答案会不同。
|
|
50
|
+
* 文档是 `JSON.parse` 的产物,自有键都是可枚举数据属性,`Object.keys` 就是它的全部键。
|
|
51
|
+
*/
|
|
52
|
+
function soleOwnValue(v, key) {
|
|
53
|
+
if (!isPlainJsonObject(v))
|
|
54
|
+
return undefined;
|
|
55
|
+
const keys = Object.keys(v);
|
|
56
|
+
return keys.length === 1 && keys[0] === key ? v[key] : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** 文本 → JSON 值:空 / 纯空白 / 坏 JSON ⇒ 读不成(一份被截断的文件不是缺席)。 */
|
|
59
|
+
function parseJsonText(text) {
|
|
60
|
+
const trimmed = text.trim();
|
|
61
|
+
if (!trimmed)
|
|
62
|
+
return { ok: false };
|
|
63
|
+
try {
|
|
64
|
+
return { ok: true, value: JSON.parse(trimmed) };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { ok: false };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** 包内:决定文档的**字节读法**(唯一一份;`./config-root-node` 的读者在拿到字节之后调它)—— 字节 → 答案(带这些字节的 digest)。 */
|
|
71
|
+
export function readEnvModelsDecisionBytes(bytes) {
|
|
72
|
+
const digest = digestOf(bytes);
|
|
73
|
+
const json = parseJsonText(textOf(bytes));
|
|
74
|
+
if (!json.ok)
|
|
75
|
+
return { kind: "unreadable", reason: "bad_json", digest };
|
|
76
|
+
const word = soleOwnValue(json.value, "envModels");
|
|
77
|
+
return isDecisionWord(word) ? { kind: word, digest } : { kind: "unreadable", reason: "bad_shape", digest };
|
|
78
|
+
}
|
|
79
|
+
/** 包内:字节没读到时的决定读数(errno 的拼法只在本文件)。 */
|
|
80
|
+
export function ioDecision(errno) {
|
|
81
|
+
return { kind: "unreadable", reason: "io", errno: errno ?? "uncoded" };
|
|
82
|
+
}
|
|
83
|
+
/** 包内:运行记录文档的值(`{"migration":<id>}`,写与读同一只形)。 */
|
|
84
|
+
export const migrationMarkerValue = (id) => ({ migration: id });
|
|
85
|
+
/** 包内:运行记录的**字节读法**(唯一一份):`{"migration":<这条记录的 id>}` strict ⇒ `present`;别的 ⇒ 读不懂(id 对不上也是坏形)。 */
|
|
86
|
+
export function readMigrationMarkerBytes(id, bytes) {
|
|
87
|
+
const json = parseJsonText(textOf(bytes));
|
|
88
|
+
if (!json.ok)
|
|
89
|
+
return { kind: "unreadable", reason: "bad_json" };
|
|
90
|
+
return soleOwnValue(json.value, "migration") === id ? { kind: "present" } : { kind: "unreadable", reason: "bad_shape" };
|
|
91
|
+
}
|
|
92
|
+
/** 包内:字节 → 文本(`config.d/models.json` 的读也经它,与上面两份同一解码)。 */
|
|
93
|
+
export const decodeRootBytes = textOf;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type ConfigRootDocument, type EnvModelsDecision, type EnvModelsDecisionWord, type MarkedMigrationId, type MigrationMarker, type RootDocumentUnreadableReason } from "./config-root-documents.js";
|
|
2
|
+
import { type EnvModelTableEntry, type EnvSnapshot } from "./env-model-table.js";
|
|
3
|
+
import type { MigrationReceipt } from "./migration-receipt.js";
|
|
4
|
+
/** 根级文档读写的 I/O 座(注入;真绑定 {@link nodeConfigRootIo})。写的两只按文档名取模式类({@link ROOT_DOCUMENT_CLASS},类的规则见原子写器的模式表)。 */
|
|
5
|
+
export interface ConfigRootIo {
|
|
6
|
+
/** 读一份文件的**字节**(digest 对字节求;解码由本包做);不存在 ⇒ 抛 `code: "ENOENT"` 的错,别的读不出照常抛(带 errno)。 */
|
|
7
|
+
readFile(file: string): Promise<Uint8Array>;
|
|
8
|
+
/** 原子覆盖写一份根级文档(同目录临时文件 → 改名;模式按该文档的类:新建经 umask、已有的沿用)。父目录须已存在。 */
|
|
9
|
+
writeDocument(document: ConfigRootDocument, file: string, text: string): Promise<void>;
|
|
10
|
+
/** 原子地**独占创建**一份根级文档(独占建临时文件、fsync → `link`;新建模式同 {@link writeDocument}):建成 ⇒ `created`;目标名已在(只有 `EEXIST`)⇒ `present`(一字不动);别的失败照常抛。 */
|
|
11
|
+
createDocument(document: ConfigRootDocument, file: string, text: string): Promise<"created" | "present">;
|
|
12
|
+
/** 目录不在就建(仅属主可进,连同缺的上级):建了 ⇒ `created`;已在 ⇒ `present`。 */
|
|
13
|
+
ensureDirectory(dir: string): Promise<"created" | "present">;
|
|
14
|
+
}
|
|
15
|
+
/** 真文件系统的绑定:读先过本包的符号链接判法(链接 ⇒ 它指向的真文件;悬空 ⇒ 抛,不是缺席),只读常规文件;写走本包唯一的原子写器。 */
|
|
16
|
+
export declare const nodeConfigRootIo: ConfigRootIo;
|
|
17
|
+
/** 配置根上 env 模型表决定的**唯一读者**(闭集答案见 `EnvModelsDecision`;从字节读出的答案带同一次读的 `digest`,io 带 `errno`)。从不抛。 */
|
|
18
|
+
export declare function readEnvModelsDecision(root: string, io?: ConfigRootIo): Promise<EnvModelsDecision>;
|
|
19
|
+
/**
|
|
20
|
+
* 一条一次性迁移的运行记录的**唯一读者**(文件 `<根>/config.migration.<id>.json`):不存在 ⇒ `absent`;`{"migration":<id>}` ⇒ `present`;别的 ⇒ `unreadable`
|
|
21
|
+
* (坏 JSON / 坏形 —— id 对不上也算 / 读不出)。「跑过」只问这一份文件在不在、读不读得懂,不读任何聚合文件。读数从不抛;`id` 不是写运行记录的迁移(根级文档名单里没有它的记录)⇒
|
|
22
|
+
* `TypeError`,不拿它拼路径。
|
|
23
|
+
*/
|
|
24
|
+
export declare function readMigrationMarker(root: string, id: MarkedMigrationId, io?: ConfigRootIo): Promise<MigrationMarker>;
|
|
25
|
+
/** {@link writeEnvModelsDecision} 的写法(闭集):`overwrite` = 写者(用户确认之后,整份覆盖);`createIfAbsent` = 不存在才建。 */
|
|
26
|
+
export type EnvModelsDecisionWrite = "overwrite" | "createIfAbsent";
|
|
27
|
+
/**
|
|
28
|
+
* 写配置根上的 env 模型表决定 —— 决定文件的**唯一写函数**(迁移铸确认也经它的 `createIfAbsent`;运行记录不归它,见文件头)。`overwrite` 恒答 `written`;`createIfAbsent` 在文档已在(`EEXIST`)时答 `present`(一字不动)。
|
|
29
|
+
* 决定词 / 写法不在闭集 ⇒ `TypeError`(一字不写)。写失败 ⇒ 抛本包的文件系统失败句(点名路径)。根须已存在。
|
|
30
|
+
*/
|
|
31
|
+
export declare function writeEnvModelsDecision(root: string, decision: EnvModelsDecisionWord, opts: {
|
|
32
|
+
readonly write: EnvModelsDecisionWrite;
|
|
33
|
+
}, io?: ConfigRootIo): Promise<"written" | "present">;
|
|
34
|
+
/** SS-M11 的结局(闭集,调用方穷举;本函数不出声,告警由调用方按结局发)。 */
|
|
35
|
+
export type EnvModelsMigration =
|
|
36
|
+
/** 运行记录已在:不跑。 */
|
|
37
|
+
{
|
|
38
|
+
readonly kind: "already_ran";
|
|
39
|
+
}
|
|
40
|
+
/** 运行记录读不懂:按已跑处理(读不懂记录就不替人铸确认),一字不写。调用方告警。 */
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: "marker_unreadable";
|
|
43
|
+
readonly reason: RootDocumentUnreadableReason;
|
|
44
|
+
}
|
|
45
|
+
/** 独占创建运行记录时它已在(`EEXIST`):别的进程已是本根的铸造者 ⇒ 本进程永不铸。 */
|
|
46
|
+
| {
|
|
47
|
+
readonly kind: "not_minter";
|
|
48
|
+
}
|
|
49
|
+
/** 运行记录建不成(只读根、根建不出、`ENOSPC`、文件系统不支持硬链接 —— `ENOTSUP` / `EPERM` …):不铸 —— 缺席照旧读作 kept。
|
|
50
|
+
* 记录始终建不成 ⇒ 每次起服都重跑、都答这一格(调用方每次告警;手写的决定照常生效,告警不因它停)。`error` 点名记录文件,`wayOut` 是手工恢复句。 */
|
|
51
|
+
| {
|
|
52
|
+
readonly kind: "marker_write_failed";
|
|
53
|
+
readonly errno: string | undefined;
|
|
54
|
+
readonly error: string;
|
|
55
|
+
readonly wayOut: string;
|
|
56
|
+
}
|
|
57
|
+
/** 本进程独占创建了运行记录 = 本根唯一的铸造者;`decision` = 选举**之前**读到的决定(铸只按它),`mint` = 铸没铸。 */
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: "recorded";
|
|
60
|
+
readonly createdRoot: boolean;
|
|
61
|
+
readonly decision: EnvModelsDecision;
|
|
62
|
+
readonly mint: EnvModelsMint;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* SS-M11 铸确认的结局(闭集)。**披露**:运行记录先于确认建;进程在两者之间中断,或铸失败、或 models.json 读不出 ⇒ 此后无人再铸、决定缺席读作 kept ——
|
|
66
|
+
* 升级前由目录服务的根这时换成 env 模型服务,旧目录不会自动保住;出路是 `wayOut` 那一句(手写决定文件)。
|
|
67
|
+
*/
|
|
68
|
+
export type EnvModelsMint =
|
|
69
|
+
/** 铸了 `replaced`:`replacedEnvModels` = 从此被目录顶替的 env 模型(名 + 槽 + 来源键;env 表没声明 ⇒ 空)—— 非空时调用方据此发一行一次性告警,点名它们与出路(删掉 / 写 kept)。 */
|
|
70
|
+
{
|
|
71
|
+
readonly kind: "minted";
|
|
72
|
+
readonly receipt: MigrationReceipt;
|
|
73
|
+
readonly replacedEnvModels: readonly EnvModelTableEntry[];
|
|
74
|
+
}
|
|
75
|
+
/** 独占创建决定时它已在(只有 `EEXIST` 算):期间有人写了决定(用户的 kept / 写者的 replaced)⇒ 一字不动。 */
|
|
76
|
+
| {
|
|
77
|
+
readonly kind: "exists";
|
|
78
|
+
}
|
|
79
|
+
/** 决定建不成(`ENOSPC` / `EIO` / `EPERM` / 文件系统不支持硬链接 …):缺席照旧读作 kept,记录在 ⇒ 此后不再尝试。`error` 点名决定文件,`wayOut` 是手工恢复句。 */
|
|
80
|
+
| {
|
|
81
|
+
readonly kind: "failed";
|
|
82
|
+
readonly errno: string | undefined;
|
|
83
|
+
readonly error: string;
|
|
84
|
+
readonly wayOut: string;
|
|
85
|
+
}
|
|
86
|
+
/** 不铸:选举前读到的决定已在(replaced / kept / 读不懂 —— 读不懂的不覆盖;它之后被删也不重铸)/ 目录不管模型面(没有 models.json —— 只有 `ENOENT` 算没有 —— 或坏文件)。
|
|
87
|
+
* 判序:决定 → 目录。铸不铸不看 env 表:迁移保的是升级前「管模型面的目录整表顶替 env」这个读法,与 env 声没声明无关。 */
|
|
88
|
+
| {
|
|
89
|
+
readonly kind: "skipped";
|
|
90
|
+
readonly why: "decision_present" | "not_managing";
|
|
91
|
+
}
|
|
92
|
+
/** 不铸,且要运维动手:决定缺席,而 models.json 读不出(`EACCES` / `EIO` / `ESTALE` / 悬空符号链接 / 不是常规文件 …)—— 管不管判不了,不替人铸(fail-closed)。
|
|
93
|
+
* 运行记录已在 ⇒ 此后不再尝试:与 `failed` 同一出路(`wayOut`,手写决定文件);`error` 点名 models.json 与读错,`errno` 只在底层错误带码时有
|
|
94
|
+
* (悬空链接 / 不是常规文件 ⇒ `undefined`),调用方告警。 */
|
|
95
|
+
| {
|
|
96
|
+
readonly kind: "models_unreadable";
|
|
97
|
+
readonly errno: string | undefined;
|
|
98
|
+
readonly error: string;
|
|
99
|
+
readonly wayOut: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* **一次性迁移 SS-M11**(登记见 `ONE_TIME_MIGRATIONS`;起服、第一次读本地配置之前调,只在配置源是本地时):
|
|
103
|
+
* 1. 读运行记录:`present` ⇒ `already_ran`;读不懂 ⇒ `marker_unreadable`(按已跑处理)。
|
|
104
|
+
* 2. **先读**:决定 D0 = `readEnvModelsDecision`,M0 = `config.d/models.json` 管不管模型面(管 / 不管 / 读不出)。此后的铸只按 D0 / M0 决定,不再读决定 ——
|
|
105
|
+
* D0 是 replaced(预置)而它在选举之后被删 ⇒ 不重铸;D0 是 absent 而选举之后有人写了决定 ⇒ 第 5 步的独占创建守住它。
|
|
106
|
+
* 3. 根缺席 ⇒ 建(仅属主可进,连同缺的上级,模式恰是 0o700);**不建 `config.d`**。
|
|
107
|
+
* 4. **独占创建运行记录** `{"migration":"SS-M11"}`:建成 ⇒ 本进程是本根**唯一的铸造者**;已在(`EEXIST`)⇒ `not_minter`,永不铸;别的失败 ⇒ `marker_write_failed`,不铸。
|
|
108
|
+
* 5. 仅铸造者:D0 不是 `absent` ⇒ 不铸(读不懂的也不覆盖);M0 不管 ⇒ 不铸;M0 读不出 ⇒ 不铸(`models_unreadable`,带 errno(若有)与手工恢复句);否则**独占创建** `{"envModels":"replaced"}` —— 结局三词:`minted` / `exists`(只有 `EEXIST`)/
|
|
109
|
+
* `failed`(带 errno 与手工恢复句)。铸不铸不看 env;env 只在铸成之后读一次、只读两槽,用来点名被顶替的 env 模型。
|
|
110
|
+
* 运行记录在场之后,确认缺席 ≡ kept(读这份决定的一律这么读:引擎强制这道闸之后、迁移、写者)。
|
|
111
|
+
* 撤销的终局按运行记录的创建者算:只有创建者能写确认;以运行记录在场为前提,创建者写完 / 判定不写 / 失败(结局 `minted` / `exists` / `skipped` / `models_unreadable` / `failed`,调用方各记一行)之后,删掉决定文件就是撤销且是终局(删掉运行记录再起服 ⇒ 迁移重跑,决定缺席即重铸);其它副本读到记录在场 = 已跑、跳过(或得 `EEXIST` 答 `not_minter`),它们的返回不构成终局;创建者当选到结局落定之间的窗口在挂起 / I/O 卡顿下无上界,窗口内唯一安全的撤销是写 `kept`(独占创建护住它;删掉的会被创建者照它先读的 D0 铸回);创建者在写确认之前崩溃 ⇒ 永久 `kept`(fail-closed,披露)。
|
|
112
|
+
*/
|
|
113
|
+
export declare function migrateEnvModelsDecision(input: {
|
|
114
|
+
readonly root: string;
|
|
115
|
+
readonly env: EnvSnapshot;
|
|
116
|
+
readonly io?: ConfigRootIo;
|
|
117
|
+
}): Promise<EnvModelsMigration>;
|