@sema-agent/settings-schema 14.7.0 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +306 -0
- package/README.md +80 -3
- package/conformance/VERSION +1 -1
- package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +4 -1
- package/conformance/cases/c61-s784-g3-other-lane-key/input.json +1 -0
- package/conformance/cases/c64-s784-multitenant-local-store/input.json +2 -1
- package/dist/atomic-write.d.ts +15 -0
- package/dist/atomic-write.js +109 -5
- package/dist/catalog-manages.d.ts +15 -0
- package/dist/catalog-manages.js +60 -0
- package/dist/center-refs.d.ts +68 -0
- package/dist/center-refs.js +70 -0
- package/dist/config-dir-migration.js +1 -0
- package/dist/config-fns.js +5 -3
- package/dist/config-root-documents.d.ts +106 -0
- package/dist/config-root-documents.js +93 -0
- package/dist/config-root-node.d.ts +117 -0
- package/dist/config-root-node.js +260 -0
- package/dist/conformance/run.d.ts +3 -0
- package/dist/conformance/run.js +10 -6
- package/dist/conformance/temp-roots.d.ts +31 -0
- package/dist/conformance/temp-roots.js +184 -0
- package/dist/default-model.d.ts +1 -1
- package/dist/default-model.js +5 -4
- package/dist/env-model-table.d.ts +163 -0
- package/dist/env-model-table.js +257 -0
- package/dist/execution-lane-index.d.ts +6 -0
- package/dist/execution-lane-index.js +8 -0
- package/dist/execution-lanes.d.ts +2 -1
- package/dist/execution-lanes.js +6 -4
- package/dist/fleet.d.ts +25 -7
- package/dist/fleet.js +24 -6
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/mcp-request-entry.d.ts +58 -6
- package/dist/mcp-request-entry.js +45 -13
- package/dist/mcp-server-name.d.ts +17 -0
- package/dist/mcp-server-name.js +19 -0
- package/dist/migration-receipt.d.ts +10 -6
- package/dist/migration-receipt.js +5 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +2 -0
- package/dist/one-time-migrations.d.ts +4 -2
- package/dist/one-time-migrations.js +29 -1
- package/dist/own-key-read.d.ts +1 -1
- package/dist/remote-exec.d.ts +1 -1
- package/dist/remote-exec.js +4 -2
- package/dist/retired-form.d.ts +19 -0
- package/dist/retired-form.js +24 -1
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/tier-words.d.ts +33 -0
- package/dist/tier-words.js +43 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +29 -5
- package/dist/worker-row-verdict.js +7 -1
- package/docs/API-SURFACE.md +68 -4
- package/docs/CONSUMER-INTEGRATION.md +72 -2
- package/llms-full.txt +8 -3
- package/llms.txt +9 -3
- package/package.json +2 -2
|
@@ -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
|
+
}
|
|
@@ -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;
|
package/dist/conformance/run.js
CHANGED
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
* 比对分三面,刻意不混:**effective**(剥掉元数据后结构化深相等)、**告警多重集**、**元数据**
|
|
12
12
|
* (`version`/`updatedAt` 由 harness 注入或由腿自己算,独立断言 —— 不让 canonical 把它盖过去)。
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import { tmpdir } from "node:os";
|
|
14
|
+
import { mkdir, readFile, realpath, stat, writeFile, utimes } from "node:fs/promises";
|
|
16
15
|
import { basename, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
17
16
|
import { isJsonArray, isPlainJsonObject, jsonShapeOf } from "../plain-json-object.js";
|
|
18
17
|
import { canonicalize, deepEqual, diffJson, stableKey } from "./canonical.js";
|
|
@@ -20,6 +19,7 @@ import { diffWarningMultiset, findSecretLeaks, projectWarning } from "./warnings
|
|
|
20
19
|
import { deriveLocalExpectation, deriveLocalInput, deriveWireExpectation, deriveWireInput, toManifestDoc } from "./derive.js";
|
|
21
20
|
import { kitIdentity, loadCorpus } from "./corpus.js";
|
|
22
21
|
import { contractOwnedEntry, contractOwnedEntryWhy } from "./entry-origin.js";
|
|
22
|
+
import { mintLocalRoot, removeLocalRoot } from "./temp-roots.js";
|
|
23
23
|
import { LEDGER_REF_RULE, LEG_DECLINE_REASONS, LEG_NAMES, } from "./types.js";
|
|
24
24
|
/** 一个**模块**文件的样子 —— 来源律用它挡住「随便找个存在的文件当入口」(`package.json` 也是文件)。 */
|
|
25
25
|
const MODULE_EXTENSIONS = [".ts", ".mts", ".cts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"];
|
|
@@ -28,16 +28,20 @@ const MODULE_EXTENSIONS = [".ts", ".mts", ".cts", ".tsx", ".js", ".mjs", ".cjs",
|
|
|
28
28
|
* mtime 怎么设、目录怎么建就会各不相同,而 `updatedAt` 正是按 mtime 算的)。
|
|
29
29
|
*
|
|
30
30
|
* 返回 `root` 与 `cleanup`;所有文件的 mtime 统一设成 `mtime`,于是 `updatedAt` 对同一份快照恒定。
|
|
31
|
+
*
|
|
32
|
+
* 临时根只在 `./temp-roots` 里铸、只在那里删:进程在 `cleanup()` 之前死掉(`process.exit`、未捕获异常、信号、SIGKILL)时根留在 tmp 下,
|
|
33
|
+
* 根里的 `.alive` 超过 60 分钟没更新之后,由下一轮第一次铸根的清扫收走 —— 这里不碰临时目录的路径、前缀和清理,也不在进程上装任何监听。
|
|
31
34
|
*/
|
|
32
35
|
export async function materializeLocalRoot(files, mtime) {
|
|
33
36
|
// 闸**先于**任何 fs 动作:一个坏名字不许留下半个临时根(「建了再清」比这多一条「清到一半又抛」的路)。
|
|
34
37
|
for (const name of Object.keys(files))
|
|
35
38
|
assertBareFileName(name);
|
|
36
|
-
const root = await
|
|
39
|
+
const root = await mintLocalRoot();
|
|
37
40
|
const dir = join(root, "config.d");
|
|
38
|
-
await mkdir(dir, { recursive: true });
|
|
39
41
|
const when = new Date(mtime);
|
|
40
42
|
try {
|
|
43
|
+
// 不递归:根是刚铸出来的,只差这一层;根若已不在,这里响着失败(失败即清根),不顺手把根重建成一个不是 `mintLocalRoot` 铸出来的目录
|
|
44
|
+
await mkdir(dir);
|
|
41
45
|
for (const [name, text] of Object.entries(files)) {
|
|
42
46
|
const p = join(dir, name);
|
|
43
47
|
// 上面那条单段律要保证的**就是**这条包含律。它是承重面上的断言,不是可跳过的装饰:真要有一天
|
|
@@ -50,10 +54,10 @@ export async function materializeLocalRoot(files, mtime) {
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
catch (e) {
|
|
53
|
-
await
|
|
57
|
+
await removeLocalRoot(root);
|
|
54
58
|
throw e;
|
|
55
59
|
}
|
|
56
|
-
return { root, cleanup: () =>
|
|
60
|
+
return { root, cleanup: () => removeLocalRoot(root) };
|
|
57
61
|
}
|
|
58
62
|
/**
|
|
59
63
|
* 语料落盘的**文件名闸**。语料是仓外贡献面(每条 case 的 `input.json` 里的 `localFiles` 键,以及
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** 临时根的目录名前缀;名字 = 前缀 + `mkdtemp` 的六位随机(清扫只认这一种形)。 */
|
|
2
|
+
export declare const LOCAL_ROOT_PREFIX = "settings-conformance-";
|
|
3
|
+
/** 根内的心跳文件名:静态为空的文件,活进程定时(同一个 inode 才写)写一个字节再截回空、mtime 由文件系统自己打;清扫只看它的 mtime 年龄。 */
|
|
4
|
+
export declare const LOCAL_ROOT_ALIVE_FILE = ".alive";
|
|
5
|
+
/**
|
|
6
|
+
* 旧件的年龄线:60 分钟 —— 根里 `.alive` 的 mtime 早于它,清扫才删这个根。一个 case 的一条本地腿只要几毫秒、整套语料也只要几秒,60 分钟远长于一次正常的跑,
|
|
7
|
+
* 所以心跳停了这么久的根绝大多数是上一轮被收走的残件;线又短到清扫能及时收走它们,不让它们一天天攒。它必须远大于 {@link ALIVE_TOUCH_INTERVAL_MS},事件循环照常转的活进程的 `.alive` 才不会老到这条线。
|
|
8
|
+
* 具名常量,不是旋钮。
|
|
9
|
+
*/
|
|
10
|
+
export declare const STALE_LOCAL_ROOT_AGE_MS: number;
|
|
11
|
+
/** 心跳间隔:5 分钟 —— 活进程每隔这么久在根里的 `.alive` 上写一个字节再截回空(文件系统给它打新的 mtime)。它必须远小于 {@link STALE_LOCAL_ROOT_AGE_MS}(现为十二分之一),偶尔一拍慢了也不会让活进程的 `.alive` 老过年龄线。具名常量,不是旋钮。 */
|
|
12
|
+
export declare const ALIVE_TOUCH_INTERVAL_MS: number;
|
|
13
|
+
/**
|
|
14
|
+
* 清扫 `dir` 下的旧临时根:名字恰是「前缀 + 六位字母数字」、`lstat` 是目录(符号链接与文件一概不动)、属主是当前用户(有 `getuid` 的平台)、
|
|
15
|
+
* 根内有 `.alive` 且 `lstat` 是常规文件(目录、符号链接都不算)、`nowMs - <.alive 的 mtime>` 超过 `maxAgeMs` ⇒ 整根删。没有 `.alive` 的一概不动。
|
|
16
|
+
* 不问进程号、不问主机名 —— 活性证据只有 `.alive` 的 mtime 年龄。返回被删的目录名。读不了 `dir` / 删不掉某一个 ⇒ 跳过,不抛。
|
|
17
|
+
*/
|
|
18
|
+
export declare function sweepStaleLocalRoots(opts: {
|
|
19
|
+
readonly dir: string;
|
|
20
|
+
readonly nowMs: number;
|
|
21
|
+
readonly maxAgeMs?: number;
|
|
22
|
+
}): Promise<string[]>;
|
|
23
|
+
/**
|
|
24
|
+
* 铸一个临时根:根内建空文件 `.alive`、记下它的身份就关(本模块按路径建它只此一处)、起它的心跳定时器;本进程第一次铸根时先清扫旧件(见文件头 ①)。
|
|
25
|
+
* 铸出这个根之前任何一步失败(建 `.alive`、起定时器),都回滚到什么也没留下 —— 停定时器、删根,再把原错抛出去。返回根的绝对路径。
|
|
26
|
+
*/
|
|
27
|
+
export declare function mintLocalRoot(): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* 删一个临时根:先停它的心跳定时器,再只删这一个(删不掉就抛,留下的根等 `.alive` 超过 60 分钟没更新之后的旧件清扫收)。不开不关任何文件;还在飞的一拍不等它。
|
|
30
|
+
*/
|
|
31
|
+
export declare function removeLocalRoot(root: string): Promise<void>;
|