@sema-agent/settings-schema 17.1.0 → 17.2.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 +46 -0
- package/README.md +32 -0
- package/conformance/VERSION +1 -1
- package/dist/atomic-write.d.ts +4 -3
- package/dist/atomic-write.js +25 -11
- package/dist/catalog-manages.d.ts +8 -3
- package/dist/catalog-manages.js +16 -7
- package/dist/config-d-file.d.ts +4 -2
- package/dist/config-d-file.js +27 -6
- package/dist/config-root-documents.d.ts +2 -2
- package/dist/config-root-documents.js +1 -1
- package/dist/config-root-node.d.ts +8 -1
- package/dist/config-root-node.js +105 -23
- package/dist/env-model-table.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.d.ts +4 -1
- package/dist/node.js +6 -2
- package/dist/settings-units.d.ts +3 -0
- package/dist/settings-units.js +18 -0
- package/docs/API-SURFACE.md +11 -3
- package/docs/CC-DIVERGENCE.md +6 -0
- package/docs/CONSUMER-INTEGRATION.md +72 -0
- package/llms-full.txt +5 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,52 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 17.2.0 — 2026-11-02 — minor:`./node` 出三只同步读写口(`config.d` 文档读数 · 决定文档读 / 写)· 根桶出「显式停用」谓词 `isExplicitlyDisabledEntry` · settings 单元读不进的处置律 · 关停位值域的实现差登记
|
|
12
|
+
|
|
13
|
+
> 档位(按发版纪律):**minor** —— 新导出(`./node` 三个函数加四个类型,根桶一个函数)是加性的,既有导出的答案逐格不变;零别名、零双读。三只同步口与各自的异步口读 / 写同一份判据:读数、拒句、落盘字节、文件模式逐项相同(对拍钉)。
|
|
14
|
+
> 谁需要:在**同步具名锁**里读写配置根的壳(CLI):读 `config.d/<名>.json` 要和包同一套的悬空符号链接 / 空白 / 过深判,写模型池要在锁里先写决定、再写目录;包里此前只有异步口,壳只好自读文本(第四只缺席读法)。
|
|
15
|
+
> 谁受伤:无 —— 加性。壳把自读文本换成同步读口时有三处**可见差**(= 它与包的分叉清零):纯空白 / 零字节的文档由「缺席」变 `empty` 拒;悬空符号链接变 `io` 拒;嵌得过深变 `too_deep` 拒。补偿:拒句点名路径与出路(删掉它回到缺省,或写一个合法对象)。
|
|
16
|
+
> 导出面:`./node` +3 函数 +4 类型,根桶 +1 函数(登记表 `docs/API-SURFACE.md` 已重生)。`entryEnabledFlag`(三态读数)收回成模块私有 —— 它从未在任何子路径上可达,所以不是导出面变化。
|
|
17
|
+
|
|
18
|
+
- **`readConfigDFileReadingSync(root, name)`(`./node`)与类型 `ConfigDReading` / `ConfigDRefused` / `ConfigDFileReason` / `ConfigDDocumentName`**。读一个 `config.d/<名>.json` → 读数(`absent` / `present{value}` / `refused{reason, error, thrown, fingerprint}`),与包内异步读口**共用同一只纯判** `judgeConfigDTextReading` 与**同一个缺席尾**:
|
|
19
|
+
缺席尾抽成一只 `absentUnlessDangling`(`ENOENT` ⇒ 对文档与 `config.d` 目录各问一次链接判法 ⇒ 悬空抛那一句 ⇒ 否则缺席),`readTextIfPresent` 与新的同步孪生 `readTextIfPresentSync` 各留一行读文本的 I/O;`config-d-file` 里读口的函数体只剩「摆路径、读一次文本、交给同一只 `readingOfText` / `refusedIo`」。
|
|
20
|
+
**不**在根桶出裸纯判(壳自读文本 = 第四只缺席读法;`config-d-file` 依赖 `node:path` 与原子写器,不是浏览器安全叶),异步读口也仍包内私有 —— 本版只出壳要的同步口。名字不是 `config.d` 里能放的文档名 ⇒ 同步抛(与异步口的拒绝同一个错)。
|
|
21
|
+
钉:`test/config-d-sync-reading-17.2.0.test.ts`(同步 / 异步逐格对拍〔缺席 / 在场各形 / 空与纯空白 / 坏 JSON / 非对象五形 / 过深 / 目录 / 悬空链接 / 成环 / 权限〕+ 每格绝对答案 + 指纹绝对值 · 结构:判官 `JSON.parse` 恰一处、缺席臂恰一处、两口函数体只留一行 I/O · 出口:根桶不出)。
|
|
22
|
+
- **`readEnvModelsDecisionSync(root)` / `writeEnvModelsDecisionSync(root, word, { write })`(`./node`)**。配置根上 env 模型表决定文档的同步读 / 写:同一个字节读法(`readEnvModelsDecisionBytes`)、同一个缺席律(只有 `ENOENT` 算缺席,别的读不出带 errno)、同一份答案表;
|
|
23
|
+
写口与异步口共用**同一份写计划**(决定词与写法的闭集校验 · 文本铸造 · 失败包成本包的文件系统失败句)和**同一对同步原子写原语**(异步座 `nodeConfigRootIo` 的两只写本来就是它们的异步包装,现在与同步写口调同一对,模式类表只经这一对取:新建 0o644 经进程 umask,已有的沿用原模式)。
|
|
24
|
+
读写真文件系统,**不收注入座**(`ConfigRootIo` 不动,异步形不变)。读数从不抛;写失败抛文件系统失败句,词 / 写法不在闭集抛 `TypeError`、一字不写。写序契约不变:先写决定 `replaced`、再写 `config.d/models.json`,两次写在写者对 models.json 的同一把锁里。
|
|
25
|
+
钉:`test/env-models-decision-sync-17.2.0.test.ts`(读口逐格对拍;写口〔文件不在 / 已有的各形 / 目录 / 悬空链接 / 根不存在 / 根是文件 / 根不可写〕× 两个词 × 两种写法的两棵同构树对拍:返回值 / 抛的错的类与整句 / 落盘字节 / 模式 / 残片 / 事后读数;结构:缺席律、字节读法、写计划、原子写原语各一处;`ConfigRootIo` 不动)。
|
|
26
|
+
- **根桶 `isExplicitlyDisabledEntry(entry)`**。一条目录条目**原文**是不是「显式停用」(纯 JSON 对象、自有数据属性 `enabled` 恰为 `false`):「恰为 `false`」这条律此前在包内两处、壳一处(`enabledEntryCount({ models: [entry] }) === 0`)各复述一遍;现在谓词是唯一一处写它,
|
|
27
|
+
`catalogManagesModelPlane` / `enabledEntryCount` 与 `planEnvModelTableWrite` 的启用过滤都读它(`env-model-table` 的 `entryEnabledFlag(m) !== false` 改读 `!isExplicitlyDisabledEntry(m)`)。**三态读数不出口**:出三态就是把这条律交给每个消费者各写一遍 `=== false`。
|
|
28
|
+
在 JSON 数据上与壳今天的写法逐格同答。非纯 JSON 对象的值、`enabled` 缺席 / 只靠继承 / 是访问器(不调)/ 不是布尔 ⇒ `false`;读 `enabled` 的反射本身抛(Proxy 的描述符陷阱)⇒ 照抛,不吞成 `false`(`ownEnabledFlag` 的既有臂);要整份文档的口径(读不出 ⇒ `TypeError`)问 `enabledEntryCount` / `catalogManagesModelPlane`。
|
|
29
|
+
钉:`test/explicitly-disabled-entry-17.2.0.test.ts`(真值表 · 与壳旧写法逐格同答 · 自有数据属性 / 访问器不调 / 原型污染 / 非朴素 · 三态不出口 · 「`=== false`」在 `src/` 恰一处 · 两处消费者读谓词)。
|
|
30
|
+
- **settings 单元读不进的处置律(文字律 + 全员表驱动钉;零代码、零新导出)**。`member` / `members` 单元里 `readSettingsUnit` 答 `unreadable` 的成员**不是缺席**(缺席 = 文档没设,读不进 = 设了但设成契约放不进的形)。类目 `fail` 为 `closed` 的成员:
|
|
31
|
+
方向 `loosen` ⇒ 调用方可丢(丢了授得更少),但要响;方向 `tighten` ⇒ 必须持住、在它自己的面上给最严的答案(规则面拒一切放不进的调用、关停位当开、沙箱开关取严极),不得丢、不得读成缺席 —— 丢掉它 = 把一条读不懂的限制当成没有限制,只有最严的答案不会比作者的本意松。
|
|
32
|
+
律的两个输入**已经是表**:方向 = `memberAdmission(category, member).direction`,失败方向 = `SAFETY_MERGE_SPEC[category].fail`;**读数上不加 `direction` 字段**(`whole` 单元没有方向来源,字段只能可选、每个消费者仍要对缺席分支;且与成员行是同一行的第二个读者)。文字在 `src/settings-units.ts` 头注、`readSettingsUnit` 文档与 `docs/CONSUMER-INTEGRATION.md` §15。
|
|
33
|
+
钉:`test/member-unreadable-law-17.2.0.test.ts`(枚举每个 `member` / `members` 行的每个成员:类目 `fail` 是 `closed`、成员行有合法 `direction` 且 `memberAdmission` 同读、界内值读得进、一组界外探针恒 `unreadable` 不抛;合成的坏表上当场见红 · 读数键恒为 `ok` + `value` | `reason` · 文字在三处)。
|
|
34
|
+
**未闭环(如实)**:`whole` 单元(`permissionMode` / `additionalDirectories` / `hooks` / `env` / `model` / `outputStyle`)没有方向列,「读不进即缺席」对它们是否闭合取决于各自值域的次序 —— 另一个设计题,本版没有答案;真正的 fail-closed 执法钉在读它的壳 / 服务端,本包不执法。
|
|
35
|
+
- **关停位值域的实现差登记(`docs/CC-DIVERGENCE.md`;零代码、零新导出)**。`disableBypassPermissionsMode` / `disableAutoMode` 的读法 `readSettingsUnit` 读**布尔超集** —— `"disable"`(CC 的「开」的拼写)≡ `true`,`false` 是一个值(比较器的最松极、折叠的单位元,不改变有效值),别的 `unreadable`;
|
|
36
|
+
CC 的文件面只认字符串 `"disable"`。这是实现差、不是行为偏离(合并语义无偏差)。把值写回 CC 拼写面是壳 / 服务端的渲染(开 → `"disable"`,关 → 缺席),**本包不出反向拼写函数**(把 CC 的拼写表再搬一份进来就是第二个陈述)。钉:`test/kill-switch-spelling-17.2.0.test.ts`。
|
|
37
|
+
- **订正四处过期注释**(随包源注释,无行为变化):`config-root-documents` 的两处与 `config-root-node` 的一处、`env-model-table` 头注里的「今天引擎还不读它 / 还不强制」—— 服务端的本地配置腿读这份决定并强制这道闸。
|
|
38
|
+
|
|
39
|
+
**自 v17.1.0 起运行期代码有改动的公开导出(机器重算;上界 —— 可达 ≠ 一定变,不可达 ⇒ 一定不变;本版新增的导出不入表)**
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
FileConfigStore [./node]
|
|
43
|
+
catalogManagesModelPlane [.]
|
|
44
|
+
enabledEntryCount [.]
|
|
45
|
+
loadEnvFile [./node]
|
|
46
|
+
loadLocalConfig [./node]
|
|
47
|
+
loadRemoteExec [./node]
|
|
48
|
+
migrateEnvModelsDecision [./node]
|
|
49
|
+
nodeConfigRootIo [./node]
|
|
50
|
+
planEnvModelTableWrite [.]
|
|
51
|
+
readBundle [./node]
|
|
52
|
+
readEnvModelsDecision [./node]
|
|
53
|
+
readMigrationMarker [./node]
|
|
54
|
+
writeEnvModelsDecision [./node]
|
|
55
|
+
```
|
|
56
|
+
|
|
11
57
|
## 17.1.0 — 2026-11-02 — minor:车道表 device 行的可选栏登记 `DEVICE_TRUSTED_PROXY_CIDRS`(引擎读它作设备 WS 未鉴权握手 per-remote 帽键的可信代理名单)
|
|
12
58
|
|
|
13
59
|
> 档位(按发版纪律):**minor** —— 车道表一行加一个 env 名(加性):写闸自此受理 `runtime.execution.coords.DEVICE_TRUSTED_PROXY_CIDRS`(此前判「不是任何车道的坐标」)。导出面零变化(没有导出增删,签名不变)。
|
package/README.md
CHANGED
|
@@ -47,6 +47,38 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
|
|
|
47
47
|
> zod 3, nested inside every zod-4 host). The move to zod 4 did not change what any schema accepts or produces —
|
|
48
48
|
> the conformance corpus reads byte-identically on both — only the wording of validation errors (see CHANGELOG 11.0.0).
|
|
49
49
|
|
|
50
|
+
## 升级指引:17.1.x → 17.2.0(加性,零动作可升)
|
|
51
|
+
|
|
52
|
+
`./node` 多三只**同步**读写口,根桶多一只谓词;既有导出的答案逐格不变。在同步具名锁里读写配置根的壳(CLI)用得上 —— 以前包里只有异步口,壳只好自读文本(一只多出来的缺席读法:没有悬空符号链接判、空白读成缺席、过深不判)。
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { readConfigDFileReadingSync, readEnvModelsDecisionSync, writeEnvModelsDecisionSync } from "@sema-agent/settings-schema/node";
|
|
56
|
+
import { catalogManagesModelPlane, isExplicitlyDisabledEntry } from "@sema-agent/settings-schema";
|
|
57
|
+
|
|
58
|
+
// 1. 在你的同步锁里读 config.d/models.json —— 三态读数,拒句点名路径与出路
|
|
59
|
+
const reading = readConfigDFileReadingSync(configRoot, "models");
|
|
60
|
+
switch (reading.status) {
|
|
61
|
+
case "absent": /* 路径上没有任何条目 */ break;
|
|
62
|
+
case "present": /* reading.value:一个 JSON 对象 */ break;
|
|
63
|
+
case "refused": /* reading.reason ∈ empty | invalid_json | not_object | too_deep | io;reading.error 是整句 */ break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. 整份文档管不管模型面 —— 用文档级判官(读不出抛 TypeError),不要自己数条目
|
|
67
|
+
const manages = reading.status === "present" && catalogManagesModelPlane(reading.value);
|
|
68
|
+
// 逐条问「这一条停用了吗」(例如给界面标灰)—— 不要自己写 `entry.enabled === false`;`models` 不一定是数组,先判
|
|
69
|
+
const models: unknown[] = reading.status === "present" && Array.isArray(reading.value.models) ? reading.value.models : [];
|
|
70
|
+
const disabledEntries = models.filter((m) => isExplicitlyDisabledEntry(m));
|
|
71
|
+
|
|
72
|
+
// 3. 写模型池的序(同一把锁里):先写决定 replaced,再写 models.json
|
|
73
|
+
const decision = readEnvModelsDecisionSync(configRoot); // absent | replaced | kept | unreadable;读数从不抛
|
|
74
|
+
void [manages, disabledEntries];
|
|
75
|
+
if (decision.kind !== "replaced") writeEnvModelsDecisionSync(configRoot, "replaced", { write: "overwrite" }); // 写失败抛文件系统失败句
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
把自读文本换成同步读口时的**可见差**(= 与包的分叉清零):纯空白 / 零字节的文档由「缺席」变 `empty` 拒;悬空符号链接变 `io` 拒;嵌得过深变 `too_deep` 拒。
|
|
79
|
+
同步口与异步口读 / 写同一份判据(同一只纯判、同一个缺席尾、同一份写计划、同一对原子写原语),读数 / 拒句 / 落盘字节 / 文件模式逐项相同;同步口读写真文件系统,不收注入座(`ConfigRootIo` 不动)。
|
|
80
|
+
settings 单元读不进的成员**不是缺席**(`loosen` 可丢且要响,`tighten` 必须持住并给最严答案)的处置律在 `docs/CONSUMER-INTEGRATION.md` §15,同步读写口的契约在 §16;关停位值域的实现差登在 `docs/CC-DIVERGENCE.md`。导出面:`./node` +3 函数 +4 类型,根桶 +1 函数。逐条见 CHANGELOG 17.2.0。
|
|
81
|
+
|
|
50
82
|
## 升级指引:17.0.x → 17.1.0(加性,零动作可升)
|
|
51
83
|
|
|
52
84
|
车道表 `EXECUTION_LANES` 的 device 行在可选栏多一个非密坐标 `DEVICE_TRUSTED_PROXY_CIDRS`(在 `DEVICE_SUPERSEDE` 之前):引擎读它作设备 WS 未鉴权握手 per-remote 帽键的可信代理名单(CIDR 逗号分隔,缺省空 = 不读任何代理头)。
|
package/conformance/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
17.
|
|
1
|
+
17.2.0
|
package/dist/atomic-write.d.ts
CHANGED
|
@@ -16,10 +16,11 @@ export declare function danglingSymlinkSentence(link: string, pointsAt: string):
|
|
|
16
16
|
/** 读写的真目标 —— **一只判法**(写器与目录迁移的读口共用):`target` 是符号链接 ⇒ 解析到它指向的真文件(悬空 ⇒ 抛 {@link danglingSymlinkSentence});
|
|
17
17
|
* 不是链接或不存在 ⇒ 原样(不存在的由调用方的读 / 写照常报)。 */
|
|
18
18
|
export declare function resolveSymlinkTarget(target: string): string;
|
|
19
|
-
/** 读一份**可以缺席**的 UTF-8 文本 —— `config.d` 文档与 `.env`
|
|
20
|
-
*
|
|
21
|
-
* 别的读不出(`EACCES` / `EISDIR` / `ELOOP` …)原样抛。配置根自己是悬空链接(祖先座)不在问的范围内:`dirSeats` 只列文件所在的那一级布局座。 */
|
|
19
|
+
/** 读一份**可以缺席**的 UTF-8 文本 —— `config.d` 文档与 `.env` 的缺席律在 {@link absentUnlessDangling}:读得到 ⇒ 文本;读不出 ⇒ 缺席尾(`ENOENT` 且无座位是悬空链接 ⇒ `undefined`;悬空 ⇒ 抛那一句;别的读不出原样抛)。
|
|
20
|
+
* 配置根自己是悬空链接(祖先座)不在问的范围内:`dirSeats` 只列文件所在的那一级布局座。 */
|
|
22
21
|
export declare function readTextIfPresent(file: string, dirSeats?: readonly string[]): Promise<string | undefined>;
|
|
22
|
+
/** {@link readTextIfPresent} 的同步孪生(壳在同步具名锁里读 `config.d` 文档要它):同一个缺席尾,只有读文本那一行 I/O 不同。 */
|
|
23
|
+
export declare function readTextIfPresentSync(file: string, dirSeats?: readonly string[]): string | undefined;
|
|
23
24
|
/** 原子写 `text`(UTF-8)到 `target`(符号链接则写到它指向的真文件,见文件头);模式按 `kind` 在 {@link ATOMIC_FILE_CLASSES} 里的行。父目录须已存在。 */
|
|
24
25
|
export declare function writeFileAtomic(target: string, text: string, kind: AtomicFileClass): void;
|
|
25
26
|
/**
|
package/dist/atomic-write.js
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* 「链接 ⇒ 真文件、悬空 ⇒ 响亮抛」这一只判法是 {@link resolveSymlinkTarget}:目录迁移的读口(`./config-dir-io`)读目录 / 读文件前也问它,
|
|
21
21
|
* 悬空链接在读阶段与写阶段是**同一句**({@link danglingSymlinkSentence})。
|
|
22
22
|
*
|
|
23
|
-
* ## `config.d` 文档与 `.env` 的缺席只有一种读法({@link
|
|
23
|
+
* ## `config.d` 文档与 `.env` 的缺席只有一种读法({@link absentUnlessDangling})
|
|
24
24
|
* **缺席 = 路径上没有任何条目。** `readFile` 跟着符号链接走,链接在而目标不在时内核同样答 `ENOENT`:那是在场却读不出,不是缺席。读一份可以缺席的文本(`config.d` 的文档、配置根下的 `.env`)
|
|
25
|
-
*
|
|
25
|
+
* 因此都过同一只缺席尾({@link absentUnlessDangling};异步读者 {@link readTextIfPresent} 与同步读者 {@link readTextIfPresentSync} 各留一行读文本的 I/O) —— 读不出且是 `ENOENT` ⇒ 在**缺席臂里**问一次链接判法再答「缺席」,悬空 ⇒ 抛那一句(同一句,不带 errno 码)。判在缺席臂而不在读前:读得到的路径零变化(不多一次系统调用);读之后目标恰好出现仍答缺席 —— 读的那一刻它确实不在,与旧读法同一边界,不是放宽。
|
|
26
26
|
*
|
|
27
27
|
* ## 不存在才建({@link createFileExclusive})
|
|
28
28
|
* 另一种写法只有一处不同:最后一步不是 `rename` 盖过目标,而是把(已 fsync 的)临时文件 `link` 成目标名 —— 目标名已在(任何东西:文件、目录、哪怕是悬空的符号链接)⇒
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
import { randomBytes } from "node:crypto";
|
|
45
45
|
import { readFile } from "node:fs/promises";
|
|
46
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
46
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
47
47
|
import path from "node:path";
|
|
48
48
|
import { FsFailure } from "./fs-failure.js";
|
|
49
49
|
/** 按文档类的模式表(闭集)。 */
|
|
@@ -80,20 +80,34 @@ export function resolveSymlinkTarget(target) {
|
|
|
80
80
|
throw e;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
83
|
+
/** 读一份**可以缺席**的文本之后,「读不出」怎么答的**缺席尾**(异步 {@link readTextIfPresent} 与同步 {@link readTextIfPresentSync} 共用,缺席律只此一处):读失败 `e` 不是 `ENOENT` ⇒ 原样抛(`EACCES` / `EISDIR` / `ELOOP` …);
|
|
84
|
+
* 是 `ENOENT` ⇒ 先在缺席臂里对 `file` 自己与 `dirSeats`(这份文件所在、包把它当布局座的目录,如 `config.d`)各问一次 {@link resolveSymlinkTarget}(与目录迁移的读口同一只判官):任一座是悬空链接 ⇒ 抛
|
|
85
|
+
* {@link danglingSymlinkSentence} 那一句(在场却读不出,不是缺席);都不是 ⇒ `undefined`(路径上真的没有条目)。 */
|
|
86
|
+
function absentUnlessDangling(e, file, dirSeats) {
|
|
87
|
+
if (e.code !== "ENOENT")
|
|
88
|
+
throw e;
|
|
89
|
+
for (const seat of [file, ...dirSeats])
|
|
90
|
+
resolveSymlinkTarget(seat);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/** 读一份**可以缺席**的 UTF-8 文本 —— `config.d` 文档与 `.env` 的缺席律在 {@link absentUnlessDangling}:读得到 ⇒ 文本;读不出 ⇒ 缺席尾(`ENOENT` 且无座位是悬空链接 ⇒ `undefined`;悬空 ⇒ 抛那一句;别的读不出原样抛)。
|
|
94
|
+
* 配置根自己是悬空链接(祖先座)不在问的范围内:`dirSeats` 只列文件所在的那一级布局座。 */
|
|
86
95
|
export async function readTextIfPresent(file, dirSeats = []) {
|
|
87
96
|
try {
|
|
88
97
|
return await readFile(file, "utf8");
|
|
89
98
|
}
|
|
90
99
|
catch (e) {
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
return absentUnlessDangling(e, file, dirSeats);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** {@link readTextIfPresent} 的同步孪生(壳在同步具名锁里读 `config.d` 文档要它):同一个缺席尾,只有读文本那一行 I/O 不同。 */
|
|
104
|
+
export function readTextIfPresentSync(file, dirSeats = []) {
|
|
105
|
+
try {
|
|
106
|
+
return readFileSync(file, "utf8");
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
return absentUnlessDangling(e, file, dirSeats);
|
|
93
110
|
}
|
|
94
|
-
for (const seat of [file, ...dirSeats])
|
|
95
|
-
resolveSymlinkTarget(seat);
|
|
96
|
-
return undefined;
|
|
97
111
|
}
|
|
98
112
|
/** 原子写 `text`(UTF-8)到 `target`(符号链接则写到它指向的真文件,见文件头);模式按 `kind` 在 {@link ATOMIC_FILE_CLASSES} 里的行。父目录须已存在。 */
|
|
99
113
|
export function writeFileAtomic(target, text, kind) {
|
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
* 包内私有(不进包根):这是「`enabled` 怎么读」的**唯一实现**;条目的形由调用方判一次(被丢条目身份在读 `name` / `id` 之前已判过,这里不再问第二次)。
|
|
5
5
|
*/
|
|
6
6
|
export declare function ownEnabledFlag(entry: object): boolean | undefined;
|
|
7
|
-
/** 一条目录条目**原文**的 `enabled`:先判形(不是纯 JSON 对象 ⇒ `undefined`),再读 {@link ownEnabledFlag}。「显式停用」与计划助手的启用过滤都调它。 */
|
|
8
|
-
export declare function entryEnabledFlag(entry: unknown): boolean | undefined;
|
|
9
7
|
/**
|
|
10
|
-
*
|
|
8
|
+
* 一条目录条目**原文**是不是「显式停用」—— **根桶出口**,「显式停用」这条律(纯 JSON 对象、自有数据属性 `enabled` 恰为 `false`)全包只在这里写一次。
|
|
9
|
+
* 入参是解析出来的 JSON(models 文档里的一项);**不是**纯 JSON 对象的值(串 / 数 / `null` / 数组 / 类实例 / `Map` / 函数 / 撤销的 Proxy)、`enabled` 缺席、只靠继承、是访问器(不调)、不是布尔 ⇒ `false`
|
|
10
|
+
* (不是显式停用 ⇒ 在管文档里计入,与判官同答)。读 `enabled` 的反射本身抛(Proxy 的描述符陷阱,JSON 解析不出这种值)⇒ 照抛,不吞成 `false`({@link ownEnabledFlag} 的既有臂);要整份文档的口径(读不出 ⇒ `TypeError`)
|
|
11
|
+
* 问 {@link enabledEntryCount} / {@link catalogManagesModelPlane}。纯、浏览器安全。
|
|
12
|
+
*/
|
|
13
|
+
export declare function isExplicitlyDisabledEntry(entry: unknown): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* 文档里**不是显式停用**({@link isExplicitlyDisabledEntry})的条目数(日志计数用;判官 {@link catalogManagesModelPlane} 就是它 > 0)。入参是 models 文档原文或读过的 `ModelsConfig`(两者同形)。
|
|
11
16
|
* 读不出 ⇒ `TypeError`(见文件头)。
|
|
12
17
|
*/
|
|
13
18
|
export declare function enabledEntryCount(doc: unknown): number;
|
package/dist/catalog-manages.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ## 律(一条)
|
|
5
5
|
* **文档的 `models` 里至少一条不是「显式停用」的条目 ⇔ 管。** 「显式停用」= 纯 JSON 对象、自有数据属性 `enabled` 恰为 `false`
|
|
6
|
-
* ({@link ownEnabledFlag} 是这一读法的**唯一**实现:被丢条目身份的 `enabled`(`./config-fns` 的 `readEntryIdentity`)读的是同一只;{@link entryEnabledFlag}
|
|
6
|
+
* ({@link ownEnabledFlag} 是这一读法的**唯一**实现:被丢条目身份的 `enabled`(`./config-fns` 的 `readEntryIdentity`)读的是同一只;{@link entryEnabledFlag} 是它前面判一次形的入口,**模块私有**;
|
|
7
|
+
* 「恰为 `false`」这一句只在 {@link isExplicitlyDisabledEntry} 写一次,它是出口 —— 判官、计数、`./env-model-table` 的启用过滤与每个要问「这一条停用了吗」的消费者都读它,不各写一份 `=== false`)。
|
|
7
8
|
* 判官 = 计数 > 0:「管不管」与「几条在管」是同一个事实的两种问法,只有一种读法。
|
|
8
9
|
*
|
|
9
10
|
* · 文档不是纯对象、`models` 缺席或不是数组 ⇒ 0 条(不管);数组里的洞不是条目;
|
|
@@ -25,14 +26,22 @@ export function ownEnabledFlag(entry) {
|
|
|
25
26
|
const value = d !== undefined && !isAccessorDescriptor(d) ? d.value : undefined; // 访问器不调(描述符的字段只认自有的,见内核)
|
|
26
27
|
return typeof value === "boolean" ? value : undefined;
|
|
27
28
|
}
|
|
28
|
-
/** 一条目录条目**原文**的 `enabled`:先判形(不是纯 JSON 对象 ⇒ `undefined`),再读 {@link ownEnabledFlag}
|
|
29
|
-
|
|
29
|
+
/** 一条目录条目**原文**的 `enabled`:先判形(不是纯 JSON 对象 ⇒ `undefined`),再读 {@link ownEnabledFlag}。**模块私有** —— 三态读数不出门:出三态就是把「显式停用 ⇔ flag 恰为 false」这条律交给每个消费者各复述一遍,
|
|
30
|
+
* 消费者问的是 {@link isExplicitlyDisabledEntry}。 */
|
|
31
|
+
function entryEnabledFlag(entry) {
|
|
30
32
|
return isPlainJsonObject(entry) ? ownEnabledFlag(entry) : undefined;
|
|
31
33
|
}
|
|
32
|
-
/** 「显式停用」:{@link entryEnabledFlag} 恰为 `false`。 */
|
|
33
|
-
const isExplicitlyDisabled = (entry) => entryEnabledFlag(entry) === false;
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* 一条目录条目**原文**是不是「显式停用」—— **根桶出口**,「显式停用」这条律(纯 JSON 对象、自有数据属性 `enabled` 恰为 `false`)全包只在这里写一次。
|
|
36
|
+
* 入参是解析出来的 JSON(models 文档里的一项);**不是**纯 JSON 对象的值(串 / 数 / `null` / 数组 / 类实例 / `Map` / 函数 / 撤销的 Proxy)、`enabled` 缺席、只靠继承、是访问器(不调)、不是布尔 ⇒ `false`
|
|
37
|
+
* (不是显式停用 ⇒ 在管文档里计入,与判官同答)。读 `enabled` 的反射本身抛(Proxy 的描述符陷阱,JSON 解析不出这种值)⇒ 照抛,不吞成 `false`({@link ownEnabledFlag} 的既有臂);要整份文档的口径(读不出 ⇒ `TypeError`)
|
|
38
|
+
* 问 {@link enabledEntryCount} / {@link catalogManagesModelPlane}。纯、浏览器安全。
|
|
39
|
+
*/
|
|
40
|
+
export function isExplicitlyDisabledEntry(entry) {
|
|
41
|
+
return entryEnabledFlag(entry) === false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 文档里**不是显式停用**({@link isExplicitlyDisabledEntry})的条目数(日志计数用;判官 {@link catalogManagesModelPlane} 就是它 > 0)。入参是 models 文档原文或读过的 `ModelsConfig`(两者同形)。
|
|
36
45
|
* 读不出 ⇒ `TypeError`(见文件头)。
|
|
37
46
|
*/
|
|
38
47
|
export function enabledEntryCount(doc) {
|
|
@@ -49,7 +58,7 @@ export function enabledEntryCount(doc) {
|
|
|
49
58
|
for (let i = 0; i < models.length; i++) {
|
|
50
59
|
if (!Object.hasOwn(models, i))
|
|
51
60
|
continue; // 洞不是条目:只认自有下标(`forEach` / `for…of` 会读到被污染的 `Array.prototype[i]`)
|
|
52
|
-
if (!
|
|
61
|
+
if (!isExplicitlyDisabledEntry(models[i]))
|
|
53
62
|
count++;
|
|
54
63
|
}
|
|
55
64
|
return count;
|
package/dist/config-d-file.d.ts
CHANGED
|
@@ -41,9 +41,11 @@ export declare function judgeConfigDText(name: ConfigDDocumentName, text: string
|
|
|
41
41
|
* 非朴素对象、嵌得过深 ⇒ 抛。{@link judgeConfigDText} 的最后一道判就是这只(文本解析出来之后过它),所以「不是对象」「过深」各一种坏形一句话,不论文档是从文件文本来的还是调用方直接给的;
|
|
42
42
|
* `label` = 句首的称呼(文本路径给文件的真实路径,值路径给 `the <名> document`)。 */
|
|
43
43
|
export declare function judgeConfigDValue(name: ConfigDDocumentName, value: unknown, label: string): Record<string, unknown>;
|
|
44
|
-
/** 读一个 `config.d/<名>.json` → 读数:路径上没有任何条目 ⇒ 缺席;在场 ⇒ {@link judgeConfigDTextReading};在场却读不出 ⇒ 被拒,词 `io`
|
|
45
|
-
* (文档本身或 `config.d` 目录:内核跟链接读到 ENOENT,但条目在场;句子是 `danglingSymlinkSentence`(`./atomic-write`)那一句,与目录迁移的读口同一句)。缺席与悬空的分辨在 `readTextIfPresent`。 */
|
|
44
|
+
/** 读一个 `config.d/<名>.json` → 读数:路径上没有任何条目 ⇒ 缺席;在场 ⇒ {@link judgeConfigDTextReading};在场却读不出 ⇒ 被拒,词 `io`({@link refusedIo})。缺席与悬空的分辨在 `readTextIfPresent` 的缺席尾。 */
|
|
46
45
|
export declare function readConfigDFileReading(root: string, name: ConfigDDocumentName): Promise<ConfigDReading>;
|
|
46
|
+
/** {@link readConfigDFileReading} 的同步孪生(`./node` 出口;壳在同步具名锁里读 `config.d` 文档要它):**同一只纯判**({@link readingOfText} → {@link judgeConfigDTextReading})、**同一个缺席尾**
|
|
47
|
+
* (`readTextIfPresentSync` 与异步读者共用 `absentUnlessDangling`),只有读文本那一行 I/O 不同。名字不是 `config.d` 里能放的文档名 ⇒ 同步抛(与异步口的拒绝同一个错)。 */
|
|
48
|
+
export declare function readConfigDFileReadingSync(root: string, name: ConfigDDocumentName): ConfigDReading;
|
|
47
49
|
/** 抛出口:读一个 `config.d/<名>.json`:路径上没有任何条目 ⇒ `undefined`;在场 ⇒ 它的值;被拒 ⇒ 抛读数的 `thrown`(见 {@link readConfigDFileReading})。 */
|
|
48
50
|
export declare function readConfigDFile(root: string, name: ConfigDDocumentName): Promise<Record<string, unknown> | undefined>;
|
|
49
51
|
/** 一棵 `config.d` 的原文快照:便携域与 retired 域的文档原文(缺席 ⇒ `undefined`),版本哈希的入参。 */
|
package/dist/config-d-file.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 句尾出路按文档类({@link documentWayOut}),从不回显文件原文。
|
|
17
17
|
*/
|
|
18
18
|
import path from "node:path";
|
|
19
|
-
import { readTextIfPresent } from "./atomic-write.js";
|
|
19
|
+
import { readTextIfPresent, readTextIfPresentSync } from "./atomic-write.js";
|
|
20
20
|
import { refuseRetiredDomain, sanitizeZodError } from "./config-fns.js";
|
|
21
21
|
import { MAX_HASH_DEPTH, stableHash } from "./hash.js";
|
|
22
22
|
import { UNINSPECTABLE, guardedSafeParse, ownElementsOf, ownEntriesOf } from "./own-key-read.js";
|
|
@@ -156,8 +156,17 @@ function configDValueFlaw(name, value, label) {
|
|
|
156
156
|
}
|
|
157
157
|
return { reason: "not_object", error: new Error(`${label} must be a JSON object (got ${kind}) — ${documentWayOut(name)}`) };
|
|
158
158
|
}
|
|
159
|
-
/**
|
|
160
|
-
*
|
|
159
|
+
/** 在场却读不出的读数:词 `io`(非 `ENOENT` 的 fs 错误,以及悬空的符号链接 —— 文档本身或 `config.d` 目录:内核跟链接读到 `ENOENT`,但条目在场;句子是 `danglingSymlinkSentence`(`./atomic-write`)那一句,
|
|
160
|
+
* 与目录迁移的读口同一句)。`thrown` 是读文本抛的那个错原样,指纹取整句的哈希。异步 / 同步两条读口共用。 */
|
|
161
|
+
function refusedIo(thrown) {
|
|
162
|
+
const error = messageOf(thrown);
|
|
163
|
+
return { status: "refused", reason: "io", error, thrown, fingerprint: stableHash(error) };
|
|
164
|
+
}
|
|
165
|
+
/** 读文本之后的读数(异步 / 同步两条读口共用,**判官只此一处**):没有文本(路径上没有任何条目)⇒ 缺席;有 ⇒ {@link judgeConfigDTextReading}。 */
|
|
166
|
+
function readingOfText(name, file, text) {
|
|
167
|
+
return text === undefined ? { status: "absent" } : judgeConfigDTextReading(name, text, file);
|
|
168
|
+
}
|
|
169
|
+
/** 读一个 `config.d/<名>.json` → 读数:路径上没有任何条目 ⇒ 缺席;在场 ⇒ {@link judgeConfigDTextReading};在场却读不出 ⇒ 被拒,词 `io`({@link refusedIo})。缺席与悬空的分辨在 `readTextIfPresent` 的缺席尾。 */
|
|
161
170
|
export async function readConfigDFileReading(root, name) {
|
|
162
171
|
const file = configDPath(root, name);
|
|
163
172
|
let text;
|
|
@@ -165,10 +174,22 @@ export async function readConfigDFileReading(root, name) {
|
|
|
165
174
|
text = await readTextIfPresent(file, [path.dirname(file)]);
|
|
166
175
|
}
|
|
167
176
|
catch (e) {
|
|
168
|
-
|
|
169
|
-
return { status: "refused", reason: "io", error, thrown: e, fingerprint: stableHash(error) };
|
|
177
|
+
return refusedIo(e);
|
|
170
178
|
}
|
|
171
|
-
return
|
|
179
|
+
return readingOfText(name, file, text);
|
|
180
|
+
}
|
|
181
|
+
/** {@link readConfigDFileReading} 的同步孪生(`./node` 出口;壳在同步具名锁里读 `config.d` 文档要它):**同一只纯判**({@link readingOfText} → {@link judgeConfigDTextReading})、**同一个缺席尾**
|
|
182
|
+
* (`readTextIfPresentSync` 与异步读者共用 `absentUnlessDangling`),只有读文本那一行 I/O 不同。名字不是 `config.d` 里能放的文档名 ⇒ 同步抛(与异步口的拒绝同一个错)。 */
|
|
183
|
+
export function readConfigDFileReadingSync(root, name) {
|
|
184
|
+
const file = configDPath(root, name);
|
|
185
|
+
let text;
|
|
186
|
+
try {
|
|
187
|
+
text = readTextIfPresentSync(file, [path.dirname(file)]);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
return refusedIo(e);
|
|
191
|
+
}
|
|
192
|
+
return readingOfText(name, file, text);
|
|
172
193
|
}
|
|
173
194
|
/** 抛出口:读一个 `config.d/<名>.json`:路径上没有任何条目 ⇒ `undefined`;在场 ⇒ 它的值;被拒 ⇒ 抛读数的 `thrown`(见 {@link readConfigDFileReading})。 */
|
|
174
195
|
export async function readConfigDFile(root, name) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 离线预置(引擎在本根起服之前铺好:镜像 / ConfigMap / 还原)时两份文档与域文档可按任意顺序写;运行时写序是定的:写者先写决定、再写 `config.d/models.json`
|
|
16
16
|
* (都在它对 models.json 的那把锁里),迁移先建运行记录、再铸决定。
|
|
17
17
|
* · **一份读法、一张闭集答案表**:决定只有 {@link EnvModelsDecision} 这一种答案(从字节读出的带同一次读的 `digest`,读不到字节的带 `errno`),
|
|
18
|
-
* 读这份决定的三方 ——
|
|
18
|
+
* 读这份决定的三方 —— 服务端的本地配置腿(读它并强制这道闸)、一次性迁移 SS-M11、写者的计划助手 —— 吃同一个答案;它的身份(告警去重键、本地 etag 的贡献)只有一种拼法 {@link envModelsDecisionTag}。
|
|
19
19
|
* · **运行记录按 id 一份**:「迁移 X 跑过」⇔ `<根>/config.migration.<X>.json` 在场({@link MigrationMarker});没有聚合文件、没有追加写;
|
|
20
20
|
* 读不懂的记录按「跑过」处置(fail-closed)。记录由迁移以「不存在才建」写:同一个 id 只有一个进程建得成 —— 它是那条迁移在本根上唯一的执行者。
|
|
21
21
|
* · 两份文档的形都是 strict 的:多一个键(含自有 `__proto__`)、拼错的词都读成 `bad_shape`,不剥、不猜;digest 对读到的**字节**求。名字只在这里定一次(引擎的目录信任判官取 {@link CONFIG_ROOT_DOCUMENTS})。
|
|
@@ -72,7 +72,7 @@ export type EnvModelsDecision = {
|
|
|
72
72
|
readonly errno: string;
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
|
-
* 一份决定读数的**身份**(带标签的一种拼法):`absent` / `d:<digest>`(有字节的四臂)/ `io:<errno
|
|
75
|
+
* 一份决定读数的**身份**(带标签的一种拼法):`absent` / `d:<digest>`(有字节的四臂)/ `io:<errno>`。服务端的本地配置腿读这份决定时,「每份内容只告警一次」的去重键与本地 etag 的贡献都取它 ——
|
|
76
76
|
* 来自同一次读,不再开文件(那就是第二个读者)。
|
|
77
77
|
*/
|
|
78
78
|
export declare function envModelsDecisionTag(decision: EnvModelsDecision): string;
|
|
@@ -22,7 +22,7 @@ export const isDecisionWord = (v) => ENV_MODELS_DECISIONS.includes(v);
|
|
|
22
22
|
/** 根级文档读不懂的三种原因(闭集):`bad_json` = 空 / 纯空白 / 不是 JSON;`bad_shape` = 是 JSON 但不是这份文档的形;`io` = 字节读不出(权限 —— 可信属主的 0000 文件读恒 `EACCES`,是持久的 —— 不是常规文件、悬空链接、瞬时读错)。 */
|
|
23
23
|
export const ROOT_DOCUMENT_UNREADABLE_REASONS = ["bad_json", "bad_shape", "io"];
|
|
24
24
|
/**
|
|
25
|
-
* 一份决定读数的**身份**(带标签的一种拼法):`absent` / `d:<digest>`(有字节的四臂)/ `io:<errno
|
|
25
|
+
* 一份决定读数的**身份**(带标签的一种拼法):`absent` / `d:<digest>`(有字节的四臂)/ `io:<errno>`。服务端的本地配置腿读这份决定时,「每份内容只告警一次」的去重键与本地 etag 的贡献都取它 ——
|
|
26
26
|
* 来自同一次读,不再开文件(那就是第二个读者)。
|
|
27
27
|
*/
|
|
28
28
|
export function envModelsDecisionTag(decision) {
|
|
@@ -16,6 +16,8 @@ export interface ConfigRootIo {
|
|
|
16
16
|
export declare const nodeConfigRootIo: ConfigRootIo;
|
|
17
17
|
/** 配置根上 env 模型表决定的**唯一读者**(闭集答案见 `EnvModelsDecision`;从字节读出的答案带同一次读的 `digest`,io 带 `errno`)。从不抛。 */
|
|
18
18
|
export declare function readEnvModelsDecision(root: string, io?: ConfigRootIo): Promise<EnvModelsDecision>;
|
|
19
|
+
/** {@link readEnvModelsDecision} 的**同步孪生**(壳在同步具名锁里写模型池,要在锁里读决定):同一个字节读法、同一个缺席律、同一个答案表;读真文件系统(不收注入座 —— 注入座是异步的)。从不抛。 */
|
|
20
|
+
export declare function readEnvModelsDecisionSync(root: string): EnvModelsDecision;
|
|
19
21
|
/**
|
|
20
22
|
* 一条一次性迁移的运行记录的**唯一读者**(文件 `<根>/config.migration.<id>.json`):不存在 ⇒ `absent`;`{"migration":<id>}` ⇒ `present`;别的 ⇒ `unreadable`
|
|
21
23
|
* (坏 JSON / 坏形 —— id 对不上也算 / 读不出)。「跑过」只问这一份文件在不在、读不读得懂,不读任何聚合文件。读数从不抛;`id` 不是写运行记录的迁移(根级文档名单里没有它的记录)⇒
|
|
@@ -31,6 +33,11 @@ export type EnvModelsDecisionWrite = "overwrite" | "createIfAbsent";
|
|
|
31
33
|
export declare function writeEnvModelsDecision(root: string, decision: EnvModelsDecisionWord, opts: {
|
|
32
34
|
readonly write: EnvModelsDecisionWrite;
|
|
33
35
|
}, io?: ConfigRootIo): Promise<"written" | "present">;
|
|
36
|
+
/** {@link writeEnvModelsDecision} 的**同步孪生**(壳在同步具名锁里写模型池:先写决定 `replaced`、再写目录,两次写在同一把锁里):同一份计划({@link planDecisionWrite})、同一对同步原子写原语(异步座调的也是它们);
|
|
37
|
+
* 写真文件系统(不收注入座)。返回值、抛的错与落盘结果与异步口逐项同。 */
|
|
38
|
+
export declare function writeEnvModelsDecisionSync(root: string, decision: EnvModelsDecisionWord, opts: {
|
|
39
|
+
readonly write: EnvModelsDecisionWrite;
|
|
40
|
+
}): "written" | "present";
|
|
34
41
|
/** SS-M11 的结局(闭集,调用方穷举;本函数不出声,告警由调用方按结局发)。 */
|
|
35
42
|
export type EnvModelsMigration =
|
|
36
43
|
/** 运行记录已在:不跑。 */
|
|
@@ -107,7 +114,7 @@ export type EnvModelsMint =
|
|
|
107
114
|
* 4. **独占创建运行记录** `{"migration":"SS-M11"}`:建成 ⇒ 本进程是本根**唯一的铸造者**;已在(`EEXIST`)⇒ `not_minter`,永不铸;别的失败 ⇒ `marker_write_failed`,不铸。
|
|
108
115
|
* 5. 仅铸造者:D0 不是 `absent` ⇒ 不铸(读不懂的也不覆盖);M0 不管 ⇒ 不铸;M0 读不出 ⇒ 不铸(`models_unreadable`,带 errno(若有)与手工恢复句);否则**独占创建** `{"envModels":"replaced"}` —— 结局三词:`minted` / `exists`(只有 `EEXIST`)/
|
|
109
116
|
* `failed`(带 errno 与手工恢复句)。铸不铸不看 env;env 只在铸成之后读一次、只读两槽,用来点名被顶替的 env 模型。
|
|
110
|
-
* 运行记录在场之后,确认缺席 ≡ kept(
|
|
117
|
+
* 运行记录在场之后,确认缺席 ≡ kept(读这份决定的一律这么读:服务端的本地配置腿、迁移、写者)。
|
|
111
118
|
* 撤销的终局按运行记录的创建者算:只有创建者能写确认;以运行记录在场为前提,创建者写完 / 判定不写 / 失败(结局 `minted` / `exists` / `skipped` / `models_unreadable` / `failed`,调用方各记一行)之后,删掉决定文件就是撤销且是终局(删掉运行记录再起服 ⇒ 迁移重跑,决定缺席即重铸);其它副本读到记录在场 = 已跑、跳过(或得 `EEXIST` 答 `not_minter`),它们的返回不构成终局;创建者当选到结局落定之间的窗口在挂起 / I/O 卡顿下无上界,窗口内唯一安全的撤销是写 `kept`(独占创建护住它;删掉的会被创建者照它先读的 D0 铸回);创建者在写确认之前崩溃 ⇒ 永久 `kept`(fail-closed,披露)。
|
|
112
119
|
*/
|
|
113
120
|
export declare function migrateEnvModelsDecision(input: {
|
package/dist/config-root-node.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* 读恒 `EACCES`,是**持久**的 —— 不是常规文件、悬空符号链接、瞬时错)⇒ `unreadable io`;字节在 ⇒ 文本读法(`./config-root-documents`;决定的答案带同一次读的 `digest`)。
|
|
9
9
|
* 读数从不抛(`readMigrationMarker` 只对不写运行记录的 id 抛 `TypeError` —— 调用方的错,不拿它拼路径)。**引擎在读之前先过它自己的目录信任判官**(属主 / 写位 / 常规文件);
|
|
10
10
|
* 本函数不判属主。确认文件的字节只在这里被打开。
|
|
11
|
+
* · {@link readEnvModelsDecisionSync} / {@link writeEnvModelsDecisionSync} —— 决定文档读 / 写的**同步孪生**(壳在同步具名锁里写模型池:先写决定 `replaced`、再写目录,两次写在同一把锁里 —— 要在锁里读、在锁里写):
|
|
12
|
+
* 同一个字节读法、同一个缺席律、同一份答案表、同一份写计划、同一对同步原子写原语(异步座的写本来就是它们的异步包装);读写真文件系统,不收注入座(注入座是异步的,`ConfigRootIo` 不动)。
|
|
11
13
|
* · {@link writeEnvModelsDecision} —— 决定文件的唯一写函数(迁移铸确认也经它):写者(用户确认之后)用 `overwrite`(原子改名覆盖,读不懂的旧文件整份换掉);
|
|
12
14
|
* `createIfAbsent` 不存在才建(已在 ⇒ `present`;迁移铸确认用的就是这一种)。运行记录不经它 —— 由迁移独占创建。
|
|
13
15
|
* 模式与 `config.d` 域文档同一条规则(原子写器的 `config-document` 类:新建 0o644 经进程 umask、更严的 umask 只会更严;已有的文件沿用它原来的模式)—— 写者不要自己写这份文件:
|
|
@@ -16,6 +18,7 @@
|
|
|
16
18
|
* 运行记录 `config.migration.SS-M11.json` —— 建成的那个进程是本根唯一的铸造者,且只按它选举之前读到的决定铸;别的进程见记录已在即停、永不铸。
|
|
17
19
|
* 撤销的终局按创建者算(见 {@link migrateEnvModelsDecision}):运行记录在场、创建者的结局落定之后,删掉决定文件与写 `kept` 同义、是终局(删掉运行记录再起服会重跑迁移);其它副本的返回不构成终局。
|
|
18
20
|
*/
|
|
21
|
+
import { readFileSync, statSync } from "node:fs";
|
|
19
22
|
import { readFile, stat } from "node:fs/promises";
|
|
20
23
|
import path from "node:path";
|
|
21
24
|
import { createFileExclusive, ensureOwnerOnlyDirectory, resolveSymlinkTarget, writeFileAtomic } from "./atomic-write.js";
|
|
@@ -31,19 +34,32 @@ const ROOT_DOCUMENT_CLASS = {
|
|
|
31
34
|
[ENV_MODELS_DOCUMENT]: "config-document",
|
|
32
35
|
[migrationMarkerDocument("SS-M11")]: "config-document",
|
|
33
36
|
};
|
|
37
|
+
/** 「不是常规文件」的拒句(异步读座与同步读口共用一句)。 */
|
|
38
|
+
const notRegularFile = (file) => new Error(`${file} is not a regular file`);
|
|
39
|
+
/** 真文件系统上的两只**同步**写原语(模式类表 {@link ROOT_DOCUMENT_CLASS} 只经这一对取):原子覆盖写 / 独占创建。异步座 {@link nodeConfigRootIo} 与同步写口 {@link writeEnvModelsDecisionSync} 调的是同一对 ——
|
|
40
|
+
* 原子写器本来就是同步实现,异步座的两只写只是它的异步包装。 */
|
|
41
|
+
const writeRootDocumentSync = (document, file, text) => writeFileAtomic(file, text, ROOT_DOCUMENT_CLASS[document]);
|
|
42
|
+
const createRootDocumentSync = (document, file, text) => createFileExclusive(file, text, ROOT_DOCUMENT_CLASS[document]);
|
|
34
43
|
/** 真文件系统的绑定:读先过本包的符号链接判法(链接 ⇒ 它指向的真文件;悬空 ⇒ 抛,不是缺席),只读常规文件;写走本包唯一的原子写器。 */
|
|
35
44
|
export const nodeConfigRootIo = {
|
|
36
45
|
readFile: async (file) => {
|
|
37
46
|
const real = resolveSymlinkTarget(file);
|
|
38
47
|
const st = await stat(real);
|
|
39
48
|
if (!st.isFile())
|
|
40
|
-
throw
|
|
49
|
+
throw notRegularFile(file);
|
|
41
50
|
return readFile(real);
|
|
42
51
|
},
|
|
43
|
-
writeDocument: async (document, file, text) =>
|
|
44
|
-
createDocument: async (document, file, text) =>
|
|
52
|
+
writeDocument: async (document, file, text) => writeRootDocumentSync(document, file, text),
|
|
53
|
+
createDocument: async (document, file, text) => createRootDocumentSync(document, file, text),
|
|
45
54
|
ensureDirectory: async (dir) => ensureOwnerOnlyDirectory(dir),
|
|
46
55
|
};
|
|
56
|
+
/** {@link nodeConfigRootIo}`.readFile` 的同步孪生(同步读口 {@link readEnvModelsDecisionSync} 用):同一条判法(符号链接 ⇒ 真文件,悬空抛;只读常规文件),只有读字节那一步同步。 */
|
|
57
|
+
function readRootFileSync(file) {
|
|
58
|
+
const real = resolveSymlinkTarget(file);
|
|
59
|
+
if (!statSync(real).isFile())
|
|
60
|
+
throw notRegularFile(file);
|
|
61
|
+
return readFileSync(real);
|
|
62
|
+
}
|
|
47
63
|
const decisionFile = (root) => path.join(root, `${ENV_MODELS_DOCUMENT}.json`);
|
|
48
64
|
const markerFile = (root, id) => path.join(root, `${migrationMarkerDocument(id)}.json`);
|
|
49
65
|
/** 一个被抛出的东西的 errno:**只认自有的**字符串 `code`(经内核的键读法);读不出(会抛的 `code` getter、已撤销的 Proxy)、只靠继承、不是串 ⇒ `undefined`。从不抛(读者「从不抛」靠它)。 */
|
|
@@ -66,33 +82,52 @@ const errnoOf = (e) => {
|
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
};
|
|
69
|
-
/**
|
|
70
|
-
|
|
85
|
+
/** 读字节失败之后怎么答 —— 本文件唯一的缺席律(异步 {@link readRootBytes} 与同步 {@link readRootBytesSync} 共用):不存在(只有 `ENOENT`)⇒ `absent`;别的读不出 ⇒ `io`(带 errno 与捕获的错,调用方要句子时据它铸)。 */
|
|
86
|
+
function rootBytesFailure(e) {
|
|
87
|
+
const errno = errnoOf(e);
|
|
88
|
+
return errno === "ENOENT" ? { kind: "absent" } : { kind: "io", errno, cause: e };
|
|
89
|
+
}
|
|
90
|
+
/** 经注入座读一份文件的字节(异步);失败的分类是 {@link rootBytesFailure}。 */
|
|
71
91
|
async function readRootBytes(io, file) {
|
|
72
92
|
try {
|
|
73
93
|
return { kind: "bytes", bytes: await io.readFile(file) };
|
|
74
94
|
}
|
|
75
95
|
catch (e) {
|
|
76
|
-
|
|
77
|
-
return errno === "ENOENT" ? { kind: "absent" } : { kind: "io", errno, cause: e };
|
|
96
|
+
return rootBytesFailure(e);
|
|
78
97
|
}
|
|
79
98
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
/** 真文件系统上读一份文件的字节(同步);失败的分类是 {@link rootBytesFailure}。 */
|
|
100
|
+
function readRootBytesSync(file) {
|
|
101
|
+
try {
|
|
102
|
+
return { kind: "bytes", bytes: readRootFileSync(file) };
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return rootBytesFailure(e);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** 决定文档的字节读数 → 答案(异步 / 同步两只读者共用,字节读法 {@link readEnvModelsDecisionBytes} 只在这里被调)。 */
|
|
109
|
+
function decisionOfBytes(read) {
|
|
110
|
+
switch (read.kind) {
|
|
84
111
|
case "absent":
|
|
85
112
|
return { kind: "absent" };
|
|
86
113
|
case "io":
|
|
87
|
-
return ioDecision(
|
|
114
|
+
return ioDecision(read.errno);
|
|
88
115
|
case "bytes":
|
|
89
|
-
return readEnvModelsDecisionBytes(
|
|
116
|
+
return readEnvModelsDecisionBytes(read.bytes);
|
|
90
117
|
default: {
|
|
91
|
-
const unhandled =
|
|
118
|
+
const unhandled = read;
|
|
92
119
|
throw new Error(`readEnvModelsDecision: unhandled reading ${JSON.stringify(unhandled)}`);
|
|
93
120
|
}
|
|
94
121
|
}
|
|
95
122
|
}
|
|
123
|
+
/** 配置根上 env 模型表决定的**唯一读者**(闭集答案见 `EnvModelsDecision`;从字节读出的答案带同一次读的 `digest`,io 带 `errno`)。从不抛。 */
|
|
124
|
+
export async function readEnvModelsDecision(root, io = nodeConfigRootIo) {
|
|
125
|
+
return decisionOfBytes(await readRootBytes(io, decisionFile(root)));
|
|
126
|
+
}
|
|
127
|
+
/** {@link readEnvModelsDecision} 的**同步孪生**(壳在同步具名锁里写模型池,要在锁里读决定):同一个字节读法、同一个缺席律、同一个答案表;读真文件系统(不收注入座 —— 注入座是异步的)。从不抛。 */
|
|
128
|
+
export function readEnvModelsDecisionSync(root) {
|
|
129
|
+
return decisionOfBytes(readRootBytesSync(decisionFile(root)));
|
|
130
|
+
}
|
|
96
131
|
/**
|
|
97
132
|
* 一条一次性迁移的运行记录的**唯一读者**(文件 `<根>/config.migration.<id>.json`):不存在 ⇒ `absent`;`{"migration":<id>}` ⇒ `present`;别的 ⇒ `unreadable`
|
|
98
133
|
* (坏 JSON / 坏形 —— id 对不上也算 / 读不出)。「跑过」只问这一份文件在不在、读不读得懂,不读任何聚合文件。读数从不抛;`id` 不是写运行记录的迁移(根级文档名单里没有它的记录)⇒
|
|
@@ -115,13 +150,12 @@ export async function readMigrationMarker(root, id, io = nodeConfigRootIo) {
|
|
|
115
150
|
}
|
|
116
151
|
}
|
|
117
152
|
}
|
|
118
|
-
/**
|
|
119
|
-
*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
export async function writeEnvModelsDecision(root, decision, opts, io = nodeConfigRootIo) {
|
|
153
|
+
/** 写决定的**计划**(异步 {@link writeEnvModelsDecision} 与同步 {@link writeEnvModelsDecisionSync} 共用,三件事只在这里):决定词与写法都在闭集里(不在 ⇒ `TypeError`,调用方的错,一字不写)· 铸文本 · 写失败 ⇒ 本包的文件系统失败句(点名路径)。
|
|
154
|
+
* `who` 只用来在 `TypeError` 里点名调用的是哪一只。 */
|
|
155
|
+
function planDecisionWrite(who, root, decision, opts) {
|
|
156
|
+
// 校验次序 = 上一个已发版本里异步口的次序(决定词 → 取路径 → 写法),逐位不动:两处同时坏的输入,先抛的错不变
|
|
123
157
|
if (!isDecisionWord(decision))
|
|
124
|
-
throw new TypeError(
|
|
158
|
+
throw new TypeError(`${who}: decision must be one of ${ENV_MODELS_DECISIONS.join(" / ")} (got ${JSON.stringify(decision)})`);
|
|
125
159
|
const file = decisionFile(root);
|
|
126
160
|
const text = configDDocumentText({ envModels: decision });
|
|
127
161
|
const loud = (e) => {
|
|
@@ -130,16 +164,64 @@ export async function writeEnvModelsDecision(root, decision, opts, io = nodeConf
|
|
|
130
164
|
const write = opts.write;
|
|
131
165
|
switch (write) {
|
|
132
166
|
case "overwrite":
|
|
133
|
-
|
|
167
|
+
case "createIfAbsent":
|
|
168
|
+
break;
|
|
169
|
+
default: {
|
|
170
|
+
const unhandled = write;
|
|
171
|
+
throw new TypeError(`${who}: write must be "overwrite" or "createIfAbsent" (got ${JSON.stringify(unhandled)})`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { write, file, text, loud };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 写配置根上的 env 模型表决定 —— 决定文件的**唯一写函数**(迁移铸确认也经它的 `createIfAbsent`;运行记录不归它,见文件头)。`overwrite` 恒答 `written`;`createIfAbsent` 在文档已在(`EEXIST`)时答 `present`(一字不动)。
|
|
178
|
+
* 决定词 / 写法不在闭集 ⇒ `TypeError`(一字不写)。写失败 ⇒ 抛本包的文件系统失败句(点名路径)。根须已存在。
|
|
179
|
+
*/
|
|
180
|
+
export async function writeEnvModelsDecision(root, decision, opts, io = nodeConfigRootIo) {
|
|
181
|
+
const plan = planDecisionWrite("writeEnvModelsDecision", root, decision, opts);
|
|
182
|
+
const write = plan.write;
|
|
183
|
+
switch (write) {
|
|
184
|
+
case "overwrite":
|
|
185
|
+
await io.writeDocument(ENV_MODELS_DOCUMENT, plan.file, plan.text).catch(plan.loud);
|
|
134
186
|
return "written";
|
|
135
187
|
case "createIfAbsent":
|
|
136
|
-
return (await io.createDocument(ENV_MODELS_DOCUMENT, file, text).catch(loud)) === "created" ? "written" : "present";
|
|
188
|
+
return (await io.createDocument(ENV_MODELS_DOCUMENT, plan.file, plan.text).catch(plan.loud)) === "created" ? "written" : "present";
|
|
137
189
|
default: {
|
|
138
190
|
const unhandled = write;
|
|
139
191
|
throw new TypeError(`writeEnvModelsDecision: write must be "overwrite" or "createIfAbsent" (got ${JSON.stringify(unhandled)})`);
|
|
140
192
|
}
|
|
141
193
|
}
|
|
142
194
|
}
|
|
195
|
+
/** {@link writeEnvModelsDecision} 的**同步孪生**(壳在同步具名锁里写模型池:先写决定 `replaced`、再写目录,两次写在同一把锁里):同一份计划({@link planDecisionWrite})、同一对同步原子写原语(异步座调的也是它们);
|
|
196
|
+
* 写真文件系统(不收注入座)。返回值、抛的错与落盘结果与异步口逐项同。 */
|
|
197
|
+
export function writeEnvModelsDecisionSync(root, decision, opts) {
|
|
198
|
+
const plan = planDecisionWrite("writeEnvModelsDecisionSync", root, decision, opts);
|
|
199
|
+
const write = plan.write;
|
|
200
|
+
switch (write) {
|
|
201
|
+
case "overwrite":
|
|
202
|
+
try {
|
|
203
|
+
writeRootDocumentSync(ENV_MODELS_DOCUMENT, plan.file, plan.text);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
return plan.loud(e);
|
|
207
|
+
}
|
|
208
|
+
return "written";
|
|
209
|
+
case "createIfAbsent": {
|
|
210
|
+
let created;
|
|
211
|
+
try {
|
|
212
|
+
created = createRootDocumentSync(ENV_MODELS_DOCUMENT, plan.file, plan.text);
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
return plan.loud(e);
|
|
216
|
+
}
|
|
217
|
+
return created === "created" ? "written" : "present";
|
|
218
|
+
}
|
|
219
|
+
default: {
|
|
220
|
+
const unhandled = write;
|
|
221
|
+
throw new TypeError(`writeEnvModelsDecisionSync: write must be "overwrite" or "createIfAbsent" (got ${JSON.stringify(unhandled)})`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
143
225
|
/** 手工恢复句(迁移建不成收据 / 建不成决定时随结局交出;调用方原样放进它那一行告警):点名决定文件与两种写法。 */
|
|
144
226
|
const wayOutFor = (root) => `write ${decisionFile(root)} by hand — {"envModels":"replaced"} lets the catalog replace the env models, {"envModels":"kept"} keeps them`;
|
|
145
227
|
/** 读 `config.d/models.json` 一次(字节经 {@link readRootBytes} —— 与两份根级文档同一条缺席律;文本过 `config.d` 文档的那一只文本判 —— 与文件仓库同一读法):
|
|
@@ -179,7 +261,7 @@ async function readModelsDocument(io, root) {
|
|
|
179
261
|
* 4. **独占创建运行记录** `{"migration":"SS-M11"}`:建成 ⇒ 本进程是本根**唯一的铸造者**;已在(`EEXIST`)⇒ `not_minter`,永不铸;别的失败 ⇒ `marker_write_failed`,不铸。
|
|
180
262
|
* 5. 仅铸造者:D0 不是 `absent` ⇒ 不铸(读不懂的也不覆盖);M0 不管 ⇒ 不铸;M0 读不出 ⇒ 不铸(`models_unreadable`,带 errno(若有)与手工恢复句);否则**独占创建** `{"envModels":"replaced"}` —— 结局三词:`minted` / `exists`(只有 `EEXIST`)/
|
|
181
263
|
* `failed`(带 errno 与手工恢复句)。铸不铸不看 env;env 只在铸成之后读一次、只读两槽,用来点名被顶替的 env 模型。
|
|
182
|
-
* 运行记录在场之后,确认缺席 ≡ kept(
|
|
264
|
+
* 运行记录在场之后,确认缺席 ≡ kept(读这份决定的一律这么读:服务端的本地配置腿、迁移、写者)。
|
|
183
265
|
* 撤销的终局按运行记录的创建者算:只有创建者能写确认;以运行记录在场为前提,创建者写完 / 判定不写 / 失败(结局 `minted` / `exists` / `skipped` / `models_unreadable` / `failed`,调用方各记一行)之后,删掉决定文件就是撤销且是终局(删掉运行记录再起服 ⇒ 迁移重跑,决定缺席即重铸);其它副本读到记录在场 = 已跑、跳过(或得 `EEXIST` 答 `not_minter`),它们的返回不构成终局;创建者当选到结局落定之间的窗口在挂起 / I/O 卡顿下无上界,窗口内唯一安全的撤销是写 `kept`(独占创建护住它;删掉的会被创建者照它先读的 D0 铸回);创建者在写确认之前崩溃 ⇒ 永久 `kept`(fail-closed,披露)。
|
|
184
266
|
*/
|
|
185
267
|
export async function migrateEnvModelsDecision(input) {
|
package/dist/env-model-table.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* ## 律
|
|
6
6
|
* · **env 两槽只在这里读一次**({@link readEnvModelSlots}):`MODEL_ID` 缺席或空串 = 没有主槽(纯空白算在场 —— 与引擎的 env 路由读口同尺);
|
|
7
7
|
* cheap 槽四态({@link EnvCheapSlot})。「env 模型表已声明」({@link envModelTableDeclared})与角色底({@link buildEnvModelBaseline})都由它派生 ——
|
|
8
|
-
* 这道闸的执行方(
|
|
8
|
+
* 这道闸的执行方(服务端的本地配置腿)与写者的计划助手调同一份(一次性迁移 SS-M11 只用两槽点名被顶替的 env 模型,铸不铸不看它),不各写一份。env 快照按「读到即算」(下标读;原型分层的快照里继承来的键算在场)。
|
|
9
9
|
* 引擎另有「名册占位 ⇒ env 表算空」一格,那是引擎自己的部署事实,不在本包。
|
|
10
10
|
* · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 →
|
|
11
11
|
* env 表未声明 ⇒ 照写、**不写确认** → 配置根的决定是 `replaced` ⇒ 照写 →
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* 读成未设、按角色回退链落下一级并说一次 —— 没有哪种读法因它拒组;它照样写在回执里(`dangles`),要确认的写法把它给用户看。
|
|
19
19
|
*/
|
|
20
20
|
import { z } from "zod";
|
|
21
|
-
import {
|
|
21
|
+
import { catalogManagesModelPlane, isExplicitlyDisabledEntry } from "./catalog-manages.js";
|
|
22
22
|
import { parseDomainSalvaged, isDisclosableIdentity, resolveActiveTiers, withheldNameToken } from "./config-fns.js";
|
|
23
23
|
import { ENV_MODELS_DOCUMENT } from "./config-root-documents.js";
|
|
24
24
|
import { resolveCatalogDefaultModel } from "./default-model.js";
|
|
@@ -128,7 +128,7 @@ function candidateView(candidate) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
function buildReceipt(baseline, view, decision) {
|
|
131
|
-
const enabled = view.models.filter((m) =>
|
|
131
|
+
const enabled = view.models.filter((m) => !isExplicitlyDisabledEntry(m)).map((m) => ({ name: m.name, id: m.id }));
|
|
132
132
|
const byName = new Map(enabled.map((e) => [e.name, e]));
|
|
133
133
|
const tiers = resolveActiveTiers(view);
|
|
134
134
|
/** 一个名采纳后落到哪条:经与引擎同一张档位展开(目录键先赢;env 底继承来的 `flash` 一类档位词按活跃组解)。 */
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export * from "./url-shape.js";
|
|
|
9
9
|
export * from "./device-relay.js";
|
|
10
10
|
export * from "./url-credential.js";
|
|
11
11
|
export * from "./default-model.js";
|
|
12
|
-
export { catalogManagesModelPlane, enabledEntryCount } from "./catalog-manages.js";
|
|
12
|
+
export { catalogManagesModelPlane, enabledEntryCount, isExplicitlyDisabledEntry } from "./catalog-manages.js";
|
|
13
13
|
export { DEFAULT_MODEL_KEY, ENV_MODEL_TABLE_KEYS, buildEnvModelBaseline, envModelTableDeclared, envModelTableWriteRefusal, mintedCheapId, planEnvModelTableWrite, readEnvModelSlots, type EnvCheapSlot, type EnvModelBaseline, type EnvModelTableEntry, type EnvModelTableKey, type EnvModelTableLanding, type EnvModelTableReceipt, type EnvModelTableRoleRow, type EnvModelTableTarget, type EnvModelTableWriteInput, type EnvModelTableWriteIntent, type EnvModelTableWritePlan, type EnvRoleBinding, type EnvSnapshot, } from "./env-model-table.js";
|
|
14
14
|
export { CONFIG_ROOT_DOCUMENTS, ENV_MODELS_DECISIONS, ENV_MODELS_DOCUMENT, ROOT_DOCUMENT_UNREADABLE_REASONS, envModelsDecisionTag, type ConfigRootDocument, type EnvModelsDecision, type EnvModelsDecisionWord, type MarkedMigrationId, type MigrationMarker, type RootDocumentUnreadableReason } from "./config-root-documents.js";
|
|
15
15
|
export * from "./loopback.js";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export * from "./url-shape.js"; // 11.0.0: THE URL shape judge (httpUrlFlaws / h
|
|
|
9
9
|
export * from "./device-relay.js"; // 16.0.0: THE value grammar of the device lane's DEVICE_RELAY_URL (DEVICE_RELAY_NONE / deviceRelayFlaws / DEVICE_RELAY_RULE), pure/browser-safe
|
|
10
10
|
export * from "./url-credential.js"; // 14.5.0: THE "does this URL carry a credential" judge (findUrlCredential / findUrlUserinfo / URL_CREDENTIAL_REASONS / CREDENTIAL_PARAM_NAMES …), pure/browser-safe
|
|
11
11
|
export * from "./default-model.js"; // 11.0.0: THE catalog-default resolver (roles.default → active tier ladder → fallback), pure/browser-safe
|
|
12
|
-
export { catalogManagesModelPlane, enabledEntryCount } from "./catalog-manages.js"; // THE "does this models document manage the model plane" judge (raw half: at least one entry not explicitly disabled) + the log count it is derived from, pure/browser-safe
|
|
12
|
+
export { catalogManagesModelPlane, enabledEntryCount, isExplicitlyDisabledEntry } from "./catalog-manages.js"; // THE "does this models document manage the model plane" judge (raw half: at least one entry not explicitly disabled) + the log count it is derived from + THE "is this one entry explicitly disabled" predicate they (and every per-entry consumer) read (the three-state `enabled` reading stays package-private), pure/browser-safe
|
|
13
13
|
export { DEFAULT_MODEL_KEY, ENV_MODEL_TABLE_KEYS, buildEnvModelBaseline, envModelTableDeclared, envModelTableWriteRefusal, mintedCheapId, planEnvModelTableWrite, readEnvModelSlots, } from "./env-model-table.js"; // the env model table (MODEL_ID / MODEL_CHEAP_ID) read once: readEnvModelSlots / envModelTableDeclared / buildEnvModelBaseline + the writer's plan planEnvModelTableWrite (receipt by id, confirmKey) + envModelTableWriteRefusal, pure/browser-safe (named: the env snapshot helper stays package-private)
|
|
14
14
|
export { CONFIG_ROOT_DOCUMENTS, ENV_MODELS_DECISIONS, ENV_MODELS_DOCUMENT, ROOT_DOCUMENT_UNREADABLE_REASONS, envModelsDecisionTag } from "./config-root-documents.js"; // the config-root documents (config.env-models.json + the per-id migration marker config.migration.<id>.json): names + closed answer tables + the one tagged identity of a decision reading (envModelsDecisionTag); the readers / writer are in ./node
|
|
15
15
|
export * from "./loopback.js"; // 11.0.0: the two「本机」predicates (auth relaxation — narrow / connection routing — wide), pure/browser-safe
|
package/dist/node.d.ts
CHANGED
|
@@ -8,8 +8,11 @@ export * from "./file-store.js";
|
|
|
8
8
|
export * from "./file-edit.js";
|
|
9
9
|
export * from "./bundle.js";
|
|
10
10
|
export * from "./local-load.js";
|
|
11
|
+
export { readConfigDFileReadingSync, type ConfigDDocumentName, type ConfigDFileReason, type ConfigDReading, type ConfigDRefused } from "./config-d-file.js";
|
|
11
12
|
export * from "./scheduler-store-node.js";
|
|
12
13
|
export { migrateConfigDirectory, migrateConfigDocuments, // 14.5.0:同一段「迁 → 整域终判」的同步口(入参是已有的存量文档,无 io)—— 目录形调的就是它的内核
|
|
13
14
|
type ConfigDirectoryDocument, type ConfigDirectoryIo, type ConfigDirectoryMigration, type ConfigDirectoryRefusal, type ConfigDocumentsMigration, type MigrateConfigDirectoryOptions, type MigrateConfigDocumentsOptions, } from "./config-dir-migration.js";
|
|
14
15
|
export { nodeConfigDirectoryIo } from "./config-dir-io.js";
|
|
15
|
-
export { migrateEnvModelsDecision, nodeConfigRootIo, readEnvModelsDecision,
|
|
16
|
+
export { migrateEnvModelsDecision, nodeConfigRootIo, readEnvModelsDecision, readEnvModelsDecisionSync, // 同步孪生:同一个字节读法 / 缺席律 / 答案表(壳在同步具名锁里写模型池要在锁里读决定)
|
|
17
|
+
readMigrationMarker, writeEnvModelsDecision, writeEnvModelsDecisionSync, // 同步孪生:同一份写计划 + 同一对同步原子写原语
|
|
18
|
+
type ConfigRootIo, type EnvModelsDecisionWrite, type EnvModelsMigration, type EnvModelsMint, } from "./config-root-node.js";
|
package/dist/node.js
CHANGED
|
@@ -8,10 +8,14 @@ export * from "./file-store.js";
|
|
|
8
8
|
export * from "./file-edit.js";
|
|
9
9
|
export * from "./bundle.js";
|
|
10
10
|
export * from "./local-load.js";
|
|
11
|
+
// 一份 `config.d/<名>.json` 的读数(缺席 / 在场 / 被拒)的**同步读口**:与包内异步读口同一只纯判、同一个缺席尾(壳在同步具名锁里读 `config.d` 文档用它,不自读文本);纯判本身与异步读口不出门。显式列名。
|
|
12
|
+
export { readConfigDFileReadingSync } from "./config-d-file.js";
|
|
11
13
|
export * from "./scheduler-store-node.js"; // R7 self-wake scheduler store — fs binding of the ./scheduler-store contract
|
|
12
14
|
// 目录形的一次性迁移(读目录 → 迁 → 整域终判 → 原子写 → 删 retired 域文件)—— 语义只此一处,命令行是它的渲染器、引擎起服调同一只。显式列名:该模块的包内私有件(出门形/拼路径)不外泄。
|
|
13
15
|
export { migrateConfigDirectory, migrateConfigDocuments, // 14.5.0:同一段「迁 → 整域终判」的同步口(入参是已有的存量文档,无 io)—— 目录形调的就是它的内核
|
|
14
16
|
} from "./config-dir-migration.js";
|
|
15
17
|
export { nodeConfigDirectoryIo } from "./config-dir-io.js"; // 上面那只函数的真 I/O 绑定:node:fs + 本包唯一的原子写器(config-document 模式规则)
|
|
16
|
-
// 配置根上的决定文档(config.env-models.json)与按 id 的运行记录(config.migration.<id>.json)的两只读者、决定文档的唯一写函数(
|
|
17
|
-
export { migrateEnvModelsDecision, nodeConfigRootIo, readEnvModelsDecision,
|
|
18
|
+
// 配置根上的决定文档(config.env-models.json)与按 id 的运行记录(config.migration.<id>.json)的两只读者、决定文档的唯一写函数(运行记录由迁移独占创建;决定文档的读写各有同步孪生),与一次性迁移 SS-M11 的迁移体(名字与闭集答案表在包根)。显式列名。
|
|
19
|
+
export { migrateEnvModelsDecision, nodeConfigRootIo, readEnvModelsDecision, readEnvModelsDecisionSync, // 同步孪生:同一个字节读法 / 缺席律 / 答案表(壳在同步具名锁里写模型池要在锁里读决定)
|
|
20
|
+
readMigrationMarker, writeEnvModelsDecision, writeEnvModelsDecisionSync, // 同步孪生:同一份写计划 + 同一对同步原子写原语
|
|
21
|
+
} from "./config-root-node.js";
|
package/dist/settings-units.d.ts
CHANGED
|
@@ -75,6 +75,9 @@ export declare function unitTighten(unit: SettingsUnitName, member?: string): Ti
|
|
|
75
75
|
* are `"manual"` ⇒ `"default"` (permissionMode) and `"disable"` ⇒ `true` (kill-switches); `"bypassPermissions"` is `launch_only`.
|
|
76
76
|
* The last step of every reading is the unit comparator's own domain: whatever reads, compares (file header).
|
|
77
77
|
* Addressing is {@link unitTighten}'s (same RangeErrors: a `members` unit needs a declared member, …).
|
|
78
|
+
* `unreadable` on a `member` / `members` unit is NOT absence, and the reading carries no direction: what the caller does with it depends on the member's
|
|
79
|
+
* `memberAdmission(category, member).direction` (`loosen` ⇒ may be dropped, loudly; `tighten` ⇒ must be kept and answered strictest on its own surface) —
|
|
80
|
+
* see WHEN A MEMBER DOES NOT READ in the file header.
|
|
78
81
|
*/
|
|
79
82
|
export declare function readSettingsUnit(unit: SettingsUnitName, raw: unknown, member?: string): {
|
|
80
83
|
readonly ok: true;
|
package/dist/settings-units.js
CHANGED
|
@@ -38,6 +38,21 @@
|
|
|
38
38
|
* owner, the comparator; a reading only adds what it needs on top (non-empty list elements, the hooks judge, …). So a value the
|
|
39
39
|
* shape half accepts but the comparator cannot place — a hooks document the hooks judge loads while a key it strips hides data
|
|
40
40
|
* nested deeper than `MAX_HASH_DEPTH`, or an `undefined` — is `unreadable`, never "read but not comparable".
|
|
41
|
+
* WHEN A MEMBER DOES NOT READ (the disposition law). On a `member` / `members` unit, `unreadable` is NOT absence: absence is "the document does
|
|
42
|
+
* not set this member", `unreadable` is "the document sets it, in a form the contract cannot place". The two facts a caller needs to dispose of
|
|
43
|
+
* it are already tables, and neither is carried by the reading (a reading has no `direction`: a `whole` unit has no direction source, so the field
|
|
44
|
+
* could only be optional and every consumer would still branch on its absence — and it would be a second reader of a member row's `direction`):
|
|
45
|
+
* the member's direction is `memberAdmission(category, member).direction`, and the category's failure direction is `SAFETY_MERGE_SPEC[category].fail`.
|
|
46
|
+
* For a member of a category whose `fail` is `closed` (today every category that declares members: `permissions`, `sandboxNetwork`, `killSwitches`):
|
|
47
|
+
* · a `loosen` member — the caller MAY drop it (dropping grants less), loudly: it says which member of which layer was dropped and why;
|
|
48
|
+
* · a `tighten` member — the caller must NOT drop it and must NOT read it as absent: it keeps the member and answers on ITS surface with the
|
|
49
|
+
* strictest answer it has (the rules surface refuses every call it cannot place; a kill-switch counts as ON; a sandbox switch takes its strict
|
|
50
|
+
* pole), because dropping a tighten member reads a restriction its author wrote — in a form the contract cannot place — as no restriction at all;
|
|
51
|
+
* the contract cannot tell what it was meant to forbid, and the strictest answer is the only one that cannot be looser than what the author meant.
|
|
52
|
+
* The package enforces nothing here (the surfaces are the consumers'); what it pins is that both inputs exist for every such member and that every
|
|
53
|
+
* out-of-domain value reads `unreadable` without throwing. Not covered, and not settled here: the `whole` units (`permissionMode`, `additionalDirectories`,
|
|
54
|
+
* `hooks`, `env`, `model`, `outputStyle`) have no direction column; whether "unreadable = absent" closes for them depends on the order of the
|
|
55
|
+
* unit's own value domain, which is a design question of its own.
|
|
41
56
|
* The env DOCUMENT's literal grammar (boolean words, csv + empty-set sentinel, a "≤ 0 means off" cap) is not here: it belongs to
|
|
42
57
|
* the engine's env catalog, whose one implementation is the server's.
|
|
43
58
|
*/
|
|
@@ -165,6 +180,9 @@ function shapedReading(unit, row, rule, raw) {
|
|
|
165
180
|
* are `"manual"` ⇒ `"default"` (permissionMode) and `"disable"` ⇒ `true` (kill-switches); `"bypassPermissions"` is `launch_only`.
|
|
166
181
|
* The last step of every reading is the unit comparator's own domain: whatever reads, compares (file header).
|
|
167
182
|
* Addressing is {@link unitTighten}'s (same RangeErrors: a `members` unit needs a declared member, …).
|
|
183
|
+
* `unreadable` on a `member` / `members` unit is NOT absence, and the reading carries no direction: what the caller does with it depends on the member's
|
|
184
|
+
* `memberAdmission(category, member).direction` (`loosen` ⇒ may be dropped, loudly; `tighten` ⇒ must be kept and answered strictest on its own surface) —
|
|
185
|
+
* see WHEN A MEMBER DOES NOT READ in the file header.
|
|
168
186
|
*/
|
|
169
187
|
export function readSettingsUnit(unit, raw, member) {
|
|
170
188
|
const t = unitTarget(unit, member);
|
package/docs/API-SURFACE.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
> 每次全量重算并逐名比对 —— 加 / 删 / 改形一个导出而不重生成本档 ⇒ 红。导出面的每次变动因此以本档的 diff 出现在评审里。
|
|
5
5
|
> 形:`value` = 运行期拿得到的值;`type` = 只在类型层;`value+type` = 同名的值与类型(zod schema 与它推出的类型)。
|
|
6
6
|
|
|
7
|
-
子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计
|
|
7
|
+
子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计 1480 个(同名在多条子路径上各计一次)。
|
|
8
8
|
|
|
9
|
-
## `.`(
|
|
9
|
+
## `.`(773)
|
|
10
10
|
|
|
11
11
|
- `ABSOLUTE_HOST_PATH_MAX` — value
|
|
12
12
|
- `ACCESS_TOKEN_TTL_SECONDS` — value
|
|
@@ -660,6 +660,7 @@
|
|
|
660
660
|
- `isCredentialParamName` — value
|
|
661
661
|
- `isDisclosableIdentity` — value
|
|
662
662
|
- `isDomainName` — value
|
|
663
|
+
- `isExplicitlyDisabledEntry` — value
|
|
663
664
|
- `isHierarchicalUrl` — value
|
|
664
665
|
- `isInstanceStale` — value
|
|
665
666
|
- `isLoopbackAuthHost` — value
|
|
@@ -1176,9 +1177,13 @@
|
|
|
1176
1177
|
- `tightenVerdict` — value
|
|
1177
1178
|
- `trustRank` — value
|
|
1178
1179
|
|
|
1179
|
-
## `./node`(
|
|
1180
|
+
## `./node`(56)
|
|
1180
1181
|
|
|
1181
1182
|
- `ConfigBundle` — type
|
|
1183
|
+
- `ConfigDDocumentName` — type
|
|
1184
|
+
- `ConfigDFileReason` — type
|
|
1185
|
+
- `ConfigDReading` — type
|
|
1186
|
+
- `ConfigDRefused` — type
|
|
1182
1187
|
- `ConfigDirectoryDocument` — type
|
|
1183
1188
|
- `ConfigDirectoryIo` — type
|
|
1184
1189
|
- `ConfigDirectoryMigration` — type
|
|
@@ -1217,7 +1222,9 @@
|
|
|
1217
1222
|
- `nodeConfigDirectoryIo` — value
|
|
1218
1223
|
- `nodeConfigRootIo` — value
|
|
1219
1224
|
- `readBundle` — value
|
|
1225
|
+
- `readConfigDFileReadingSync` — value
|
|
1220
1226
|
- `readEnvModelsDecision` — value
|
|
1227
|
+
- `readEnvModelsDecisionSync` — value
|
|
1221
1228
|
- `readMigrationMarker` — value
|
|
1222
1229
|
- `remoteExecPath` — value
|
|
1223
1230
|
- `saveSchedulerStore` — value
|
|
@@ -1226,6 +1233,7 @@
|
|
|
1226
1233
|
- `writeBundle` — value
|
|
1227
1234
|
- `writeDomainFile` — value
|
|
1228
1235
|
- `writeEnvModelsDecision` — value
|
|
1236
|
+
- `writeEnvModelsDecisionSync` — value
|
|
1229
1237
|
- `writeRemoteExec` — value
|
|
1230
1238
|
|
|
1231
1239
|
## `./one-time-migrations`(18)
|
package/docs/CC-DIVERGENCE.md
CHANGED
|
@@ -46,3 +46,9 @@
|
|
|
46
46
|
| SS-CCD-08 | 仓库里写给 CC 的 `.claude/settings.json` / `.claude/settings.local.json` | 那就是 CC 自己的项目层 / 本地层:工作区受信时整份读(hooks / env / allow 规则 / MCP 服务器……) | 源 `foreignProjectSettings`(`tightenOnly`):不论工作区读数只读收紧方向(deny / ask 规则、关停位的开、沙箱收紧极),其余答 `tighten_only` 或 `below_trust_floor`,一行披露没读的部分 | 更严 | 对本工具它是另一个工具的配置:用户接受本工具的工作区,不等于同意把另一个工具的 hooks、环境变量与 allow 规则搬进来;收紧方向不需要信任 | 壳与服务端读仓库设置文件的那一腿 |
|
|
47
47
|
|
|
48
48
|
关停位(`SAFETY_MERGE_SPEC.killSwitches`)无偏差:任一层可写「开」、合并严者胜,与限制性键表 `{path:["permissions","disableBypassPermissionsMode"],restrictive:"disable"}`(`:237449`)同形。
|
|
49
|
+
|
|
50
|
+
关停位的值域(**实现差**,不是行为偏离,登在这里免得被当成偏离;与上面 SS-CCD-07 后那段同体例):读法 `readSettingsUnit`(单元 `disableBypassPermissionsMode` / `disableAutoMode`)读**布尔超集** ——
|
|
51
|
+
`"disable"`(CC 的「开」的拼写)≡ `true`;`true`;`false` 是一个值 —— 合并比较器 `boolStricter` 的最松极、折叠的单位元,不改变任何有效值,不是缺席;别的一律 `unreadable`。
|
|
52
|
+
CC 的文件面只认字符串 `"disable"`(2.1.309 语料 `cli309.js:258025`:`permissions?.disableBypassPermissionsMode==="disable"`),桌面端管理策略项的文案把同一个键写作 `true`(同一份语料 `:214806`)—— CC 自己也有两种拼写。
|
|
53
|
+
所以把值写回 CC 的拼写面是壳 / 服务端的**渲染**(开 → `"disable"`,关 → 缺席;CC 里缺席 = 未禁用,对 CC 无损),不是第二个读法;本包不出反向拼写函数(把 CC 的拼写表再搬一份进来就是第二个陈述,而读法已经是单源 `readSettingsUnit`)。
|
|
54
|
+
渲染时显式 `false` 变成缺席丢的是文档层的「显式关」,不是有效值;壳若在入闸前就删掉仓库层的 `false`,层准入对它本会答的 `below_trust_floor` 披露不被问到 —— 无效果差。
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
> **14.7.0** 的沙箱 git 凭据平面(取值口 wire 型、有效配置块 `sandboxGit`、`VerifiedIdentity.principalInc`)在 §12。
|
|
11
11
|
> **14.7.0** 的路径基址判官(`PATH_BASE_URL_FORBIDDEN_PARTS` / `pathBaseUrlFlaws`)与公告地址 env 名(`FLEET_ADVERTISE_ADDRESS_ENV`)在 §13。
|
|
12
12
|
> 中心引用解析口 `resolveCenterRefs` 与记录读器 `readPlainStringRecord`(原因词表 `PLAIN_STRING_RECORD_FAULTS`)在 §14。
|
|
13
|
+
> **17.2.0** 的 settings 单元读不进处置律在 §15,`./node` 的同步读写口(`config.d` 文档读数、决定文档读 / 写)与根桶的「显式停用」谓词在 §16。
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -531,3 +532,74 @@ fleet 成员对中心公告的可达地址(`FLEET_ADVERTISE_ADDRESS`,中心往
|
|
|
531
532
|
- **挂不挂**:读数是判决,不是处置。
|
|
532
533
|
- **变量名的形**:名册的 schema 已把变量名限在 env 名的字符集内;本口把变量名当不透明串交给 `lookup`,不再判一遍形。
|
|
533
534
|
- **值的好坏**:解到串就是 `resolved`,空串、很长的串都不另判;值是不是凭据、扣不扣留,是读数之后的另一道判官的事。
|
|
535
|
+
|
|
536
|
+
## 15. settings 单元读不进时的处置律(17.2.0 起)—— 读设置文件的壳与服务端两方
|
|
537
|
+
|
|
538
|
+
**一句话:** `member` / `members` 单元里 `readSettingsUnit` 答 `unreadable` 的成员**不是缺席**。缺席 = 文档没设这个成员;读不进 = 文档设了,但设成了契约放不进的形。
|
|
539
|
+
把它当缺席丢掉 = 当作「没设」,而对收紧方向的成员,「没设」比「设了」更松。
|
|
540
|
+
|
|
541
|
+
### 15.1 本包给什么(律的两个输入都已经是表;读数上不带方向)
|
|
542
|
+
|
|
543
|
+
| 要问的 | 从哪读 |
|
|
544
|
+
|---|---|
|
|
545
|
+
| 这个值读不读得进 | `readSettingsUnit(unit, raw, member?)` → `{ ok: true, value }` 或 `{ ok: false, reason: "unreadable" \| "launch_only" }` |
|
|
546
|
+
| 这个成员往哪个方向动(`loosen` 放宽 / `tighten` 收紧) | `memberAdmission(category, member).direction`(成员行的 `direction`;层准入已经在读它) |
|
|
547
|
+
| 读不进时这个类目往哪边失败 | `SAFETY_MERGE_SPEC[category].fail`(`closed` / `open`;今天有成员的三个类目 —— `permissions`、`sandboxNetwork`、`killSwitches` —— 全是 `closed`) |
|
|
548
|
+
|
|
549
|
+
### 15.2 律(类目 `fail` 为 `closed` 的成员)
|
|
550
|
+
|
|
551
|
+
| 成员方向 | 读不进时调用方做什么 |
|
|
552
|
+
|---|---|
|
|
553
|
+
| `loosen`(今天:`permissions.allow`) | **可以丢**(丢了授得更少),但要响:点名哪一层、哪个成员、为什么丢 |
|
|
554
|
+
| `tighten`(今天:`permissions.deny` / `ask`、`sandbox` 的各成员、两个关停位) | **必须持住,不得丢,不得读成缺席**;在它自己的面上给最严的答案 —— 规则面对放不进的条目拒一切它放不进的调用、关停位当「开」、沙箱开关取严极 |
|
|
555
|
+
|
|
556
|
+
为什么不能丢收紧成员:丢掉它 = 把作者写下的、但契约放不进的一条限制当成「没有限制」。契约读不出它本想禁什么,唯一不会比作者本意松的答案是最严的那个。
|
|
557
|
+
|
|
558
|
+
### 15.3 为什么读数上不带 `direction` 字段(显式拒)
|
|
559
|
+
|
|
560
|
+
- `whole` 单元(`permissionMode`、`additionalDirectories`、`hooks`、`env`、`model`、`outputStyle`)没有成员行,没有方向来源 —— 字段只能是可选的,每个消费者仍要对它缺席的分支写一遍处置;
|
|
561
|
+
- 方向是成员行的属性,`memberAdmission(...).direction` 已经是它的读口;读数再带一份 = 同一行有第二个读者。
|
|
562
|
+
- 要方向就读 `memberAdmission`,要失败方向就读 `SAFETY_MERGE_SPEC[category].fail`;读数只回答「读得进读不进」。
|
|
563
|
+
|
|
564
|
+
### 15.4 本包做什么、不做什么(如实)
|
|
565
|
+
|
|
566
|
+
- **不执法**:读不进的成员落在哪个面、怎么拒,是读它的壳 / 服务端的事。本包的钉保证两件:① 每个 `member` / `members` 单元的每个成员都有合法的 `direction`,类目是 `closed`(表里加成员而漏方向、或类目被改成 `open` ⇒ 红);
|
|
567
|
+
② 对一组界外值(`undefined` / `null` / 错类型 / 空串 / 稀疏数组 / 读就抛的对象 …)`readSettingsUnit` 恒 `unreadable`、不抛,界内值读得进。收紧成员读不进时不放宽的执法钉,落在消费方自己的测试里。
|
|
568
|
+
- **`whole` 单元不在律内**:`permissionMode` 读不进当缺席会落到系统缺省档,`hooks` / `env` / `model` 同理 —— 「读不进即缺席」对它们是否闭合,取决于各自值域的次序而不是成员方向,是另一个设计题,本版没有答案,也不假装有。
|
|
569
|
+
|
|
570
|
+
## 16. 同步读写口与「显式停用」谓词(17.2.0 起)—— 在同步具名锁里读写配置根的壳
|
|
571
|
+
|
|
572
|
+
**一句话:** 以前包里只有异步读写口,壳只好自读文本;现在 `./node` 有同步孪生,读 / 写**同一份判据**。读写示例见 README 的 17.1.x → 17.2.0 升级指引。
|
|
573
|
+
|
|
574
|
+
### 16.1 本包发什么
|
|
575
|
+
|
|
576
|
+
| 名字 | 子路径 | 作用 |
|
|
577
|
+
|---|---|---|
|
|
578
|
+
| `readConfigDFileReadingSync(root, name)` + 类型 `ConfigDReading` / `ConfigDRefused` / `ConfigDFileReason` / `ConfigDDocumentName` | `./node` | 读 `config.d/<名>.json` → `absent` / `present{value}` / `refused{reason, error, thrown, fingerprint}` |
|
|
579
|
+
| `readEnvModelsDecisionSync(root)` | `./node` | 配置根上 env 模型表决定的读数(`absent` / `replaced` / `kept` / `unreadable{bad_json \| bad_shape \| io}`),从不抛 |
|
|
580
|
+
| `writeEnvModelsDecisionSync(root, word, { write })` | `./node` | 写决定:`overwrite` 恒 `written`;`createIfAbsent` 已在则 `present`;词 / 写法不在闭集抛 `TypeError`;写失败抛文件系统失败句 |
|
|
581
|
+
| `isExplicitlyDisabledEntry(entry)` | 根桶 | 一条目录条目原文是不是「显式停用」(自有数据属性 `enabled` 恰为 `false`) |
|
|
582
|
+
|
|
583
|
+
### 16.2 与异步口是同一份判据(不是「差不多」)
|
|
584
|
+
|
|
585
|
+
- 读:同一只纯判(`judgeConfigDTextReading`)、同一个缺席尾(只有 `ENOENT` 算缺席,且文档与 `config.d` 目录都不是悬空符号链接;悬空 ⇒ `io` 拒)。决定文档同一个字节读法、同一个缺席律、同一份答案表。
|
|
586
|
+
- 写:同一份写计划(闭集校验 · 文本铸造 · 失败句)、同一对同步原子写原语;模式规则不变(新建 0o644 经进程 umask,已有的文件沿用原模式)。同步写口与异步口写出的字节、模式、抛的错逐项相同。
|
|
587
|
+
- 同步口读写**真文件系统**,不收注入座;`ConfigRootIo`(异步注入座)类型不变。
|
|
588
|
+
|
|
589
|
+
### 16.3 把自读文本换成同步读口:三处可见差
|
|
590
|
+
|
|
591
|
+
纯空白 / 零字节的文档由「缺席」变 `empty` 拒;悬空符号链接变 `io` 拒;嵌得过深变 `too_deep` 拒。拒句点名路径与出路。壳需要原数组形的旧文件迁移(裸数组旧形)时,`ConfigDReading` 的 `not_object` 拒不带原值 —— 那条迁移在壳自己的退役线上。
|
|
592
|
+
|
|
593
|
+
### 16.4 写模型池的序(不变)
|
|
594
|
+
|
|
595
|
+
先写决定 `replaced`、再写 `config.d/models.json`,两次写在写者对 models.json 的同一把锁里(`planEnvModelTableWrite` 给出的 `write_with_decision` 一步)。**不要自己写决定文件**:写函数给的模式与 `config.d` 域文档同一条规则,自写在 umask 002 下落 0664,引擎的目录信任判官会拒。
|
|
596
|
+
|
|
597
|
+
### 16.5 谓词:不要自己写 `=== false`
|
|
598
|
+
|
|
599
|
+
「显式停用 ⇔ `enabled` 恰为 `false`」这条律只在 `isExplicitlyDisabledEntry` 写一次;`catalogManagesModelPlane` / `enabledEntryCount` 与计划助手都读它。三态读数(`enabled` 缺席 / `true` / `false`)**不出口** —— 出三态就是把这条律交给每个消费者各写一遍。
|
|
600
|
+
条目不是纯 JSON 对象 / `enabled` 不是自有数据属性或不是布尔 ⇒ `false`(不是显式停用,在管文档里计入);要整份文档口径(读不出 ⇒ `TypeError`)问 `enabledEntryCount`。
|
|
601
|
+
|
|
602
|
+
### 16.6 本包不做的(如实)
|
|
603
|
+
|
|
604
|
+
- 不出裸纯判 `judgeConfigDTextReading` 与异步的 `readConfigDFileReading`(壳自读文本 = 第四只缺席读法;前者依赖 `node:path` 与原子写器,不是浏览器安全叶)。
|
|
605
|
+
- 不替壳决定「读不进」之后挂不挂:读数是判决,不是处置(settings 单元的处置律见 §15)。
|
package/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
llms-full.txt — 自包含全文 · 消费方(使用者)视角 · @sema-agent/settings-schema
|
|
3
|
-
重写日期: 2026-11-02 | 包版本: 17.
|
|
3
|
+
重写日期: 2026-11-02 | 包版本: 17.2.0
|
|
4
4
|
生成方式: 无生成脚本——按当前树 src/*.ts 导出面 + package.json + README/CHANGELOG 手写事实版。
|
|
5
5
|
证据约定: 每条"它怎么工作"的断言附 (文件: 符号) 坐标,刻意不用行号(行号逐版腐烂;符号名由
|
|
6
6
|
typecheck 与 test/package-surface.test.ts、test/docs-hygiene.test.ts 等常驻门钉住)。无证据推断显式标【推断】。
|
|
@@ -364,6 +364,7 @@ entitlement/execution/limits = `none`;`projects` = `salvage`,整域档位仍 `"t
|
|
|
364
364
|
- settings 单元表(14.6.0,`src/settings-units.ts`,主 barrel):`SETTINGS_UNITS` 十单元(path / category / reads / reading)、
|
|
365
365
|
`unitTighten(unit, member?)`、`readSettingsUnit(unit, raw, member?)`(manual ⇒ default、"disable" ⇒ true、false 仍 false、
|
|
366
366
|
bypassPermissions ⇒ launch_only;表外与 undefined / null ⇒ unreadable;收下原样返回)。
|
|
367
|
+
`member` / `members` 单元里 `unreadable` 的成员**不是缺席**:类目 `fail:closed` 时 `loosen` 成员可丢(要响)、`tighten` 成员必须持住并给最严答案;方向读 `memberAdmission(category, member).direction`,读数不带方向(docs/CONSUMER-INTEGRATION.md §15)。
|
|
367
368
|
- 信任阶梯(1.2.0):`trustRank`(managed 6 > global 5 > project 4 > local 3 > session 2 > task 1)、
|
|
368
369
|
`isAtLeast`;`MinTrust` 含 `task`(声明序非阶梯,阶梯唯一真源是 `trustRank`)。
|
|
369
370
|
词表是一只纯常量 `MIN_TRUST_WORDS`(`local, session, project, global, managed, task`;`MinTrust` zod 枚举由它派生):`trustRank` 的闭包自由名 = `MIN_TRUST_WORDS` + JS 内建
|
|
@@ -529,6 +530,9 @@ entitlement/execution/limits = `none`;`projects` = `salvage`,整域档位仍 `"t
|
|
|
529
530
|
- `validateDomain`/`validateRemoteExec` → `ValidateResult`(不 throw 用户数据)、`writeDomainFile`/
|
|
530
531
|
`domainFilePath`、`listRequiredEnvNames` → `RequiredEnvName[]`。
|
|
531
532
|
- `exportBundle`/`writeBundle`/`readBundle`(`ConfigBundle` 的键 = `PortableDomain`;11.0.0 起三只函数类型层只收可搬域,非便携域在运行期也拒)。
|
|
533
|
+
- 同步孪生(17.2.0,`./node`):`readConfigDFileReadingSync(root, name)` → `ConfigDReading`(`absent` / `present{value}` / `refused{reason,error,thrown,fingerprint}`;与包内异步读口同一只纯判、同一个缺席尾)、
|
|
534
|
+
`readEnvModelsDecisionSync(root)` / `writeEnvModelsDecisionSync(root, word, { write })`(决定文档;与异步口同字节读法 / 缺席律 / 写计划 / 原子写原语;不收注入座)。
|
|
535
|
+
根桶 `isExplicitlyDisabledEntry(entry)` = 「显式停用」(自有数据属性 `enabled` 恰为 `false`)的唯一写处,三态读数不出口。(src/config-d-file.ts: readConfigDFileReadingSync;src/config-root-node.ts: readEnvModelsDecisionSync / writeEnvModelsDecisionSync;src/catalog-manages.ts: isExplicitlyDisabledEntry)
|
|
532
536
|
- `FileConfigStore.read()` → `FileConfigRead`(16.0.0,`./node`;单读口,取代 15.2.0 的 `inspectBundle`):一次读完 `config.d` 里契约读的每一份文档(全部便携域 + retired 域的墓碑文档,每份只读一次),
|
|
533
537
|
交一个结局 —— 文件层都读得进 ⇒ `{ kind: "effective", effective, warnings }`(effective 与严读 `getEffective()` 逐位同;读侧告警交成数据,不经 `onWarning`);
|
|
534
538
|
有文档被拒 ⇒ `{ kind: "refused", refusals, version, warnings }`。每条拒 `ConfigDocumentRefusal`(包根的类型,按 `reason` 判别):`{ document, reason, error }`,`reason` ∈ 闭集词 `CONFIG_DOCUMENT_REFUSAL_REASONS`
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
llms.txt — 消费方(使用者)视角索引 · @sema-agent/settings-schema
|
|
3
|
-
重写日期: 2026-11-02 | 包版本: 17.
|
|
3
|
+
重写日期: 2026-11-02 | 包版本: 17.2.0
|
|
4
4
|
生成方式: 无生成脚本——按当前树的 src/*.ts 导出面 + package.json + README/CHANGELOG 手写事实版;
|
|
5
5
|
每条断言附证据坐标(文件:符号),刻意不用行号(行号随每版腐烂,符号名由 typecheck/test 钉住)。
|
|
6
6
|
刷新触发: package.json 版号变更(test/docs-hygiene.test.ts D 组钉「包版本:」与 package.json 相等、
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/settings-schema",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.2.0",
|
|
4
4
|
"description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-web-admin, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|