@sema-agent/settings-schema 14.6.0 → 14.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +475 -0
- package/README.md +89 -5
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/atomic-write.d.ts +15 -0
- package/dist/atomic-write.js +109 -5
- package/dist/catalog-manages.d.ts +15 -0
- package/dist/catalog-manages.js +60 -0
- package/dist/center-refs.d.ts +68 -0
- package/dist/center-refs.js +70 -0
- package/dist/config-d-file.js +33 -15
- package/dist/config-dir-migration.js +1 -0
- package/dist/config-fns.js +147 -81
- package/dist/config-root-documents.d.ts +106 -0
- package/dist/config-root-documents.js +93 -0
- package/dist/config-root-node.d.ts +117 -0
- package/dist/config-root-node.js +260 -0
- package/dist/conformance/canonical.js +4 -4
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +4 -4
- package/dist/conformance/derive.js +2 -2
- package/dist/conformance/run.d.ts +3 -0
- package/dist/conformance/run.js +53 -12
- package/dist/conformance/temp-roots.d.ts +31 -0
- package/dist/conformance/temp-roots.js +184 -0
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/default-model.d.ts +1 -1
- package/dist/default-model.js +5 -4
- package/dist/env-model-table.d.ts +163 -0
- package/dist/env-model-table.js +257 -0
- package/dist/file-edit.js +11 -15
- package/dist/fleet.d.ts +36 -7
- package/dist/fleet.js +83 -32
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.js +18 -3
- package/dist/http-hook-allowlists.d.ts +22 -9
- package/dist/http-hook-allowlists.js +169 -91
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/mcp-request-entry.d.ts +67 -13
- package/dist/mcp-request-entry.js +132 -93
- package/dist/mcp-server-name.d.ts +17 -0
- package/dist/mcp-server-name.js +19 -0
- package/dist/migration-receipt.d.ts +10 -6
- package/dist/migration-receipt.js +5 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +2 -0
- package/dist/one-time-migrations.d.ts +4 -2
- package/dist/one-time-migrations.js +236 -96
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +11 -1
- package/dist/plain-json-object.js +37 -1
- package/dist/remote-exec.js +9 -1
- package/dist/retired-form.d.ts +19 -0
- package/dist/retired-form.js +24 -1
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/safety-merge-spec.js +50 -37
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +19 -11
- package/dist/settings-units.js +14 -12
- package/dist/tier-words.d.ts +33 -0
- package/dist/tier-words.js +43 -0
- package/dist/types.d.ts +12 -2
- package/dist/types.js +35 -6
- package/dist/url-shape.d.ts +16 -0
- package/dist/url-shape.js +18 -0
- package/dist/web-admin-credential.js +5 -2
- package/dist/web-admin-session-url.js +2 -2
- package/dist/worker-row-verdict.d.ts +11 -0
- package/dist/worker-row-verdict.js +58 -2
- package/dist/workers-rows.js +38 -10
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +100 -5
- package/docs/CONSUMER-INTEGRATION.md +250 -2
- package/llms-full.txt +26 -4
- package/llms.txt +11 -4
- package/package.json +2 -2
- package/dist/definition-copy.d.ts +0 -23
- package/dist/definition-copy.js +0 -52
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type ConfigRootDocument, type EnvModelsDecision, type EnvModelsDecisionWord, type MarkedMigrationId, type MigrationMarker, type RootDocumentUnreadableReason } from "./config-root-documents.js";
|
|
2
|
+
import { type EnvModelTableEntry, type EnvSnapshot } from "./env-model-table.js";
|
|
3
|
+
import type { MigrationReceipt } from "./migration-receipt.js";
|
|
4
|
+
/** 根级文档读写的 I/O 座(注入;真绑定 {@link nodeConfigRootIo})。写的两只按文档名取模式类({@link ROOT_DOCUMENT_CLASS},类的规则见原子写器的模式表)。 */
|
|
5
|
+
export interface ConfigRootIo {
|
|
6
|
+
/** 读一份文件的**字节**(digest 对字节求;解码由本包做);不存在 ⇒ 抛 `code: "ENOENT"` 的错,别的读不出照常抛(带 errno)。 */
|
|
7
|
+
readFile(file: string): Promise<Uint8Array>;
|
|
8
|
+
/** 原子覆盖写一份根级文档(同目录临时文件 → 改名;模式按该文档的类:新建经 umask、已有的沿用)。父目录须已存在。 */
|
|
9
|
+
writeDocument(document: ConfigRootDocument, file: string, text: string): Promise<void>;
|
|
10
|
+
/** 原子地**独占创建**一份根级文档(独占建临时文件、fsync → `link`;新建模式同 {@link writeDocument}):建成 ⇒ `created`;目标名已在(只有 `EEXIST`)⇒ `present`(一字不动);别的失败照常抛。 */
|
|
11
|
+
createDocument(document: ConfigRootDocument, file: string, text: string): Promise<"created" | "present">;
|
|
12
|
+
/** 目录不在就建(仅属主可进,连同缺的上级):建了 ⇒ `created`;已在 ⇒ `present`。 */
|
|
13
|
+
ensureDirectory(dir: string): Promise<"created" | "present">;
|
|
14
|
+
}
|
|
15
|
+
/** 真文件系统的绑定:读先过本包的符号链接判法(链接 ⇒ 它指向的真文件;悬空 ⇒ 抛,不是缺席),只读常规文件;写走本包唯一的原子写器。 */
|
|
16
|
+
export declare const nodeConfigRootIo: ConfigRootIo;
|
|
17
|
+
/** 配置根上 env 模型表决定的**唯一读者**(闭集答案见 `EnvModelsDecision`;从字节读出的答案带同一次读的 `digest`,io 带 `errno`)。从不抛。 */
|
|
18
|
+
export declare function readEnvModelsDecision(root: string, io?: ConfigRootIo): Promise<EnvModelsDecision>;
|
|
19
|
+
/**
|
|
20
|
+
* 一条一次性迁移的运行记录的**唯一读者**(文件 `<根>/config.migration.<id>.json`):不存在 ⇒ `absent`;`{"migration":<id>}` ⇒ `present`;别的 ⇒ `unreadable`
|
|
21
|
+
* (坏 JSON / 坏形 —— id 对不上也算 / 读不出)。「跑过」只问这一份文件在不在、读不读得懂,不读任何聚合文件。读数从不抛;`id` 不是写运行记录的迁移(根级文档名单里没有它的记录)⇒
|
|
22
|
+
* `TypeError`,不拿它拼路径。
|
|
23
|
+
*/
|
|
24
|
+
export declare function readMigrationMarker(root: string, id: MarkedMigrationId, io?: ConfigRootIo): Promise<MigrationMarker>;
|
|
25
|
+
/** {@link writeEnvModelsDecision} 的写法(闭集):`overwrite` = 写者(用户确认之后,整份覆盖);`createIfAbsent` = 不存在才建。 */
|
|
26
|
+
export type EnvModelsDecisionWrite = "overwrite" | "createIfAbsent";
|
|
27
|
+
/**
|
|
28
|
+
* 写配置根上的 env 模型表决定 —— 决定文件的**唯一写函数**(迁移铸确认也经它的 `createIfAbsent`;运行记录不归它,见文件头)。`overwrite` 恒答 `written`;`createIfAbsent` 在文档已在(`EEXIST`)时答 `present`(一字不动)。
|
|
29
|
+
* 决定词 / 写法不在闭集 ⇒ `TypeError`(一字不写)。写失败 ⇒ 抛本包的文件系统失败句(点名路径)。根须已存在。
|
|
30
|
+
*/
|
|
31
|
+
export declare function writeEnvModelsDecision(root: string, decision: EnvModelsDecisionWord, opts: {
|
|
32
|
+
readonly write: EnvModelsDecisionWrite;
|
|
33
|
+
}, io?: ConfigRootIo): Promise<"written" | "present">;
|
|
34
|
+
/** SS-M11 的结局(闭集,调用方穷举;本函数不出声,告警由调用方按结局发)。 */
|
|
35
|
+
export type EnvModelsMigration =
|
|
36
|
+
/** 运行记录已在:不跑。 */
|
|
37
|
+
{
|
|
38
|
+
readonly kind: "already_ran";
|
|
39
|
+
}
|
|
40
|
+
/** 运行记录读不懂:按已跑处理(读不懂记录就不替人铸确认),一字不写。调用方告警。 */
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: "marker_unreadable";
|
|
43
|
+
readonly reason: RootDocumentUnreadableReason;
|
|
44
|
+
}
|
|
45
|
+
/** 独占创建运行记录时它已在(`EEXIST`):别的进程已是本根的铸造者 ⇒ 本进程永不铸。 */
|
|
46
|
+
| {
|
|
47
|
+
readonly kind: "not_minter";
|
|
48
|
+
}
|
|
49
|
+
/** 运行记录建不成(只读根、根建不出、`ENOSPC`、文件系统不支持硬链接 —— `ENOTSUP` / `EPERM` …):不铸 —— 缺席照旧读作 kept。
|
|
50
|
+
* 记录始终建不成 ⇒ 每次起服都重跑、都答这一格(调用方每次告警;手写的决定照常生效,告警不因它停)。`error` 点名记录文件,`wayOut` 是手工恢复句。 */
|
|
51
|
+
| {
|
|
52
|
+
readonly kind: "marker_write_failed";
|
|
53
|
+
readonly errno: string | undefined;
|
|
54
|
+
readonly error: string;
|
|
55
|
+
readonly wayOut: string;
|
|
56
|
+
}
|
|
57
|
+
/** 本进程独占创建了运行记录 = 本根唯一的铸造者;`decision` = 选举**之前**读到的决定(铸只按它),`mint` = 铸没铸。 */
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: "recorded";
|
|
60
|
+
readonly createdRoot: boolean;
|
|
61
|
+
readonly decision: EnvModelsDecision;
|
|
62
|
+
readonly mint: EnvModelsMint;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* SS-M11 铸确认的结局(闭集)。**披露**:运行记录先于确认建;进程在两者之间中断,或铸失败、或 models.json 读不出 ⇒ 此后无人再铸、决定缺席读作 kept ——
|
|
66
|
+
* 升级前由目录服务的根这时换成 env 模型服务,旧目录不会自动保住;出路是 `wayOut` 那一句(手写决定文件)。
|
|
67
|
+
*/
|
|
68
|
+
export type EnvModelsMint =
|
|
69
|
+
/** 铸了 `replaced`:`replacedEnvModels` = 从此被目录顶替的 env 模型(名 + 槽 + 来源键;env 表没声明 ⇒ 空)—— 非空时调用方据此发一行一次性告警,点名它们与出路(删掉 / 写 kept)。 */
|
|
70
|
+
{
|
|
71
|
+
readonly kind: "minted";
|
|
72
|
+
readonly receipt: MigrationReceipt;
|
|
73
|
+
readonly replacedEnvModels: readonly EnvModelTableEntry[];
|
|
74
|
+
}
|
|
75
|
+
/** 独占创建决定时它已在(只有 `EEXIST` 算):期间有人写了决定(用户的 kept / 写者的 replaced)⇒ 一字不动。 */
|
|
76
|
+
| {
|
|
77
|
+
readonly kind: "exists";
|
|
78
|
+
}
|
|
79
|
+
/** 决定建不成(`ENOSPC` / `EIO` / `EPERM` / 文件系统不支持硬链接 …):缺席照旧读作 kept,记录在 ⇒ 此后不再尝试。`error` 点名决定文件,`wayOut` 是手工恢复句。 */
|
|
80
|
+
| {
|
|
81
|
+
readonly kind: "failed";
|
|
82
|
+
readonly errno: string | undefined;
|
|
83
|
+
readonly error: string;
|
|
84
|
+
readonly wayOut: string;
|
|
85
|
+
}
|
|
86
|
+
/** 不铸:选举前读到的决定已在(replaced / kept / 读不懂 —— 读不懂的不覆盖;它之后被删也不重铸)/ 目录不管模型面(没有 models.json —— 只有 `ENOENT` 算没有 —— 或坏文件)。
|
|
87
|
+
* 判序:决定 → 目录。铸不铸不看 env 表:迁移保的是升级前「管模型面的目录整表顶替 env」这个读法,与 env 声没声明无关。 */
|
|
88
|
+
| {
|
|
89
|
+
readonly kind: "skipped";
|
|
90
|
+
readonly why: "decision_present" | "not_managing";
|
|
91
|
+
}
|
|
92
|
+
/** 不铸,且要运维动手:决定缺席,而 models.json 读不出(`EACCES` / `EIO` / `ESTALE` / 悬空符号链接 / 不是常规文件 …)—— 管不管判不了,不替人铸(fail-closed)。
|
|
93
|
+
* 运行记录已在 ⇒ 此后不再尝试:与 `failed` 同一出路(`wayOut`,手写决定文件);`error` 点名 models.json 与读错,`errno` 只在底层错误带码时有
|
|
94
|
+
* (悬空链接 / 不是常规文件 ⇒ `undefined`),调用方告警。 */
|
|
95
|
+
| {
|
|
96
|
+
readonly kind: "models_unreadable";
|
|
97
|
+
readonly errno: string | undefined;
|
|
98
|
+
readonly error: string;
|
|
99
|
+
readonly wayOut: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* **一次性迁移 SS-M11**(登记见 `ONE_TIME_MIGRATIONS`;起服、第一次读本地配置之前调,只在配置源是本地时):
|
|
103
|
+
* 1. 读运行记录:`present` ⇒ `already_ran`;读不懂 ⇒ `marker_unreadable`(按已跑处理)。
|
|
104
|
+
* 2. **先读**:决定 D0 = `readEnvModelsDecision`,M0 = `config.d/models.json` 管不管模型面(管 / 不管 / 读不出)。此后的铸只按 D0 / M0 决定,不再读决定 ——
|
|
105
|
+
* D0 是 replaced(预置)而它在选举之后被删 ⇒ 不重铸;D0 是 absent 而选举之后有人写了决定 ⇒ 第 5 步的独占创建守住它。
|
|
106
|
+
* 3. 根缺席 ⇒ 建(仅属主可进,连同缺的上级,模式恰是 0o700);**不建 `config.d`**。
|
|
107
|
+
* 4. **独占创建运行记录** `{"migration":"SS-M11"}`:建成 ⇒ 本进程是本根**唯一的铸造者**;已在(`EEXIST`)⇒ `not_minter`,永不铸;别的失败 ⇒ `marker_write_failed`,不铸。
|
|
108
|
+
* 5. 仅铸造者:D0 不是 `absent` ⇒ 不铸(读不懂的也不覆盖);M0 不管 ⇒ 不铸;M0 读不出 ⇒ 不铸(`models_unreadable`,带 errno(若有)与手工恢复句);否则**独占创建** `{"envModels":"replaced"}` —— 结局三词:`minted` / `exists`(只有 `EEXIST`)/
|
|
109
|
+
* `failed`(带 errno 与手工恢复句)。铸不铸不看 env;env 只在铸成之后读一次、只读两槽,用来点名被顶替的 env 模型。
|
|
110
|
+
* 运行记录在场之后,确认缺席 ≡ kept(读这份决定的一律这么读:引擎强制这道闸之后、迁移、写者)。
|
|
111
|
+
* 撤销的终局按运行记录的创建者算:只有创建者能写确认;以运行记录在场为前提,创建者写完 / 判定不写 / 失败(结局 `minted` / `exists` / `skipped` / `models_unreadable` / `failed`,调用方各记一行)之后,删掉决定文件就是撤销且是终局(删掉运行记录再起服 ⇒ 迁移重跑,决定缺席即重铸);其它副本读到记录在场 = 已跑、跳过(或得 `EEXIST` 答 `not_minter`),它们的返回不构成终局;创建者当选到结局落定之间的窗口在挂起 / I/O 卡顿下无上界,窗口内唯一安全的撤销是写 `kept`(独占创建护住它;删掉的会被创建者照它先读的 D0 铸回);创建者在写确认之前崩溃 ⇒ 永久 `kept`(fail-closed,披露)。
|
|
112
|
+
*/
|
|
113
|
+
export declare function migrateEnvModelsDecision(input: {
|
|
114
|
+
readonly root: string;
|
|
115
|
+
readonly env: EnvSnapshot;
|
|
116
|
+
readonly io?: ConfigRootIo;
|
|
117
|
+
}): Promise<EnvModelsMigration>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置根两份根级文档的**读写函数**与一次性迁移 SS-M11 的迁移体(Node 专用,经 `./node` 出口;名字、形与闭集答案表在 `./config-root-documents`)。
|
|
3
|
+
*
|
|
4
|
+
* 读写都经一只注入的 I/O 座 {@link ConfigRootIo}(真绑定 {@link nodeConfigRootIo}:读走 `node:fs/promises`,写走本包唯一的原子写器与它的模式表),
|
|
5
|
+
* 所以单元层能把任意读写失败造出来;调用方不传就是真文件系统。
|
|
6
|
+
*
|
|
7
|
+
* · {@link readEnvModelsDecision} / {@link readMigrationMarker} —— 唯一的读者:不存在(`ENOENT`)⇒ `absent`;别的读错(权限 —— 可信属主的 0000 文件过得了引擎的目录信任判官、
|
|
8
|
+
* 读恒 `EACCES`,是**持久**的 —— 不是常规文件、悬空符号链接、瞬时错)⇒ `unreadable io`;字节在 ⇒ 文本读法(`./config-root-documents`;决定的答案带同一次读的 `digest`)。
|
|
9
|
+
* 读数从不抛(`readMigrationMarker` 只对不写运行记录的 id 抛 `TypeError` —— 调用方的错,不拿它拼路径)。**引擎在读之前先过它自己的目录信任判官**(属主 / 写位 / 常规文件);
|
|
10
|
+
* 本函数不判属主。确认文件的字节只在这里被打开。
|
|
11
|
+
* · {@link writeEnvModelsDecision} —— 决定文件的唯一写函数(迁移铸确认也经它):写者(用户确认之后)用 `overwrite`(原子改名覆盖,读不懂的旧文件整份换掉);
|
|
12
|
+
* `createIfAbsent` 不存在才建(已在 ⇒ `present`;迁移铸确认用的就是这一种)。运行记录不经它 —— 由迁移独占创建。
|
|
13
|
+
* 模式与 `config.d` 域文档同一条规则(原子写器的 `config-document` 类:新建 0o644 经进程 umask、更严的 umask 只会更严;已有的文件沿用它原来的模式)—— 写者不要自己写这份文件:
|
|
14
|
+
* 自写在 umask 002 下落 0664,引擎的目录信任判官会拒。
|
|
15
|
+
* · {@link migrateEnvModelsDecision} —— SS-M11(窗与删法见 `ONE_TIME_MIGRATIONS` 登记表那一行)。**先读后选举**:先读决定与 models.json,再以「不存在才建」写
|
|
16
|
+
* 运行记录 `config.migration.SS-M11.json` —— 建成的那个进程是本根唯一的铸造者,且只按它选举之前读到的决定铸;别的进程见记录已在即停、永不铸。
|
|
17
|
+
* 撤销的终局按创建者算(见 {@link migrateEnvModelsDecision}):运行记录在场、创建者的结局落定之后,删掉决定文件与写 `kept` 同义、是终局(删掉运行记录再起服会重跑迁移);其它副本的返回不构成终局。
|
|
18
|
+
*/
|
|
19
|
+
import { readFile, stat } from "node:fs/promises";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { createFileExclusive, ensureOwnerOnlyDirectory, resolveSymlinkTarget, writeFileAtomic } from "./atomic-write.js";
|
|
22
|
+
import { catalogManagesModelPlane } from "./catalog-manages.js";
|
|
23
|
+
import { configDDocumentText, configDPath, judgeConfigDText } from "./config-d-file.js";
|
|
24
|
+
import { ENV_MODELS_DECISIONS, ENV_MODELS_DOCUMENT, decodeRootBytes, ioDecision, isDecisionWord, isMarkedMigrationId, migrationMarkerDocument, migrationMarkerValue, readEnvModelsDecisionBytes, readMigrationMarkerBytes, } from "./config-root-documents.js";
|
|
25
|
+
import { ENV_MODEL_TABLE_KEYS, buildEnvModelBaseline, snapshotEnvModelTable } from "./env-model-table.js";
|
|
26
|
+
import { FsFailure, fsFailureMessage } from "./fs-failure.js";
|
|
27
|
+
import { readKey } from "./own-key-read.js";
|
|
28
|
+
import { jsonShapeOf } from "./plain-json-object.js";
|
|
29
|
+
/** 根级文档 → 原子写器的模式类(穷举:加一份根级文档不补这一行 ⇒ 编译红,逼着为它选一类)。两份现有的根级文档都是契约文档(只带固定词,不带秘密值),与 `config.d` 域文档同属 `config-document`。 */
|
|
30
|
+
const ROOT_DOCUMENT_CLASS = {
|
|
31
|
+
[ENV_MODELS_DOCUMENT]: "config-document",
|
|
32
|
+
[migrationMarkerDocument("SS-M11")]: "config-document",
|
|
33
|
+
};
|
|
34
|
+
/** 真文件系统的绑定:读先过本包的符号链接判法(链接 ⇒ 它指向的真文件;悬空 ⇒ 抛,不是缺席),只读常规文件;写走本包唯一的原子写器。 */
|
|
35
|
+
export const nodeConfigRootIo = {
|
|
36
|
+
readFile: async (file) => {
|
|
37
|
+
const real = resolveSymlinkTarget(file);
|
|
38
|
+
const st = await stat(real);
|
|
39
|
+
if (!st.isFile())
|
|
40
|
+
throw new Error(`${file} is not a regular file`);
|
|
41
|
+
return readFile(real);
|
|
42
|
+
},
|
|
43
|
+
writeDocument: async (document, file, text) => writeFileAtomic(file, text, ROOT_DOCUMENT_CLASS[document]),
|
|
44
|
+
createDocument: async (document, file, text) => createFileExclusive(file, text, ROOT_DOCUMENT_CLASS[document]),
|
|
45
|
+
ensureDirectory: async (dir) => ensureOwnerOnlyDirectory(dir),
|
|
46
|
+
};
|
|
47
|
+
const decisionFile = (root) => path.join(root, `${ENV_MODELS_DOCUMENT}.json`);
|
|
48
|
+
const markerFile = (root, id) => path.join(root, `${migrationMarkerDocument(id)}.json`);
|
|
49
|
+
/** 一个被抛出的东西的 errno:**只认自有的**字符串 `code`(经内核的键读法);读不出(会抛的 `code` getter、已撤销的 Proxy)、只靠继承、不是串 ⇒ `undefined`。从不抛(读者「从不抛」靠它)。 */
|
|
50
|
+
const errnoOf = (e) => {
|
|
51
|
+
const shape = jsonShapeOf(e); // 形状只问座位(总、不抛);穷举 switch:座位加一个词这里编译红
|
|
52
|
+
switch (shape) {
|
|
53
|
+
case "plain":
|
|
54
|
+
case "non_plain_object": {
|
|
55
|
+
const code = readKey(e, "code"); // Error 实例是 non_plain_object;读不出 ⇒ unreadable(不抛)
|
|
56
|
+
return code.presence === "own" && typeof code.value === "string" ? code.value : undefined;
|
|
57
|
+
}
|
|
58
|
+
case "array":
|
|
59
|
+
case "null":
|
|
60
|
+
case "primitive":
|
|
61
|
+
case "uninspectable": // 检视不了(撤销的 Proxy / 陷阱抛)的东西没有 errno,不是读者的错
|
|
62
|
+
return undefined;
|
|
63
|
+
default: {
|
|
64
|
+
const unhandled = shape;
|
|
65
|
+
throw new Error(`errnoOf: unhandled shape ${String(unhandled)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/** 配置根下一份文件(两份根级文档与 `config.d/models.json`)的字节 —— 本文件唯一的缺席律:不存在(只有 `ENOENT`)⇒ `absent`;
|
|
70
|
+
* 别的读不出 ⇒ `io`(带 errno 与捕获的错,调用方要句子时据它铸)。 */
|
|
71
|
+
async function readRootBytes(io, file) {
|
|
72
|
+
try {
|
|
73
|
+
return { kind: "bytes", bytes: await io.readFile(file) };
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
const errno = errnoOf(e);
|
|
77
|
+
return errno === "ENOENT" ? { kind: "absent" } : { kind: "io", errno, cause: e };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** 配置根上 env 模型表决定的**唯一读者**(闭集答案见 `EnvModelsDecision`;从字节读出的答案带同一次读的 `digest`,io 带 `errno`)。从不抛。 */
|
|
81
|
+
export async function readEnvModelsDecision(root, io = nodeConfigRootIo) {
|
|
82
|
+
const bytes = await readRootBytes(io, decisionFile(root));
|
|
83
|
+
switch (bytes.kind) {
|
|
84
|
+
case "absent":
|
|
85
|
+
return { kind: "absent" };
|
|
86
|
+
case "io":
|
|
87
|
+
return ioDecision(bytes.errno);
|
|
88
|
+
case "bytes":
|
|
89
|
+
return readEnvModelsDecisionBytes(bytes.bytes);
|
|
90
|
+
default: {
|
|
91
|
+
const unhandled = bytes;
|
|
92
|
+
throw new Error(`readEnvModelsDecision: unhandled reading ${JSON.stringify(unhandled)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 一条一次性迁移的运行记录的**唯一读者**(文件 `<根>/config.migration.<id>.json`):不存在 ⇒ `absent`;`{"migration":<id>}` ⇒ `present`;别的 ⇒ `unreadable`
|
|
98
|
+
* (坏 JSON / 坏形 —— id 对不上也算 / 读不出)。「跑过」只问这一份文件在不在、读不读得懂,不读任何聚合文件。读数从不抛;`id` 不是写运行记录的迁移(根级文档名单里没有它的记录)⇒
|
|
99
|
+
* `TypeError`,不拿它拼路径。
|
|
100
|
+
*/
|
|
101
|
+
export async function readMigrationMarker(root, id, io = nodeConfigRootIo) {
|
|
102
|
+
if (!isMarkedMigrationId(id))
|
|
103
|
+
throw new TypeError(`readMigrationMarker: ${JSON.stringify(id)} is not a migration that writes a run marker (the root documents list it) — no path is built from it`);
|
|
104
|
+
const bytes = await readRootBytes(io, markerFile(root, id));
|
|
105
|
+
switch (bytes.kind) {
|
|
106
|
+
case "absent":
|
|
107
|
+
return { kind: "absent" };
|
|
108
|
+
case "io":
|
|
109
|
+
return { kind: "unreadable", reason: "io" };
|
|
110
|
+
case "bytes":
|
|
111
|
+
return readMigrationMarkerBytes(id, bytes.bytes);
|
|
112
|
+
default: {
|
|
113
|
+
const unhandled = bytes;
|
|
114
|
+
throw new Error(`readMigrationMarker: unhandled reading ${JSON.stringify(unhandled)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 写配置根上的 env 模型表决定 —— 决定文件的**唯一写函数**(迁移铸确认也经它的 `createIfAbsent`;运行记录不归它,见文件头)。`overwrite` 恒答 `written`;`createIfAbsent` 在文档已在(`EEXIST`)时答 `present`(一字不动)。
|
|
120
|
+
* 决定词 / 写法不在闭集 ⇒ `TypeError`(一字不写)。写失败 ⇒ 抛本包的文件系统失败句(点名路径)。根须已存在。
|
|
121
|
+
*/
|
|
122
|
+
export async function writeEnvModelsDecision(root, decision, opts, io = nodeConfigRootIo) {
|
|
123
|
+
if (!isDecisionWord(decision))
|
|
124
|
+
throw new TypeError(`writeEnvModelsDecision: decision must be one of ${ENV_MODELS_DECISIONS.join(" / ")} (got ${JSON.stringify(decision)})`);
|
|
125
|
+
const file = decisionFile(root);
|
|
126
|
+
const text = configDDocumentText({ envModels: decision });
|
|
127
|
+
const loud = (e) => {
|
|
128
|
+
throw e instanceof FsFailure ? e : new FsFailure({ landing: "write", path: file }, e);
|
|
129
|
+
};
|
|
130
|
+
const write = opts.write;
|
|
131
|
+
switch (write) {
|
|
132
|
+
case "overwrite":
|
|
133
|
+
await io.writeDocument(ENV_MODELS_DOCUMENT, file, text).catch(loud);
|
|
134
|
+
return "written";
|
|
135
|
+
case "createIfAbsent":
|
|
136
|
+
return (await io.createDocument(ENV_MODELS_DOCUMENT, file, text).catch(loud)) === "created" ? "written" : "present";
|
|
137
|
+
default: {
|
|
138
|
+
const unhandled = write;
|
|
139
|
+
throw new TypeError(`writeEnvModelsDecision: write must be "overwrite" or "createIfAbsent" (got ${JSON.stringify(unhandled)})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** 手工恢复句(迁移建不成收据 / 建不成决定时随结局交出;调用方原样放进它那一行告警):点名决定文件与两种写法。 */
|
|
144
|
+
const wayOutFor = (root) => `write ${decisionFile(root)} by hand — {"envModels":"replaced"} lets the catalog replace the env models, {"envModels":"kept"} keeps them`;
|
|
145
|
+
/** 读 `config.d/models.json` 一次(字节经 {@link readRootBytes} —— 与两份根级文档同一条缺席律;文本过 `config.d` 文档的那一只文本判 —— 与文件仓库同一读法):
|
|
146
|
+
* 缺席 / 坏文件 ⇒ 不管;别的读错 ⇒ 读不出(不折成不管)。 */
|
|
147
|
+
async function readModelsDocument(io, root) {
|
|
148
|
+
const file = configDPath(root, "models");
|
|
149
|
+
const read = await readRootBytes(io, file);
|
|
150
|
+
let bytes;
|
|
151
|
+
switch (read.kind) {
|
|
152
|
+
case "absent":
|
|
153
|
+
return { kind: "not_managing" };
|
|
154
|
+
case "io":
|
|
155
|
+
return { kind: "unreadable", errno: read.errno, error: fsFailureMessage({ landing: "read", path: file, of: "file" }, read.cause) };
|
|
156
|
+
case "bytes":
|
|
157
|
+
bytes = read.bytes;
|
|
158
|
+
break;
|
|
159
|
+
default: {
|
|
160
|
+
const unhandled = read;
|
|
161
|
+
throw new Error(`readModelsDocument: unhandled reading ${JSON.stringify(unhandled)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let doc;
|
|
165
|
+
try {
|
|
166
|
+
doc = judgeConfigDText("models", decodeRootBytes(bytes), file);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { kind: "not_managing" }; // a bad file manages nothing; the file store refuses it loudly on its own read
|
|
170
|
+
}
|
|
171
|
+
return catalogManagesModelPlane(doc) ? { kind: "manages" } : { kind: "not_managing" }; // JSON 解析出来的值恒是纯数据:判官不会抛
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* **一次性迁移 SS-M11**(登记见 `ONE_TIME_MIGRATIONS`;起服、第一次读本地配置之前调,只在配置源是本地时):
|
|
175
|
+
* 1. 读运行记录:`present` ⇒ `already_ran`;读不懂 ⇒ `marker_unreadable`(按已跑处理)。
|
|
176
|
+
* 2. **先读**:决定 D0 = `readEnvModelsDecision`,M0 = `config.d/models.json` 管不管模型面(管 / 不管 / 读不出)。此后的铸只按 D0 / M0 决定,不再读决定 ——
|
|
177
|
+
* D0 是 replaced(预置)而它在选举之后被删 ⇒ 不重铸;D0 是 absent 而选举之后有人写了决定 ⇒ 第 5 步的独占创建守住它。
|
|
178
|
+
* 3. 根缺席 ⇒ 建(仅属主可进,连同缺的上级,模式恰是 0o700);**不建 `config.d`**。
|
|
179
|
+
* 4. **独占创建运行记录** `{"migration":"SS-M11"}`:建成 ⇒ 本进程是本根**唯一的铸造者**;已在(`EEXIST`)⇒ `not_minter`,永不铸;别的失败 ⇒ `marker_write_failed`,不铸。
|
|
180
|
+
* 5. 仅铸造者:D0 不是 `absent` ⇒ 不铸(读不懂的也不覆盖);M0 不管 ⇒ 不铸;M0 读不出 ⇒ 不铸(`models_unreadable`,带 errno(若有)与手工恢复句);否则**独占创建** `{"envModels":"replaced"}` —— 结局三词:`minted` / `exists`(只有 `EEXIST`)/
|
|
181
|
+
* `failed`(带 errno 与手工恢复句)。铸不铸不看 env;env 只在铸成之后读一次、只读两槽,用来点名被顶替的 env 模型。
|
|
182
|
+
* 运行记录在场之后,确认缺席 ≡ kept(读这份决定的一律这么读:引擎强制这道闸之后、迁移、写者)。
|
|
183
|
+
* 撤销的终局按运行记录的创建者算:只有创建者能写确认;以运行记录在场为前提,创建者写完 / 判定不写 / 失败(结局 `minted` / `exists` / `skipped` / `models_unreadable` / `failed`,调用方各记一行)之后,删掉决定文件就是撤销且是终局(删掉运行记录再起服 ⇒ 迁移重跑,决定缺席即重铸);其它副本读到记录在场 = 已跑、跳过(或得 `EEXIST` 答 `not_minter`),它们的返回不构成终局;创建者当选到结局落定之间的窗口在挂起 / I/O 卡顿下无上界,窗口内唯一安全的撤销是写 `kept`(独占创建护住它;删掉的会被创建者照它先读的 D0 铸回);创建者在写确认之前崩溃 ⇒ 永久 `kept`(fail-closed,披露)。
|
|
184
|
+
*/
|
|
185
|
+
export async function migrateEnvModelsDecision(input) {
|
|
186
|
+
const { root, env } = input;
|
|
187
|
+
const io = input.io ?? nodeConfigRootIo;
|
|
188
|
+
const marker = await readMigrationMarker(root, "SS-M11", io);
|
|
189
|
+
switch (marker.kind) {
|
|
190
|
+
case "present":
|
|
191
|
+
return { kind: "already_ran" };
|
|
192
|
+
case "unreadable":
|
|
193
|
+
return { kind: "marker_unreadable", reason: marker.reason };
|
|
194
|
+
case "absent":
|
|
195
|
+
break;
|
|
196
|
+
default: {
|
|
197
|
+
const unhandled = marker;
|
|
198
|
+
throw new Error(`migrateEnvModelsDecision: unhandled marker ${JSON.stringify(unhandled)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const decision = await readEnvModelsDecision(root, io);
|
|
202
|
+
const models = await readModelsDocument(io, root);
|
|
203
|
+
const file = markerFile(root, "SS-M11");
|
|
204
|
+
let createdRoot;
|
|
205
|
+
let created;
|
|
206
|
+
try {
|
|
207
|
+
createdRoot = (await io.ensureDirectory(root)) === "created";
|
|
208
|
+
created = await io.createDocument(migrationMarkerDocument("SS-M11"), file, configDDocumentText(migrationMarkerValue("SS-M11")));
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
return { kind: "marker_write_failed", errno: errnoOf(e), error: fsFailureMessage({ landing: "write", path: file }, e), wayOut: wayOutFor(root) };
|
|
212
|
+
}
|
|
213
|
+
if (created === "present")
|
|
214
|
+
return { kind: "not_minter" };
|
|
215
|
+
return { kind: "recorded", createdRoot, decision, mint: await mintDecision(io, root, env, decision, models) };
|
|
216
|
+
}
|
|
217
|
+
async function mintDecision(io, root, env, decision, models) {
|
|
218
|
+
switch (decision.kind) {
|
|
219
|
+
case "absent":
|
|
220
|
+
break;
|
|
221
|
+
case "replaced":
|
|
222
|
+
case "kept":
|
|
223
|
+
case "unreadable":
|
|
224
|
+
return { kind: "skipped", why: "decision_present" };
|
|
225
|
+
default: {
|
|
226
|
+
const unhandled = decision;
|
|
227
|
+
throw new Error(`migrateEnvModelsDecision: unhandled decision ${JSON.stringify(unhandled)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
switch (models.kind) {
|
|
231
|
+
case "manages":
|
|
232
|
+
break;
|
|
233
|
+
case "not_managing":
|
|
234
|
+
return { kind: "skipped", why: "not_managing" };
|
|
235
|
+
case "unreadable":
|
|
236
|
+
return { kind: "models_unreadable", errno: models.errno, error: models.error, wayOut: wayOutFor(root) };
|
|
237
|
+
default: {
|
|
238
|
+
const unhandled = models;
|
|
239
|
+
throw new Error(`migrateEnvModelsDecision: unhandled models reading ${JSON.stringify(unhandled)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let wrote;
|
|
243
|
+
try {
|
|
244
|
+
wrote = await writeEnvModelsDecision(root, "replaced", { write: "createIfAbsent" }, io);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
// the write function throws only its file-system failure (it names the decision file and carries the errno)
|
|
248
|
+
return { kind: "failed", errno: errnoOf(e), error: fsFailureMessage({ landing: "write", path: decisionFile(root) }, e), wayOut: wayOutFor(root) };
|
|
249
|
+
}
|
|
250
|
+
if (wrote === "present")
|
|
251
|
+
return { kind: "exists" };
|
|
252
|
+
// env is read only now, once, and only the two slot keys: it names the env models the minted decision replaces (none when no table is declared);
|
|
253
|
+
// a run that stops earlier reads no env at all
|
|
254
|
+
const slots = snapshotEnvModelTable(env, [ENV_MODEL_TABLE_KEYS.mainId, ENV_MODEL_TABLE_KEYS.cheapId]);
|
|
255
|
+
return {
|
|
256
|
+
kind: "minted",
|
|
257
|
+
receipt: { migration: "SS-M11", document: ENV_MODELS_DOCUMENT, path: ["envModels"], appliedAs: "minted" },
|
|
258
|
+
replacedEnvModels: buildEnvModelBaseline(slots).entries,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* `stableKey` 是唯一一处把值转成串的地方,它只用来给**多重集计数**当哈希键 —— 用的是 JSON 编码,
|
|
11
11
|
* 数组边界与段类型(`"1"` vs `1`)在编码里都还在,所以它不是那种有损的点号串。
|
|
12
12
|
*/
|
|
13
|
-
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
13
|
+
import { isJsonArray, isPlainJsonObject } from "../plain-json-object.js";
|
|
14
14
|
/**
|
|
15
15
|
* 「这个对象**在场**的键」—— 本文件里唯一一处判键在不在 / 取键的值的地方。
|
|
16
16
|
*
|
|
@@ -37,7 +37,7 @@ function presentKeys(o) {
|
|
|
37
37
|
* 「键不在场」与「键在场但值是 undefined」在比对上分成两件事,而 wire 上它们同形)。
|
|
38
38
|
*/
|
|
39
39
|
export function canonicalize(value) {
|
|
40
|
-
if (
|
|
40
|
+
if (isJsonArray(value))
|
|
41
41
|
return value.map(canonicalize);
|
|
42
42
|
if (isPlainJsonObject(value)) {
|
|
43
43
|
// `Object.fromEntries`,**不是**逐键 `out[k] = …`:后者对 `"__proto__"` 走的是原型 setter(改掉对象的
|
|
@@ -55,7 +55,7 @@ export function stableKey(value) {
|
|
|
55
55
|
export function deepEqual(a, b) {
|
|
56
56
|
if (Object.is(a, b))
|
|
57
57
|
return true;
|
|
58
|
-
if (
|
|
58
|
+
if (isJsonArray(a) && isJsonArray(b))
|
|
59
59
|
return a.length === b.length && a.every((x, i) => deepEqual(x, b[i]));
|
|
60
60
|
if (isPlainJsonObject(a) && isPlainJsonObject(b)) {
|
|
61
61
|
const ka = presentKeys(a);
|
|
@@ -78,7 +78,7 @@ export function diffJson(expected, actual, at = []) {
|
|
|
78
78
|
const path = at.length === 0 ? "" : "/" + at.map(pointerSegment).join("/");
|
|
79
79
|
if (deepEqual(expected, actual))
|
|
80
80
|
return [];
|
|
81
|
-
if (
|
|
81
|
+
if (isJsonArray(expected) && isJsonArray(actual)) {
|
|
82
82
|
const out = [];
|
|
83
83
|
for (let i = 0; i < Math.max(expected.length, actual.length); i++) {
|
|
84
84
|
if (i >= expected.length)
|
|
@@ -24,8 +24,33 @@
|
|
|
24
24
|
import { materializeLocalRoot } from "./run.js";
|
|
25
25
|
import { contractOwnedEntry, contractOwnedEntryWhy } from "./entry-origin.js";
|
|
26
26
|
import { LEG_NAMES, REF_INTEGRITY_REJECT_CODE } from "./types.js";
|
|
27
|
+
import { inspect } from "../own-key-read.js";
|
|
28
|
+
import { isJsonArray, jsonShapeOf } from "../plain-json-object.js";
|
|
27
29
|
/** ZodError 形(鸭子判,不用 `instanceof` —— historical 模式下旧版 dist 可能挂在另一份 zod 上)。 */
|
|
28
|
-
|
|
30
|
+
/** A thrown value that looks like a zod error (an object — not a function — with an `issues` array on it; v14.6.0's `typeof e === "object"` test, nothing more). Shape from the seat;
|
|
31
|
+
* the one member read goes through the inspection kernel. The seat reads the prototype once to classify, but the answer never depends on it: an object whose shape cannot
|
|
32
|
+
* be read (a `getPrototypeOf` trap that throws) is still asked for `issues` — a thrown proxy whose `issues` cannot be read is not zod-like. */
|
|
33
|
+
const isZodLike = (e) => {
|
|
34
|
+
const shape = jsonShapeOf(e);
|
|
35
|
+
switch (shape) {
|
|
36
|
+
case "null":
|
|
37
|
+
case "primitive":
|
|
38
|
+
return false;
|
|
39
|
+
case "array":
|
|
40
|
+
case "plain":
|
|
41
|
+
case "non_plain_object":
|
|
42
|
+
case "uninspectable": {
|
|
43
|
+
if (typeof e === "function")
|
|
44
|
+
return false; // 被抛的函数不是 zod 错(v14.6.0 的 `typeof e === "object"` 前置判把函数排除在外)
|
|
45
|
+
const issues = inspect(() => e.issues);
|
|
46
|
+
return issues.ok && isJsonArray(issues.value);
|
|
47
|
+
}
|
|
48
|
+
default: {
|
|
49
|
+
const unhandled = shape;
|
|
50
|
+
throw new Error(`isZodLike: unhandled shape ${String(unhandled)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
29
54
|
/** 内容哈希那一层拒绝的自述 —— `stableStringify` 的深度闸是**本包自己**抛的、也只有它会抛这一句;
|
|
30
55
|
* 适配器给自家流水线分层,认得自家那句话是合理的(消费方的适配器认它自己那几层的错)。 */
|
|
31
56
|
const HASH_STAGE_MARK = "stableStringify:";
|
|
@@ -20,7 +20,7 @@ import { dirname, join } from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { LEG_NAMES, REJECT_STAGES } from "./types.js";
|
|
22
22
|
import { LEG_EXPECTATION } from "./derive.js";
|
|
23
|
-
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
23
|
+
import { isJsonArray, isPlainJsonObject } from "../plain-json-object.js";
|
|
24
24
|
/** 本包的根目录 —— `src/conformance/*.ts` 与 `dist/conformance/*.js` 距根同深,两处求值同一个地方。 */
|
|
25
25
|
export const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
26
26
|
/** 语料目录(默认 = 本包的 `conformance/`)。 */
|
|
@@ -45,7 +45,7 @@ function checkLegExpectation(v, where) {
|
|
|
45
45
|
}
|
|
46
46
|
if (r.outcome === "effective") {
|
|
47
47
|
requireRecord(r.effective, `${where}.effective`);
|
|
48
|
-
if (!
|
|
48
|
+
if (!isJsonArray(r.warnings))
|
|
49
49
|
throw new Error(`conformance corpus: ${where}.warnings must be an array`);
|
|
50
50
|
const meta = requireRecord(r.metadata, `${where}.metadata`);
|
|
51
51
|
if (typeof meta.updatedAt !== "string")
|
|
@@ -56,7 +56,7 @@ function checkLegExpectation(v, where) {
|
|
|
56
56
|
if (!REJECT_STAGES.includes(r.stage))
|
|
57
57
|
throw new Error(`conformance corpus: ${where}.stage must be one of ${REJECT_STAGES.join("|")}`);
|
|
58
58
|
if (r.refIssues !== undefined) {
|
|
59
|
-
const ok =
|
|
59
|
+
const ok = isJsonArray(r.refIssues) && r.refIssues.every((x) => isPlainJsonObject(x) && isJsonArray(x.path) && x.path.every((seg) => typeof seg === "string" || typeof seg === "number"));
|
|
60
60
|
if (!ok)
|
|
61
61
|
throw new Error(`conformance corpus: ${where}.refIssues must be an array of { path: (string|number)[] }`);
|
|
62
62
|
}
|
|
@@ -76,7 +76,7 @@ export async function loadCase(dir, id) {
|
|
|
76
76
|
requireRecord(input.domains, `${id}/input.json#domains`);
|
|
77
77
|
if (typeof meta.claim !== "string" || meta.claim.trim().length === 0)
|
|
78
78
|
throw new Error(`conformance corpus: ${id}/meta.json#claim is empty — 每条语料必须写它想打谁`);
|
|
79
|
-
if (!
|
|
79
|
+
if (!isJsonArray(meta.firstRedOn))
|
|
80
80
|
throw new Error(`conformance corpus: ${id}/meta.json#firstRedOn must be an array`);
|
|
81
81
|
const legs = {};
|
|
82
82
|
for (const leg of LEG_NAMES) {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { NON_DOMAIN_CONFIG_FILES } from "../file-store.js";
|
|
38
38
|
import { PORTABLE_DOMAINS } from "../portable-domains.js";
|
|
39
|
-
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
39
|
+
import { isJsonArray, isPlainJsonObject } from "../plain-json-object.js";
|
|
40
40
|
import { skillContentHash } from "../skills-manifest.js";
|
|
41
41
|
import { DOMAINS, isDomainName } from "../types.js";
|
|
42
42
|
/** 变换表(本文件头注的机器形;`docs/CONFORMANCE.md` 与 `test/conformance-coverage.test.ts` 都读它)。 */
|
|
@@ -72,7 +72,7 @@ export const foreignKeysOf = (docs) => Object.keys(docs).filter((k) => !isDomain
|
|
|
72
72
|
/** T2 的**入参**半场:域形的 skills 文档 → wire 形的 manifest 文档。结构变换,不解析 —— 坏形原样传下去,
|
|
73
73
|
* 让 wire 腿自己的判据去裁(否则语料就没法喂一份坏 manifest 了)。 */
|
|
74
74
|
export function toManifestDoc(doc) {
|
|
75
|
-
if (!isPlainJsonObject(doc) || !
|
|
75
|
+
if (!isPlainJsonObject(doc) || !isJsonArray(doc.skills))
|
|
76
76
|
return doc;
|
|
77
77
|
return {
|
|
78
78
|
...doc,
|
|
@@ -6,6 +6,9 @@ import { type ConformanceAdapter, type ConformanceOptions, type ConformanceRepor
|
|
|
6
6
|
* mtime 怎么设、目录怎么建就会各不相同,而 `updatedAt` 正是按 mtime 算的)。
|
|
7
7
|
*
|
|
8
8
|
* 返回 `root` 与 `cleanup`;所有文件的 mtime 统一设成 `mtime`,于是 `updatedAt` 对同一份快照恒定。
|
|
9
|
+
*
|
|
10
|
+
* 临时根只在 `./temp-roots` 里铸、只在那里删:进程在 `cleanup()` 之前死掉(`process.exit`、未捕获异常、信号、SIGKILL)时根留在 tmp 下,
|
|
11
|
+
* 根里的 `.alive` 超过 60 分钟没更新之后,由下一轮第一次铸根的清扫收走 —— 这里不碰临时目录的路径、前缀和清理,也不在进程上装任何监听。
|
|
9
12
|
*/
|
|
10
13
|
export declare function materializeLocalRoot(files: Readonly<Record<string, string>>, mtime: string): Promise<{
|
|
11
14
|
root: string;
|