@sema-agent/settings-schema 15.2.0 → 17.0.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 +183 -0
- package/README.md +71 -46
- package/conformance/VERSION +1 -1
- package/conformance/cases/c16-deep-nesting-205/expected.json +2 -2
- package/conformance/cases/c16-deep-nesting-205/meta.json +1 -1
- package/conformance/cases/c50-s784-lane-host-legal/expected.json +6 -2
- package/conformance/cases/c50-s784-lane-host-legal/input.json +3 -1
- package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +6 -2
- package/conformance/cases/c51-s784-lane-e2b-legal/input.json +3 -1
- package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +6 -2
- package/conformance/cases/c52-s784-lane-k8s-legal/input.json +3 -1
- package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +6 -2
- package/conformance/cases/c53-s784-lane-ssh-legal/input.json +3 -1
- package/conformance/cases/c54-s784-lane-adb-legal/expected.json +6 -2
- package/conformance/cases/c54-s784-lane-adb-legal/input.json +3 -1
- package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +6 -2
- package/conformance/cases/c55-s784-lane-local-docker-legal/input.json +3 -1
- package/conformance/cases/c56-s784-lane-device-legal/expected.json +8 -2
- package/conformance/cases/c56-s784-lane-device-legal/input.json +5 -2
- package/conformance/cases/c57-s784-g1-enabled-no-lane/meta.json +1 -1
- package/conformance/cases/c58-s784-g2-multitenant-host/input.json +3 -1
- package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +3 -1
- package/conformance/cases/c60-s784-g3-e2b-missing-key/input.json +3 -1
- package/conformance/cases/c61-s784-g3-other-lane-key/input.json +3 -1
- package/conformance/cases/c62-s784-g7a-session-tidb/input.json +3 -1
- package/conformance/cases/c63-s784-host-two-replicas/input.json +3 -1
- package/conformance/cases/c64-s784-multitenant-local-store/input.json +3 -1
- package/conformance/cases/c65-s784-pg-without-db/input.json +3 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/input.json +3 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/meta.json +1 -1
- package/conformance/cases/c68-s784-device-local-store/input.json +6 -1
- package/conformance/cases/cli04-env-value-with-newline/expected.json +6 -2
- package/conformance/cases/cli04-env-value-with-newline/input.json +3 -1
- package/dist/atomic-write.d.ts +4 -0
- package/dist/atomic-write.js +20 -0
- package/dist/bundle.d.ts +0 -18
- package/dist/bundle.js +1 -54
- package/dist/cli.d.ts +1 -1
- package/dist/config-d-file.d.ts +13 -12
- package/dist/config-d-file.js +72 -27
- package/dist/config-document-refusal.d.ts +9 -7
- package/dist/config-document-refusal.js +2 -2
- package/dist/device-relay.d.ts +21 -0
- package/dist/device-relay.js +23 -0
- package/dist/env-model-table.d.ts +2 -9
- package/dist/env-model-table.js +6 -14
- package/dist/execution-lane-index.d.ts +19 -7
- package/dist/execution-lane-index.js +26 -8
- package/dist/execution-lanes.d.ts +9 -4
- package/dist/execution-lanes.js +18 -6
- package/dist/file-store.d.ts +31 -4
- package/dist/file-store.js +78 -11
- package/dist/fleet.d.ts +6 -8
- package/dist/fleet.js +5 -7
- package/dist/hooks.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/local-load.d.ts +12 -4
- package/dist/local-load.js +20 -31
- package/dist/migration-receipt.d.ts +1 -1
- package/dist/migration-receipt.js +1 -1
- package/dist/one-time-migrations.d.ts +1 -1
- package/dist/one-time-migrations.js +166 -45
- package/dist/remote-exec.js +11 -4
- package/dist/types.d.ts +15 -11
- package/dist/types.js +99 -48
- package/dist/url-shape.d.ts +18 -0
- package/dist/url-shape.js +22 -0
- package/docs/API-SURFACE.md +12 -8
- package/docs/CONFORMANCE.md +1 -7
- package/docs/CONSUMER-INTEGRATION.md +3 -3
- package/llms-full.txt +25 -29
- package/llms.txt +9 -8
- package/package.json +1 -1
package/dist/config-d-file.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 包内私有(不进主桶、不进 exports 表):**一只** `config.d/<名>.json` 文件读法 —— `FileConfigStore`、`readBundle`、`loadRemoteExec` 与命令行 `migrate` 读同一个目录,
|
|
3
|
-
* 同一种文件形同一个答案、同一句话(12.0.0 起 `remote-exec.json` 也是这一只;此前它有自己的一只文本判,只拒空与坏 JSON
|
|
4
|
-
*
|
|
3
|
+
* 同一种文件形同一个答案、同一句话(12.0.0 起 `remote-exec.json` 也是这一只;此前它有自己的一只文本判,只拒空与坏 JSON,非对象在命令行上被当成「无事可做」;17.0.0 起连**读盘**也是这一只 ——
|
|
4
|
+
* `loadRemoteExec` 不再有私有的 `readFile`,走 `readConfigDFile(root, "remote-exec")`)。
|
|
5
|
+
* 分两只:纯判 `judgeConfigDText`(文本 → 值 | 抛,零 I/O;命令行自己读文本,只调它)与读盘的 `readConfigDFile`(调它);路径与名字的判只在 `configDPath`(`remote-exec` 的路径也出自它)。
|
|
5
6
|
*
|
|
6
7
|
* **一次读法,两种渲染**(15.2.0):内核交**结构化读数**({@link ConfigDReading}:缺席 / 在场 / 被拒 + 闭集词 + 整句 + 抛出物 + 指纹)——
|
|
7
8
|
* `judgeConfigDTextReading`(文本 → 读数)与 `readConfigDFileReading`(文件 → 读数)是判据的唯一一处;抛出口 `judgeConfigDText` / `readConfigDFile` 把被拒的读数渲染成「抛它的 `thrown`」
|
|
8
|
-
* (原来抛的那个对象、那句话,io 是原样的 fs 错误)
|
|
9
|
+
* (原来抛的那个对象、那句话,io 是原样的 fs 错误),单读口 `FileConfigStore.read()`(16.0.0)把整个目录的读数渲染成 effective 或类型化的拒单。两种渲染读同一份判据,不会各说各话。
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
* (`
|
|
11
|
+
* 律:**缺席 = 路径上没有任何条目**(`undefined`):`ENOENT`,且文档本身与 `config.d` 目录都不是悬空的符号链接(链接在而目标不在是**在场却读不出**,读数是 `io` 拒,句子点名链接与出路 ——
|
|
12
|
+
* 本文件的缺席臂就是 `readTextIfPresent`(`./atomic-write`));在场的文件必须是一个 JSON 对象。空 / 纯空白(半写、截断)、坏 JSON、解析出来不是对象
|
|
13
|
+
* (`null` / 数组 / 数 / 串 / 布尔)、嵌得比整棵目录的内容版本能算的还深(16.0.0)一律是**坏文件** ⇒ 抛(fail-closed:把一个被截断的文件读成缺席,就是静默丢掉那一个域 —— 例:截断的
|
|
12
14
|
* governance.json 丢掉它的护栏;截断的 remote-exec.json 把隔离车道降成宿主车道)。三种坏形每种一句,点名**调用方寻址这份文档用的路径**与形(读的人给路径,
|
|
13
15
|
* 判官不拼 `config.d/` 字面 —— 多个目录各有一份时,句子认得出是哪个 scope 的哪一份;文档是符号链接时点名链接这一端:句尾「delete it」删的是这个 scope 里的这一项,不是被几个 scope 共用的目标)、
|
|
14
16
|
* 句尾出路按文档类({@link documentWayOut}),从不回显文件原文。
|
|
15
17
|
*/
|
|
16
|
-
import { readFile } from "node:fs/promises";
|
|
17
18
|
import path from "node:path";
|
|
19
|
+
import { readTextIfPresent } from "./atomic-write.js";
|
|
18
20
|
import { refuseRetiredDomain, sanitizeZodError } from "./config-fns.js";
|
|
19
|
-
import { stableHash } from "./hash.js";
|
|
20
|
-
import { UNINSPECTABLE, guardedSafeParse } from "./own-key-read.js";
|
|
21
|
+
import { MAX_HASH_DEPTH, stableHash } from "./hash.js";
|
|
22
|
+
import { UNINSPECTABLE, guardedSafeParse, ownElementsOf, ownEntriesOf } from "./own-key-read.js";
|
|
21
23
|
import { jsonShapeOf } from "./plain-json-object.js";
|
|
22
24
|
import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
|
|
23
25
|
import { RETIRED_DOMAINS, isDomainName, isRetiredDomainName } from "./types.js";
|
|
@@ -36,9 +38,9 @@ export function parseRemoteExecDocument(raw) {
|
|
|
36
38
|
return judged.value.data;
|
|
37
39
|
throw judged.value.error;
|
|
38
40
|
}
|
|
39
|
-
/** `<root>/config.d/<名>.json` 的路径 —— 名字的判也在这里(**一处**)
|
|
41
|
+
/** `<root>/config.d/<名>.json` 的路径 —— 名字的判也在这里(**一处**):不是 `config.d` 里能放的文档名(配置域、retired 域、`remote-exec`)⇒ 拒(不拿用户给的名拼路径,防 `../` 穿越)。 */
|
|
40
42
|
export function configDPath(root, name) {
|
|
41
|
-
if (!isDomainName(name) && !isRetiredDomainName(name))
|
|
43
|
+
if (!isDomainName(name) && !isRetiredDomainName(name) && name !== REMOTE_EXEC_DOCUMENT)
|
|
42
44
|
throw new Error(`"${name}" is not a config domain — refusing to build a config.d path from it (no traversal)`);
|
|
43
45
|
return path.join(root, "config.d", `${name}.json`);
|
|
44
46
|
}
|
|
@@ -47,8 +49,47 @@ function documentWayOut(name) {
|
|
|
47
49
|
return name === REMOTE_EXEC_DOCUMENT ? "delete it for the host default, or write a valid remoteExec spec" : "delete it for the domain default, or write a valid object";
|
|
48
50
|
}
|
|
49
51
|
const messageOf = (thrown) => (thrown instanceof Error ? thrown.message : String(thrown));
|
|
52
|
+
/** 一份文档在内容版本的哈希里比快照的根深一层(快照 = `{ <文档名>: 文档 }`),所以文档自己最深嵌 {@link MAX_HASH_DEPTH} − 1 层(文档的根 = 第 0 层)——
|
|
53
|
+
* 再深一层,整棵目录的版本就算不出(16.0.0 之前它在算版本那一步抛,把已经收集的拒一并丢掉;现在它是这一份文档的文件层拒,别的拒照列)。 */
|
|
54
|
+
const MAX_CONFIG_D_DOCUMENT_DEPTH = MAX_HASH_DEPTH - 1;
|
|
55
|
+
/** 一份文档是否嵌得比 `limit` 深(根 = 第 0 层;迭代、遇超即停 —— 多深的文档都不会把调用栈压爆)。成员经包的自有键读法取(数据属性取值,访问器调它自己的 getter 一次,不问代理陷阱);
|
|
56
|
+
* 读不出的容器不往下量 —— 它不是「过深」,是读不出,由之后读它的那一步响亮拒(版本哈希、迁移的快照都对它抛)。 */
|
|
57
|
+
function nestsDeeperThan(value, limit) {
|
|
58
|
+
const stack = [[value, 0]];
|
|
59
|
+
const descend = (members, depth) => {
|
|
60
|
+
if (members.ok)
|
|
61
|
+
for (const [, child] of members.value)
|
|
62
|
+
stack.push([child, depth + 1]);
|
|
63
|
+
};
|
|
64
|
+
for (let top = stack.pop(); top !== undefined; top = stack.pop()) {
|
|
65
|
+
const [node, depth] = top;
|
|
66
|
+
if (depth > limit)
|
|
67
|
+
return true;
|
|
68
|
+
// 往下量的容器与版本哈希往下走的同一组(对象 —— 朴素或不朴素,函数除外 —— 与数组);叶子与读不出的不往下。
|
|
69
|
+
const shape = jsonShapeOf(node);
|
|
70
|
+
switch (shape) {
|
|
71
|
+
case "plain":
|
|
72
|
+
case "non_plain_object":
|
|
73
|
+
if (typeof node !== "function")
|
|
74
|
+
descend(ownEntriesOf(node), depth);
|
|
75
|
+
break;
|
|
76
|
+
case "array":
|
|
77
|
+
descend(ownElementsOf(node), depth);
|
|
78
|
+
break;
|
|
79
|
+
case "null":
|
|
80
|
+
case "primitive":
|
|
81
|
+
case "uninspectable":
|
|
82
|
+
break;
|
|
83
|
+
default: {
|
|
84
|
+
const unhandled = shape;
|
|
85
|
+
throw new Error(`config.d: unhandled shape ${String(unhandled)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
50
91
|
/** 一个 `config.d/<名>.json` 在场时的**文本** → 读数(纯判,零 I/O;见文件头的律):一个 JSON 对象 ⇒ 在场;空 / 纯空白、坏 JSON、
|
|
51
|
-
*
|
|
92
|
+
* 不是对象、嵌得过深({@link MAX_CONFIG_D_DOCUMENT_DEPTH})⇒ 被拒,每种一个词一句话(点名 `file` —— 调用方读的那份文件的真实路径 —— 与形,句尾 {@link documentWayOut})。 */
|
|
52
93
|
export function judgeConfigDTextReading(name, text, file) {
|
|
53
94
|
const trimmed = text.trim();
|
|
54
95
|
const refuse = (reason, thrown) => ({ status: "refused", reason, error: thrown.message, thrown, fingerprint: stableHash(trimmed) });
|
|
@@ -62,9 +103,9 @@ export function judgeConfigDTextReading(name, text, file) {
|
|
|
62
103
|
return refuse("invalid_json", new Error(`invalid JSON in ${file} — ${documentWayOut(name)}`)); // sanitized: never echo the raw file text into the error
|
|
63
104
|
}
|
|
64
105
|
const flaw = configDValueFlaw(name, parsed, file);
|
|
65
|
-
return flaw === undefined ? { status: "present", value: parsed } : refuse(
|
|
106
|
+
return flaw === undefined ? { status: "present", value: parsed } : refuse(flaw.reason, flaw.error);
|
|
66
107
|
}
|
|
67
|
-
/** 抛出口:一个 `config.d/<名>.json` 在场时的**文本** → 它的值:读数是被拒 ⇒ 抛它的 `thrown`(空 / 坏 JSON / 不是对象各一句)。读盘的入口(`readConfigDFile
|
|
108
|
+
/** 抛出口:一个 `config.d/<名>.json` 在场时的**文本** → 它的值:读数是被拒 ⇒ 抛它的 `thrown`(空 / 坏 JSON / 不是对象各一句)。读盘的入口(`readConfigDFile`)
|
|
68
109
|
* 与自己读文本的命令行(`migrate`)都过它 —— 一种坏形一句话,不论是哪份文档。 */
|
|
69
110
|
export function judgeConfigDText(name, text, file) {
|
|
70
111
|
const reading = judgeConfigDTextReading(name, text, file);
|
|
@@ -72,20 +113,25 @@ export function judgeConfigDText(name, text, file) {
|
|
|
72
113
|
throw reading.thrown;
|
|
73
114
|
return reading.value;
|
|
74
115
|
}
|
|
75
|
-
/** 一份**已解析的**存量文档在场时的值(同步口 `migrateConfigDocuments` 收的是各层的文档原文,不是文件文本)→
|
|
76
|
-
*
|
|
116
|
+
/** 一份**已解析的**存量文档在场时的值(同步口 `migrateConfigDocuments` 收的是各层的文档原文,不是文件文本)→ 它自己:一个嵌得不过深的 JSON 对象 ⇒ 它;`null` / 数组 / 数 / 串 / 布尔 /
|
|
117
|
+
* 非朴素对象、嵌得过深 ⇒ 抛。{@link judgeConfigDText} 的最后一道判就是这只(文本解析出来之后过它),所以「不是对象」「过深」各一种坏形一句话,不论文档是从文件文本来的还是调用方直接给的;
|
|
77
118
|
* `label` = 句首的称呼(文本路径给文件的真实路径,值路径给 `the <名> document`)。 */
|
|
78
119
|
export function judgeConfigDValue(name, value, label) {
|
|
79
120
|
const flaw = configDValueFlaw(name, value, label);
|
|
80
121
|
if (flaw !== undefined)
|
|
81
|
-
throw flaw;
|
|
122
|
+
throw flaw.error;
|
|
82
123
|
return value;
|
|
83
124
|
}
|
|
84
|
-
/** {@link judgeConfigDValue}
|
|
125
|
+
/** {@link judgeConfigDValue} 的判据本体:值是一个嵌得不过深的朴素 JSON 对象 ⇒ `undefined`;否则 ⇒ 拒因词与要抛的那个错(交回,不抛 —— 读数与抛出口各取所需)。 */
|
|
126
|
+
/** 过深那一句(点名文件 / 文档与层数上限,句尾出路)。 */
|
|
127
|
+
function tooDeep(name, label) {
|
|
128
|
+
return { reason: "too_deep", error: new Error(`${label} nests deeper than ${MAX_CONFIG_D_DOCUMENT_DEPTH} levels (the config snapshot holding it could not be versioned) — ${documentWayOut(name)}`) };
|
|
129
|
+
}
|
|
85
130
|
function configDValueFlaw(name, value, label) {
|
|
86
131
|
const shape = jsonShapeOf(value);
|
|
132
|
+
// 「朴素」那一臂:是 JSON 对象 ⇒ 只剩深度一问(过深 = 这一份文档的文件层拒)
|
|
87
133
|
if (shape === "plain")
|
|
88
|
-
return undefined;
|
|
134
|
+
return nestsDeeperThan(value, MAX_CONFIG_D_DOCUMENT_DEPTH) ? tooDeep(name, label) : undefined;
|
|
89
135
|
// `shape` is narrowed to the five non-plain words: an exhaustive switch (a sixth word is a compile error here)
|
|
90
136
|
let kind;
|
|
91
137
|
switch (shape) {
|
|
@@ -99,7 +145,7 @@ function configDValueFlaw(name, value, label) {
|
|
|
99
145
|
kind = typeof value === "function" ? "function" : "non-plain object";
|
|
100
146
|
break;
|
|
101
147
|
case "uninspectable": // 读不出(撤销的 Proxy / 陷阱抛)不是「不是对象」:调用方给的对象读不出是调用方的错 ⇒ `TypeError`(v14.6.0:引擎对它抛的就是 TypeError),句子带全包唯一那一句
|
|
102
|
-
return new TypeError(`${label} must be a JSON object (got ${UNINSPECTABLE}) — ${documentWayOut(name)}`);
|
|
148
|
+
return { reason: "not_object", error: new TypeError(`${label} must be a JSON object (got ${UNINSPECTABLE}) — ${documentWayOut(name)}`) };
|
|
103
149
|
case "primitive":
|
|
104
150
|
kind = typeof value;
|
|
105
151
|
break;
|
|
@@ -108,31 +154,30 @@ function configDValueFlaw(name, value, label) {
|
|
|
108
154
|
throw new Error(`judgeConfigDValue: unhandled shape ${String(unhandled)}`);
|
|
109
155
|
}
|
|
110
156
|
}
|
|
111
|
-
return new Error(`${label} must be a JSON object (got ${kind}) — ${documentWayOut(name)}`);
|
|
157
|
+
return { reason: "not_object", error: new Error(`${label} must be a JSON object (got ${kind}) — ${documentWayOut(name)}`) };
|
|
112
158
|
}
|
|
113
|
-
/** 读一个 `config.d/<名>.json` →
|
|
159
|
+
/** 读一个 `config.d/<名>.json` → 读数:路径上没有任何条目 ⇒ 缺席;在场 ⇒ {@link judgeConfigDTextReading};在场却读不出 ⇒ 被拒,词 `io` —— 非 ENOENT 的 fs 错误,以及悬空的符号链接
|
|
160
|
+
* (文档本身或 `config.d` 目录:内核跟链接读到 ENOENT,但条目在场;句子是 `danglingSymlinkSentence`(`./atomic-write`)那一句,与目录迁移的读口同一句)。缺席与悬空的分辨在 `readTextIfPresent`。 */
|
|
114
161
|
export async function readConfigDFileReading(root, name) {
|
|
115
162
|
const file = configDPath(root, name);
|
|
116
163
|
let text;
|
|
117
164
|
try {
|
|
118
|
-
text = await
|
|
165
|
+
text = await readTextIfPresent(file, [path.dirname(file)]);
|
|
119
166
|
}
|
|
120
167
|
catch (e) {
|
|
121
|
-
if (e.code === "ENOENT")
|
|
122
|
-
return { status: "absent" };
|
|
123
168
|
const error = messageOf(e);
|
|
124
169
|
return { status: "refused", reason: "io", error, thrown: e, fingerprint: stableHash(error) };
|
|
125
170
|
}
|
|
126
|
-
return judgeConfigDTextReading(name, text, file);
|
|
171
|
+
return text === undefined ? { status: "absent" } : judgeConfigDTextReading(name, text, file);
|
|
127
172
|
}
|
|
128
|
-
/** 抛出口:读一个 `config.d/<名>.json
|
|
173
|
+
/** 抛出口:读一个 `config.d/<名>.json`:路径上没有任何条目 ⇒ `undefined`;在场 ⇒ 它的值;被拒 ⇒ 抛读数的 `thrown`(见 {@link readConfigDFileReading})。 */
|
|
129
174
|
export async function readConfigDFile(root, name) {
|
|
130
175
|
const reading = await readConfigDFileReading(root, name);
|
|
131
176
|
if (reading.status === "refused")
|
|
132
177
|
throw reading.thrown;
|
|
133
178
|
return reading.status === "present" ? reading.value : undefined;
|
|
134
179
|
}
|
|
135
|
-
/** 一棵 `config.d` 原文快照的内容版本(稳定的 u53 整数):同样的文档 ⇒ 同一个数。`FileConfigStore
|
|
180
|
+
/** 一棵 `config.d` 原文快照的内容版本(稳定的 u53 整数):同样的文档 ⇒ 同一个数。`FileConfigStore` 的 `getVersion()` / `getEffective()` / `read()` 读同一只。 */
|
|
136
181
|
export function configDVersionOf(raws) {
|
|
137
182
|
return parseInt(stableHash(raws).slice(0, 13), 16); // 13 hex digits ≤ 2^52, safely within u53
|
|
138
183
|
}
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 契约的文件层拒一份 `config.d` 文档时交出的**类型化拒**(15.2.0)。纯数据与类型,零 node 依赖、零运行期 import —— 词表在包根,读目录的口(`
|
|
2
|
+
* 契约的文件层拒一份 `config.d` 文档时交出的**类型化拒**(15.2.0)。纯数据与类型,零 node 依赖、零运行期 import —— 词表在包根,读目录的口(`FileConfigStore.read()`,16.0.0)在 `./node`。
|
|
3
3
|
*
|
|
4
4
|
* ## 为什么
|
|
5
5
|
* 文件层读一份文档只有抛一句话的出口:句子不带结构化的文档名,也分不出「空 / 坏 JSON / 不是对象 / 读不出」;并发读时一次只抛第一个到达的坏文档;
|
|
6
6
|
* 带事实的retired 域文档(墓碑)抛的是 `RetiredDomainError`,而retired 域文档自己读不出时谁也归不了属。消费方(服务端的本地读腿)于是在严读抛了之后,
|
|
7
7
|
* 再逐域重探、自己归属、自己铸「拒的身份」—— 契约的判据在别人那里有了第二份。这里把拒的**事实**交成数据:读法只有一种(文件读法的内核交结构化读数),
|
|
8
|
-
* 旧的抛出口是它的渲染(抛的还是原来那个对象、那句话)
|
|
8
|
+
* 旧的抛出口是它的渲染(抛的还是原来那个对象、那句话),单读口 `FileConfigStore.read()` 是它在整个目录上的渲染(16.0.0 起;15.2.0 的 `inspectBundle` 只给拒单不给 effective,消费方因此要严读一次、
|
|
9
|
+
* 抛了再问它一次 —— 两读之间文档被修好,第二读的拒单是空的;它随单读口退役)。
|
|
9
10
|
*
|
|
10
11
|
* ## 形
|
|
11
12
|
* 一条拒 = `{ document, reason, error }`:
|
|
12
13
|
* · `document`:被拒的文档 —— 便携域名,或 retired 域名(retired 域的文档就以域名相称,墓碑拒的「域」就是这个名字,不另设第二个键);
|
|
13
|
-
* · `reason`:闭集词 {@link CONFIG_DOCUMENT_REFUSAL_REASONS}
|
|
14
|
-
* `null` / 数组 / 数 / 串 / 布尔)· `io`(在场却读不出:权限、路径是目录、I/O
|
|
15
|
-
*
|
|
14
|
+
* · `reason`:闭集词 {@link CONFIG_DOCUMENT_REFUSAL_REASONS}。文件层五个 —— `empty`(空 / 纯空白:半写、截断)· `invalid_json`(JSON 解析不过)· `not_object`(解析出来不是 JSON 对象:
|
|
15
|
+
* `null` / 数组 / 数 / 串 / 布尔)· `io`(在场却读不出:权限、路径是目录、I/O 错误、悬空的符号链接〔文档本身或 `config.d` 目录:链接在、目标不在,17.0.0 起;句子点名链接与出路〕;缺席 = 路径上没有任何条目,不是拒)· `too_deep`(16.0.0:嵌套深过整棵目录的内容版本能算的深度 ——
|
|
16
|
+
* 此前它在算版本那一步抛,已经收集的拒一并丢掉);第六个 `retired_domain` 是墓碑(retired 域文档带事实);
|
|
17
|
+
* · `error`:契约的整句,与旧出口抛出来的那一句逐字同(`empty` / `invalid_json` / `not_object` / `too_deep` 的句子点名文件的真实路径与形,从不回显文件原文;`io` 是 fs 错误的原话 ——
|
|
16
18
|
* 有的带真实路径(如 EACCES 的 `open '<路径>'`)、有的不带(EISDIR),要认是哪份文档读 `document`;调用方把 `error` 送出进程前自己脱敏);
|
|
17
19
|
* · 墓碑拒另带 `migration`(一次性迁移的编号,与 `RetiredDomainError.migration` 同值)。
|
|
18
20
|
* 词表是闭集:消费方用 `switch (refusal.reason)` 穷举,加词 ⇒ 它们编译红(这是通知)。
|
|
19
21
|
*/
|
|
20
22
|
import type { PortableDomain } from "./portable-domains.js";
|
|
21
23
|
import type { RetiredDomainName } from "./types.js";
|
|
22
|
-
/**
|
|
23
|
-
export declare const CONFIG_DOCUMENT_REFUSAL_REASONS: readonly ["empty", "invalid_json", "not_object", "io", "retired_domain"];
|
|
24
|
+
/** 文件层拒一份文档的闭集词:五个文件层成因 + 墓碑(恒在末位)。墓碑那个词就是 `RetiredDomainError.code`(一个词,不立第二个名)。 */
|
|
25
|
+
export declare const CONFIG_DOCUMENT_REFUSAL_REASONS: readonly ["empty", "invalid_json", "not_object", "io", "too_deep", "retired_domain"];
|
|
24
26
|
export type ConfigDocumentRefusalReason = (typeof CONFIG_DOCUMENT_REFUSAL_REASONS)[number];
|
|
25
27
|
/** 能被拒的 `config.d` 文档:便携域的文档,或retired 域的文档(读它只为墓碑)。 */
|
|
26
28
|
export type ConfigDocumentName = PortableDomain | RetiredDomainName;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export const CONFIG_DOCUMENT_REFUSAL_REASONS = ["empty", "invalid_json", "not_object", "io", "retired_domain"];
|
|
1
|
+
/** 文件层拒一份文档的闭集词:五个文件层成因 + 墓碑(恒在末位)。墓碑那个词就是 `RetiredDomainError.code`(一个词,不立第二个名)。 */
|
|
2
|
+
export const CONFIG_DOCUMENT_REFUSAL_REASONS = ["empty", "invalid_json", "not_object", "io", "too_deep", "retired_domain"];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* device 车道的设备入口 `DEVICE_RELAY_URL` 的**值文法** —— 唯一判官(16.0.0)。纯函数,浏览器安全;只 import 零依赖的 URL 形判官模块。
|
|
3
|
+
*
|
|
4
|
+
* ## 规则(一条)
|
|
5
|
+
* 设备拨哪里是部署的必填声明,合法值只有两种:
|
|
6
|
+
* · 一个 ws(s) 绝对 URL —— 设备经中继 / 网关拨它。不带 userinfo / query / fragment:引擎把它**逐字**发在能力面上(设备拨号的地址),
|
|
7
|
+
* 没有放凭据的位置(设备凭签名鉴权);
|
|
8
|
+
* · 字面 {@link DEVICE_RELAY_NONE} —— 没有中继,设备按引擎自己的地址拨。只认这一种拼法(`None` / `NONE` 不是它)。
|
|
9
|
+
* 这里不判的两件,各有自己的判官:缺席 / 空白是车道表必需栏的事(写闸对每个必需坐标同一句,`isBlankCoordinate`);值里认得出凭据形的片段由写闸对
|
|
10
|
+
* 每个坐标值的那一只密钥判据拒。
|
|
11
|
+
*
|
|
12
|
+
* ## 谁读
|
|
13
|
+
* 写闸(`WorkerSpec` 的 `runtime.execution.coords.DEVICE_RELAY_URL`)、web-admin 表单与引擎起服问同一只;引擎把 `none` 读成「没有中继」。
|
|
14
|
+
*/
|
|
15
|
+
import { type WsUrlFlaw } from "./url-shape.js";
|
|
16
|
+
/** 「没有中继,设备拨引擎自己的地址」的那个字面值。 */
|
|
17
|
+
export declare const DEVICE_RELAY_NONE = "none";
|
|
18
|
+
/** 规则句(不含值):写闸的拒句、表单的提示读这一句。 */
|
|
19
|
+
export declare const DEVICE_RELAY_RULE = "must be an absolute ws:// or wss:// URL with no userinfo, query or fragment (ws(s)://host[:port][/path]) \u2014 the address devices dial, published verbatim \u2014 or the literal \"none\" (no relay: devices dial this engine's own address)";
|
|
20
|
+
/** 判一个**非空白**的值:合格(`none`,或合格的 ws(s) URL)⇒ `[]`;否则按判序交回拒因词(`not-ws-url` / `userinfo` / `query` / `fragment`)。 */
|
|
21
|
+
export declare function deviceRelayFlaws(value: string): readonly WsUrlFlaw[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* device 车道的设备入口 `DEVICE_RELAY_URL` 的**值文法** —— 唯一判官(16.0.0)。纯函数,浏览器安全;只 import 零依赖的 URL 形判官模块。
|
|
3
|
+
*
|
|
4
|
+
* ## 规则(一条)
|
|
5
|
+
* 设备拨哪里是部署的必填声明,合法值只有两种:
|
|
6
|
+
* · 一个 ws(s) 绝对 URL —— 设备经中继 / 网关拨它。不带 userinfo / query / fragment:引擎把它**逐字**发在能力面上(设备拨号的地址),
|
|
7
|
+
* 没有放凭据的位置(设备凭签名鉴权);
|
|
8
|
+
* · 字面 {@link DEVICE_RELAY_NONE} —— 没有中继,设备按引擎自己的地址拨。只认这一种拼法(`None` / `NONE` 不是它)。
|
|
9
|
+
* 这里不判的两件,各有自己的判官:缺席 / 空白是车道表必需栏的事(写闸对每个必需坐标同一句,`isBlankCoordinate`);值里认得出凭据形的片段由写闸对
|
|
10
|
+
* 每个坐标值的那一只密钥判据拒。
|
|
11
|
+
*
|
|
12
|
+
* ## 谁读
|
|
13
|
+
* 写闸(`WorkerSpec` 的 `runtime.execution.coords.DEVICE_RELAY_URL`)、web-admin 表单与引擎起服问同一只;引擎把 `none` 读成「没有中继」。
|
|
14
|
+
*/
|
|
15
|
+
import { DIAL_ADDRESS_URL_FORBIDDEN_PARTS, wsUrlFlaws } from "./url-shape.js";
|
|
16
|
+
/** 「没有中继,设备拨引擎自己的地址」的那个字面值。 */
|
|
17
|
+
export const DEVICE_RELAY_NONE = "none";
|
|
18
|
+
/** 规则句(不含值):写闸的拒句、表单的提示读这一句。 */
|
|
19
|
+
export const DEVICE_RELAY_RULE = `must be an absolute ws:// or wss:// URL with no userinfo, query or fragment (ws(s)://host[:port][/path]) — the address devices dial, published verbatim — or the literal "${DEVICE_RELAY_NONE}" (no relay: devices dial this engine's own address)`;
|
|
20
|
+
/** 判一个**非空白**的值:合格(`none`,或合格的 ws(s) URL)⇒ `[]`;否则按判序交回拒因词(`not-ws-url` / `userinfo` / `query` / `fragment`)。 */
|
|
21
|
+
export function deviceRelayFlaws(value) {
|
|
22
|
+
return value === DEVICE_RELAY_NONE ? [] : wsUrlFlaws(value, DIAL_ADDRESS_URL_FORBIDDEN_PARTS);
|
|
23
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type EnvModelsDecision } from "./config-root-documents.js";
|
|
2
|
-
import { SUMMARIZE_MODEL_MISSING_CODE } from "./fleet.js";
|
|
3
2
|
import type { ModelRole } from "./types.js";
|
|
4
3
|
/** 一份 env 快照(`process.env` 也满足这个形)。 */
|
|
5
4
|
export type EnvSnapshot = Readonly<Record<string, string | undefined>>;
|
|
@@ -142,12 +141,6 @@ export type EnvModelTableWritePlan = {
|
|
|
142
141
|
readonly receipt: EnvModelTableReceipt;
|
|
143
142
|
readonly confirmKey: string;
|
|
144
143
|
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
144
|
};
|
|
152
145
|
/**
|
|
153
146
|
* 写者在写 `config.d/models.json` 之前调的**计划**(判序见文件头)。纯函数:不读环境、不读盘、不改入参。
|
|
@@ -155,9 +148,9 @@ export type EnvModelTableWritePlan = {
|
|
|
155
148
|
*/
|
|
156
149
|
export declare function planEnvModelTableWrite(input: EnvModelTableWriteInput): EnvModelTableWritePlan;
|
|
157
150
|
/**
|
|
158
|
-
*
|
|
151
|
+
* 计划停下来的那一种结局(`confirm`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
|
|
159
152
|
* 写类结局不是拒 ⇒ `TypeError`。
|
|
160
153
|
*/
|
|
161
154
|
export declare function envModelTableWriteRefusal(plan: Extract<EnvModelTableWritePlan, {
|
|
162
|
-
readonly kind: "confirm"
|
|
155
|
+
readonly kind: "confirm";
|
|
163
156
|
}>): string;
|
package/dist/env-model-table.js
CHANGED
|
@@ -7,21 +7,21 @@
|
|
|
7
7
|
* cheap 槽四态({@link EnvCheapSlot})。「env 模型表已声明」({@link envModelTableDeclared})与角色底({@link buildEnvModelBaseline})都由它派生 ——
|
|
8
8
|
* 这道闸的执行方(引擎一侧;今天引擎还不强制)与写者的计划助手调同一份(一次性迁移 SS-M11 只用两槽点名被顶替的 env 模型,铸不铸不看它),不各写一份。env 快照按「读到即算」(下标读;原型分层的快照里继承来的键算在场)。
|
|
9
9
|
* 引擎另有「名册占位 ⇒ env 表算空」一格,那是引擎自己的部署事实,不在本包。
|
|
10
|
-
* · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 →
|
|
11
|
-
*
|
|
10
|
+
* · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 →
|
|
11
|
+
* env 表未声明 ⇒ 照写、**不写确认** → 配置根的决定是 `replaced` ⇒ 照写 →
|
|
12
12
|
* 调用方带回的确认键等于这一次的 ⇒ `write_with_decision`(先经 `writeEnvModelsDecision` 写 `replaced`,再写目录;两次写在写者对 models.json 的同一把锁里)→
|
|
13
13
|
* 否则 `confirm`(把回执给用户看)。执法在引擎一侧(本地腿的顶替闸;今天引擎不拒这个组),本助手只是让写者在写之前知道这道闸强制起来会不会拒、会顶掉什么。
|
|
14
14
|
* · **回执按 id**:每个退场的 env 条目带 `servedAs`(候选里同 id 的启用条目名 —— 上游模型仍被服务,形可能不同);每个 env 底角色带写前绑谁
|
|
15
15
|
* (env 表键 + 它解到的 id)与采纳后落到哪个条目(`{name, id}`)。缺省落点经 `resolveCatalogDefaultModel`(与引擎的 stage 同一只解析器);
|
|
16
16
|
* 绑到停用条目或目录外的名 = `dangles`。确认键 = 回执的定序 JSON —— 后果(落点名 / id、env 槽、决定读数)一变,旧键就过期。
|
|
17
|
-
* · 可用性(引擎的 brain 腿、端点、读腿逐条丢)
|
|
17
|
+
* · 可用性(引擎的 brain 腿、端点、读腿逐条丢)计划判不了。悬空的 summarize 不是拒(16.0.0 起;此前计划把它判成「不可确认」,前提不成立,那一臂删了):引擎把点名目录外 / 停用条目的角色席
|
|
18
|
+
* 读成未设、按角色回退链落下一级并说一次 —— 没有哪种读法因它拒组;它照样写在回执里(`dangles`),要确认的写法把它给用户看。
|
|
18
19
|
*/
|
|
19
20
|
import { z } from "zod";
|
|
20
21
|
import { entryEnabledFlag, catalogManagesModelPlane } from "./catalog-manages.js";
|
|
21
22
|
import { parseDomainSalvaged, isDisclosableIdentity, resolveActiveTiers, withheldNameToken } from "./config-fns.js";
|
|
22
23
|
import { ENV_MODELS_DOCUMENT } from "./config-root-documents.js";
|
|
23
24
|
import { resolveCatalogDefaultModel } from "./default-model.js";
|
|
24
|
-
import { SUMMARIZE_MODEL_MISSING_CODE } from "./fleet.js";
|
|
25
25
|
import { landCatalogWord } from "./tier-words.js";
|
|
26
26
|
import { stableStringify } from "./hash.js";
|
|
27
27
|
import { captureJson, dataDescriptor, isolatedObject } from "./own-key-read.js";
|
|
@@ -205,8 +205,6 @@ export function planEnvModelTableWrite(input) {
|
|
|
205
205
|
return { kind: "write", why: "not_managing" };
|
|
206
206
|
const env = snapshotEnvModelTable(input.env); // read once: the receipt and the env-table rule answer from the same reading
|
|
207
207
|
const receipt = buildReceipt(buildEnvModelBaseline(env), candidateView(copy.value), decision);
|
|
208
|
-
if (receipt.roles.find((r) => r.role === "summarize")?.after.kind === "dangles")
|
|
209
|
-
return { kind: "fix_first", code: SUMMARIZE_MODEL_MISSING_CODE, receipt };
|
|
210
208
|
if (!envModelTableDeclared(env))
|
|
211
209
|
return { kind: "write", why: "env_table_empty" };
|
|
212
210
|
if (decision === "replaced")
|
|
@@ -226,7 +224,7 @@ const landingText = (after) => {
|
|
|
226
224
|
return after.kind === "dangles" ? "nothing (no enabled catalog entry)" : "an entry the engine selects ({select})";
|
|
227
225
|
};
|
|
228
226
|
/**
|
|
229
|
-
*
|
|
227
|
+
* 计划停下来的那一种结局(`confirm`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
|
|
230
228
|
* 写类结局不是拒 ⇒ `TypeError`。
|
|
231
229
|
*/
|
|
232
230
|
export function envModelTableWriteRefusal(plan) {
|
|
@@ -245,13 +243,7 @@ export function envModelTableWriteRefusal(plan) {
|
|
|
245
243
|
return (`writing this models document replaces the env model table on the local config leg: ${models}; after adoption ${roles}.${decision} ` +
|
|
246
244
|
`To let the catalog replace them, confirm — the writer records ${file} = {"envModels":"replaced"} in the config root first, then writes the models document; to keep the env models, do not write it.`);
|
|
247
245
|
}
|
|
248
|
-
case "fix_first": {
|
|
249
|
-
const row = plan.receipt.roles.find((x) => x.role === "summarize");
|
|
250
|
-
const ref = row === undefined ? "" : ` (env binding ${shown(row.before.ref)})`;
|
|
251
|
-
return (`this models document leaves the summarize role unresolved${ref}: it lands on no enabled catalog entry, so every task would be refused after adoption (${SUMMARIZE_MODEL_MISSING_CODE}). ` +
|
|
252
|
-
"Bind roles.summarize to an enabled entry of the document, or add an enabled entry under the name it points at, then plan the write again.");
|
|
253
|
-
}
|
|
254
246
|
default:
|
|
255
|
-
throw new TypeError(`envModelTableWriteRefusal: only a "confirm"
|
|
247
|
+
throw new TypeError(`envModelTableWriteRefusal: only a "confirm" plan is a refusal (got ${JSON.stringify(kind)})`);
|
|
256
248
|
}
|
|
257
249
|
}
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* (必需坐标算不算「缺」的唯一判官 `isBlankCoordinate` 不在这里:它住在零依赖的叶模块 `./execution-lanes`,包根与 `./execution-lanes` 子路径是同一只。)
|
|
6
6
|
* 索引只从表派生,不另存任何词:表改了,这里跟着变。
|
|
7
7
|
*
|
|
8
|
-
* ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts
|
|
9
|
-
* 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)
|
|
10
|
-
* 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS})
|
|
11
|
-
* ({@link
|
|
8
|
+
* ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`;16.0.0 起指路点名**哪一条**迁移)
|
|
9
|
+
* 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**;带的话点名搬它的那一条迁移。
|
|
10
|
+
* 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只函数**
|
|
11
|
+
* ({@link envKeyMigration}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
|
|
12
12
|
* (`test/migration-signpost-11.0.0.test.ts` 用真跑迁移的读数逐个保留名对拍)。
|
|
13
|
+
* 哪一条:车道表登记 7.0.0 之后才进表的车道键({@link LATE_LANE_ENV_KEYS})在 7.0.0 起的存量行里可以**合法地**写在 `env` / `secretEnv` 逃生口里
|
|
14
|
+
* (那时它还不是保留名),它的旧形归登记它那一版的迁移;其余键的旧形只可能出自 6.x 行,归 SS-M4。
|
|
13
15
|
*/
|
|
14
16
|
import { type ExecutionLane, type ExecutionLaneRow } from "./execution-lanes.js";
|
|
17
|
+
import type { OneTimeMigrationId } from "./migration-receipt.js";
|
|
15
18
|
/** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
|
|
16
19
|
* 找不到 = 本包自身的不变量坏了(不是输入错),所以抛。 */
|
|
17
20
|
export declare function laneRowOf(lane: ExecutionLane): ExecutionLaneRow;
|
|
@@ -25,11 +28,20 @@ export interface LaneEnvOwner {
|
|
|
25
28
|
export declare const LANE_ENV_OWNER: ReadonlyMap<string, LaneEnvOwner>;
|
|
26
29
|
/** 一个车道 env 名在 WorkerSpec 里的**唯一写位**(保留键的拒因指路用它)。 */
|
|
27
30
|
export declare function laneEnvSeat(name: string, owner: LaneEnvOwner): string;
|
|
28
|
-
/** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.
|
|
31
|
+
/** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lanes`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
|
|
29
32
|
export declare const LANE_WORD_KEYS: readonly ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
|
|
30
33
|
/** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`;迁移搬进 `store`)。 */
|
|
31
34
|
export declare const STORE_ENV_KEYS: readonly ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
|
|
32
35
|
/** 一个名字是不是车道块的键:车道词两键,或车道表里的任一车道 env 名。 */
|
|
33
36
|
export declare function isLaneEnvKey(name: string): boolean;
|
|
34
|
-
/**
|
|
35
|
-
export
|
|
37
|
+
/** 搬一个逃生口里的保留名的那一条一次性迁移,与它改写的存量形是哪一版之前存下的(指路句 `migrationSignpost(migration, before)` 的两个参数)。 */
|
|
38
|
+
export interface EnvKeyMigration {
|
|
39
|
+
readonly migration: OneTimeMigrationId;
|
|
40
|
+
readonly before: string;
|
|
41
|
+
}
|
|
42
|
+
/** 7.0.0 之后才登记进车道表的车道 env 名 → 改写它的迁移(只增不减;每加一个,在这里写一行)。16.0.0:`DEVICE_RELAY_URL`(device 行的必需坐标;
|
|
43
|
+
* 此前 device 臂就读它而表漏登记,7.0.0–15.x 的写闸把它当普通 env 收下)→ SS-M12。 */
|
|
44
|
+
export declare const LATE_LANE_ENV_KEYS: ReadonlyMap<string, EnvKeyMigration>;
|
|
45
|
+
/** 一个 env 名在逃生口里 ⇒ 哪一条迁移搬它(见文件头「迁移指路」):晚登记的车道键 ⇒ 登记它那一版的迁移;别的车道块键与店键 ⇒ SS-M4;其余 ⇒ `undefined`(没有迁移搬它)。
|
|
46
|
+
* 写闸拒因带不带指路、指哪一条只看它。 */
|
|
47
|
+
export declare function envKeyMigration(name: string): EnvKeyMigration | undefined;
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* (必需坐标算不算「缺」的唯一判官 `isBlankCoordinate` 不在这里:它住在零依赖的叶模块 `./execution-lanes`,包根与 `./execution-lanes` 子路径是同一只。)
|
|
6
6
|
* 索引只从表派生,不另存任何词:表改了,这里跟着变。
|
|
7
7
|
*
|
|
8
|
-
* ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts
|
|
9
|
-
* 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)
|
|
10
|
-
* 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS})
|
|
11
|
-
* ({@link
|
|
8
|
+
* ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`;16.0.0 起指路点名**哪一条**迁移)
|
|
9
|
+
* 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**;带的话点名搬它的那一条迁移。
|
|
10
|
+
* 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只函数**
|
|
11
|
+
* ({@link envKeyMigration}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
|
|
12
12
|
* (`test/migration-signpost-11.0.0.test.ts` 用真跑迁移的读数逐个保留名对拍)。
|
|
13
|
+
* 哪一条:车道表登记 7.0.0 之后才进表的车道键({@link LATE_LANE_ENV_KEYS})在 7.0.0 起的存量行里可以**合法地**写在 `env` / `secretEnv` 逃生口里
|
|
14
|
+
* (那时它还不是保留名),它的旧形归登记它那一版的迁移;其余键的旧形只可能出自 6.x 行,归 SS-M4。
|
|
13
15
|
*/
|
|
14
16
|
import { EXECUTION_LANES } from "./execution-lanes.js";
|
|
15
17
|
/** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
|
|
@@ -29,7 +31,7 @@ export const LANE_ENV_OWNER = new Map(EXECUTION_LANES.flatMap((r) => {
|
|
|
29
31
|
export function laneEnvSeat(name, owner) {
|
|
30
32
|
return `runtime.execution.${owner.secret ? "secretRefs" : "coords"}.${name}`;
|
|
31
33
|
}
|
|
32
|
-
/** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.
|
|
34
|
+
/** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lanes`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
|
|
33
35
|
export const LANE_WORD_KEYS = ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
|
|
34
36
|
/** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`;迁移搬进 `store`)。 */
|
|
35
37
|
export const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
|
|
@@ -37,7 +39,23 @@ export const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BAC
|
|
|
37
39
|
export function isLaneEnvKey(name) {
|
|
38
40
|
return LANE_WORD_KEYS.some((k) => k === name) || LANE_ENV_OWNER.has(name);
|
|
39
41
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
const SIX_X_ENV = Object.freeze({ migration: "SS-M4", before: "7.0.0" });
|
|
43
|
+
/** 7.0.0 之后才登记进车道表的车道 env 名 → 改写它的迁移(只增不减;每加一个,在这里写一行)。16.0.0:`DEVICE_RELAY_URL`(device 行的必需坐标;
|
|
44
|
+
* 此前 device 臂就读它而表漏登记,7.0.0–15.x 的写闸把它当普通 env 收下)→ SS-M12。 */
|
|
45
|
+
export const LATE_LANE_ENV_KEYS = lateLaneEnvKeys([["DEVICE_RELAY_URL", Object.freeze({ migration: "SS-M12", before: "16.0.0" })]]);
|
|
46
|
+
/** 建 {@link LATE_LANE_ENV_KEYS}:每一行的名字必须在车道表里(载入时验,名字拼错 ⇒ 包载入即抛,不静默成一条永不命中的行)。 */
|
|
47
|
+
function lateLaneEnvKeys(rows) {
|
|
48
|
+
for (const [name] of rows) {
|
|
49
|
+
if (!LANE_ENV_OWNER.has(name))
|
|
50
|
+
throw new Error(`LATE_LANE_ENV_KEYS names "${name}", which is not in the execution lane table (package invariant broken)`);
|
|
51
|
+
}
|
|
52
|
+
return new Map(rows);
|
|
53
|
+
}
|
|
54
|
+
/** 一个 env 名在逃生口里 ⇒ 哪一条迁移搬它(见文件头「迁移指路」):晚登记的车道键 ⇒ 登记它那一版的迁移;别的车道块键与店键 ⇒ SS-M4;其余 ⇒ `undefined`(没有迁移搬它)。
|
|
55
|
+
* 写闸拒因带不带指路、指哪一条只看它。 */
|
|
56
|
+
export function envKeyMigration(name) {
|
|
57
|
+
const late = LATE_LANE_ENV_KEYS.get(name);
|
|
58
|
+
if (late !== undefined)
|
|
59
|
+
return late;
|
|
60
|
+
return isLaneEnvKey(name) || STORE_ENV_KEYS.some((k) => k === name) ? SIX_X_ENV : undefined;
|
|
43
61
|
}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* · `tenantSeparation`:同文件 `executionLaneCaps` 每臂的 `tenantSeparation` 位(:209 host / :230 e2b / :249 k8s /
|
|
18
18
|
* :269 ssh / :288 adb / :308 local-docker / :327 device)。`tenantSeparationBasis`(11.0.0):分租户的那几臂凭什么分 ——
|
|
19
19
|
* e2b / k8s / local-docker 每任务一只隔离目标(`isolation`),device 是发起者自己的设备(`principal-binding`)。
|
|
20
|
+
* · `singleReplica`(16.0.0):这条车道的执行目标挂在**一个副本**上 —— host 是副本自己的文件系统,device 是设备连上的那一个副本
|
|
21
|
+
* (设备的连接落在一个副本上,跨副本把会话送到它的设备今天没有)。表里有一条这种车道的 worker 只能一个副本(写闸按这一列判,不点名车道词)。
|
|
20
22
|
* · `requiredEnv ∪ requiredSecretEnv`:这条车道缺了就起不了服的键。最初逐字抄自 `src/config.ts` 的手写表 `REQUIRED_ENVS_BY_LANE`(:1054),
|
|
21
23
|
* 那张手写表已退役 —— 服务端的必需 env 门按本表行的这两栏拒启,部署面的车道清单读同一行,所以今天**本表就是**必需键的唯一来源。按 `src/config-catalog.ts`
|
|
22
24
|
* 该键那一行的值类型分栏 —— 类型 `secret` / `opaque`(目录自己「绝不回显值」的两类)进 `*SecretEnv`,其余进 `*Env`。
|
|
@@ -66,9 +68,11 @@ export type LaneTenancy = {
|
|
|
66
68
|
/** 一条车道的表行。四栏 env 名两两不交、跨车道也不交(`test/execution-lanes-7.0.0.test.ts` 钉)。
|
|
67
69
|
* - `requiredEnv` / `optionalEnv`:非密坐标 —— 写在 `runtime.execution.coords`(字面值);
|
|
68
70
|
* - `requiredSecretEnv` / `optionalSecretEnv`:密钥 —— 写在 `runtime.execution.secretRefs`(服务变量名 → 宿主 env-NAME,绝不写值);
|
|
69
|
-
* - 租户分离位 + 依据:见 {@link LaneTenancy}。
|
|
71
|
+
* - 租户分离位 + 依据:见 {@link LaneTenancy}。
|
|
72
|
+
* - `singleReplica`(16.0.0):执行目标挂在一个副本上 ⇒ 声明了这条车道的 worker 只能一个副本(见文件头)。 */
|
|
70
73
|
export type ExecutionLaneRow = {
|
|
71
74
|
readonly lane: ExecutionLane;
|
|
75
|
+
readonly singleReplica: boolean;
|
|
72
76
|
readonly requiredEnv: readonly string[];
|
|
73
77
|
readonly requiredSecretEnv: readonly string[];
|
|
74
78
|
readonly optionalEnv: readonly string[];
|
|
@@ -79,7 +83,7 @@ export declare const EXECUTION_LANES: readonly ExecutionLaneRow[];
|
|
|
79
83
|
/**
|
|
80
84
|
* 车道坐标的**唯一判官**:一个必需坐标「未设 / 空串 / 只含空白」一律算缺。本函数判其中的后两种,且只看**已经有的字符串值**:空串,或只含空白(`String.prototype.trim` 认的空白:空格 / 制表 / 换行 / 不换行空格 / 字节序标记 …)⇒ `true`;
|
|
81
85
|
* 「这一键根本没设」由调用方先判(写闸对没设的键另有一句 `requires coords.<键>`,两种缺法同样被拒)。
|
|
82
|
-
*
|
|
86
|
+
* 写闸对所有车道、所有必需坐标同判这一只;本地文件契约 `RemoteExecSpec`(`config.d/remote-exec.json`)的每个非空串字段(镜像 / 命名空间 / 主机 / 路径 / …)问的也是这一只(16.0.0 起:同一个部署声明在两处说,「空白」不再有两种读法);它只判「是不是空白」,不 trim 值、不管值的格式(格式不在这一条的范围:`" sandboxes "` 不是空白)。
|
|
83
87
|
* 它只答「这个值是不是空白」,不管这个键是必需还是可选:据它**拒不拒**一个坐标,由调用方按键的必需 / 可选决定 —— 写闸只对必需坐标据它拒;个别可选坐标上空串有语义(如 `K8S_RUNTIME_CLASS` 的空串 = 不用 Kata),可选坐标的空串照收。
|
|
84
88
|
*
|
|
85
89
|
* 住在这个零依赖的叶模块里:包根(`export *`)与 `./execution-lanes` 子路径是**同一个函数对象** —— 只想问这一个问题的消费方走子路径,不必冷载入整只契约包桶;
|
|
@@ -93,6 +97,7 @@ export type StoreBackend = (typeof STORE_BACKENDS)[number];
|
|
|
93
97
|
* (`tidb` 不在集内:引擎见之拒启)。 */
|
|
94
98
|
export declare const SESSION_BACKENDS: readonly ["memory", "mysql", "auto"];
|
|
95
99
|
export type SessionBackend = (typeof SESSION_BACKENDS)[number];
|
|
96
|
-
/** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)
|
|
97
|
-
|
|
100
|
+
/** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。16.0.0:MySQL 族那一只叫 `mysql`(此前 `tidb` —— 同一只 SQL 店有店词 `mysql` 与方言 `tidb` 两个字;
|
|
101
|
+
* 收成一个字,与 {@link STORE_BACKENDS} 同字;硬破坏,`tidb` 读写两面拒、指路一次性迁移 SS-M12)。次序 file → pg → mysql。 */
|
|
102
|
+
export declare const MEMORY_ENGINE_BACKENDS: readonly ["file", "pg", "mysql"];
|
|
98
103
|
export type MemoryEngineBackend = (typeof MEMORY_ENGINE_BACKENDS)[number];
|